@tycoworks/tycoslide 0.13.3 → 0.14.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 CHANGED
@@ -1,26 +1,28 @@
1
1
  # tycoslide
2
2
 
3
- Create editable, on-brand PowerPoint slides from markdown.
3
+ Create editable, on-brand PowerPoint slides from markdown, using your existing .pptx templates.
4
4
 
5
- > **Early release** tycoslide is under active development.
5
+ > **Early release.** tycoslide is under active development.
6
6
 
7
- ## How it works
7
+ ## Getting started
8
8
 
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.**
9
+ 1. **Create a tycoslide theme.** Install the [create-theme](skills/create-theme) skill (`npx skills add tycoworks/tycoslide`) and give an agent such as Claude Code or Codex your `.pptx`. You'll get back an npm package of mapped layouts, colors, and visual assets, like the [tycoworks-theme](https://github.com/tycoworks/tycoworks-theme).
10
+ 2. **Write slides in markdown.** Bullets, tables, images, speaker notes, syntax-highlighted code and mermaid diagrams are all supported. Every theme comes with an agent skill, so an agent can write the markdown for you.
11
+ 3. **Build.** `npx tycoslide build deck.md` compiles the markdown into an editable PowerPoint file.
12
12
 
13
- ## Quick Start
13
+ ## Example
14
+
15
+ [tycoworks-theme](https://github.com/tycoworks/tycoworks-theme) is a finished theme wrapping `template/tycoworks-demo.pptx`. Clone it and build its showcase deck, 21 slides covering all 18 layouts:
14
16
 
15
17
  ```bash
16
- npm install @tycoworks/tycoslide
18
+ git clone https://github.com/tycoworks/tycoworks-theme && cd tycoworks-theme
19
+ npm install
20
+ npx tycoslide build showcase.md
17
21
  ```
18
22
 
19
- Mermaid diagrams need Chrome on the machine. See [Diagrams](#diagrams).
20
-
21
- Create `deck.md`:
23
+ A deck file against that theme looks like this (full syntax in [syntax.md](theme-package/syntax.md)):
22
24
 
23
- ```markdown
25
+ ````markdown
24
26
  ---
25
27
  theme: ./theme.json
26
28
  ---
@@ -33,64 +35,35 @@ jobTitle: Engineering
33
35
  ---
34
36
 
35
37
  ---
36
- layout: Body
37
- title: Highlights
38
+ layout: Image right
39
+ title: How requests flow
38
40
  ---
39
41
 
40
42
  ::body::
41
43
 
42
- - Revenue up 12% quarter-over-quarter
43
- - Three major product launches completed
44
- ```
45
-
46
- Build:
44
+ - Every request is checked before it reaches the model
45
+ - Rejected requests never leave the gateway
47
46
 
48
- ```bash
49
- npx tycoslide build deck.md # → deck.pptx
50
- ```
47
+ ::image::
51
48
 
52
- ## CLI
53
-
54
- ```bash
55
- npx tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
56
- npx tycoslide build deck.md --no-notes # omit speaker notes from the output
57
- npx tycoslide package # regenerate the skill files + zip the theme into a self-contained <package-name>.zip
49
+ ```mermaid
50
+ flowchart TD
51
+ A[Client] --> B[Gateway] --> C[Model]
58
52
  ```
59
53
 
60
- ## Theme Structure
54
+ ---
55
+ layout: Code
56
+ title: Calling the API
57
+ ---
61
58
 
62
- A theme packages a PPTX template, design assets, and a config file into one directory.
59
+ ::code::
63
60
 
61
+ ```python
62
+ client = Client(api_key)
63
+ deck = client.build("deck.md")
64
64
  ```
65
- my-theme/
66
- template/corp-template.pptx
67
- assets/logos/
68
- assets/icons/
69
- theme.json
70
- package.json
71
- SKILL.md # generated by `tycoslide package`
72
- syntax.md # generated by `tycoslide package`
73
- manifest.json # generated by `tycoslide package` -- the theme's layouts
74
- assets.json # generated by `tycoslide package` -- the theme's picture catalog
75
- assets.dat # generated by `tycoslide package` -- every declared asset, in one file
76
- <package-name>.zip # uploadable Agent Skill bundle
77
- ```
78
-
79
- **Template** — the PPTX file with named shapes that tycoslide fills.
80
- **Layout** — a slide pattern in the template (Title, Body, Quote, etc.).
81
- **Theme** — the directory that bundles a template, assets, and config.
82
- **Manifest** — the machine-readable list of layouts, for an AI agent to read.
83
- **Catalog** — the machine-readable list of assets, for an AI agent to search.
84
-
85
- A packaged skill ships its assets as one `assets.dat` archive. `tycoslide build` expands it automatically before filling; the catalog stays a plain file.
86
-
87
- ## Diagrams
88
-
89
- Mermaid blocks are rendered with a headless Chrome. tycoslide does not download one.
90
- It uses a browser you already have, in this order:
65
+ ````
91
66
 
92
- 1. `--browser-path <path>`
93
- 2. A system Chrome install
94
- 3. `npx playwright install chromium-headless-shell`
67
+ ## Requirements
95
68
 
96
- Decks without diagrams need no browser.
69
+ Node 23.6 or later. Mermaid diagrams need Chrome on the machine; tycoslide finds an installed one, or run `npx playwright install chromium-headless-shell`.
package/dist/cli.js CHANGED
@@ -2,14 +2,14 @@ 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
- import { ASSETS_FILE, MANIFEST_FILE, SKILL_FILE, SYNTAX_FILE, THEME_CONFIG } from "./files.js";
5
+ import { ASSETS_FILE, MANIFEST_FILE, SKILL_FILE, SYNTAX_FILE, THEME_CONFIG, THEME_PACKAGE_DIR } from "./files.js";
6
6
  import { buildDeck } from "./index.js";
7
7
  import { generateAssetCatalog, generateManifest } from "./manifest.js";
8
8
  import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
9
9
  import { renameSkill, skillPackageJson, zipDir } from "./skillZip.js";
10
10
  const sdkDir = dirname(fileURLToPath(import.meta.url));
11
- const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
12
- const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
11
+ const skillMdPath = resolve(sdkDir, "..", THEME_PACKAGE_DIR, SKILL_FILE);
12
+ const syntaxMdPath = resolve(sdkDir, "..", THEME_PACKAGE_DIR, SYNTAX_FILE);
13
13
  const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
14
14
  const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
15
15
  program
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Post-write sweep of relationships whose target part no longer exists.
3
+ *
4
+ * pptx-automizer's `cleanup` removes media parts that no shape embeds any more,
5
+ * and `removeExistingSlides` drops the root template's original slide parts.
6
+ * Neither touches the `.rels` files that named them, so a filled picture leaves
7
+ * two dead entries behind (the template's placeholder image and automizer's
8
+ * copy of it) and every removed template slide leaves an orphan rels file.
9
+ * PowerPoint treats a relationship to a missing part as a damaged package.
10
+ *
11
+ * Runs on the written file: cleanup is the last thing automizer does during
12
+ * `write()`, and it offers no hook after it.
13
+ */
14
+ export type PruneResult = {
15
+ removedEntries: number;
16
+ removedParts: number;
17
+ };
18
+ export declare function pruneDanglingRels(outputPath: string): Promise<PruneResult>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Post-write sweep of relationships whose target part no longer exists.
3
+ *
4
+ * pptx-automizer's `cleanup` removes media parts that no shape embeds any more,
5
+ * and `removeExistingSlides` drops the root template's original slide parts.
6
+ * Neither touches the `.rels` files that named them, so a filled picture leaves
7
+ * two dead entries behind (the template's placeholder image and automizer's
8
+ * copy of it) and every removed template slide leaves an orphan rels file.
9
+ * PowerPoint treats a relationship to a missing part as a damaged package.
10
+ *
11
+ * Runs on the written file: cleanup is the last thing automizer does during
12
+ * `write()`, and it offers no hook after it.
13
+ */
14
+ import { readFileSync, writeFileSync } from "node:fs";
15
+ import { posix } from "node:path";
16
+ import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
17
+ import JSZip from "jszip";
18
+ import { Attr, Tag } from "./dom.js";
19
+ const SLIDE_RELS = /^ppt\/slides\/_rels\/(slide\d+\.xml)\.rels$/;
20
+ const SLIDES_DIR = "ppt/slides";
21
+ export async function pruneDanglingRels(outputPath) {
22
+ const zip = await JSZip.loadAsync(readFileSync(outputPath));
23
+ const parts = new Set(Object.keys(zip.files).filter((name) => !zip.files[name].dir));
24
+ const result = { removedEntries: 0, removedParts: 0 };
25
+ for (const relsPart of [...parts]) {
26
+ const match = SLIDE_RELS.exec(relsPart);
27
+ if (!match)
28
+ continue;
29
+ if (!parts.has(`${SLIDES_DIR}/${match[1]}`)) {
30
+ zip.remove(relsPart);
31
+ result.removedParts++;
32
+ continue;
33
+ }
34
+ const doc = new DOMParser().parseFromString(await zip.file(relsPart).async("string"), "text/xml");
35
+ const rels = doc.getElementsByTagName(Tag.RELATIONSHIP);
36
+ const dead = [];
37
+ for (let i = 0; i < rels.length; i++) {
38
+ const target = rels[i].getAttribute(Attr.TARGET);
39
+ if (!target || rels[i].getAttribute("TargetMode") === "External")
40
+ continue;
41
+ if (!parts.has(posix.normalize(posix.join(SLIDES_DIR, target))))
42
+ dead.push(rels[i]);
43
+ }
44
+ if (dead.length === 0)
45
+ continue;
46
+ for (const rel of dead)
47
+ rel.parentNode?.removeChild(rel);
48
+ result.removedEntries += dead.length;
49
+ zip.file(relsPart, new XMLSerializer().serializeToString(doc));
50
+ }
51
+ if (result.removedEntries > 0 || result.removedParts > 0) {
52
+ writeFileSync(outputPath, await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }));
53
+ }
54
+ return result;
55
+ }
package/dist/files.d.ts CHANGED
@@ -18,5 +18,6 @@ export declare const ASSETS_FILE = "assets.json";
18
18
  export declare const ASSETS_ARCHIVE = "assets.dat";
19
19
  export declare const SKILL_FILE = "SKILL.md";
20
20
  export declare const SYNTAX_FILE = "syntax.md";
21
+ export declare const THEME_PACKAGE_DIR = "theme-package";
21
22
  /** The manifest a packaged skill installs from, authored rather than copied. */
22
23
  export declare const PACKAGE_JSON = "package.json";
package/dist/files.js CHANGED
@@ -21,5 +21,8 @@ export const ASSETS_ARCHIVE = "assets.dat";
21
21
  // other.
22
22
  export const SKILL_FILE = "SKILL.md";
23
23
  export const SYNTAX_FILE = "syntax.md";
24
+ // The folder holding the SKILL.md template copied into each packaged theme, kept
25
+ // out of the repo root so skill installers do not mistake it for this repo's own skill.
26
+ export const THEME_PACKAGE_DIR = "theme-package";
24
27
  /** The manifest a packaged skill installs from, authored rather than copied. */
25
28
  export const PACKAGE_JSON = "package.json";
@@ -143,6 +143,8 @@ function fontFaceCss(fonts) {
143
143
  .join("\n");
144
144
  }
145
145
  const LAUNCH_ARGS = { headless: true, args: ["--no-sandbox"] };
146
+ /** Device pixels per CSS pixel when rasterising a diagram. Sized for print density. */
147
+ const MERMAID_RASTER_SCALE = 6;
146
148
  /**
147
149
  * Find a browser rather than ship one. tycoslide never downloads Chromium: the
148
150
  * binary comes from a CDN rather than the npm registry, so bundling it breaks
@@ -245,7 +247,13 @@ ${fontFaceCss(fonts)}
245
247
  // Written inside the try so the finally always cleans it up, even if launch throws.
246
248
  writeFileSync(htmlPath, html);
247
249
  browser = await launchChromium(chromium, browserPath);
248
- const page = await browser.newPage({ viewport: { width: 800, height: 600 }, deviceScaleFactor: 2 });
250
+ // Diagrams land in slide-sized frames, so they are rasterised well above CSS
251
+ // scale: a typical flowchart is ~270 CSS px wide against a slot wanting
252
+ // ~1400 px at print density. 2x left them visibly pixelated.
253
+ const page = await browser.newPage({
254
+ viewport: { width: 800, height: 600 },
255
+ deviceScaleFactor: MERMAID_RASTER_SCALE,
256
+ });
249
257
  await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "load" });
250
258
  await page.waitForSelector(`#output[${RENDER_SIGNAL_ATTR}="${RenderSignal.Done}"]`, { timeout: 30000 });
251
259
  const error = await page.getAttribute("#output", RENDER_ERROR_ATTR);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.13.3",
4
- "description": "Create editable, on-brand PowerPoint slides from markdown.",
3
+ "version": "0.14.0",
4
+ "description": "Create editable, on-brand PowerPoint slides from markdown, using your existing .pptx templates.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -11,12 +11,14 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "bin",
14
- "SKILL.md",
15
- "syntax.md"
14
+ "theme-package"
16
15
  ],
16
+ "engines": {
17
+ "node": ">=23.6"
18
+ },
17
19
  "scripts": {
18
20
  "build": "tsc --build",
19
- "test": "tsc --build && tsc -p tsconfig.test.json && node --test --experimental-transform-types test/**/*.test.ts",
21
+ "test": "tsc --build && tsc -p tsconfig.test.json && node --test test/**/*.test.ts",
20
22
  "typecheck": "tsc --build && tsc -p tsconfig.test.json",
21
23
  "lint": "biome check .",
22
24
  "lint:fix": "biome check --write .",
File without changes
File without changes