@tycoworks/tycoslide 0.7.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/LICENSE +21 -0
- package/README.md +73 -0
- package/SKILL.md +249 -0
- package/bin/tycoslide.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +197 -0
- package/dist/engine/dom.d.ts +92 -0
- package/dist/engine/dom.js +354 -0
- package/dist/engine/fillers/filler.d.ts +22 -0
- package/dist/engine/fillers/filler.js +53 -0
- package/dist/engine/fillers/image.d.ts +19 -0
- package/dist/engine/fillers/image.js +105 -0
- package/dist/engine/fillers/table.d.ts +21 -0
- package/dist/engine/fillers/table.js +62 -0
- package/dist/engine/fillers/template.d.ts +27 -0
- package/dist/engine/fillers/template.js +221 -0
- package/dist/engine/fillers/text.d.ts +28 -0
- package/dist/engine/fillers/text.js +29 -0
- package/dist/engine/generate.d.ts +51 -0
- package/dist/engine/generate.js +161 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/types.d.ts +128 -0
- package/dist/engine/types.js +22 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +146 -0
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js +88 -0
- package/dist/markdown/deckCompiler.d.ts +36 -0
- package/dist/markdown/deckCompiler.js +289 -0
- package/dist/markdown/index.d.ts +13 -0
- package/dist/markdown/index.js +13 -0
- package/dist/markdown/parsers.d.ts +32 -0
- package/dist/markdown/parsers.js +233 -0
- package/dist/markdown/resolvers/code.d.ts +17 -0
- package/dist/markdown/resolvers/code.js +44 -0
- package/dist/markdown/resolvers/mermaid.d.ts +14 -0
- package/dist/markdown/resolvers/mermaid.js +89 -0
- package/dist/markdown/resolvers/mermaidTheme.d.ts +27 -0
- package/dist/markdown/resolvers/mermaidTheme.js +113 -0
- package/dist/markdown/resolvers/resolver.d.ts +42 -0
- package/dist/markdown/resolvers/resolver.js +52 -0
- package/dist/markdown/slideParser.d.ts +14 -0
- package/dist/markdown/slideParser.js +196 -0
- package/dist/markdown/textTemplate.d.ts +15 -0
- package/dist/markdown/textTemplate.js +75 -0
- package/dist/markdown/types.d.ts +239 -0
- package/dist/markdown/types.js +40 -0
- package/package.json +41 -0
- package/syntax.md +291 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tycoworks LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# tycoslide
|
|
2
|
+
|
|
3
|
+
Generate slides from markdown using your real PowerPoint templates.
|
|
4
|
+
|
|
5
|
+
> **Early release** — tycoslide is under active development.
|
|
6
|
+
|
|
7
|
+
## Why tycoslide?
|
|
8
|
+
|
|
9
|
+
AI can write great slide content, but it can never get things on-brand. No matter what you try, fonts, logos, and colors end up slightly wrong, and you spend hours fixing it by hand.
|
|
10
|
+
|
|
11
|
+
tycoslide helps AI agents build presentations using your real slide templates, so they're always on-brand. You define a theme with your .pptx files, layouts and design assets, from which agents can quickly build new presentations using markdown.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @tycoworks/tycoslide
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Create `deck.md`:
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
---
|
|
23
|
+
theme: ./theme.json
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
layout: Title
|
|
28
|
+
title: Quarterly Review
|
|
29
|
+
name: Jane Doe
|
|
30
|
+
jobTitle: Engineering
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
layout: Body
|
|
35
|
+
title: Highlights
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
- Revenue up 12% quarter-over-quarter
|
|
39
|
+
- Three major product launches completed
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Build:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
tycoslide build deck.md # → deck.pptx
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## CLI
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
|
|
52
|
+
tycoslide smoke # one slide per layout → smoke-all.pptx
|
|
53
|
+
tycoslide plugin # generate AI agent plugin package
|
|
54
|
+
tycoslide manifest # print layout + asset catalog to stdout
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Theme Structure
|
|
58
|
+
|
|
59
|
+
A theme packages a PPTX template, design assets, and a config file into one directory.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
my-theme/
|
|
63
|
+
template/corp-template.pptx
|
|
64
|
+
assets/logos/
|
|
65
|
+
assets/icons/
|
|
66
|
+
theme.json
|
|
67
|
+
package.json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Template** — the PPTX file with named shapes that tycoslide fills.
|
|
71
|
+
**Layout** — a slide pattern in the template (Title, Body, Quote, etc.).
|
|
72
|
+
**Theme** — the directory that bundles a template, assets, and config.
|
|
73
|
+
**Manifest** — a machine-readable catalog of layouts and assets for AI agents.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slides
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill any time the user wants to create branded slides, presentations,
|
|
5
|
+
pitch decks, or sales collateral as a .pptx. Trigger whenever the user mentions "deck," "slides,"
|
|
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."
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# slides
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Before first use, install dependencies from the plugin root:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This installs the tycoslide engine and its dependencies. You only need to do this once.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
For brand voice and naming guidelines, read `brand.md` if it exists alongside this skill.
|
|
27
|
+
|
|
28
|
+
## Quick Reference
|
|
29
|
+
|
|
30
|
+
| Task | Guide |
|
|
31
|
+
|------|-------|
|
|
32
|
+
| Discover layouts and assets | Read `manifest.json` |
|
|
33
|
+
| Write a deck (structure, slots, assets) | See [Creating Slides](#creating-slides) below |
|
|
34
|
+
| Fix build errors | See [QA](#qa-required) below |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Layout Discovery
|
|
39
|
+
|
|
40
|
+
Before writing anything, read `manifest.json`. It contains:
|
|
41
|
+
|
|
42
|
+
- **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions), each with `type` and optionally `required`, `fit`, `limit`, `codeTheme`, `mermaidVariant`, plus documentation (`whenToUse`, `whenNotToUse`)
|
|
43
|
+
- **assets** -- brand logos, client logos, illustrations, and icons (`description`, `whenToUse`)
|
|
44
|
+
|
|
45
|
+
A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
|
|
46
|
+
- **parameters** -- one value on a frontmatter line. Types: `template`, `image`. Fill by putting a value under the parameter's key in the slide frontmatter.
|
|
47
|
+
- **slots** -- a multi-line region in the body. Types: `text`, `table`, `code`, `mermaid`. Fill as the default body region (after the closing `---`) or a `::name::` region.
|
|
48
|
+
|
|
49
|
+
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
|
+
|
|
51
|
+
Study each layout's `whenToUse` and `limit`s before writing any slides. These are your primary guide for matching content to layouts.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Creating Slides
|
|
56
|
+
|
|
57
|
+
Write a deck file in markdown. The file starts with a global frontmatter block declaring the theme, followed by slides separated by `---`.
|
|
58
|
+
|
|
59
|
+
```markdown
|
|
60
|
+
---
|
|
61
|
+
theme: ./theme.json
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
layout: Title
|
|
66
|
+
title: Q2 Business Review
|
|
67
|
+
subtitle: Engineering Division
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
layout: Quote dark
|
|
72
|
+
quote: Great products are built by great teams.
|
|
73
|
+
attributionName: Jane Smith
|
|
74
|
+
attributionTitle: CEO, Acme Corp
|
|
75
|
+
logo: assets/clients/acme.png
|
|
76
|
+
---
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A text shape that holds several lines (e.g. an attribution with a name over a title) surfaces as one key per line -- fill each as its own scalar (`attributionName`, `attributionTitle` above), never as a YAML list.
|
|
80
|
+
|
|
81
|
+
### File structure
|
|
82
|
+
|
|
83
|
+
A deck file has three parts:
|
|
84
|
+
|
|
85
|
+
1. **Global frontmatter** (required) -- the first `---`-delimited block. Must contain `theme:` pointing to the theme config file. May also contain `output:` to set the filename (defaults to `deck.md` → `deck.pptx`).
|
|
86
|
+
2. **Slides** -- each begins with a `---` separator. A slide's frontmatter sits between `---` delimiters. Body content follows the closing `---`.
|
|
87
|
+
3. **Slide separators** -- a `---` on its own line separates slides.
|
|
88
|
+
|
|
89
|
+
### Slide frontmatter
|
|
90
|
+
|
|
91
|
+
Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters** (template and image inputs).
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
---
|
|
95
|
+
layout: Body
|
|
96
|
+
title: Key Achievements
|
|
97
|
+
subtitle: This Quarter
|
|
98
|
+
---
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `layout` is required and consumed by the compiler (not forwarded as content).
|
|
102
|
+
- All other frontmatter keys fill parameters: `title` fills the `title` template parameter, `subtitle` fills the `subtitle` parameter, `hero` fills the `hero` image parameter, etc. A multi-line text shape surfaces as several keys (e.g. `name` + `jobTitle`); fill each as its own scalar line.
|
|
103
|
+
- Slots (`text`, `table`, `code`, `mermaid`) are filled by body regions, not frontmatter -- see below.
|
|
104
|
+
|
|
105
|
+
### Body content, slots, and formatting
|
|
106
|
+
|
|
107
|
+
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: text, table, code, mermaid), and image parameters.
|
|
108
|
+
|
|
109
|
+
### Build
|
|
110
|
+
|
|
111
|
+
Run the command from `manifest.json`'s `build.command`:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# e.g.: npx tycoslide build deck.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The deck is written to your current working directory (not inside the skill).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Layout Selection
|
|
122
|
+
|
|
123
|
+
**Don't create boring decks.** Repeating the same layout on every slide makes a forgettable presentation. Use variety and match content shape to layout purpose.
|
|
124
|
+
|
|
125
|
+
### Before Starting
|
|
126
|
+
|
|
127
|
+
1. **Read the manifest thoroughly.** Each layout has `whenToUse`, `whenNotToUse`, and `limit`s. Respect all three.
|
|
128
|
+
2. **Match content shape to layout purpose.** A comparison belongs in a two/three-column layout, quantified proof belongs in stat blocks, a customer voice belongs in a quote or testimonial layout. Don't force content into the wrong layout.
|
|
129
|
+
3. **Plan narrative arc first.** Decide the sequence of ideas before picking layouts. Then assign each idea to its best-fit layout from the manifest. Keep one variant (all dark or all light) across the deck.
|
|
130
|
+
|
|
131
|
+
### For Each Slide
|
|
132
|
+
|
|
133
|
+
**Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides.
|
|
134
|
+
|
|
135
|
+
Check each layout's `limit` in the manifest for content density constraints. When content overflows, split across slides.
|
|
136
|
+
|
|
137
|
+
### Avoid (Common Mistakes)
|
|
138
|
+
|
|
139
|
+
- **Don't repeat the same layout** -- vary layouts for visual rhythm
|
|
140
|
+
- **Don't dump all content on one slide** -- two clear slides beat one crowded slide
|
|
141
|
+
- **Don't ignore layout limits** -- if a slot says max 4 stats, use 4 or fewer
|
|
142
|
+
- **Don't open with a body/content layout** -- use the Title layout for impact
|
|
143
|
+
- **Don't skip section dividers** -- for decks over 5 slides, use Section title layouts to group sections
|
|
144
|
+
- **Don't restyle the layout** -- the theme owns all design; you only fill slots
|
|
145
|
+
- **Don't invent layout or asset names** -- only use what exists in the manifest
|
|
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
|
|
147
|
+
- **Don't mix dark and light** -- keep one variant across the deck
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## QA (Required)
|
|
152
|
+
|
|
153
|
+
**Assume the first build will fail. Your job is to fix it.**
|
|
154
|
+
|
|
155
|
+
Your first draft almost never comes out clean. Approach QA as a debugging session, not a confirmation step. If you haven't run at least one build-fix cycle, you're not done.
|
|
156
|
+
|
|
157
|
+
### Build
|
|
158
|
+
|
|
159
|
+
Run the command from `manifest.json`'s `build.command`:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# e.g.: npx tycoslide build deck.md
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Read output carefully. Common errors and fixes:
|
|
166
|
+
|
|
167
|
+
| Error | Fix |
|
|
168
|
+
|-------|-----|
|
|
169
|
+
| `Unknown layout: 'xyz'` | Check layout names in `manifest.json` |
|
|
170
|
+
| A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
|
|
171
|
+
| 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` |
|
|
172
|
+
| YAML parse error | Fix the YAML syntax in the slide's frontmatter |
|
|
173
|
+
| `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
|
|
174
|
+
| `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
|
|
175
|
+
| `mermaid-cli is required` | Install mermaid-cli: `npm i -D @mermaid-js/mermaid-cli` |
|
|
176
|
+
| `no "mermaid" block` | The theme has no mermaid color config -- add a `mermaid` section to theme.json |
|
|
177
|
+
|
|
178
|
+
### Verification Loop
|
|
179
|
+
|
|
180
|
+
1. Write the deck file → Build
|
|
181
|
+
2. **Read every error** -- fix all of them
|
|
182
|
+
3. Rebuild
|
|
183
|
+
4. **If content overflows**: reduce content or split into two slides
|
|
184
|
+
5. Repeat until the build exits cleanly
|
|
185
|
+
|
|
186
|
+
**Do not declare success until you've completed at least one build-fix cycle.**
|
|
187
|
+
|
|
188
|
+
### Visual Check
|
|
189
|
+
|
|
190
|
+
After a clean build, render the `.pptx` to PNGs and inspect them:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
soffice --headless --convert-to pdf --outdir . <deck>.pptx
|
|
194
|
+
pdftoppm -png -r 96 <deck>.pdf <name>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Read each slide image and check for:
|
|
198
|
+
|
|
199
|
+
- **Word wrapping** -- text that breaks mid-word or overflows its container
|
|
200
|
+
- **Cramped text** -- content too dense for the slide area
|
|
201
|
+
- **Leftover placeholders** -- dummy text ("Lorem ipsum", "Firstname Lastname") or a placeholder logo that should have been swapped
|
|
202
|
+
- **Cut-off content** -- text or images clipped at slide edges
|
|
203
|
+
|
|
204
|
+
If you spot issues, reduce content, switch layouts, or split into multiple slides. Rebuild and re-check.
|
|
205
|
+
|
|
206
|
+
### Content Review (Use Subagents)
|
|
207
|
+
|
|
208
|
+
**Use subagents for review** -- even for short decks. You've been staring at the content and will see what you expect, not what's there. Subagents have fresh eyes.
|
|
209
|
+
|
|
210
|
+
After a successful build, spawn a subagent:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Review this deck. Assume there are issues -- find them.
|
|
214
|
+
|
|
215
|
+
Check for:
|
|
216
|
+
- Slides that are too dense (>7 bullets, >5 paragraphs, too many stats/rows)
|
|
217
|
+
- Same layout repeated multiple times with no variety
|
|
218
|
+
- Content that doesn't match layout purpose (check whenToUse in manifest.json)
|
|
219
|
+
- Narrative that doesn't flow logically
|
|
220
|
+
- Missing opening (Title) or closing (Thank you) slide
|
|
221
|
+
- Slides that are too sparse (a single bullet doesn't need its own slide)
|
|
222
|
+
- Leftover placeholder logos or dummy text in the rendered images
|
|
223
|
+
|
|
224
|
+
For each issue, suggest a specific fix.
|
|
225
|
+
|
|
226
|
+
Read: /path/to/deck.md and the rendered PNGs in the working directory
|
|
227
|
+
Also read: manifest.json (for layout documentation)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
If the subagent finds issues, fix them and rebuild.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Full Example
|
|
235
|
+
|
|
236
|
+
See [syntax.md](syntax.md#full-example) for a complete multi-slide deck example.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Core Commands
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Build a deck (use command from manifest.json's build.command)
|
|
244
|
+
# e.g.: npx tycoslide build deck.md
|
|
245
|
+
|
|
246
|
+
# Render to images for visual QA
|
|
247
|
+
soffice --headless --convert-to pdf --outdir . <deck>.pptx
|
|
248
|
+
pdftoppm -png -r 96 <deck>.pdf <name>
|
|
249
|
+
```
|
package/bin/tycoslide.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { FitMode, generate, SlotType } from "./engine/index.js";
|
|
6
|
+
import { buildDeck, toEngineConfig } from "./index.js";
|
|
7
|
+
import { generateManifest } from "./manifest.js";
|
|
8
|
+
import { toImageFill } from "./markdown/deckCompiler.js";
|
|
9
|
+
import { CompilerSlotType, compileDeck, ParameterType, parseSlideDocument, RESERVED_KEY, } from "./markdown/index.js";
|
|
10
|
+
import { templateKeys, templateToSegments } from "./markdown/textTemplate.js";
|
|
11
|
+
// ── Smoke content fixtures (used by the `smoke` subcommand) ──────────────────
|
|
12
|
+
const line = (text) => ({ runs: [{ text }] });
|
|
13
|
+
const bulletLine = (text, level = 0) => ({ runs: [{ text }], bullet: { level } });
|
|
14
|
+
const SMOKE_TABLE = {
|
|
15
|
+
headers: ["Feature", "Starter", "Pro", "Business", "Enterprise"].map(line),
|
|
16
|
+
rows: [
|
|
17
|
+
["Users", "5", "25", "100", "Unlimited"].map(line),
|
|
18
|
+
["Storage", "1 GB", "10 GB", "100 GB", "1 TB"].map(line),
|
|
19
|
+
["Support", "Email", "Priority", "24/7", "Dedicated"].map(line),
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
const SMOKE_CODE = [
|
|
23
|
+
{ runs: [{ text: "SELECT name, total", color: "FF7B72" }] },
|
|
24
|
+
{ runs: [{ text: "FROM orders", color: "FF7B72" }] },
|
|
25
|
+
{ runs: [{ text: "WHERE created_at > now();", color: "FF7B72" }] },
|
|
26
|
+
];
|
|
27
|
+
const SMOKE_PROSE = [
|
|
28
|
+
line("Sample intro line for this block."),
|
|
29
|
+
bulletLine("First point"),
|
|
30
|
+
bulletLine("Second point"),
|
|
31
|
+
];
|
|
32
|
+
function pickFirstAsset(config) {
|
|
33
|
+
for (const group of Object.values(config.assets)) {
|
|
34
|
+
for (const entry of Object.values(group))
|
|
35
|
+
return entry.path;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function smokeSteps(config) {
|
|
40
|
+
const firstAsset = pickFirstAsset(config);
|
|
41
|
+
const absAsset = firstAsset ? resolve(config.rootDir, firstAsset) : undefined;
|
|
42
|
+
return config.layouts.map((layout) => {
|
|
43
|
+
const content = {};
|
|
44
|
+
// Parameters (frontmatter): text → a placeholder-filled template, image → the first asset.
|
|
45
|
+
for (const p of layout.parameters) {
|
|
46
|
+
switch (p.type) {
|
|
47
|
+
case ParameterType.Image: {
|
|
48
|
+
if (!absAsset)
|
|
49
|
+
continue;
|
|
50
|
+
content[p.key] = toImageFill(p, absAsset);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case ParameterType.Template: {
|
|
54
|
+
// Text shapes carry no key — the engine slot is keyed by shapeName.
|
|
55
|
+
const values = new Map(templateKeys(p.template).map((k) => [k, "Sample"]));
|
|
56
|
+
content[p.shapeName] = { lines: templateToSegments(p.template, values, p.shapeName) };
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Slots (body regions): text → prose, table → table, code → code, mermaid → image.
|
|
62
|
+
for (const s of layout.slots) {
|
|
63
|
+
switch (s.type) {
|
|
64
|
+
case CompilerSlotType.Table:
|
|
65
|
+
content[s.key] = SMOKE_TABLE;
|
|
66
|
+
break;
|
|
67
|
+
case CompilerSlotType.Code:
|
|
68
|
+
content[s.key] = { paragraphs: SMOKE_CODE };
|
|
69
|
+
break;
|
|
70
|
+
case CompilerSlotType.Mermaid: {
|
|
71
|
+
// Mermaid slots don't declare fit; smoke fills them with a fixed
|
|
72
|
+
// contained ImageFill so the projected engine slot (Image, contain)
|
|
73
|
+
// stays consistent with the real renderer's output.
|
|
74
|
+
if (!absAsset)
|
|
75
|
+
continue;
|
|
76
|
+
content[s.key] = { type: SlotType.Image, path: absAsset, fit: FitMode.Contain };
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case CompilerSlotType.Text:
|
|
80
|
+
content[s.key] = { paragraphs: SMOKE_PROSE };
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { layout: layout.name, content };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const DEFAULT_CONFIG = "theme.json";
|
|
88
|
+
const DEFAULT_SMOKE_OUTPUT = "smoke-all.pptx";
|
|
89
|
+
const SKILL_DIR = "skills/slides";
|
|
90
|
+
const PLUGIN_DIR = ".claude-plugin";
|
|
91
|
+
const PLUGIN_FILE = "plugin.json";
|
|
92
|
+
const MANIFEST_FILE = "manifest.json";
|
|
93
|
+
const SKILL_FILE = "SKILL.md";
|
|
94
|
+
const SYNTAX_FILE = "syntax.md";
|
|
95
|
+
const BUILD_COMMAND = "npx tycoslide build";
|
|
96
|
+
const sdkDir = dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
|
|
98
|
+
const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
|
|
99
|
+
function loadConfig(absPath) {
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = JSON.parse(readFileSync(absPath, "utf-8"));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`Config file not found or invalid JSON: ${absPath}`);
|
|
106
|
+
}
|
|
107
|
+
return { ...raw, rootDir: dirname(absPath) };
|
|
108
|
+
}
|
|
109
|
+
const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
|
|
110
|
+
const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
|
|
111
|
+
program
|
|
112
|
+
.command("build")
|
|
113
|
+
.description("Build a PPTX deck from a Markdown spec")
|
|
114
|
+
.argument("<deck>", "path to deck markdown file")
|
|
115
|
+
.option(`-c, --config <path>`, "override theme config path (default: read from frontmatter)")
|
|
116
|
+
.action(async (deckPath, opts) => {
|
|
117
|
+
const absDeckPath = resolve(process.cwd(), deckPath);
|
|
118
|
+
let source;
|
|
119
|
+
try {
|
|
120
|
+
source = readFileSync(absDeckPath, "utf-8");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error(`Deck file not found: ${deckPath}`);
|
|
124
|
+
}
|
|
125
|
+
const doc = parseSlideDocument(source);
|
|
126
|
+
const themePath = doc.global.theme;
|
|
127
|
+
const absConfigPath = opts.config
|
|
128
|
+
? resolve(process.cwd(), opts.config)
|
|
129
|
+
: themePath
|
|
130
|
+
? resolve(dirname(absDeckPath), String(themePath))
|
|
131
|
+
: undefined;
|
|
132
|
+
if (!absConfigPath) {
|
|
133
|
+
throw new Error(`${basename(deckPath)}: missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
|
|
134
|
+
}
|
|
135
|
+
const config = loadConfig(absConfigPath);
|
|
136
|
+
const deck = compileDeck(doc, config.layouts, config.rootDir);
|
|
137
|
+
if (!deck.output)
|
|
138
|
+
deck.output = basename(deckPath).replace(/\.md$/, ".pptx");
|
|
139
|
+
await buildDeck(deck, config);
|
|
140
|
+
});
|
|
141
|
+
program
|
|
142
|
+
.command("manifest")
|
|
143
|
+
.description("Generate manifest.json from theme config")
|
|
144
|
+
.option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
|
|
145
|
+
.option(`-o, --out <file>`, "write to file instead of stdout")
|
|
146
|
+
.action(async (opts) => {
|
|
147
|
+
const config = loadConfig(resolve(process.cwd(), opts.config));
|
|
148
|
+
const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
|
|
149
|
+
if (opts.out) {
|
|
150
|
+
writeFileSync(resolve(process.cwd(), opts.out), `${json}\n`);
|
|
151
|
+
console.log(`WROTE ${opts.out}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
process.stdout.write(`${json}\n`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
program
|
|
158
|
+
.command("smoke")
|
|
159
|
+
.description("Generate one smoke-test slide per layout")
|
|
160
|
+
.option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
|
|
161
|
+
.option(`-o, --out <file>`, "output PPTX filename", DEFAULT_SMOKE_OUTPUT)
|
|
162
|
+
.action(async (opts) => {
|
|
163
|
+
const config = loadConfig(resolve(process.cwd(), opts.config));
|
|
164
|
+
const steps = smokeSteps(config);
|
|
165
|
+
await generate({ theme: opts.config, output: opts.out, steps }, toEngineConfig(config));
|
|
166
|
+
});
|
|
167
|
+
program
|
|
168
|
+
.command("plugin")
|
|
169
|
+
.description("Generate plugin package (plugin.json, manifest.json, SKILL.md, syntax.md) for AI agents")
|
|
170
|
+
.option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
|
|
171
|
+
.action(async (opts) => {
|
|
172
|
+
const config = loadConfig(resolve(process.cwd(), opts.config));
|
|
173
|
+
const cwd = process.cwd();
|
|
174
|
+
const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
|
|
175
|
+
const pluginMeta = {
|
|
176
|
+
name: pkg.name,
|
|
177
|
+
version: pkg.version,
|
|
178
|
+
description: pkg.description ?? "",
|
|
179
|
+
skills: "./skills",
|
|
180
|
+
};
|
|
181
|
+
if (pkg.author)
|
|
182
|
+
pluginMeta.author = pkg.author;
|
|
183
|
+
const pluginDir = resolve(cwd, PLUGIN_DIR);
|
|
184
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
185
|
+
writeFileSync(resolve(pluginDir, PLUGIN_FILE), `${JSON.stringify(pluginMeta, null, 2)}\n`);
|
|
186
|
+
console.log(`WROTE ${PLUGIN_DIR}/${PLUGIN_FILE}`);
|
|
187
|
+
const skillDir = resolve(cwd, SKILL_DIR);
|
|
188
|
+
mkdirSync(skillDir, { recursive: true });
|
|
189
|
+
const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
|
|
190
|
+
writeFileSync(resolve(skillDir, MANIFEST_FILE), `${json}\n`);
|
|
191
|
+
console.log(`WROTE ${SKILL_DIR}/${MANIFEST_FILE}`);
|
|
192
|
+
copyFileSync(skillMdPath, resolve(skillDir, SKILL_FILE));
|
|
193
|
+
console.log(`WROTE ${SKILL_DIR}/${SKILL_FILE}`);
|
|
194
|
+
copyFileSync(syntaxMdPath, resolve(skillDir, SYNTAX_FILE));
|
|
195
|
+
console.log(`WROTE ${SKILL_DIR}/${SYNTAX_FILE}`);
|
|
196
|
+
});
|
|
197
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shape / DOM helpers used by every fill primitive.
|
|
3
|
+
*
|
|
4
|
+
* Two layers live here. First the low-level slide-XML substrate: `Tag` names the
|
|
5
|
+
* DrawingML / PresentationML elements, plus node utilities (element collection,
|
|
6
|
+
* run text access, run and paragraph builders) and hyperlink relationship
|
|
7
|
+
* management. Second, the higher-level StyledParagraph-rebuild machinery shared
|
|
8
|
+
* by fillText and fillTable: harvest specimen (pPr, rPr) buckets grouped by
|
|
9
|
+
* (bullet kind, level), detach the specimens, then build fresh paragraphs
|
|
10
|
+
* cloning the appropriate bucket. No fill strategy lives here — this is the
|
|
11
|
+
* substrate the fill modules build on.
|
|
12
|
+
*/
|
|
13
|
+
import type { StyledParagraph, TextRun } from "./types.js";
|
|
14
|
+
/** DrawingML and PresentationML tag names used when manipulating slide XML. */
|
|
15
|
+
export declare const Tag: {
|
|
16
|
+
readonly RUN: "a:r";
|
|
17
|
+
readonly TEXT: "a:t";
|
|
18
|
+
readonly PARAGRAPH: "a:p";
|
|
19
|
+
readonly RUN_PROPS: "a:rPr";
|
|
20
|
+
readonly PARA_PROPS: "a:pPr";
|
|
21
|
+
readonly BULLET_CHAR: "a:buChar";
|
|
22
|
+
readonly BULLET_AUTO: "a:buAutoNum";
|
|
23
|
+
readonly BULLET_NONE: "a:buNone";
|
|
24
|
+
readonly LINE_BREAK: "a:br";
|
|
25
|
+
readonly END_PARA_RUN_PROPS: "a:endParaRPr";
|
|
26
|
+
readonly OFFSET: "a:off";
|
|
27
|
+
readonly EXTENT: "a:ext";
|
|
28
|
+
readonly BLIP: "a:blip";
|
|
29
|
+
readonly SRC_RECT: "a:srcRect";
|
|
30
|
+
readonly BLIP_FILL: "p:blipFill";
|
|
31
|
+
readonly SPACE_BEFORE: "a:spcBef";
|
|
32
|
+
readonly HLINK_CLICK: "a:hlinkClick";
|
|
33
|
+
readonly RELATIONSHIP: "Relationship";
|
|
34
|
+
readonly TABLE: "a:tbl";
|
|
35
|
+
readonly TABLE_ROW: "a:tr";
|
|
36
|
+
readonly TABLE_CELL: "a:tc";
|
|
37
|
+
readonly TX_BODY: "a:txBody";
|
|
38
|
+
readonly SOLID_FILL: "a:solidFill";
|
|
39
|
+
readonly SRGB_CLR: "a:srgbClr";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* OOXML attribute names and fixed attribute values. Element names live in `Tag`;
|
|
43
|
+
* this is the same discipline for the attributes and enumerated values written
|
|
44
|
+
* onto them, so no bare string literal leaks into the fill code.
|
|
45
|
+
*/
|
|
46
|
+
export declare const Attr: {
|
|
47
|
+
readonly BOLD: "b";
|
|
48
|
+
readonly ITALIC: "i";
|
|
49
|
+
readonly STRIKE: "strike";
|
|
50
|
+
readonly UNDERLINE: "u";
|
|
51
|
+
readonly ON: "1";
|
|
52
|
+
readonly STRIKE_SINGLE: "sngStrike";
|
|
53
|
+
readonly UNDERLINE_SINGLE: "sng";
|
|
54
|
+
readonly VALUE: "val";
|
|
55
|
+
readonly ID: "Id";
|
|
56
|
+
readonly TYPE: "Type";
|
|
57
|
+
readonly TARGET: "Target";
|
|
58
|
+
readonly TARGET_MODE: "TargetMode";
|
|
59
|
+
readonly EXTERNAL: "External";
|
|
60
|
+
readonly HLINK_ID: "r:id";
|
|
61
|
+
readonly LEVEL: "lvl";
|
|
62
|
+
readonly X: "x";
|
|
63
|
+
readonly Y: "y";
|
|
64
|
+
readonly CX: "cx";
|
|
65
|
+
readonly CY: "cy";
|
|
66
|
+
readonly LEFT: "l";
|
|
67
|
+
readonly TOP: "t";
|
|
68
|
+
readonly RIGHT: "r";
|
|
69
|
+
readonly BOTTOM: "b";
|
|
70
|
+
readonly XML_SPACE: "xml:space";
|
|
71
|
+
readonly PRESERVE: "preserve";
|
|
72
|
+
};
|
|
73
|
+
/** True for a non-null, non-array object — the shared prefix of every `isXFill`. */
|
|
74
|
+
export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
|
|
75
|
+
export declare function collectElements(parent: any, tagName: string): any[];
|
|
76
|
+
export declare function childrenByTag(parent: any, tagName: string): any[];
|
|
77
|
+
export declare function detach(node: any): void;
|
|
78
|
+
export declare function runText(run: any): string;
|
|
79
|
+
export declare function setRunTextPreservingStyle(run: any, text: string): void;
|
|
80
|
+
export declare function leadingDecorativePrefix(text: string): string;
|
|
81
|
+
export declare function buildRun(doc: any, cloneRPr: any | null, text: string): any;
|
|
82
|
+
export declare function buildParagraph(doc: any, clonePPr: any | null, run: any): any;
|
|
83
|
+
export declare const HYPERLINK_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
|
84
|
+
export declare function addRelationship(relation: any, url: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Replace all runs in a paragraph with a sequence of styled runs, cloning the
|
|
87
|
+
* first existing run's rPr as the template and layering per-run marks on top.
|
|
88
|
+
*
|
|
89
|
+
* Exported for tests; not part of the public engine surface (index.ts).
|
|
90
|
+
*/
|
|
91
|
+
export declare function setRichRuns(para: any, runs: TextRun[], relation?: any): void;
|
|
92
|
+
export declare function rebuildParagraphs(shape: any, paragraphs: StyledParagraph[], startIndex: number, relation?: any, shapeName?: string): void;
|