@tycoworks/tycoslide 0.15.1 → 0.16.1

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.
Files changed (63) hide show
  1. package/README.md +9 -5
  2. package/dist/agents/catalog.d.ts +32 -0
  3. package/dist/agents/catalog.js +37 -0
  4. package/dist/agents/commands.d.ts +9 -0
  5. package/dist/agents/commands.js +57 -0
  6. package/dist/agents/extract.d.ts +22 -0
  7. package/dist/agents/extract.js +54 -0
  8. package/dist/{files.d.ts → agents/files.d.ts} +14 -3
  9. package/dist/{files.js → agents/files.js} +20 -5
  10. package/dist/agents/inventory.d.ts +83 -0
  11. package/dist/agents/inventory.js +568 -0
  12. package/dist/agents/manifest.d.ts +3 -0
  13. package/dist/{manifest.js → agents/manifest.js} +3 -14
  14. package/dist/agents/media.d.ts +38 -0
  15. package/dist/agents/media.js +83 -0
  16. package/dist/agents/pptx.d.ts +48 -0
  17. package/dist/agents/pptx.js +123 -0
  18. package/dist/agents/skill.d.ts +43 -0
  19. package/dist/agents/skill.js +134 -0
  20. package/dist/cli.js +8 -44
  21. package/dist/engine/dom.d.ts +3 -0
  22. package/dist/engine/dom.js +4 -0
  23. package/dist/engine/fillers/image.d.ts +6 -5
  24. package/dist/engine/fillers/image.js +24 -7
  25. package/dist/engine/generate.d.ts +2 -2
  26. package/dist/engine/generate.js +2 -2
  27. package/dist/engine/index.d.ts +3 -0
  28. package/dist/engine/index.js +2 -0
  29. package/dist/engine/notes.js +20 -62
  30. package/dist/engine/ooxml.d.ts +36 -0
  31. package/dist/engine/ooxml.js +56 -0
  32. package/dist/engine/types.d.ts +4 -0
  33. package/dist/index.d.ts +4 -11
  34. package/dist/index.js +2 -15
  35. package/dist/markdown/blocks/code.js +1 -2
  36. package/dist/markdown/blocks/image.d.ts +23 -0
  37. package/dist/markdown/blocks/image.js +65 -3
  38. package/dist/markdown/blocks/mermaid.d.ts +1 -1
  39. package/dist/markdown/blocks/mermaid.js +6 -4
  40. package/dist/markdown/blocks/table.js +7 -6
  41. package/dist/markdown/blocks/text.js +9 -8
  42. package/dist/markdown/deckCompiler.d.ts +2 -13
  43. package/dist/markdown/deckCompiler.js +7 -78
  44. package/dist/markdown/index.d.ts +4 -2
  45. package/dist/markdown/index.js +3 -1
  46. package/dist/markdown/inline.d.ts +2 -0
  47. package/dist/markdown/inline.js +4 -3
  48. package/dist/markdown/schema/themeConfigSchema.d.ts +2 -11
  49. package/dist/markdown/schema/themeConfigSchema.js +6 -16
  50. package/dist/markdown/slideParser.d.ts +2 -0
  51. package/dist/markdown/slideParser.js +6 -3
  52. package/dist/markdown/types.d.ts +4 -29
  53. package/dist/markdown/types.js +0 -11
  54. package/{theme-package/syntax.md → docs/markdown.md} +60 -28
  55. package/docs/theme.md +144 -0
  56. package/package.json +5 -4
  57. package/theme-package/SKILL.template.md +161 -0
  58. package/dist/engine/rels.d.ts +0 -18
  59. package/dist/engine/rels.js +0 -55
  60. package/dist/manifest.d.ts +0 -5
  61. package/dist/skillZip.d.ts +0 -49
  62. package/dist/skillZip.js +0 -177
  63. package/theme-package/SKILL.md +0 -226
@@ -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";
@@ -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 { DOMParser, XMLSerializer } from "@xmldom/xmldom";
16
+ import { XMLSerializer } from "@xmldom/xmldom";
17
17
  import { Attr, buildParagraph, buildRun, nextFreeRId as nextFreeRIdForDoc, Tag } from "./dom.js";
18
- // Package-level OOXML names not covered by dom.ts's `Tag`/`Attr` (which name
19
- // slide-XML tokens): `[Content_Types].xml` Override entries and the tail
20
- // segments of relationship-type URLs.
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 = new DOMParser().parseFromString(NOTES_SKELETON, "text/xml");
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(new DOMParser().parseFromString(relsXml, "text/xml"));
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 relsDoc = parse(await readString(archive, PRESENTATION_RELS_PART));
118
- const rels = relsDoc.getElementsByTagName(Tag.RELATIONSHIP);
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 = parse(await readString(archive, CONTENT_TYPES_PART));
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 = parse(await readString(archive, CONTENT_TYPES_PART));
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 = parse(await readString(archive, PRESENTATION_PART));
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 relsDoc = parse(await readString(archive, PRESENTATION_RELS_PART));
194
- const rels = relsDoc.getElementsByTagName(Tag.RELATIONSHIP);
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(resolveRelative("ppt", target));
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 rels = parse(await readString(archive, relsPath)).getElementsByTagName(Tag.RELATIONSHIP);
238
- for (let i = 0; i < rels.length; i++) {
239
- const target = rels[i].getAttribute(Attr.TARGET);
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 = parse(await readString(archive, CONTENT_TYPES_PART));
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 = parse(await readString(archive, slideRelsPath));
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 = parse(await readString(archive, slideRelsPath));
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
+ }
@@ -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 { ASSETS_ARCHIVE, ASSETS_FILE } from "./files.js";
38
- export { generateAssetCatalog, generateManifest } from "./manifest.js";
39
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, LoadedTheme, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
40
- 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(`Slide ${ctx.slideNo}: layout "${ctx.layoutName}" slot content (from ${ctx.source}) has a code fence ` +
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: `![alt](src "fit: cover")`.
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
- // `node.url` is the raw `$category.name` ref; `resolveAssetRef` validates and
7
- // resolves it (fail-fast on a malformed or unknown reference). `alt` is ignored.
8
- compile: async (node, ctx) => ctx.resolveAssetRef(node.url),
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: `![alt](src "fit: cover")`.
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: ![${title}](…)`;
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
- const require = createRequire(join(rootDir, "package.json"));
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(cellParagraph) : [],
15
- rows: body.map((row) => row.children.map(cellParagraph)),
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, { breakAsNewline: true }))) {
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 `${where} is a standalone "${nodeType}" block, which is not a supported content kind.`;
40
+ return `${ctx.region} is a standalone "${nodeType}" block, which is not a supported content kind.`;
40
41
  }
41
- return (`${where} mixes a "${nodeType}" block with other content; ` +
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, { breakAsNewline: true }))) {
59
+ for (const runs of splitRunsIntoParagraphs(walkPhrasingChildren(child.children, state))) {
59
60
  out.push({ runs, bullet: { level } });
60
61
  }
61
62
  }