@tycoworks/tycoslide 0.10.1 → 0.11.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 +2 -0
- package/SKILL.md +28 -54
- package/dist/cli.js +4 -4
- package/dist/engine/fillers/filler.d.ts +31 -5
- package/dist/engine/fillers/filler.js +29 -21
- package/dist/engine/fillers/image.d.ts +1 -1
- package/dist/engine/fillers/image.js +2 -2
- package/dist/engine/fillers/table.d.ts +33 -16
- package/dist/engine/fillers/table.js +62 -27
- package/dist/engine/generate.d.ts +5 -4
- package/dist/engine/generate.js +51 -17
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/types.d.ts +41 -13
- package/dist/index.d.ts +3 -4
- package/dist/index.js +28 -15
- package/dist/manifest.d.ts +1 -6
- package/dist/manifest.js +1 -17
- package/dist/markdown/blocks/mermaid.d.ts +1 -1
- package/dist/markdown/blocks/mermaid.js +3 -3
- package/dist/markdown/blocks/table.js +1 -1
- package/dist/markdown/blocks/text.js +5 -6
- package/dist/markdown/deckCompiler.d.ts +2 -2
- package/dist/markdown/deckCompiler.js +7 -32
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/schema/deckSchema.d.ts +2 -2
- package/dist/markdown/schema/deckSchema.js +2 -2
- package/dist/markdown/schema/themeConfigSchema.d.ts +12 -21
- package/dist/markdown/schema/themeConfigSchema.js +22 -14
- package/dist/markdown/slideParser.d.ts +0 -1
- package/dist/markdown/slideParser.js +7 -4
- package/dist/markdown/types.d.ts +44 -46
- package/dist/markdown/types.js +2 -4
- package/package.json +1 -1
- package/syntax.md +28 -25
package/README.md
CHANGED
package/SKILL.md
CHANGED
|
@@ -21,8 +21,6 @@ This installs the tycoslide engine and its dependencies. You only need to do thi
|
|
|
21
21
|
|
|
22
22
|
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.
|
|
23
23
|
|
|
24
|
-
For brand voice and naming guidelines, read `brand.md` if it exists alongside this skill.
|
|
25
|
-
|
|
26
24
|
## Quick Reference
|
|
27
25
|
|
|
28
26
|
| Task | Guide |
|
|
@@ -37,21 +35,25 @@ For brand voice and naming guidelines, read `brand.md` if it exists alongside th
|
|
|
37
35
|
|
|
38
36
|
Before writing anything, read `manifest.json`. It contains:
|
|
39
37
|
|
|
40
|
-
- **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions), each with `
|
|
41
|
-
- **assets** -- brand logos, client logos, illustrations, and icons (`description
|
|
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
|
+
- **assets** -- brand logos, client logos, illustrations, and icons (`description`)
|
|
42
40
|
|
|
43
41
|
A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
|
|
44
42
|
- **parameters** -- one value on a frontmatter line. Types: `template`, `image`. Fill by putting a value under the parameter's key in the slide frontmatter.
|
|
45
|
-
- **slots** -- a multi-line region in the body.
|
|
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
|
+
|
|
45
|
+
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
46
|
|
|
47
|
-
|
|
47
|
+
Parameters and slots you leave unfilled are dropped from the slide, so a layout with numbered slots (several sections, stats, columns) renders only the ones you fill -- fill as many as you have.
|
|
48
48
|
|
|
49
|
-
Study each layout's `slots`
|
|
49
|
+
Study each layout's `slots` before writing any slides.
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
53
|
## Creating Slides
|
|
54
54
|
|
|
55
|
+
> **Every layout, parameter, and slot name in the examples below is a placeholder.** Your theme's real names live in `manifest.json` — read it first, and never assume a name shown in an example exists in your theme.
|
|
56
|
+
|
|
55
57
|
Write a deck file in markdown. The file starts with a global frontmatter block declaring the theme, followed by slides separated by `---`.
|
|
56
58
|
|
|
57
59
|
```markdown
|
|
@@ -60,8 +62,8 @@ theme: ./theme.json
|
|
|
60
62
|
---
|
|
61
63
|
|
|
62
64
|
---
|
|
63
|
-
layout: Title
|
|
64
|
-
title: Q2 Business Review
|
|
65
|
+
layout: Title # ← a layout from your manifest.json
|
|
66
|
+
title: Q2 Business Review # ← a parameter that layout declares
|
|
65
67
|
subtitle: Engineering Division
|
|
66
68
|
---
|
|
67
69
|
|
|
@@ -80,7 +82,7 @@ A text shape that holds several lines (e.g. an attribution with a name over a ti
|
|
|
80
82
|
|
|
81
83
|
A deck file has three parts:
|
|
82
84
|
|
|
83
|
-
1. **Global frontmatter** (required) -- the first `---`-delimited block. Must contain `theme:` pointing to the theme config file.
|
|
85
|
+
1. **Global frontmatter** (required) -- the first `---`-delimited block. Must contain `theme:` pointing to the theme config file. The output `.pptx` is written next to the deck, named after it (`deck.md` → `deck.pptx`).
|
|
84
86
|
2. **Slides** -- each begins with a `---` separator. A slide's frontmatter sits between `---` delimiters. Body content follows the closing `---`.
|
|
85
87
|
3. **Slide separators** -- a `---` on its own line separates slides.
|
|
86
88
|
|
|
@@ -98,53 +100,49 @@ subtitle: This Quarter
|
|
|
98
100
|
|
|
99
101
|
- `layout` is required and consumed by the compiler (not forwarded as content).
|
|
100
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.
|
|
101
|
-
- Slots (`text`, `table`, `
|
|
103
|
+
- Slots (accepting `text`, `table`, `image`) are filled by body regions, not frontmatter -- see below.
|
|
102
104
|
- A slide may also carry a `notes:` block in frontmatter -- plain-text speaker notes for the slide's notes page (see [syntax.md](syntax.md#speaker-notes)). It is slide-level metadata, not a parameter or slot.
|
|
103
105
|
|
|
104
106
|
### Body content, slots, and formatting
|
|
105
107
|
|
|
106
|
-
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,
|
|
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.
|
|
107
109
|
|
|
108
110
|
### Build
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
Build the deck (replace `<deck.md>` with your deck file):
|
|
111
113
|
|
|
112
114
|
```bash
|
|
113
|
-
|
|
115
|
+
npx tycoslide build <deck.md>
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
The
|
|
118
|
+
The `.pptx` is written next to your deck file (same directory as `<deck.md>`).
|
|
117
119
|
|
|
118
120
|
---
|
|
119
121
|
|
|
120
122
|
## Layout Selection
|
|
121
123
|
|
|
122
|
-
**Don't create boring decks.** Repeating the same layout on every slide makes a forgettable presentation. Use variety and match content shape to layout
|
|
124
|
+
**Don't create boring decks.** Repeating the same layout on every slide makes a forgettable presentation. Use variety and match content shape to the layout's slots.
|
|
123
125
|
|
|
124
126
|
### Before Starting
|
|
125
127
|
|
|
126
|
-
1. **Read the manifest thoroughly.** Each layout
|
|
127
|
-
2. **Match content shape to layout
|
|
128
|
-
3. **
|
|
128
|
+
1. **Read the manifest thoroughly.** Each layout declares its `slots` (and the `accepts` types each one takes) -- the shape of what it can hold. Respect them.
|
|
129
|
+
2. **Match content shape to the layout's slots.** A comparison fits a layout with two or three text columns; quantified proof fits one with stat slots; a customer voice fits a quote slot. The slots tell you what fits -- don't force content into the wrong shape.
|
|
130
|
+
3. **Assign each idea to its best-fit layout** from the manifest. Some layouts come in more than one variant (e.g. a dark and a light version, shown in the name) — consider which suits the deck.
|
|
129
131
|
|
|
130
132
|
### For Each Slide
|
|
131
133
|
|
|
132
|
-
**Every slide communicates one idea.**
|
|
134
|
+
**Every slide communicates one idea.** Put the takeaway in the headline, then support it.
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
Keep each slot's content to what its region comfortably holds. When content overflows, split it across slides.
|
|
135
137
|
|
|
136
138
|
### Avoid (Common Mistakes)
|
|
137
139
|
|
|
138
140
|
- **Don't repeat the same layout** -- vary layouts for visual rhythm
|
|
139
|
-
- **Don't
|
|
140
|
-
- **Don't ignore layout limits** -- if a slot says max 4 stats, use 4 or fewer
|
|
141
|
-
- **Don't open with a body/content layout** -- use the Title layout for impact
|
|
142
|
-
- **Don't skip section dividers** -- for decks over 5 slides, use Section title layouts to group sections
|
|
141
|
+
- **Don't overstuff a slot** -- keep content to what its region comfortably holds; split across slides when there's too much
|
|
143
142
|
- **Don't restyle the layout** -- the theme owns all design; you only fill slots
|
|
144
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
|
|
145
144
|
- **Don't invent layout or asset names** -- only use what exists in the manifest
|
|
146
145
|
- **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
146
|
|
|
149
147
|
---
|
|
150
148
|
|
|
@@ -154,15 +152,7 @@ Check each layout's `limit` in the manifest for content density constraints. Whe
|
|
|
154
152
|
|
|
155
153
|
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
154
|
|
|
157
|
-
|
|
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:
|
|
155
|
+
Build the deck again ([Build](#build)) and read the output carefully. Common errors and fixes:
|
|
166
156
|
|
|
167
157
|
| Error | Fix |
|
|
168
158
|
|-------|-----|
|
|
@@ -172,7 +162,7 @@ Read output carefully. Common errors and fixes:
|
|
|
172
162
|
| YAML parse error | Fix the YAML syntax in the slide's frontmatter |
|
|
173
163
|
| `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
|
|
174
164
|
| `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
|
|
175
|
-
| `
|
|
165
|
+
| `Mermaid render failed` | The engine renders diagrams with Playwright's Chromium; install it once with `npx playwright install chromium` |
|
|
176
166
|
| `no "mermaid" block` | The theme has no mermaid color config -- add a `mermaid` section to theme.json |
|
|
177
167
|
|
|
178
168
|
### Verification Loop
|
|
@@ -213,12 +203,9 @@ After a successful build, spawn a subagent:
|
|
|
213
203
|
Review this deck. Assume there are issues -- find them.
|
|
214
204
|
|
|
215
205
|
Check for:
|
|
216
|
-
- Slides
|
|
206
|
+
- Slides where content overflows its slot (more bullets/stats/rows than the region holds)
|
|
217
207
|
- Same layout repeated multiple times with no variety
|
|
218
|
-
- Content that doesn't match layout
|
|
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)
|
|
208
|
+
- Content that doesn't match the layout's slots (check `accepts` in manifest.json)
|
|
222
209
|
- Leftover placeholder logos or dummy text in the rendered images
|
|
223
210
|
|
|
224
211
|
For each issue, suggest a specific fix.
|
|
@@ -234,16 +221,3 @@ If the subagent finds issues, fix them and rebuild.
|
|
|
234
221
|
## Full Example
|
|
235
222
|
|
|
236
223
|
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/dist/cli.js
CHANGED
|
@@ -12,7 +12,6 @@ const MANIFEST_FILE = "manifest.json";
|
|
|
12
12
|
// SKILL.md), so the skill folder can live at the theme repo root.
|
|
13
13
|
const SKILL_FILE = "skill.md";
|
|
14
14
|
const SYNTAX_FILE = "syntax.md";
|
|
15
|
-
const BUILD_COMMAND = "npx tycoslide build";
|
|
16
15
|
const sdkDir = dirname(fileURLToPath(import.meta.url));
|
|
17
16
|
const skillMdPath = resolve(sdkDir, "..", "SKILL.md");
|
|
18
17
|
const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
|
|
@@ -45,8 +44,9 @@ program
|
|
|
45
44
|
}
|
|
46
45
|
const config = loadThemeConfig(absConfigPath);
|
|
47
46
|
const deck = await compileDeck(doc, config);
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Always write the .pptx next to the input deck, named after it.
|
|
48
|
+
const outName = basename(deckPath).replace(/\.md$/, ".pptx");
|
|
49
|
+
deck.output = resolve(dirname(absDeckPath), outName);
|
|
50
50
|
await buildDeck(deck, config, { excludeNotes: !opts.notes });
|
|
51
51
|
});
|
|
52
52
|
program
|
|
@@ -61,7 +61,7 @@ program
|
|
|
61
61
|
}
|
|
62
62
|
// basename drops any npm scope, e.g. "@acme/mz-slides" -> "mz-slides".
|
|
63
63
|
const skillName = basename(themePkg.name);
|
|
64
|
-
const manifestJson = `${generateManifest(config
|
|
64
|
+
const manifestJson = `${generateManifest(config)}\n`;
|
|
65
65
|
writeFileSync(resolve(process.cwd(), MANIFEST_FILE), manifestJson);
|
|
66
66
|
console.log(`WROTE ${MANIFEST_FILE}`);
|
|
67
67
|
let skillMd;
|
|
@@ -15,16 +15,42 @@
|
|
|
15
15
|
* Element-level geometry lives in the `fillX` primitives; cross-shape concerns
|
|
16
16
|
* (media pre-swap for images) live in the callbacks here.
|
|
17
17
|
*/
|
|
18
|
-
import { SlotType } from "../types.js";
|
|
19
|
-
/**
|
|
20
|
-
|
|
18
|
+
import { type BodyRows, SlotType } from "../types.js";
|
|
19
|
+
/**
|
|
20
|
+
* The shape a filler targets — a union discriminated by `type` (mirroring the
|
|
21
|
+
* `Block` variants), so each filler's callback sees only its own specimen
|
|
22
|
+
* options. Every target carries a `shapeName` and a human `label` for
|
|
23
|
+
* diagnostics (the author-facing "slide N, layout …, slot …" a filler prints in
|
|
24
|
+
* advisory warnings instead of the raw PPTX shape id); a text target may carry
|
|
25
|
+
* `startAt`, and a table target carries its required `bodyRows` range.
|
|
26
|
+
*/
|
|
27
|
+
export type TemplateFillTarget = {
|
|
28
|
+
type: typeof SlotType.Template;
|
|
29
|
+
shapeName: string;
|
|
30
|
+
label: string;
|
|
31
|
+
};
|
|
32
|
+
export type TextFillTarget = {
|
|
33
|
+
type: typeof SlotType.Text;
|
|
21
34
|
shapeName: string;
|
|
35
|
+
label: string;
|
|
22
36
|
startAt?: number;
|
|
23
37
|
};
|
|
38
|
+
export type TableFillTarget = {
|
|
39
|
+
type: typeof SlotType.Table;
|
|
40
|
+
shapeName: string;
|
|
41
|
+
label: string;
|
|
42
|
+
bodyRows: BodyRows;
|
|
43
|
+
};
|
|
44
|
+
export type ImageFillTarget = {
|
|
45
|
+
type: typeof SlotType.Image;
|
|
46
|
+
shapeName: string;
|
|
47
|
+
label: string;
|
|
48
|
+
};
|
|
49
|
+
export type FillTarget = TemplateFillTarget | TextFillTarget | TableFillTarget | ImageFillTarget;
|
|
24
50
|
/** A pptx-automizer element-modify callback: `(element, relation) => void`. */
|
|
25
51
|
export type ShapeCallback = (element: any, relation: any) => unknown;
|
|
26
|
-
export interface Filler<T> {
|
|
52
|
+
export interface Filler<T, Tgt extends FillTarget = FillTarget> {
|
|
27
53
|
matches(v: unknown): v is T;
|
|
28
|
-
callbacks(value: T, target:
|
|
54
|
+
callbacks(value: T, target: Tgt): ShapeCallback[];
|
|
29
55
|
}
|
|
30
56
|
export declare const FILLERS: Record<SlotType, Filler<any>>;
|
|
@@ -22,26 +22,34 @@ import { fillImage, isImageFill } from "./image.js";
|
|
|
22
22
|
import { fillTable, isTableFill } from "./table.js";
|
|
23
23
|
import { fillTemplate, isTemplateFill } from "./template.js";
|
|
24
24
|
import { fillText, isTextFill } from "./text.js";
|
|
25
|
+
const templateFiller = {
|
|
26
|
+
matches: isTemplateFill,
|
|
27
|
+
callbacks: (v, t) => [(el) => fillTemplate(el, v, t.shapeName)],
|
|
28
|
+
};
|
|
29
|
+
const textFiller = {
|
|
30
|
+
matches: isTextFill,
|
|
31
|
+
callbacks: (v, t) => [
|
|
32
|
+
(el, relation) => fillText(el, v, { startAt: t.startAt ?? 0, relation, shapeName: t.shapeName }),
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
// `t` is a TableFillTarget, so `t.bodyRows` is a guaranteed `BodyRows` tuple.
|
|
36
|
+
// `targetOf` builds this target from a `TableBlock`, whose `bodyRows` is required,
|
|
37
|
+
// and the Zod theme schema rejects a table block without a `bodyRows` range; a
|
|
38
|
+
// table filler therefore cannot be reached without one.
|
|
39
|
+
const tableFiller = {
|
|
40
|
+
matches: isTableFill,
|
|
41
|
+
callbacks: (v, t) => [(el) => fillTable(el, v, t.shapeName, t.bodyRows)],
|
|
42
|
+
};
|
|
43
|
+
const imageFiller = {
|
|
44
|
+
matches: isImageFill,
|
|
45
|
+
callbacks: (v, t) => [
|
|
46
|
+
ModifyImageHelper.setRelationTarget(basename(v.path)),
|
|
47
|
+
(el) => fillImage(el, v, t.shapeName, t.label),
|
|
48
|
+
],
|
|
49
|
+
};
|
|
25
50
|
export const FILLERS = {
|
|
26
|
-
[SlotType.Template]:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
[SlotType.Text]: {
|
|
31
|
-
matches: isTextFill,
|
|
32
|
-
callbacks: (v, t) => [
|
|
33
|
-
(el, relation) => fillText(el, v, { startAt: t.startAt ?? 0, relation, shapeName: t.shapeName }),
|
|
34
|
-
],
|
|
35
|
-
},
|
|
36
|
-
[SlotType.Table]: {
|
|
37
|
-
matches: isTableFill,
|
|
38
|
-
callbacks: (v, t) => [(el) => fillTable(el, v, t.shapeName)],
|
|
39
|
-
},
|
|
40
|
-
[SlotType.Image]: {
|
|
41
|
-
matches: isImageFill,
|
|
42
|
-
callbacks: (v, t) => [
|
|
43
|
-
ModifyImageHelper.setRelationTarget(basename(v.path)),
|
|
44
|
-
(el) => fillImage(el, v, t.shapeName),
|
|
45
|
-
],
|
|
46
|
-
},
|
|
51
|
+
[SlotType.Template]: templateFiller,
|
|
52
|
+
[SlotType.Text]: textFiller,
|
|
53
|
+
[SlotType.Table]: tableFiller,
|
|
54
|
+
[SlotType.Image]: imageFiller,
|
|
47
55
|
};
|
|
@@ -42,7 +42,7 @@ export type GeometryResult = {
|
|
|
42
42
|
* `image.path` is assumed absolute — the compiler / caller resolves it before the
|
|
43
43
|
* ImageFill reaches the engine.
|
|
44
44
|
*/
|
|
45
|
-
export declare function fillImage(shape: any, image: ImageFill, shapeName
|
|
45
|
+
export declare function fillImage(shape: any, image: ImageFill, shapeName: string, label: string): void;
|
|
46
46
|
/**
|
|
47
47
|
* Pure fit geometry — no DOM, so it is trivially unit-testable in isolation.
|
|
48
48
|
* `fit` picks the strategy: `cover` scales to the larger axis ratio and
|
|
@@ -30,7 +30,7 @@ const MIN_SCALE = 0.2;
|
|
|
30
30
|
* `image.path` is assumed absolute — the compiler / caller resolves it before the
|
|
31
31
|
* ImageFill reaches the engine.
|
|
32
32
|
*/
|
|
33
|
-
export function fillImage(shape, image, shapeName
|
|
33
|
+
export function fillImage(shape, image, shapeName, label) {
|
|
34
34
|
const dims = imageSize(new Uint8Array(readFileSync(image.path)));
|
|
35
35
|
if (!dims.width || !dims.height) {
|
|
36
36
|
throw new Error(`Image shape "${shapeName}": could not read image dimensions from "${image.path}".`);
|
|
@@ -49,7 +49,7 @@ export function fillImage(shape, image, shapeName = "") {
|
|
|
49
49
|
};
|
|
50
50
|
const { geometry, warnings } = computeGeometry(frame, dims.width, dims.height, image.fit);
|
|
51
51
|
for (const w of warnings)
|
|
52
|
-
console.warn(`Image
|
|
52
|
+
console.warn(`Image ${label}: ${w}`);
|
|
53
53
|
if (geometry.placement === Placement.Crop) {
|
|
54
54
|
// fill: symmetric crop on the overflowing axis, written as srcRect insets.
|
|
55
55
|
applySrcRect(shape, blipFill, geometry.left, geometry.top, geometry.left, geometry.top);
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Table fill —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Table fill — composes a variable number of output rows from a fixed table
|
|
3
|
+
* specimen in `<a:tbl>` by cloning specimen rows around one contiguous `bodyRows`
|
|
4
|
+
* range, then fills each cell's first paragraph with the corresponding
|
|
5
|
+
* StyledParagraph. Row 0 styles the header; the rows before the range are top
|
|
6
|
+
* fixed rows (rendered once — the under-header row lives here, never looped, so
|
|
7
|
+
* its divider survives); the `[start, end]` range is the repeatable body, cycled
|
|
8
|
+
* to fill the deck's data; the rows after it are bottom fixed rows (rendered once
|
|
9
|
+
* — a decorated total row lives here). `<a:tcPr>` (fill, borders, margins) is
|
|
10
|
+
* preserved per row — only the cell text is rebuilt. Cells and `<a:gridCol>`
|
|
11
|
+
* entries are cloned or trimmed to the header count, sharing the template's total
|
|
12
|
+
* width. Row, cell, and grid cloning stay engine-side because they need
|
|
13
|
+
* pptx-automizer DOM access.
|
|
7
14
|
*/
|
|
8
|
-
import type { TableFill } from "../types.js";
|
|
15
|
+
import type { BodyRows, TableFill } from "../types.js";
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Validate a `bodyRows = [start, end]` range against a specimen's actual row
|
|
18
|
+
* count `R`, then compose the `K` specimen-row picks that back the deck's `K` data
|
|
19
|
+
* rows (row 0 always styles the header, so it is not a pick). Everything outside
|
|
20
|
+
* the range is a fixed row rendered once: rows `[1, start-1]` (top — the
|
|
21
|
+
* under-header row, never looped, so its divider survives) back the first data
|
|
22
|
+
* rows; the `[start, end]` range cycles to fill the middle; rows `[end+1, R-1]`
|
|
23
|
+
* (bottom — a decorated total row) back the last data rows. Throws (naming the
|
|
24
|
+
* shape) when the range is out of bounds or the deck supplies too few rows to fill
|
|
25
|
+
* the fixed rows.
|
|
19
26
|
*/
|
|
20
|
-
export declare function
|
|
27
|
+
export declare function resolveRowPlan(bodyRows: BodyRows, R: number, K: number, shapeName: string): {
|
|
28
|
+
headerIdx: number;
|
|
29
|
+
picks: number[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Fill a table shape by composing its specimen rows per the body-range plan (see
|
|
33
|
+
* `resolveRowPlan`). Each output cell's first paragraph is rebuilt from the
|
|
34
|
+
* corresponding StyledParagraph, so tables inherit rich-run/bullet support for
|
|
35
|
+
* free; the specimen row's `<a:tcPr>` is carried over untouched.
|
|
36
|
+
*/
|
|
37
|
+
export declare function fillTable(shape: any, table: TableFill, shapeName: string, bodyRows: BodyRows): void;
|
|
21
38
|
/** Discriminator for TableFill values. */
|
|
22
39
|
export declare function isTableFill(v: unknown): v is TableFill;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Table fill —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Table fill — composes a variable number of output rows from a fixed table
|
|
3
|
+
* specimen in `<a:tbl>` by cloning specimen rows around one contiguous `bodyRows`
|
|
4
|
+
* range, then fills each cell's first paragraph with the corresponding
|
|
5
|
+
* StyledParagraph. Row 0 styles the header; the rows before the range are top
|
|
6
|
+
* fixed rows (rendered once — the under-header row lives here, never looped, so
|
|
7
|
+
* its divider survives); the `[start, end]` range is the repeatable body, cycled
|
|
8
|
+
* to fill the deck's data; the rows after it are bottom fixed rows (rendered once
|
|
9
|
+
* — a decorated total row lives here). `<a:tcPr>` (fill, borders, margins) is
|
|
10
|
+
* preserved per row — only the cell text is rebuilt. Cells and `<a:gridCol>`
|
|
11
|
+
* entries are cloned or trimmed to the header count, sharing the template's total
|
|
12
|
+
* width. Row, cell, and grid cloning stay engine-side because they need
|
|
13
|
+
* pptx-automizer DOM access.
|
|
7
14
|
*/
|
|
8
15
|
import { Attr, collectElements, isPlainObject, rebuildParagraphs, Tag } from "../dom.js";
|
|
9
16
|
const EMPTY_CELL = { runs: [{ text: "" }] };
|
|
@@ -50,30 +57,58 @@ function reconcileGrid(tbl, n) {
|
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Validate a `bodyRows = [start, end]` range against a specimen's actual row
|
|
61
|
+
* count `R`, then compose the `K` specimen-row picks that back the deck's `K` data
|
|
62
|
+
* rows (row 0 always styles the header, so it is not a pick). Everything outside
|
|
63
|
+
* the range is a fixed row rendered once: rows `[1, start-1]` (top — the
|
|
64
|
+
* under-header row, never looped, so its divider survives) back the first data
|
|
65
|
+
* rows; the `[start, end]` range cycles to fill the middle; rows `[end+1, R-1]`
|
|
66
|
+
* (bottom — a decorated total row) back the last data rows. Throws (naming the
|
|
67
|
+
* shape) when the range is out of bounds or the deck supplies too few rows to fill
|
|
68
|
+
* the fixed rows.
|
|
62
69
|
*/
|
|
63
|
-
export function
|
|
70
|
+
export function resolveRowPlan(bodyRows, R, K, shapeName) {
|
|
71
|
+
const [start, end] = bodyRows;
|
|
72
|
+
// Row 0 is the header, so the body must start at 1 or later; the range must be
|
|
73
|
+
// non-empty and stay within the specimen's rows.
|
|
74
|
+
if (!(start >= 1 && start <= end && end <= R - 1)) {
|
|
75
|
+
throw new Error(`Table shape "${shapeName}": bodyRows [${start}, ${end}] is out of range; require 1 <= start <= end <= ${R - 1} (row 0 is the header).`);
|
|
76
|
+
}
|
|
77
|
+
const topFixed = start - 1; // rows [1, start-1]
|
|
78
|
+
const bottomFixed = R - 1 - end; // rows [end+1, R-1]
|
|
79
|
+
const bodyCount = K - topFixed - bottomFixed;
|
|
80
|
+
const span = end - start + 1;
|
|
81
|
+
// The deck must supply enough data rows to back every fixed row (top + bottom);
|
|
82
|
+
// below that there is nothing to cycle the body over and the total row goes unfilled.
|
|
83
|
+
if (bodyCount < 0) {
|
|
84
|
+
throw new Error(`Table shape "${shapeName}": deck supplies ${K} data row(s) but the specimen's fixed rows need at least ${topFixed + bottomFixed}.`);
|
|
85
|
+
}
|
|
86
|
+
const picks = [];
|
|
87
|
+
for (let r = 1; r <= start - 1; r++)
|
|
88
|
+
picks.push(r); // top fixed
|
|
89
|
+
for (let i = 0; i < bodyCount; i++)
|
|
90
|
+
picks.push(start + (i % span)); // body, cycled
|
|
91
|
+
for (let r = end + 1; r <= R - 1; r++)
|
|
92
|
+
picks.push(r); // bottom fixed
|
|
93
|
+
return { headerIdx: 0, picks };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fill a table shape by composing its specimen rows per the body-range plan (see
|
|
97
|
+
* `resolveRowPlan`). Each output cell's first paragraph is rebuilt from the
|
|
98
|
+
* corresponding StyledParagraph, so tables inherit rich-run/bullet support for
|
|
99
|
+
* free; the specimen row's `<a:tcPr>` is carried over untouched.
|
|
100
|
+
*/
|
|
101
|
+
export function fillTable(shape, table, shapeName, bodyRows) {
|
|
64
102
|
const tbl = shape.getElementsByTagName(Tag.TABLE)[0];
|
|
65
103
|
if (!tbl) {
|
|
66
104
|
throw new Error(`Table shape "${shapeName}": has no <a:tbl> element (is it actually a table?).`);
|
|
67
105
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
const specimenRows = collectElements(tbl, Tag.TABLE_ROW);
|
|
107
|
+
const R = specimenRows.length;
|
|
108
|
+
if (R < 2) {
|
|
109
|
+
throw new Error(`fillTable: template table has ${R} row(s), need at least 2 (header + data specimen)`);
|
|
71
110
|
}
|
|
72
|
-
const
|
|
73
|
-
// Only rows 0-2 are specimens (header, data, optional zebra); any further
|
|
74
|
-
// template rows are intentionally dropped — the specimen rows are re-cloned
|
|
75
|
-
// per data row below.
|
|
76
|
-
const dataTpls = rows.length > 2 ? [rows[1], rows[2]] : [rows[1]];
|
|
111
|
+
const { headerIdx, picks } = resolveRowPlan(bodyRows, R, table.rows.length, shapeName);
|
|
77
112
|
// Headers are the source of truth for column count; every row is normalized to
|
|
78
113
|
// it, and short/long data rows are padded/truncated to match.
|
|
79
114
|
const n = table.headers.length;
|
|
@@ -88,15 +123,15 @@ export function fillTable(shape, table, shapeName = "") {
|
|
|
88
123
|
return clone;
|
|
89
124
|
};
|
|
90
125
|
const built = [];
|
|
91
|
-
built.push(fillRow(
|
|
92
|
-
for (let r = 0; r <
|
|
93
|
-
built.push(fillRow(
|
|
126
|
+
built.push(fillRow(specimenRows[headerIdx], table.headers));
|
|
127
|
+
for (let r = 0; r < picks.length; r++) {
|
|
128
|
+
built.push(fillRow(specimenRows[picks[r]], table.rows[r]));
|
|
94
129
|
}
|
|
95
130
|
// Column invariant: grid `<a:gridCol>` count must equal every row's `<a:tc>`
|
|
96
131
|
// count (both == n). `normalizeCellCount` (in fillRow) holds the per-row half;
|
|
97
132
|
// `reconcileGrid` holds the grid half. Run the grid half once, table-global.
|
|
98
133
|
reconcileGrid(tbl, n);
|
|
99
|
-
for (const row of
|
|
134
|
+
for (const row of specimenRows)
|
|
100
135
|
tbl.removeChild(row);
|
|
101
136
|
for (const row of built)
|
|
102
137
|
tbl.appendChild(row);
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* `generate()` is first below; its helpers follow (function declarations hoist).
|
|
28
28
|
*/
|
|
29
|
-
import type
|
|
29
|
+
import { type Config, type Deck, type DeckStep, type Layout } from "./types.js";
|
|
30
30
|
/** Options for `generate` / `buildDeck`. */
|
|
31
31
|
export type GenerateOptions = {
|
|
32
32
|
/**
|
|
@@ -53,12 +53,13 @@ export declare function generate(deck: Deck, config: Config, options?: GenerateO
|
|
|
53
53
|
/**
|
|
54
54
|
* Fill one cloned slide. Per slot the step supplies a value for: resolve WHICH
|
|
55
55
|
* shape realizes it (`resolveBlock`), build WHAT to write (`FILLERS[…].callbacks`),
|
|
56
|
-
* and place it WHERE/HOW (`applyBlock`).
|
|
57
|
-
*
|
|
56
|
+
* and place it WHERE/HOW (`applyBlock`). A slot the step leaves unfilled has its
|
|
57
|
+
* base-slide shape removed, so a layout with numbered slots renders exactly the
|
|
58
|
+
* ones supplied. Every ambiguity fails fast, naming layout + slot.
|
|
58
59
|
*
|
|
59
60
|
* Exported for tests; `generate()` calls it inside the `addSlide` callback.
|
|
60
61
|
*/
|
|
61
|
-
export declare function fillSlide(slide: any, layout: Layout, step: DeckStep, sourceAlias: string): void;
|
|
62
|
+
export declare function fillSlide(slide: any, layout: Layout, step: DeckStep, sourceAlias: string, slideNumber: number): void;
|
|
62
63
|
/**
|
|
63
64
|
* Reject a slot whose `accepts` lists two blocks of the same type — the
|
|
64
65
|
* value→block lookup would silently pick the first. Called once per layout at
|