@tycoworks/tycoslide 0.10.0 → 0.11.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.
@@ -33,6 +33,7 @@ import { Automizer, modify } from "pptx-automizer";
33
33
  import { FILLERS } from "./fillers/filler.js";
34
34
  import { isImageFill } from "./fillers/image.js";
35
35
  import { applyNotesToSlide, sweepOrphanNotes } from "./notes.js";
36
+ import { SlotType } from "./types.js";
36
37
  /**
37
38
  * Generate a PPTX file from a deck definition and a theme configuration.
38
39
  *
@@ -46,9 +47,11 @@ import { applyNotesToSlide, sweepOrphanNotes } from "./notes.js";
46
47
  * 4. Write the output PPTX.
47
48
  */
48
49
  export async function generate(deck, config, options = {}) {
49
- const { layouts, rootDir, template, outputDir } = config;
50
- const outFile = deck.output;
51
- const outDir = outputDir ?? process.cwd();
50
+ const { layouts, rootDir, template } = config;
51
+ // deck.output is the absolute output path; automizer takes a dir + a bare
52
+ // filename, so split it. The CLI resolves it next to the deck file.
53
+ const outDir = dirname(deck.output);
54
+ const outFile = basename(deck.output);
52
55
  const automizer = new Automizer({
53
56
  templateDir: resolve(rootDir, "template"),
54
57
  outputDir: outDir,
@@ -98,11 +101,12 @@ export async function generate(deck, config, options = {}) {
98
101
  // onto slides. When true, no notes are written but inherited notes are still
99
102
  // stripped — the deck is guaranteed notes-free.
100
103
  const excludeNotes = options.excludeNotes ?? false;
101
- for (const step of deck.steps) {
104
+ for (const [index, step] of deck.steps.entries()) {
102
105
  const layout = resolveLayout(step.layout);
106
+ const slideNumber = index + 1;
103
107
  pres.addSlide(sourceAlias, layout.baseSlide, (slide) => {
104
108
  captureFillErrors(slide);
105
- fillSlide(slide, layout, step, sourceAlias);
109
+ fillSlide(slide, layout, step, sourceAlias, slideNumber);
106
110
  // In-band notes pass: automizer runs this during write() and hands us the
107
111
  // OUTPUT archive (parent.targetArchive) and the real output slide number
108
112
  // (parent.targetNumber), so notes map 1:1 with no slide-number mapping.
@@ -179,22 +183,39 @@ function describeValue(v) {
179
183
  /**
180
184
  * Fill one cloned slide. Per slot the step supplies a value for: resolve WHICH
181
185
  * shape realizes it (`resolveBlock`), build WHAT to write (`FILLERS[…].callbacks`),
182
- * and place it WHERE/HOW (`applyBlock`). Every ambiguity fails fast, naming layout
183
- * + slot.
186
+ * and place it WHERE/HOW (`applyBlock`). A slot the step leaves unfilled has its
187
+ * base-slide shape removed, so a layout with numbered slots renders exactly the
188
+ * ones supplied. Every ambiguity fails fast, naming layout + slot.
184
189
  *
185
190
  * Exported for tests; `generate()` calls it inside the `addSlide` callback.
186
191
  */
187
- export function fillSlide(slide, layout, step, sourceAlias) {
192
+ export function fillSlide(slide, layout, step, sourceAlias, slideNumber) {
188
193
  assertNoUnknownSlots(step, layout);
189
194
  for (const slot of layout.slots) {
190
195
  const value = step.content?.[slot.key];
191
- if (value === undefined)
192
- continue; // Empty slot: leave the base slide's shape untouched.
196
+ if (value === undefined) {
197
+ // An unfilled slot places no content — remove its base-slide shape (if it
198
+ // has one) so the clone carries nothing there. A transplant-only slot never
199
+ // placed a shape on the clone, so there is nothing to remove.
200
+ const baseBlock = slot.accepts.find((b) => b.sourceSlide === layout.baseSlide);
201
+ if (baseBlock)
202
+ slide.removeElement(baseBlock.shapeName);
203
+ continue;
204
+ }
193
205
  const block = resolveBlock(step, slot, value);
194
- const callbacks = FILLERS[block.type].callbacks(value, targetOf(block));
206
+ const target = targetOf(block, slotLabel(slideNumber, layout, slot));
207
+ const callbacks = FILLERS[block.type].callbacks(value, target);
195
208
  applyBlock(slide, sourceAlias, layout.baseSlide, slot, block, callbacks);
196
209
  }
197
210
  }
211
+ /**
212
+ * The author-facing descriptor a filler prints in advisory warnings, so a
213
+ * message points at `slide 3, layout "Body", slot "photo"` rather than the raw
214
+ * PPTX shape id.
215
+ */
216
+ function slotLabel(slideNumber, layout, slot) {
217
+ return `slide ${slideNumber}, layout "${layout.name}", slot "${slot.key}"`;
218
+ }
198
219
  /** The SlotType a resolved `*Fill` value maps to, via the filler discriminators. */
199
220
  function fillTypeOf(value) {
200
221
  for (const type of Object.keys(FILLERS)) {
@@ -249,12 +270,25 @@ function resolveBlock(step, slot, value) {
249
270
  }
250
271
  return block;
251
272
  }
252
- /** The shape a filler targets, plus `startAt` when the (text) block declares it. */
253
- function targetOf(block) {
254
- const target = { shapeName: block.shapeName };
255
- if (block.startAt !== undefined)
256
- target.startAt = block.startAt;
257
- return target;
273
+ /**
274
+ * The shape a filler targets, plus its diagnostic `label` and the intra-specimen
275
+ * options a block declares: `startAt` (text) and the required `bodyRows` (table).
276
+ * The block's `type` selects the matching target variant, so each filler's
277
+ * callback sees only its own options.
278
+ */
279
+ function targetOf(block, label) {
280
+ switch (block.type) {
281
+ case SlotType.Template:
282
+ return { type: SlotType.Template, shapeName: block.shapeName, label };
283
+ case SlotType.Text:
284
+ return block.startAt !== undefined
285
+ ? { type: SlotType.Text, shapeName: block.shapeName, label, startAt: block.startAt }
286
+ : { type: SlotType.Text, shapeName: block.shapeName, label };
287
+ case SlotType.Table:
288
+ return { type: SlotType.Table, shapeName: block.shapeName, label, bodyRows: block.bodyRows };
289
+ case SlotType.Image:
290
+ return { type: SlotType.Image, shapeName: block.shapeName, label };
291
+ }
258
292
  }
259
293
  /**
260
294
  * WHERE/HOW: place the (already-built) fill callbacks. A base-slide block is
@@ -5,5 +5,5 @@ export { fillTemplate } from "./fillers/template.js";
5
5
  export { fillText, isTextFill } from "./fillers/text.js";
6
6
  export type { GenerateOptions } from "./generate.js";
7
7
  export { generate } from "./generate.js";
8
- export type { Block, Config, Deck, DeckStep, Frame, ImageFill, Layout, Slot, StyledParagraph, TableFill, TemplateFill, TemplateSegment, TextFill, TextRun, ThemeConfig, } from "./types.js";
8
+ export type { Block, BodyRows, Config, Deck, DeckStep, Frame, ImageFill, Layout, Slot, StyledParagraph, TableFill, TemplateFill, TemplateSegment, TextFill, TextRun, ThemeConfig, } from "./types.js";
9
9
  export { ImageFit, SlotType } from "./types.js";
@@ -14,6 +14,15 @@ export declare const SlotType: {
14
14
  readonly Image: "image";
15
15
  };
16
16
  export type SlotType = (typeof SlotType)[keyof typeof SlotType];
17
+ /**
18
+ * The contiguous range of a table specimen's rows that repeat, as `[start, end]`
19
+ * (0-based, inclusive). Row 0 is always the header; rows `[1, start-1]` are top
20
+ * fixed rows rendered once (e.g. a distinctly-styled under-header row); rows
21
+ * `[start, end]` are the repeatable body, cycled to fill the deck's data; rows
22
+ * `[end+1, R-1]` are bottom fixed rows rendered once (e.g. a decorated total row).
23
+ * Only the body range repeats; every row outside it is rendered once, in place.
24
+ */
25
+ export type BodyRows = [number, number];
17
26
  /** A single styled span of text within a paragraph. */
18
27
  export type TextRun = {
19
28
  text: string;
@@ -102,23 +111,43 @@ export type Frame = {
102
111
  };
103
112
  /**
104
113
  * A kind of content a slot accepts, and the real template shape that realizes
105
- * it. `type` is the fill-strategy discriminator; `shapeName` names the shape on
106
- * `sourceSlide` that carries the specimen styling. When `sourceSlide` equals the
107
- * layout's `baseSlide` the shape is already on the cloned slide (fill in place);
108
- * otherwise the shape is transplanted from `sourceSlide` into the slot's frame.
109
- * `startAt` is a text-specimen concern (leave the first N specimen paragraphs
110
- * untouched) and only meaningful on a text block.
114
+ * it — a union discriminated by `type` (the fill-strategy selector), so each
115
+ * variant carries only its own specimen options. `shapeName` names the shape on
116
+ * `sourceSlide` that carries the specimen styling; when `sourceSlide` equals the
117
+ * layout's `baseSlide` the shape is already on the cloned slide (fill in place),
118
+ * otherwise it is transplanted from `sourceSlide` into the slot's frame. A text
119
+ * block may pin `startAt` (leave the first N specimen paragraphs untouched); a
120
+ * table block MUST declare its `bodyRows` range (the repeatable specimen rows);
121
+ * a template or image block carries neither. `Template` reaches the engine only
122
+ * via a frontmatter parameter (`paramToEngineSlot`), never as an author body block.
111
123
  *
112
- * Named `Block` — a kind of content (image / table / text) the way an author
113
- * thinks of it. Distinct from the compiler's `MarkdownBlock` (a parsed markdown
114
- * block); different layer, kept separate on purpose.
124
+ * Named `Block` — a kind of content the way an author thinks of it. Distinct
125
+ * from the compiler's `MarkdownBlock` (a parsed markdown block); different layer,
126
+ * kept separate on purpose.
115
127
  */
116
- export type Block = {
117
- type: SlotType;
128
+ export type TemplateBlock = {
129
+ type: typeof SlotType.Template;
130
+ sourceSlide: number;
131
+ shapeName: string;
132
+ };
133
+ export type TextBlock = {
134
+ type: typeof SlotType.Text;
118
135
  sourceSlide: number;
119
136
  shapeName: string;
120
137
  startAt?: number;
121
138
  };
139
+ export type TableBlock = {
140
+ type: typeof SlotType.Table;
141
+ sourceSlide: number;
142
+ shapeName: string;
143
+ bodyRows: BodyRows;
144
+ };
145
+ export type ImageBlock = {
146
+ type: typeof SlotType.Image;
147
+ sourceSlide: number;
148
+ shapeName: string;
149
+ };
150
+ export type Block = TemplateBlock | TextBlock | TableBlock | ImageBlock;
122
151
  /**
123
152
  * An author-facing fill region. Not welded to one shape+type: a slot owns its
124
153
  * `frame` and `accepts` a set of `Block`s; the supplied value's shape selects
@@ -147,14 +176,13 @@ export type DeckStep = {
147
176
  };
148
177
  export type Deck = {
149
178
  theme: string;
150
- /** Output filename. Required — no silent default. */
179
+ /** Absolute output path for the .pptx. Required — no silent default. */
151
180
  output: string;
152
181
  steps: DeckStep[];
153
182
  };
154
183
  export type ThemeConfig = {
155
184
  layouts: Layout[];
156
185
  template: string;
157
- outputDir?: string;
158
186
  };
159
187
  export type Config = ThemeConfig & {
160
188
  rootDir: string;
package/dist/index.d.ts CHANGED
@@ -28,13 +28,12 @@ export declare function toEngineConfig(config: CompilerConfig): Config;
28
28
  * populates it before calling `buildDeck`; a programmatic caller that forgot to
29
29
  * set it hits this error instead of a confusing engine-side failure.
30
30
  *
31
- * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
32
- * post-write cleanup is needed.
31
+ * Mermaid PNGs are cached under `<rootDir>/.tycoslide-cache/mermaid/` (the theme
32
+ * directory) so no post-write cleanup is needed.
33
33
  */
34
34
  export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig, options?: GenerateOptions): Promise<void>;
35
35
  export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
36
36
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
37
- export type { ManifestOptions } from "./manifest.js";
38
37
  export { generateManifest } from "./manifest.js";
39
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, Limit, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
38
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
40
39
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { generate, SlotType, } from "./engine/index.js";
2
- import { ParameterType, } from "./markdown/types.js";
2
+ import { AcceptType, ParameterType, } from "./markdown/types.js";
3
3
  /**
4
4
  * A frontmatter parameter always fills one physical shape on the layout's own
5
5
  * slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
@@ -8,15 +8,22 @@ import { ParameterType, } from "./markdown/types.js";
8
8
  */
9
9
  const NO_FRAME = { x: 0, y: 0, cx: 0, cy: 0 };
10
10
  function paramToEngineSlot(param, baseSlide) {
11
- const block = (type) => ({ type, sourceSlide: baseSlide, shapeName: param.shapeName });
12
11
  switch (param.type) {
13
12
  case ParameterType.Template:
14
13
  // A text shape carries no top-level key — its template placeholders are the keys. The
15
14
  // compiler emits its expanded content under shapeName, so the engine slot
16
15
  // is keyed by shapeName too.
17
- return { key: param.shapeName, frame: NO_FRAME, accepts: [block(SlotType.Template)] };
16
+ return {
17
+ key: param.shapeName,
18
+ frame: NO_FRAME,
19
+ accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
20
+ };
18
21
  case ParameterType.Image:
19
- return { key: param.key, frame: NO_FRAME, accepts: [block(SlotType.Image)] };
22
+ return {
23
+ key: param.key,
24
+ frame: NO_FRAME,
25
+ accepts: [{ type: SlotType.Image, sourceSlide: baseSlide, shapeName: param.shapeName }],
26
+ };
20
27
  }
21
28
  }
22
29
  /**
@@ -26,14 +33,21 @@ function paramToEngineSlot(param, baseSlide) {
26
33
  * so projection is 1:1. A slot with no declared `frame` (a base-only slot that
27
34
  * never transplants) gets `NO_FRAME`, which the engine never reads.
28
35
  */
36
+ /** Project one CompilerBlock variant onto its matching engine `Block` variant. */
37
+ function compilerBlockToEngineBlock(b) {
38
+ switch (b.type) {
39
+ case AcceptType.Text:
40
+ return b.startAt !== undefined
41
+ ? { type: SlotType.Text, sourceSlide: b.sourceSlide, shapeName: b.shapeName, startAt: b.startAt }
42
+ : { type: SlotType.Text, sourceSlide: b.sourceSlide, shapeName: b.shapeName };
43
+ case AcceptType.Table:
44
+ return { type: SlotType.Table, sourceSlide: b.sourceSlide, shapeName: b.shapeName, bodyRows: b.bodyRows };
45
+ case AcceptType.Image:
46
+ return { type: SlotType.Image, sourceSlide: b.sourceSlide, shapeName: b.shapeName };
47
+ }
48
+ }
29
49
  function slotToEngineSlot(slot) {
30
- const accepts = slot.accepts.map((b) => {
31
- const eb = { type: b.type, sourceSlide: b.sourceSlide, shapeName: b.shapeName };
32
- if (b.startAt !== undefined)
33
- eb.startAt = b.startAt;
34
- return eb;
35
- });
36
- return { key: slot.key, frame: slot.frame ?? NO_FRAME, accepts };
50
+ return { key: slot.key, frame: slot.frame ?? NO_FRAME, accepts: slot.accepts.map(compilerBlockToEngineBlock) };
37
51
  }
38
52
  /**
39
53
  * Compiler→engine boundary check: a slot with a transplant block (any block
@@ -74,8 +88,6 @@ export function toEngineThemeConfig(config) {
74
88
  layouts: config.layouts.map(toEngineLayout),
75
89
  template: config.template,
76
90
  };
77
- if (config.outputDir !== undefined)
78
- result.outputDir = config.outputDir;
79
91
  return result;
80
92
  }
81
93
  /**
@@ -102,8 +114,8 @@ export function toEngineConfig(config) {
102
114
  * populates it before calling `buildDeck`; a programmatic caller that forgot to
103
115
  * set it hits this error instead of a confusing engine-side failure.
104
116
  *
105
- * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
106
- * post-write cleanup is needed.
117
+ * Mermaid PNGs are cached under `<rootDir>/.tycoslide-cache/mermaid/` (the theme
118
+ * directory) so no post-write cleanup is needed.
107
119
  */
108
120
  export async function buildDeck(deck, config, options = {}) {
109
121
  if (deck.output === undefined) {
@@ -114,6 +126,7 @@ export async function buildDeck(deck, config, options = {}) {
114
126
  }
115
127
  // Engine — primitives-only public surface.
116
128
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
129
+ // Authoring
117
130
  export { generateManifest } from "./manifest.js";
118
131
  // Markdown / Compiler
119
132
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
@@ -1,7 +1,2 @@
1
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;
2
+ export declare function generateManifest(config: CompilerConfig): string;
package/dist/manifest.js CHANGED
@@ -5,11 +5,6 @@ import { ParameterType } from "./markdown/types.js";
5
5
  * A template parameter has no top-level key — its template's keys are the keys, so it
6
6
  * flattens to one entry per key (shapeName/template stay manifest-internal). An
7
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
8
  */
14
9
  function stripParameter(param) {
15
10
  switch (param.type) {
@@ -32,11 +27,9 @@ function stripSlot(slot) {
32
27
  const result = { key: slot.key, accepts: slot.accepts.map((b) => b.type) };
33
28
  if (slot.required)
34
29
  result.required = true;
35
- if (slot.limit)
36
- result.limit = slot.limit;
37
30
  return result;
38
31
  }
39
- export function generateManifest(config, options) {
32
+ export function generateManifest(config) {
40
33
  const layouts = config.layouts.map((layout) => {
41
34
  const ml = {
42
35
  name: layout.name,
@@ -46,10 +39,6 @@ export function generateManifest(config, options) {
46
39
  };
47
40
  if (layout.description !== undefined)
48
41
  ml.description = layout.description;
49
- if (layout.whenToUse !== undefined)
50
- ml.whenToUse = layout.whenToUse;
51
- if (layout.whenNotToUse !== undefined)
52
- ml.whenNotToUse = layout.whenNotToUse;
53
42
  return ml;
54
43
  });
55
44
  const assets = {};
@@ -61,8 +50,6 @@ export function generateManifest(config, options) {
61
50
  type: entry.type,
62
51
  description: entry.description,
63
52
  };
64
- if (entry.whenToUse)
65
- manifestEntry.whenToUse = entry.whenToUse;
66
53
  assets[category][name] = manifestEntry;
67
54
  }
68
55
  }
@@ -70,9 +57,6 @@ export function generateManifest(config, options) {
70
57
  version: 1,
71
58
  layouts,
72
59
  assets,
73
- build: {
74
- command: options.build.command,
75
- },
76
60
  };
77
61
  return JSON.stringify(manifest, null, 2);
78
62
  }
@@ -6,7 +6,7 @@ export declare const MERMAID_LANG = "mermaid";
6
6
  /**
7
7
  * Recognize a ```mermaid fenced block at a region's top level, folding it to an
8
8
  * Image fill, and compile it by rendering the definition to a PNG (cached under
9
- * `<outputDir>/.tycoslide-cache/mermaid/<hash>.png`) and wrapping it as an
9
+ * `<rootDir>/.tycoslide-cache/mermaid/<hash>.png`, in the theme directory) and wrapping it as an
10
10
  * ImageFill. Fit is always `contain` — mermaid diagrams are shown in their
11
11
  * entirety. Resolution is strict: the theme MUST carry a `mermaid` block, MUST
12
12
  * declare a `mermaidVariant`, and that variant MUST exist — each missing piece
@@ -14,7 +14,7 @@ export const MERMAID_LANG = "mermaid";
14
14
  /**
15
15
  * Recognize a ```mermaid fenced block at a region's top level, folding it to an
16
16
  * Image fill, and compile it by rendering the definition to a PNG (cached under
17
- * `<outputDir>/.tycoslide-cache/mermaid/<hash>.png`) and wrapping it as an
17
+ * `<rootDir>/.tycoslide-cache/mermaid/<hash>.png`, in the theme directory) and wrapping it as an
18
18
  * ImageFill. Fit is always `contain` — mermaid diagrams are shown in their
19
19
  * entirety. Resolution is strict: the theme MUST carry a `mermaid` block, MUST
20
20
  * declare a `mermaidVariant`, and that variant MUST exist — each missing piece
@@ -69,7 +69,7 @@ function hashKey(definition, variantName, renderConfig, fonts) {
69
69
  return hash.digest("hex").slice(0, 16);
70
70
  }
71
71
  function ensureCacheDir(config) {
72
- const base = resolve(config.outputDir ?? process.cwd(), ".tycoslide-cache", "mermaid");
72
+ const base = resolve(config.rootDir, ".tycoslide-cache", "mermaid");
73
73
  mkdirSync(base, { recursive: true });
74
74
  return base;
75
75
  }
@@ -198,7 +198,7 @@ ${fontFaceCss(fonts)}
198
198
  </body></html>`;
199
199
  // Serve the page from a real file:// URL, not setContent — an about:blank
200
200
  // origin can't fetch the file:// font resources (@font-face silently fails),
201
- // whereas a file://-origin document loads them. Mirrors the old harness.
201
+ // whereas a file://-origin document loads them.
202
202
  const htmlPath = `${outputPath}.html`;
203
203
  const { chromium } = await import("playwright");
204
204
  let browser;
@@ -16,7 +16,7 @@ function compileTable(node) {
16
16
  };
17
17
  }
18
18
  /** One table cell → a StyledParagraph. An empty cell keeps a single empty run so
19
- * downstream code sees a run to style, matching the old `parseInlineRuns("")`. */
19
+ * downstream code always sees a run to style. */
20
20
  function cellParagraph(cell) {
21
21
  const runs = walkPhrasingChildren(cell.children, {});
22
22
  return { runs: runs.length > 0 ? runs : [{ text: "" }] };
@@ -64,12 +64,11 @@ function listParagraphs(list, level) {
64
64
  return out;
65
65
  }
66
66
  /**
67
- * Split a paragraph's runs on newlines into one `TextRun[]` per source line
68
- * preserving the pre-mdast "one source line = one StyledParagraph" behavior.
69
- * Both a soft break (`\n` inside a text run) and a markdown hard break (a
70
- * `break` node the inline walk emits as a `"\n"` run in block context) split
71
- * here. Empty segments (a style boundary landing on a line edge) are dropped,
72
- * and empty lines produce no paragraph, matching the old blank-line filter.
67
+ * Split a paragraph's runs on newlines into one `TextRun[]` per source line, so
68
+ * one source line becomes one StyledParagraph. Both a soft break (`\n` inside a
69
+ * text run) and a markdown hard break (a `break` node the inline walk emits as a
70
+ * `"\n"` run in block context) split here. Empty segments (a style boundary
71
+ * landing on a line edge) are dropped, and empty lines produce no paragraph.
73
72
  */
74
73
  function splitRunsIntoParagraphs(runs) {
75
74
  const lines = [];
@@ -18,7 +18,7 @@ export declare function toImageFill(path: string, type: AssetType): ImageFill;
18
18
  * returned unchanged — callers that already produce absolute paths (or don't need
19
19
  * resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
20
20
  * are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
21
- * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` /
22
- * `config.outputDir` feed the code and mermaid compiles.
21
+ * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` feed
22
+ * the code and mermaid compiles.
23
23
  */
24
24
  export declare function compileDeck(doc: ParsedDocument, config: CompilerConfig): Promise<CompilerDeck>;
@@ -18,7 +18,7 @@ const FIT_FOR = {
18
18
  export function toImageFill(path, type) {
19
19
  return { type: SlotType.Image, path, fit: FIT_FOR[type] };
20
20
  }
21
- const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
21
+ const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME]);
22
22
  // Anchored whole-field reference: the entire value is `$category.name` or it is
23
23
  // not a reference at all. Anchored ⇒ no escaping concerns.
24
24
  const ASSET_REF_RE = /^\$([a-zA-Z]\w*)\.([a-zA-Z]\w*)$/;
@@ -50,8 +50,8 @@ function resolveImagePath(rootDir, path) {
50
50
  /**
51
51
  * Assert no two layouts sample the same base slide. In the sampled-composition
52
52
  * model one layout is one sampled base slide's region-arrangement, so a shared
53
- * `slideNumber` means the old single-content-type welding leaked through two
54
- * layouts on one physical slide that should be one multi-`accepts` layout.
53
+ * `slideNumber` means two layouts on one physical slide that should be one
54
+ * multi-`accepts` layout.
55
55
  * `sourceSlide` inside a slot's `accepts` block is a distinct concept (a
56
56
  * specimen source for one content type) and is legitimately reused across
57
57
  * layouts, so it is deliberately not checked here.
@@ -123,7 +123,7 @@ function validateLayout(layout) {
123
123
  }
124
124
  async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
125
125
  const { layouts, rootDir } = config;
126
- const { frontmatter, body, slots, index } = slide;
126
+ const { frontmatter, slots, index } = slide;
127
127
  const layout = frontmatter[RESERVED_KEY.LAYOUT];
128
128
  if (layout === undefined) {
129
129
  throw new Error(`Slide ${index}: missing required "${RESERVED_KEY.LAYOUT}" in frontmatter`);
@@ -211,26 +211,6 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
211
211
  lines: templateToSegments(templateParam.template, supplied, templateParam.shapeName),
212
212
  };
213
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
- resolveAssetRef,
222
- layoutName,
223
- slideIdx: index,
224
- source: "body content",
225
- config,
226
- layoutVariant: layoutDef.variant,
227
- });
228
- // Validate the slot accepts this region's type BEFORE running the (possibly
229
- // expensive — Shiki, Playwright) fill: a mismatched region fails fast without
230
- // spinning up a renderer.
231
- assertSlotRegion(bodySlot, parsedBody.acceptType, layoutName, index, "body content");
232
- content[RESERVED_KEY.BODY] = await parsedBody.fill();
233
- }
234
214
  // `::name::` regions resolve against the layout's slots.
235
215
  for (const [name, text] of Object.entries(slots)) {
236
216
  const slot = slotsByKey.get(name);
@@ -281,8 +261,8 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
281
261
  * returned unchanged — callers that already produce absolute paths (or don't need
282
262
  * resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
283
263
  * are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
284
- * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` /
285
- * `config.outputDir` feed the code and mermaid compiles.
264
+ * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` feed
265
+ * the code and mermaid compiles.
286
266
  */
287
267
  export async function compileDeck(doc, config) {
288
268
  const { layouts, rootDir, assets } = config;
@@ -333,10 +313,5 @@ export async function compileDeck(doc, config) {
333
313
  for (const slide of doc.slides) {
334
314
  steps.push(await compileStep(slide, config, assetTypeByPath, resolveAssetRef));
335
315
  }
336
- const deck = { theme: String(theme), steps };
337
- const output = doc.global[RESERVED_KEY.OUTPUT];
338
- if (output !== undefined) {
339
- deck.output = String(output);
340
- }
341
- return deck;
316
+ return { theme: String(theme), steps };
342
317
  }
@@ -9,5 +9,5 @@ export { parseRegion } from "./mdast.js";
9
9
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
10
10
  export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
11
  export { parseSlideDocument } from "./slideParser.js";
12
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerParameter, CompilerSlot, CompilerTemplateParameter, CompilerThemeConfig, EngineFill, Limit, } from "./types.js";
12
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerParameter, CompilerSlot, CompilerTemplateParameter, CompilerThemeConfig, EngineFill, } from "./types.js";
13
13
  export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
@@ -15,8 +15,8 @@ import type { CompilerLayout } from "../types.js";
15
15
  * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
16
16
  * "all a template's keys present"). Value-typing and required-encoding are deliberate
17
17
  * later commits.
18
- * The strict object IS the unknown-key check that used to be imperative in
19
- * `compileStep`; a stray frontmatter key throws instead of being silently ignored.
18
+ * The strict object IS the unknown-key check: a stray frontmatter key throws
19
+ * instead of being silently ignored.
20
20
  */
21
21
  export declare function deckFrontmatterSchema(layout: CompilerLayout): z.ZodObject<{
22
22
  [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
@@ -17,8 +17,8 @@ import { strict } from "./strict.js";
17
17
  * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
18
18
  * "all a template's keys present"). Value-typing and required-encoding are deliberate
19
19
  * later commits.
20
- * The strict object IS the unknown-key check that used to be imperative in
21
- * `compileStep`; a stray frontmatter key throws instead of being silently ignored.
20
+ * The strict object IS the unknown-key check: a stray frontmatter key throws
21
+ * instead of being silently ignored.
22
22
  */
23
23
  export function deckFrontmatterSchema(layout) {
24
24
  const shape = {};
@@ -5,19 +5,12 @@ export declare const ThemeConfigSchema: z.ZodObject<{
5
5
  name: z.ZodString;
6
6
  slideNumber: z.ZodNumber;
7
7
  description: z.ZodOptional<z.ZodString>;
8
- whenToUse: z.ZodOptional<z.ZodString>;
9
- whenNotToUse: z.ZodOptional<z.ZodString>;
10
8
  variant: z.ZodOptional<z.ZodEnum<{
11
9
  light: "light";
12
10
  dark: "dark";
13
11
  }>>;
14
12
  parameters: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
15
13
  shapeName: z.ZodString;
16
- limit: z.ZodOptional<z.ZodObject<{
17
- maxChars: z.ZodOptional<z.ZodNumber>;
18
- maxLines: z.ZodOptional<z.ZodNumber>;
19
- maxItems: z.ZodOptional<z.ZodNumber>;
20
- }, z.core.$strict>>;
21
14
  required: z.ZodOptional<z.ZodBoolean>;
22
15
  type: z.ZodLiteral<"template">;
23
16
  template: z.ZodString;
@@ -29,27 +22,27 @@ export declare const ThemeConfigSchema: z.ZodObject<{
29
22
  }, z.core.$strict>], "type">>;
30
23
  slots: z.ZodArray<z.ZodObject<{
31
24
  key: z.ZodString;
32
- accepts: z.ZodArray<z.ZodObject<{
33
- type: z.ZodEnum<{
34
- text: "text";
35
- table: "table";
36
- image: "image";
37
- }>;
25
+ accepts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
26
+ type: z.ZodLiteral<"text">;
38
27
  sourceSlide: z.ZodNumber;
39
28
  shapeName: z.ZodString;
40
29
  startAt: z.ZodOptional<z.ZodNumber>;
41
- }, z.core.$strict>>;
30
+ }, z.core.$strict>, z.ZodObject<{
31
+ type: z.ZodLiteral<"table">;
32
+ sourceSlide: z.ZodNumber;
33
+ shapeName: z.ZodString;
34
+ bodyRows: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
35
+ }, z.core.$strict>, z.ZodObject<{
36
+ type: z.ZodLiteral<"image">;
37
+ sourceSlide: z.ZodNumber;
38
+ shapeName: z.ZodString;
39
+ }, z.core.$strict>], "type">>;
42
40
  frame: z.ZodOptional<z.ZodObject<{
43
41
  x: z.ZodNumber;
44
42
  y: z.ZodNumber;
45
43
  cx: z.ZodNumber;
46
44
  cy: z.ZodNumber;
47
45
  }, z.core.$strict>>;
48
- limit: z.ZodOptional<z.ZodObject<{
49
- maxChars: z.ZodOptional<z.ZodNumber>;
50
- maxLines: z.ZodOptional<z.ZodNumber>;
51
- maxItems: z.ZodOptional<z.ZodNumber>;
52
- }, z.core.$strict>>;
53
46
  required: z.ZodOptional<z.ZodBoolean>;
54
47
  }, z.core.$strict>>;
55
48
  }, z.core.$strict>>;
@@ -61,10 +54,8 @@ export declare const ThemeConfigSchema: z.ZodObject<{
61
54
  background: "background";
62
55
  }>;
63
56
  description: z.ZodString;
64
- whenToUse: z.ZodOptional<z.ZodString>;
65
57
  }, z.core.$strict>>>;
66
58
  template: z.ZodString;
67
- outputDir: z.ZodOptional<z.ZodString>;
68
59
  mermaid: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
69
60
  primary: z.ZodString;
70
61
  primaryContrast: z.ZodString;