@tycoworks/tycoslide 0.11.2 → 0.11.4
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 +13 -2
- package/SKILL.md +7 -6
- package/dist/cli.js +12 -2
- package/dist/markdown/blocks/code.js +1 -1
- package/dist/markdown/blocks/mermaid.d.ts +15 -0
- package/dist/markdown/blocks/mermaid.js +43 -4
- package/dist/markdown/blocks/text.js +1 -1
- package/dist/markdown/deckCompiler.js +14 -10
- package/dist/markdown/schema/deckSchema.d.ts +1 -1
- package/dist/markdown/schema/deckSchema.js +2 -2
- package/dist/markdown/schema/themeConfigSchema.d.ts +1 -1
- package/dist/markdown/types.d.ts +7 -1
- package/package.json +2 -3
- package/syntax.md +7 -7
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Create editable, on-brand PowerPoint slides from markdown.
|
|
|
16
16
|
npm install @tycoworks/tycoslide
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Mermaid diagrams need Chrome on the machine. See [Diagrams](#diagrams).
|
|
20
20
|
|
|
21
21
|
Create `deck.md`:
|
|
22
22
|
|
|
@@ -54,7 +54,7 @@ npx tycoslide build deck.md # → deck.pptx
|
|
|
54
54
|
```bash
|
|
55
55
|
npx tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
|
|
56
56
|
npx tycoslide build deck.md --no-notes # omit speaker notes from the output
|
|
57
|
-
npx tycoslide package # regenerate skill.md/syntax.md/manifest.json + zip the
|
|
57
|
+
npx tycoslide package # regenerate skill.md/syntax.md/manifest.json + zip the theme into a self-contained <package-name>.zip
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Theme Structure
|
|
@@ -78,3 +78,14 @@ my-theme/
|
|
|
78
78
|
**Layout** — a slide pattern in the template (Title, Body, Quote, etc.).
|
|
79
79
|
**Theme** — the directory that bundles a template, assets, and config.
|
|
80
80
|
**Manifest** — a machine-readable catalog of layouts and assets for AI agents.
|
|
81
|
+
|
|
82
|
+
## Diagrams
|
|
83
|
+
|
|
84
|
+
Mermaid blocks are rendered with a headless Chrome. tycoslide does not download one.
|
|
85
|
+
It uses a browser you already have, in this order:
|
|
86
|
+
|
|
87
|
+
1. `--browser-path <path>`
|
|
88
|
+
2. A system Chrome install
|
|
89
|
+
3. `npx playwright install chromium-headless-shell`
|
|
90
|
+
|
|
91
|
+
Decks without diagrams need no browser.
|
package/SKILL.md
CHANGED
|
@@ -17,6 +17,7 @@ npm install
|
|
|
17
17
|
|
|
18
18
|
This installs the tycoslide engine and its dependencies. You only need to do this once.
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
## Overview
|
|
21
22
|
|
|
22
23
|
This skill builds on-brand decks from a markdown deck file. The theme provides slide layouts that control design. Your job: pick the right layouts, fill them with content, and build. You never restyle the layout; the engine clones the real slides, so brand, layout, fonts, and chrome come for free.
|
|
@@ -35,12 +36,12 @@ This skill builds on-brand decks from a markdown deck file. The theme provides s
|
|
|
35
36
|
|
|
36
37
|
Before writing anything, read `manifest.json`. It contains:
|
|
37
38
|
|
|
38
|
-
- **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions)
|
|
39
|
+
- **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions). A layout is identified by its `name`; every parameter and slot by its `key`. Parameters carry a `type`, slots carry `accepts`, and either may be `required`
|
|
39
40
|
- **assets** -- brand logos, client logos, illustrations, and icons (`description`)
|
|
40
41
|
|
|
41
42
|
A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
|
|
42
43
|
- **parameters** -- one value on a frontmatter line. Types: `template`, `image`. Fill by putting a value under the parameter's key in the slide frontmatter.
|
|
43
|
-
- **slots** -- a multi-line region in the body. Accept types: `text`, `table`, `image`. Fill as a `::
|
|
44
|
+
- **slots** -- a multi-line region in the body. Accept types: `text`, `table`, `image`. Fill as a `::key::` region, using the slot's `key` from the manifest; a fenced code block routes to a `text` slot, a fenced mermaid block to an `image` slot.
|
|
44
45
|
|
|
45
46
|
A slot may accept more than one content type -- the manifest lists each slot's `accepts`, and the shape of the content you write selects which one.
|
|
46
47
|
|
|
@@ -105,7 +106,7 @@ subtitle: This Quarter
|
|
|
105
106
|
|
|
106
107
|
### Body content, slots, and formatting
|
|
107
108
|
|
|
108
|
-
See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::
|
|
109
|
+
See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), the parameter/slot split (parameters: template, image; slots accept: text, table, image), and image parameters.
|
|
109
110
|
|
|
110
111
|
### Build
|
|
111
112
|
|
|
@@ -140,7 +141,7 @@ Keep each slot's content to what its region comfortably holds. When content over
|
|
|
140
141
|
- **Don't repeat the same layout** -- vary layouts for visual rhythm
|
|
141
142
|
- **Don't overstuff a slot** -- keep content to what its region comfortably holds; split across slides when there's too much
|
|
142
143
|
- **Don't restyle the layout** -- the theme owns all design; you only fill slots
|
|
143
|
-
- **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration
|
|
144
|
+
- **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration. If you get a `shrunk to X%` warning, look at the rendered slide: if the image is now too small to make out, use a simpler one.
|
|
144
145
|
- **Don't invent layout or asset names** -- only use what exists in the manifest
|
|
145
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
|
|
146
147
|
|
|
@@ -156,7 +157,7 @@ Build the deck again ([Build](#build)) and read the output carefully. Common err
|
|
|
156
157
|
|
|
157
158
|
| Error | Fix |
|
|
158
159
|
|-------|-----|
|
|
159
|
-
| `
|
|
160
|
+
| `unknown layout "xyz"` | Check layout names in `manifest.json` |
|
|
160
161
|
| A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
|
|
161
162
|
| An image didn't swap / placeholder remains | Use the image parameter's key name in frontmatter, and an asset path that exists in `manifest.json` |
|
|
162
163
|
| YAML parse error | Fix the YAML syntax in the slide's frontmatter |
|
|
@@ -167,7 +168,7 @@ Build the deck again ([Build](#build)) and read the output carefully. Common err
|
|
|
167
168
|
### Verification Loop
|
|
168
169
|
|
|
169
170
|
1. Write the deck file → Build
|
|
170
|
-
2. **
|
|
171
|
+
2. **Fix the error** -- the build stops at the first one, so expect several rounds
|
|
171
172
|
3. Rebuild
|
|
172
173
|
4. **If content overflows**: reduce content or split into two slides
|
|
173
174
|
5. Repeat until the build exits cleanly
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ program
|
|
|
23
23
|
.argument("<deck>", "path to deck markdown file")
|
|
24
24
|
.option(`-c, --config <path>`, "override theme config path (default: read from frontmatter)")
|
|
25
25
|
.option("--no-notes", "omit speaker notes from the output (also strips any inherited template notes)")
|
|
26
|
+
.option("--browser-path <path>", "Chrome executable to render diagrams with (default: auto-detect)")
|
|
26
27
|
.action(async (deckPath, opts) => {
|
|
27
28
|
const absDeckPath = resolve(process.cwd(), deckPath);
|
|
28
29
|
let source;
|
|
@@ -42,7 +43,7 @@ program
|
|
|
42
43
|
if (!absConfigPath) {
|
|
43
44
|
throw new Error(`${basename(deckPath)}: missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
|
|
44
45
|
}
|
|
45
|
-
const config = loadThemeConfig(absConfigPath);
|
|
46
|
+
const config = { ...loadThemeConfig(absConfigPath), browserPath: opts.browserPath };
|
|
46
47
|
const deck = await compileDeck(doc, config);
|
|
47
48
|
// Always write the .pptx next to the input deck, named after it.
|
|
48
49
|
const outName = basename(deckPath).replace(/\.md$/, ".pptx");
|
|
@@ -83,4 +84,13 @@ program
|
|
|
83
84
|
writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName, config, generated));
|
|
84
85
|
console.log(`WROTE ${zipFile}`);
|
|
85
86
|
});
|
|
86
|
-
|
|
87
|
+
// Everything below the CLI throws plain Errors carrying a written-for-humans
|
|
88
|
+
// message. Print that message and stop; a Node stack trace tells a deck author
|
|
89
|
+
// nothing about their deck, and buries the part that does.
|
|
90
|
+
try {
|
|
91
|
+
await program.parseAsync(process.argv);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
@@ -18,7 +18,7 @@ export const CODE = {
|
|
|
18
18
|
// A fence with no language can't be highlighted (no Shiki grammar to pick),
|
|
19
19
|
// so fail fast naming the slot rather than attempt a language-less highlight.
|
|
20
20
|
if (!code.lang) {
|
|
21
|
-
throw new Error(`Slide ${ctx.
|
|
21
|
+
throw new Error(`Slide ${ctx.slideNo}: layout "${ctx.layoutName}" slot content (from ${ctx.source}) has a code fence ` +
|
|
22
22
|
"with no language; add one after the opening ``` (e.g. ```sql).");
|
|
23
23
|
}
|
|
24
24
|
const codeTheme = ctx.config.codeTheme;
|
|
@@ -13,3 +13,18 @@ export declare const MERMAID_LANG = "mermaid";
|
|
|
13
13
|
* throws by name.
|
|
14
14
|
*/
|
|
15
15
|
export declare const MERMAID: BlockHandler;
|
|
16
|
+
type Chromium = typeof import("playwright-core")["chromium"];
|
|
17
|
+
/**
|
|
18
|
+
* Find a browser rather than ship one. tycoslide never downloads Chromium: the
|
|
19
|
+
* binary comes from a CDN rather than the npm registry, so bundling it breaks
|
|
20
|
+
* installs anywhere egress is restricted to the registry, and pins the build to
|
|
21
|
+
* one revision that a machine's existing browser will rarely match.
|
|
22
|
+
*
|
|
23
|
+
* Three ways in, most explicit first: `--browser-path` names an executable
|
|
24
|
+
* outright (any build, the revision is not checked), `channel: "chrome"` picks
|
|
25
|
+
* up a system Chrome install, and the bare launch falls back to whatever `npx
|
|
26
|
+
* playwright install` put in Playwright's own cache. If none work, the error
|
|
27
|
+
* names every remedy a caller can act on.
|
|
28
|
+
*/
|
|
29
|
+
export declare function launchChromium(chromium: Chromium, browserPath?: string): Promise<Awaited<ReturnType<Chromium["launch"]>>>;
|
|
30
|
+
export {};
|
|
@@ -125,7 +125,7 @@ async function renderOne(definition, variantName, variant, cacheDir, compilerCon
|
|
|
125
125
|
const outputPath = join(cacheDir, `${key}.png`);
|
|
126
126
|
if (existsSync(outputPath))
|
|
127
127
|
return outputPath;
|
|
128
|
-
await renderMermaidToPng(processed, renderConfig, fonts, outputPath);
|
|
128
|
+
await renderMermaidToPng(processed, renderConfig, fonts, outputPath, compilerConfig.browserPath);
|
|
129
129
|
return outputPath;
|
|
130
130
|
}
|
|
131
131
|
let bundleCache = null;
|
|
@@ -142,6 +142,45 @@ function fontFaceCss(fonts) {
|
|
|
142
142
|
.map((f) => `@font-face { font-family: '${f.family}'; src: url('${f.url}') format('${f.format}'); font-weight: ${f.weight}; font-style: normal; }`)
|
|
143
143
|
.join("\n");
|
|
144
144
|
}
|
|
145
|
+
const LAUNCH_ARGS = { headless: true, args: ["--no-sandbox"] };
|
|
146
|
+
/**
|
|
147
|
+
* Find a browser rather than ship one. tycoslide never downloads Chromium: the
|
|
148
|
+
* binary comes from a CDN rather than the npm registry, so bundling it breaks
|
|
149
|
+
* installs anywhere egress is restricted to the registry, and pins the build to
|
|
150
|
+
* one revision that a machine's existing browser will rarely match.
|
|
151
|
+
*
|
|
152
|
+
* Three ways in, most explicit first: `--browser-path` names an executable
|
|
153
|
+
* outright (any build, the revision is not checked), `channel: "chrome"` picks
|
|
154
|
+
* up a system Chrome install, and the bare launch falls back to whatever `npx
|
|
155
|
+
* playwright install` put in Playwright's own cache. If none work, the error
|
|
156
|
+
* names every remedy a caller can act on.
|
|
157
|
+
*/
|
|
158
|
+
export async function launchChromium(chromium, browserPath) {
|
|
159
|
+
const attempts = [
|
|
160
|
+
...(browserPath
|
|
161
|
+
? [
|
|
162
|
+
{
|
|
163
|
+
label: `--browser-path ${browserPath}`,
|
|
164
|
+
launch: () => chromium.launch({ ...LAUNCH_ARGS, executablePath: browserPath }),
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
: []),
|
|
168
|
+
{ label: "system Chrome", launch: () => chromium.launch({ ...LAUNCH_ARGS, channel: "chrome" }) },
|
|
169
|
+
{ label: "Playwright's downloaded browser", launch: () => chromium.launch(LAUNCH_ARGS) },
|
|
170
|
+
];
|
|
171
|
+
const failures = [];
|
|
172
|
+
for (const attempt of attempts) {
|
|
173
|
+
try {
|
|
174
|
+
return await attempt.launch();
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
failures.push(` ${attempt.label}: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`No Chromium available to render mermaid. Tried:\n${failures.join("\n")}\n` +
|
|
181
|
+
"Fix by any one of: pass --browser-path, install Google Chrome, " +
|
|
182
|
+
"or run `npx playwright install chromium-headless-shell`.");
|
|
183
|
+
}
|
|
145
184
|
/**
|
|
146
185
|
* Render a mermaid definition to a transparent PNG via a headless Chromium
|
|
147
186
|
* (Playwright — the proven old driver, stronger headless font fidelity). The
|
|
@@ -151,7 +190,7 @@ function fontFaceCss(fonts) {
|
|
|
151
190
|
* font instead of a substitute. mmdc's `--cssFile` injects too late (after layout)
|
|
152
191
|
* to affect metrics, which is why this replaces its programmatic API.
|
|
153
192
|
*/
|
|
154
|
-
async function renderMermaidToPng(processed, renderConfig, fonts, outputPath) {
|
|
193
|
+
async function renderMermaidToPng(processed, renderConfig, fonts, outputPath, browserPath) {
|
|
155
194
|
const bundle = getMermaidBundle();
|
|
156
195
|
// JSON script blocks pass data without escaping issues; escape `</` so a value
|
|
157
196
|
// can't close the surrounding <script>.
|
|
@@ -200,12 +239,12 @@ ${fontFaceCss(fonts)}
|
|
|
200
239
|
// origin can't fetch the file:// font resources (@font-face silently fails),
|
|
201
240
|
// whereas a file://-origin document loads them.
|
|
202
241
|
const htmlPath = `${outputPath}.html`;
|
|
203
|
-
const { chromium } = await import("playwright");
|
|
242
|
+
const { chromium } = await import("playwright-core");
|
|
204
243
|
let browser;
|
|
205
244
|
try {
|
|
206
245
|
// Written inside the try so the finally always cleans it up, even if launch throws.
|
|
207
246
|
writeFileSync(htmlPath, html);
|
|
208
|
-
browser = await chromium
|
|
247
|
+
browser = await launchChromium(chromium, browserPath);
|
|
209
248
|
const page = await browser.newPage({ viewport: { width: 800, height: 600 }, deviceScaleFactor: 2 });
|
|
210
249
|
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "load" });
|
|
211
250
|
await page.waitForSelector(`#output[${RENDER_SIGNAL_ATTR}="${RenderSignal.Done}"]`, { timeout: 30000 });
|
|
@@ -34,7 +34,7 @@ export function compileTextAggregate(nodes, ctx) {
|
|
|
34
34
|
* (table/image/code) mixed into prose — both name the layout/slide/slot + type.
|
|
35
35
|
*/
|
|
36
36
|
function reject(nodeCount, nodeType, ctx) {
|
|
37
|
-
const where = `Slide ${ctx.
|
|
37
|
+
const where = `Slide ${ctx.slideNo}: layout "${ctx.layoutName}" slot content (from ${ctx.source})`;
|
|
38
38
|
if (nodeCount === 1) {
|
|
39
39
|
return `${where} is a standalone "${nodeType}" block, which is not a supported content kind.`;
|
|
40
40
|
}
|
|
@@ -29,10 +29,10 @@ const ASSET_REF_RE = /^\$([a-zA-Z]\w*)\.([a-zA-Z]\w*)$/;
|
|
|
29
29
|
* the author's markdown shape selects one. A type the slot does not accept fails
|
|
30
30
|
* fast, naming the layout, slot, the type it got, and the types the slot accepts.
|
|
31
31
|
*/
|
|
32
|
-
function assertSlotRegion(slot, got, layoutName,
|
|
32
|
+
function assertSlotRegion(slot, got, layoutName, slideNo, source) {
|
|
33
33
|
if (!slot.accepts.some((b) => b.type === got)) {
|
|
34
34
|
const accepted = slot.accepts.map((b) => b.type).join(", ");
|
|
35
|
-
throw new Error(`Slide ${
|
|
35
|
+
throw new Error(`Slide ${slideNo}: layout "${layoutName}" slot "${slot.key}" does not accept ${got} content ` +
|
|
36
36
|
`(from ${source}); it accepts: ${accepted}.`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -124,9 +124,13 @@ function validateLayout(layout) {
|
|
|
124
124
|
async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
125
125
|
const { layouts, rootDir } = config;
|
|
126
126
|
const { frontmatter, slots, index } = slide;
|
|
127
|
+
// Slide numbers in errors are 1-based, matching how an author counts slides in
|
|
128
|
+
// the deck file. Derive it once: every message below, and the parse-time errors
|
|
129
|
+
// in slideParser, must agree or the author looks at the wrong slide.
|
|
130
|
+
const slideNo = index + 1;
|
|
127
131
|
const layout = frontmatter[RESERVED_KEY.LAYOUT];
|
|
128
132
|
if (layout === undefined) {
|
|
129
|
-
throw new Error(`Slide ${
|
|
133
|
+
throw new Error(`Slide ${slideNo}: missing required "${RESERVED_KEY.LAYOUT}" in frontmatter`);
|
|
130
134
|
}
|
|
131
135
|
// Speaker notes are slide-level metadata, stripped from frontmatter before
|
|
132
136
|
// slot/param resolution — exactly like `layout`. Coerce to string if present.
|
|
@@ -138,12 +142,12 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
138
142
|
const layoutDef = layouts.find((l) => l.name === layoutName);
|
|
139
143
|
if (!layoutDef) {
|
|
140
144
|
const known = layouts.map((l) => l.name).join(", ");
|
|
141
|
-
throw new Error(`Slide ${
|
|
145
|
+
throw new Error(`Slide ${slideNo}: unknown layout "${layoutName}". Available layouts: ${known}`);
|
|
142
146
|
}
|
|
143
147
|
// Reject any frontmatter key not declared by this layout's parameters (reserved
|
|
144
148
|
// layout/notes stripped first). The per-layout strict schema IS the unknown-key
|
|
145
149
|
// check — it fires before the resolution loop, so that loop only sees valid keys.
|
|
146
|
-
validateSlideFrontmatter(frontmatter, layoutDef,
|
|
150
|
+
validateSlideFrontmatter(frontmatter, layoutDef, slideNo);
|
|
147
151
|
// Map each author-facing key to its owning parameter: template keys → the template
|
|
148
152
|
// parameter that declares them, image keys → the image parameter. validateLayout
|
|
149
153
|
// (run once per layout in compileDeck) has already proven these key spaces are
|
|
@@ -202,7 +206,7 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
202
206
|
const supplied = valuesByTemplateParam.get(templateParam);
|
|
203
207
|
if (!supplied) {
|
|
204
208
|
if (templateParam.required) {
|
|
205
|
-
throw new Error(`Slide ${
|
|
209
|
+
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires template parameter "${templateParam.shapeName}" ` +
|
|
206
210
|
`(keys: ${templateKeys(templateParam.template).join(", ")}); none provided`);
|
|
207
211
|
}
|
|
208
212
|
continue;
|
|
@@ -215,14 +219,14 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
215
219
|
for (const [name, text] of Object.entries(slots)) {
|
|
216
220
|
const slot = slotsByKey.get(name);
|
|
217
221
|
if (!slot) {
|
|
218
|
-
throw new Error(`Slide ${
|
|
222
|
+
throw new Error(`Slide ${slideNo}: unknown slot "::${name}::" in layout "${layoutName}". ` +
|
|
219
223
|
`Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
|
|
220
224
|
}
|
|
221
225
|
const source = `::${name}::`;
|
|
222
226
|
const parsed = parseSlotContent(text, {
|
|
223
227
|
resolveAssetRef,
|
|
224
228
|
layoutName,
|
|
225
|
-
|
|
229
|
+
slideNo: slideNo,
|
|
226
230
|
source,
|
|
227
231
|
config,
|
|
228
232
|
layoutVariant: layoutDef.variant,
|
|
@@ -235,12 +239,12 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
235
239
|
// enforced during expansion above.
|
|
236
240
|
for (const image of imageByKey.values()) {
|
|
237
241
|
if (image.required && content[image.key] === undefined) {
|
|
238
|
-
throw new Error(`Slide ${
|
|
242
|
+
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
|
|
239
243
|
}
|
|
240
244
|
}
|
|
241
245
|
for (const slot of layoutDef.slots) {
|
|
242
246
|
if (slot.required && content[slot.key] === undefined) {
|
|
243
|
-
throw new Error(`Slide ${
|
|
247
|
+
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
// Content values are already engine fills — code fences highlighted to TextFill
|
|
@@ -27,4 +27,4 @@ export declare function deckFrontmatterSchema(layout: CompilerLayout): z.ZodObje
|
|
|
27
27
|
* shared `strict` "Valid keys: …" formatter). Reserved keys (`layout`, `notes`) are
|
|
28
28
|
* slide-level metadata, not parameters, so they are stripped before `safeParse`.
|
|
29
29
|
*/
|
|
30
|
-
export declare function validateSlideFrontmatter(frontmatter: Record<string, unknown>, layout: CompilerLayout,
|
|
30
|
+
export declare function validateSlideFrontmatter(frontmatter: Record<string, unknown>, layout: CompilerLayout, slideNo: number): void;
|
|
@@ -42,10 +42,10 @@ export function deckFrontmatterSchema(layout) {
|
|
|
42
42
|
* shared `strict` "Valid keys: …" formatter). Reserved keys (`layout`, `notes`) are
|
|
43
43
|
* slide-level metadata, not parameters, so they are stripped before `safeParse`.
|
|
44
44
|
*/
|
|
45
|
-
export function validateSlideFrontmatter(frontmatter, layout,
|
|
45
|
+
export function validateSlideFrontmatter(frontmatter, layout, slideNo) {
|
|
46
46
|
const { [RESERVED_KEY.LAYOUT]: _layout, [RESERVED_KEY.NOTES]: _notes, ...params } = frontmatter;
|
|
47
47
|
const result = deckFrontmatterSchema(layout).safeParse(params);
|
|
48
48
|
if (!result.success) {
|
|
49
|
-
throw new Error(`Slide ${
|
|
49
|
+
throw new Error(`Slide ${slideNo}: ${z.prettifyError(result.error)}`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -49,8 +49,8 @@ export declare const ThemeConfigSchema: z.ZodObject<{
|
|
|
49
49
|
assets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
50
50
|
path: z.ZodString;
|
|
51
51
|
type: z.ZodEnum<{
|
|
52
|
-
image: "image";
|
|
53
52
|
icon: "icon";
|
|
53
|
+
image: "image";
|
|
54
54
|
background: "background";
|
|
55
55
|
}>;
|
|
56
56
|
description: z.ZodString;
|
package/dist/markdown/types.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export type BlockFill = TextFill | TableFill | ImageFill;
|
|
|
93
93
|
export type BlockContext = {
|
|
94
94
|
resolveAssetRef: (ref: string) => ImageFill;
|
|
95
95
|
layoutName: string;
|
|
96
|
-
|
|
96
|
+
slideNo: number;
|
|
97
97
|
source: string;
|
|
98
98
|
config: CompilerConfig;
|
|
99
99
|
/**
|
|
@@ -308,4 +308,10 @@ export type CompilerThemeConfig = {
|
|
|
308
308
|
};
|
|
309
309
|
export type CompilerConfig = CompilerThemeConfig & {
|
|
310
310
|
rootDir: string;
|
|
311
|
+
/**
|
|
312
|
+
* Browser executable to render mermaid with, from `--browser-path`. Added
|
|
313
|
+
* after load like `rootDir`: it is a per-invocation choice, not a theme's.
|
|
314
|
+
* Unset means discover one (see `launchChromium`).
|
|
315
|
+
*/
|
|
316
|
+
browserPath?: string;
|
|
311
317
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tycoworks/tycoslide",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Create editable, on-brand PowerPoint slides from markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,13 +23,12 @@
|
|
|
23
23
|
"format": "biome format --write ."
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@playwright/browser-chromium": "^1.62.1",
|
|
27
26
|
"@xmldom/xmldom": "^0.9.10",
|
|
28
27
|
"commander": "^15.0.0",
|
|
29
28
|
"image-size": "^2.0.2",
|
|
30
29
|
"jszip": "^3.10.1",
|
|
31
30
|
"mermaid": "^11.16.1",
|
|
32
|
-
"playwright": "^1.62.1",
|
|
31
|
+
"playwright-core": "^1.62.1",
|
|
33
32
|
"pptx-automizer": "^0.8.2",
|
|
34
33
|
"remark-gfm": "^4.0.1",
|
|
35
34
|
"remark-ins": "^1.2.5",
|
package/syntax.md
CHANGED
|
@@ -24,7 +24,7 @@ The output `.pptx` is written next to the deck file, named after it (`deck.md`
|
|
|
24
24
|
|
|
25
25
|
## Body content
|
|
26
26
|
|
|
27
|
-
A slide's body is split into
|
|
27
|
+
A slide's body is split into regions with `::key::` markers. Each region fills the slot whose `key` matches the marker.
|
|
28
28
|
|
|
29
29
|
```markdown
|
|
30
30
|
---
|
|
@@ -58,9 +58,9 @@ Inline formatting is supported in body content:
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
## Named slots with `::
|
|
61
|
+
## Named slots with `::key::` markers
|
|
62
62
|
|
|
63
|
-
For layouts with multiple content regions (e.g., two-column layouts), use `::
|
|
63
|
+
For layouts with multiple content regions (e.g., two-column layouts), use `::key::` markers to split body content into named slots.
|
|
64
64
|
|
|
65
65
|
```markdown
|
|
66
66
|
---
|
|
@@ -87,7 +87,7 @@ Content after a marker goes to the slot matching that name. The marker names mus
|
|
|
87
87
|
|
|
88
88
|
## Parameters and slots (in `manifest.json`)
|
|
89
89
|
|
|
90
|
-
A layout advertises two kinds of author-facing input, split by one rule: **a parameter is one value on a frontmatter line; a slot is a multi-line region in the body** (a `::
|
|
90
|
+
A layout advertises two kinds of author-facing input, split by one rule: **a parameter is one value on a frontmatter line; a slot is a multi-line region in the body** (a `::key::` region). In `manifest.json` each layout carries two lists, `parameters` and `slots`:
|
|
91
91
|
|
|
92
92
|
```jsonc
|
|
93
93
|
{
|
|
@@ -125,7 +125,7 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
|
|
|
125
125
|
|
|
126
126
|
### Body content shapes
|
|
127
127
|
|
|
128
|
-
Fill a slot by writing a `::
|
|
128
|
+
Fill a slot by writing a `::key::` region in the body; the marker maps to the slot with that `key`. A slot's manifest entry lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
|
|
129
129
|
|
|
130
130
|
- **text** (slots that accept `text`) -- markdown paragraphs and bullets, rebuilt from the template's specimen paragraph styles. A fenced code block also routes here:
|
|
131
131
|
````markdown
|
|
@@ -227,7 +227,7 @@ Each layout also declares a `slideNumber` pointing at the physical slide in the
|
|
|
227
227
|
|
|
228
228
|
Any slide may carry a `notes:` key in its frontmatter -- a plain-text speaker-notes block attached to that slide's notes page. It is slide-level metadata, not a parameter or a slot: it is never routed to a shape and never appears on the slide face, only in the presenter/notes view.
|
|
229
229
|
|
|
230
|
-
Write multiple lines with a YAML block scalar (`|`); each line becomes one notes paragraph.
|
|
230
|
+
Write multiple lines with a YAML block scalar (`|`); each line becomes one notes paragraph. Template notes on the underlying slide are always stripped, so only what you author here shows up.
|
|
231
231
|
|
|
232
232
|
```yaml
|
|
233
233
|
---
|
|
@@ -239,7 +239,7 @@ notes: |
|
|
|
239
239
|
---
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
To build with all speaker notes omitted, pass `--no-notes` to `npx tycoslide build
|
|
242
|
+
To build with all speaker notes omitted, pass `--no-notes` to `npx tycoslide build`.
|
|
243
243
|
|
|
244
244
|
---
|
|
245
245
|
|