@tycoworks/tycoslide 0.15.1 → 0.16.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 +10 -6
- package/dist/agents/catalog.d.ts +32 -0
- package/dist/agents/catalog.js +37 -0
- package/dist/agents/commands.d.ts +9 -0
- package/dist/agents/commands.js +57 -0
- package/dist/agents/extract.d.ts +22 -0
- package/dist/agents/extract.js +54 -0
- package/dist/{files.d.ts → agents/files.d.ts} +13 -3
- package/dist/{files.js → agents/files.js} +16 -3
- package/dist/agents/inventory.d.ts +83 -0
- package/dist/agents/inventory.js +568 -0
- package/dist/agents/manifest.d.ts +3 -0
- package/dist/{manifest.js → agents/manifest.js} +3 -14
- package/dist/agents/media.d.ts +38 -0
- package/dist/agents/media.js +83 -0
- package/dist/agents/pptx.d.ts +48 -0
- package/dist/agents/pptx.js +123 -0
- package/dist/agents/skill.d.ts +43 -0
- package/dist/agents/skill.js +134 -0
- package/dist/cli.js +8 -44
- package/dist/engine/dom.d.ts +3 -0
- package/dist/engine/dom.js +4 -0
- package/dist/engine/fillers/image.d.ts +6 -5
- package/dist/engine/fillers/image.js +24 -7
- package/dist/engine/generate.d.ts +2 -2
- package/dist/engine/generate.js +2 -2
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/notes.js +20 -62
- package/dist/engine/ooxml.d.ts +36 -0
- package/dist/engine/ooxml.js +56 -0
- package/dist/engine/types.d.ts +4 -0
- package/dist/index.d.ts +4 -11
- package/dist/index.js +2 -15
- package/dist/markdown/blocks/code.js +1 -2
- package/dist/markdown/blocks/image.d.ts +23 -0
- package/dist/markdown/blocks/image.js +65 -3
- package/dist/markdown/blocks/mermaid.d.ts +1 -1
- package/dist/markdown/blocks/mermaid.js +6 -4
- package/dist/markdown/blocks/table.js +7 -6
- package/dist/markdown/blocks/text.js +9 -8
- package/dist/markdown/deckCompiler.d.ts +2 -13
- package/dist/markdown/deckCompiler.js +7 -78
- package/dist/markdown/index.d.ts +4 -2
- package/dist/markdown/index.js +3 -1
- package/dist/markdown/inline.d.ts +2 -0
- package/dist/markdown/inline.js +4 -3
- package/dist/markdown/schema/themeConfigSchema.d.ts +2 -11
- package/dist/markdown/schema/themeConfigSchema.js +6 -16
- package/dist/markdown/slideParser.d.ts +2 -0
- package/dist/markdown/slideParser.js +6 -3
- package/dist/markdown/types.d.ts +4 -29
- package/dist/markdown/types.js +0 -11
- package/{theme-package/syntax.md → docs/markdown.md} +60 -28
- package/docs/theme.md +144 -0
- package/package.json +3 -2
- package/theme-package/SKILL.md +24 -90
- package/dist/engine/rels.d.ts +0 -18
- package/dist/engine/rels.js +0 -55
- package/dist/manifest.d.ts +0 -5
- package/dist/skillZip.d.ts +0 -49
- package/dist/skillZip.js +0 -177
package/dist/engine/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export { childrenByTag, collectElements } from "./dom.js";
|
|
1
2
|
export { FILLERS } from "./fillers/filler.js";
|
|
2
3
|
export { fillImage } from "./fillers/image.js";
|
|
3
4
|
export { fillTable, isTableFill } from "./fillers/table.js";
|
|
4
5
|
export { fillTemplate } from "./fillers/template.js";
|
|
5
6
|
export { fillText, isTextFill } from "./fillers/text.js";
|
|
6
7
|
export { generate } from "./generate.js";
|
|
8
|
+
export { PRESENTATION_PART, parseXml, readRelationships, relsPathFor, resolveTarget } from "./ooxml.js";
|
|
7
9
|
export { ImageFit, SlotType, TEMPLATE_DIR } from "./types.js";
|
package/dist/engine/notes.js
CHANGED
|
@@ -13,18 +13,14 @@
|
|
|
13
13
|
* step authored notes) or strip any auto-copied part (when it did not, or notes
|
|
14
14
|
* are excluded) — so the designer's template notes never leak into the output.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { XMLSerializer } from "@xmldom/xmldom";
|
|
17
17
|
import { Attr, buildParagraph, buildRun, nextFreeRId as nextFreeRIdForDoc, Tag } from "./dom.js";
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
import { PRESENTATION_PART, PRESENTATION_RELS_PART, parseXml, RelTypeSuffix, readRelationships, relsPathFor, resolveTarget, } from "./ooxml.js";
|
|
19
|
+
// `[Content_Types].xml` Override entries: package-level names not covered by
|
|
20
|
+
// dom.ts's `Tag`/`Attr` (which name slide-XML tokens).
|
|
21
21
|
const CT_OVERRIDE = "Override";
|
|
22
22
|
const CT_PART_NAME = "PartName";
|
|
23
23
|
const CT_CONTENT_TYPE = "ContentType";
|
|
24
|
-
const REL_TYPE_SUFFIX = {
|
|
25
|
-
NotesSlide: "/notesSlide",
|
|
26
|
-
NotesMaster: "/notesMaster",
|
|
27
|
-
};
|
|
28
24
|
const NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
29
25
|
const NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main";
|
|
30
26
|
const NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
@@ -38,7 +34,6 @@ const REL_TYPE = {
|
|
|
38
34
|
const NOTES_SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml";
|
|
39
35
|
const XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n';
|
|
40
36
|
const CONTENT_TYPES_PART = "[Content_Types].xml";
|
|
41
|
-
const PRESENTATION_RELS_PART = "ppt/_rels/presentation.xml.rels";
|
|
42
37
|
const NOTES_SKELETON = `<p:notes xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld><p:spTree>` +
|
|
43
38
|
`<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>` +
|
|
44
39
|
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="Notes Placeholder 1"/>` +
|
|
@@ -51,7 +46,7 @@ const NOTES_SKELETON = `<p:notes xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${
|
|
|
51
46
|
* is handled by the serializer.
|
|
52
47
|
*/
|
|
53
48
|
export function buildNotesSlideXml(notes) {
|
|
54
|
-
const doc =
|
|
49
|
+
const doc = parseXml(NOTES_SKELETON);
|
|
55
50
|
const txBody = doc.getElementsByTagName("p:txBody")[0];
|
|
56
51
|
for (const line of notes.replace(/\r?\n+$/, "").split(/\r?\n/)) {
|
|
57
52
|
// Reuse the engine's run/paragraph builders — they set xml:space="preserve"
|
|
@@ -63,7 +58,7 @@ export function buildNotesSlideXml(notes) {
|
|
|
63
58
|
}
|
|
64
59
|
/** Next unused `rIdN` in a `.rels` XML string (max existing id + 1). */
|
|
65
60
|
export function nextFreeRId(relsXml) {
|
|
66
|
-
return nextFreeRIdForDoc(
|
|
61
|
+
return nextFreeRIdForDoc(parseXml(relsXml));
|
|
67
62
|
}
|
|
68
63
|
function serialize(doc) {
|
|
69
64
|
// xmldom re-emits any `<?xml?>` declaration present in the parsed source, so a
|
|
@@ -72,9 +67,6 @@ function serialize(doc) {
|
|
|
72
67
|
const body = new XMLSerializer().serializeToString(doc).replace(/^<\?xml[^>]*\?>\s*/, "");
|
|
73
68
|
return XML_DECL + body;
|
|
74
69
|
}
|
|
75
|
-
function parse(xml) {
|
|
76
|
-
return new DOMParser().parseFromString(xml, "text/xml");
|
|
77
|
-
}
|
|
78
70
|
async function readString(archive, file) {
|
|
79
71
|
const raw = await archive.read(file, "string");
|
|
80
72
|
return typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
@@ -114,14 +106,8 @@ function notesRelsXml(slideNumber, masterTarget) {
|
|
|
114
106
|
async function findNotesMaster(archive) {
|
|
115
107
|
if (!archive.fileExists(PRESENTATION_RELS_PART))
|
|
116
108
|
return undefined;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
for (let i = 0; i < rels.length; i++) {
|
|
120
|
-
if (rels[i].getAttribute(Attr.TYPE)?.endsWith(REL_TYPE_SUFFIX.NotesMaster)) {
|
|
121
|
-
return `../${rels[i].getAttribute(Attr.TARGET)}`;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return undefined;
|
|
109
|
+
const master = readRelationships(await readString(archive, PRESENTATION_RELS_PART)).find((rel) => rel.type.endsWith(RelTypeSuffix.NotesMaster));
|
|
110
|
+
return master && `../${master.target}`;
|
|
125
111
|
}
|
|
126
112
|
function findOverride(ctDoc, partName) {
|
|
127
113
|
const overrides = ctDoc.getElementsByTagName(CT_OVERRIDE);
|
|
@@ -132,7 +118,7 @@ function findOverride(ctDoc, partName) {
|
|
|
132
118
|
return undefined;
|
|
133
119
|
}
|
|
134
120
|
async function addContentTypeOverride(archive, partName) {
|
|
135
|
-
const ctDoc =
|
|
121
|
+
const ctDoc = parseXml(await readString(archive, CONTENT_TYPES_PART));
|
|
136
122
|
if (findOverride(ctDoc, partName))
|
|
137
123
|
return;
|
|
138
124
|
const override = ctDoc.createElementNS(NS_CT, CT_OVERRIDE);
|
|
@@ -150,26 +136,12 @@ function removeOverrideFromDoc(ctDoc, partName) {
|
|
|
150
136
|
return true;
|
|
151
137
|
}
|
|
152
138
|
async function removeContentTypeOverride(archive, partName) {
|
|
153
|
-
const ctDoc =
|
|
139
|
+
const ctDoc = parseXml(await readString(archive, CONTENT_TYPES_PART));
|
|
154
140
|
if (!removeOverrideFromDoc(ctDoc, partName))
|
|
155
141
|
return;
|
|
156
142
|
await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
|
|
157
143
|
}
|
|
158
|
-
/** Resolve `target` (from a rels file) against `baseDir`, collapsing `.`/`..`. */
|
|
159
|
-
function resolveRelative(baseDir, target) {
|
|
160
|
-
const parts = baseDir.split("/").filter(Boolean);
|
|
161
|
-
for (const seg of target.split("/")) {
|
|
162
|
-
if (seg === "" || seg === ".")
|
|
163
|
-
continue;
|
|
164
|
-
if (seg === "..")
|
|
165
|
-
parts.pop();
|
|
166
|
-
else
|
|
167
|
-
parts.push(seg);
|
|
168
|
-
}
|
|
169
|
-
return parts.join("/");
|
|
170
|
-
}
|
|
171
144
|
const NOTES_SLIDE_PART = /^ppt\/notesSlides\/notesSlide\d+\.xml$/;
|
|
172
|
-
const PRESENTATION_PART = "ppt/presentation.xml";
|
|
173
145
|
/**
|
|
174
146
|
* The slide parts a deck actually presents, in `<p:sldIdLst>` order, resolved via
|
|
175
147
|
* `presentation.xml.rels`. Returns full archive paths (e.g. `ppt/slides/slide1.xml`).
|
|
@@ -182,7 +154,7 @@ const PRESENTATION_PART = "ppt/presentation.xml";
|
|
|
182
154
|
* referenced. Only slides reachable from `presentation.xml` are live.
|
|
183
155
|
*/
|
|
184
156
|
async function liveSlideParts(archive) {
|
|
185
|
-
const pres =
|
|
157
|
+
const pres = parseXml(await readString(archive, PRESENTATION_PART));
|
|
186
158
|
const sldIds = pres.getElementsByTagName("p:sldId");
|
|
187
159
|
const rIds = [];
|
|
188
160
|
for (let i = 0; i < sldIds.length; i++) {
|
|
@@ -190,27 +162,16 @@ async function liveSlideParts(archive) {
|
|
|
190
162
|
if (rid)
|
|
191
163
|
rIds.push(rid);
|
|
192
164
|
}
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const targetById = new Map();
|
|
196
|
-
for (let i = 0; i < rels.length; i++) {
|
|
197
|
-
const id = rels[i].getAttribute(Attr.ID);
|
|
198
|
-
const target = rels[i].getAttribute(Attr.TARGET);
|
|
199
|
-
if (id && target)
|
|
200
|
-
targetById.set(id, target);
|
|
201
|
-
}
|
|
165
|
+
const rels = readRelationships(await readString(archive, PRESENTATION_RELS_PART));
|
|
166
|
+
const targetById = new Map(rels.map((rel) => [rel.id, rel.target]));
|
|
202
167
|
const parts = [];
|
|
203
168
|
for (const rid of rIds) {
|
|
204
169
|
const target = targetById.get(rid);
|
|
205
170
|
if (target)
|
|
206
|
-
parts.push(
|
|
171
|
+
parts.push(resolveTarget(PRESENTATION_PART, target));
|
|
207
172
|
}
|
|
208
173
|
return parts;
|
|
209
174
|
}
|
|
210
|
-
/** The `_rels` path for an archive part (`ppt/slides/slide1.xml` → `ppt/slides/_rels/slide1.xml.rels`). */
|
|
211
|
-
function relsPathFor(part) {
|
|
212
|
-
return part.replace(/\/([^/]+)$/, "/_rels/$1.rels");
|
|
213
|
-
}
|
|
214
175
|
/**
|
|
215
176
|
* Remove every notesSlide part that no live slide references. automizer drops the
|
|
216
177
|
* template's orphaned *slides* from the deck when building it but leaves all their
|
|
@@ -234,18 +195,15 @@ export async function sweepOrphanNotes(archive) {
|
|
|
234
195
|
const relsPath = relsPathFor(slidePart);
|
|
235
196
|
if (!archive.fileExists(relsPath))
|
|
236
197
|
continue;
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (target && rels[i].getAttribute(Attr.TYPE)?.endsWith(REL_TYPE_SUFFIX.NotesSlide)) {
|
|
241
|
-
referenced.add(resolveRelative("ppt/slides", target));
|
|
242
|
-
}
|
|
198
|
+
for (const rel of readRelationships(await readString(archive, relsPath))) {
|
|
199
|
+
if (rel.type.endsWith(RelTypeSuffix.NotesSlide))
|
|
200
|
+
referenced.add(resolveTarget(slidePart, rel.target));
|
|
243
201
|
}
|
|
244
202
|
}
|
|
245
203
|
const orphans = notesParts.filter((part) => !referenced.has(part));
|
|
246
204
|
if (orphans.length === 0)
|
|
247
205
|
return;
|
|
248
|
-
const ctDoc =
|
|
206
|
+
const ctDoc = parseXml(await readString(archive, CONTENT_TYPES_PART));
|
|
249
207
|
let ctChanged = false;
|
|
250
208
|
for (const part of orphans) {
|
|
251
209
|
await archive.remove(part);
|
|
@@ -295,7 +253,7 @@ export async function applyNotesToSlide(archive, slideNumber, notes, excludeNote
|
|
|
295
253
|
}
|
|
296
254
|
await archive.write(notesPath, buildNotesSlideXml(notes));
|
|
297
255
|
await archive.write(notesRelsPath, notesRelsXml(slideNumber, masterTarget));
|
|
298
|
-
const relsDoc =
|
|
256
|
+
const relsDoc = parseXml(await readString(archive, slideRelsPath));
|
|
299
257
|
appendRel(relsDoc, nextFreeRIdForDoc(relsDoc), REL_TYPE.NotesSlide, `../notesSlides/notesSlide${slideNumber}.xml`);
|
|
300
258
|
await archive.write(slideRelsPath, serialize(relsDoc));
|
|
301
259
|
await addContentTypeOverride(archive, `/${notesPath}`);
|
|
@@ -306,7 +264,7 @@ export async function applyNotesToSlide(archive, slideNumber, notes, excludeNote
|
|
|
306
264
|
await archive.remove(notesPath);
|
|
307
265
|
if (archive.fileExists(notesRelsPath))
|
|
308
266
|
await archive.remove(notesRelsPath);
|
|
309
|
-
const relsDoc =
|
|
267
|
+
const relsDoc = parseXml(await readString(archive, slideRelsPath));
|
|
310
268
|
if (removeNotesRel(relsDoc))
|
|
311
269
|
await archive.write(slideRelsPath, serialize(relsDoc));
|
|
312
270
|
await removeContentTypeOverride(archive, `/${notesPath}`);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-level OOXML: where a .pptx keeps its parts and how they point at each
|
|
3
|
+
* other. Used by speaker notes; the generic helpers are exported from the core's
|
|
4
|
+
* public entry for the agent layer's template reading. Slide-XML tokens live in
|
|
5
|
+
* `dom.ts`'s `Tag` / `Attr`.
|
|
6
|
+
*/
|
|
7
|
+
/** The presentation part; its presence is what makes a zip a .pptx. */
|
|
8
|
+
export declare const PRESENTATION_PART = "ppt/presentation.xml";
|
|
9
|
+
/** The presentation part's relationships: slides, masters, the notes master. */
|
|
10
|
+
export declare const PRESENTATION_RELS_PART = "ppt/_rels/presentation.xml.rels";
|
|
11
|
+
/**
|
|
12
|
+
* Tail segments of relationship-type URLs. Matching the tail tolerates both the
|
|
13
|
+
* transitional (`schemas.openxmlformats.org`) and strict (`purl.oclc.org`) forms.
|
|
14
|
+
*/
|
|
15
|
+
export declare const RelTypeSuffix: {
|
|
16
|
+
readonly NotesSlide: "/notesSlide";
|
|
17
|
+
readonly NotesMaster: "/notesMaster";
|
|
18
|
+
};
|
|
19
|
+
export declare function parseXml(xml: string): any;
|
|
20
|
+
/** One relationship from a part's rels file. An `external` target is a URL, not a part. */
|
|
21
|
+
export type Relationship = {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
target: string;
|
|
25
|
+
external: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** The relationships in a rels file. One missing its `Id`, `Type` or `Target` is malformed, and skipped. */
|
|
28
|
+
export declare function readRelationships(relsXml: string): Relationship[];
|
|
29
|
+
/** The `_rels` path for an archive part (`ppt/slides/slide1.xml` → `ppt/slides/_rels/slide1.xml.rels`). */
|
|
30
|
+
export declare function relsPathFor(part: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a relationship `target` from `part`'s rels to an archive path. A target
|
|
33
|
+
* starting with `/` is package-absolute; anything else is relative to `part`'s
|
|
34
|
+
* directory. `.` and `..` segments collapse.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveTarget(part: string, target: string): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-level OOXML: where a .pptx keeps its parts and how they point at each
|
|
3
|
+
* other. Used by speaker notes; the generic helpers are exported from the core's
|
|
4
|
+
* public entry for the agent layer's template reading. Slide-XML tokens live in
|
|
5
|
+
* `dom.ts`'s `Tag` / `Attr`.
|
|
6
|
+
*/
|
|
7
|
+
import { posix } from "node:path";
|
|
8
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
9
|
+
import { Attr, collectElements, Tag } from "./dom.js";
|
|
10
|
+
/** The presentation part; its presence is what makes a zip a .pptx. */
|
|
11
|
+
export const PRESENTATION_PART = "ppt/presentation.xml";
|
|
12
|
+
/** The presentation part's relationships: slides, masters, the notes master. */
|
|
13
|
+
export const PRESENTATION_RELS_PART = "ppt/_rels/presentation.xml.rels";
|
|
14
|
+
/**
|
|
15
|
+
* Tail segments of relationship-type URLs. Matching the tail tolerates both the
|
|
16
|
+
* transitional (`schemas.openxmlformats.org`) and strict (`purl.oclc.org`) forms.
|
|
17
|
+
*/
|
|
18
|
+
export const RelTypeSuffix = {
|
|
19
|
+
NotesSlide: "/notesSlide",
|
|
20
|
+
NotesMaster: "/notesMaster",
|
|
21
|
+
};
|
|
22
|
+
export function parseXml(xml) {
|
|
23
|
+
return new DOMParser().parseFromString(xml, "text/xml");
|
|
24
|
+
}
|
|
25
|
+
/** The relationships in a rels file. One missing its `Id`, `Type` or `Target` is malformed, and skipped. */
|
|
26
|
+
export function readRelationships(relsXml) {
|
|
27
|
+
return collectElements(parseXml(relsXml), Tag.RELATIONSHIP).flatMap((rel) => {
|
|
28
|
+
const id = rel.getAttribute(Attr.ID);
|
|
29
|
+
const type = rel.getAttribute(Attr.TYPE);
|
|
30
|
+
const target = rel.getAttribute(Attr.TARGET);
|
|
31
|
+
if (!id || !type || !target)
|
|
32
|
+
return [];
|
|
33
|
+
return [{ id, type, target, external: rel.getAttribute(Attr.TARGET_MODE) === Attr.EXTERNAL }];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** The `_rels` path for an archive part (`ppt/slides/slide1.xml` → `ppt/slides/_rels/slide1.xml.rels`). */
|
|
37
|
+
export function relsPathFor(part) {
|
|
38
|
+
return part.replace(/\/([^/]+)$/, "/_rels/$1.rels");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a relationship `target` from `part`'s rels to an archive path. A target
|
|
42
|
+
* starting with `/` is package-absolute; anything else is relative to `part`'s
|
|
43
|
+
* directory. `.` and `..` segments collapse.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveTarget(part, target) {
|
|
46
|
+
const parts = target.startsWith("/") ? [] : posix.dirname(part).split("/").filter(Boolean);
|
|
47
|
+
for (const seg of target.split("/")) {
|
|
48
|
+
if (seg === "" || seg === ".")
|
|
49
|
+
continue;
|
|
50
|
+
if (seg === "..")
|
|
51
|
+
parts.pop();
|
|
52
|
+
else
|
|
53
|
+
parts.push(seg);
|
|
54
|
+
}
|
|
55
|
+
return parts.join("/");
|
|
56
|
+
}
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -102,11 +102,15 @@ export type ImageFit = (typeof ImageFit)[keyof typeof ImageFit];
|
|
|
102
102
|
* Input to fillImage — a resolved image path plus its object-fit directive.
|
|
103
103
|
* `path` must be absolute; the compiler resolves it (and maps the asset's
|
|
104
104
|
* semantic type to a `fit`) before the ImageFill reaches the engine.
|
|
105
|
+
* `alt` is the picture's accessibility description (`descr` on its
|
|
106
|
+
* `<p:cNvPr>`); empty means none, and clears any description the placeholder
|
|
107
|
+
* carried.
|
|
105
108
|
*/
|
|
106
109
|
export type ImageFill = {
|
|
107
110
|
type: typeof SlotType.Image;
|
|
108
111
|
path: string;
|
|
109
112
|
fit: ImageFit;
|
|
113
|
+
alt: string;
|
|
110
114
|
};
|
|
111
115
|
/** A shape's absolute position and size, in EMU — the slot's frame. */
|
|
112
116
|
export type Frame = {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,6 @@ import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig } from
|
|
|
3
3
|
/**
|
|
4
4
|
* Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
|
|
5
5
|
* Fields are copied cell-by-cell so the boundary is explicit — no casts.
|
|
6
|
-
* The compiler's `assets` catalog is intentionally NOT forwarded: the engine
|
|
7
|
-
* is ignorant of theme-level asset metadata; the compiler resolves any asset
|
|
8
|
-
* reference to a filesystem path and wraps it as an ImageFill in
|
|
9
|
-
* `step.content` before this projection runs.
|
|
10
6
|
*/
|
|
11
7
|
export declare function toEngineThemeConfig(config: CompilerThemeConfig): ThemeConfig;
|
|
12
8
|
/**
|
|
@@ -32,10 +28,7 @@ export declare function toEngineConfig(config: CompilerConfig): Config;
|
|
|
32
28
|
* directory) so no post-write cleanup is needed.
|
|
33
29
|
*/
|
|
34
30
|
export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig, options?: GenerateOptions): Promise<void>;
|
|
35
|
-
export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
|
|
36
|
-
export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
|
|
37
|
-
export {
|
|
38
|
-
export {
|
|
39
|
-
export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
|
|
40
|
-
export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
|
|
41
|
-
export { expandAssets } from "./skillZip.js";
|
|
31
|
+
export type { Config, Deck, DeckStep, Frame, GenerateOptions, ImageFill, Layout, Relationship, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
|
|
32
|
+
export { childrenByTag, collectElements, fillImage, fillTable, fillTemplate, fillText, generate, ImageFit, PRESENTATION_PART, parseXml, readRelationships, relsPathFor, resolveTarget, SlotType, TEMPLATE_DIR, } from "./engine/index.js";
|
|
33
|
+
export type { CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
|
|
34
|
+
export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig, strict, templateKeys, } from "./markdown/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { generate, SlotType, } from "./engine/index.js";
|
|
2
2
|
import { AcceptType, } from "./markdown/types.js";
|
|
3
|
-
import { expandAssets } from "./skillZip.js";
|
|
4
3
|
/**
|
|
5
4
|
* A frontmatter parameter always fills one physical shape on the layout's own
|
|
6
5
|
* slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
|
|
@@ -70,10 +69,6 @@ function toEngineLayout(layout) {
|
|
|
70
69
|
/**
|
|
71
70
|
* Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
|
|
72
71
|
* Fields are copied cell-by-cell so the boundary is explicit — no casts.
|
|
73
|
-
* The compiler's `assets` catalog is intentionally NOT forwarded: the engine
|
|
74
|
-
* is ignorant of theme-level asset metadata; the compiler resolves any asset
|
|
75
|
-
* reference to a filesystem path and wraps it as an ImageFill in
|
|
76
|
-
* `step.content` before this projection runs.
|
|
77
72
|
*/
|
|
78
73
|
export function toEngineThemeConfig(config) {
|
|
79
74
|
const result = {
|
|
@@ -113,18 +108,10 @@ export async function buildDeck(deck, config, options = {}) {
|
|
|
113
108
|
if (deck.output === undefined) {
|
|
114
109
|
throw new Error('buildDeck: deck.output is not set. Set it (e.g. "deck.pptx") before calling buildDeck.');
|
|
115
110
|
}
|
|
116
|
-
// A packaged theme ships its assets as one archive, because hosts cap how many
|
|
117
|
-
// files a skill may contain. Expand it here rather than at compile: the catalog
|
|
118
|
-
// is what an author reads, and only filling needs the bytes.
|
|
119
|
-
await expandAssets(config.rootDir);
|
|
120
111
|
const engineDeck = { theme: deck.theme, output: deck.output, steps: deck.steps };
|
|
121
112
|
await generate(engineDeck, toEngineConfig(config), options);
|
|
122
113
|
}
|
|
123
114
|
// Engine — primitives-only public surface.
|
|
124
|
-
export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
|
|
125
|
-
export { ASSETS_ARCHIVE, ASSETS_FILE } from "./files.js";
|
|
126
|
-
// Authoring
|
|
127
|
-
export { generateAssetCatalog, generateManifest } from "./manifest.js";
|
|
115
|
+
export { childrenByTag, collectElements, fillImage, fillTable, fillTemplate, fillText, generate, ImageFit, PRESENTATION_PART, parseXml, readRelationships, relsPathFor, resolveTarget, SlotType, TEMPLATE_DIR, } from "./engine/index.js";
|
|
128
116
|
// Markdown / Compiler
|
|
129
|
-
export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
|
|
130
|
-
export { expandAssets } from "./skillZip.js";
|
|
117
|
+
export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig, strict, templateKeys, } from "./markdown/index.js";
|
|
@@ -18,8 +18,7 @@ export const CODE = {
|
|
|
18
18
|
// A fence with no language can't be highlighted (no Shiki grammar to pick),
|
|
19
19
|
// so fail fast naming the slot rather than attempt a language-less highlight.
|
|
20
20
|
if (!code.lang) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
"with no language; add one after the opening ``` (e.g. ```sql).");
|
|
21
|
+
throw new Error(`${ctx.region} has a code fence with no language; add one after the opening \`\`\` (e.g. \`\`\`sql).`);
|
|
23
22
|
}
|
|
24
23
|
const codeTheme = ctx.config.codeTheme;
|
|
25
24
|
if (!codeTheme) {
|
|
@@ -1,2 +1,25 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
1
2
|
import { type BlockHandler } from "../types.js";
|
|
2
3
|
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,9 +1,71 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import { ImageFit, SlotType } from "../../engine/index.js";
|
|
1
6
|
import { MdastType } from "../mdast.js";
|
|
7
|
+
import { strict } from "../schema/strict.js";
|
|
8
|
+
import { isYamlMapping } from "../slideParser.js";
|
|
2
9
|
import { AcceptType } from "../types.js";
|
|
10
|
+
/** The fit when the title gives none: the whole picture, never cropped. */
|
|
11
|
+
const DEFAULT_FIT = ImageFit.Contain;
|
|
3
12
|
export const IMAGE = {
|
|
4
13
|
match: (node) => node.type === MdastType.Image,
|
|
5
14
|
acceptType: AcceptType.Image,
|
|
6
|
-
// `
|
|
7
|
-
//
|
|
8
|
-
|
|
15
|
+
// `url` is a path relative to the deck. `alt` is the picture's alt text; the
|
|
16
|
+
// parser flattens it to plain text, and an image written without any is "".
|
|
17
|
+
// `title` carries the image's options.
|
|
18
|
+
compile: async (node, ctx) => {
|
|
19
|
+
const { url, alt, title } = node;
|
|
20
|
+
const options = parseImageTitle(title, ctx.region);
|
|
21
|
+
const path = resolve(ctx.config.deckDir, url);
|
|
22
|
+
if (!existsSync(path)) {
|
|
23
|
+
throw new Error(`${ctx.region}: image "${url}" not found at ${path}`);
|
|
24
|
+
}
|
|
25
|
+
return { type: SlotType.Image, path, fit: options.fit ?? DEFAULT_FIT, alt: alt ?? "" };
|
|
26
|
+
},
|
|
9
27
|
};
|
|
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
|
+
}
|
|
@@ -8,7 +8,7 @@ export declare const MERMAID_LANG = "mermaid";
|
|
|
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
10
|
* ImageFill. Fit is always `contain` — mermaid diagrams are shown in their
|
|
11
|
-
* entirety. Resolution is strict: the theme MUST carry a `mermaid` block, MUST
|
|
11
|
+
* entirety — and a fence 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
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { extname, join, resolve } from "node:path";
|
|
4
|
+
import { extname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { ImageFit, SlotType } from "../../engine/index.js";
|
|
7
7
|
import { MdastType } from "../mdast.js";
|
|
@@ -16,7 +16,7 @@ export const MERMAID_LANG = "mermaid";
|
|
|
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
18
|
* ImageFill. Fit is always `contain` — mermaid diagrams are shown in their
|
|
19
|
-
* entirety. Resolution is strict: the theme MUST carry a `mermaid` block, MUST
|
|
19
|
+
* entirety — and a fence 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, fit: ImageFit.Contain };
|
|
45
|
+
return { type: SlotType.Image, path: pngPath, fit: ImageFit.Contain, alt: "" };
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
48
|
const FONT_FORMATS = {
|
|
@@ -80,7 +80,9 @@ function ensureCacheDir(config) {
|
|
|
80
80
|
* naming the family + path — never a silent skip.
|
|
81
81
|
*/
|
|
82
82
|
function resolveFonts(rootDir, fonts) {
|
|
83
|
-
|
|
83
|
+
// `createRequire` treats a path ending in a separator as a directory, so package
|
|
84
|
+
// names resolve from the theme's folder (its node_modules).
|
|
85
|
+
const require = createRequire(join(rootDir, sep));
|
|
84
86
|
return fonts.map((font) => {
|
|
85
87
|
const isFsPath = font.path.startsWith(".") || font.path.startsWith("/");
|
|
86
88
|
let absPath;
|
|
@@ -4,20 +4,21 @@ import { AcceptType } from "../types.js";
|
|
|
4
4
|
export const TABLE = {
|
|
5
5
|
match: (node) => node.type === MdastType.Table,
|
|
6
6
|
acceptType: AcceptType.Table,
|
|
7
|
-
compile: async (node) => compileTable(node),
|
|
7
|
+
compile: async (node, ctx) => compileTable(node, { region: ctx.region }),
|
|
8
8
|
};
|
|
9
9
|
/** A GFM `table` → TableFill: first row is headers, the rest are body rows; each
|
|
10
10
|
* cell's phrasing children become `TextRun[]` via the shared inline walk. */
|
|
11
|
-
function compileTable(node) {
|
|
11
|
+
function compileTable(node, state) {
|
|
12
12
|
const [head, ...body] = node.children;
|
|
13
|
+
const cell = (tableCell) => cellParagraph(tableCell, state);
|
|
13
14
|
return {
|
|
14
|
-
headers: head ? head.children.map(
|
|
15
|
-
rows: body.map((row) => row.children.map(
|
|
15
|
+
headers: head ? head.children.map(cell) : [],
|
|
16
|
+
rows: body.map((row) => row.children.map(cell)),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
/** One table cell → a StyledParagraph. An empty cell keeps a single empty run so
|
|
19
20
|
* downstream code always sees a run to style. */
|
|
20
|
-
function cellParagraph(cell) {
|
|
21
|
-
const runs = walkPhrasingChildren(cell.children,
|
|
21
|
+
function cellParagraph(cell, state) {
|
|
22
|
+
const runs = walkPhrasingChildren(cell.children, state);
|
|
22
23
|
return { runs: runs.length > 0 ? runs : [{ text: "" }] };
|
|
23
24
|
}
|
|
@@ -10,16 +10,18 @@ import { MdastType } from "../mdast.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export function compileTextAggregate(nodes, ctx) {
|
|
12
12
|
const paragraphs = [];
|
|
13
|
+
// Hard breaks split paragraphs here, and inline errors name the region.
|
|
14
|
+
const state = { breakAsNewline: true, region: ctx.region };
|
|
13
15
|
for (const node of nodes) {
|
|
14
16
|
switch (node.type) {
|
|
15
17
|
case MdastType.Paragraph:
|
|
16
18
|
case MdastType.Heading:
|
|
17
|
-
for (const runs of splitRunsIntoParagraphs(walkPhrasingChildren(node.children,
|
|
19
|
+
for (const runs of splitRunsIntoParagraphs(walkPhrasingChildren(node.children, state))) {
|
|
18
20
|
paragraphs.push({ runs });
|
|
19
21
|
}
|
|
20
22
|
break;
|
|
21
23
|
case MdastType.List:
|
|
22
|
-
paragraphs.push(...listParagraphs(node, 0));
|
|
24
|
+
paragraphs.push(...listParagraphs(node, 0, state));
|
|
23
25
|
break;
|
|
24
26
|
default:
|
|
25
27
|
throw new Error(reject(nodes.length, node.type, ctx));
|
|
@@ -34,11 +36,10 @@ export function compileTextAggregate(nodes, ctx) {
|
|
|
34
36
|
* (table/image/code) mixed into prose — both name the layout/slide/slot + type.
|
|
35
37
|
*/
|
|
36
38
|
function reject(nodeCount, nodeType, ctx) {
|
|
37
|
-
const where = `Slide ${ctx.slideNo}: layout "${ctx.layoutName}" slot content (from ${ctx.source})`;
|
|
38
39
|
if (nodeCount === 1) {
|
|
39
|
-
return `${
|
|
40
|
+
return `${ctx.region} is a standalone "${nodeType}" block, which is not a supported content kind.`;
|
|
40
41
|
}
|
|
41
|
-
return (`${
|
|
42
|
+
return (`${ctx.region} mixes a "${nodeType}" block with other content; ` +
|
|
42
43
|
"a table, image, or code block must be the region's only content.");
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
@@ -47,15 +48,15 @@ function reject(nodeCount, nodeType, ctx) {
|
|
|
47
48
|
* `level + 1`. Ordered and unordered both yield plain bullets — the engine has
|
|
48
49
|
* no ordered flag.
|
|
49
50
|
*/
|
|
50
|
-
function listParagraphs(list, level) {
|
|
51
|
+
function listParagraphs(list, level, state) {
|
|
51
52
|
const out = [];
|
|
52
53
|
for (const item of list.children) {
|
|
53
54
|
for (const child of item.children) {
|
|
54
55
|
if (child.type === MdastType.List) {
|
|
55
|
-
out.push(...listParagraphs(child, level + 1));
|
|
56
|
+
out.push(...listParagraphs(child, level + 1, state));
|
|
56
57
|
}
|
|
57
58
|
else if (child.type === MdastType.Paragraph || child.type === MdastType.Heading) {
|
|
58
|
-
for (const runs of splitRunsIntoParagraphs(walkPhrasingChildren(child.children,
|
|
59
|
+
for (const runs of splitRunsIntoParagraphs(walkPhrasingChildren(child.children, state))) {
|
|
59
60
|
out.push({ runs, bullet: { level } });
|
|
60
61
|
}
|
|
61
62
|
}
|