@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 CHANGED
@@ -16,7 +16,7 @@ Create editable, on-brand PowerPoint slides from markdown.
16
16
  npm install @tycoworks/tycoslide
17
17
  ```
18
18
 
19
- Installing also downloads Chromium (~554MB), via `@playwright/browser-chromium`, which tycoslide uses to render mermaid diagrams. If you install with `--ignore-scripts`, run `npx playwright install chromium-headless-shell` before building a deck with diagrams.
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 whole theme into a self-contained <package-name>.zip
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), each with `accepts` and optionally `required` (plus, for assets, a `type` of `icon`/`image`/`background`)
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 `::name::` region; a fenced code block routes to a `text` slot, a fenced mermaid block to an `image` slot.
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 (`::name::` markers), the parameter/slot split (parameters: template, image; slots accept: text, table, image), and image parameters.
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; if the build warns an image shrank to a small %, swap it for a simpler one
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
- | `Unknown layout: 'xyz'` | Check layout names in `manifest.json` |
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 &rarr; Build
170
- 2. **Read every error** -- fix all of them
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
- await program.parseAsync(process.argv);
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.slideIdx}: layout "${ctx.layoutName}" slot content (from ${ctx.source}) has a code fence ` +
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.launch({ headless: true, args: ["--no-sandbox"] });
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.slideIdx}: layout "${ctx.layoutName}" slot content (from ${ctx.source})`;
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, slideIdx, source) {
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 ${slideIdx}: layout "${layoutName}" slot "${slot.key}" does not accept ${got} content ` +
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 ${index}: missing required "${RESERVED_KEY.LAYOUT}" in frontmatter`);
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 ${index}: unknown layout "${layoutName}". Available layouts: ${known}`);
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, index);
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 ${index}: layout "${layoutName}" requires template parameter "${templateParam.shapeName}" ` +
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 ${index}: unknown slot "::${name}::" in layout "${layoutName}". ` +
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
- slideIdx: index,
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 ${index}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
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 ${index}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
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, slideIdx: number): void;
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, slideIdx) {
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 ${slideIdx}: ${z.prettifyError(result.error)}`);
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;
@@ -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
- slideIdx: number;
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.2",
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 named regions with `::name::` markers. Each region fills the slot whose key matches the marker name.
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 `::name::` markers
61
+ ## Named slots with `::key::` markers
62
62
 
63
- For layouts with multiple content regions (e.g., two-column layouts), use `::name::` markers to split body content into named slots.
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 `::name::` region). In `manifest.json` each layout carries two lists, `parameters` and `slots`:
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 `::name::` region in the body; the marker maps to the slot of that name. A slot's manifest entry lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
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. Blank template notes on the underlying slide are always stripped, so only what you author here shows up.
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` (see [README](README.md#cli)).
242
+ To build with all speaker notes omitted, pass `--no-notes` to `npx tycoslide build`.
243
243
 
244
244
  ---
245
245