@tycoworks/tycoslide 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +5 -15
- package/dist/engine/fillers/filler.d.ts +21 -13
- package/dist/engine/fillers/filler.js +21 -24
- package/dist/engine/generate.d.ts +23 -17
- package/dist/engine/generate.js +157 -70
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/types.d.ts +39 -9
- package/dist/index.d.ts +15 -21
- package/dist/index.js +63 -88
- package/dist/manifest.js +17 -25
- package/dist/markdown/blocks/code.d.ts +15 -0
- package/dist/markdown/blocks/code.js +50 -0
- package/dist/markdown/blocks/image.d.ts +2 -0
- package/dist/markdown/blocks/image.js +9 -0
- package/dist/markdown/blocks/mermaid.d.ts +15 -0
- package/dist/markdown/blocks/mermaid.js +227 -0
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
- package/dist/markdown/blocks/registry.d.ts +16 -0
- package/dist/markdown/blocks/registry.js +44 -0
- package/dist/markdown/blocks/table.d.ts +2 -0
- package/dist/markdown/blocks/table.js +23 -0
- package/dist/markdown/blocks/text.d.ts +12 -0
- package/dist/markdown/blocks/text.js +90 -0
- package/dist/markdown/deckCompiler.d.ts +17 -20
- package/dist/markdown/deckCompiler.js +135 -113
- package/dist/markdown/index.d.ts +11 -11
- package/dist/markdown/index.js +9 -8
- package/dist/markdown/inline.d.ts +26 -0
- package/dist/markdown/inline.js +136 -0
- package/dist/markdown/mdast.d.ts +25 -0
- package/dist/markdown/mdast.js +49 -0
- package/dist/markdown/schema/deckSchema.d.ts +30 -0
- package/dist/markdown/schema/deckSchema.js +51 -0
- package/dist/markdown/schema/strict.d.ts +9 -0
- package/dist/markdown/schema/strict.js +18 -0
- package/dist/markdown/schema/themeConfigSchema.d.ts +99 -0
- package/dist/markdown/schema/themeConfigSchema.js +145 -0
- package/dist/markdown/types.d.ts +168 -135
- package/dist/markdown/types.js +23 -23
- package/package.json +6 -3
- package/dist/markdown/parsers.d.ts +0 -32
- package/dist/markdown/parsers.js +0 -233
- package/dist/markdown/resolvers/code.d.ts +0 -17
- package/dist/markdown/resolvers/code.js +0 -44
- package/dist/markdown/resolvers/mermaid.d.ts +0 -14
- package/dist/markdown/resolvers/mermaid.js +0 -81
- package/dist/markdown/resolvers/resolver.d.ts +0 -42
- package/dist/markdown/resolvers/resolver.js +0 -52
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { buildDeck } from "./index.js";
|
|
6
6
|
import { generateManifest } from "./manifest.js";
|
|
7
|
-
import { compileDeck, parseSlideDocument, RESERVED_KEY
|
|
7
|
+
import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
|
|
8
8
|
const DEFAULT_CONFIG = "theme.json";
|
|
9
9
|
const SKILL_DIR = "skills/slides";
|
|
10
10
|
const PLUGIN_DIR = ".claude-plugin";
|
|
@@ -16,16 +16,6 @@ const BUILD_COMMAND = "npx tycoslide build";
|
|
|
16
16
|
const sdkDir = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
|
|
18
18
|
const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
|
|
19
|
-
function loadConfig(absPath) {
|
|
20
|
-
let raw;
|
|
21
|
-
try {
|
|
22
|
-
raw = JSON.parse(readFileSync(absPath, "utf-8"));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
throw new Error(`Config file not found or invalid JSON: ${absPath}`);
|
|
26
|
-
}
|
|
27
|
-
return { ...raw, rootDir: dirname(absPath) };
|
|
28
|
-
}
|
|
29
19
|
const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
|
|
30
20
|
const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
|
|
31
21
|
program
|
|
@@ -53,8 +43,8 @@ program
|
|
|
53
43
|
if (!absConfigPath) {
|
|
54
44
|
throw new Error(`${basename(deckPath)}: missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
|
|
55
45
|
}
|
|
56
|
-
const config =
|
|
57
|
-
const deck = compileDeck(doc, config
|
|
46
|
+
const config = loadThemeConfig(absConfigPath);
|
|
47
|
+
const deck = await compileDeck(doc, config);
|
|
58
48
|
if (!deck.output)
|
|
59
49
|
deck.output = basename(deckPath).replace(/\.md$/, ".pptx");
|
|
60
50
|
await buildDeck(deck, config, { excludeNotes: !opts.notes });
|
|
@@ -65,7 +55,7 @@ program
|
|
|
65
55
|
.option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
|
|
66
56
|
.option(`-o, --out <file>`, "write to file instead of stdout")
|
|
67
57
|
.action(async (opts) => {
|
|
68
|
-
const config =
|
|
58
|
+
const config = loadThemeConfig(resolve(process.cwd(), opts.config));
|
|
69
59
|
const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
|
|
70
60
|
if (opts.out) {
|
|
71
61
|
writeFileSync(resolve(process.cwd(), opts.out), `${json}\n`);
|
|
@@ -80,7 +70,7 @@ program
|
|
|
80
70
|
.description("Generate plugin package (plugin.json, manifest.json, SKILL.md, syntax.md) for AI agents")
|
|
81
71
|
.option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
|
|
82
72
|
.action(async (opts) => {
|
|
83
|
-
const config =
|
|
73
|
+
const config = loadThemeConfig(resolve(process.cwd(), opts.config));
|
|
84
74
|
const cwd = process.cwd();
|
|
85
75
|
const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
|
|
86
76
|
const pluginMeta = {
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `Filler` strategy registry — one plain-object strategy per SlotType, each
|
|
3
|
-
* pairing a value discriminator with
|
|
4
|
-
* slot type, so a strategy carries no redundant
|
|
5
|
-
*
|
|
6
|
-
* value shape, then `fill` applies it to the slide.
|
|
3
|
+
* pairing a value discriminator with the element-level modify callbacks that
|
|
4
|
+
* apply it. The record key IS the slot type, so a strategy carries no redundant
|
|
5
|
+
* `type` field.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `callbacks(value, target)` returns the `(element, relation)` callbacks that
|
|
8
|
+
* fill one shape. They are deliberately shape-name-agnostic beyond the `target`
|
|
9
|
+
* so they can be applied two ways: `slide.modifyElement(name, callbacks)` for a
|
|
10
|
+
* shape already on the cloned base slide, or `slide.addElement(alias, n, name,
|
|
11
|
+
* callbacks)` for a shape transplanted from another slide — pptx-automizer runs
|
|
12
|
+
* an appended shape's callbacks against the imported element itself, so the same
|
|
13
|
+
* callbacks refill a transplant.
|
|
14
|
+
*
|
|
15
|
+
* Element-level geometry lives in the `fillX` primitives; cross-shape concerns
|
|
16
|
+
* (media pre-swap for images) live in the callbacks here.
|
|
11
17
|
*/
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
import { SlotType } from "../types.js";
|
|
19
|
+
/** The shape a filler targets, plus its slot-level options (startAt for text). */
|
|
20
|
+
export type FillTarget = {
|
|
21
|
+
shapeName: string;
|
|
22
|
+
startAt?: number;
|
|
15
23
|
};
|
|
24
|
+
/** A pptx-automizer element-modify callback: `(element, relation) => void`. */
|
|
25
|
+
export type ShapeCallback = (element: any, relation: any) => unknown;
|
|
16
26
|
export interface Filler<T> {
|
|
17
27
|
matches(v: unknown): v is T;
|
|
18
|
-
|
|
19
|
-
label: string;
|
|
20
|
-
fill(slide: any, slot: Slot, value: T, ctx: FillContext): void;
|
|
28
|
+
callbacks(value: T, target: FillTarget): ShapeCallback[];
|
|
21
29
|
}
|
|
22
30
|
export declare const FILLERS: Record<SlotType, Filler<any>>;
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `Filler` strategy registry — one plain-object strategy per SlotType, each
|
|
3
|
-
* pairing a value discriminator with
|
|
4
|
-
* slot type, so a strategy carries no redundant
|
|
5
|
-
*
|
|
6
|
-
* value shape, then `fill` applies it to the slide.
|
|
3
|
+
* pairing a value discriminator with the element-level modify callbacks that
|
|
4
|
+
* apply it. The record key IS the slot type, so a strategy carries no redundant
|
|
5
|
+
* `type` field.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `callbacks(value, target)` returns the `(element, relation)` callbacks that
|
|
8
|
+
* fill one shape. They are deliberately shape-name-agnostic beyond the `target`
|
|
9
|
+
* so they can be applied two ways: `slide.modifyElement(name, callbacks)` for a
|
|
10
|
+
* shape already on the cloned base slide, or `slide.addElement(alias, n, name,
|
|
11
|
+
* callbacks)` for a shape transplanted from another slide — pptx-automizer runs
|
|
12
|
+
* an appended shape's callbacks against the imported element itself, so the same
|
|
13
|
+
* callbacks refill a transplant.
|
|
14
|
+
*
|
|
15
|
+
* Element-level geometry lives in the `fillX` primitives; cross-shape concerns
|
|
16
|
+
* (media pre-swap for images) live in the callbacks here.
|
|
11
17
|
*/
|
|
12
18
|
import { basename } from "node:path";
|
|
13
19
|
import { ModifyImageHelper } from "pptx-automizer";
|
|
@@ -19,32 +25,23 @@ import { fillText, isTextFill } from "./text.js";
|
|
|
19
25
|
export const FILLERS = {
|
|
20
26
|
[SlotType.Template]: {
|
|
21
27
|
matches: isTemplateFill,
|
|
22
|
-
|
|
23
|
-
fill: (slide, slot, v) => slide.modifyElement(slot.shapeName, [(el) => fillTemplate(el, v, slot.shapeName)]),
|
|
28
|
+
callbacks: (v, t) => [(el) => fillTemplate(el, v, t.shapeName)],
|
|
24
29
|
},
|
|
25
30
|
[SlotType.Text]: {
|
|
26
31
|
matches: isTextFill,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
slide.modifyElement(slot.shapeName, [
|
|
31
|
-
(el, relation) => fillText(el, v, { startAt, relation, shapeName: slot.shapeName }),
|
|
32
|
-
]);
|
|
33
|
-
},
|
|
32
|
+
callbacks: (v, t) => [
|
|
33
|
+
(el, relation) => fillText(el, v, { startAt: t.startAt ?? 0, relation, shapeName: t.shapeName }),
|
|
34
|
+
],
|
|
34
35
|
},
|
|
35
36
|
[SlotType.Table]: {
|
|
36
37
|
matches: isTableFill,
|
|
37
|
-
|
|
38
|
-
fill: (slide, slot, v) => {
|
|
39
|
-
slide.modifyElement(slot.shapeName, [(el) => fillTable(el, v, slot.shapeName)]);
|
|
40
|
-
},
|
|
38
|
+
callbacks: (v, t) => [(el) => fillTable(el, v, t.shapeName)],
|
|
41
39
|
},
|
|
42
40
|
[SlotType.Image]: {
|
|
43
41
|
matches: isImageFill,
|
|
44
|
-
|
|
45
|
-
fill: (slide, slot, v) => slide.modifyElement(slot.shapeName, [
|
|
42
|
+
callbacks: (v, t) => [
|
|
46
43
|
ModifyImageHelper.setRelationTarget(basename(v.path)),
|
|
47
|
-
(el) => fillImage(el, v,
|
|
48
|
-
]
|
|
44
|
+
(el) => fillImage(el, v, t.shapeName),
|
|
45
|
+
],
|
|
49
46
|
},
|
|
50
47
|
};
|
|
@@ -18,11 +18,15 @@
|
|
|
18
18
|
* file (registered by generate(), swapped by the ImageFiller) and adjusts
|
|
19
19
|
* geometry for the chosen fit. See fillers/image.ts.
|
|
20
20
|
*
|
|
21
|
-
* generate() loads the template, registers media, and for each DeckStep
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
21
|
+
* generate() loads the template, registers media, and for each DeckStep clones
|
|
22
|
+
* the layout's base slide and calls `fillSlide`. Each value in `step.content`
|
|
23
|
+
* selects, by its own shape, the `Block` in the slot's `accepts` whose type it
|
|
24
|
+
* matches: a base-slide block fills in place; any other block is transplanted
|
|
25
|
+
* from its source slide onto the clone, then filled with the same callbacks.
|
|
26
|
+
*
|
|
27
|
+
* `generate()` is first below; its helpers follow (function declarations hoist).
|
|
24
28
|
*/
|
|
25
|
-
import type { Config, Deck,
|
|
29
|
+
import type { Config, Deck, DeckStep, Layout } from "./types.js";
|
|
26
30
|
/** Options for `generate` / `buildDeck`. */
|
|
27
31
|
export type GenerateOptions = {
|
|
28
32
|
/**
|
|
@@ -40,22 +44,24 @@ export type GenerateOptions = {
|
|
|
40
44
|
* 1. Load the template; register it twice (as root and under an alias).
|
|
41
45
|
* 2. Pre-register every image's media buffer with pptx-automizer, walking
|
|
42
46
|
* the unified `step.content` for ImageFill values.
|
|
43
|
-
* 3. For each deck step, clone the layout's
|
|
44
|
-
* addSlide callback
|
|
45
|
-
*
|
|
47
|
+
* 3. For each deck step, clone the layout's base slide, then within the
|
|
48
|
+
* addSlide callback call `fillSlide` to dispatch each content value to the
|
|
49
|
+
* matching `Block` (fill in place, or transplant + fill).
|
|
46
50
|
* 4. Write the output PPTX.
|
|
47
51
|
*/
|
|
48
52
|
export declare function generate(deck: Deck, config: Config, options?: GenerateOptions): Promise<void>;
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
54
|
+
* Fill one cloned slide. Per slot the step supplies a value for: resolve WHICH
|
|
55
|
+
* shape realizes it (`resolveBlock`), build WHAT to write (`FILLERS[…].callbacks`),
|
|
56
|
+
* and place it WHERE/HOW (`applyBlock`). Every ambiguity fails fast, naming layout
|
|
57
|
+
* + slot.
|
|
53
58
|
*
|
|
54
|
-
* Exported for tests;
|
|
59
|
+
* Exported for tests; `generate()` calls it inside the `addSlide` callback.
|
|
60
|
+
*/
|
|
61
|
+
export declare function fillSlide(slide: any, layout: Layout, step: DeckStep, sourceAlias: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Reject a slot whose `accepts` lists two blocks of the same type — the
|
|
64
|
+
* value→block lookup would silently pick the first. Called once per layout at
|
|
65
|
+
* build start.
|
|
55
66
|
*/
|
|
56
|
-
export declare function
|
|
57
|
-
layout: string;
|
|
58
|
-
content?: Record<string, unknown>;
|
|
59
|
-
}, tpl: {
|
|
60
|
-
slots: Slot[];
|
|
61
|
-
}): void;
|
|
67
|
+
export declare function assertSlotsWellFormed(layout: Layout): void;
|
package/dist/engine/generate.js
CHANGED
|
@@ -18,9 +18,13 @@
|
|
|
18
18
|
* file (registered by generate(), swapped by the ImageFiller) and adjusts
|
|
19
19
|
* geometry for the chosen fit. See fillers/image.ts.
|
|
20
20
|
*
|
|
21
|
-
* generate() loads the template, registers media, and for each DeckStep
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
21
|
+
* generate() loads the template, registers media, and for each DeckStep clones
|
|
22
|
+
* the layout's base slide and calls `fillSlide`. Each value in `step.content`
|
|
23
|
+
* selects, by its own shape, the `Block` in the slot's `accepts` whose type it
|
|
24
|
+
* matches: a base-slide block fills in place; any other block is transplanted
|
|
25
|
+
* from its source slide onto the clone, then filled with the same callbacks.
|
|
26
|
+
*
|
|
27
|
+
* `generate()` is first below; its helpers follow (function declarations hoist).
|
|
24
28
|
*/
|
|
25
29
|
import { existsSync, rmSync } from "node:fs";
|
|
26
30
|
import { basename, dirname, resolve } from "node:path";
|
|
@@ -36,9 +40,9 @@ import { applyNotesToSlide, sweepOrphanNotes } from "./notes.js";
|
|
|
36
40
|
* 1. Load the template; register it twice (as root and under an alias).
|
|
37
41
|
* 2. Pre-register every image's media buffer with pptx-automizer, walking
|
|
38
42
|
* the unified `step.content` for ImageFill values.
|
|
39
|
-
* 3. For each deck step, clone the layout's
|
|
40
|
-
* addSlide callback
|
|
41
|
-
*
|
|
43
|
+
* 3. For each deck step, clone the layout's base slide, then within the
|
|
44
|
+
* addSlide callback call `fillSlide` to dispatch each content value to the
|
|
45
|
+
* matching `Block` (fill in place, or transplant + fill).
|
|
42
46
|
* 4. Write the output PPTX.
|
|
43
47
|
*/
|
|
44
48
|
export async function generate(deck, config, options = {}) {
|
|
@@ -62,33 +66,17 @@ export async function generate(deck, config, options = {}) {
|
|
|
62
66
|
throw new Error(`Unknown layout: ${name}`);
|
|
63
67
|
return match;
|
|
64
68
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!isImageFill(value))
|
|
73
|
-
continue;
|
|
74
|
-
if (!existsSync(value.path)) {
|
|
75
|
-
throw new Error(`Layout "${step.layout}" image "${value.path}": file not found`);
|
|
76
|
-
}
|
|
77
|
-
const file = basename(value.path);
|
|
78
|
-
if (registeredMedia.has(file))
|
|
79
|
-
continue;
|
|
80
|
-
registeredMedia.add(file);
|
|
81
|
-
pres.loadMedia(file, dirname(value.path));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// pptx-automizer runs modifyElement callbacks during write() and SWALLOWS any
|
|
85
|
-
// error they throw (it logs a stack trace but keeps going, producing a broken
|
|
86
|
-
// slide). A fill primitive's fail-fast throw would therefore never fail the
|
|
87
|
-
// build. Collect those errors and re-surface them after write().
|
|
69
|
+
registerMedia(pres, deck);
|
|
70
|
+
assertLayoutsWellFormed(layouts);
|
|
71
|
+
// pptx-automizer runs fill callbacks during write() and SWALLOWS any error they
|
|
72
|
+
// throw (it logs a stack trace but keeps going, producing a broken slide). A
|
|
73
|
+
// fill primitive's fail-fast throw would therefore never fail the build. This
|
|
74
|
+
// wraps BOTH fill entry points — `modifyElement` (in-place) and `addElement`
|
|
75
|
+
// (transplant) — so a throw on a transplanted shape fails the build too.
|
|
88
76
|
const fillErrors = [];
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
77
|
+
const wrapCallbacks = (callbacks) => {
|
|
78
|
+
const arr = Array.isArray(callbacks) ? callbacks : callbacks === undefined ? [] : [callbacks];
|
|
79
|
+
return arr.map((cb) => typeof cb === "function"
|
|
92
80
|
? (...args) => {
|
|
93
81
|
try {
|
|
94
82
|
return cb(...args);
|
|
@@ -98,29 +86,13 @@ export async function generate(deck, config, options = {}) {
|
|
|
98
86
|
throw err;
|
|
99
87
|
}
|
|
100
88
|
}
|
|
101
|
-
: cb)
|
|
89
|
+
: cb);
|
|
102
90
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const value = step.content?.[slot.key];
|
|
109
|
-
if (value === undefined)
|
|
110
|
-
continue;
|
|
111
|
-
if (typeof value === "string") {
|
|
112
|
-
// Fallback: bare string. Compiler normally normalizes to
|
|
113
|
-
// StyledParagraph[] / ImageFill; this branch only fires when
|
|
114
|
-
// callers construct decks by hand.
|
|
115
|
-
slide.modifyElement(slot.shapeName, [modify.setText(value)]);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const filler = FILLERS[slot.type];
|
|
119
|
-
if (!filler.matches(value)) {
|
|
120
|
-
throw new Error(`Layout "${step.layout}" slot "${slot.key}" (type "${slot.type}"): expected ${filler.label}, got ${describeValue(value)}`);
|
|
121
|
-
}
|
|
122
|
-
filler.fill(slide, slot, value, { layoutName: step.layout });
|
|
123
|
-
}
|
|
91
|
+
const captureFillErrors = (slide) => {
|
|
92
|
+
const modifyElement = slide.modifyElement.bind(slide);
|
|
93
|
+
slide.modifyElement = (shapeName, callbacks) => modifyElement(shapeName, wrapCallbacks(callbacks));
|
|
94
|
+
const addElement = slide.addElement.bind(slide);
|
|
95
|
+
slide.addElement = (presName, slideNumber, selector, callbacks) => addElement(presName, slideNumber, selector, wrapCallbacks(callbacks));
|
|
124
96
|
};
|
|
125
97
|
// Default: write authored notes and strip any template notes automizer clones
|
|
126
98
|
// onto slides. When true, no notes are written but inherited notes are still
|
|
@@ -128,8 +100,9 @@ export async function generate(deck, config, options = {}) {
|
|
|
128
100
|
const excludeNotes = options.excludeNotes ?? false;
|
|
129
101
|
for (const step of deck.steps) {
|
|
130
102
|
const layout = resolveLayout(step.layout);
|
|
131
|
-
pres.addSlide(sourceAlias, layout.
|
|
132
|
-
|
|
103
|
+
pres.addSlide(sourceAlias, layout.baseSlide, (slide) => {
|
|
104
|
+
captureFillErrors(slide);
|
|
105
|
+
fillSlide(slide, layout, step, sourceAlias);
|
|
133
106
|
// In-band notes pass: automizer runs this during write() and hands us the
|
|
134
107
|
// OUTPUT archive (parent.targetArchive) and the real output slide number
|
|
135
108
|
// (parent.targetNumber), so notes map 1:1 with no slide-number mapping.
|
|
@@ -162,6 +135,35 @@ export async function generate(deck, config, options = {}) {
|
|
|
162
135
|
}
|
|
163
136
|
console.log(`tycoslide: built ${deck.steps.length} slide(s) → ${resolve(outDir, outFile)}`);
|
|
164
137
|
}
|
|
138
|
+
// ── generate() helpers ───────────────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Register the media for every image slot. Each file is validated (absolute
|
|
141
|
+
* paths are the caller's responsibility; a stray relative path trips the
|
|
142
|
+
* existsSync guard here or readFileSync in fillImage) and handed to
|
|
143
|
+
* pptx-automizer once per distinct filename.
|
|
144
|
+
*/
|
|
145
|
+
function registerMedia(pres, deck) {
|
|
146
|
+
const registeredMedia = new Set();
|
|
147
|
+
for (const step of deck.steps) {
|
|
148
|
+
for (const value of Object.values(step.content ?? {})) {
|
|
149
|
+
if (!isImageFill(value))
|
|
150
|
+
continue;
|
|
151
|
+
if (!existsSync(value.path)) {
|
|
152
|
+
throw new Error(`Layout "${step.layout}" image "${value.path}": file not found`);
|
|
153
|
+
}
|
|
154
|
+
const file = basename(value.path);
|
|
155
|
+
if (registeredMedia.has(file))
|
|
156
|
+
continue;
|
|
157
|
+
registeredMedia.add(file);
|
|
158
|
+
pres.loadMedia(file, dirname(value.path));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Validate every layout once, up front (see assertSlotsWellFormed).
|
|
163
|
+
function assertLayoutsWellFormed(layouts) {
|
|
164
|
+
for (const layout of layouts)
|
|
165
|
+
assertSlotsWellFormed(layout);
|
|
166
|
+
}
|
|
165
167
|
function describeValue(v) {
|
|
166
168
|
if (v === null)
|
|
167
169
|
return "null";
|
|
@@ -173,6 +175,105 @@ function describeValue(v) {
|
|
|
173
175
|
}
|
|
174
176
|
return typeof v;
|
|
175
177
|
}
|
|
178
|
+
// ── Slot fill dispatch (composition-aware) ───────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* Fill one cloned slide. Per slot the step supplies a value for: resolve WHICH
|
|
181
|
+
* 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.
|
|
184
|
+
*
|
|
185
|
+
* Exported for tests; `generate()` calls it inside the `addSlide` callback.
|
|
186
|
+
*/
|
|
187
|
+
export function fillSlide(slide, layout, step, sourceAlias) {
|
|
188
|
+
assertNoUnknownSlots(step, layout);
|
|
189
|
+
for (const slot of layout.slots) {
|
|
190
|
+
const value = step.content?.[slot.key];
|
|
191
|
+
if (value === undefined)
|
|
192
|
+
continue; // Empty slot: leave the base slide's shape untouched.
|
|
193
|
+
const block = resolveBlock(step, slot, value);
|
|
194
|
+
const callbacks = FILLERS[block.type].callbacks(value, targetOf(block));
|
|
195
|
+
applyBlock(slide, sourceAlias, layout.baseSlide, slot, block, callbacks);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** The SlotType a resolved `*Fill` value maps to, via the filler discriminators. */
|
|
199
|
+
function fillTypeOf(value) {
|
|
200
|
+
for (const type of Object.keys(FILLERS)) {
|
|
201
|
+
if (FILLERS[type].matches(value))
|
|
202
|
+
return type;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Reject a slot whose `accepts` lists two blocks of the same type — the
|
|
208
|
+
* value→block lookup would silently pick the first. Called once per layout at
|
|
209
|
+
* build start.
|
|
210
|
+
*/
|
|
211
|
+
export function assertSlotsWellFormed(layout) {
|
|
212
|
+
for (const slot of layout.slots) {
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
for (const block of slot.accepts) {
|
|
215
|
+
if (seen.has(block.type)) {
|
|
216
|
+
throw new Error(`Layout "${layout.name}" slot "${slot.key}": accepts two ${block.type} blocks; each content type may appear once.`);
|
|
217
|
+
}
|
|
218
|
+
seen.add(block.type);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A deck supplying content for a slot the layout doesn't declare is an authoring
|
|
224
|
+
* mistake, not a silent no-op. Runs per step, over `step.content`'s keys.
|
|
225
|
+
*/
|
|
226
|
+
function assertNoUnknownSlots(step, layout) {
|
|
227
|
+
const keys = new Set(layout.slots.map((s) => s.key));
|
|
228
|
+
for (const key of Object.keys(step.content ?? {})) {
|
|
229
|
+
if (!keys.has(key)) {
|
|
230
|
+
const declared = layout.slots.map((s) => s.key).join(", ") || "none";
|
|
231
|
+
throw new Error(`Layout "${step.layout}" has no slot "${key}" (declared slots: ${declared}).`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* WHICH shape: pick the `Block` in `slot.accepts` whose type matches the value's
|
|
237
|
+
* shape. Fails fast — first on an unrecognized value, then on a value no block
|
|
238
|
+
* accepts (order matters; both messages are asserted).
|
|
239
|
+
*/
|
|
240
|
+
function resolveBlock(step, slot, value) {
|
|
241
|
+
const requestedType = fillTypeOf(value);
|
|
242
|
+
if (requestedType === undefined) {
|
|
243
|
+
throw new Error(`Layout "${step.layout}" slot "${slot.key}": unrecognized content value ${describeValue(value)}.`);
|
|
244
|
+
}
|
|
245
|
+
const block = slot.accepts.find((b) => b.type === requestedType);
|
|
246
|
+
if (!block) {
|
|
247
|
+
const available = slot.accepts.map((b) => b.type).join(", ") || "none";
|
|
248
|
+
throw new Error(`Layout "${step.layout}" slot "${slot.key}": no block accepts ${requestedType} content (this slot accepts: ${available}).`);
|
|
249
|
+
}
|
|
250
|
+
return block;
|
|
251
|
+
}
|
|
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;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* WHERE/HOW: place the (already-built) fill callbacks. A base-slide block is
|
|
261
|
+
* already on the cloned slide → fill in place. Any other block is transplanted:
|
|
262
|
+
* pptx-automizer runs an appended shape's callbacks against the imported element
|
|
263
|
+
* itself, so the same callbacks refill it — position it to the slot's frame,
|
|
264
|
+
* then remove the base shape it supersedes (a base block, if this slot has one —
|
|
265
|
+
* a slot need not).
|
|
266
|
+
*/
|
|
267
|
+
function applyBlock(slide, sourceAlias, baseSlide, slot, block, callbacks) {
|
|
268
|
+
if (block.sourceSlide === baseSlide) {
|
|
269
|
+
slide.modifyElement(block.shapeName, callbacks);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
slide.addElement(sourceAlias, block.sourceSlide, block.shapeName, [modify.setPosition(slot.frame), ...callbacks]);
|
|
273
|
+
const baseBlock = slot.accepts.find((b) => b.sourceSlide === baseSlide);
|
|
274
|
+
if (baseBlock && baseBlock.shapeName !== block.shapeName)
|
|
275
|
+
slide.removeElement(baseBlock.shapeName);
|
|
276
|
+
}
|
|
176
277
|
// ── Notes archive adapter (automizer-buffer glue) ────────────────────────────
|
|
177
278
|
/** Installed pptx-automizer version this notes-buffer adapter is pinned to. */
|
|
178
279
|
const PPTX_AUTOMIZER_VERSION = "0.8.2";
|
|
@@ -227,17 +328,3 @@ function toNotesArchive(target) {
|
|
|
227
328
|
},
|
|
228
329
|
};
|
|
229
330
|
}
|
|
230
|
-
// ── Content-slot validation (test-visible helper) ────────────────────────────
|
|
231
|
-
/**
|
|
232
|
-
* Validate that every required slot on a layout is supplied. Type/shape
|
|
233
|
-
* validation happens in the compiler now — this is a thin required-only
|
|
234
|
-
* checker kept for tests and defensive callers.
|
|
235
|
-
*
|
|
236
|
-
* Exported for tests; not part of the public engine surface (index.ts).
|
|
237
|
-
*/
|
|
238
|
-
export function validateContentSlots(step, tpl) {
|
|
239
|
-
const missing = tpl.slots.filter((s) => step.content?.[s.key] === undefined).map((s) => s.key);
|
|
240
|
-
if (missing.length > 0) {
|
|
241
|
-
throw new Error(`Layout "${step.layout}": missing content for slot(s): ${missing.join(", ")}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -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 { Config, Deck, DeckStep, ImageFill, Layout, Slot, StyledParagraph, TableFill, TemplateFill, TemplateSegment, TextFill, TextRun, ThemeConfig, } from "./types.js";
|
|
8
|
+
export type { Block, 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";
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -93,20 +93,50 @@ export type ImageFill = {
|
|
|
93
93
|
path: string;
|
|
94
94
|
fit: ImageFit;
|
|
95
95
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
/** A shape's absolute position and size, in EMU — the slot's frame. */
|
|
97
|
+
export type Frame = {
|
|
98
|
+
x: number;
|
|
99
|
+
y: number;
|
|
100
|
+
cx: number;
|
|
101
|
+
cy: number;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 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.
|
|
111
|
+
*
|
|
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.
|
|
115
|
+
*/
|
|
116
|
+
export type Block = {
|
|
100
117
|
type: SlotType;
|
|
101
|
-
|
|
118
|
+
sourceSlide: number;
|
|
119
|
+
shapeName: string;
|
|
102
120
|
startAt?: number;
|
|
103
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* An author-facing fill region. Not welded to one shape+type: a slot owns its
|
|
124
|
+
* `frame` and `accepts` a set of `Block`s; the supplied value's shape selects
|
|
125
|
+
* which block fills. A block whose `sourceSlide === baseSlide` fills in place;
|
|
126
|
+
* any other block is transplanted into the slot's `frame`.
|
|
127
|
+
*/
|
|
128
|
+
export type Slot = {
|
|
129
|
+
key: string;
|
|
130
|
+
frame: Frame;
|
|
131
|
+
accepts: Block[];
|
|
132
|
+
};
|
|
104
133
|
export type Layout = {
|
|
105
134
|
name: string;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
/**
|
|
136
|
+
* The template slide cloned for chrome/background. A block whose `sourceSlide`
|
|
137
|
+
* equals this is filled in place; any other block is transplanted onto the clone.
|
|
138
|
+
*/
|
|
139
|
+
baseSlide: number;
|
|
110
140
|
slots: Slot[];
|
|
111
141
|
};
|
|
112
142
|
export type DeckStep = {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import { type Config, type GenerateOptions, type ThemeConfig } from "./engine/index.js";
|
|
2
|
-
import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig
|
|
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>;
|
|
2
|
+
import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig } from "./markdown/types.js";
|
|
16
3
|
/**
|
|
17
4
|
* Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
|
|
18
5
|
* Fields are copied cell-by-cell so the boundary is explicit — no casts.
|
|
@@ -28,11 +15,18 @@ export declare function toEngineThemeConfig(config: CompilerThemeConfig): ThemeC
|
|
|
28
15
|
*/
|
|
29
16
|
export declare function toEngineConfig(config: CompilerConfig): Config;
|
|
30
17
|
/**
|
|
31
|
-
* End-to-end build:
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
18
|
+
* End-to-end build: `compileDeck` already produced engine-shaped content (code
|
|
19
|
+
* highlighted, mermaid rendered), so `buildDeck` only asserts an `output` is set
|
|
20
|
+
* and hands the deck to the engine's primitives-only `generate()`. The deck is
|
|
21
|
+
* structurally equivalent to the engine's `Deck` once `output` is present, so no
|
|
22
|
+
* cast is required. `buildDeck` does not itself validate `config` — a
|
|
23
|
+
* programmatic caller assembling a `CompilerConfig` by hand should load it
|
|
24
|
+
* through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
|
|
25
|
+
* fail-fast structural checks the CLI gets.
|
|
26
|
+
*
|
|
27
|
+
* Fails fast if `deck.output` is missing: `generate()` requires it, and the CLI
|
|
28
|
+
* populates it before calling `buildDeck`; a programmatic caller that forgot to
|
|
29
|
+
* set it hits this error instead of a confusing engine-side failure.
|
|
36
30
|
*
|
|
37
31
|
* Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
|
|
38
32
|
* post-write cleanup is needed.
|
|
@@ -42,5 +36,5 @@ export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot,
|
|
|
42
36
|
export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
|
|
43
37
|
export type { ManifestOptions } from "./manifest.js";
|
|
44
38
|
export { generateManifest } from "./manifest.js";
|
|
45
|
-
export type { AssetCatalog, AssetEntry,
|
|
46
|
-
export {
|
|
39
|
+
export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, Limit, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
|
|
40
|
+
export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
|