@tycoworks/tycoslide 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +10 -6
  2. package/dist/agents/catalog.d.ts +32 -0
  3. package/dist/agents/catalog.js +37 -0
  4. package/dist/agents/commands.d.ts +9 -0
  5. package/dist/agents/commands.js +57 -0
  6. package/dist/agents/extract.d.ts +22 -0
  7. package/dist/agents/extract.js +54 -0
  8. package/dist/{files.d.ts → agents/files.d.ts} +13 -3
  9. package/dist/{files.js → agents/files.js} +16 -3
  10. package/dist/agents/inventory.d.ts +83 -0
  11. package/dist/agents/inventory.js +568 -0
  12. package/dist/agents/manifest.d.ts +3 -0
  13. package/dist/{manifest.js → agents/manifest.js} +3 -14
  14. package/dist/agents/media.d.ts +38 -0
  15. package/dist/agents/media.js +83 -0
  16. package/dist/agents/pptx.d.ts +48 -0
  17. package/dist/agents/pptx.js +123 -0
  18. package/dist/agents/skill.d.ts +43 -0
  19. package/dist/agents/skill.js +134 -0
  20. package/dist/cli.js +8 -44
  21. package/dist/engine/dom.d.ts +3 -0
  22. package/dist/engine/dom.js +4 -0
  23. package/dist/engine/fillers/image.d.ts +6 -5
  24. package/dist/engine/fillers/image.js +24 -7
  25. package/dist/engine/generate.d.ts +2 -2
  26. package/dist/engine/generate.js +2 -2
  27. package/dist/engine/index.d.ts +3 -0
  28. package/dist/engine/index.js +2 -0
  29. package/dist/engine/notes.js +20 -62
  30. package/dist/engine/ooxml.d.ts +36 -0
  31. package/dist/engine/ooxml.js +56 -0
  32. package/dist/engine/types.d.ts +4 -0
  33. package/dist/index.d.ts +4 -11
  34. package/dist/index.js +2 -15
  35. package/dist/markdown/blocks/code.js +1 -2
  36. package/dist/markdown/blocks/image.d.ts +23 -0
  37. package/dist/markdown/blocks/image.js +65 -3
  38. package/dist/markdown/blocks/mermaid.d.ts +1 -1
  39. package/dist/markdown/blocks/mermaid.js +6 -4
  40. package/dist/markdown/blocks/table.js +7 -6
  41. package/dist/markdown/blocks/text.js +9 -8
  42. package/dist/markdown/deckCompiler.d.ts +2 -13
  43. package/dist/markdown/deckCompiler.js +7 -78
  44. package/dist/markdown/index.d.ts +4 -2
  45. package/dist/markdown/index.js +3 -1
  46. package/dist/markdown/inline.d.ts +2 -0
  47. package/dist/markdown/inline.js +4 -3
  48. package/dist/markdown/schema/themeConfigSchema.d.ts +2 -11
  49. package/dist/markdown/schema/themeConfigSchema.js +6 -16
  50. package/dist/markdown/slideParser.d.ts +2 -0
  51. package/dist/markdown/slideParser.js +6 -3
  52. package/dist/markdown/types.d.ts +4 -29
  53. package/dist/markdown/types.js +0 -11
  54. package/{theme-package/syntax.md → docs/markdown.md} +60 -28
  55. package/docs/theme.md +144 -0
  56. package/package.json +3 -2
  57. package/theme-package/SKILL.md +24 -90
  58. package/dist/engine/rels.d.ts +0 -18
  59. package/dist/engine/rels.js +0 -55
  60. package/dist/manifest.d.ts +0 -5
  61. package/dist/skillZip.d.ts +0 -49
  62. package/dist/skillZip.js +0 -177
@@ -1,23 +1,12 @@
1
- import { type ImageFill } from "../engine/index.js";
2
1
  import type { ParsedDocument } from "./slideParser.js";
3
- import { AssetType, type CompilerConfig, type CompilerDeck } from "./types.js";
4
- /**
5
- * Wrap an absolute image path as an ImageFill, expanding the resolved asset
6
- * `type` into the engine's scaling constraints. Callers resolve the path (see
7
- * `resolveImagePath`) and the type (from the catalog) first.
8
- */
9
- export declare function toImageFill(path: string, type: AssetType): ImageFill;
2
+ import { type CompilerConfig, type CompilerDeck } from "./types.js";
10
3
  /**
11
4
  * Compile a parsed deck document against a theme `config`. Each slide's content
12
5
  * is compiled straight into engine fills — prose/tables/images plus highlighted
13
6
  * code (Shiki) and rendered mermaid (PNG) — so the returned deck is
14
7
  * engine-shaped, ready for `buildDeck`.
15
8
  *
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` /
9
+ * Image paths resolve against `config.deckDir`. `config.codeTheme` /
21
10
  * `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
22
11
  */
23
12
  export declare function compileDeck(doc: ParsedDocument, config: CompilerConfig): Promise<CompilerDeck>;
@@ -1,27 +1,8 @@
1
- import { resolve } from "node:path";
2
- import { ImageFit, SlotType } from "../engine/index.js";
3
1
  import { parseSlotContent } from "./blocks/registry.js";
4
2
  import { validateSlideFrontmatter } from "./schema/deckSchema.js";
5
3
  import { templateKeys, templateToSegments } from "./textTemplate.js";
6
- import { AssetType, RESERVED_KEY, } from "./types.js";
7
- /** Map each semantic asset type to the engine's object-fit directive. */
8
- const FIT_FOR = {
9
- [AssetType.Icon]: ImageFit.ScaleDown,
10
- [AssetType.Image]: ImageFit.Contain,
11
- [AssetType.Background]: ImageFit.Cover,
12
- };
13
- /**
14
- * Wrap an absolute image path as an ImageFill, expanding the resolved asset
15
- * `type` into the engine's scaling constraints. Callers resolve the path (see
16
- * `resolveImagePath`) and the type (from the catalog) first.
17
- */
18
- export function toImageFill(path, type) {
19
- return { type: SlotType.Image, path, fit: FIT_FOR[type] };
20
- }
4
+ import { RESERVED_KEY, } from "./types.js";
21
5
  const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME]);
22
- // Anchored whole-field reference: the entire value is `$category.name` or it is
23
- // not a reference at all. Anchored ⇒ no escaping concerns.
24
- const ASSET_REF_RE = /^\$([a-zA-Z]\w*)\.([a-zA-Z]\w*)$/;
25
6
  /**
26
7
  * Assert that a region's parsed block folds to a type the slot `accepts`. The
27
8
  * folded type comes straight from `parseSlotContent` (which returns it beside
@@ -36,17 +17,6 @@ function assertSlotRegion(slot, got, layoutName, slideNo, source) {
36
17
  `(from ${source}); it accepts: ${accepted}.`);
37
18
  }
38
19
  }
39
- /**
40
- * Resolve a catalog image path against the deck's root directory.
41
- * When `rootDir` is empty, the path is returned unchanged so callers that
42
- * already produce absolute paths (or callers that don't care about
43
- * resolution) can opt out. Absolute paths always pass through.
44
- */
45
- function resolveImagePath(rootDir, path) {
46
- if (!rootDir || path.startsWith("/"))
47
- return path;
48
- return resolve(rootDir, path);
49
- }
50
20
  /**
51
21
  * Assert no two layouts sample the same base slide. In the sampled-composition
52
22
  * model one layout is one sampled base slide's region-arrangement, so a shared
@@ -111,7 +81,7 @@ function validateLayout(layout) {
111
81
  claimContentKey(slot.key, "slot");
112
82
  }
113
83
  }
114
- async function compileStep(slide, config, resolveAssetRef) {
84
+ async function compileStep(slide, config) {
115
85
  const { layouts } = config;
116
86
  const { frontmatter, slots, index } = slide;
117
87
  // Slide numbers in errors are 1-based, matching how an author counts slides in
@@ -192,10 +162,10 @@ async function compileStep(slide, config, resolveAssetRef) {
192
162
  }
193
163
  const source = `::${name}::`;
194
164
  const parsed = parseSlotContent(text, {
195
- resolveAssetRef,
196
165
  layoutName,
197
166
  slideNo: slideNo,
198
167
  source,
168
+ region: `Slide ${slideNo}: layout "${layoutName}" slot content (from ${source})`,
199
169
  config,
200
170
  layoutVariant: layoutDef.variant,
201
171
  });
@@ -222,15 +192,11 @@ async function compileStep(slide, config, resolveAssetRef) {
222
192
  * code (Shiki) and rendered mermaid (PNG) — so the returned deck is
223
193
  * engine-shaped, ready for `buildDeck`.
224
194
  *
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` /
195
+ * Image paths resolve against `config.deckDir`. `config.codeTheme` /
230
196
  * `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
231
197
  */
232
198
  export async function compileDeck(doc, config) {
233
- const { layouts, rootDir, assets } = config;
199
+ const { layouts } = config;
234
200
  const theme = doc.global[RESERVED_KEY.THEME];
235
201
  if (theme === undefined) {
236
202
  throw new Error(`Missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
@@ -245,48 +211,11 @@ export async function compileDeck(doc, config) {
245
211
  assertUniqueSlideNumbers(layouts);
246
212
  for (const layout of layouts)
247
213
  validateLayout(layout);
248
- // A body image names a picture one of two ways, and both end as an absolute
249
- // path plus a fit: `$category.name` looks the picture up in the theme's
250
- // catalog (which carries its declared type), while anything else is a file
251
- // path relative to the deck (typed `image`, since nothing declares it). Only
252
- // the lookup differs; the wrapping is shared.
253
- const fromCatalog = (ref) => {
254
- const match = ASSET_REF_RE.exec(ref);
255
- if (!match) {
256
- throw new Error(`Asset reference "${ref}" must be in the form $category.name (e.g. $logos.primary).`);
257
- }
258
- const [, category, name] = match;
259
- const entry = assets[category]?.[name];
260
- if (!entry) {
261
- // Every asset in the catalog is far too many to read: a theme's icon set
262
- // alone can run to thousands.
263
- // A known category narrows it to that category's names, which is what the
264
- // author is choosing between; an unknown one lists the categories instead.
265
- const group = assets[category];
266
- const available = group
267
- ? Object.keys(group)
268
- .map((n) => `$${category}.${n}`)
269
- .join(", ")
270
- : Object.keys(assets)
271
- .map((c) => `$${c}.*`)
272
- .join(", ");
273
- throw new Error(`Unknown asset reference "${ref}". Available: ${available}`);
274
- }
275
- return { path: resolveImagePath(rootDir, entry.path), type: entry.type };
276
- };
277
- const fromDeck = (ref) => ({
278
- path: resolveImagePath(config.deckDir, ref),
279
- type: AssetType.Image,
280
- });
281
- const resolveAssetRef = (ref) => {
282
- const { path, type } = ref.startsWith("$") ? fromCatalog(ref) : fromDeck(ref);
283
- return toImageFill(path, type);
284
- };
285
214
  // Slides compile in order: a slide's structural errors (unknown layout/key,
286
- // bad asset ref, accept-type mismatch) fire before its own content is rendered.
215
+ // accept-type mismatch) fire before its own content is rendered.
287
216
  const steps = [];
288
217
  for (const slide of doc.slides) {
289
- steps.push(await compileStep(slide, config, resolveAssetRef));
218
+ steps.push(await compileStep(slide, config));
290
219
  }
291
220
  return { theme: String(theme), steps };
292
221
  }
@@ -6,8 +6,10 @@ export { type BlockContext, type BlockHandler, parseSlotContent } from "./blocks
6
6
  export { compileDeck } from "./deckCompiler.js";
7
7
  export { parseInlineRuns } from "./inline.js";
8
8
  export { parseRegion } from "./mdast.js";
9
+ export { strict } from "./schema/strict.js";
9
10
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
10
11
  export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
12
  export { parseSlideDocument } from "./slideParser.js";
12
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, } from "./types.js";
13
- export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
13
+ export { templateKeys } from "./textTemplate.js";
14
+ export type { CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, } from "./types.js";
15
+ export { AcceptType, RESERVED_KEY } from "./types.js";
@@ -9,6 +9,8 @@ export { parseSlotContent } from "./blocks/registry.js";
9
9
  export { compileDeck } from "./deckCompiler.js";
10
10
  export { parseInlineRuns } from "./inline.js";
11
11
  export { parseRegion } from "./mdast.js";
12
+ export { strict } from "./schema/strict.js";
12
13
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
13
14
  export { parseSlideDocument } from "./slideParser.js";
14
- export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
15
+ export { templateKeys } from "./textTemplate.js";
16
+ export { AcceptType, RESERVED_KEY } from "./types.js";
@@ -22,5 +22,7 @@ export interface InlineState {
22
22
  underline?: boolean;
23
23
  link?: string;
24
24
  breakAsNewline?: boolean;
25
+ /** Where the text sits (the slot region), to name in an error. */
26
+ region?: string;
25
27
  }
26
28
  export declare function walkPhrasingChildren(children: PhrasingContent[], state: InlineState): TextRun[];
@@ -71,12 +71,13 @@ function walkPhrasing(node, state) {
71
71
  // path it collapses to a space.
72
72
  return [makeRun(state.breakAsNewline ? "\n" : " ", state)];
73
73
  default:
74
- // Any other phrasing node with a literal `value` (footnote references,
75
- // etc.) — emit its text if it has one, otherwise nothing.
74
+ // Any other phrasing node with a literal `value` (inline HTML) is emitted as
75
+ // that text. One without (an image inside a sentence) has nothing to show,
76
+ // so it fails rather than vanish from the slide.
76
77
  if ("value" in node && typeof node.value === "string") {
77
78
  return [makeRun(node.value, state)];
78
79
  }
79
- return [];
80
+ throw new Error(`${state.region ? `${state.region}: ` : ""}"${node.type}" inside text isn't supported.`);
80
81
  }
81
82
  }
82
83
  export function walkPhrasingChildren(children, state) {
@@ -6,8 +6,8 @@ export declare const ThemeConfigSchema: z.ZodObject<{
6
6
  slideNumber: z.ZodNumber;
7
7
  description: z.ZodOptional<z.ZodString>;
8
8
  variant: z.ZodOptional<z.ZodEnum<{
9
- light: "light";
10
- dark: "dark";
9
+ readonly Light: "light";
10
+ readonly Dark: "dark";
11
11
  }>>;
12
12
  parameters: z.ZodArray<z.ZodObject<{
13
13
  shapeName: z.ZodString;
@@ -40,15 +40,6 @@ export declare const ThemeConfigSchema: z.ZodObject<{
40
40
  required: z.ZodOptional<z.ZodBoolean>;
41
41
  }, z.core.$strict>>;
42
42
  }, z.core.$strict>>;
43
- assets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
44
- path: z.ZodString;
45
- type: z.ZodEnum<{
46
- icon: "icon";
47
- image: "image";
48
- background: "background";
49
- }>;
50
- description: z.ZodString;
51
- }, z.core.$strict>>>;
52
43
  template: z.ZodString;
53
44
  mermaid: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
54
45
  primary: z.ZodString;
@@ -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, Variant } from "../types.js";
4
+ import { AcceptType, 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
@@ -16,13 +16,12 @@ import { strict } from "./strict.js";
16
16
  *
17
17
  * Every object is a `strictObject` so an unknown key throws instead of being
18
18
  * silently dropped — strictness does NOT propagate, so each nested object is
19
- * independently strict. The two exceptions are the `AssetCatalog` and `mermaid`
20
- * records, whose keys are user-defined category/asset/variant names (a
21
- * `z.record`, open by design); strictness lands on their leaf entries.
19
+ * independently strict. The exception is the `mermaid` record, whose keys are
20
+ * user-defined variant names (a `z.record`, open by design); strictness lands on
21
+ * its entries.
22
22
  */
23
- // Reuse the const-object enums as runtime values — no third copy of the literals.
24
- const assetTypeSchema = z.enum(Object.values(AssetType));
25
- const variantSchema = z.enum(Object.values(Variant));
23
+ // Reuse the const-object enum as runtime values — no third copy of the literals.
24
+ const variantSchema = z.enum(Variant);
26
25
  // A table specimen's repeatable row range: `[start, end]`, 0-based inclusive,
27
26
  // non-negative integers. The range is validated against the specimen's actual row
28
27
  // count at fill time, where the row count is known.
@@ -35,14 +34,6 @@ const FrameSchema = strict({
35
34
  cx: z.number(),
36
35
  cy: z.number(),
37
36
  });
38
- const AssetEntrySchema = strict({
39
- path: z.string(),
40
- type: assetTypeSchema,
41
- description: z.string(),
42
- });
43
- // AssetCatalog: `{ category: { name: AssetEntry } }`. The two record levels are
44
- // OPEN (names are user-defined); only the leaf entry is strict.
45
- const AssetCatalogSchema = z.record(z.string(), z.record(z.string(), AssetEntrySchema));
46
37
  const MermaidVariantSchema = strict({
47
38
  primary: z.string(),
48
39
  primaryContrast: z.string(),
@@ -106,7 +97,6 @@ const LayoutSchema = strict({
106
97
  });
107
98
  export const ThemeConfigSchema = strict({
108
99
  layouts: z.array(LayoutSchema),
109
- assets: AssetCatalogSchema,
110
100
  template: z.string(),
111
101
  mermaid: MermaidConfigSchema.optional(),
112
102
  fonts: z.array(ThemeFontSchema).optional(),
@@ -8,6 +8,8 @@ export interface ParsedDocument {
8
8
  slides: RawSlide[];
9
9
  }
10
10
  export declare function parseSlideDocument(source: string): ParsedDocument;
11
+ /** True for a parsed YAML document that is a mapping (`key: value` pairs). */
12
+ export declare function isYamlMapping(parsed: unknown): parsed is Record<string, unknown>;
11
13
  export declare class FrontmatterParseError extends Error {
12
14
  constructor(slideIndex: number, yamlSource: string, cause: unknown);
13
15
  }
@@ -15,12 +15,16 @@ export function parseSlideDocument(source) {
15
15
  // GLOBAL FRONTMATTER
16
16
  // ============================================
17
17
  const GLOBAL_FM_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
18
+ /** True for a parsed YAML document that is a mapping (`key: value` pairs). */
19
+ export function isYamlMapping(parsed) {
20
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
21
+ }
18
22
  function extractGlobalFrontmatter(source) {
19
23
  const match = source.match(GLOBAL_FM_RE);
20
24
  if (!match)
21
25
  return { global: {}, rest: source };
22
26
  const parsed = parseYaml(match[1]);
23
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
27
+ if (isYamlMapping(parsed)) {
24
28
  return {
25
29
  global: parsed,
26
30
  rest: source.slice(match[0].length),
@@ -142,9 +146,8 @@ function parseFrontmatter(yaml, slideIndex) {
142
146
  return {};
143
147
  try {
144
148
  const result = parseYaml(yaml);
145
- if (result && typeof result === "object" && !Array.isArray(result)) {
149
+ if (isYamlMapping(result))
146
150
  return result;
147
- }
148
151
  }
149
152
  catch (err) {
150
153
  throw new FrontmatterParseError(slideIndex, yaml, err);
@@ -2,31 +2,6 @@ import type { RootContent } from "mdast";
2
2
  import { type BodyRows, type Frame, type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
3
3
  import type { MermaidConfig } from "./blocks/mermaidTheme.js";
4
4
  export type { BodyRows };
5
- /**
6
- * An asset's scaling/cropping tolerance, declared in the theme catalog. The
7
- * compiler maps it to the engine's object-fit `fit`. `icon`: never enlarge,
8
- * never crop. `image`: never crop, may scale. `background`: crop and scale freely.
9
- */
10
- export declare const AssetType: {
11
- readonly Icon: "icon";
12
- readonly Image: "image";
13
- readonly Background: "background";
14
- };
15
- export type AssetType = (typeof AssetType)[keyof typeof AssetType];
16
- /**
17
- * A theme's declaration of a reusable image asset. Purely compiler-facing —
18
- * the engine never sees this type; the compiler resolves an entry's `path`
19
- * to a fully-qualified filesystem path and wraps it as an ImageFill before
20
- * handing the deck to the engine.
21
- */
22
- export type AssetEntry = {
23
- path: string;
24
- /** Required — a missing type is a fail-fast error. */
25
- type: AssetType;
26
- description: string;
27
- };
28
- /** Two-level catalog: `{ category: { name: AssetEntry } }`. */
29
- export type AssetCatalog = Record<string, Record<string, AssetEntry>>;
30
5
  /**
31
6
  * The engine content types a slot may `accept`. A subset of the engine's
32
7
  * `SlotType`: `Template` is parameter-only, never a body block. There is no
@@ -68,7 +43,7 @@ export type EngineFill = TextFill | TableFill | ImageFill | TemplateFill;
68
43
  export type BlockFill = TextFill | TableFill | ImageFill;
69
44
  /**
70
45
  * Everything a block handler needs to compile a node into its engine fill: the
71
- * asset resolver, the diagnostic context to name the offending layout/slide/slot
46
+ * diagnostic context to name the offending layout/slide/slot
72
47
  * when a region's markdown shape is illegal (a stray standalone block mixed into
73
48
  * prose), and the theme-level `config` — from which code/mermaid compile read
74
49
  * their one-per-theme style (`codeTheme`, `mermaid`, `mermaidVariant`), not the
@@ -79,10 +54,11 @@ export type BlockFill = TextFill | TableFill | ImageFill;
79
54
  * imports the per-kind handlers, so the reverse would cycle.
80
55
  */
81
56
  export type BlockContext = {
82
- resolveAssetRef: (ref: string) => ImageFill;
83
57
  layoutName: string;
84
58
  slideNo: number;
85
59
  source: string;
60
+ /** Where the region sits, for the author: the prefix block handlers put on their errors. */
61
+ region: string;
86
62
  config: CompilerConfig;
87
63
  /**
88
64
  * The current layout's tonal surface, threaded from its `variant`. The code
@@ -251,7 +227,6 @@ export type ThemeFont = {
251
227
  */
252
228
  export type CompilerThemeConfig = {
253
229
  layouts: CompilerLayout[];
254
- assets: AssetCatalog;
255
230
  template: string;
256
231
  mermaid?: MermaidConfig;
257
232
  /**
@@ -276,7 +251,7 @@ export type CompilerThemeConfig = {
276
251
  };
277
252
  /**
278
253
  * A theme as loaded from disk: its validated config plus the directory it
279
- * lives in, which the catalog's relative asset paths resolve against.
254
+ * lives in, which its relative paths (template, fonts) resolve against.
280
255
  */
281
256
  export type LoadedTheme = CompilerThemeConfig & {
282
257
  rootDir: string;
@@ -1,15 +1,4 @@
1
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
- };
13
2
  // ── AcceptType discriminator (what a slot accepts) ────────────────────────────
14
3
  /**
15
4
  * The engine content types a slot may `accept`. A subset of the engine's
@@ -1,11 +1,19 @@
1
1
  # Markdown Syntax Reference
2
2
 
3
- This document covers the detailed syntax for writing slide content in tycoslide deck files. For an overview of how to create slides, see [SKILL.md](SKILL.md).
3
+ This document covers the detailed syntax for writing slide content in tycoslide deck files.
4
4
 
5
- > **Every layout, parameter, and slot name in the examples below is a placeholder.** Your theme's real names live in `manifest.json` — read it first, and never assume a name shown here (`Body`, `TwoColumn`, `hero`, `::left::`, etc.) exists in your theme.
5
+ > **Every layout, parameter, and slot name in the examples below is a placeholder.** Your theme's real names live in `theme.json` — read it first, and never assume a name shown here (`Body`, `TwoColumn`, `hero`, `::left::`, etc.) exists in your theme.
6
6
 
7
7
  ---
8
8
 
9
+ ## File structure
10
+
11
+ A deck file has three parts:
12
+
13
+ 1. **Global frontmatter** (required) -- the first `---`-delimited block.
14
+ 2. **Slides** -- each begins with a `---` separator. A slide's frontmatter sits between `---` delimiters. Body content follows the closing `---`.
15
+ 3. **Slide separators** -- a `---` on its own line separates slides.
16
+
9
17
  ## Global frontmatter
10
18
 
11
19
  Every deck file starts with a global frontmatter block declaring the theme:
@@ -20,6 +28,23 @@ theme: ./theme.json
20
28
 
21
29
  The output `.pptx` is written next to the deck file, named after it (`deck.md` → `deck.pptx`).
22
30
 
31
+ ## Slide frontmatter
32
+
33
+ Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters**. A value containing a colon-then-space must be quoted -- `title: "The change: compute on the difference"` -- or YAML reads it as a second key and the build fails.
34
+
35
+ ```yaml
36
+ ---
37
+ layout: Body
38
+ title: Key Achievements
39
+ subtitle: This Quarter
40
+ ---
41
+ ```
42
+
43
+ - `layout` is required and consumed by the compiler (not forwarded as content).
44
+ - All other frontmatter keys fill parameters: `title` fills the `title` parameter, `subtitle` fills the `subtitle` parameter, etc.
45
+ - Slots (accepting `text`, `table`, `image`) are filled by body regions, not frontmatter -- see below.
46
+ - A slide may also carry a `notes:` block in frontmatter -- plain-text speaker notes for the slide's notes page (see [Speaker notes](#speaker-notes)). It is slide-level metadata, not a parameter or slot.
47
+
23
48
  ---
24
49
 
25
50
  ## Body content
@@ -28,7 +53,7 @@ A slide's body is split into regions with `::key::` markers. Each region fills t
28
53
 
29
54
  ```markdown
30
55
  ---
31
- layout: Body # ← placeholder; use a real layout from your manifest.json
56
+ layout: Body # ← placeholder; use a real layout from your theme
32
57
  title: Key Achievements
33
58
  ---
34
59
 
@@ -85,30 +110,15 @@ Content after a marker goes to the slot matching that name. The marker names mus
85
110
 
86
111
  ---
87
112
 
88
- ## Parameters and slots (in `manifest.json`)
89
-
90
- A layout advertises two kinds of author-facing input, split by one rule: **a parameter is one value on a frontmatter line; a slot is a multi-line region in the body** (a `::key::` region). In `manifest.json` each layout carries two lists, `parameters` and `slots`:
113
+ ## Parameters and slots
91
114
 
92
- ```jsonc
93
- {
94
- "name": "Feature with code",
95
- "parameters": [
96
- { "key": "title", "type": "template" },
97
- { "key": "subtitle", "type": "template" },
98
- { "key": "logo", "type": "image", "required": true }
99
- ],
100
- "slots": [
101
- { "key": "body", "accepts": ["text"] },
102
- { "key": "diagram", "accepts": ["image"] }
103
- ]
104
- }
105
- ```
115
+ A layout advertises two kinds of author-facing input, split by one rule: **a parameter is one value on a frontmatter line; a slot is a multi-line region in the body** (a `::key::` region).
106
116
 
107
117
  ### Parameter types (frontmatter lines)
108
118
 
109
119
  Fill a parameter by putting a value under its key in the slide's frontmatter.
110
120
 
111
- - **`template`** -- a shape whose existing runs are walked and replaced in place, preserving each run's style. Behind the scenes each text shape carries one `template` string with `{key}` placeholders (e.g. `{title}`, or `{name}\n{jobTitle}` for a two-line credits shape), but you never see the template: the manifest advertises **one key per placeholder**, and you fill each key as a plain scalar in frontmatter. A single-placeholder title shape gives you one key:
121
+ - **`template`** -- a shape whose existing runs are walked and replaced in place, preserving each run's style. Behind the scenes each text shape carries one `template` string with `{key}` placeholders (e.g. `{title}`, or `{name}\n{jobTitle}` for a two-line credits shape), but you never see the template: the layout advertises **one key per placeholder**, and you fill each key as a plain scalar in frontmatter. A single-placeholder title shape gives you one key:
112
122
  ```yaml
113
123
  title: Q3 Results
114
124
  ```
@@ -120,7 +130,7 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
120
130
  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
131
  ### Body content shapes
122
132
 
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:
133
+ Fill a slot by writing a `::key::` region in the body; the marker maps to the slot with that `key`. A slot lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
124
134
 
125
135
  - **text** (slots that accept `text`) -- markdown paragraphs and bullets, rebuilt from the template's specimen paragraph styles. A fenced code block also routes here:
126
136
  ````markdown
@@ -134,13 +144,13 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
134
144
  ````
135
145
  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.
136
146
  - **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).
137
- - **image** (slots that accept `image`) -- a picture, written as `![]($category.name)` for one from the theme's asset catalog, or `![](path/to/file.png)` for a file relative to the deck:
147
+ - **image** (slots that accept `image`) -- an image, written as `![alt](path "options")` with the path relative to the deck:
138
148
  ```markdown
139
149
  ::logo::
140
150
 
141
- ![]($brand.primaryDarkWordmark)
151
+ ![Acme Corp logo](assets/acme-logo.png "fit: contain")
142
152
  ```
143
- The categories and names are cataloged in `assets.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 file path has no catalog entry, so it is always fitted as `image`; put a picture in the catalog when it needs another fit or when agents should be able to find it. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
153
+ The alt text becomes the image's alt text in PowerPoint; leave it empty for an image that is only decoration. The title is optional and holds image options as YAML. The only option is `fit`, one of `contain` (the default, which shows the whole image and never crops), `cover` (fills the frame and center-crops) or `scale-down` (shows the whole image and never enlarges it). Anything else in the title fails the build. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
144
154
 
145
155
  ---
146
156
 
@@ -205,9 +215,8 @@ The theme owns all styling. These directives are rejected at build time:
205
215
 
206
216
  Each parameter or slot in the layout definition may declare:
207
217
  - **`accepts`** (slots, required) -- an array of `text`, `table`, `image`.
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.
218
+ - **`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).
209
219
  - **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.
210
- - **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).
211
220
 
212
221
  Each layout also declares a `slideNumber` pointing at the physical slide in the theme's template -- unique per layout (one layout maps to one physical slide).
213
222
 
@@ -247,6 +256,29 @@ subtitle: Advanced Topics
247
256
 
248
257
  ---
249
258
 
259
+ ## Build
260
+
261
+ Build the deck (replace `<deck.md>` with your deck file):
262
+
263
+ ```bash
264
+ npx tycoslide build <deck.md>
265
+ ```
266
+
267
+ Common errors and fixes:
268
+
269
+ | Error | Fix |
270
+ |-------|-----|
271
+ | `unknown layout "xyz"` | Check layout names in `theme.json` |
272
+ | A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
273
+ | YAML parse error | Fix the YAML syntax in the slide's frontmatter |
274
+ | `image "…" not found at …` | Check the path; it is relative to the deck file |
275
+ | `is not a set of options` | The image title holds options like `"fit: contain"`; put a description in the alt text |
276
+ | `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
277
+ | `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
278
+ | `no "mermaid" block` | The theme has no mermaid color config -- add a `mermaid` section to theme.json |
279
+
280
+ ---
281
+
250
282
  ## Full Example
251
283
 
252
284
  ```markdown
@@ -303,5 +335,5 @@ title: Office Map
303
335
 
304
336
  ::hero::
305
337
 
306
- ![]($images.officeFloorPlan)
338
+ ![Office floor plan with meeting rooms marked](assets/office-floor-plan.png)
307
339
  ```