@tycoworks/tycoslide 0.7.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/SKILL.md +249 -0
  4. package/bin/tycoslide.js +2 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +197 -0
  7. package/dist/engine/dom.d.ts +92 -0
  8. package/dist/engine/dom.js +354 -0
  9. package/dist/engine/fillers/filler.d.ts +22 -0
  10. package/dist/engine/fillers/filler.js +53 -0
  11. package/dist/engine/fillers/image.d.ts +19 -0
  12. package/dist/engine/fillers/image.js +105 -0
  13. package/dist/engine/fillers/table.d.ts +21 -0
  14. package/dist/engine/fillers/table.js +62 -0
  15. package/dist/engine/fillers/template.d.ts +27 -0
  16. package/dist/engine/fillers/template.js +221 -0
  17. package/dist/engine/fillers/text.d.ts +28 -0
  18. package/dist/engine/fillers/text.js +29 -0
  19. package/dist/engine/generate.d.ts +51 -0
  20. package/dist/engine/generate.js +161 -0
  21. package/dist/engine/index.d.ts +8 -0
  22. package/dist/engine/index.js +7 -0
  23. package/dist/engine/types.d.ts +128 -0
  24. package/dist/engine/types.js +22 -0
  25. package/dist/index.d.ts +46 -0
  26. package/dist/index.js +146 -0
  27. package/dist/manifest.d.ts +7 -0
  28. package/dist/manifest.js +88 -0
  29. package/dist/markdown/deckCompiler.d.ts +36 -0
  30. package/dist/markdown/deckCompiler.js +289 -0
  31. package/dist/markdown/index.d.ts +13 -0
  32. package/dist/markdown/index.js +13 -0
  33. package/dist/markdown/parsers.d.ts +32 -0
  34. package/dist/markdown/parsers.js +233 -0
  35. package/dist/markdown/resolvers/code.d.ts +17 -0
  36. package/dist/markdown/resolvers/code.js +44 -0
  37. package/dist/markdown/resolvers/mermaid.d.ts +14 -0
  38. package/dist/markdown/resolvers/mermaid.js +89 -0
  39. package/dist/markdown/resolvers/mermaidTheme.d.ts +27 -0
  40. package/dist/markdown/resolvers/mermaidTheme.js +113 -0
  41. package/dist/markdown/resolvers/resolver.d.ts +42 -0
  42. package/dist/markdown/resolvers/resolver.js +52 -0
  43. package/dist/markdown/slideParser.d.ts +14 -0
  44. package/dist/markdown/slideParser.js +196 -0
  45. package/dist/markdown/textTemplate.d.ts +15 -0
  46. package/dist/markdown/textTemplate.js +75 -0
  47. package/dist/markdown/types.d.ts +239 -0
  48. package/dist/markdown/types.js +40 -0
  49. package/package.json +41 -0
  50. package/syntax.md +291 -0
@@ -0,0 +1,46 @@
1
+ import { type Config, type ThemeConfig } from "./engine/index.js";
2
+ import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig, type ResolvedCompilerDeck } from "./markdown/types.js";
3
+ /**
4
+ * Run every compiler-owned resolver over `deck` (highlight code fences,
5
+ * render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
6
+ * values are narrowed to the engine's `TextFill | TableFill | ImageFill |
7
+ * TemplateFill` union. Structurally equivalent to the engine's `Deck` — a
8
+ * caller passes the returned value straight to `generate()` with no cast.
9
+ *
10
+ * Fails fast if `deck.output` is missing: downstream `generate()` requires it,
11
+ * and the CLI populates it before calling `buildDeck`; a programmatic caller
12
+ * that forgot to set it hits the error here instead of a confusing engine-side
13
+ * failure.
14
+ */
15
+ export declare function resolveDeck(deck: CompilerDeck, config: CompilerConfig): Promise<ResolvedCompilerDeck>;
16
+ /**
17
+ * Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
18
+ * Fields are copied cell-by-cell so the boundary is explicit — no casts.
19
+ * The compiler's `assets` catalog is intentionally NOT forwarded: the engine
20
+ * is ignorant of theme-level asset metadata; the compiler resolves any asset
21
+ * reference to a filesystem path and wraps it as an ImageFill in
22
+ * `step.content` before this projection runs.
23
+ */
24
+ export declare function toEngineThemeConfig(config: CompilerThemeConfig): ThemeConfig;
25
+ /**
26
+ * Project a CompilerConfig down to the engine's Config shape. Explicit
27
+ * projection at the boundary where compiler-only fields stop mattering.
28
+ */
29
+ export declare function toEngineConfig(config: CompilerConfig): Config;
30
+ /**
31
+ * End-to-end build: run compiler-owned resolvers (syntax highlighting, mermaid
32
+ * PNG rendering) over the deck via `resolveDeck`, which returns a narrowed
33
+ * `ResolvedCompilerDeck` — structurally equivalent to the engine's `Deck` —
34
+ * then hand it to the engine's primitives-only `generate()`. No cast required:
35
+ * the narrowing happens at the type level via `resolveDeck`.
36
+ *
37
+ * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
38
+ * post-write cleanup is needed.
39
+ */
40
+ export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig): Promise<void>;
41
+ export type { Config, Deck, DeckStep, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
42
+ export { FitMode, fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
43
+ export type { ManifestOptions } from "./manifest.js";
44
+ export { generateManifest } from "./manifest.js";
45
+ export type { AssetCatalog, AssetEntry, CodeFence, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, MarkdownBlock, MermaidConfig, MermaidFence, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
46
+ export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ import { generate, SlotType, } from "./engine/index.js";
2
+ import { isFence, resolveFences } from "./markdown/resolvers/resolver.js";
3
+ import { CompilerSlotType, ParameterType, } from "./markdown/types.js";
4
+ /**
5
+ * Narrow a resolved content map to the engine-shaped value union. Runs after
6
+ * `resolveFences`, so no CodeFence or MermaidFence should remain — a leftover is
7
+ * a resolver bug and throws. Every surviving value is already an engine fill
8
+ * (TextFill / TableFill / ImageFill / TemplateFill), so no unwrapping is needed.
9
+ */
10
+ function narrowContent(content) {
11
+ const out = {};
12
+ for (const [key, value] of Object.entries(content)) {
13
+ if (isFence(value)) {
14
+ throw new Error(`resolveDeck: slot "${key}" still holds an unresolved ${value.type} block ` +
15
+ "after resolvers ran. This is a resolver bug.");
16
+ }
17
+ out[key] = value;
18
+ }
19
+ return out;
20
+ }
21
+ /**
22
+ * Run every compiler-owned resolver over `deck` (highlight code fences,
23
+ * render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
24
+ * values are narrowed to the engine's `TextFill | TableFill | ImageFill |
25
+ * TemplateFill` union. Structurally equivalent to the engine's `Deck` — a
26
+ * caller passes the returned value straight to `generate()` with no cast.
27
+ *
28
+ * Fails fast if `deck.output` is missing: downstream `generate()` requires it,
29
+ * and the CLI populates it before calling `buildDeck`; a programmatic caller
30
+ * that forgot to set it hits the error here instead of a confusing engine-side
31
+ * failure.
32
+ */
33
+ export async function resolveDeck(deck, config) {
34
+ await resolveFences(deck, config);
35
+ if (deck.output === undefined) {
36
+ throw new Error('resolveDeck: deck.output is not set. Set it (e.g. "deck.pptx") before calling buildDeck.');
37
+ }
38
+ return {
39
+ theme: deck.theme,
40
+ output: deck.output,
41
+ steps: deck.steps.map((step) => {
42
+ const resolvedStep = { layout: step.layout };
43
+ if (step.content)
44
+ resolvedStep.content = narrowContent(step.content);
45
+ return resolvedStep;
46
+ }),
47
+ };
48
+ }
49
+ /**
50
+ * Project a CompilerParameter or CompilerSlot down to the engine's flat Slot.
51
+ * Parameters (Template, Image) map straight to their engine equivalent; compiler-
52
+ * only slot types (Code, Mermaid) map to Text / Image since their resolved
53
+ * StyledParagraph[] / ImageFill content is filled by the corresponding engine
54
+ * primitive once the compiler is done.
55
+ *
56
+ * The discriminated unions narrow per-variant fields, so the projection is a
57
+ * straight switch over all six type values — no runtime "wrong field on wrong
58
+ * type" checks; TypeScript enforces the invariants at authoring time.
59
+ */
60
+ function toEngineSlot(slot) {
61
+ switch (slot.type) {
62
+ case ParameterType.Template:
63
+ // A text shape carries no top-level key — its template placeholders are the keys. The
64
+ // compiler emits its expanded content under shapeName, so the engine slot
65
+ // is keyed by shapeName too.
66
+ return { key: slot.shapeName, shapeName: slot.shapeName, type: SlotType.Template };
67
+ case ParameterType.Image:
68
+ return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Image };
69
+ case CompilerSlotType.Text: {
70
+ const result = { key: slot.key, shapeName: slot.shapeName, type: SlotType.Text };
71
+ if (slot.startAt !== undefined)
72
+ result.startAt = slot.startAt;
73
+ return result;
74
+ }
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
+ }
81
+ case CompilerSlotType.Code:
82
+ // Highlighter resolves the code fence into StyledParagraph[]; engine
83
+ // fills it via fillText.
84
+ return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Text };
85
+ case CompilerSlotType.Mermaid:
86
+ // Mermaid renderer produces a PNG (ImageFill); engine fills it via
87
+ // fillImage. The fit lives on the ImageFill, not the engine Slot.
88
+ return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Image };
89
+ }
90
+ }
91
+ function toEngineLayout(layout) {
92
+ return {
93
+ name: layout.name,
94
+ slideNumber: layout.slideNumber,
95
+ description: layout.description,
96
+ whenToUse: layout.whenToUse,
97
+ whenNotToUse: layout.whenNotToUse,
98
+ slots: [...layout.parameters.map(toEngineSlot), ...layout.slots.map(toEngineSlot)],
99
+ };
100
+ }
101
+ /**
102
+ * Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
103
+ * Fields are copied cell-by-cell so the boundary is explicit — no casts.
104
+ * The compiler's `assets` catalog is intentionally NOT forwarded: the engine
105
+ * is ignorant of theme-level asset metadata; the compiler resolves any asset
106
+ * reference to a filesystem path and wraps it as an ImageFill in
107
+ * `step.content` before this projection runs.
108
+ */
109
+ export function toEngineThemeConfig(config) {
110
+ const result = {
111
+ layouts: config.layouts.map(toEngineLayout),
112
+ template: config.template,
113
+ };
114
+ if (config.outputDir !== undefined)
115
+ result.outputDir = config.outputDir;
116
+ return result;
117
+ }
118
+ /**
119
+ * Project a CompilerConfig down to the engine's Config shape. Explicit
120
+ * projection at the boundary where compiler-only fields stop mattering.
121
+ */
122
+ export function toEngineConfig(config) {
123
+ return {
124
+ ...toEngineThemeConfig(config),
125
+ rootDir: config.rootDir,
126
+ };
127
+ }
128
+ /**
129
+ * End-to-end build: run compiler-owned resolvers (syntax highlighting, mermaid
130
+ * PNG rendering) over the deck via `resolveDeck`, which returns a narrowed
131
+ * `ResolvedCompilerDeck` — structurally equivalent to the engine's `Deck` —
132
+ * then hand it to the engine's primitives-only `generate()`. No cast required:
133
+ * the narrowing happens at the type level via `resolveDeck`.
134
+ *
135
+ * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
136
+ * post-write cleanup is needed.
137
+ */
138
+ export async function buildDeck(deck, config) {
139
+ const resolved = await resolveDeck(deck, config);
140
+ await generate(resolved, toEngineConfig(config));
141
+ }
142
+ // Engine — primitives-only public surface.
143
+ export { FitMode, fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
144
+ export { generateManifest } from "./manifest.js";
145
+ // Markdown / Compiler
146
+ export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
@@ -0,0 +1,7 @@
1
+ import type { CompilerConfig } from "./markdown/types.js";
2
+ export type ManifestOptions = {
3
+ build: {
4
+ command: string;
5
+ };
6
+ };
7
+ export declare function generateManifest(config: CompilerConfig, options: ManifestOptions): string;
@@ -0,0 +1,88 @@
1
+ import { templateKeys } from "./markdown/textTemplate.js";
2
+ import { CompilerSlotType, ParameterType } from "./markdown/types.js";
3
+ /**
4
+ * 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.
8
+ *
9
+ * A template parameter's `limit` is deliberately NOT projected here: it measures the
10
+ * expanded run text (parameter-level), so surfacing it per key would misrepresent
11
+ * it as per-key. How to advertise a parameter-level limit is a Phase 2 decision;
12
+ * until then it stays manifest-internal.
13
+ */
14
+ function stripParameter(param) {
15
+ switch (param.type) {
16
+ case ParameterType.Template:
17
+ return templateKeys(param.template).map((key) => {
18
+ const result = { key, type: param.type };
19
+ if (param.required)
20
+ result.required = true;
21
+ return result;
22
+ });
23
+ case ParameterType.Image: {
24
+ const result = { key: param.key, type: param.type, fit: param.fit };
25
+ if (param.required)
26
+ result.required = true;
27
+ if (param.limit)
28
+ result.limit = param.limit;
29
+ return [result];
30
+ }
31
+ }
32
+ }
33
+ function stripSlot(slot) {
34
+ const result = { key: slot.key, type: slot.type };
35
+ if (slot.required)
36
+ result.required = true;
37
+ if (slot.limit)
38
+ result.limit = slot.limit;
39
+ switch (slot.type) {
40
+ case CompilerSlotType.Text:
41
+ // startAt is a fill hint, not a manifest surface concern.
42
+ break;
43
+ case CompilerSlotType.Table:
44
+ if (slot.columns !== undefined)
45
+ result.columns = slot.columns;
46
+ break;
47
+ case CompilerSlotType.Code:
48
+ result.codeTheme = slot.codeTheme;
49
+ break;
50
+ case CompilerSlotType.Mermaid:
51
+ result.mermaidVariant = slot.mermaidVariant;
52
+ break;
53
+ }
54
+ return result;
55
+ }
56
+ export function generateManifest(config, options) {
57
+ const layouts = config.layouts.map((layout) => ({
58
+ name: layout.name,
59
+ slideNumber: layout.slideNumber,
60
+ description: layout.description,
61
+ whenToUse: layout.whenToUse,
62
+ whenNotToUse: layout.whenNotToUse,
63
+ parameters: layout.parameters.flatMap(stripParameter),
64
+ slots: layout.slots.map(stripSlot),
65
+ }));
66
+ const assets = {};
67
+ for (const [category, entries] of Object.entries(config.assets)) {
68
+ assets[category] = {};
69
+ for (const [name, entry] of Object.entries(entries)) {
70
+ const manifestEntry = {
71
+ path: entry.path,
72
+ description: entry.description,
73
+ };
74
+ if (entry.whenToUse)
75
+ manifestEntry.whenToUse = entry.whenToUse;
76
+ assets[category][name] = manifestEntry;
77
+ }
78
+ }
79
+ const manifest = {
80
+ version: 1,
81
+ layouts,
82
+ assets,
83
+ build: {
84
+ command: options.build.command,
85
+ },
86
+ };
87
+ return JSON.stringify(manifest, null, 2);
88
+ }
@@ -0,0 +1,36 @@
1
+ import { FitMode, type ImageFill } from "../engine/index.js";
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;
14
+ /**
15
+ * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
16
+ * (layout, body). Exported so callers (e.g. cli.ts) reference the constants
17
+ * instead of literal strings.
18
+ */
19
+ export declare const RESERVED_KEY: {
20
+ readonly LAYOUT: "layout";
21
+ readonly BODY: "body";
22
+ readonly OUTPUT: "output";
23
+ readonly THEME: "theme";
24
+ };
25
+ /**
26
+ * Compile a parsed deck document against a set of layouts.
27
+ *
28
+ * `rootDir` (optional) is the base directory for resolving relative image
29
+ * paths declared in the deck's frontmatter or named slots. When omitted (or
30
+ * empty), image paths are returned unchanged — callers that already produce
31
+ * absolute paths (or callers that don't need resolution, e.g. unit tests)
32
+ * can rely on the pass-through. When provided, relative paths are resolved
33
+ * to absolute via `path.resolve(rootDir, path)`; absolute paths pass through.
34
+ */
35
+ export declare function compileDeck(doc: ParsedDocument, layouts: CompilerLayout[], rootDir?: string): CompilerDeck;
36
+ export { FitMode };
@@ -0,0 +1,289 @@
1
+ import { resolve } from "node:path";
2
+ import { FILLERS, FitMode, SlotType } from "../engine/index.js";
3
+ import { parseGfmTable, parseStyledParagraph } from "./parsers.js";
4
+ import { RESOLVERS } from "./resolvers/resolver.js";
5
+ import { templateKeys, templateToSegments } from "./textTemplate.js";
6
+ import { CompilerSlotType, FenceType, ParameterType, } from "./types.js";
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).
15
+ */
16
+ export function toImageFill(param, path) {
17
+ return { type: SlotType.Image, path, fit: param.fit };
18
+ }
19
+ /**
20
+ * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
21
+ * (layout, body). Exported so callers (e.g. cli.ts) reference the constants
22
+ * instead of literal strings.
23
+ */
24
+ export const RESERVED_KEY = {
25
+ LAYOUT: "layout",
26
+ BODY: "body",
27
+ OUTPUT: "output",
28
+ THEME: "theme",
29
+ };
30
+ const CODE_FENCE_RE = /^```(\w+)\n([\s\S]*?)```\s*$/;
31
+ function toTextFill(text) {
32
+ const paragraphs = text
33
+ .split(/\r?\n/)
34
+ .filter((line) => line.trim() !== "")
35
+ .map(parseStyledParagraph);
36
+ return { paragraphs };
37
+ }
38
+ function parseSlotContent(text) {
39
+ const fence = CODE_FENCE_RE.exec(text.trim());
40
+ if (fence) {
41
+ if (fence[1] === FenceType.Mermaid) {
42
+ const block = { type: FenceType.Mermaid, definition: fence[2].replace(/\n$/, "") };
43
+ return block;
44
+ }
45
+ const block = { type: FenceType.Code, language: fence[1], source: fence[2].replace(/\n$/, "") };
46
+ return block;
47
+ }
48
+ const table = parseGfmTable(text);
49
+ if (table)
50
+ return table;
51
+ return toTextFill(text);
52
+ }
53
+ /**
54
+ * Matcher for each CompilerSlotType, composed from the engine's `FILLERS` (text,
55
+ * table) and the compiler's `RESOLVERS` (code, mermaid) — no raw guards imported
56
+ * here, so a slot's expected shape always tracks whatever those registries say a
57
+ * fill/fence looks like.
58
+ */
59
+ const REGION_MATCHERS = {
60
+ [CompilerSlotType.Text]: FILLERS[SlotType.Text].matches,
61
+ [CompilerSlotType.Table]: FILLERS[SlotType.Table].matches,
62
+ [CompilerSlotType.Code]: RESOLVERS[FenceType.Code].matches,
63
+ [CompilerSlotType.Mermaid]: RESOLVERS[FenceType.Mermaid].matches,
64
+ };
65
+ /**
66
+ * Assert that a region's parsed block matches the slot's declared type. Called
67
+ * after `parseSlotContent` narrows a body/`::name::` region into a MarkdownBlock.
68
+ * Keys off the CompilerSlotType discriminator via `REGION_MATCHERS`.
69
+ */
70
+ function assertSlotRegion(slot, block, slideIdx, source) {
71
+ if (!REGION_MATCHERS[slot.type](block)) {
72
+ throw new Error(`Slide ${slideIdx}: slot "${slot.key}" (type "${slot.type}") got wrong content shape from ${source}.`);
73
+ }
74
+ }
75
+ function isImageParameter(param) {
76
+ return param.type === ParameterType.Image;
77
+ }
78
+ function isTemplateParameter(param) {
79
+ return param.type === ParameterType.Template;
80
+ }
81
+ /**
82
+ * Resolve a user-supplied image path against the deck's root directory.
83
+ * When `rootDir` is empty, the path is returned unchanged so callers that
84
+ * already produce absolute paths (or callers that don't care about
85
+ * resolution) can opt out. Absolute paths always pass through.
86
+ */
87
+ function resolveImagePath(rootDir, path) {
88
+ if (!rootDir || path.startsWith("/"))
89
+ return path;
90
+ return resolve(rootDir, path);
91
+ }
92
+ /**
93
+ * Validate a layout's key spaces once, independent of any slide, so every later
94
+ * frontmatter lookup and content-map write is unambiguous. Two spaces must each
95
+ * be collision-free — otherwise a fill silently clobbers or throws a misleading
96
+ * error:
97
+ *
98
+ * - **Author keys** — what a frontmatter line addresses: every template-parameter key
99
+ * and every image-parameter key must be distinct, so a line routes to exactly
100
+ * one parameter.
101
+ * - **Content keys** — what `step.content` is addressed by: every image key, every
102
+ * template parameter's `shapeName`, and every slot key must be distinct, so no two
103
+ * overwrite each other in the content map.
104
+ *
105
+ * Also rejects a required template parameter whose template has no keys — it declares
106
+ * no way to be filled, so `required` on it is unsatisfiable.
107
+ */
108
+ function validateLayout(layout) {
109
+ const authorKeys = new Set();
110
+ const contentKeys = new Set();
111
+ const claimAuthorKey = (key, owner) => {
112
+ if (authorKeys.has(key)) {
113
+ throw new Error(`Layout "${layout.name}": key "${key}" (${owner}) is declared twice; a frontmatter key fills exactly one parameter.`);
114
+ }
115
+ authorKeys.add(key);
116
+ };
117
+ const claimContentKey = (key, owner) => {
118
+ if (contentKeys.has(key)) {
119
+ throw new Error(`Layout "${layout.name}": name "${key}" (${owner}) collides with another parameter or slot; ` +
120
+ "each template parameter's shape, image key, and slot key must be distinct.");
121
+ }
122
+ contentKeys.add(key);
123
+ };
124
+ for (const param of layout.parameters) {
125
+ if (isImageParameter(param)) {
126
+ claimAuthorKey(param.key, "image parameter");
127
+ claimContentKey(param.key, "image parameter");
128
+ }
129
+ else {
130
+ const keys = templateKeys(param.template);
131
+ if (param.required && keys.length === 0) {
132
+ throw new Error(`Layout "${layout.name}": template parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
133
+ }
134
+ for (const key of keys)
135
+ claimAuthorKey(key, `template parameter "${param.shapeName}"`);
136
+ claimContentKey(param.shapeName, "template parameter");
137
+ }
138
+ }
139
+ for (const slot of layout.slots) {
140
+ claimContentKey(slot.key, "slot");
141
+ }
142
+ }
143
+ function compileStep(slide, layouts, rootDir) {
144
+ const { frontmatter, body, slots, index } = slide;
145
+ const layout = frontmatter[RESERVED_KEY.LAYOUT];
146
+ if (layout === undefined) {
147
+ throw new Error(`Slide ${index}: missing required "${RESERVED_KEY.LAYOUT}" in frontmatter`);
148
+ }
149
+ const layoutName = String(layout);
150
+ const layoutDef = layouts.find((l) => l.name === layoutName);
151
+ if (!layoutDef) {
152
+ const known = layouts.map((l) => l.name).join(", ");
153
+ throw new Error(`Slide ${index}: unknown layout "${layoutName}". Available layouts: ${known}`);
154
+ }
155
+ // Map each author-facing key to its owning parameter: template keys → the template
156
+ // parameter that declares them, image keys → the image parameter. validateLayout
157
+ // (run once per layout in compileDeck) has already proven these key spaces are
158
+ // collision-free, so a later lookup is unambiguous.
159
+ const templateParams = layoutDef.parameters.filter(isTemplateParameter);
160
+ const imageByKey = new Map();
161
+ const templateParamByKey = new Map();
162
+ for (const param of layoutDef.parameters) {
163
+ if (isImageParameter(param)) {
164
+ imageByKey.set(param.key, param);
165
+ }
166
+ else {
167
+ for (const key of templateKeys(param.template))
168
+ templateParamByKey.set(key, param);
169
+ }
170
+ }
171
+ const slotsByKey = new Map(layoutDef.slots.map((s) => [s.key, s]));
172
+ const content = {};
173
+ // Frontmatter lines fill image parameters (by key) or template-parameter keys
174
+ // (gathered per parameter, expanded together once every line is read).
175
+ const valuesByTemplateParam = new Map();
176
+ for (const [key, value] of Object.entries(frontmatter)) {
177
+ if (key === RESERVED_KEY.LAYOUT)
178
+ continue;
179
+ const image = imageByKey.get(key);
180
+ if (image) {
181
+ content[image.key] = toImageFill(image, resolveImagePath(rootDir, String(value)));
182
+ continue;
183
+ }
184
+ const templateParam = templateParamByKey.get(key);
185
+ if (templateParam) {
186
+ let bucket = valuesByTemplateParam.get(templateParam);
187
+ if (!bucket) {
188
+ bucket = new Map();
189
+ valuesByTemplateParam.set(templateParam, bucket);
190
+ }
191
+ bucket.set(key, String(value));
192
+ continue;
193
+ }
194
+ const validKeys = [...templateParamByKey.keys(), ...imageByKey.keys()].join(", ");
195
+ throw new Error(`Slide ${index}: unknown key "${key}" in layout "${layoutName}". Valid parameters: ${validKeys}`);
196
+ }
197
+ // Expand each template parameter whose keys were supplied. Filling any key fills the
198
+ // parameter as a whole — a missing key throws (fail-fast in templateToSegments). A
199
+ // parameter with no supplied keys stays designer-sample unless required. Content
200
+ // is keyed by shapeName: one parameter → one entry, regardless of key count.
201
+ for (const templateParam of templateParams) {
202
+ const supplied = valuesByTemplateParam.get(templateParam);
203
+ if (!supplied) {
204
+ if (templateParam.required) {
205
+ throw new Error(`Slide ${index}: layout "${layoutName}" requires template parameter "${templateParam.shapeName}" ` +
206
+ `(keys: ${templateKeys(templateParam.template).join(", ")}); none provided`);
207
+ }
208
+ continue;
209
+ }
210
+ content[templateParam.shapeName] = {
211
+ lines: templateToSegments(templateParam.template, supplied, templateParam.shapeName),
212
+ };
213
+ }
214
+ // The default body region resolves against the layout's body slot.
215
+ if (body.trim()) {
216
+ const bodySlot = slotsByKey.get(RESERVED_KEY.BODY);
217
+ if (!bodySlot) {
218
+ throw new Error(`Slide ${index}: layout "${layoutName}" does not accept body content. Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
219
+ }
220
+ const parsedBody = parseSlotContent(body);
221
+ assertSlotRegion(bodySlot, parsedBody, index, "body content");
222
+ content[RESERVED_KEY.BODY] = parsedBody;
223
+ }
224
+ // `::name::` regions resolve against the layout's slots.
225
+ for (const [name, text] of Object.entries(slots)) {
226
+ const slot = slotsByKey.get(name);
227
+ if (!slot) {
228
+ throw new Error(`Slide ${index}: unknown slot "::${name}::" in layout "${layoutName}". ` +
229
+ `Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
230
+ }
231
+ const block = parseSlotContent(text);
232
+ assertSlotRegion(slot, block, index, `::${name}::`);
233
+ content[name] = block;
234
+ }
235
+ // Required image parameters (missing frontmatter key) and required slots
236
+ // (missing region) throw with layout + key context. Required template parameters are
237
+ // enforced during expansion above.
238
+ for (const image of imageByKey.values()) {
239
+ if (image.required && content[image.key] === undefined) {
240
+ throw new Error(`Slide ${index}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
241
+ }
242
+ }
243
+ for (const slot of layoutDef.slots) {
244
+ if (slot.required && content[slot.key] === undefined) {
245
+ throw new Error(`Slide ${index}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
246
+ }
247
+ }
248
+ // CompilerDeckStep.content values are MarkdownBlock — CodeFence and
249
+ // MermaidFence are legal in transit until the resolvers narrow them into
250
+ // StyledParagraph[] / ImageFill before the engine sees the deck.
251
+ return { layout: layoutName, content };
252
+ }
253
+ const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
254
+ /**
255
+ * Compile a parsed deck document against a set of layouts.
256
+ *
257
+ * `rootDir` (optional) is the base directory for resolving relative image
258
+ * paths declared in the deck's frontmatter or named slots. When omitted (or
259
+ * empty), image paths are returned unchanged — callers that already produce
260
+ * absolute paths (or callers that don't need resolution, e.g. unit tests)
261
+ * can rely on the pass-through. When provided, relative paths are resolved
262
+ * to absolute via `path.resolve(rootDir, path)`; absolute paths pass through.
263
+ */
264
+ export function compileDeck(doc, layouts, rootDir = "") {
265
+ const theme = doc.global[RESERVED_KEY.THEME];
266
+ if (theme === undefined) {
267
+ throw new Error(`Missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
268
+ }
269
+ const unknownGlobal = Object.keys(doc.global).filter((k) => !KNOWN_GLOBAL_KEYS.has(k));
270
+ if (unknownGlobal.length > 0) {
271
+ throw new Error(`Unknown key(s) in global frontmatter: ${unknownGlobal.join(", ")}. Valid keys: ${[...KNOWN_GLOBAL_KEYS].join(", ")}`);
272
+ }
273
+ // Validate every layout's key spaces up front (once per layout), so a broken
274
+ // theme fails fast regardless of which layouts this deck's slides use.
275
+ for (const layout of layouts)
276
+ validateLayout(layout);
277
+ const deck = {
278
+ theme: String(theme),
279
+ steps: doc.slides.map((slide) => compileStep(slide, layouts, rootDir)),
280
+ };
281
+ const output = doc.global[RESERVED_KEY.OUTPUT];
282
+ if (output !== undefined) {
283
+ deck.output = String(output);
284
+ }
285
+ return deck;
286
+ }
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 };
@@ -0,0 +1,13 @@
1
+ import type { CompilerDeck, CompilerLayout } from "./types.js";
2
+ export declare function compileMarkdownDeck(source: string, layouts: CompilerLayout[], rootDir?: string): CompilerDeck;
3
+ export { compileDeck, RESERVED_KEY } from "./deckCompiler.js";
4
+ export { parseGfmTable, parseInlineRuns, parseProseLine, parseStyledParagraph } from "./parsers.js";
5
+ export { CodeResolver, highlightCode, isCodeBlock } from "./resolvers/code.js";
6
+ export { isMermaidBlock, MermaidResolver } from "./resolvers/mermaid.js";
7
+ export type { MermaidConfig, MermaidVariant } from "./resolvers/mermaidTheme.js";
8
+ export type { ResolveContext, Resolver } from "./resolvers/resolver.js";
9
+ export { isFence, RESOLVERS, resolveFences } from "./resolvers/resolver.js";
10
+ export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
+ export { parseSlideDocument } from "./slideParser.js";
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";
@@ -0,0 +1,13 @@
1
+ import { compileDeck } from "./deckCompiler.js";
2
+ import { parseSlideDocument } from "./slideParser.js";
3
+ export function compileMarkdownDeck(source, layouts, rootDir = "") {
4
+ const doc = parseSlideDocument(source);
5
+ return compileDeck(doc, layouts, rootDir);
6
+ }
7
+ export { compileDeck, RESERVED_KEY } from "./deckCompiler.js";
8
+ export { parseGfmTable, parseInlineRuns, parseProseLine, parseStyledParagraph } from "./parsers.js";
9
+ export { CodeResolver, highlightCode, isCodeBlock } from "./resolvers/code.js";
10
+ export { isMermaidBlock, MermaidResolver } from "./resolvers/mermaid.js";
11
+ export { isFence, RESOLVERS, resolveFences } from "./resolvers/resolver.js";
12
+ export { parseSlideDocument } from "./slideParser.js";
13
+ export { CompilerSlotType, FenceType, ParameterType } from "./types.js";