@tycoworks/tycoslide 0.9.0 → 0.10.1
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 +12 -9
- package/SKILL.md +6 -7
- package/dist/cli.js +32 -45
- package/dist/markdown/blocks/code.d.ts +4 -2
- package/dist/markdown/blocks/code.js +19 -4
- package/dist/markdown/deckCompiler.js +9 -1
- package/dist/markdown/schema/themeConfigSchema.d.ts +8 -1
- package/dist/markdown/schema/themeConfigSchema.js +4 -2
- package/dist/markdown/types.d.ts +29 -2
- package/dist/markdown/types.js +11 -0
- package/dist/skillZip.d.ts +17 -0
- package/dist/skillZip.js +64 -0
- package/package.json +3 -2
- package/syntax.md +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# tycoslide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create editable, on-brand PowerPoint slides from markdown.
|
|
4
4
|
|
|
5
5
|
> **Early release** — tycoslide is under active development.
|
|
6
6
|
|
|
7
7
|
## How it works
|
|
8
8
|
|
|
9
|
-
1. **tycoslide wraps
|
|
10
|
-
2. **
|
|
11
|
-
3. **tycoslide builds
|
|
9
|
+
1. **tycoslide wraps existing PowerPoint files as reusable templates.**
|
|
10
|
+
2. **You (or an agent) write slides in markdown.**
|
|
11
|
+
3. **tycoslide builds new PowerPoint files.**
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
@@ -42,16 +42,15 @@ title: Highlights
|
|
|
42
42
|
Build:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
tycoslide build deck.md
|
|
45
|
+
npx tycoslide build deck.md # → deck.pptx
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## CLI
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
|
|
52
|
-
tycoslide build deck.md --no-notes # omit speaker notes from the output
|
|
53
|
-
tycoslide
|
|
54
|
-
tycoslide manifest # print layout + asset catalog to stdout
|
|
51
|
+
npx tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
|
|
52
|
+
npx tycoslide build deck.md --no-notes # omit speaker notes from the output
|
|
53
|
+
npx tycoslide package # regenerate skill.md/syntax.md/manifest.json + zip the whole theme into a self-contained <package-name>.zip
|
|
55
54
|
```
|
|
56
55
|
|
|
57
56
|
## Theme Structure
|
|
@@ -65,6 +64,10 @@ my-theme/
|
|
|
65
64
|
assets/icons/
|
|
66
65
|
theme.json
|
|
67
66
|
package.json
|
|
67
|
+
skill.md # generated by `tycoslide package`
|
|
68
|
+
syntax.md # generated by `tycoslide package`
|
|
69
|
+
manifest.json # generated by `tycoslide package`
|
|
70
|
+
<package-name>.zip # uploadable Agent Skill bundle
|
|
68
71
|
```
|
|
69
72
|
|
|
70
73
|
**Template** — the PPTX file with named shapes that tycoslide fills.
|
package/SKILL.md
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slides
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"presentation," "pitch," or .pptx output. Also trigger when the user says "build me a deck,"
|
|
7
|
-
"make slides about X," or "turn this into a presentation."
|
|
4
|
+
Build branded slides, presentations, pitch decks, or sales collateral as a .pptx.
|
|
5
|
+
Trigger on "deck," "slides," "presentation," "pitch," ".pptx," or "build me a deck."
|
|
8
6
|
---
|
|
9
7
|
|
|
10
8
|
# slides
|
|
11
9
|
|
|
12
10
|
## Setup
|
|
13
11
|
|
|
14
|
-
Before first use, install dependencies from the
|
|
12
|
+
Before first use, install dependencies from the theme root:
|
|
15
13
|
|
|
16
14
|
```bash
|
|
17
15
|
npm install
|
|
@@ -48,7 +46,7 @@ A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
|
|
|
48
46
|
|
|
49
47
|
A single physical slide may back multiple layouts. When two manifest entries share the same `slideNumber`, they render into the same underlying PPTX shapes but declare their fill differently -- e.g. one layout exposing the fill as an `image` parameter and a sibling exposing it as a `mermaid` slot. Pick between them by naming the layout you want in frontmatter (`layout: Full bleed diagram` vs `layout: Full bleed image`); the declaration is unambiguous per layout, so the compiler always knows how to interpret the content you provide.
|
|
50
48
|
|
|
51
|
-
Study each layout's `
|
|
49
|
+
Study each layout's `slots` and `limit`s before writing any slides.
|
|
52
50
|
|
|
53
51
|
---
|
|
54
52
|
|
|
@@ -131,7 +129,7 @@ The deck is written to your current working directory (not inside the skill).
|
|
|
131
129
|
|
|
132
130
|
### For Each Slide
|
|
133
131
|
|
|
134
|
-
**Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides.
|
|
132
|
+
**Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides. Put the takeaway in the headline, then support it.
|
|
135
133
|
|
|
136
134
|
Check each layout's `limit` in the manifest for content density constraints. When content overflows, split across slides.
|
|
137
135
|
|
|
@@ -143,6 +141,7 @@ Check each layout's `limit` in the manifest for content density constraints. Whe
|
|
|
143
141
|
- **Don't open with a body/content layout** -- use the Title layout for impact
|
|
144
142
|
- **Don't skip section dividers** -- for decks over 5 slides, use Section title layouts to group sections
|
|
145
143
|
- **Don't restyle the layout** -- the theme owns all design; you only fill slots
|
|
144
|
+
- **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration; if the build warns an image shrank to a small %, swap it for a simpler one
|
|
146
145
|
- **Don't invent layout or asset names** -- only use what exists in the manifest
|
|
147
146
|
- **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image parameter you care about
|
|
148
147
|
- **Don't mix dark and light** -- keep one variant across the deck
|
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
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
7
|
import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
|
|
8
|
+
import { renameSkill, zipDir } from "./skillZip.js";
|
|
8
9
|
const DEFAULT_CONFIG = "theme.json";
|
|
9
|
-
const SKILL_DIR = "skills/slides";
|
|
10
|
-
const PLUGIN_DIR = ".claude-plugin";
|
|
11
|
-
const PLUGIN_FILE = "plugin.json";
|
|
12
10
|
const MANIFEST_FILE = "manifest.json";
|
|
13
|
-
|
|
11
|
+
// The theme skill is written as lowercase skill.md (copied from tycoslide's own
|
|
12
|
+
// SKILL.md), so the skill folder can live at the theme repo root.
|
|
13
|
+
const SKILL_FILE = "skill.md";
|
|
14
14
|
const SYNTAX_FILE = "syntax.md";
|
|
15
15
|
const BUILD_COMMAND = "npx tycoslide build";
|
|
16
16
|
const sdkDir = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const skillMdPath = resolve(sdkDir, "..",
|
|
17
|
+
const skillMdPath = resolve(sdkDir, "..", "SKILL.md");
|
|
18
18
|
const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
|
|
19
19
|
const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
|
|
20
20
|
const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
|
|
@@ -50,49 +50,36 @@ program
|
|
|
50
50
|
await buildDeck(deck, config, { excludeNotes: !opts.notes });
|
|
51
51
|
});
|
|
52
52
|
program
|
|
53
|
-
.command("
|
|
54
|
-
.description("Generate manifest.json
|
|
53
|
+
.command("package")
|
|
54
|
+
.description("Generate the Agent Skill (manifest.json, SKILL.md, syntax.md) for AI agents")
|
|
55
55
|
.option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
|
|
56
|
-
.option(`-o, --out <file>`, "write to file instead of stdout")
|
|
57
56
|
.action(async (opts) => {
|
|
58
57
|
const config = loadThemeConfig(resolve(process.cwd(), opts.config));
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
console.log(`WROTE ${opts.out}`);
|
|
58
|
+
const themePkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
59
|
+
if (!themePkg.name) {
|
|
60
|
+
throw new Error('Cannot name the skill: the theme\'s package.json has no "name" field.');
|
|
63
61
|
}
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
// basename drops any npm scope, e.g. "@acme/mz-slides" -> "mz-slides".
|
|
63
|
+
const skillName = basename(themePkg.name);
|
|
64
|
+
const manifestJson = `${generateManifest(config, { build: { command: BUILD_COMMAND } })}\n`;
|
|
65
|
+
writeFileSync(resolve(process.cwd(), MANIFEST_FILE), manifestJson);
|
|
66
|
+
console.log(`WROTE ${MANIFEST_FILE}`);
|
|
67
|
+
let skillMd;
|
|
68
|
+
try {
|
|
69
|
+
skillMd = renameSkill(readFileSync(skillMdPath, "utf-8"), skillName);
|
|
66
70
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
skills: "./skills",
|
|
81
|
-
};
|
|
82
|
-
if (pkg.author)
|
|
83
|
-
pluginMeta.author = pkg.author;
|
|
84
|
-
const pluginDir = resolve(cwd, PLUGIN_DIR);
|
|
85
|
-
mkdirSync(pluginDir, { recursive: true });
|
|
86
|
-
writeFileSync(resolve(pluginDir, PLUGIN_FILE), `${JSON.stringify(pluginMeta, null, 2)}\n`);
|
|
87
|
-
console.log(`WROTE ${PLUGIN_DIR}/${PLUGIN_FILE}`);
|
|
88
|
-
const skillDir = resolve(cwd, SKILL_DIR);
|
|
89
|
-
mkdirSync(skillDir, { recursive: true });
|
|
90
|
-
const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
|
|
91
|
-
writeFileSync(resolve(skillDir, MANIFEST_FILE), `${json}\n`);
|
|
92
|
-
console.log(`WROTE ${SKILL_DIR}/${MANIFEST_FILE}`);
|
|
93
|
-
copyFileSync(skillMdPath, resolve(skillDir, SKILL_FILE));
|
|
94
|
-
console.log(`WROTE ${SKILL_DIR}/${SKILL_FILE}`);
|
|
95
|
-
copyFileSync(syntaxMdPath, resolve(skillDir, SYNTAX_FILE));
|
|
96
|
-
console.log(`WROTE ${SKILL_DIR}/${SYNTAX_FILE}`);
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw new Error(`${skillMdPath}: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
writeFileSync(resolve(process.cwd(), SKILL_FILE), skillMd);
|
|
75
|
+
console.log(`WROTE ${SKILL_FILE}`);
|
|
76
|
+
const syntaxMd = readFileSync(syntaxMdPath, "utf-8");
|
|
77
|
+
writeFileSync(resolve(process.cwd(), SYNTAX_FILE), syntaxMd);
|
|
78
|
+
console.log(`WROTE ${SYNTAX_FILE}`);
|
|
79
|
+
// Bundle the WHOLE theme so the skill is self-contained: unzip ->
|
|
80
|
+
// `npm install` (pulls the engine + its deps) -> `npx tycoslide build`.
|
|
81
|
+
const zipFile = `${skillName}.zip`;
|
|
82
|
+
writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName));
|
|
83
|
+
console.log(`WROTE ${zipFile}`);
|
|
97
84
|
});
|
|
98
85
|
await program.parseAsync(process.argv);
|
|
@@ -4,8 +4,10 @@ import { type BlockHandler } from "../types.js";
|
|
|
4
4
|
* Recognize a fenced code block (any language except mermaid) at a region's top
|
|
5
5
|
* level, folding it to a Text fill, and compile it by Shiki-highlighting its
|
|
6
6
|
* source into a TextFill. Theme resolution is strict: the theme MUST declare a
|
|
7
|
-
* `codeTheme` (one style per theme — the design-system
|
|
8
|
-
*
|
|
7
|
+
* `codeTheme` — either one Shiki id (one style per theme — the design-system
|
|
8
|
+
* framing) or a `{ light, dark }` pair the layout's `variant` selects (a pair
|
|
9
|
+
* with no layout `variant` throws). A deck with code fences but no theme-level
|
|
10
|
+
* `codeTheme` throws, naming the offending slot.
|
|
9
11
|
*/
|
|
10
12
|
export declare const CODE: BlockHandler;
|
|
11
13
|
/**
|
|
@@ -5,8 +5,10 @@ import { MERMAID_LANG } from "./mermaid.js";
|
|
|
5
5
|
* Recognize a fenced code block (any language except mermaid) at a region's top
|
|
6
6
|
* level, folding it to a Text fill, and compile it by Shiki-highlighting its
|
|
7
7
|
* source into a TextFill. Theme resolution is strict: the theme MUST declare a
|
|
8
|
-
* `codeTheme` (one style per theme — the design-system
|
|
9
|
-
*
|
|
8
|
+
* `codeTheme` — either one Shiki id (one style per theme — the design-system
|
|
9
|
+
* framing) or a `{ light, dark }` pair the layout's `variant` selects (a pair
|
|
10
|
+
* with no layout `variant` throws). A deck with code fences but no theme-level
|
|
11
|
+
* `codeTheme` throws, naming the offending slot.
|
|
10
12
|
*/
|
|
11
13
|
export const CODE = {
|
|
12
14
|
match: (node) => node.type === MdastType.Code && node.lang !== MERMAID_LANG,
|
|
@@ -19,11 +21,24 @@ export const CODE = {
|
|
|
19
21
|
throw new Error(`Slide ${ctx.slideIdx}: layout "${ctx.layoutName}" slot content (from ${ctx.source}) has a code fence ` +
|
|
20
22
|
"with no language; add one after the opening ``` (e.g. ```sql).");
|
|
21
23
|
}
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
24
|
+
const codeTheme = ctx.config.codeTheme;
|
|
25
|
+
if (!codeTheme) {
|
|
24
26
|
throw new Error(`Layout "${ctx.layoutName}" slot content (from ${ctx.source}): deck contains a code fence but the theme ` +
|
|
25
27
|
'declares no "codeTheme". Add a theme-level "codeTheme" (a Shiki theme id) to theme.json.');
|
|
26
28
|
}
|
|
29
|
+
// A single string is one style for every code layout; a `{ light, dark }`
|
|
30
|
+
// pair requires the layout to declare which via `variant` — no default.
|
|
31
|
+
let theme;
|
|
32
|
+
if (typeof codeTheme === "string") {
|
|
33
|
+
theme = codeTheme;
|
|
34
|
+
}
|
|
35
|
+
else if (ctx.layoutVariant) {
|
|
36
|
+
theme = codeTheme[ctx.layoutVariant];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(`Layout "${ctx.layoutName}" slot content (from ${ctx.source}): the theme's "codeTheme" is a ` +
|
|
40
|
+
'{ light, dark } pair, but this layout declares no "variant". Add variant: "light" or "dark" to the layout.');
|
|
41
|
+
}
|
|
27
42
|
return { paragraphs: await highlightCode(code.value, code.lang, theme) };
|
|
28
43
|
},
|
|
29
44
|
};
|
|
@@ -223,6 +223,7 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
223
223
|
slideIdx: index,
|
|
224
224
|
source: "body content",
|
|
225
225
|
config,
|
|
226
|
+
layoutVariant: layoutDef.variant,
|
|
226
227
|
});
|
|
227
228
|
// Validate the slot accepts this region's type BEFORE running the (possibly
|
|
228
229
|
// expensive — Shiki, Playwright) fill: a mismatched region fails fast without
|
|
@@ -238,7 +239,14 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
238
239
|
`Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
|
|
239
240
|
}
|
|
240
241
|
const source = `::${name}::`;
|
|
241
|
-
const parsed = parseSlotContent(text, {
|
|
242
|
+
const parsed = parseSlotContent(text, {
|
|
243
|
+
resolveAssetRef,
|
|
244
|
+
layoutName,
|
|
245
|
+
slideIdx: index,
|
|
246
|
+
source,
|
|
247
|
+
config,
|
|
248
|
+
layoutVariant: layoutDef.variant,
|
|
249
|
+
});
|
|
242
250
|
assertSlotRegion(slot, parsed.acceptType, layoutName, index, source);
|
|
243
251
|
content[name] = await parsed.fill();
|
|
244
252
|
}
|
|
@@ -7,6 +7,10 @@ export declare const ThemeConfigSchema: z.ZodObject<{
|
|
|
7
7
|
description: z.ZodOptional<z.ZodString>;
|
|
8
8
|
whenToUse: z.ZodOptional<z.ZodString>;
|
|
9
9
|
whenNotToUse: z.ZodOptional<z.ZodString>;
|
|
10
|
+
variant: z.ZodOptional<z.ZodEnum<{
|
|
11
|
+
light: "light";
|
|
12
|
+
dark: "dark";
|
|
13
|
+
}>>;
|
|
10
14
|
parameters: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
11
15
|
shapeName: z.ZodString;
|
|
12
16
|
limit: z.ZodOptional<z.ZodObject<{
|
|
@@ -79,7 +83,10 @@ export declare const ThemeConfigSchema: z.ZodObject<{
|
|
|
79
83
|
path: z.ZodString;
|
|
80
84
|
weight: z.ZodOptional<z.ZodNumber>;
|
|
81
85
|
}, z.core.$strict>>>;
|
|
82
|
-
codeTheme: z.ZodOptional<z.ZodString
|
|
86
|
+
codeTheme: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
87
|
+
light: z.ZodString;
|
|
88
|
+
dark: z.ZodString;
|
|
89
|
+
}, z.core.$strict>]>>;
|
|
83
90
|
mermaidVariant: z.ZodOptional<z.ZodString>;
|
|
84
91
|
}, z.core.$strict>;
|
|
85
92
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname } from "node:path";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
-
import { AcceptType, AssetType, ParameterType } from "../types.js";
|
|
4
|
+
import { AcceptType, AssetType, ParameterType, Variant, } from "../types.js";
|
|
5
5
|
import { strict } from "./strict.js";
|
|
6
6
|
/**
|
|
7
7
|
* Fail-fast runtime validation for a `theme.json`. The hand-written types in
|
|
@@ -23,6 +23,7 @@ import { strict } from "./strict.js";
|
|
|
23
23
|
// Reuse the const-object enums as runtime values — no third copy of the literals.
|
|
24
24
|
const acceptTypeSchema = z.enum(Object.values(AcceptType));
|
|
25
25
|
const assetTypeSchema = z.enum(Object.values(AssetType));
|
|
26
|
+
const variantSchema = z.enum(Object.values(Variant));
|
|
26
27
|
// Re-declared here (not imported from the engine) so the schema layer never
|
|
27
28
|
// depends on the engine — mirrors engine `Frame`, guarded by `_drift`.
|
|
28
29
|
const FrameSchema = strict({
|
|
@@ -98,6 +99,7 @@ const LayoutSchema = strict({
|
|
|
98
99
|
description: z.string().optional(),
|
|
99
100
|
whenToUse: z.string().optional(),
|
|
100
101
|
whenNotToUse: z.string().optional(),
|
|
102
|
+
variant: variantSchema.optional(),
|
|
101
103
|
parameters: z.array(ParameterSchema),
|
|
102
104
|
slots: z.array(SlotSchema),
|
|
103
105
|
});
|
|
@@ -108,7 +110,7 @@ export const ThemeConfigSchema = strict({
|
|
|
108
110
|
outputDir: z.string().optional(),
|
|
109
111
|
mermaid: MermaidConfigSchema.optional(),
|
|
110
112
|
fonts: z.array(ThemeFontSchema).optional(),
|
|
111
|
-
codeTheme: z.string().optional(),
|
|
113
|
+
codeTheme: z.union([z.string(), strict({ light: z.string().min(1), dark: z.string().min(1) })]).optional(),
|
|
112
114
|
mermaidVariant: z.string().optional(),
|
|
113
115
|
});
|
|
114
116
|
const _drift = true;
|
package/dist/markdown/types.d.ts
CHANGED
|
@@ -54,6 +54,17 @@ export declare const AcceptType: {
|
|
|
54
54
|
readonly Image: "image";
|
|
55
55
|
};
|
|
56
56
|
export type AcceptType = (typeof AcceptType)[keyof typeof AcceptType];
|
|
57
|
+
/**
|
|
58
|
+
* The tonal surface a layout sits on. A code layout on a light panel needs a
|
|
59
|
+
* light Shiki theme (dark syntax would wash out); one on a dark panel needs a
|
|
60
|
+
* dark theme. A layout declares its `variant`; the code compile resolves a
|
|
61
|
+
* `{ light, dark }` `codeTheme` pair against it (a pair with no `variant` throws).
|
|
62
|
+
*/
|
|
63
|
+
export declare const Variant: {
|
|
64
|
+
readonly Light: "light";
|
|
65
|
+
readonly Dark: "dark";
|
|
66
|
+
};
|
|
67
|
+
export type Variant = (typeof Variant)[keyof typeof Variant];
|
|
57
68
|
/**
|
|
58
69
|
* The four engine content shapes a slot's compiled content can be — the value
|
|
59
70
|
* `compile` produces and the engine fills. ImageFill carries a `.type`
|
|
@@ -85,6 +96,12 @@ export type BlockContext = {
|
|
|
85
96
|
slideIdx: number;
|
|
86
97
|
source: string;
|
|
87
98
|
config: CompilerConfig;
|
|
99
|
+
/**
|
|
100
|
+
* The current layout's tonal surface, threaded from its `variant`. The code
|
|
101
|
+
* compile reads it to pick the arm of a `{ light, dark }` `codeTheme` pair;
|
|
102
|
+
* a pair with no `variant` throws (no default).
|
|
103
|
+
*/
|
|
104
|
+
layoutVariant?: Variant;
|
|
88
105
|
};
|
|
89
106
|
/**
|
|
90
107
|
* A block handler recognizes one content kind at the region's top level, folds
|
|
@@ -233,6 +250,11 @@ export type CompilerLayout = {
|
|
|
233
250
|
description?: string;
|
|
234
251
|
whenToUse?: string;
|
|
235
252
|
whenNotToUse?: string;
|
|
253
|
+
/**
|
|
254
|
+
* The layout's tonal surface. Selects the arm of a `{ light, dark }` `codeTheme`
|
|
255
|
+
* pair for code fences on this layout. Required when `codeTheme` is a pair.
|
|
256
|
+
*/
|
|
257
|
+
variant?: Variant;
|
|
236
258
|
/** Frontmatter inputs (template, image) — one value per `key: value` line. */
|
|
237
259
|
parameters: CompilerParameter[];
|
|
238
260
|
/** Body regions — the default body or `::name::` regions; each `accepts` blocks. */
|
|
@@ -276,9 +298,14 @@ export type CompilerThemeConfig = {
|
|
|
276
298
|
* Theme-level defaults for the two content types the compiler resolves before
|
|
277
299
|
* the engine sees them. One code style and one mermaid style per theme (the
|
|
278
300
|
* design-system framing) — they can't sit on a multi-type slot. `codeTheme` is
|
|
279
|
-
* a Shiki theme id
|
|
301
|
+
* a Shiki theme id, or a `{ light, dark }` pair when the theme has both light
|
|
302
|
+
* and dark code layouts (each layout's `variant` picks the arm);
|
|
303
|
+
* `mermaidVariant` names an entry in `mermaid`.
|
|
280
304
|
*/
|
|
281
|
-
codeTheme?: string
|
|
305
|
+
codeTheme?: string | {
|
|
306
|
+
light: string;
|
|
307
|
+
dark: string;
|
|
308
|
+
};
|
|
282
309
|
mermaidVariant?: string;
|
|
283
310
|
};
|
|
284
311
|
export type CompilerConfig = CompilerThemeConfig & {
|
package/dist/markdown/types.js
CHANGED
|
@@ -37,6 +37,17 @@ export const AcceptType = {
|
|
|
37
37
|
Table: SlotType.Table,
|
|
38
38
|
Image: SlotType.Image,
|
|
39
39
|
};
|
|
40
|
+
// ── Variant discriminator (a layout's light/dark surface) ─────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* The tonal surface a layout sits on. A code layout on a light panel needs a
|
|
43
|
+
* light Shiki theme (dark syntax would wash out); one on a dark panel needs a
|
|
44
|
+
* dark theme. A layout declares its `variant`; the code compile resolves a
|
|
45
|
+
* `{ light, dark }` `codeTheme` pair against it (a pair with no `variant` throws).
|
|
46
|
+
*/
|
|
47
|
+
export const Variant = {
|
|
48
|
+
Light: "light",
|
|
49
|
+
Dark: "dark",
|
|
50
|
+
};
|
|
40
51
|
/**
|
|
41
52
|
* Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
|
|
42
53
|
* (layout, body). Exported so callers (e.g. cli.ts) reference the constants
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite the `name:` value in a skill.md's leading YAML frontmatter so the
|
|
3
|
+
* packaged skill is named after the consuming theme, not the source template.
|
|
4
|
+
* Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
|
|
5
|
+
* Throws if there is no frontmatter or no `name:` line — the caller names the file.
|
|
6
|
+
*/
|
|
7
|
+
export declare function renameSkill(md: string, name: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Zip an entire theme directory into an uploadable Agent Skill archive whose
|
|
10
|
+
* entries all live under a single root folder (e.g. `mz-slides/theme.json`),
|
|
11
|
+
* matching Anthropic's custom-skill format. Recursively includes every file
|
|
12
|
+
* except node_modules, hidden entries (any name starting with `.`), and
|
|
13
|
+
* top-level build artifacts (.pptx/.pdf/.zip at the repo root; the template
|
|
14
|
+
* .pptx under template/ is kept). Subdirectory structure is preserved with
|
|
15
|
+
* POSIX slashes. Fails fast if nothing is left to zip.
|
|
16
|
+
*/
|
|
17
|
+
export declare function zipDir(rootDir: string, folderName: string): Promise<Buffer>;
|
package/dist/skillZip.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { extname, join, relative, sep } from "node:path";
|
|
3
|
+
import JSZip from "jszip";
|
|
4
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
|
|
5
|
+
const NAME_LINE = /^name:[ \t]*.*$/m;
|
|
6
|
+
/**
|
|
7
|
+
* Rewrite the `name:` value in a skill.md's leading YAML frontmatter so the
|
|
8
|
+
* packaged skill is named after the consuming theme, not the source template.
|
|
9
|
+
* Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
|
|
10
|
+
* Throws if there is no frontmatter or no `name:` line — the caller names the file.
|
|
11
|
+
*/
|
|
12
|
+
export function renameSkill(md, name) {
|
|
13
|
+
const block = md.match(FRONTMATTER);
|
|
14
|
+
if (!block)
|
|
15
|
+
throw new Error("skill.md has no YAML frontmatter block");
|
|
16
|
+
if (!NAME_LINE.test(block[1]))
|
|
17
|
+
throw new Error('skill.md frontmatter has no "name:" line');
|
|
18
|
+
return md.replace(block[0], block[0].replace(NAME_LINE, `name: ${name}`));
|
|
19
|
+
}
|
|
20
|
+
// Never packaged: dependencies (npm install rebuilds them) and hidden entries
|
|
21
|
+
// (name starting with "." — covers VCS, tooling, caches, secrets like .env/.npmrc).
|
|
22
|
+
const EXCLUDE_DIRS = new Set(["node_modules"]);
|
|
23
|
+
// Build artifacts, dropped ONLY at the repo root: decks build to cwd
|
|
24
|
+
// (showcase.pptx, deck.pptx, the output .zip), while the template .pptx lives
|
|
25
|
+
// under template/ and must be kept — so these extensions are pruned top-level only.
|
|
26
|
+
const ROOT_ARTIFACT_EXTS = new Set([".pptx", ".pdf", ".zip"]);
|
|
27
|
+
/**
|
|
28
|
+
* Zip an entire theme directory into an uploadable Agent Skill archive whose
|
|
29
|
+
* entries all live under a single root folder (e.g. `mz-slides/theme.json`),
|
|
30
|
+
* matching Anthropic's custom-skill format. Recursively includes every file
|
|
31
|
+
* except node_modules, hidden entries (any name starting with `.`), and
|
|
32
|
+
* top-level build artifacts (.pptx/.pdf/.zip at the repo root; the template
|
|
33
|
+
* .pptx under template/ is kept). Subdirectory structure is preserved with
|
|
34
|
+
* POSIX slashes. Fails fast if nothing is left to zip.
|
|
35
|
+
*/
|
|
36
|
+
export async function zipDir(rootDir, folderName) {
|
|
37
|
+
const zip = new JSZip();
|
|
38
|
+
const folder = zip.folder(folderName);
|
|
39
|
+
if (!folder)
|
|
40
|
+
throw new Error(`Failed to create zip folder: ${folderName}`);
|
|
41
|
+
let count = 0;
|
|
42
|
+
const walk = (dir) => {
|
|
43
|
+
const atRoot = dir === rootDir;
|
|
44
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
45
|
+
if (entry.name.startsWith("."))
|
|
46
|
+
continue;
|
|
47
|
+
const abs = join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
if (!EXCLUDE_DIRS.has(entry.name))
|
|
50
|
+
walk(abs);
|
|
51
|
+
}
|
|
52
|
+
else if (entry.isFile()) {
|
|
53
|
+
if (atRoot && ROOT_ARTIFACT_EXTS.has(extname(entry.name)))
|
|
54
|
+
continue;
|
|
55
|
+
folder.file(relative(rootDir, abs).split(sep).join("/"), readFileSync(abs));
|
|
56
|
+
count++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk(rootDir);
|
|
61
|
+
if (count === 0)
|
|
62
|
+
throw new Error(`No files to zip in directory: ${rootDir}`);
|
|
63
|
+
return zip.generateAsync({ type: "nodebuffer" });
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tycoworks/tycoslide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"description": "Create editable, on-brand PowerPoint slides from markdown.",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"@xmldom/xmldom": "^0.9.10",
|
|
26
27
|
"commander": "^15.0.0",
|
|
27
28
|
"image-size": "^2.0.2",
|
|
29
|
+
"jszip": "^3.10.1",
|
|
28
30
|
"mermaid": "^11.16.1",
|
|
29
31
|
"playwright": "^1.58.2",
|
|
30
32
|
"pptx-automizer": "^0.8.2",
|
|
@@ -39,7 +41,6 @@
|
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@biomejs/biome": "^2.5.1",
|
|
41
43
|
"@types/node": "^22.20.0",
|
|
42
|
-
"jszip": "^3.10.1",
|
|
43
44
|
"typescript": "^5.8.0"
|
|
44
45
|
}
|
|
45
46
|
}
|
package/syntax.md
CHANGED
|
@@ -238,7 +238,7 @@ notes: |
|
|
|
238
238
|
---
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
To build with all speaker notes omitted, pass `--no-notes` to `tycoslide build` (see [README](README.md#cli)).
|
|
241
|
+
To build with all speaker notes omitted, pass `--no-notes` to `npx tycoslide build` (see [README](README.md#cli)).
|
|
242
242
|
|
|
243
243
|
---
|
|
244
244
|
|