@tycoworks/tycoslide 0.13.3 → 0.14.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 +33 -60
- package/dist/cli.js +3 -3
- package/dist/engine/dom.js +0 -3
- package/dist/engine/fillers/image.js +3 -1
- package/dist/engine/rels.d.ts +18 -0
- package/dist/engine/rels.js +55 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.js +3 -0
- package/dist/markdown/blocks/mermaid.js +9 -1
- package/package.json +7 -5
- /package/{SKILL.md → theme-package/SKILL.md} +0 -0
- /package/{syntax.md → theme-package/syntax.md} +0 -0
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
|
|
5
|
+
> **Early release.** tycoslide is under active development.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Getting started
|
|
8
8
|
|
|
9
|
-
1. **tycoslide
|
|
10
|
-
2. **
|
|
11
|
-
3. **tycoslide
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
37
|
-
title:
|
|
38
|
+
layout: Image right
|
|
39
|
+
title: How requests flow
|
|
38
40
|
---
|
|
39
41
|
|
|
40
42
|
::body::
|
|
41
43
|
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Build:
|
|
44
|
+
- Every request is checked before it reaches the model
|
|
45
|
+
- Rejected requests never leave the gateway
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
npx tycoslide build deck.md # → deck.pptx
|
|
50
|
-
```
|
|
47
|
+
::image::
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
54
|
+
---
|
|
55
|
+
layout: Code
|
|
56
|
+
title: Calling the API
|
|
57
|
+
---
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
::code::
|
|
63
60
|
|
|
61
|
+
```python
|
|
62
|
+
client = Client(api_key)
|
|
63
|
+
deck = client.build("deck.md")
|
|
64
64
|
```
|
|
65
|
-
|
|
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
|
-
|
|
93
|
-
2. A system Chrome install
|
|
94
|
-
3. `npx playwright install chromium-headless-shell`
|
|
67
|
+
## Requirements
|
|
95
68
|
|
|
96
|
-
|
|
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
|
package/dist/engine/dom.js
CHANGED
|
@@ -346,9 +346,6 @@ export function rebuildParagraphs(shape, paragraphs, startIndex, relation, shape
|
|
|
346
346
|
continue;
|
|
347
347
|
const isBullet = para.bullet !== undefined;
|
|
348
348
|
const inLevel = para.bullet?.level ?? 0;
|
|
349
|
-
// Skip fully-empty text (matches previous fillText behavior).
|
|
350
|
-
if (para.runs.length === 1 && !para.runs[0].text)
|
|
351
|
-
continue;
|
|
352
349
|
// Bulleted content needs a bulleted specimen to model on. A plain-only shape
|
|
353
350
|
// (e.g. a designer "description" line, buNone) accepts plain text only, so
|
|
354
351
|
// bulleting it is an authoring error — fail fast rather than emit a run with
|
|
@@ -83,7 +83,9 @@ export function computeGeometry(frame, imgW, imgH, fit) {
|
|
|
83
83
|
if (scaleRatio > 1) {
|
|
84
84
|
warnings.push(`enlarged to ${Math.round(scaleRatio * 100)}% of native — will look soft; supply a larger image`);
|
|
85
85
|
}
|
|
86
|
-
else if (scaleRatio < MIN_SCALE) {
|
|
86
|
+
else if (scaleRatio < MIN_SCALE && fit !== ImageFit.ScaleDown) {
|
|
87
|
+
// An icon's contract is "never enlarge"; landing small in an icon well is
|
|
88
|
+
// its normal use, so only image and background warn about shrinking.
|
|
87
89
|
warnings.push(`shrunk to ${Math.round(scaleRatio * 100)}% of native — the slot is far smaller than the image`);
|
|
88
90
|
}
|
|
89
91
|
const shownW = imgW * scale;
|
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"description": "Create editable, on-brand PowerPoint slides from markdown.",
|
|
3
|
+
"version": "0.14.1",
|
|
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
|
-
"
|
|
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
|
|
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
|