@tycoworks/tycoslide 0.14.0 → 0.15.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
@@ -2,67 +2,32 @@
2
2
 
3
3
  Create editable, on-brand PowerPoint slides from markdown, using your existing .pptx templates.
4
4
 
5
- > **Early release.** tycoslide is under active development.
6
-
7
5
  ## Getting started
8
6
 
9
7
  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.
8
+ 2. **Write slides in markdown.** GitHub-flavored markdown is supported; see [Markdown support](#markdown-support) below. tycoslide themes also include an agent skill, so an agent can write the slides for you.
11
9
  3. **Build.** `npx tycoslide build deck.md` compiles the markdown into an editable PowerPoint file.
12
10
 
13
- ## Example
11
+ ## See it working
14
12
 
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:
13
+ [how-it-works.md](https://github.com/tycoworks/tycoworks-theme/blob/main/how-it-works.md) is a deck about tycoslide, written against the [tycoworks-theme](https://github.com/tycoworks/tycoworks-theme). Clone the theme and build it:
16
14
 
17
15
  ```bash
18
16
  git clone https://github.com/tycoworks/tycoworks-theme && cd tycoworks-theme
19
17
  npm install
20
- npx tycoslide build showcase.md
18
+ npx tycoslide build how-it-works.md
21
19
  ```
22
20
 
23
- A deck file against that theme looks like this (full syntax in [syntax.md](theme-package/syntax.md)):
24
-
25
- ````markdown
26
- ---
27
- theme: ./theme.json
28
- ---
29
-
30
- ---
31
- layout: Title
32
- title: Quarterly Review
33
- name: Jane Doe
34
- jobTitle: Engineering
35
- ---
36
-
37
- ---
38
- layout: Image right
39
- title: How requests flow
40
- ---
41
-
42
- ::body::
43
-
44
- - Every request is checked before it reaches the model
45
- - Rejected requests never leave the gateway
21
+ ## Markdown support
46
22
 
47
- ::image::
23
+ - Paragraphs, bullets and numbered lists, with bold, italic and links
24
+ - Tables
25
+ - Code, with [Shiki](https://shiki.style) syntax highlighting
26
+ - [Mermaid](https://mermaid.js.org) diagrams
27
+ - Images, from the theme's asset catalog or a file path
28
+ - Speaker notes
48
29
 
49
- ```mermaid
50
- flowchart TD
51
- A[Client] --> B[Gateway] --> C[Model]
52
- ```
53
-
54
- ---
55
- layout: Code
56
- title: Calling the API
57
- ---
58
-
59
- ::code::
60
-
61
- ```python
62
- client = Client(api_key)
63
- deck = client.build("deck.md")
64
- ```
65
- ````
30
+ Full syntax in [syntax.md](theme-package/syntax.md).
66
31
 
67
32
  ## Requirements
68
33
 
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ program
38
38
  if (!absConfigPath) {
39
39
  throw new Error(`${basename(deckPath)}: missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
40
40
  }
41
- const config = { ...loadThemeConfig(absConfigPath), browserPath: opts.browserPath };
41
+ const config = { ...loadThemeConfig(absConfigPath), browserPath: opts.browserPath, deckDir: dirname(absDeckPath) };
42
42
  const deck = await compileDeck(doc, config);
43
43
  // Always write the .pptx next to the input deck, named after it.
44
44
  const outName = basename(deckPath).replace(/\.md$/, ".pptx");
@@ -346,14 +346,11 @@ 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
355
- // no modelled style (which renders in the app default colour, e.g. black on
356
- // a dark slide whose real text colour lives only in the specimen's rPr).
352
+ // no modelled style (which renders in the app default color, e.g. black on
353
+ // a dark slide whose real text color lives only in the specimen's rPr).
357
354
  if (isBullet && bullets.size === 0) {
358
355
  throw new Error(`Shape "${shapeName}": received bulleted content, but its specimen has no bulleted paragraph to model — this slot accepts plain text only.`);
359
356
  }
@@ -17,7 +17,7 @@ declare const Placement: {
17
17
  readonly Crop: "crop";
18
18
  readonly Fit: "fit";
19
19
  };
20
- /** Either a symmetric `<a:srcRect>` crop (fill/cover) or a resized+re-centred frame (fit/contain). */
20
+ /** Either a symmetric `<a:srcRect>` crop (fill/cover) or a resized+re-centerd frame (fit/contain). */
21
21
  type FitGeometry = {
22
22
  placement: typeof Placement.Crop;
23
23
  left: number;
@@ -36,7 +36,7 @@ export type GeometryResult = {
36
36
  /**
37
37
  * Size a picture shape from its `ImageFit` (via `computeGeometry`): either write
38
38
  * `<a:srcRect>` insets to fill-and-crop, or shrink the frame to the image's
39
- * aspect ratio and re-centre (fit/letterbox). Advisory warnings from the
39
+ * aspect ratio and re-center (fit/letterbox). Advisory warnings from the
40
40
  * geometry pass go to `console.warn`.
41
41
  *
42
42
  * `image.path` is assumed absolute — the compiler / caller resolves it before the
@@ -24,7 +24,7 @@ const MIN_SCALE = 0.2;
24
24
  /**
25
25
  * Size a picture shape from its `ImageFit` (via `computeGeometry`): either write
26
26
  * `<a:srcRect>` insets to fill-and-crop, or shrink the frame to the image's
27
- * aspect ratio and re-centre (fit/letterbox). Advisory warnings from the
27
+ * aspect ratio and re-center (fit/letterbox). Advisory warnings from the
28
28
  * geometry pass go to `console.warn`.
29
29
  *
30
30
  * `image.path` is assumed absolute — the compiler / caller resolves it before the
@@ -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;
@@ -89,7 +89,7 @@ export type TableFill = {
89
89
  /**
90
90
  * How a picture is scaled into its frame — the engine's image-sizing directive,
91
91
  * mirroring CSS `object-fit`. `contain`: fit the whole image, scale both ways,
92
- * letterbox. `cover`: fill the frame, centre-crop the overflow. `scale-down`:
92
+ * letterbox. `cover`: fill the frame, center-crop the overflow. `scale-down`:
93
93
  * like contain but never enlarge past native (a small image sits at native size).
94
94
  */
95
95
  export declare const ImageFit: {
@@ -22,7 +22,7 @@ export const SlotType = {
22
22
  /**
23
23
  * How a picture is scaled into its frame — the engine's image-sizing directive,
24
24
  * mirroring CSS `object-fit`. `contain`: fit the whole image, scale both ways,
25
- * letterbox. `cover`: fill the frame, centre-crop the overflow. `scale-down`:
25
+ * letterbox. `cover`: fill the frame, center-crop the overflow. `scale-down`:
26
26
  * like contain but never enlarge past native (a small image sits at native size).
27
27
  */
28
28
  export const ImageFit = {
package/dist/index.d.ts CHANGED
@@ -20,9 +20,9 @@ export declare function toEngineConfig(config: CompilerConfig): Config;
20
20
  * and hands the deck to the engine's primitives-only `generate()`. The deck is
21
21
  * structurally equivalent to the engine's `Deck` once `output` is present, so no
22
22
  * cast is required. `buildDeck` does not itself validate `config` — a
23
- * programmatic caller assembling a `CompilerConfig` by hand should load it
24
- * through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
25
- * fail-fast structural checks the CLI gets.
23
+ * programmatic caller assembling a `CompilerConfig` by hand should load the
24
+ * theme through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
25
+ * fail-fast structural checks the CLI gets, then add the deck's `deckDir`.
26
26
  *
27
27
  * Fails fast if `deck.output` is missing: `generate()` requires it, and the CLI
28
28
  * populates it before calling `buildDeck`; a programmatic caller that forgot to
@@ -36,6 +36,6 @@ export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot,
36
36
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
37
37
  export { ASSETS_ARCHIVE, ASSETS_FILE } from "./files.js";
38
38
  export { generateAssetCatalog, generateManifest } from "./manifest.js";
39
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
39
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
40
40
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
41
41
  export { expandAssets } from "./skillZip.js";
package/dist/index.js CHANGED
@@ -98,9 +98,9 @@ export function toEngineConfig(config) {
98
98
  * and hands the deck to the engine's primitives-only `generate()`. The deck is
99
99
  * structurally equivalent to the engine's `Deck` once `output` is present, so no
100
100
  * cast is required. `buildDeck` does not itself validate `config` — a
101
- * programmatic caller assembling a `CompilerConfig` by hand should load it
102
- * through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
103
- * fail-fast structural checks the CLI gets.
101
+ * programmatic caller assembling a `CompilerConfig` by hand should load the
102
+ * theme through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
103
+ * fail-fast structural checks the CLI gets, then add the deck's `deckDir`.
104
104
  *
105
105
  * Fails fast if `deck.output` is missing: `generate()` requires it, and the CLI
106
106
  * populates it before calling `buildDeck`; a programmatic caller that forgot to
@@ -1,5 +1,5 @@
1
- import type { CompilerConfig } from "./markdown/types.js";
1
+ import type { CompilerThemeConfig } from "./markdown/types.js";
2
2
  /** The layouts document: read whole, so it carries no open-ended list. */
3
- export declare function generateManifest(config: CompilerConfig): string;
3
+ export declare function generateManifest(config: CompilerThemeConfig): string;
4
4
  /** The catalog document: searched by name, never read whole. */
5
- export declare function generateAssetCatalog(config: CompilerConfig): string;
5
+ export declare function generateAssetCatalog(config: CompilerThemeConfig): string;
@@ -143,7 +143,7 @@ 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. */
146
+ /** Device pixels per CSS pixel when rasterizing a diagram. Sized for print density. */
147
147
  const MERMAID_RASTER_SCALE = 6;
148
148
  /**
149
149
  * Find a browser rather than ship one. tycoslide never downloads Chromium: the
@@ -247,7 +247,7 @@ ${fontFaceCss(fonts)}
247
247
  // Written inside the try so the finally always cleans it up, even if launch throws.
248
248
  writeFileSync(htmlPath, html);
249
249
  browser = await launchChromium(chromium, browserPath);
250
- // Diagrams land in slide-sized frames, so they are rasterised well above CSS
250
+ // Diagrams land in slide-sized frames, so they are rasterized well above CSS
251
251
  // scale: a typical flowchart is ~270 CSS px wide against a slot wanting
252
252
  // ~1400 px at print density. 2x left them visibly pixelated.
253
253
  const page = await browser.newPage({
@@ -245,11 +245,12 @@ export async function compileDeck(doc, config) {
245
245
  assertUniqueSlideNumbers(layouts);
246
246
  for (const layout of layouts)
247
247
  validateLayout(layout);
248
- // Resolve a `$category.name` reference against the theme's curated asset
249
- // catalog. Anchored the whole ref is the reference or it is nothing; a found
250
- // entry wraps through `toImageFill`, which maps the asset's declared type to
251
- // its ImageFit.
252
- const resolveAssetRef = (ref) => {
248
+ // A body image names a picture one of two ways, and both end as an absolute
249
+ // path plus a fit: `$category.name` looks the picture up in the theme's
250
+ // catalog (which carries its declared type), while anything else is a file
251
+ // path relative to the deck (typed `image`, since nothing declares it). Only
252
+ // the lookup differs; the wrapping is shared.
253
+ const fromCatalog = (ref) => {
253
254
  const match = ASSET_REF_RE.exec(ref);
254
255
  if (!match) {
255
256
  throw new Error(`Asset reference "${ref}" must be in the form $category.name (e.g. $logos.primary).`);
@@ -271,7 +272,15 @@ export async function compileDeck(doc, config) {
271
272
  .join(", ");
272
273
  throw new Error(`Unknown asset reference "${ref}". Available: ${available}`);
273
274
  }
274
- return toImageFill(resolveImagePath(rootDir, entry.path), entry.type);
275
+ return { path: resolveImagePath(rootDir, entry.path), type: entry.type };
276
+ };
277
+ const fromDeck = (ref) => ({
278
+ path: resolveImagePath(config.deckDir, ref),
279
+ type: AssetType.Image,
280
+ });
281
+ const resolveAssetRef = (ref) => {
282
+ const { path, type } = ref.startsWith("$") ? fromCatalog(ref) : fromDeck(ref);
283
+ return toImageFill(path, type);
275
284
  };
276
285
  // Slides compile in order: a slide's structural errors (unknown layout/key,
277
286
  // bad asset ref, accept-type mismatch) fire before its own content is rendered.
@@ -9,5 +9,5 @@ export { parseRegion } from "./mdast.js";
9
9
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
10
10
  export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
11
  export { parseSlideDocument } from "./slideParser.js";
12
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, } from "./types.js";
12
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, } from "./types.js";
13
13
  export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
@@ -1,5 +1,5 @@
1
1
  import * as z from "zod";
2
- import { type CompilerConfig, type CompilerThemeConfig } from "../types.js";
2
+ import { type CompilerThemeConfig, type LoadedTheme } from "../types.js";
3
3
  export declare const ThemeConfigSchema: z.ZodObject<{
4
4
  layouts: z.ZodArray<z.ZodObject<{
5
5
  name: z.ZodString;
@@ -43,8 +43,8 @@ export declare const ThemeConfigSchema: z.ZodObject<{
43
43
  assets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
44
44
  path: z.ZodString;
45
45
  type: z.ZodEnum<{
46
- image: "image";
47
46
  icon: "icon";
47
+ image: "image";
48
48
  background: "background";
49
49
  }>;
50
50
  description: z.ZodString;
@@ -82,10 +82,10 @@ export declare const ThemeConfigSchema: z.ZodObject<{
82
82
  */
83
83
  export declare function parseThemeConfig(raw: unknown, sourcePath: string): CompilerThemeConfig;
84
84
  /**
85
- * Read, validate, and root a `theme.json` at `absPath` into a `CompilerConfig`.
85
+ * Read, validate, and root a `theme.json` at `absPath` into a `LoadedTheme`.
86
86
  * The single loader the CLI and programmatic callers share: file read/JSON
87
87
  * failures fail fast with the path; structural failures go through
88
88
  * `parseThemeConfig`. `rootDir` (the config's directory) is attached AFTER
89
89
  * validation — it is not a JSON field.
90
90
  */
91
- export declare function loadThemeConfig(absPath: string): CompilerConfig;
91
+ export declare function loadThemeConfig(absPath: string): LoadedTheme;
@@ -129,7 +129,7 @@ export function parseThemeConfig(raw, sourcePath) {
129
129
  return r.data;
130
130
  }
131
131
  /**
132
- * Read, validate, and root a `theme.json` at `absPath` into a `CompilerConfig`.
132
+ * Read, validate, and root a `theme.json` at `absPath` into a `LoadedTheme`.
133
133
  * The single loader the CLI and programmatic callers share: file read/JSON
134
134
  * failures fail fast with the path; structural failures go through
135
135
  * `parseThemeConfig`. `rootDir` (the config's directory) is attached AFTER
@@ -274,8 +274,25 @@ export type CompilerThemeConfig = {
274
274
  };
275
275
  mermaidVariant?: string;
276
276
  };
277
- export type CompilerConfig = CompilerThemeConfig & {
277
+ /**
278
+ * A theme as loaded from disk: its validated config plus the directory it
279
+ * lives in, which the catalog's relative asset paths resolve against.
280
+ */
281
+ export type LoadedTheme = CompilerThemeConfig & {
278
282
  rootDir: string;
283
+ };
284
+ /**
285
+ * Everything one build needs: the loaded theme plus what this particular deck
286
+ * brings. Neither addition is a theme.json field; both are attached per
287
+ * invocation, the way `rootDir` is attached after load.
288
+ */
289
+ export type CompilerConfig = LoadedTheme & {
290
+ /**
291
+ * The deck file's directory, which a body image written as a file path
292
+ * resolves against. The CLI takes it from the deck's location; a
293
+ * programmatic caller that compiles from a string supplies it.
294
+ */
295
+ deckDir: string;
279
296
  /**
280
297
  * Browser executable to render mermaid with, from `--browser-path`. Added
281
298
  * after load like `rootDir`: it is a per-invocation choice, not a theme's.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
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",
@@ -134,13 +134,13 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
134
134
  ````
135
135
  The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting, using the theme's `codeTheme` (set once in `theme.json`, not per slot). Colors are applied as native text runs in the output, not images.
136
136
  - **table** (slots that accept `table`) -- a GFM table. Write it in the slot region between `|`-delimited headers and rows; cells inherit inline formatting (bold, italic, links).
137
- - **image** (slots that accept `image`) -- a picture from the theme's asset catalog, written as `![]($category.name)`:
137
+ - **image** (slots that accept `image`) -- a picture, written as `![]($category.name)` for one from the theme's asset catalog, or `![](path/to/file.png)` for a file relative to the deck:
138
138
  ```markdown
139
139
  ::logo::
140
140
 
141
141
  ![]($brand.primaryDarkWordmark)
142
142
  ```
143
- The categories and names are catalogued in `assets.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
143
+ The categories and names are cataloged in `assets.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A file path has no catalog entry, so it is always fitted as `image`; put a picture in the catalog when it needs another fit or when agents should be able to find it. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
144
144
 
145
145
  ---
146
146