@tycoworks/tycoslide 0.16.1 → 0.17.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 +1 -1
- package/dist/agents/commands.js +1 -2
- package/dist/agents/inventory.js +20 -1
- package/dist/agents/manifest.js +4 -1
- package/dist/agents/skill.d.ts +13 -1
- package/dist/agents/skill.js +31 -2
- package/dist/engine/dom.d.ts +2 -0
- package/dist/engine/dom.js +40 -17
- package/dist/engine/fillers/filler.d.ts +4 -2
- package/dist/engine/fillers/filler.js +2 -2
- package/dist/engine/fillers/image.d.ts +3 -3
- package/dist/engine/fillers/image.js +4 -4
- package/dist/engine/generate.js +3 -2
- package/dist/engine/types.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/markdown/blocks/image.d.ts +0 -23
- package/dist/markdown/blocks/image.js +3 -55
- package/dist/markdown/blocks/mermaid.d.ts +2 -2
- package/dist/markdown/blocks/mermaid.js +7 -7
- package/dist/markdown/blocks/mermaidTheme.d.ts +1 -1
- package/dist/markdown/blocks/mermaidTheme.js +2 -2
- package/dist/markdown/schema/themeConfigSchema.d.ts +5 -0
- package/dist/markdown/schema/themeConfigSchema.js +6 -4
- package/dist/markdown/slideParser.d.ts +0 -2
- package/dist/markdown/slideParser.js +1 -1
- package/dist/markdown/types.d.ts +7 -4
- package/dist/markdown/types.js +2 -1
- package/docs/markdown.md +5 -6
- package/docs/theme.md +6 -5
- package/package.json +1 -1
- package/theme-package/SKILL.template.md +7 -7
- package/dist/agents/catalog.d.ts +0 -32
- package/dist/agents/catalog.js +0 -37
package/README.md
CHANGED
|
@@ -35,4 +35,4 @@ Node 23.6 or later. Mermaid diagrams need Chrome on the machine; tycoslide finds
|
|
|
35
35
|
|
|
36
36
|
## Thanks
|
|
37
37
|
|
|
38
|
-
tycoslide was inspired by [pptx-automizer](https://github.com/singerla/pptx-automizer), which it uses under the hood, and by [Slidev](https://sli.dev)
|
|
38
|
+
tycoslide was inspired by [pptx-automizer](https://github.com/singerla/pptx-automizer), which it uses under the hood, and by [Slidev](https://sli.dev)'s way of writing slides in markdown.
|
package/dist/agents/commands.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, posix, resolve } from "node:path";
|
|
3
3
|
import { loadThemeConfig } from "../index.js";
|
|
4
|
-
import { loadAssetCatalog } from "./catalog.js";
|
|
5
4
|
import { extractTemplate, summarizeExtraction } from "./extract.js";
|
|
6
5
|
import { ASSETS_DIR, ASSETS_FILE, MANIFEST_FILE, PACKAGE_JSON, SKILL_FILE, SKILL_TEMPLATE_FILE, SKILL_ZIP_EXT, TEMPLATE_FILE, THEME_CONFIG, THEME_PACKAGE_DIR, } from "./files.js";
|
|
7
6
|
import { generateManifest } from "./manifest.js";
|
|
8
|
-
import { renameSkill, skillPackageJson, zipDir } from "./skill.js";
|
|
7
|
+
import { loadAssetCatalog, renameSkill, skillPackageJson, zipDir } from "./skill.js";
|
|
9
8
|
/** The CLI's line for something written, with an optional note on what it holds. */
|
|
10
9
|
function wrote(target, detail) {
|
|
11
10
|
console.log(detail ? `WROTE ${target} ${detail}` : `WROTE ${target}`);
|
package/dist/agents/inventory.js
CHANGED
|
@@ -87,6 +87,8 @@ const Attr = {
|
|
|
87
87
|
X: "x",
|
|
88
88
|
Y: "y",
|
|
89
89
|
TEXT_BOX: "txBox",
|
|
90
|
+
WIDTH: "w",
|
|
91
|
+
HEIGHT: "h",
|
|
90
92
|
};
|
|
91
93
|
/** The twelve colour-scheme slots, in the order PowerPoint lists them. */
|
|
92
94
|
export const SCHEME_SLOTS = [
|
|
@@ -349,7 +351,7 @@ function describeShape(el, prefix) {
|
|
|
349
351
|
return {
|
|
350
352
|
name: prefix + (find(nonVisualProps(el), Tag.NON_VISUAL_PROPS)?.getAttribute(Attr.NAME) ?? ""),
|
|
351
353
|
kind: shapeKind(el, table),
|
|
352
|
-
frame: shapeFrame(el),
|
|
354
|
+
frame: table ? tableFrame(el, table) : shapeFrame(el),
|
|
353
355
|
rows: table && elementChildren(table, Tag.TABLE_ROW).length,
|
|
354
356
|
cols: table && elementChildren(find(table, Tag.TABLE_GRID), Tag.GRID_COLUMN).length,
|
|
355
357
|
text: el.tagName === Tag.GROUP ? "" : shapeText(el),
|
|
@@ -399,6 +401,23 @@ function shapeFrame(el) {
|
|
|
399
401
|
cy: Number(extent.getAttribute(Attr.CY)),
|
|
400
402
|
};
|
|
401
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* A table's frame: where its graphic frame puts it, at the size its column widths
|
|
406
|
+
* and row heights give it. PowerPoint draws a table at that size, rows growing
|
|
407
|
+
* taller when their text needs it, and some tools (Google Slides among them)
|
|
408
|
+
* write a placeholder size on the frame.
|
|
409
|
+
*/
|
|
410
|
+
function tableFrame(el, table) {
|
|
411
|
+
const frame = shapeFrame(el);
|
|
412
|
+
if (!frame)
|
|
413
|
+
return undefined;
|
|
414
|
+
const total = (els, attr) => els.reduce((sum, e) => sum + Number(e.getAttribute(attr) || 0), 0);
|
|
415
|
+
return {
|
|
416
|
+
...frame,
|
|
417
|
+
cx: total(elementChildren(find(table, Tag.TABLE_GRID), Tag.GRID_COLUMN), Attr.WIDTH) || frame.cx,
|
|
418
|
+
cy: total(elementChildren(table, Tag.TABLE_ROW), Attr.HEIGHT) || frame.cy,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
402
421
|
function nonVisualProps(el) {
|
|
403
422
|
return elementChildren(el).find((child) => child.tagName.startsWith(NON_VISUAL_PREFIX));
|
|
404
423
|
}
|
package/dist/agents/manifest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { templateKeys, } from "../index.js";
|
|
1
|
+
import { AcceptType, templateKeys, } from "../index.js";
|
|
2
2
|
import { ASSETS_FILE, jsonFile } from "./files.js";
|
|
3
3
|
/**
|
|
4
4
|
* Flatten a compiler parameter to the manifest entries advertised to AI authors.
|
|
@@ -15,6 +15,9 @@ function stripParameter(param) {
|
|
|
15
15
|
}
|
|
16
16
|
function stripSlot(slot) {
|
|
17
17
|
const result = { key: slot.key, accepts: slot.accepts.map((b) => b.type) };
|
|
18
|
+
for (const block of slot.accepts)
|
|
19
|
+
if (block.type === AcceptType.Image)
|
|
20
|
+
result.fit = block.fit;
|
|
18
21
|
if (slot.required)
|
|
19
22
|
result.required = true;
|
|
20
23
|
return result;
|
package/dist/agents/skill.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
1
2
|
import { type CompilerThemeConfig } from "../index.js";
|
|
2
|
-
|
|
3
|
+
declare const AssetCatalogSchema: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
4
|
+
path: z.ZodString;
|
|
5
|
+
description: z.ZodString;
|
|
6
|
+
}, z.core.$strict>>>;
|
|
7
|
+
export type AssetCatalog = z.infer<typeof AssetCatalogSchema>;
|
|
8
|
+
/**
|
|
9
|
+
* Read and validate a theme's image catalog, `assets.json`: what a deck-writing
|
|
10
|
+
* agent searches and copies images from, and what a packaged skill archives. It
|
|
11
|
+
* is authored with the theme and required; a theme with no images has `{}`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadAssetCatalog(themeDir: string): AssetCatalog;
|
|
3
14
|
/**
|
|
4
15
|
* Pack `paths` (theme-relative, POSIX) into one archive, reading each through
|
|
5
16
|
* `read`. Entries keep their declared paths, so expanding reproduces the layout
|
|
@@ -41,3 +52,4 @@ export declare function skillPackageJson(theme: Record<string, unknown>, engine:
|
|
|
41
52
|
* catalog declares but that is missing is an error.
|
|
42
53
|
*/
|
|
43
54
|
export declare function zipDir(rootDir: string, folderName: string, config: CompilerThemeConfig, catalog: AssetCatalog, themeFiles: string[], generated: Record<string, string>): Promise<Buffer>;
|
|
55
|
+
export {};
|
package/dist/agents/skill.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import JSZip from "jszip";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import { strict, TEMPLATE_DIR } from "../index.js";
|
|
6
|
+
import { ASSETS_ARCHIVE, ASSETS_FILE, jsonFile } from "./files.js";
|
|
7
|
+
/** One image a theme offers deck authors: where it is, and what it shows. */
|
|
8
|
+
const AssetEntrySchema = strict({
|
|
9
|
+
path: z.string(),
|
|
10
|
+
description: z.string(),
|
|
11
|
+
});
|
|
12
|
+
// `{ category: { name: entry } }`. Both record levels are open (the names are the
|
|
13
|
+
// theme's own); only the entry is strict.
|
|
14
|
+
const AssetCatalogSchema = z.record(z.string(), z.record(z.string(), AssetEntrySchema));
|
|
15
|
+
/**
|
|
16
|
+
* Read and validate a theme's image catalog, `assets.json`: what a deck-writing
|
|
17
|
+
* agent searches and copies images from, and what a packaged skill archives. It
|
|
18
|
+
* is authored with the theme and required; a theme with no images has `{}`.
|
|
19
|
+
*/
|
|
20
|
+
export function loadAssetCatalog(themeDir) {
|
|
21
|
+
const path = join(themeDir, ASSETS_FILE);
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Image catalog not found or invalid JSON: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
const result = AssetCatalogSchema.safeParse(raw);
|
|
30
|
+
if (!result.success) {
|
|
31
|
+
throw new Error(`${ASSETS_FILE}: invalid image catalog\n${z.prettifyError(result.error)}`);
|
|
32
|
+
}
|
|
33
|
+
return result.data;
|
|
34
|
+
}
|
|
6
35
|
/** Entries are stored, not deflated: assets are already-compressed images. */
|
|
7
36
|
const NO_COMPRESSION = { type: "nodebuffer", compression: "STORE" };
|
|
8
37
|
/**
|
package/dist/engine/dom.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare const Tag: {
|
|
|
30
30
|
readonly BLIP_FILL: "p:blipFill";
|
|
31
31
|
readonly NON_VISUAL_PROPS: "p:cNvPr";
|
|
32
32
|
readonly SPACE_BEFORE: "a:spcBef";
|
|
33
|
+
readonly LINE_SPACING: "a:lnSpc";
|
|
34
|
+
readonly OUTLINE: "a:ln";
|
|
33
35
|
readonly HLINK_CLICK: "a:hlinkClick";
|
|
34
36
|
readonly RELATIONSHIP: "Relationship";
|
|
35
37
|
readonly TABLE: "a:tbl";
|
package/dist/engine/dom.js
CHANGED
|
@@ -30,6 +30,8 @@ export const Tag = {
|
|
|
30
30
|
BLIP_FILL: "p:blipFill",
|
|
31
31
|
NON_VISUAL_PROPS: "p:cNvPr",
|
|
32
32
|
SPACE_BEFORE: "a:spcBef",
|
|
33
|
+
LINE_SPACING: "a:lnSpc",
|
|
34
|
+
OUTLINE: "a:ln",
|
|
33
35
|
HLINK_CLICK: "a:hlinkClick",
|
|
34
36
|
RELATIONSHIP: "Relationship",
|
|
35
37
|
TABLE: "a:tbl",
|
|
@@ -106,6 +108,16 @@ export function childrenByTag(parent, tagName) {
|
|
|
106
108
|
}
|
|
107
109
|
return out;
|
|
108
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Insert `child` as `parent`'s first child, or right after a leading `leadingTag`
|
|
113
|
+
* child when there is one. The schema puts a run's fill after only its outline, and
|
|
114
|
+
* a paragraph's space before after only its line spacing; PowerPoint reports a file
|
|
115
|
+
* with either out of place as needing repair.
|
|
116
|
+
*/
|
|
117
|
+
function insertAfterLeading(parent, child, leadingTag) {
|
|
118
|
+
const leading = childrenByTag(parent, leadingTag)[0];
|
|
119
|
+
parent.insertBefore(child, leading ? leading.nextSibling : parent.firstChild);
|
|
120
|
+
}
|
|
109
121
|
export function detach(node) {
|
|
110
122
|
if (node?.parentNode)
|
|
111
123
|
node.parentNode.removeChild(node);
|
|
@@ -225,7 +237,7 @@ export function setRichRuns(para, runs, relation) {
|
|
|
225
237
|
const srgbClr = para.ownerDocument.createElement(Tag.SRGB_CLR);
|
|
226
238
|
srgbClr.setAttribute(Attr.VALUE, run.color);
|
|
227
239
|
solidFill.appendChild(srgbClr);
|
|
228
|
-
rPr.
|
|
240
|
+
insertAfterLeading(rPr, solidFill, Tag.OUTLINE);
|
|
229
241
|
}
|
|
230
242
|
if (run.link && relation) {
|
|
231
243
|
let rPr = clone.getElementsByTagName(Tag.RUN_PROPS)[0];
|
|
@@ -266,9 +278,27 @@ function paragraphLevel(paragraph) {
|
|
|
266
278
|
const lvl = ppr?.getAttribute(Attr.LEVEL);
|
|
267
279
|
return lvl ? Number(lvl) || 0 : 0;
|
|
268
280
|
}
|
|
281
|
+
/** A paragraph's `<a:spcBef>`, or null when it has none. */
|
|
282
|
+
function spaceBefore(paragraph) {
|
|
283
|
+
return paragraph.getElementsByTagName(Tag.PARA_PROPS)[0]?.getElementsByTagName(Tag.SPACE_BEFORE)[0] ?? null;
|
|
284
|
+
}
|
|
285
|
+
/** Give the paragraph a copy of `spacing` as its space before, or none when null. */
|
|
286
|
+
function setSpaceBefore(paragraph, spacing) {
|
|
287
|
+
const ppr = paragraph.getElementsByTagName(Tag.PARA_PROPS)[0];
|
|
288
|
+
if (!ppr)
|
|
289
|
+
return;
|
|
290
|
+
for (const old of childrenByTag(ppr, Tag.SPACE_BEFORE))
|
|
291
|
+
ppr.removeChild(old);
|
|
292
|
+
if (spacing)
|
|
293
|
+
insertAfterLeading(ppr, spacing.cloneNode(true), Tag.LINE_SPACING);
|
|
294
|
+
}
|
|
269
295
|
function harvestStyles(specimen) {
|
|
270
296
|
const bullets = new Map();
|
|
271
297
|
let paras = null;
|
|
298
|
+
// Level → the space before a bullet that follows another, taken from the
|
|
299
|
+
// specimen's second bullet at that level. The first often carries extra space
|
|
300
|
+
// to set the list off from the text above it.
|
|
301
|
+
const followOn = new Map();
|
|
272
302
|
for (const p of specimen) {
|
|
273
303
|
const kind = paragraphBulletKind(p);
|
|
274
304
|
const lvl = paragraphLevel(p);
|
|
@@ -280,13 +310,15 @@ function harvestStyles(specimen) {
|
|
|
280
310
|
if (kind === "bullet") {
|
|
281
311
|
if (!bullets.has(lvl))
|
|
282
312
|
bullets.set(lvl, bucket);
|
|
313
|
+
else if (!followOn.has(lvl))
|
|
314
|
+
followOn.set(lvl, spaceBefore(p));
|
|
283
315
|
}
|
|
284
316
|
else {
|
|
285
317
|
if (paras == null)
|
|
286
318
|
paras = bucket;
|
|
287
319
|
}
|
|
288
320
|
}
|
|
289
|
-
return { bullets, paras };
|
|
321
|
+
return { bullets, paras, followOn };
|
|
290
322
|
}
|
|
291
323
|
function specimenBulletPara(specimen, level) {
|
|
292
324
|
for (const p of specimen) {
|
|
@@ -317,7 +349,7 @@ export function rebuildParagraphs(shape, paragraphs, startIndex, relation, shape
|
|
|
317
349
|
if (specimen.length === 0) {
|
|
318
350
|
throw new Error(`Shape "${shapeName}": startAt ${startIndex} is past the last paragraph (shape has ${allParas.length}).`);
|
|
319
351
|
}
|
|
320
|
-
const { bullets, paras } = harvestStyles(specimen);
|
|
352
|
+
const { bullets, paras, followOn } = harvestStyles(specimen);
|
|
321
353
|
const maxBulletLevel = bullets.size > 0 ? Math.max(...bullets.keys()) : -1;
|
|
322
354
|
const pickBullet = (lvl) => {
|
|
323
355
|
if (bullets.has(lvl))
|
|
@@ -329,18 +361,7 @@ export function rebuildParagraphs(shape, paragraphs, startIndex, relation, shape
|
|
|
329
361
|
const plainBucket = paras ?? bullets.get(0) ?? null;
|
|
330
362
|
const firstBulletKey = bullets.has(0) ? 0 : maxBulletLevel;
|
|
331
363
|
const firstBullet = firstBulletKey >= 0 ? specimenBulletPara(specimen, firstBulletKey) : null;
|
|
332
|
-
const transitionSpcBef = firstBullet
|
|
333
|
-
const applyTransitionSpacing = (paragraph) => {
|
|
334
|
-
if (!transitionSpcBef)
|
|
335
|
-
return;
|
|
336
|
-
const ppr = paragraph.getElementsByTagName(Tag.PARA_PROPS)[0];
|
|
337
|
-
if (!ppr)
|
|
338
|
-
return;
|
|
339
|
-
const old = ppr.getElementsByTagName(Tag.SPACE_BEFORE)[0];
|
|
340
|
-
if (old)
|
|
341
|
-
ppr.removeChild(old);
|
|
342
|
-
ppr.appendChild(transitionSpcBef.cloneNode(true));
|
|
343
|
-
};
|
|
364
|
+
const transitionSpcBef = firstBullet ? spaceBefore(firstBullet) : null;
|
|
344
365
|
for (const p of specimen)
|
|
345
366
|
detach(p);
|
|
346
367
|
const doc = shape.ownerDocument;
|
|
@@ -362,8 +383,10 @@ export function rebuildParagraphs(shape, paragraphs, startIndex, relation, shape
|
|
|
362
383
|
const seedRun = buildRun(doc, bucket?.rPr ?? null, "");
|
|
363
384
|
const newPara = buildParagraph(doc, bucket?.pPr ?? null, seedRun);
|
|
364
385
|
maybeOverrideLevel(newPara, isBullet ? effectiveLvl : null);
|
|
365
|
-
if (!isBullet && prevWasBullet)
|
|
366
|
-
|
|
386
|
+
if (!isBullet && prevWasBullet && transitionSpcBef)
|
|
387
|
+
setSpaceBefore(newPara, transitionSpcBef);
|
|
388
|
+
if (isBullet && prevWasBullet && followOn.has(effectiveLvl))
|
|
389
|
+
setSpaceBefore(newPara, followOn.get(effectiveLvl));
|
|
367
390
|
setRichRuns(newPara, para.runs, relation);
|
|
368
391
|
txBody.appendChild(newPara);
|
|
369
392
|
prevWasBullet = isBullet;
|
|
@@ -15,14 +15,15 @@
|
|
|
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 { type BodyRows, SlotType } from "../types.js";
|
|
18
|
+
import { type BodyRows, type ImageFit, SlotType } from "../types.js";
|
|
19
19
|
/**
|
|
20
20
|
* The shape a filler targets — a union discriminated by `type` (mirroring the
|
|
21
21
|
* `Block` variants), so each filler's callback sees only its own specimen
|
|
22
22
|
* options. Every target carries a `shapeName` and a human `label` for
|
|
23
23
|
* diagnostics (the author-facing "slide N, layout …, slot …" a filler prints in
|
|
24
24
|
* advisory warnings instead of the raw PPTX shape id); a text target may carry
|
|
25
|
-
* `startAt`,
|
|
25
|
+
* `startAt`, a table target carries its required `bodyRows` range, and an image
|
|
26
|
+
* target its required `fit`.
|
|
26
27
|
*/
|
|
27
28
|
export type TemplateFillTarget = {
|
|
28
29
|
type: typeof SlotType.Template;
|
|
@@ -45,6 +46,7 @@ export type ImageFillTarget = {
|
|
|
45
46
|
type: typeof SlotType.Image;
|
|
46
47
|
shapeName: string;
|
|
47
48
|
label: string;
|
|
49
|
+
fit: ImageFit;
|
|
48
50
|
};
|
|
49
51
|
export type FillTarget = TemplateFillTarget | TextFillTarget | TableFillTarget | ImageFillTarget;
|
|
50
52
|
/** A pptx-automizer element-modify callback: `(element, relation) => void`. */
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { basename } from "node:path";
|
|
19
19
|
import { ModifyImageHelper } from "pptx-automizer";
|
|
20
|
-
import { SlotType } from "../types.js";
|
|
20
|
+
import { SlotType, } from "../types.js";
|
|
21
21
|
import { fillImage, isImageFill } from "./image.js";
|
|
22
22
|
import { fillTable, isTableFill } from "./table.js";
|
|
23
23
|
import { fillTemplate, isTemplateFill } from "./template.js";
|
|
@@ -44,7 +44,7 @@ const imageFiller = {
|
|
|
44
44
|
matches: isImageFill,
|
|
45
45
|
callbacks: (v, t) => [
|
|
46
46
|
ModifyImageHelper.setRelationTarget(basename(v.path)),
|
|
47
|
-
(el) => fillImage(el, v, t.shapeName, t.label),
|
|
47
|
+
(el) => fillImage(el, v, t.shapeName, t.label, t.fit),
|
|
48
48
|
],
|
|
49
49
|
};
|
|
50
50
|
export const FILLERS = {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Image fill — element-level picture geometry and alt text. The media swap
|
|
3
3
|
* (pointing the blip relationship at the new file) is a slide-level modifier in
|
|
4
4
|
* the ImageFiller (`fillers/filler.ts`); this module resolves the frame geometry
|
|
5
|
-
* from the
|
|
5
|
+
* from the slot's `ImageFit` (contain/cover/scale-down) and the source's true
|
|
6
6
|
* pixel size, and rewrites the picture's alt text for the new image.
|
|
7
7
|
*/
|
|
8
8
|
import { type ImageFill, ImageFit } from "../types.js";
|
|
@@ -35,7 +35,7 @@ export type GeometryResult = {
|
|
|
35
35
|
warnings: string[];
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
* Size a picture shape
|
|
38
|
+
* Size a picture shape by its slot's `fit` (via `computeGeometry`): either write
|
|
39
39
|
* `<a:srcRect>` insets to fill-and-crop, or shrink the frame to the image's
|
|
40
40
|
* aspect ratio and re-center (fit/letterbox). Advisory warnings from the
|
|
41
41
|
* geometry pass go to `console.warn`. Also sets its alt text (`setAltText`).
|
|
@@ -43,7 +43,7 @@ export type GeometryResult = {
|
|
|
43
43
|
* `image.path` is assumed absolute — the compiler / caller resolves it before the
|
|
44
44
|
* ImageFill reaches the engine.
|
|
45
45
|
*/
|
|
46
|
-
export declare function fillImage(shape: any, image: ImageFill, shapeName: string, label: string): void;
|
|
46
|
+
export declare function fillImage(shape: any, image: ImageFill, shapeName: string, label: string, fit: ImageFit): void;
|
|
47
47
|
/**
|
|
48
48
|
* Pure fit geometry — no DOM, so it is trivially unit-testable in isolation.
|
|
49
49
|
* `fit` picks the strategy: `cover` scales to the larger axis ratio and
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Image fill — element-level picture geometry and alt text. The media swap
|
|
3
3
|
* (pointing the blip relationship at the new file) is a slide-level modifier in
|
|
4
4
|
* the ImageFiller (`fillers/filler.ts`); this module resolves the frame geometry
|
|
5
|
-
* from the
|
|
5
|
+
* from the slot's `ImageFit` (contain/cover/scale-down) and the source's true
|
|
6
6
|
* pixel size, and rewrites the picture's alt text for the new image.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
@@ -23,7 +23,7 @@ const SEVERE_MISMATCH_FRACTION = 0.5;
|
|
|
23
23
|
/** Warn once the image renders below this fraction of its native pixel size. */
|
|
24
24
|
const MIN_SCALE = 0.2;
|
|
25
25
|
/**
|
|
26
|
-
* Size a picture shape
|
|
26
|
+
* Size a picture shape by its slot's `fit` (via `computeGeometry`): either write
|
|
27
27
|
* `<a:srcRect>` insets to fill-and-crop, or shrink the frame to the image's
|
|
28
28
|
* aspect ratio and re-center (fit/letterbox). Advisory warnings from the
|
|
29
29
|
* geometry pass go to `console.warn`. Also sets its alt text (`setAltText`).
|
|
@@ -31,7 +31,7 @@ const MIN_SCALE = 0.2;
|
|
|
31
31
|
* `image.path` is assumed absolute — the compiler / caller resolves it before the
|
|
32
32
|
* ImageFill reaches the engine.
|
|
33
33
|
*/
|
|
34
|
-
export function fillImage(shape, image, shapeName, label) {
|
|
34
|
+
export function fillImage(shape, image, shapeName, label, fit) {
|
|
35
35
|
const dims = imageSize(new Uint8Array(readFileSync(image.path)));
|
|
36
36
|
if (!dims.width || !dims.height) {
|
|
37
37
|
throw new Error(`Image shape "${shapeName}": could not read image dimensions from "${image.path}".`);
|
|
@@ -50,7 +50,7 @@ export function fillImage(shape, image, shapeName, label) {
|
|
|
50
50
|
w: Number(ext.getAttribute(Attr.CX)),
|
|
51
51
|
h: Number(ext.getAttribute(Attr.CY)),
|
|
52
52
|
};
|
|
53
|
-
const { geometry, warnings } = computeGeometry(frame, dims.width, dims.height,
|
|
53
|
+
const { geometry, warnings } = computeGeometry(frame, dims.width, dims.height, fit);
|
|
54
54
|
for (const w of warnings)
|
|
55
55
|
console.warn(`Image ${label}: ${w}`);
|
|
56
56
|
if (geometry.placement === Placement.Crop) {
|
package/dist/engine/generate.js
CHANGED
|
@@ -272,7 +272,8 @@ function resolveBlock(step, slot, value) {
|
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
274
274
|
* The shape a filler targets, plus its diagnostic `label` and the intra-specimen
|
|
275
|
-
* options a block declares: `startAt` (text)
|
|
275
|
+
* options a block declares: `startAt` (text), the required `bodyRows` (table) and
|
|
276
|
+
* the required `fit` (image).
|
|
276
277
|
* The block's `type` selects the matching target variant, so each filler's
|
|
277
278
|
* callback sees only its own options.
|
|
278
279
|
*/
|
|
@@ -287,7 +288,7 @@ function targetOf(block, label) {
|
|
|
287
288
|
case SlotType.Table:
|
|
288
289
|
return { type: SlotType.Table, shapeName: block.shapeName, label, bodyRows: block.bodyRows };
|
|
289
290
|
case SlotType.Image:
|
|
290
|
-
return { type: SlotType.Image, shapeName: block.shapeName, label };
|
|
291
|
+
return { type: SlotType.Image, shapeName: block.shapeName, label, fit: block.fit };
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
/**
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -99,17 +99,16 @@ export declare const ImageFit: {
|
|
|
99
99
|
};
|
|
100
100
|
export type ImageFit = (typeof ImageFit)[keyof typeof ImageFit];
|
|
101
101
|
/**
|
|
102
|
-
* Input to fillImage — a resolved image path
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* `alt` is the picture's accessibility description (`descr` on its
|
|
102
|
+
* Input to fillImage — a resolved image path and its alt text. How the image
|
|
103
|
+
* fills its shape is the slot's, from its image block's `fit`.
|
|
104
|
+
* `path` must be absolute; the compiler resolves it before the ImageFill reaches
|
|
105
|
+
* the engine. `alt` is the picture's accessibility description (`descr` on its
|
|
106
106
|
* `<p:cNvPr>`); empty means none, and clears any description the placeholder
|
|
107
107
|
* carried.
|
|
108
108
|
*/
|
|
109
109
|
export type ImageFill = {
|
|
110
110
|
type: typeof SlotType.Image;
|
|
111
111
|
path: string;
|
|
112
|
-
fit: ImageFit;
|
|
113
112
|
alt: string;
|
|
114
113
|
};
|
|
115
114
|
/** A shape's absolute position and size, in EMU — the slot's frame. */
|
|
@@ -128,7 +127,7 @@ export type Frame = {
|
|
|
128
127
|
* otherwise it is transplanted from `sourceSlide` into the slot's frame. A text
|
|
129
128
|
* block may pin `startAt` (leave the first N specimen paragraphs untouched); a
|
|
130
129
|
* table block MUST declare its `bodyRows` range (the repeatable specimen rows);
|
|
131
|
-
* a template
|
|
130
|
+
* an image block MUST declare its `fit`; a template block carries none of them. `Template` reaches the engine only
|
|
132
131
|
* via a frontmatter parameter (`paramToEngineSlot`), never as an author body block.
|
|
133
132
|
*
|
|
134
133
|
* Named `Block` — a kind of content the way an author thinks of it. Distinct
|
|
@@ -156,6 +155,7 @@ export type ImageBlock = {
|
|
|
156
155
|
type: typeof SlotType.Image;
|
|
157
156
|
sourceSlide: number;
|
|
158
157
|
shapeName: string;
|
|
158
|
+
fit: ImageFit;
|
|
159
159
|
};
|
|
160
160
|
export type Block = TemplateBlock | TextBlock | TableBlock | ImageBlock;
|
|
161
161
|
/**
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function compilerBlockToEngineBlock(b) {
|
|
|
34
34
|
case AcceptType.Table:
|
|
35
35
|
return { type: SlotType.Table, sourceSlide: b.sourceSlide, shapeName: b.shapeName, bodyRows: b.bodyRows };
|
|
36
36
|
case AcceptType.Image:
|
|
37
|
-
return { type: SlotType.Image, sourceSlide: b.sourceSlide, shapeName: b.shapeName };
|
|
37
|
+
return { type: SlotType.Image, sourceSlide: b.sourceSlide, shapeName: b.shapeName, fit: b.fit };
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
function slotToEngineSlot(slot) {
|
|
@@ -1,25 +1,2 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
1
|
import { type BlockHandler } from "../types.js";
|
|
3
2
|
export declare const IMAGE: BlockHandler;
|
|
4
|
-
/**
|
|
5
|
-
* Image options, carried in a markdown image's title: ``.
|
|
6
|
-
* The title is a YAML mapping validated by this strict schema, so an unknown key
|
|
7
|
-
* throws naming the valid ones, and adding an option is one line here.
|
|
8
|
-
*/
|
|
9
|
-
declare const imageOptionsSchema: z.ZodObject<{
|
|
10
|
-
fit: z.ZodOptional<z.ZodEnum<{
|
|
11
|
-
readonly Contain: "contain";
|
|
12
|
-
readonly Cover: "cover";
|
|
13
|
-
readonly ScaleDown: "scale-down";
|
|
14
|
-
}>>;
|
|
15
|
-
}, z.core.$strict>;
|
|
16
|
-
export type ImageOptions = z.infer<typeof imageOptionsSchema>;
|
|
17
|
-
/**
|
|
18
|
-
* Parse an image title into its options. A missing or blank title means no
|
|
19
|
-
* options. Anything else must be a YAML mapping of known options, or the build
|
|
20
|
-
* fails with `where` (the slide, layout and slot) and a hint for the common
|
|
21
|
-
* mistakes: a caption written in the title, a missing space after the colon, and
|
|
22
|
-
* several options without flow braces.
|
|
23
|
-
*/
|
|
24
|
-
export declare function parseImageTitle(title: string | null | undefined, where: string): ImageOptions;
|
|
25
|
-
export {};
|
|
@@ -1,71 +1,19 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import * as z from "zod";
|
|
5
|
-
import { ImageFit, SlotType } from "../../engine/index.js";
|
|
3
|
+
import { SlotType } from "../../engine/index.js";
|
|
6
4
|
import { MdastType } from "../mdast.js";
|
|
7
|
-
import { strict } from "../schema/strict.js";
|
|
8
|
-
import { isYamlMapping } from "../slideParser.js";
|
|
9
5
|
import { AcceptType } from "../types.js";
|
|
10
|
-
/** The fit when the title gives none: the whole picture, never cropped. */
|
|
11
|
-
const DEFAULT_FIT = ImageFit.Contain;
|
|
12
6
|
export const IMAGE = {
|
|
13
7
|
match: (node) => node.type === MdastType.Image,
|
|
14
8
|
acceptType: AcceptType.Image,
|
|
15
9
|
// `url` is a path relative to the deck. `alt` is the picture's alt text; the
|
|
16
10
|
// parser flattens it to plain text, and an image written without any is "".
|
|
17
|
-
// `title` carries the image's options.
|
|
18
11
|
compile: async (node, ctx) => {
|
|
19
|
-
const { url, alt
|
|
20
|
-
const options = parseImageTitle(title, ctx.region);
|
|
12
|
+
const { url, alt } = node;
|
|
21
13
|
const path = resolve(ctx.config.deckDir, url);
|
|
22
14
|
if (!existsSync(path)) {
|
|
23
15
|
throw new Error(`${ctx.region}: image "${url}" not found at ${path}`);
|
|
24
16
|
}
|
|
25
|
-
return { type: SlotType.Image, path,
|
|
17
|
+
return { type: SlotType.Image, path, alt: alt ?? "" };
|
|
26
18
|
},
|
|
27
19
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Image options, carried in a markdown image's title: ``.
|
|
30
|
-
* The title is a YAML mapping validated by this strict schema, so an unknown key
|
|
31
|
-
* throws naming the valid ones, and adding an option is one line here.
|
|
32
|
-
*/
|
|
33
|
-
const imageOptionsSchema = strict({
|
|
34
|
-
fit: z.enum(ImageFit).optional(),
|
|
35
|
-
});
|
|
36
|
-
/** The one-option title every hint quotes, so an author sees the expected form. */
|
|
37
|
-
const EXAMPLE_TITLE = `"fit: ${ImageFit.Contain}"`;
|
|
38
|
-
/** A colon run into the next character, as in `fit:contain`: YAML reads that as one string. */
|
|
39
|
-
const COLON_WITHOUT_SPACE_RE = /:\S/;
|
|
40
|
-
/**
|
|
41
|
-
* Parse an image title into its options. A missing or blank title means no
|
|
42
|
-
* options. Anything else must be a YAML mapping of known options, or the build
|
|
43
|
-
* fails with `where` (the slide, layout and slot) and a hint for the common
|
|
44
|
-
* mistakes: a caption written in the title, a missing space after the colon, and
|
|
45
|
-
* several options without flow braces.
|
|
46
|
-
*/
|
|
47
|
-
export function parseImageTitle(title, where) {
|
|
48
|
-
if (!title?.trim())
|
|
49
|
-
return {};
|
|
50
|
-
const prefix = `${where}: image title "${title}"`;
|
|
51
|
-
let parsed;
|
|
52
|
-
try {
|
|
53
|
-
parsed = parseYaml(title);
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
// The yaml message's first line names the problem; the rest is a code frame.
|
|
57
|
-
const reason = err.message.split("\n")[0].replace(/:$/, "");
|
|
58
|
-
throw new Error(`${prefix} is not valid YAML (${reason}). Several options go in braces: "{key: value, key: value}".`);
|
|
59
|
-
}
|
|
60
|
-
if (!isYamlMapping(parsed)) {
|
|
61
|
-
const hint = typeof parsed === "string" && COLON_WITHOUT_SPACE_RE.test(parsed)
|
|
62
|
-
? `put a space after the colon, as in ${EXAMPLE_TITLE}`
|
|
63
|
-
: `the title holds options like ${EXAMPLE_TITLE}; put a description in the alt text instead: `;
|
|
64
|
-
throw new Error(`${prefix} is not a set of options: ${hint}.`);
|
|
65
|
-
}
|
|
66
|
-
const result = imageOptionsSchema.safeParse(parsed);
|
|
67
|
-
if (!result.success) {
|
|
68
|
-
throw new Error(`${prefix}: ${z.prettifyError(result.error)}`);
|
|
69
|
-
}
|
|
70
|
-
return result.data;
|
|
71
|
-
}
|
|
@@ -7,8 +7,8 @@ export declare const MERMAID_LANG = "mermaid";
|
|
|
7
7
|
* Recognize a ```mermaid fenced block at a region's top level, folding it to an
|
|
8
8
|
* Image fill, and compile it by rendering the definition to a PNG (cached under
|
|
9
9
|
* `<rootDir>/.tycoslide-cache/mermaid/<hash>.png`, in the theme directory) and wrapping it as an
|
|
10
|
-
* ImageFill
|
|
11
|
-
*
|
|
10
|
+
* ImageFill, which fills the slot by the slot's fit like any image. A fence
|
|
11
|
+
* carries no alt text. Resolution is strict: the theme MUST carry a `mermaid` block, MUST
|
|
12
12
|
* declare a `mermaidVariant`, and that variant MUST exist — each missing piece
|
|
13
13
|
* throws by name.
|
|
14
14
|
*/
|
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { extname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { SlotType } from "../../engine/index.js";
|
|
7
7
|
import { MdastType } from "../mdast.js";
|
|
8
8
|
import { AcceptType } from "../types.js";
|
|
9
9
|
import { buildMermaidRenderConfig, injectClassDefs, validateMermaidDefinition, } from "./mermaidTheme.js";
|
|
@@ -15,8 +15,8 @@ export const MERMAID_LANG = "mermaid";
|
|
|
15
15
|
* Recognize a ```mermaid fenced block at a region's top level, folding it to an
|
|
16
16
|
* Image fill, and compile it by rendering the definition to a PNG (cached under
|
|
17
17
|
* `<rootDir>/.tycoslide-cache/mermaid/<hash>.png`, in the theme directory) and wrapping it as an
|
|
18
|
-
* ImageFill
|
|
19
|
-
*
|
|
18
|
+
* ImageFill, which fills the slot by the slot's fit like any image. A fence
|
|
19
|
+
* carries no alt text. Resolution is strict: the theme MUST carry a `mermaid` block, MUST
|
|
20
20
|
* declare a `mermaidVariant`, and that variant MUST exist — each missing piece
|
|
21
21
|
* throws by name.
|
|
22
22
|
*/
|
|
@@ -42,7 +42,7 @@ export const MERMAID = {
|
|
|
42
42
|
}
|
|
43
43
|
const cacheDir = ensureCacheDir(config);
|
|
44
44
|
const pngPath = await renderOne(definition, variantName, variant, cacheDir, config);
|
|
45
|
-
return { type: SlotType.Image, path: pngPath,
|
|
45
|
+
return { type: SlotType.Image, path: pngPath, alt: "" };
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
48
|
const FONT_FORMATS = {
|
|
@@ -187,12 +187,12 @@ export async function launchChromium(chromium, browserPath) {
|
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
189
|
* Render a mermaid definition to a transparent PNG via a headless Chromium
|
|
190
|
-
* (Playwright
|
|
190
|
+
* (Playwright, for its headless font fidelity). The
|
|
191
191
|
* theme fonts are injected as `@font-face` and every registered face is awaited
|
|
192
192
|
* (`document.fonts.load()`) BEFORE `mermaid.render()` measures text — the only
|
|
193
193
|
* cross-platform, zero-install way to make Chromium lay out labels in the brand
|
|
194
|
-
* font instead of a substitute.
|
|
195
|
-
* to affect metrics
|
|
194
|
+
* font instead of a substitute. A stylesheet injected after layout comes too
|
|
195
|
+
* late to affect the metrics.
|
|
196
196
|
*/
|
|
197
197
|
async function renderMermaidToPng(processed, renderConfig, fonts, outputPath, browserPath) {
|
|
198
198
|
const bundle = getMermaidBundle();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Owner of MermaidVariant / MermaidConfig — these types are compiler-facing
|
|
5
5
|
* (the engine has no idea mermaid exists). `blocks/mermaid.ts` consumes
|
|
6
|
-
* them to build
|
|
6
|
+
* them to build the render config.
|
|
7
7
|
*/
|
|
8
8
|
export type MermaidVariant = {
|
|
9
9
|
primary: string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Owner of MermaidVariant / MermaidConfig — these types are compiler-facing
|
|
5
5
|
* (the engine has no idea mermaid exists). `blocks/mermaid.ts` consumes
|
|
6
|
-
* them to build
|
|
6
|
+
* them to build the render config.
|
|
7
7
|
*/
|
|
8
8
|
const FORBIDDEN_PATTERNS = [/^\s*style\s+\S+\s+/, /^\s*linkStyle\s+/, /^\s*classDef\s+/, /^\s*%%\{init/];
|
|
9
9
|
export function validateMermaidDefinition(definition) {
|
|
@@ -88,7 +88,7 @@ export function buildMermaidRenderConfig(variant) {
|
|
|
88
88
|
tertiaryColor: variant.surface,
|
|
89
89
|
textColor: variant.text,
|
|
90
90
|
titleColor: variant.text,
|
|
91
|
-
nodeTextColor: variant.
|
|
91
|
+
nodeTextColor: variant.primaryContrast,
|
|
92
92
|
clusterBkg: variant.surface,
|
|
93
93
|
clusterBorder: variant.surfaceBorder,
|
|
94
94
|
edgeLabelBackground: variant.surface,
|
|
@@ -30,6 +30,11 @@ export declare const ThemeConfigSchema: z.ZodObject<{
|
|
|
30
30
|
type: z.ZodLiteral<"image">;
|
|
31
31
|
sourceSlide: z.ZodNumber;
|
|
32
32
|
shapeName: z.ZodString;
|
|
33
|
+
fit: z.ZodEnum<{
|
|
34
|
+
readonly Contain: "contain";
|
|
35
|
+
readonly Cover: "cover";
|
|
36
|
+
readonly ScaleDown: "scale-down";
|
|
37
|
+
}>;
|
|
33
38
|
}, z.core.$strict>], "type">>;
|
|
34
39
|
frame: z.ZodOptional<z.ZodObject<{
|
|
35
40
|
x: z.ZodNumber;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname } from "node:path";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
-
import { AcceptType, Variant } from "../types.js";
|
|
4
|
+
import { AcceptType, ImageFit, Variant } from "../types.js";
|
|
5
5
|
import { strict } from "./strict.js";
|
|
6
6
|
/**
|
|
7
7
|
* Fail-fast runtime validation for a `theme.json`. The hand-written types in
|
|
@@ -60,9 +60,10 @@ const ParameterSchema = strict({
|
|
|
60
60
|
template: z.string(),
|
|
61
61
|
});
|
|
62
62
|
// One arm per accept type, discriminated by `type`, mirroring `CompilerBlock`:
|
|
63
|
-
// `startAt` lives only on the text arm,
|
|
64
|
-
// table block MUST declare its repeatable-row range
|
|
65
|
-
//
|
|
63
|
+
// `startAt` lives only on the text arm, `bodyRows` only on the table arm and `fit`
|
|
64
|
+
// only on the image arm (a table block MUST declare its repeatable-row range and
|
|
65
|
+
// an image block its fit; an option on another arm is an unknown key the `strict`
|
|
66
|
+
// arm rejects).
|
|
66
67
|
const TextBlockSchema = strict({
|
|
67
68
|
type: z.literal(AcceptType.Text),
|
|
68
69
|
sourceSlide: z.number(),
|
|
@@ -79,6 +80,7 @@ const ImageBlockSchema = strict({
|
|
|
79
80
|
type: z.literal(AcceptType.Image),
|
|
80
81
|
sourceSlide: z.number(),
|
|
81
82
|
shapeName: z.string(),
|
|
83
|
+
fit: z.enum(ImageFit),
|
|
82
84
|
});
|
|
83
85
|
const BlockSchema = z.discriminatedUnion("type", [TextBlockSchema, TableBlockSchema, ImageBlockSchema]);
|
|
84
86
|
const SlotSchema = strict({
|
|
@@ -8,8 +8,6 @@ export interface ParsedDocument {
|
|
|
8
8
|
slides: RawSlide[];
|
|
9
9
|
}
|
|
10
10
|
export declare function parseSlideDocument(source: string): ParsedDocument;
|
|
11
|
-
/** True for a parsed YAML document that is a mapping (`key: value` pairs). */
|
|
12
|
-
export declare function isYamlMapping(parsed: unknown): parsed is Record<string, unknown>;
|
|
13
11
|
export declare class FrontmatterParseError extends Error {
|
|
14
12
|
constructor(slideIndex: number, yamlSource: string, cause: unknown);
|
|
15
13
|
}
|
|
@@ -16,7 +16,7 @@ export function parseSlideDocument(source) {
|
|
|
16
16
|
// ============================================
|
|
17
17
|
const GLOBAL_FM_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
18
18
|
/** True for a parsed YAML document that is a mapping (`key: value` pairs). */
|
|
19
|
-
|
|
19
|
+
function isYamlMapping(parsed) {
|
|
20
20
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
|
|
21
21
|
}
|
|
22
22
|
function extractGlobalFrontmatter(source) {
|
package/dist/markdown/types.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RootContent } from "mdast";
|
|
2
|
-
import { type BodyRows, type Frame, type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
|
|
2
|
+
import { type BodyRows, type Frame, type ImageFill, ImageFit, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
|
|
3
3
|
import type { MermaidConfig } from "./blocks/mermaidTheme.js";
|
|
4
4
|
export type { BodyRows };
|
|
5
|
+
export { ImageFit };
|
|
5
6
|
/**
|
|
6
7
|
* The engine content types a slot may `accept`. A subset of the engine's
|
|
7
8
|
* `SlotType`: `Template` is parameter-only, never a body block. There is no
|
|
@@ -146,9 +147,10 @@ export declare const RESERVED_KEY: {
|
|
|
146
147
|
* place), otherwise it is transplanted into the slot's `frame`. A text block may
|
|
147
148
|
* pin `startAt` (leave the first N specimen paragraphs untouched); a table block
|
|
148
149
|
* MUST declare its `bodyRows` range (the repeatable `<a:tbl>` specimen rows); an
|
|
149
|
-
* image block
|
|
150
|
-
*
|
|
151
|
-
*
|
|
150
|
+
* image block MUST declare its `fit`, which sets how every image placed in it
|
|
151
|
+
* fills the shape. There is no `Template` variant — `AcceptType` excludes it (a
|
|
152
|
+
* parameter, not a body block). Named `CompilerBlock` to stay distinct from the
|
|
153
|
+
* engine's `Block` and the compiler's `BlockHandler`.
|
|
152
154
|
*/
|
|
153
155
|
export type CompilerTextBlock = {
|
|
154
156
|
type: typeof AcceptType.Text;
|
|
@@ -166,6 +168,7 @@ export type CompilerImageBlock = {
|
|
|
166
168
|
type: typeof AcceptType.Image;
|
|
167
169
|
sourceSlide: number;
|
|
168
170
|
shapeName: string;
|
|
171
|
+
fit: ImageFit;
|
|
169
172
|
};
|
|
170
173
|
export type CompilerBlock = CompilerTextBlock | CompilerTableBlock | CompilerImageBlock;
|
|
171
174
|
/**
|
package/dist/markdown/types.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { SlotType, } from "../engine/index.js";
|
|
1
|
+
import { ImageFit, SlotType, } from "../engine/index.js";
|
|
2
|
+
export { ImageFit };
|
|
2
3
|
// ── AcceptType discriminator (what a slot accepts) ────────────────────────────
|
|
3
4
|
/**
|
|
4
5
|
* The engine content types a slot may `accept`. A subset of the engine's
|
package/docs/markdown.md
CHANGED
|
@@ -144,23 +144,23 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
|
|
|
144
144
|
````
|
|
145
145
|
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.
|
|
146
146
|
- **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).
|
|
147
|
-
- **image** (slots that accept `image`) -- an image, written as ` -- an image, written as `` with the path relative to the deck:
|
|
148
148
|
```markdown
|
|
149
149
|
::logo::
|
|
150
150
|
|
|
151
|
-

|
|
152
152
|
```
|
|
153
|
-
The alt text becomes the image's alt text in PowerPoint; leave it empty for an image that is only decoration. The
|
|
153
|
+
The alt text becomes the image's alt text in PowerPoint; leave it empty for an image that is only decoration. The slot's `fit`, set in the theme, decides how the image fills its shape. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
157
|
## Mermaid diagrams
|
|
158
158
|
|
|
159
|
-
Write a fenced code block with the `mermaid` language tag in a named slot that accepts `image`. It renders to a themed PNG and
|
|
159
|
+
Write a fenced code block with the `mermaid` language tag in a named slot that accepts `image`. It renders to a themed PNG and fills the slot like any other image, by the slot's `fit`. The color variant comes from the theme's `mermaidVariant` (set once in `theme.json`, not per slot).
|
|
160
160
|
|
|
161
161
|
````markdown
|
|
162
162
|
---
|
|
163
|
-
layout:
|
|
163
|
+
layout: Diagram
|
|
164
164
|
title: System Architecture
|
|
165
165
|
---
|
|
166
166
|
|
|
@@ -272,7 +272,6 @@ Common errors and fixes:
|
|
|
272
272
|
| A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
|
|
273
273
|
| YAML parse error | Fix the YAML syntax in the slide's frontmatter |
|
|
274
274
|
| `image "…" not found at …` | Check the path; it is relative to the deck file |
|
|
275
|
-
| `is not a set of options` | The image title holds options like `"fit: contain"`; put a description in the alt text |
|
|
276
275
|
| `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
|
|
277
276
|
| `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
|
|
278
277
|
| `no "mermaid" block` | The theme has no mermaid color config -- add a `mermaid` section to theme.json |
|
package/docs/theme.md
CHANGED
|
@@ -48,7 +48,7 @@ A slot is a body region. The author's markdown shape picks which accepted block
|
|
|
48
48
|
|
|
49
49
|
### Blocks inside `accepts`
|
|
50
50
|
|
|
51
|
-
Each block is discriminated by `type` and carries only its own option. A `bodyRows` on a text block
|
|
51
|
+
Each block is discriminated by `type` and carries only its own option. A `bodyRows` on a text block, a `startAt` on a table block or a `fit` on either is an unknown key and fails the load.
|
|
52
52
|
|
|
53
53
|
| Field | Meaning | Error when wrong |
|
|
54
54
|
|---|---|---|
|
|
@@ -57,8 +57,9 @@ Each block is discriminated by `type` and carries only its own option. A `bodyRo
|
|
|
57
57
|
| `shapeName` | Exact name of the specimen shape on `sourceSlide`. Required. | `Can't find element on slide N in source:` and the placeholder remains. |
|
|
58
58
|
| `startAt` | Text only. Leave the first N specimen paragraphs untouched and rebuild from paragraph N. Optional, default 0. | `startAt 3 is past the last paragraph (shape has 2)`. |
|
|
59
59
|
| `bodyRows` | Table only. `[start, end]`, 0-based inclusive: the rows that repeat for data. Required. Row 0 is always the header. Rows above `start` and below `end` render once, as fixed rows. | `bodyRows [s, e] is out of range; require 1 <= start <= end <= R-1 (row 0 is the header)`; `has no <a:tbl> element (is it actually a table?)`. |
|
|
60
|
+
| `fit` | Image only. Sets how every image placed in the shape fills it. `"contain"` shows the whole image and never crops, `"cover"` fills the shape and center-crops, and `"scale-down"` shows the whole image and never enlarges it. Required. | Schema error listing the valid values. |
|
|
60
61
|
|
|
61
|
-
For an image block the shape must be
|
|
62
|
+
For an image block the shape must be an image shape: `is not a picture (missing <a:off>, <a:ext>, <p:blipFill>, or <p:cNvPr>)`.
|
|
62
63
|
|
|
63
64
|
Transplants: a block whose `sourceSlide` differs from the layout's `slideNumber` is copied from that other slide onto the cloned slide, positioned into the slot's `frame`. The shape the slot replaces on the base slide is removed. This is how one slot accepts both text and a table when the styled table lives on a different slide, as the worked example's `Content` layout does with a table from slide 15. The frame is the observed `<a:xfrm>` of the shape being replaced, so a text block in the same slot fills in place and the table lands exactly where the text was. The transplanted shape is positioned and sized to the frame, so its own size on the source slide does not matter.
|
|
64
65
|
|
|
@@ -69,7 +70,7 @@ Optional array of `@font-face` entries used only when rendering mermaid diagrams
|
|
|
69
70
|
| Field | Meaning | Error when wrong |
|
|
70
71
|
|---|---|---|
|
|
71
72
|
| `family` | CSS family name; must equal the mermaid variant's `fontFamily`. Required. | Warning `fontFamily "X" matches none of the declared theme fonts`, then a substitute font. |
|
|
72
|
-
| `path` | A package specifier such as `@fontsource/inter/files/inter-latin-400-normal.woff2`, or a `./` or `/` file path. Required. | `could not resolve "path" from <rootDir>`; `file not found at ...`; `unsupported format ".
|
|
73
|
+
| `path` | A package specifier such as `@fontsource/inter/files/inter-latin-400-normal.woff2`, or a `./` or `/` file path. Required. | `could not resolve "path" from <rootDir>`; `file not found at ...`; `unsupported format ".eot"`. Supported: woff2, woff, ttf, otf. |
|
|
73
74
|
| `weight` | OpenType weight. Optional, default 400. | None. |
|
|
74
75
|
|
|
75
76
|
## `codeTheme`
|
|
@@ -84,7 +85,7 @@ Optional record of named color variants. The record keys are free; every variant
|
|
|
84
85
|
|---|---|
|
|
85
86
|
| `primary` | Node fill color. |
|
|
86
87
|
| `primaryContrast` | Text color on `primary` nodes. |
|
|
87
|
-
| `text` | Default text
|
|
88
|
+
| `text` | Default text and title color, including labels outside nodes. |
|
|
88
89
|
| `line` | Edge and arrow color. |
|
|
89
90
|
| `surface` | Subgraph, secondary node and edge-label background. |
|
|
90
91
|
| `surfaceBorder` | Border color for nodes and subgraphs. |
|
|
@@ -120,7 +121,7 @@ Optional string naming a key of `mermaid`. Required as soon as any deck has a me
|
|
|
120
121
|
"accepts": [
|
|
121
122
|
{ "type": "text", "sourceSlide": 1, "shapeName": "Text 1" },
|
|
122
123
|
{ "type": "table", "sourceSlide": 2, "shapeName": "Table 0", "bodyRows": [1, 1] },
|
|
123
|
-
{ "type": "image", "sourceSlide": 3, "shapeName": "Image 0" }
|
|
124
|
+
{ "type": "image", "sourceSlide": 3, "shapeName": "Image 0", "fit": "contain" }
|
|
124
125
|
]
|
|
125
126
|
}
|
|
126
127
|
]
|
package/package.json
CHANGED
|
@@ -37,7 +37,7 @@ This skill builds decks from a markdown deck file. The theme provides slide layo
|
|
|
37
37
|
|
|
38
38
|
## Layout Discovery
|
|
39
39
|
|
|
40
|
-
Before writing anything, read `manifest.json`. It lists the theme's **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions). A layout is identified by its `name`; every parameter and slot by its `key`. Slots carry `accepts`, and either may be `required`.
|
|
40
|
+
Before writing anything, read `manifest.json`. It lists the theme's **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions). A layout is identified by its `name`; every parameter and slot by its `key`. Slots carry `accepts`, and either may be `required`. A slot that accepts an image also shows its `fit`. A `cover` slot crops the image to fill the slot, a `contain` slot shows the whole image, and a `scale-down` slot shows the whole image without ever enlarging it.
|
|
41
41
|
|
|
42
42
|
A layout's inputs split two ways, parameters and slots; see [markdown.md](node_modules/@tycoworks/tycoslide/docs/markdown.md#parameters-and-slots) for how to fill each. Unfilled ones are dropped, so fill as many of a layout's numbered slots (sections, stats, columns) as you have content for.
|
|
43
43
|
|
|
@@ -45,19 +45,19 @@ Study each layout's `slots` before writing any slides.
|
|
|
45
45
|
|
|
46
46
|
### Images
|
|
47
47
|
|
|
48
|
-
Images live in `assets.json`: every logo, illustration and icon the theme offers, keyed by category and name, each with a `path
|
|
48
|
+
Images live in `assets.json`: every logo, illustration and icon the theme offers, keyed by category and name, each with a `path` and a `description`. **Search it, do not read it whole** -- an icon set alone can run to thousands of entries. **Search for what the icon depicts, not what you mean by it**: a catalog is indexed by image, so "freshness" finds nothing while `grep -i -B1 "clock" assets.json` and `grep -i -B1 "bolt" assets.json` find the icon you wanted, with its path on the line above.
|
|
49
49
|
|
|
50
|
-
To use an image, copy it into your deck's folder at the same relative path, then write its `path`
|
|
50
|
+
To use an image, copy it into your deck's folder at the same relative path, then write its `path` in the image:
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
mkdir -p <deck dir>/assets && cp <theme dir>/assets/hub.png <deck dir>/assets/
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
```markdown
|
|
57
|
-

|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
If your deck is in the theme folder, the file is already there, so skip the copy. An image the user gives you also goes in the deck's folder
|
|
60
|
+
If your deck is in the theme folder, the file is already there, so skip the copy. An image the user gives you also goes in the deck's folder. Write alt text for this slide, describing what the image shows here rather than repeating the catalog description.
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
@@ -90,7 +90,7 @@ Keep each slot's content to what its region comfortably holds. When content over
|
|
|
90
90
|
- **Don't restyle the layout** -- the theme owns all design; you only fill slots
|
|
91
91
|
- **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration. If you get a `shrunk to X%` warning, look at the rendered slide: if the image is now too small to make out, use a simpler one. A `leaves X% of the frame empty` warning is the opposite: the image is a different shape from the slot. Neither fails the build -- judge both from the rendered slide.
|
|
92
92
|
- **Don't skip alt text** -- describe what a meaningful image shows and why it's there, not "image of". Leave it empty only for decoration, such as backgrounds and icons beside a heading.
|
|
93
|
-
- **Don't crop what can't be cropped** --
|
|
93
|
+
- **Don't crop what can't be cropped** -- a `cover` slot crops, so never put a diagram (mermaid included), chart, screenshot or logo in one; choose a layout with a `contain` image slot instead.
|
|
94
94
|
- **Don't invent layout names or image paths** -- layouts come from `manifest.json`, and theme images from `assets.json`, copied next to the deck
|
|
95
95
|
- **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image slot you care about. If you don't have a suitable image, ask the user for one.
|
|
96
96
|
|
|
@@ -106,7 +106,7 @@ Build the deck again ([Build](node_modules/@tycoworks/tycoslide/docs/markdown.md
|
|
|
106
106
|
|
|
107
107
|
| Error | Fix |
|
|
108
108
|
|-------|-----|
|
|
109
|
-
| An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `` with the path relative to the deck |
|
|
110
110
|
| `image "…" not found at …` | For a theme image, copy it into the deck's folder at the path you wrote (search `assets.json`; did you unzip `assets.dat`?) |
|
|
111
111
|
|
|
112
112
|
### Verification Loop
|
package/dist/agents/catalog.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
/**
|
|
3
|
-
* One image a theme offers deck authors. `fit` is the value an author copies
|
|
4
|
-
* into the image's title, so it is already a title value.
|
|
5
|
-
*/
|
|
6
|
-
declare const AssetEntrySchema: z.ZodObject<{
|
|
7
|
-
path: z.ZodString;
|
|
8
|
-
fit: z.ZodEnum<{
|
|
9
|
-
readonly Contain: "contain";
|
|
10
|
-
readonly Cover: "cover";
|
|
11
|
-
readonly ScaleDown: "scale-down";
|
|
12
|
-
}>;
|
|
13
|
-
description: z.ZodString;
|
|
14
|
-
}, z.core.$strict>;
|
|
15
|
-
declare const AssetCatalogSchema: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
16
|
-
path: z.ZodString;
|
|
17
|
-
fit: z.ZodEnum<{
|
|
18
|
-
readonly Contain: "contain";
|
|
19
|
-
readonly Cover: "cover";
|
|
20
|
-
readonly ScaleDown: "scale-down";
|
|
21
|
-
}>;
|
|
22
|
-
description: z.ZodString;
|
|
23
|
-
}, z.core.$strict>>>;
|
|
24
|
-
export type AssetEntry = z.infer<typeof AssetEntrySchema>;
|
|
25
|
-
export type AssetCatalog = z.infer<typeof AssetCatalogSchema>;
|
|
26
|
-
/**
|
|
27
|
-
* Read and validate a theme's image catalog, `assets.json`: what a deck-writing
|
|
28
|
-
* agent searches, copies images from, and takes each image's fit from. It is
|
|
29
|
-
* authored with the theme and required; a theme with no images has `{}`.
|
|
30
|
-
*/
|
|
31
|
-
export declare function loadAssetCatalog(themeDir: string): AssetCatalog;
|
|
32
|
-
export {};
|
package/dist/agents/catalog.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import * as z from "zod";
|
|
4
|
-
import { ImageFit, strict } from "../index.js";
|
|
5
|
-
import { ASSETS_FILE } from "./files.js";
|
|
6
|
-
/**
|
|
7
|
-
* One image a theme offers deck authors. `fit` is the value an author copies
|
|
8
|
-
* into the image's title, so it is already a title value.
|
|
9
|
-
*/
|
|
10
|
-
const AssetEntrySchema = strict({
|
|
11
|
-
path: z.string(),
|
|
12
|
-
fit: z.enum(ImageFit),
|
|
13
|
-
description: z.string(),
|
|
14
|
-
});
|
|
15
|
-
// `{ category: { name: entry } }`. Both record levels are open (the names are the
|
|
16
|
-
// theme's own); only the entry is strict.
|
|
17
|
-
const AssetCatalogSchema = z.record(z.string(), z.record(z.string(), AssetEntrySchema));
|
|
18
|
-
/**
|
|
19
|
-
* Read and validate a theme's image catalog, `assets.json`: what a deck-writing
|
|
20
|
-
* agent searches, copies images from, and takes each image's fit from. It is
|
|
21
|
-
* authored with the theme and required; a theme with no images has `{}`.
|
|
22
|
-
*/
|
|
23
|
-
export function loadAssetCatalog(themeDir) {
|
|
24
|
-
const path = join(themeDir, ASSETS_FILE);
|
|
25
|
-
let raw;
|
|
26
|
-
try {
|
|
27
|
-
raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
throw new Error(`Image catalog not found or invalid JSON: ${path}`);
|
|
31
|
-
}
|
|
32
|
-
const result = AssetCatalogSchema.safeParse(raw);
|
|
33
|
-
if (!result.success) {
|
|
34
|
-
throw new Error(`${ASSETS_FILE}: invalid image catalog\n${z.prettifyError(result.error)}`);
|
|
35
|
-
}
|
|
36
|
-
return result.data;
|
|
37
|
-
}
|