@tycoworks/tycoslide 0.11.3 → 0.12.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/SKILL.md CHANGED
@@ -40,7 +40,7 @@ Before writing anything, read `manifest.json`. It contains:
40
40
  - **assets** -- brand logos, client logos, illustrations, and icons (`description`)
41
41
 
42
42
  A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
43
- - **parameters** -- one value on a frontmatter line. Types: `template`, `image`. Fill by putting a value under the parameter's key in the slide frontmatter.
43
+ - **parameters** -- one value on a frontmatter line. Fill by putting a value under the parameter's key in the slide frontmatter.
44
44
  - **slots** -- a multi-line region in the body. Accept types: `text`, `table`, `image`. Fill as a `::key::` region, using the slot's `key` from the manifest; a fenced code block routes to a `text` slot, a fenced mermaid block to an `image` slot.
45
45
 
46
46
  A slot may accept more than one content type -- the manifest lists each slot's `accepts`, and the shape of the content you write selects which one.
@@ -89,7 +89,7 @@ A deck file has three parts:
89
89
 
90
90
  ### Slide frontmatter
91
91
 
92
- Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters** (template and image inputs).
92
+ Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters**.
93
93
 
94
94
  ```yaml
95
95
  ---
@@ -100,13 +100,13 @@ subtitle: This Quarter
100
100
  ```
101
101
 
102
102
  - `layout` is required and consumed by the compiler (not forwarded as content).
103
- - All other frontmatter keys fill parameters: `title` fills the `title` template parameter, `subtitle` fills the `subtitle` parameter, `hero` fills the `hero` image parameter, etc. A multi-line text shape surfaces as several keys (e.g. `name` + `jobTitle`); fill each as its own scalar line.
103
+ - All other frontmatter keys fill parameters: `title` fills the `title` parameter, `subtitle` fills the `subtitle` parameter, etc. A multi-line text shape surfaces as several keys (e.g. `name` + `jobTitle`); fill each as its own scalar line.
104
104
  - Slots (accepting `text`, `table`, `image`) are filled by body regions, not frontmatter -- see below.
105
105
  - A slide may also carry a `notes:` block in frontmatter -- plain-text speaker notes for the slide's notes page (see [syntax.md](syntax.md#speaker-notes)). It is slide-level metadata, not a parameter or slot.
106
106
 
107
107
  ### Body content, slots, and formatting
108
108
 
109
- See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), the parameter/slot split (parameters: template, image; slots accept: text, table, image), and image parameters.
109
+ See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), and the parameter/slot split (slots accept: text, table, image).
110
110
 
111
111
  ### Build
112
112
 
@@ -143,7 +143,7 @@ Keep each slot's content to what its region comfortably holds. When content over
143
143
  - **Don't restyle the layout** -- the theme owns all design; you only fill slots
144
144
  - **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration. If you get a `shrunk to X%` warning, look at the rendered slide: if the image is now too small to make out, use a simpler one.
145
145
  - **Don't invent layout or asset names** -- only use what exists in the manifest
146
- - **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image parameter you care about
146
+ - **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image slot you care about
147
147
 
148
148
  ---
149
149
 
@@ -159,7 +159,7 @@ Build the deck again ([Build](#build)) and read the output carefully. Common err
159
159
  |-------|-----|
160
160
  | `unknown layout "xyz"` | Check layout names in `manifest.json` |
161
161
  | A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
162
- | An image didn't swap / placeholder remains | Use the image parameter's key name in frontmatter, and an asset path that exists in `manifest.json` |
162
+ | An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `![]($category.name)` from `manifest.json` |
163
163
  | YAML parse error | Fix the YAML syntax in the slide's frontmatter |
164
164
  | `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
165
165
  | `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
package/dist/index.d.ts CHANGED
@@ -36,4 +36,4 @@ export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot,
36
36
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
37
37
  export { generateManifest } from "./manifest.js";
38
38
  export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
39
- export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
39
+ export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { generate, SlotType, } from "./engine/index.js";
2
- import { AcceptType, ParameterType, } from "./markdown/types.js";
2
+ import { AcceptType, } from "./markdown/types.js";
3
3
  /**
4
4
  * A frontmatter parameter always fills one physical shape on the layout's own
5
5
  * slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
@@ -7,24 +7,15 @@ import { AcceptType, ParameterType, } from "./markdown/types.js";
7
7
  * placeholder; only body slots with a transplant block carry a real frame.
8
8
  */
9
9
  const NO_FRAME = { x: 0, y: 0, cx: 0, cy: 0 };
10
+ // A text shape carries no top-level key — its template placeholders are the keys.
11
+ // The compiler emits its expanded content under shapeName, so the engine slot is
12
+ // keyed by shapeName too.
10
13
  function paramToEngineSlot(param, baseSlide) {
11
- switch (param.type) {
12
- case ParameterType.Template:
13
- // A text shape carries no top-level key — its template placeholders are the keys. The
14
- // compiler emits its expanded content under shapeName, so the engine slot
15
- // is keyed by shapeName too.
16
- return {
17
- key: param.shapeName,
18
- frame: NO_FRAME,
19
- accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
20
- };
21
- case ParameterType.Image:
22
- return {
23
- key: param.key,
24
- frame: NO_FRAME,
25
- accepts: [{ type: SlotType.Image, sourceSlide: baseSlide, shapeName: param.shapeName }],
26
- };
27
- }
14
+ return {
15
+ key: param.shapeName,
16
+ frame: NO_FRAME,
17
+ accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
18
+ };
28
19
  }
29
20
  /**
30
21
  * Project a body slot's real `accepts` to engine `Block[]` and pass its `frame`
@@ -129,4 +120,4 @@ export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from
129
120
  // Authoring
130
121
  export { generateManifest } from "./manifest.js";
131
122
  // Markdown / Compiler
132
- export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
123
+ export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
package/dist/manifest.js CHANGED
@@ -1,27 +1,16 @@
1
1
  import { templateKeys } from "./markdown/textTemplate.js";
2
- import { ParameterType } from "./markdown/types.js";
3
2
  /**
4
3
  * Flatten a compiler parameter to the manifest entries advertised to AI authors.
5
- * A template parameter has no top-level key — its template's keys are the keys, so it
6
- * flattens to one entry per key (shapeName/template stay manifest-internal). An
7
- * image parameter is a single key.
4
+ * A parameter has no top-level key — its template's placeholders are the keys, so
5
+ * it flattens to one entry per key (shapeName/template stay manifest-internal).
8
6
  */
9
7
  function stripParameter(param) {
10
- switch (param.type) {
11
- case ParameterType.Template:
12
- return templateKeys(param.template).map((key) => {
13
- const result = { key, type: param.type };
14
- if (param.required)
15
- result.required = true;
16
- return result;
17
- });
18
- case ParameterType.Image: {
19
- const result = { key: param.key, type: param.type };
20
- if (param.required)
21
- result.required = true;
22
- return [result];
23
- }
24
- }
8
+ return templateKeys(param.template).map((key) => {
9
+ const result = { key };
10
+ if (param.required)
11
+ result.required = true;
12
+ return result;
13
+ });
25
14
  }
26
15
  function stripSlot(slot) {
27
16
  const result = { key: slot.key, accepts: slot.accepts.map((b) => b.type) };
@@ -53,10 +42,6 @@ export function generateManifest(config) {
53
42
  assets[category][name] = manifestEntry;
54
43
  }
55
44
  }
56
- const manifest = {
57
- version: 1,
58
- layouts,
59
- assets,
60
- };
45
+ const manifest = { layouts, assets };
61
46
  return JSON.stringify(manifest, null, 2);
62
47
  }
@@ -13,12 +13,11 @@ export declare function toImageFill(path: string, type: AssetType): ImageFill;
13
13
  * code (Shiki) and rendered mermaid (PNG) — so the returned deck is
14
14
  * engine-shaped, ready for `buildDeck`.
15
15
  *
16
- * `config.rootDir` is the base directory for resolving relative image paths
17
- * declared in the deck's frontmatter or named slots. When empty, image paths are
18
- * returned unchanged — callers that already produce absolute paths (or don't need
19
- * resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
20
- * are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
21
- * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` feed
22
- * the code and mermaid compiles.
16
+ * `config.rootDir` is the base directory for resolving the asset catalog's
17
+ * relative image paths. When empty, image paths are returned unchanged — callers
18
+ * that already produce absolute paths (or don't need resolution, e.g. unit tests)
19
+ * rely on the pass-through. When set, relative paths are resolved to absolute via
20
+ * `path.resolve(rootDir, path)`; absolute paths pass through. `config.codeTheme` /
21
+ * `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
23
22
  */
24
23
  export declare function compileDeck(doc: ParsedDocument, config: CompilerConfig): Promise<CompilerDeck>;
@@ -3,7 +3,7 @@ import { ImageFit, SlotType } from "../engine/index.js";
3
3
  import { parseSlotContent } from "./blocks/registry.js";
4
4
  import { validateSlideFrontmatter } from "./schema/deckSchema.js";
5
5
  import { templateKeys, templateToSegments } from "./textTemplate.js";
6
- import { AssetType, ParameterType, RESERVED_KEY, } from "./types.js";
6
+ import { AssetType, RESERVED_KEY, } from "./types.js";
7
7
  /** Map each semantic asset type to the engine's object-fit directive. */
8
8
  const FIT_FOR = {
9
9
  [AssetType.Icon]: ImageFit.ScaleDown,
@@ -37,7 +37,7 @@ function assertSlotRegion(slot, got, layoutName, slideNo, source) {
37
37
  }
38
38
  }
39
39
  /**
40
- * Resolve a user-supplied image path against the deck's root directory.
40
+ * Resolve a catalog image path against the deck's root directory.
41
41
  * When `rootDir` is empty, the path is returned unchanged so callers that
42
42
  * already produce absolute paths (or callers that don't care about
43
43
  * resolution) can opt out. Absolute paths always pass through.
@@ -73,12 +73,11 @@ function assertUniqueSlideNumbers(layouts) {
73
73
  * be collision-free — otherwise a fill silently clobbers or throws a misleading
74
74
  * error:
75
75
  *
76
- * - **Author keys** — what a frontmatter line addresses: every template-parameter key
77
- * and every image-parameter key must be distinct, so a line routes to exactly
78
- * one parameter.
79
- * - **Content keys** what `step.content` is addressed by: every image key, every
80
- * template parameter's `shapeName`, and every slot key must be distinct, so no two
81
- * overwrite each other in the content map.
76
+ * - **Author keys** — what a frontmatter line addresses: every parameter's placeholder
77
+ * keys must be distinct across the layout, so a line routes to exactly one parameter.
78
+ * - **Content keys** — what `step.content` is addressed by: every parameter's
79
+ * `shapeName` and every slot key must be distinct, so no two overwrite each other in
80
+ * the content map.
82
81
  *
83
82
  * Also rejects a required template parameter whose template has no keys — it declares
84
83
  * no way to be filled, so `required` on it is unsatisfiable.
@@ -95,34 +94,25 @@ function validateLayout(layout) {
95
94
  const claimContentKey = (key, owner) => {
96
95
  if (contentKeys.has(key)) {
97
96
  throw new Error(`Layout "${layout.name}": name "${key}" (${owner}) collides with another parameter or slot; ` +
98
- "each template parameter's shape, image key, and slot key must be distinct.");
97
+ "each parameter's shape and each slot key must be distinct.");
99
98
  }
100
99
  contentKeys.add(key);
101
100
  };
102
101
  for (const param of layout.parameters) {
103
- switch (param.type) {
104
- case ParameterType.Image:
105
- claimAuthorKey(param.key, "image parameter");
106
- claimContentKey(param.key, "image parameter");
107
- break;
108
- case ParameterType.Template: {
109
- const keys = templateKeys(param.template);
110
- if (param.required && keys.length === 0) {
111
- throw new Error(`Layout "${layout.name}": template parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
112
- }
113
- for (const key of keys)
114
- claimAuthorKey(key, `template parameter "${param.shapeName}"`);
115
- claimContentKey(param.shapeName, "template parameter");
116
- break;
117
- }
102
+ const keys = templateKeys(param.template);
103
+ if (param.required && keys.length === 0) {
104
+ throw new Error(`Layout "${layout.name}": parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
118
105
  }
106
+ for (const key of keys)
107
+ claimAuthorKey(key, `parameter "${param.shapeName}"`);
108
+ claimContentKey(param.shapeName, "parameter");
119
109
  }
120
110
  for (const slot of layout.slots) {
121
111
  claimContentKey(slot.key, "slot");
122
112
  }
123
113
  }
124
- async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
125
- const { layouts, rootDir } = config;
114
+ async function compileStep(slide, config, resolveAssetRef) {
115
+ const { layouts } = config;
126
116
  const { frontmatter, slots, index } = slide;
127
117
  // Slide numbers in errors are 1-based, matching how an author counts slides in
128
118
  // the deck file. Derive it once: every message below, and the parse-time errors
@@ -148,44 +138,22 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
148
138
  // layout/notes stripped first). The per-layout strict schema IS the unknown-key
149
139
  // check — it fires before the resolution loop, so that loop only sees valid keys.
150
140
  validateSlideFrontmatter(frontmatter, layoutDef, slideNo);
151
- // Map each author-facing key to its owning parameter: template keys the template
152
- // parameter that declares them, image keys the image parameter. validateLayout
153
- // (run once per layout in compileDeck) has already proven these key spaces are
141
+ // Map each author-facing key to the parameter that declares it. validateLayout
142
+ // (run once per layout in compileDeck) has already proven these keys are
154
143
  // collision-free, so a later lookup is unambiguous.
155
- const templateParams = [];
156
- const imageByKey = new Map();
157
144
  const templateParamByKey = new Map();
158
145
  for (const param of layoutDef.parameters) {
159
- switch (param.type) {
160
- case ParameterType.Image:
161
- imageByKey.set(param.key, param);
162
- break;
163
- case ParameterType.Template:
164
- templateParams.push(param);
165
- for (const key of templateKeys(param.template))
166
- templateParamByKey.set(key, param);
167
- break;
168
- }
146
+ for (const key of templateKeys(param.template))
147
+ templateParamByKey.set(key, param);
169
148
  }
170
149
  const slotsByKey = new Map(layoutDef.slots.map((s) => [s.key, s]));
171
150
  const content = {};
172
- // Frontmatter lines fill image parameters (by key) or template-parameter keys
173
- // (gathered per parameter, expanded together once every line is read).
151
+ // Frontmatter lines fill template-parameter keys, gathered per parameter and
152
+ // expanded together once every line is read.
174
153
  const valuesByTemplateParam = new Map();
175
154
  for (const [key, value] of Object.entries(frontmatter)) {
176
155
  if (key === RESERVED_KEY.LAYOUT || key === RESERVED_KEY.NOTES)
177
156
  continue;
178
- const image = imageByKey.get(key);
179
- if (image) {
180
- const imgPath = resolveImagePath(rootDir, String(value));
181
- const assetType = assetTypeByPath.get(imgPath);
182
- if (assetType === undefined) {
183
- throw new Error(`Slide image "${image.key}": "${value}" has no asset-catalog entry, so no type. ` +
184
- `Add it to the theme's assets with a type (icon | image | background).`);
185
- }
186
- content[image.key] = toImageFill(imgPath, assetType);
187
- continue;
188
- }
189
157
  const templateParam = templateParamByKey.get(key);
190
158
  if (templateParam) {
191
159
  let bucket = valuesByTemplateParam.get(templateParam);
@@ -196,17 +164,17 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
196
164
  bucket.set(key, String(value));
197
165
  }
198
166
  // Unreachable: validateSlideFrontmatter (above) already rejected any key that is
199
- // neither an image key nor a template key, so every key here routes to a parameter.
167
+ // not a reserved key, so every key here routes to a parameter.
200
168
  }
201
169
  // Expand each template parameter whose keys were supplied. Filling any key fills the
202
170
  // parameter as a whole — a missing key throws (fail-fast in templateToSegments). A
203
171
  // parameter with no supplied keys stays designer-sample unless required. Content
204
172
  // is keyed by shapeName: one parameter → one entry, regardless of key count.
205
- for (const templateParam of templateParams) {
173
+ for (const templateParam of layoutDef.parameters) {
206
174
  const supplied = valuesByTemplateParam.get(templateParam);
207
175
  if (!supplied) {
208
176
  if (templateParam.required) {
209
- throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires template parameter "${templateParam.shapeName}" ` +
177
+ throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${templateParam.shapeName}" ` +
210
178
  `(keys: ${templateKeys(templateParam.template).join(", ")}); none provided`);
211
179
  }
212
180
  continue;
@@ -231,17 +199,11 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
231
199
  config,
232
200
  layoutVariant: layoutDef.variant,
233
201
  });
234
- assertSlotRegion(slot, parsed.acceptType, layoutName, index, source);
202
+ assertSlotRegion(slot, parsed.acceptType, layoutName, slideNo, source);
235
203
  content[name] = await parsed.fill();
236
204
  }
237
- // Required image parameters (missing frontmatter key) and required slots
238
- // (missing region) throw with layout + key context. Required template parameters are
239
- // enforced during expansion above.
240
- for (const image of imageByKey.values()) {
241
- if (image.required && content[image.key] === undefined) {
242
- throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
243
- }
244
- }
205
+ // A required slot with no region throws with layout + key context. Required
206
+ // parameters are enforced during template expansion above.
245
207
  for (const slot of layoutDef.slots) {
246
208
  if (slot.required && content[slot.key] === undefined) {
247
209
  throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
@@ -260,13 +222,12 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
260
222
  * code (Shiki) and rendered mermaid (PNG) — so the returned deck is
261
223
  * engine-shaped, ready for `buildDeck`.
262
224
  *
263
- * `config.rootDir` is the base directory for resolving relative image paths
264
- * declared in the deck's frontmatter or named slots. When empty, image paths are
265
- * returned unchanged — callers that already produce absolute paths (or don't need
266
- * resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
267
- * are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
268
- * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` feed
269
- * the code and mermaid compiles.
225
+ * `config.rootDir` is the base directory for resolving the asset catalog's
226
+ * relative image paths. When empty, image paths are returned unchanged — callers
227
+ * that already produce absolute paths (or don't need resolution, e.g. unit tests)
228
+ * rely on the pass-through. When set, relative paths are resolved to absolute via
229
+ * `path.resolve(rootDir, path)`; absolute paths pass through. `config.codeTheme` /
230
+ * `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
270
231
  */
271
232
  export async function compileDeck(doc, config) {
272
233
  const { layouts, rootDir, assets } = config;
@@ -284,18 +245,10 @@ export async function compileDeck(doc, config) {
284
245
  assertUniqueSlideNumbers(layouts);
285
246
  for (const layout of layouts)
286
247
  validateLayout(layout);
287
- // Index each catalog asset's resolved path its declared type, so an image
288
- // filled by path inherits the scaling tolerance intrinsic to its pixels.
289
- const assetTypeByPath = new Map();
290
- for (const group of Object.values(assets)) {
291
- for (const entry of Object.values(group)) {
292
- assetTypeByPath.set(resolveImagePath(rootDir, entry.path), entry.type);
293
- }
294
- }
295
- // Resolve a body/`::name::` `$category.name` reference against the theme's
296
- // curated asset catalog. Anchored ⇒ the whole ref is the reference or it is
297
- // nothing; a found entry wraps through the same path→ImageFit mapping as a
298
- // frontmatter image (`toImageFill`), so a body image has no second fit story.
248
+ // Resolve a `$category.name` reference against the theme's curated asset
249
+ // catalog. Anchored the whole ref is the reference or it is nothing; a found
250
+ // entry wraps through `toImageFill`, which maps the asset's declared type to
251
+ // its ImageFit.
299
252
  const resolveAssetRef = (ref) => {
300
253
  const match = ASSET_REF_RE.exec(ref);
301
254
  if (!match) {
@@ -304,9 +257,17 @@ export async function compileDeck(doc, config) {
304
257
  const [, category, name] = match;
305
258
  const entry = assets[category]?.[name];
306
259
  if (!entry) {
307
- const available = Object.entries(assets)
308
- .flatMap(([cat, group]) => Object.keys(group).map((n) => `$${cat}.${n}`))
309
- .join(", ");
260
+ // Every asset in the catalog is far too many to read (mz-slides has 126).
261
+ // A known category narrows it to that category's names, which is what the
262
+ // author is choosing between; an unknown one lists the categories instead.
263
+ const group = assets[category];
264
+ const available = group
265
+ ? Object.keys(group)
266
+ .map((n) => `$${category}.${n}`)
267
+ .join(", ")
268
+ : Object.keys(assets)
269
+ .map((c) => `$${c}.*`)
270
+ .join(", ");
310
271
  throw new Error(`Unknown asset reference "${ref}". Available: ${available}`);
311
272
  }
312
273
  return toImageFill(resolveImagePath(rootDir, entry.path), entry.type);
@@ -315,7 +276,7 @@ export async function compileDeck(doc, config) {
315
276
  // bad asset ref, accept-type mismatch) fire before its own content is rendered.
316
277
  const steps = [];
317
278
  for (const slide of doc.slides) {
318
- steps.push(await compileStep(slide, config, assetTypeByPath, resolveAssetRef));
279
+ steps.push(await compileStep(slide, config, resolveAssetRef));
319
280
  }
320
281
  return { theme: String(theme), steps };
321
282
  }
@@ -9,5 +9,5 @@ export { parseRegion } from "./mdast.js";
9
9
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
10
10
  export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
11
  export { parseSlideDocument } from "./slideParser.js";
12
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerParameter, CompilerSlot, CompilerTemplateParameter, CompilerThemeConfig, EngineFill, } from "./types.js";
13
- export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
12
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, } from "./types.js";
13
+ export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
@@ -11,4 +11,4 @@ export { parseInlineRuns } from "./inline.js";
11
11
  export { parseRegion } from "./mdast.js";
12
12
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
13
13
  export { parseSlideDocument } from "./slideParser.js";
14
- export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
14
+ export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
@@ -1,22 +1,17 @@
1
1
  import * as z from "zod";
2
2
  import type { CompilerLayout } from "../types.js";
3
3
  /**
4
- * Per-layout frontmatter validation for a deck `.md`. The old tycoslide validated
5
- * every slide's frontmatter against a Zod schema carried by the layout program;
6
- * current layouts are JSON DATA, so the schema is BUILT dynamically from a layout's
7
- * declared `parameters` using the same key-derivation the compiler already uses
8
- * (`deckCompiler.ts`): an image parameter contributes its `key`; a template
9
- * parameter contributes one field per `{key}` placeholder in its template (NOT its
10
- * `shapeName`, which addresses `step.content`, not frontmatter).
4
+ * Per-layout frontmatter validation for a deck `.md`. Layouts are JSON data, so
5
+ * the schema is built dynamically from a layout's `parameters` using the same
6
+ * key-derivation the compiler uses (`deckCompiler.ts`): a parameter contributes
7
+ * one field per `{key}` placeholder in its template, never its `shapeName`,
8
+ * which addresses `step.content` rather than frontmatter.
11
9
  *
12
- * Commit-1 scope is unknown-key detection ONLY zero behavior change:
13
- * - Every field is `z.coerce.string()` (values are `String()`-coerced today, so a
14
- * YAML number like `year: 2026` must still pass plain `z.string()` would regress)
15
- * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
16
- * "all a template's keys present"). Value-typing and required-encoding are deliberate
17
- * later commits.
18
- * The strict object IS the unknown-key check: a stray frontmatter key throws
19
- * instead of being silently ignored.
10
+ * Every field is `z.coerce.string()` so a YAML number like `year: 2026` passes
11
+ * (values are `String()`-coerced downstream), and `.optional()` because
12
+ * `required` is per-parameter and enforced in `compileStep`, not "all a
13
+ * template's keys present". The strict object is the point: a stray frontmatter
14
+ * key throws instead of being silently ignored.
20
15
  */
21
16
  export declare function deckFrontmatterSchema(layout: CompilerLayout): z.ZodObject<{
22
17
  [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
@@ -1,37 +1,25 @@
1
1
  import * as z from "zod";
2
2
  import { templateKeys } from "../textTemplate.js";
3
- import { ParameterType, RESERVED_KEY } from "../types.js";
3
+ import { RESERVED_KEY } from "../types.js";
4
4
  import { strict } from "./strict.js";
5
5
  /**
6
- * Per-layout frontmatter validation for a deck `.md`. The old tycoslide validated
7
- * every slide's frontmatter against a Zod schema carried by the layout program;
8
- * current layouts are JSON DATA, so the schema is BUILT dynamically from a layout's
9
- * declared `parameters` using the same key-derivation the compiler already uses
10
- * (`deckCompiler.ts`): an image parameter contributes its `key`; a template
11
- * parameter contributes one field per `{key}` placeholder in its template (NOT its
12
- * `shapeName`, which addresses `step.content`, not frontmatter).
6
+ * Per-layout frontmatter validation for a deck `.md`. Layouts are JSON data, so
7
+ * the schema is built dynamically from a layout's `parameters` using the same
8
+ * key-derivation the compiler uses (`deckCompiler.ts`): a parameter contributes
9
+ * one field per `{key}` placeholder in its template, never its `shapeName`,
10
+ * which addresses `step.content` rather than frontmatter.
13
11
  *
14
- * Commit-1 scope is unknown-key detection ONLY zero behavior change:
15
- * - Every field is `z.coerce.string()` (values are `String()`-coerced today, so a
16
- * YAML number like `year: 2026` must still pass plain `z.string()` would regress)
17
- * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
18
- * "all a template's keys present"). Value-typing and required-encoding are deliberate
19
- * later commits.
20
- * The strict object IS the unknown-key check: a stray frontmatter key throws
21
- * instead of being silently ignored.
12
+ * Every field is `z.coerce.string()` so a YAML number like `year: 2026` passes
13
+ * (values are `String()`-coerced downstream), and `.optional()` because
14
+ * `required` is per-parameter and enforced in `compileStep`, not "all a
15
+ * template's keys present". The strict object is the point: a stray frontmatter
16
+ * key throws instead of being silently ignored.
22
17
  */
23
18
  export function deckFrontmatterSchema(layout) {
24
19
  const shape = {};
25
20
  for (const param of layout.parameters) {
26
- switch (param.type) {
27
- case ParameterType.Image:
28
- shape[param.key] = z.coerce.string().optional();
29
- break;
30
- case ParameterType.Template:
31
- for (const key of templateKeys(param.template)) {
32
- shape[key] = z.coerce.string().optional();
33
- }
34
- break;
21
+ for (const key of templateKeys(param.template)) {
22
+ shape[key] = z.coerce.string().optional();
35
23
  }
36
24
  }
37
25
  return strict(shape);
@@ -9,17 +9,11 @@ export declare const ThemeConfigSchema: z.ZodObject<{
9
9
  light: "light";
10
10
  dark: "dark";
11
11
  }>>;
12
- parameters: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
12
+ parameters: z.ZodArray<z.ZodObject<{
13
13
  shapeName: z.ZodString;
14
14
  required: z.ZodOptional<z.ZodBoolean>;
15
- type: z.ZodLiteral<"template">;
16
15
  template: z.ZodString;
17
- }, z.core.$strict>, z.ZodObject<{
18
- shapeName: z.ZodString;
19
- required: z.ZodOptional<z.ZodBoolean>;
20
- key: z.ZodString;
21
- type: z.ZodLiteral<"image">;
22
- }, z.core.$strict>], "type">>;
16
+ }, z.core.$strict>>;
23
17
  slots: z.ZodArray<z.ZodObject<{
24
18
  key: z.ZodString;
25
19
  accepts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, dirname } from "node:path";
3
3
  import * as z from "zod";
4
- import { AcceptType, AssetType, ParameterType, Variant, } from "../types.js";
4
+ import { AcceptType, AssetType, Variant } from "../types.js";
5
5
  import { strict } from "./strict.js";
6
6
  /**
7
7
  * Fail-fast runtime validation for a `theme.json`. The hand-written types in
@@ -63,19 +63,11 @@ const ThemeFontSchema = strict({
63
63
  path: z.string(),
64
64
  weight: z.number().optional(),
65
65
  });
66
- const TemplateParamSchema = strict({
66
+ const ParameterSchema = strict({
67
67
  shapeName: z.string(),
68
68
  required: z.boolean().optional(),
69
- type: z.literal(ParameterType.Template),
70
69
  template: z.string(),
71
70
  });
72
- const ImageParamSchema = strict({
73
- shapeName: z.string(),
74
- required: z.boolean().optional(),
75
- key: z.string(),
76
- type: z.literal(ParameterType.Image),
77
- });
78
- const ParameterSchema = z.discriminatedUnion("type", [TemplateParamSchema, ImageParamSchema]);
79
71
  // One arm per accept type, discriminated by `type`, mirroring `CompilerBlock`:
80
72
  // `startAt` lives only on the text arm, and `bodyRows` only on the table arm (a
81
73
  // table block MUST declare its repeatable-row range; text/image arms have no
@@ -27,18 +27,6 @@ export type AssetEntry = {
27
27
  };
28
28
  /** Two-level catalog: `{ category: { name: AssetEntry } }`. */
29
29
  export type AssetCatalog = Record<string, Record<string, AssetEntry>>;
30
- /**
31
- * Discriminator for a layout's frontmatter *parameters* — inputs the author
32
- * writes as a single `key: value` line. Two kinds: `Template` (fills a styled
33
- * shape's runs via fillTemplate) and `Image` (a filesystem path filled via
34
- * fillImage). Both share their value with the engine's `SlotType`, since a
35
- * parameter maps straight to an engine slot with no resolution step.
36
- */
37
- export declare const ParameterType: {
38
- readonly Template: "template";
39
- readonly Image: "image";
40
- };
41
- export type ParameterType = (typeof ParameterType)[keyof typeof ParameterType];
42
30
  /**
43
31
  * The engine content types a slot may `accept`. A subset of the engine's
44
32
  * `SlotType`: `Template` is parameter-only, never a body block. There is no
@@ -143,14 +131,14 @@ export type CompilerDeck = {
143
131
  steps: CompilerDeckStep[];
144
132
  };
145
133
  /**
146
- * Template parameter: a styled text shape filled by expanding a `template` into the
134
+ * Parameter: a styled text shape filled by expanding a `template` into the
147
135
  * shape's paragraphs via fillTemplate. The template is one string with `{key}`
148
136
  * placeholders and newlines for line breaks (`"{lastname}, {firstname} -
149
137
  * {company}"`, or `"{name}\n{jobTitle}"`); a shape's fillable keys are the
150
138
  * placeholders in its template. The shape carries no top-level `key`; its
151
139
  * placeholders are the keys the author fills in frontmatter.
152
140
  */
153
- export type CompilerTemplateParameter = {
141
+ export type CompilerParameter = {
154
142
  shapeName: string;
155
143
  /**
156
144
  * Whether the parameter may be omitted from a slide. Optional (defaults to
@@ -159,29 +147,9 @@ export type CompilerTemplateParameter = {
159
147
  * from the slide.
160
148
  */
161
149
  required?: boolean;
162
- type: typeof ParameterType.Template;
163
150
  /** The shape's text as one template with `{key}` placeholders; newlines are line breaks. */
164
151
  template: string;
165
152
  };
166
- /** Image parameter: one frontmatter path filled by fillImage. Sizing/crop
167
- * behaviour comes from the resolved asset's `type`, not the slot. */
168
- export type CompilerImageParameter = {
169
- shapeName: string;
170
- /**
171
- * Whether the parameter may be omitted from a slide. Optional (defaults to
172
- * false): a required one with no value causes the compiler to throw with
173
- * layout + key names; an optional one left unfilled has its shape removed
174
- * from the slide.
175
- */
176
- required?: boolean;
177
- key: string;
178
- type: typeof ParameterType.Image;
179
- };
180
- /**
181
- * Compiler-facing parameter. A layout's frontmatter inputs — each written as a
182
- * single `key: value` line, resolved against the layout's `parameters` list.
183
- */
184
- export type CompilerParameter = CompilerTemplateParameter | CompilerImageParameter;
185
153
  /**
186
154
  * Reserved keys in a deck's frontmatter — global (theme) and per-slide
187
155
  * (layout). Exported so callers (e.g. cli.ts) reference the constants
@@ -10,18 +10,6 @@ export const AssetType = {
10
10
  Image: "image",
11
11
  Background: "background",
12
12
  };
13
- // ── ParameterType discriminator (frontmatter, one value) ──────────────────────
14
- /**
15
- * Discriminator for a layout's frontmatter *parameters* — inputs the author
16
- * writes as a single `key: value` line. Two kinds: `Template` (fills a styled
17
- * shape's runs via fillTemplate) and `Image` (a filesystem path filled via
18
- * fillImage). Both share their value with the engine's `SlotType`, since a
19
- * parameter maps straight to an engine slot with no resolution step.
20
- */
21
- export const ParameterType = {
22
- Template: SlotType.Template,
23
- Image: SlotType.Image,
24
- };
25
13
  // ── AcceptType discriminator (what a slot accepts) ────────────────────────────
26
14
  /**
27
15
  * The engine content types a slot may `accept`. A subset of the engine's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Create editable, on-brand PowerPoint slides from markdown.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,8 +16,8 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "tsc --build",
19
- "test": "tsc --build && node --test --experimental-transform-types test/**/*.test.ts",
20
- "typecheck": "tsc --noEmit",
19
+ "test": "tsc --build && tsc -p tsconfig.test.json && node --test --experimental-transform-types test/**/*.test.ts",
20
+ "typecheck": "tsc --build && tsc -p tsconfig.test.json",
21
21
  "lint": "biome check .",
22
22
  "lint:fix": "biome check --write .",
23
23
  "format": "biome format --write ."
package/syntax.md CHANGED
@@ -118,11 +118,6 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
118
118
  jobTitle: CEO, Acme Corp
119
119
  ```
120
120
  The engine substitutes each value into the run that carries its style, so if the designer made the name bold and the job title grey, the filled name stays bold and the filled title stays grey.
121
- - **`image`** -- a picture placeholder. Set it in frontmatter with the image path (from an asset catalog entry, or an absolute path). How it is scaled and cropped is set by the **asset's `type`** in the catalog (`icon` never enlarges past native and never crops; `image` fits the whole picture without cropping; `background` fills the frame and center-crops).
122
- ```yaml
123
- hero: assets/diagrams/architecture.png
124
- ```
125
-
126
121
  ### Body content shapes
127
122
 
128
123
  Fill a slot by writing a `::key::` region in the body; the marker maps to the slot with that `key`. A slot's manifest entry lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
@@ -139,7 +134,13 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
139
134
  ````
140
135
  The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting, using the theme's `codeTheme` (set once in `theme.json`, not per slot). Colors are applied as native text runs in the output, not images.
141
136
  - **table** (slots that accept `table`) -- a GFM table. Write it in the slot region between `|`-delimited headers and rows; cells inherit inline formatting (bold, italic, links).
142
- - **image** (slots that accept `image`) -- a picture. A fenced `mermaid` block renders to a themed PNG and fills it (see below).
137
+ - **image** (slots that accept `image`) -- a picture from the theme's asset catalog, written as `![]($category.name)`:
138
+ ```markdown
139
+ ::logo::
140
+
141
+ ![]($brand.primaryDarkWordmark)
142
+ ```
143
+ The categories and names are listed in `manifest.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
143
144
 
144
145
  ---
145
146
 
@@ -200,20 +201,9 @@ The theme owns all styling. These directives are rejected at build time:
200
201
 
201
202
  ---
202
203
 
203
- ## Image parameters
204
-
205
- An image is a **parameter** -- one value (a path) on a frontmatter line. Reference it using the parameter key directly:
206
-
207
- ```yaml
208
- ---
209
- layout: ImageSlide
210
- title: Architecture Diagram
211
- hero: assets/diagrams/architecture.png
212
- ---
213
- ```
204
+ ## Layout declarations
214
205
 
215
206
  Each parameter or slot in the layout definition may declare:
216
- - **`type`** (parameters, required) -- `template` or `image`.
217
207
  - **`accepts`** (slots, required) -- an array of `text`, `table`, `image`.
218
208
  - **`required: true`** -- the slide has no usable default and the build fails if the parameter/slot has no value (e.g. team-member photos, icon-grid icons, the quote logo). If you don't have a suitable image, ask the user for one.
219
209
  - **optional (the default)** -- a parameter or slot you leave unfilled is dropped from the slide (its shape is removed), so a layout with numbered slots (e.g. up to six sections, up to four stats) renders only the ones you fill.
@@ -309,6 +299,9 @@ Infrastructure:
309
299
  ---
310
300
  layout: ImageSlide
311
301
  title: Office Map
312
- hero: assets/images/office-floor-plan.png
313
302
  ---
303
+
304
+ ::hero::
305
+
306
+ ![]($images.officeFloorPlan)
314
307
  ```