@tycoworks/tycoslide 0.7.0 → 0.8.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/dist/index.js CHANGED
@@ -42,6 +42,8 @@ export async function resolveDeck(deck, config) {
42
42
  const resolvedStep = { layout: step.layout };
43
43
  if (step.content)
44
44
  resolvedStep.content = narrowContent(step.content);
45
+ if (step.notes !== undefined)
46
+ resolvedStep.notes = step.notes;
45
47
  return resolvedStep;
46
48
  }),
47
49
  };
@@ -72,12 +74,8 @@ function toEngineSlot(slot) {
72
74
  result.startAt = slot.startAt;
73
75
  return result;
74
76
  }
75
- case CompilerSlotType.Table: {
76
- const result = { key: slot.key, shapeName: slot.shapeName, type: SlotType.Table };
77
- if (slot.columns !== undefined)
78
- result.columns = slot.columns;
79
- return result;
80
- }
77
+ case CompilerSlotType.Table:
78
+ return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Table };
81
79
  case CompilerSlotType.Code:
82
80
  // Highlighter resolves the code fence into StyledParagraph[]; engine
83
81
  // fills it via fillText.
@@ -135,12 +133,12 @@ export function toEngineConfig(config) {
135
133
  * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
136
134
  * post-write cleanup is needed.
137
135
  */
138
- export async function buildDeck(deck, config) {
136
+ export async function buildDeck(deck, config, options = {}) {
139
137
  const resolved = await resolveDeck(deck, config);
140
- await generate(resolved, toEngineConfig(config));
138
+ await generate(resolved, toEngineConfig(config), options);
141
139
  }
142
140
  // Engine — primitives-only public surface.
143
- export { FitMode, fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
141
+ export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
144
142
  export { generateManifest } from "./manifest.js";
145
143
  // Markdown / Compiler
146
144
  export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
package/dist/manifest.js CHANGED
@@ -21,7 +21,7 @@ function stripParameter(param) {
21
21
  return result;
22
22
  });
23
23
  case ParameterType.Image: {
24
- const result = { key: param.key, type: param.type, fit: param.fit };
24
+ const result = { key: param.key, type: param.type };
25
25
  if (param.required)
26
26
  result.required = true;
27
27
  if (param.limit)
@@ -38,11 +38,8 @@ function stripSlot(slot) {
38
38
  result.limit = slot.limit;
39
39
  switch (slot.type) {
40
40
  case CompilerSlotType.Text:
41
- // startAt is a fill hint, not a manifest surface concern.
42
- break;
43
41
  case CompilerSlotType.Table:
44
- if (slot.columns !== undefined)
45
- result.columns = slot.columns;
42
+ // No fields beyond the shared key/type/required/limit.
46
43
  break;
47
44
  case CompilerSlotType.Code:
48
45
  result.codeTheme = slot.codeTheme;
@@ -69,6 +66,7 @@ export function generateManifest(config, options) {
69
66
  for (const [name, entry] of Object.entries(entries)) {
70
67
  const manifestEntry = {
71
68
  path: entry.path,
69
+ type: entry.type,
72
70
  description: entry.description,
73
71
  };
74
72
  if (entry.whenToUse)
@@ -1,16 +1,7 @@
1
- import { FitMode, type ImageFill } from "../engine/index.js";
1
+ import { type ImageFill } from "../engine/index.js";
2
2
  import type { ParsedDocument } from "./slideParser.js";
3
- import { type CompilerDeck, type CompilerImageParameter, type CompilerLayout } from "./types.js";
4
- /**
5
- * Wrap a resolved image path as an ImageFill using the parameter's declared
6
- * fit. `param` is narrowed to `CompilerImageParameter` — only image parameters
7
- * carry a raw path. Mermaid slots never reach here (their content flows as
8
- * fences through MermaidResolver, which wraps the rendered PNG directly).
9
- *
10
- * `path` must be absolute. Callers are responsible for resolution (see
11
- * `resolve(rootDir, ...)` in the compiler / cli.ts).
12
- */
13
- export declare function toImageFill(param: CompilerImageParameter, path: string): ImageFill;
3
+ import { type AssetCatalog, AssetType, type CompilerDeck, type CompilerLayout } from "./types.js";
4
+ export declare function toImageFill(path: string, type: AssetType): ImageFill;
14
5
  /**
15
6
  * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
16
7
  * (layout, body). Exported so callers (e.g. cli.ts) reference the constants
@@ -21,6 +12,7 @@ export declare const RESERVED_KEY: {
21
12
  readonly BODY: "body";
22
13
  readonly OUTPUT: "output";
23
14
  readonly THEME: "theme";
15
+ readonly NOTES: "notes";
24
16
  };
25
17
  /**
26
18
  * Compile a parsed deck document against a set of layouts.
@@ -32,5 +24,4 @@ export declare const RESERVED_KEY: {
32
24
  * can rely on the pass-through. When provided, relative paths are resolved
33
25
  * to absolute via `path.resolve(rootDir, path)`; absolute paths pass through.
34
26
  */
35
- export declare function compileDeck(doc: ParsedDocument, layouts: CompilerLayout[], rootDir?: string): CompilerDeck;
36
- export { FitMode };
27
+ export declare function compileDeck(doc: ParsedDocument, layouts: CompilerLayout[], rootDir?: string, assets?: AssetCatalog): CompilerDeck;
@@ -1,20 +1,22 @@
1
1
  import { resolve } from "node:path";
2
- import { FILLERS, FitMode, SlotType } from "../engine/index.js";
2
+ import { FILLERS, ImageFit, SlotType } from "../engine/index.js";
3
3
  import { parseGfmTable, parseStyledParagraph } from "./parsers.js";
4
4
  import { RESOLVERS } from "./resolvers/resolver.js";
5
5
  import { templateKeys, templateToSegments } from "./textTemplate.js";
6
- import { CompilerSlotType, FenceType, ParameterType, } from "./types.js";
6
+ import { AssetType, CompilerSlotType, FenceType, ParameterType, } from "./types.js";
7
7
  /**
8
- * Wrap a resolved image path as an ImageFill using the parameter's declared
9
- * fit. `param` is narrowed to `CompilerImageParameter` only image parameters
10
- * carry a raw path. Mermaid slots never reach here (their content flows as
11
- * fences through MermaidResolver, which wraps the rendered PNG directly).
12
- *
13
- * `path` must be absolute. Callers are responsible for resolution (see
14
- * `resolve(rootDir, ...)` in the compiler / cli.ts).
8
+ * Wrap an absolute image path as an ImageFill, expanding the resolved asset
9
+ * `type` into the engine's scaling constraints. Callers resolve the path (see
10
+ * `resolveImagePath`) and the type (from the catalog) first.
15
11
  */
16
- export function toImageFill(param, path) {
17
- return { type: SlotType.Image, path, fit: param.fit };
12
+ /** Map each semantic asset type to the engine's object-fit directive. */
13
+ const FIT_FOR = {
14
+ [AssetType.Icon]: ImageFit.ScaleDown,
15
+ [AssetType.Image]: ImageFit.Contain,
16
+ [AssetType.Background]: ImageFit.Cover,
17
+ };
18
+ export function toImageFill(path, type) {
19
+ return { type: SlotType.Image, path, fit: FIT_FOR[type] };
18
20
  }
19
21
  /**
20
22
  * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
@@ -26,6 +28,7 @@ export const RESERVED_KEY = {
26
28
  BODY: "body",
27
29
  OUTPUT: "output",
28
30
  THEME: "theme",
31
+ NOTES: "notes",
29
32
  };
30
33
  const CODE_FENCE_RE = /^```(\w+)\n([\s\S]*?)```\s*$/;
31
34
  function toTextFill(text) {
@@ -140,12 +143,18 @@ function validateLayout(layout) {
140
143
  claimContentKey(slot.key, "slot");
141
144
  }
142
145
  }
143
- function compileStep(slide, layouts, rootDir) {
146
+ function compileStep(slide, layouts, rootDir, assetTypeByPath) {
144
147
  const { frontmatter, body, slots, index } = slide;
145
148
  const layout = frontmatter[RESERVED_KEY.LAYOUT];
146
149
  if (layout === undefined) {
147
150
  throw new Error(`Slide ${index}: missing required "${RESERVED_KEY.LAYOUT}" in frontmatter`);
148
151
  }
152
+ // Speaker notes are slide-level metadata, stripped from frontmatter before
153
+ // slot/param resolution — exactly like `layout`. Coerce to string if present.
154
+ // An empty `notes:` key parses as YAML null; treat it as absent (loose `==`)
155
+ // so it doesn't write the literal string "null".
156
+ const notesRaw = frontmatter[RESERVED_KEY.NOTES];
157
+ const notes = notesRaw == null ? undefined : String(notesRaw);
149
158
  const layoutName = String(layout);
150
159
  const layoutDef = layouts.find((l) => l.name === layoutName);
151
160
  if (!layoutDef) {
@@ -174,11 +183,17 @@ function compileStep(slide, layouts, rootDir) {
174
183
  // (gathered per parameter, expanded together once every line is read).
175
184
  const valuesByTemplateParam = new Map();
176
185
  for (const [key, value] of Object.entries(frontmatter)) {
177
- if (key === RESERVED_KEY.LAYOUT)
186
+ if (key === RESERVED_KEY.LAYOUT || key === RESERVED_KEY.NOTES)
178
187
  continue;
179
188
  const image = imageByKey.get(key);
180
189
  if (image) {
181
- content[image.key] = toImageFill(image, resolveImagePath(rootDir, String(value)));
190
+ const imgPath = resolveImagePath(rootDir, String(value));
191
+ const assetType = assetTypeByPath.get(imgPath);
192
+ if (assetType === undefined) {
193
+ throw new Error(`Slide image "${image.key}": "${value}" has no asset-catalog entry, so no type. ` +
194
+ `Add it to the theme's assets with a type (icon | image | background).`);
195
+ }
196
+ content[image.key] = toImageFill(imgPath, assetType);
182
197
  continue;
183
198
  }
184
199
  const templateParam = templateParamByKey.get(key);
@@ -248,7 +263,10 @@ function compileStep(slide, layouts, rootDir) {
248
263
  // CompilerDeckStep.content values are MarkdownBlock — CodeFence and
249
264
  // MermaidFence are legal in transit until the resolvers narrow them into
250
265
  // StyledParagraph[] / ImageFill before the engine sees the deck.
251
- return { layout: layoutName, content };
266
+ const step = { layout: layoutName, content };
267
+ if (notes !== undefined)
268
+ step.notes = notes;
269
+ return step;
252
270
  }
253
271
  const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
254
272
  /**
@@ -261,7 +279,7 @@ const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
261
279
  * can rely on the pass-through. When provided, relative paths are resolved
262
280
  * to absolute via `path.resolve(rootDir, path)`; absolute paths pass through.
263
281
  */
264
- export function compileDeck(doc, layouts, rootDir = "") {
282
+ export function compileDeck(doc, layouts, rootDir = "", assets = {}) {
265
283
  const theme = doc.global[RESERVED_KEY.THEME];
266
284
  if (theme === undefined) {
267
285
  throw new Error(`Missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
@@ -274,9 +292,17 @@ export function compileDeck(doc, layouts, rootDir = "") {
274
292
  // theme fails fast regardless of which layouts this deck's slides use.
275
293
  for (const layout of layouts)
276
294
  validateLayout(layout);
295
+ // Index each catalog asset's resolved path → its declared type, so an image
296
+ // filled by path inherits the scaling tolerance intrinsic to its pixels.
297
+ const assetTypeByPath = new Map();
298
+ for (const group of Object.values(assets)) {
299
+ for (const entry of Object.values(group)) {
300
+ assetTypeByPath.set(resolveImagePath(rootDir, entry.path), entry.type);
301
+ }
302
+ }
277
303
  const deck = {
278
304
  theme: String(theme),
279
- steps: doc.slides.map((slide) => compileStep(slide, layouts, rootDir)),
305
+ steps: doc.slides.map((slide) => compileStep(slide, layouts, rootDir, assetTypeByPath)),
280
306
  };
281
307
  const output = doc.global[RESERVED_KEY.OUTPUT];
282
308
  if (output !== undefined) {
@@ -284,6 +310,3 @@ export function compileDeck(doc, layouts, rootDir = "") {
284
310
  }
285
311
  return deck;
286
312
  }
287
- // Re-export FitMode so callers that build ImageFills by hand can import it
288
- // through the compiler surface without reaching into the engine.
289
- export { FitMode };
@@ -1,5 +1,5 @@
1
- import type { CompilerDeck, CompilerLayout } from "./types.js";
2
- export declare function compileMarkdownDeck(source: string, layouts: CompilerLayout[], rootDir?: string): CompilerDeck;
1
+ import type { AssetCatalog, CompilerDeck, CompilerLayout } from "./types.js";
2
+ export declare function compileMarkdownDeck(source: string, layouts: CompilerLayout[], rootDir?: string, assets?: AssetCatalog): CompilerDeck;
3
3
  export { compileDeck, RESERVED_KEY } from "./deckCompiler.js";
4
4
  export { parseGfmTable, parseInlineRuns, parseProseLine, parseStyledParagraph } from "./parsers.js";
5
5
  export { CodeResolver, highlightCode, isCodeBlock } from "./resolvers/code.js";
@@ -10,4 +10,4 @@ export { isFence, RESOLVERS, resolveFences } from "./resolvers/resolver.js";
10
10
  export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
11
  export { parseSlideDocument } from "./slideParser.js";
12
12
  export type { AssetCatalog, AssetEntry, CodeFence, CompilerCodeSlot, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerMermaidSlot, CompilerParameter, CompilerSlot, CompilerTableSlot, CompilerTemplateParameter, CompilerTextSlot, CompilerThemeConfig, MarkdownBlock, MermaidFence, ResolvedCompilerDeck, ResolvedCompilerDeckStep, } from "./types.js";
13
- export { CompilerSlotType, FenceType, ParameterType } from "./types.js";
13
+ export { AssetType, CompilerSlotType, FenceType, ParameterType } from "./types.js";
@@ -1,8 +1,8 @@
1
1
  import { compileDeck } from "./deckCompiler.js";
2
2
  import { parseSlideDocument } from "./slideParser.js";
3
- export function compileMarkdownDeck(source, layouts, rootDir = "") {
3
+ export function compileMarkdownDeck(source, layouts, rootDir = "", assets = {}) {
4
4
  const doc = parseSlideDocument(source);
5
- return compileDeck(doc, layouts, rootDir);
5
+ return compileDeck(doc, layouts, rootDir, assets);
6
6
  }
7
7
  export { compileDeck, RESERVED_KEY } from "./deckCompiler.js";
8
8
  export { parseGfmTable, parseInlineRuns, parseProseLine, parseStyledParagraph } from "./parsers.js";
@@ -10,4 +10,4 @@ export { CodeResolver, highlightCode, isCodeBlock } from "./resolvers/code.js";
10
10
  export { isMermaidBlock, MermaidResolver } from "./resolvers/mermaid.js";
11
11
  export { isFence, RESOLVERS, resolveFences } from "./resolvers/resolver.js";
12
12
  export { parseSlideDocument } from "./slideParser.js";
13
- export { CompilerSlotType, FenceType, ParameterType } from "./types.js";
13
+ export { AssetType, CompilerSlotType, FenceType, ParameterType } from "./types.js";
@@ -1,29 +1,14 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
3
  import { tmpdir } from "node:os";
5
4
  import { join, resolve } from "node:path";
6
- import { FitMode, SlotType } from "../../engine/index.js";
5
+ import { ImageFit, SlotType } from "../../engine/index.js";
7
6
  import { CompilerSlotType, FenceType } from "../types.js";
8
7
  import { buildMermaidRenderConfig, injectClassDefs, validateMermaidDefinition, } from "./mermaidTheme.js";
9
8
  /** Discriminator for MermaidFence values. Doubles as `MermaidResolver.matches`. */
10
9
  export function isMermaidBlock(v) {
11
10
  return (typeof v === "object" && v !== null && !Array.isArray(v) && v.type === FenceType.Mermaid);
12
11
  }
13
- function findMmdc() {
14
- try {
15
- const resolved = execFileSync("npx", ["--no-install", "which", "mmdc"], {
16
- encoding: "utf-8",
17
- stdio: ["pipe", "pipe", "pipe"],
18
- }).trim();
19
- if (resolved)
20
- return resolved;
21
- }
22
- catch {
23
- // fall through
24
- }
25
- throw new Error("mermaid-cli is required for mermaid diagrams. Install it: npm i -D @mermaid-js/mermaid-cli");
26
- }
27
12
  function hashKey(definition, variantName) {
28
13
  return createHash("sha256").update(variantName).update("\n").update(definition).digest("hex").slice(0, 16);
29
14
  }
@@ -32,7 +17,7 @@ function ensureCacheDir(config) {
32
17
  mkdirSync(base, { recursive: true });
33
18
  return base;
34
19
  }
35
- function renderOne(definition, variantName, variant, cacheDir, mmdcPath) {
20
+ async function renderOne(definition, variantName, variant, cacheDir) {
36
21
  const validated = validateMermaidDefinition(definition);
37
22
  const processed = injectClassDefs(validated, variant.accents, variant.accentOpacity, variant.accentTextColor, variant.surface, variant.groupCornerRadius);
38
23
  const key = hashKey(processed, variantName);
@@ -40,17 +25,25 @@ function renderOne(definition, variantName, variant, cacheDir, mmdcPath) {
40
25
  if (existsSync(outputPath))
41
26
  return outputPath;
42
27
  const config = buildMermaidRenderConfig(variant);
43
- const scratch = tmpdir();
44
- const configPath = join(scratch, `tycoslide-mermaid-config-${key}.json`);
45
- const inputPath = join(scratch, `tycoslide-mermaid-${key}.mmd`);
46
- writeFileSync(configPath, JSON.stringify(config));
28
+ const inputPath = join(tmpdir(), `tycoslide-mermaid-${key}.mmd`);
47
29
  writeFileSync(inputPath, processed);
30
+ // mermaid-cli's programmatic API (lazy-imported so puppeteer only loads when a
31
+ // deck actually renders mermaid). Mirrors the old CLI flags: --configFile →
32
+ // mermaidConfig, -b transparent → backgroundColor, -s 2 → deviceScaleFactor.
33
+ const { run } = await import("@mermaid-js/mermaid-cli");
48
34
  try {
49
- execFileSync(mmdcPath, ["-i", inputPath, "-o", outputPath, "--configFile", configPath, "-b", "transparent", "-s", "2"], { stdio: ["pipe", "pipe", "pipe"], timeout: 30_000 });
35
+ await run(inputPath, outputPath, {
36
+ quiet: true,
37
+ outputFormat: "png",
38
+ parseMMDOptions: {
39
+ mermaidConfig: config,
40
+ backgroundColor: "transparent",
41
+ viewport: { width: 800, height: 600, deviceScaleFactor: 2 },
42
+ },
43
+ });
50
44
  }
51
45
  catch (e) {
52
- const stderr = e.stderr?.toString() ?? e.message;
53
- throw new Error(`Mermaid render failed:\n${stderr}`);
46
+ throw new Error(`Mermaid render failed:\n${e?.message ?? e}`);
54
47
  }
55
48
  return outputPath;
56
49
  }
@@ -81,9 +74,8 @@ export const MermaidResolver = {
81
74
  throw new Error(`Slide layout "${ctx.layout.name}": mermaid variant "${variantName}" not found in theme. ` +
82
75
  `Available variants: ${Object.keys(config.mermaid).join(", ")}`);
83
76
  }
84
- const mmdcPath = findMmdc();
85
77
  const cacheDir = ensureCacheDir(config);
86
- const pngPath = renderOne(fence.definition, variantName, variant, cacheDir, mmdcPath);
87
- return { type: SlotType.Image, path: pngPath, fit: FitMode.Contain };
78
+ const pngPath = await renderOne(fence.definition, variantName, variant, cacheDir);
79
+ return { type: SlotType.Image, path: pngPath, fit: ImageFit.Contain };
88
80
  },
89
81
  };
@@ -1,5 +1,16 @@
1
- import { type FitMode, type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
1
+ import { type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
2
2
  import type { MermaidConfig } from "./resolvers/mermaidTheme.js";
3
+ /**
4
+ * An asset's scaling/cropping tolerance, declared in the theme catalog. The
5
+ * compiler maps it to the engine's object-fit `fit`. `icon`: never enlarge,
6
+ * never crop. `image`: never crop, may scale. `background`: crop and scale freely.
7
+ */
8
+ export declare const AssetType: {
9
+ readonly Icon: "icon";
10
+ readonly Image: "image";
11
+ readonly Background: "background";
12
+ };
13
+ export type AssetType = (typeof AssetType)[keyof typeof AssetType];
3
14
  /**
4
15
  * A theme's declaration of a reusable image asset. Purely compiler-facing —
5
16
  * the engine never sees this type; the compiler resolves an entry's `path`
@@ -8,6 +19,8 @@ import type { MermaidConfig } from "./resolvers/mermaidTheme.js";
8
19
  */
9
20
  export type AssetEntry = {
10
21
  path: string;
22
+ /** Required — a missing type is a fail-fast error. */
23
+ type: AssetType;
11
24
  description: string;
12
25
  whenToUse?: string;
13
26
  };
@@ -86,6 +99,8 @@ export type MarkdownBlock = TextFill | TableFill | CodeFence | MermaidFence | Im
86
99
  export type CompilerDeckStep = {
87
100
  layout: string;
88
101
  content?: Record<string, MarkdownBlock>;
102
+ /** Slide-level speaker notes, stripped from frontmatter. Plain text. */
103
+ notes?: string;
89
104
  };
90
105
  /**
91
106
  * The intermediate deck shape produced by compileDeck. buildDeck runs the
@@ -104,6 +119,8 @@ export type CompilerDeck = {
104
119
  export type ResolvedCompilerDeckStep = {
105
120
  layout: string;
106
121
  content?: Record<string, TextFill | TableFill | ImageFill | TemplateFill>;
122
+ /** Slide-level speaker notes, threaded through to the engine. Plain text. */
123
+ notes?: string;
107
124
  };
108
125
  /**
109
126
  * A CompilerDeck whose steps have been passed through the resolvers via
@@ -155,11 +172,10 @@ export type CompilerTemplateParameter = CompilerShapeBase & {
155
172
  /** The shape's text as one template with `{key}` placeholders; newlines are line breaks. */
156
173
  template: string;
157
174
  };
158
- /** Image parameter: one frontmatter path filled by fillImage. */
175
+ /** Image parameter: one frontmatter path filled by fillImage. Sizing/crop
176
+ * behaviour comes from the resolved asset's `type`, not the slot. */
159
177
  export type CompilerImageParameter = CompilerSlotBase & {
160
178
  type: typeof ParameterType.Image;
161
- /** How the picture scales inside its frame (required — no silent default). */
162
- fit: FitMode;
163
179
  };
164
180
  /**
165
181
  * Compiler-facing parameter. A layout's frontmatter inputs — each written as a
@@ -175,8 +191,6 @@ export type CompilerTextSlot = CompilerSlotBase & {
175
191
  /** Table shape backed by an `<a:tbl>` with header + data specimen rows. */
176
192
  export type CompilerTableSlot = CompilerSlotBase & {
177
193
  type: typeof CompilerSlotType.Table;
178
- /** Enforced column count. */
179
- columns?: number;
180
194
  };
181
195
  /**
182
196
  * Text slot that consumes a fenced code block. The resolver (`CodeResolver`)
@@ -1,4 +1,15 @@
1
- import { SlotType, } from "../engine/index.js";
1
+ import { SlotType } from "../engine/index.js";
2
+ // ── Asset catalog (compiler / theme-metadata only) ───────────────────────────
3
+ /**
4
+ * An asset's scaling/cropping tolerance, declared in the theme catalog. The
5
+ * compiler maps it to the engine's object-fit `fit`. `icon`: never enlarge,
6
+ * never crop. `image`: never crop, may scale. `background`: crop and scale freely.
7
+ */
8
+ export const AssetType = {
9
+ Icon: "icon",
10
+ Image: "image",
11
+ Background: "background",
12
+ };
2
13
  // ── FenceType discriminator ───────────────────────────────────────────────────
3
14
  /**
4
15
  * Discriminator strings for the compiler-internal fence shapes produced by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,6 +22,8 @@
22
22
  "format": "biome format --write ."
23
23
  },
24
24
  "dependencies": {
25
+ "@mermaid-js/mermaid-cli": "^11.15.0",
26
+ "@xmldom/xmldom": "^0.9.10",
25
27
  "commander": "^15.0.0",
26
28
  "image-size": "^2.0.2",
27
29
  "pptx-automizer": "^0.8.2",
@@ -35,7 +37,6 @@
35
37
  "devDependencies": {
36
38
  "@biomejs/biome": "^2.5.1",
37
39
  "@types/node": "^22.20.0",
38
- "@xmldom/xmldom": "^0.9.10",
39
40
  "typescript": "^5.8.0"
40
41
  }
41
42
  }
package/syntax.md CHANGED
@@ -91,7 +91,7 @@ A layout advertises two kinds of author-facing input, split by one rule: **a par
91
91
  "parameters": [
92
92
  { "key": "title", "type": "template" },
93
93
  { "key": "subtitle", "type": "template" },
94
- { "key": "logo", "type": "image", "fit": "contain", "required": true }
94
+ { "key": "logo", "type": "image", "required": true }
95
95
  ],
96
96
  "slots": [
97
97
  { "key": "body", "type": "text" },
@@ -114,7 +114,7 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
114
114
  jobTitle: CEO, Acme Corp
115
115
  ```
116
116
  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.
117
- - **`image`** -- a picture placeholder. Set it in frontmatter with the image path (from an asset catalog entry, or an absolute path). The parameter declares a `fit`: `contain` shows the whole image, `cover` fills the frame and center-crops overflow.
117
+ - **`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).
118
118
  ```yaml
119
119
  hero: assets/diagrams/architecture.png
120
120
  ```
@@ -136,13 +136,13 @@ Fill a slot by writing a region in the body: the default (unmarked) region maps
136
136
  ```
137
137
  ````
138
138
  The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting. Colors are applied as native text runs in the output, not images.
139
- - **`mermaid`** -- a mermaid diagram rendered as a themed PNG (see below). Written as a fenced `mermaid` region; the resulting PNG fills the slot with `contain` fit.
139
+ - **`mermaid`** -- a mermaid diagram rendered as a themed PNG (see below). Written as a fenced `mermaid` region; the resulting PNG is shown contained (in its entirety).
140
140
 
141
141
  ---
142
142
 
143
143
  ## Mermaid diagrams
144
144
 
145
- Mermaid diagrams are rendered as themed PNGs and delivered to any slot declared with `type: mermaid`. Write a fenced code block with the `mermaid` language tag in a named slot whose layout declares that slot as `type: mermaid` (with a `mermaidVariant` naming the theme's color variant). The resulting PNG behaves like any other image in the slot -- always shown in its entirety (`contain` fit).
145
+ Mermaid diagrams are rendered as themed PNGs and delivered to any slot declared with `type: mermaid`. Write a fenced code block with the `mermaid` language tag in a named slot whose layout declares that slot as `type: mermaid` (with a `mermaidVariant` naming the theme's color variant). The resulting PNG behaves like any other image in the slot -- always shown in its entirety.
146
146
 
147
147
  To let the same physical slide accept either an image or a diagram, the theme author declares two layouts with the same `slideNumber` -- one exposing the fill as an `image` parameter (frontmatter path), one as a `mermaid` slot (a fenced region). Authors pick between them by naming the layout in frontmatter; the compiler routes content based on the layout's declaration, so there is no ambiguity.
148
148
 
@@ -214,7 +214,7 @@ hero: assets/diagrams/architecture.png
214
214
  Each parameter or slot in the layout definition may declare:
215
215
  - **`type`** (required) -- parameters: `template`, `image`; slots: `text`, `table`, `code`, `mermaid`.
216
216
  - **`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.
217
- - **`fit`** -- image parameters only (required): `contain` shows the whole image inside the frame (letterboxed); `cover` fills the frame and center-crops overflow. Fit is a layout-designer decision baked into the parameter -- callers never override it per slide. Mermaid slots don't declare `fit`; mermaid always renders contained.
217
+ - **image sizing** -- each catalog asset declares a `type` (`icon` | `image` | `background`) that determines how it is scaled and cropped: `icon` never enlarges past native and never crops; `image` fits the whole picture (no crop, may scale); `background` fills and center-crops. Mermaid renders as `image` (contained).
218
218
  - **`codeTheme`** -- code slots only (required): the Shiki theme id used to syntax-highlight fenced code that lands in this slot (e.g. `"github-dark"`).
219
219
  - **`mermaidVariant`** -- mermaid slots only (required): names the color variant from `theme.mermaid` (e.g. `"dark"`). See [Mermaid diagrams](#mermaid-diagrams) above.
220
220
 
@@ -222,6 +222,26 @@ Each layout also declares a `slideNumber` pointing at the physical slide in the
222
222
 
223
223
  ---
224
224
 
225
+ ## Speaker notes
226
+
227
+ Any slide may carry a `notes:` key in its frontmatter -- a plain-text speaker-notes block attached to that slide's notes page. It is slide-level metadata, not a parameter or a slot: it is never routed to a shape and never appears on the slide face, only in the presenter/notes view.
228
+
229
+ Write multiple lines with a YAML block scalar (`|`); each line becomes one notes paragraph. Blank template notes on the underlying slide are always stripped, so only what you author here shows up.
230
+
231
+ ```yaml
232
+ ---
233
+ layout: Body
234
+ title: Key Achievements
235
+ notes: |
236
+ Open by thanking the regional teams.
237
+ Land the 23% number, then pause before the churn stat.
238
+ ---
239
+ ```
240
+
241
+ To build with all speaker notes omitted, pass `--no-notes` to `tycoslide build` (see [README](README.md#cli)).
242
+
243
+ ---
244
+
225
245
  ## Slides with no body
226
246
 
227
247
  Slides that have all their content in frontmatter (common for title slides, section dividers) need no body: