@tycoworks/tycoslide 0.7.0 → 0.9.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 +6 -6
- package/SKILL.md +2 -1
- package/dist/cli.js +8 -107
- package/dist/engine/dom.d.ts +5 -0
- package/dist/engine/dom.js +19 -3
- package/dist/engine/fillers/filler.d.ts +21 -13
- package/dist/engine/fillers/filler.js +21 -27
- package/dist/engine/fillers/image.d.ts +46 -7
- package/dist/engine/fillers/image.js +78 -36
- package/dist/engine/fillers/table.d.ts +4 -3
- package/dist/engine/fillers/table.js +58 -9
- package/dist/engine/generate.d.ts +34 -18
- package/dist/engine/generate.js +234 -65
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/notes.d.ts +76 -0
- package/dist/engine/notes.js +313 -0
- package/dist/engine/types.d.ts +57 -24
- package/dist/engine/types.js +11 -7
- package/dist/index.d.ts +19 -25
- package/dist/index.js +65 -92
- package/dist/manifest.js +19 -29
- package/dist/markdown/blocks/code.d.ts +15 -0
- package/dist/markdown/blocks/code.js +50 -0
- package/dist/markdown/blocks/image.d.ts +2 -0
- package/dist/markdown/blocks/image.js +9 -0
- package/dist/markdown/blocks/mermaid.d.ts +15 -0
- package/dist/markdown/blocks/mermaid.js +227 -0
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
- package/dist/markdown/blocks/registry.d.ts +16 -0
- package/dist/markdown/blocks/registry.js +44 -0
- package/dist/markdown/blocks/table.d.ts +2 -0
- package/dist/markdown/blocks/table.js +23 -0
- package/dist/markdown/blocks/text.d.ts +12 -0
- package/dist/markdown/blocks/text.js +90 -0
- package/dist/markdown/deckCompiler.d.ts +18 -30
- package/dist/markdown/deckCompiler.js +167 -122
- package/dist/markdown/index.d.ts +11 -11
- package/dist/markdown/index.js +9 -8
- package/dist/markdown/inline.d.ts +26 -0
- package/dist/markdown/inline.js +136 -0
- package/dist/markdown/mdast.d.ts +25 -0
- package/dist/markdown/mdast.js +49 -0
- package/dist/markdown/schema/deckSchema.d.ts +30 -0
- package/dist/markdown/schema/deckSchema.js +51 -0
- package/dist/markdown/schema/strict.d.ts +9 -0
- package/dist/markdown/schema/strict.js +18 -0
- package/dist/markdown/schema/themeConfigSchema.d.ts +99 -0
- package/dist/markdown/schema/themeConfigSchema.js +145 -0
- package/dist/markdown/types.d.ts +184 -137
- package/dist/markdown/types.js +30 -19
- package/package.json +7 -3
- package/syntax.md +25 -5
- package/dist/markdown/parsers.d.ts +0 -32
- package/dist/markdown/parsers.js +0 -233
- package/dist/markdown/resolvers/code.d.ts +0 -17
- package/dist/markdown/resolvers/code.js +0 -44
- package/dist/markdown/resolvers/mermaid.d.ts +0 -14
- package/dist/markdown/resolvers/mermaid.js +0 -89
- package/dist/markdown/resolvers/resolver.d.ts +0 -42
- package/dist/markdown/resolvers/resolver.js +0 -52
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speaker-notes injection — an in-band pass run per slide during `write()`.
|
|
3
|
+
*
|
|
4
|
+
* The render library (pptx-automizer) has no public notes writer, but it does
|
|
5
|
+
* expose a general seam: `slide.modify((document, parent) => …)` runs while the
|
|
6
|
+
* deck is being written and hands us `parent.targetArchive` (the OUTPUT archive)
|
|
7
|
+
* and `parent.targetNumber` (the real output slide number). automizer's clone
|
|
8
|
+
* auto-copies the source slide's notes as `notesSlide${targetNumber}.xml` and
|
|
9
|
+
* wires its rels + content-type BEFORE our callback runs, so notes map 1:1 to the
|
|
10
|
+
* output slide number — no `sldIdLst`→rels→slideK mapping is needed.
|
|
11
|
+
*
|
|
12
|
+
* For each slide we either overwrite/synthesize its `notesSlide${N}.xml` (when the
|
|
13
|
+
* step authored notes) or strip any auto-copied part (when it did not, or notes
|
|
14
|
+
* are excluded) — so the designer's template notes never leak into the output.
|
|
15
|
+
*/
|
|
16
|
+
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
|
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.
|
|
21
|
+
const CT_OVERRIDE = "Override";
|
|
22
|
+
const CT_PART_NAME = "PartName";
|
|
23
|
+
const CT_CONTENT_TYPE = "ContentType";
|
|
24
|
+
const REL_TYPE_SUFFIX = {
|
|
25
|
+
NotesSlide: "/notesSlide",
|
|
26
|
+
NotesMaster: "/notesMaster",
|
|
27
|
+
};
|
|
28
|
+
const NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
29
|
+
const NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main";
|
|
30
|
+
const NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
31
|
+
const NS_REL = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
32
|
+
const NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
33
|
+
const REL_TYPE = {
|
|
34
|
+
NotesSlide: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide",
|
|
35
|
+
Slide: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
|
|
36
|
+
NotesMaster: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster",
|
|
37
|
+
};
|
|
38
|
+
const NOTES_SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml";
|
|
39
|
+
const XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n';
|
|
40
|
+
const CONTENT_TYPES_PART = "[Content_Types].xml";
|
|
41
|
+
const PRESENTATION_RELS_PART = "ppt/_rels/presentation.xml.rels";
|
|
42
|
+
const NOTES_SKELETON = `<p:notes xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld><p:spTree>` +
|
|
43
|
+
`<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>` +
|
|
44
|
+
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="Notes Placeholder 1"/>` +
|
|
45
|
+
`<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr>` +
|
|
46
|
+
`<p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/></p:txBody></p:sp>` +
|
|
47
|
+
`</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>`;
|
|
48
|
+
/**
|
|
49
|
+
* Build a minimal `<p:notes>` part whose body placeholder carries one `<a:p>`
|
|
50
|
+
* per line of `notes`. Text goes through DOM text nodes, so `& < > "` escaping
|
|
51
|
+
* is handled by the serializer.
|
|
52
|
+
*/
|
|
53
|
+
export function buildNotesSlideXml(notes) {
|
|
54
|
+
const doc = new DOMParser().parseFromString(NOTES_SKELETON, "text/xml");
|
|
55
|
+
const txBody = doc.getElementsByTagName("p:txBody")[0];
|
|
56
|
+
for (const line of notes.replace(/\r?\n+$/, "").split(/\r?\n/)) {
|
|
57
|
+
// Reuse the engine's run/paragraph builders — they set xml:space="preserve"
|
|
58
|
+
// for leading/trailing whitespace. The skeleton declares xmlns:a, so the
|
|
59
|
+
// builders' "a:"-prefixed element creation serializes correctly on this doc.
|
|
60
|
+
txBody.appendChild(buildParagraph(doc, null, buildRun(doc, null, line)));
|
|
61
|
+
}
|
|
62
|
+
return serialize(doc);
|
|
63
|
+
}
|
|
64
|
+
/** Next unused `rIdN` in a `.rels` XML string (max existing id + 1). */
|
|
65
|
+
export function nextFreeRId(relsXml) {
|
|
66
|
+
return nextFreeRIdForDoc(new DOMParser().parseFromString(relsXml, "text/xml"));
|
|
67
|
+
}
|
|
68
|
+
function serialize(doc) {
|
|
69
|
+
// xmldom re-emits any `<?xml?>` declaration present in the parsed source, so a
|
|
70
|
+
// doc parsed from an already-declared part would serialize with two of them.
|
|
71
|
+
// Strip any leading declaration, then prepend exactly one canonical XML_DECL.
|
|
72
|
+
const body = new XMLSerializer().serializeToString(doc).replace(/^<\?xml[^>]*\?>\s*/, "");
|
|
73
|
+
return XML_DECL + body;
|
|
74
|
+
}
|
|
75
|
+
function parse(xml) {
|
|
76
|
+
return new DOMParser().parseFromString(xml, "text/xml");
|
|
77
|
+
}
|
|
78
|
+
async function readString(archive, file) {
|
|
79
|
+
const raw = await archive.read(file, "string");
|
|
80
|
+
return typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
81
|
+
}
|
|
82
|
+
function appendRel(relsDoc, id, type, target) {
|
|
83
|
+
const rel = relsDoc.createElementNS(NS_REL, Tag.RELATIONSHIP);
|
|
84
|
+
rel.setAttribute(Attr.ID, id);
|
|
85
|
+
rel.setAttribute(Attr.TYPE, type);
|
|
86
|
+
rel.setAttribute(Attr.TARGET, target);
|
|
87
|
+
relsDoc.documentElement.appendChild(rel);
|
|
88
|
+
}
|
|
89
|
+
/** Remove the (single) notesSlide relationship from a slide's rels doc, if present. */
|
|
90
|
+
function removeNotesRel(relsDoc) {
|
|
91
|
+
const rels = relsDoc.getElementsByTagName(Tag.RELATIONSHIP);
|
|
92
|
+
for (let i = 0; i < rels.length; i++) {
|
|
93
|
+
if (rels[i].getAttribute(Attr.TYPE) === REL_TYPE.NotesSlide) {
|
|
94
|
+
rels[i].parentNode?.removeChild(rels[i]);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function notesRelsXml(slideNumber, masterTarget) {
|
|
101
|
+
return (XML_DECL +
|
|
102
|
+
`<Relationships xmlns="${NS_REL}">` +
|
|
103
|
+
`<Relationship Id="rId1" Type="${REL_TYPE.Slide}" Target="../slides/slide${slideNumber}.xml"/>` +
|
|
104
|
+
`<Relationship Id="rId2" Type="${REL_TYPE.NotesMaster}" Target="${masterTarget}"/>` +
|
|
105
|
+
`</Relationships>`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The notes master's target relative to `ppt/notesSlides/` (what a notesSlide's
|
|
109
|
+
* rels needs), read from the package's `presentation.xml.rels`. That file's
|
|
110
|
+
* targets are relative to `ppt/` (e.g. `notesMasters/notesMaster1.xml`), so a
|
|
111
|
+
* notesSlide part one directory deeper prefixes `../`. Returns undefined when
|
|
112
|
+
* the presentation declares no notes master relationship.
|
|
113
|
+
*/
|
|
114
|
+
async function findNotesMaster(archive) {
|
|
115
|
+
if (!archive.fileExists(PRESENTATION_RELS_PART))
|
|
116
|
+
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;
|
|
125
|
+
}
|
|
126
|
+
function findOverride(ctDoc, partName) {
|
|
127
|
+
const overrides = ctDoc.getElementsByTagName(CT_OVERRIDE);
|
|
128
|
+
for (let i = 0; i < overrides.length; i++) {
|
|
129
|
+
if (overrides[i].getAttribute(CT_PART_NAME) === partName)
|
|
130
|
+
return overrides[i];
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
async function addContentTypeOverride(archive, partName) {
|
|
135
|
+
const ctDoc = parse(await readString(archive, CONTENT_TYPES_PART));
|
|
136
|
+
if (findOverride(ctDoc, partName))
|
|
137
|
+
return;
|
|
138
|
+
const override = ctDoc.createElementNS(NS_CT, CT_OVERRIDE);
|
|
139
|
+
override.setAttribute(CT_PART_NAME, partName);
|
|
140
|
+
override.setAttribute(CT_CONTENT_TYPE, NOTES_SLIDE_CONTENT_TYPE);
|
|
141
|
+
ctDoc.documentElement.appendChild(override);
|
|
142
|
+
await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
|
|
143
|
+
}
|
|
144
|
+
/** Remove a part's `<Override>` from an already-parsed `[Content_Types].xml` doc. */
|
|
145
|
+
function removeOverrideFromDoc(ctDoc, partName) {
|
|
146
|
+
const override = findOverride(ctDoc, partName);
|
|
147
|
+
if (!override)
|
|
148
|
+
return false;
|
|
149
|
+
override.parentNode?.removeChild(override);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
async function removeContentTypeOverride(archive, partName) {
|
|
153
|
+
const ctDoc = parse(await readString(archive, CONTENT_TYPES_PART));
|
|
154
|
+
if (!removeOverrideFromDoc(ctDoc, partName))
|
|
155
|
+
return;
|
|
156
|
+
await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
|
|
157
|
+
}
|
|
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
|
+
const NOTES_SLIDE_PART = /^ppt\/notesSlides\/notesSlide\d+\.xml$/;
|
|
172
|
+
const PRESENTATION_PART = "ppt/presentation.xml";
|
|
173
|
+
/**
|
|
174
|
+
* The slide parts a deck actually presents, in `<p:sldIdLst>` order, resolved via
|
|
175
|
+
* `presentation.xml.rels`. Returns full archive paths (e.g. `ppt/slides/slide1.xml`).
|
|
176
|
+
*
|
|
177
|
+
* This deliberately does NOT enumerate `ppt/slides/_rels`: when automizer builds a
|
|
178
|
+
* deck it removes the template's original slides from `<p:sldIdLst>` first, but
|
|
179
|
+
* leaves their physical `slideN.xml`/`slideN.xml.rels` parts in the archive until
|
|
180
|
+
* a later cleanup pass. Those orphaned slide rels still carry `/notesSlide`
|
|
181
|
+
* relationships, so folder-enumeration would wrongly count template notes as
|
|
182
|
+
* referenced. Only slides reachable from `presentation.xml` are live.
|
|
183
|
+
*/
|
|
184
|
+
async function liveSlideParts(archive) {
|
|
185
|
+
const pres = parse(await readString(archive, PRESENTATION_PART));
|
|
186
|
+
const sldIds = pres.getElementsByTagName("p:sldId");
|
|
187
|
+
const rIds = [];
|
|
188
|
+
for (let i = 0; i < sldIds.length; i++) {
|
|
189
|
+
const rid = sldIds[i].getAttributeNS(NS_R, "id") || sldIds[i].getAttribute(Attr.HLINK_ID);
|
|
190
|
+
if (rid)
|
|
191
|
+
rIds.push(rid);
|
|
192
|
+
}
|
|
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
|
+
}
|
|
202
|
+
const parts = [];
|
|
203
|
+
for (const rid of rIds) {
|
|
204
|
+
const target = targetById.get(rid);
|
|
205
|
+
if (target)
|
|
206
|
+
parts.push(resolveRelative("ppt", target));
|
|
207
|
+
}
|
|
208
|
+
return parts;
|
|
209
|
+
}
|
|
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
|
+
/**
|
|
215
|
+
* Remove every notesSlide part that no live slide references. automizer drops the
|
|
216
|
+
* template's orphaned *slides* from the deck when building it but leaves all their
|
|
217
|
+
* `notesSlides` behind as unreferenced parts (plus their `[Content_Types].xml`
|
|
218
|
+
* Overrides), so the designer's private notes would otherwise leak into every
|
|
219
|
+
* output. Run once as a presentation-level pass, after the deck is assembled.
|
|
220
|
+
*
|
|
221
|
+
* A notesSlide is "referenced" iff a LIVE slide (reachable from `presentation.xml`,
|
|
222
|
+
* see {@link liveSlideParts}) carries a `/notesSlide` relationship pointing at it.
|
|
223
|
+
* Everything else is swept: the part, its rels, and its content-type Override (all
|
|
224
|
+
* Override removals batched into a single `[Content_Types].xml` read+write).
|
|
225
|
+
*/
|
|
226
|
+
export async function sweepOrphanNotes(archive) {
|
|
227
|
+
const notesParts = (await archive.folder("ppt/notesSlides"))
|
|
228
|
+
.map((entry) => entry.name)
|
|
229
|
+
.filter((name) => NOTES_SLIDE_PART.test(name));
|
|
230
|
+
if (notesParts.length === 0)
|
|
231
|
+
return;
|
|
232
|
+
const referenced = new Set();
|
|
233
|
+
for (const slidePart of await liveSlideParts(archive)) {
|
|
234
|
+
const relsPath = relsPathFor(slidePart);
|
|
235
|
+
if (!archive.fileExists(relsPath))
|
|
236
|
+
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
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const orphans = notesParts.filter((part) => !referenced.has(part));
|
|
246
|
+
if (orphans.length === 0)
|
|
247
|
+
return;
|
|
248
|
+
const ctDoc = parse(await readString(archive, CONTENT_TYPES_PART));
|
|
249
|
+
let ctChanged = false;
|
|
250
|
+
for (const part of orphans) {
|
|
251
|
+
await archive.remove(part);
|
|
252
|
+
const relsPart = `ppt/notesSlides/_rels/${part.slice(part.lastIndexOf("/") + 1)}.rels`;
|
|
253
|
+
if (archive.fileExists(relsPart))
|
|
254
|
+
await archive.remove(relsPart);
|
|
255
|
+
if (removeOverrideFromDoc(ctDoc, `/${part}`))
|
|
256
|
+
ctChanged = true;
|
|
257
|
+
}
|
|
258
|
+
if (ctChanged)
|
|
259
|
+
await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Set (or strip) the speaker notes on one output slide, keyed on its real output
|
|
263
|
+
* `slideNumber` (`parent.targetNumber`). Runs inside `slide.modify` during
|
|
264
|
+
* `write()`, operating on the OUTPUT archive.
|
|
265
|
+
*
|
|
266
|
+
* - Authored notes (non-empty and `!excludeNotes`): if automizer auto-copied a
|
|
267
|
+
* notes part (`notesSlide${N}.xml` already exists), overwrite its text only —
|
|
268
|
+
* its rels and content-type are already correct. Otherwise synthesize the part,
|
|
269
|
+
* its rels (→slide + →master), a `notesSlide` rel on the slide (next free rId),
|
|
270
|
+
* and the `[Content_Types].xml` override.
|
|
271
|
+
* - No notes (or excluded): if a part was auto-copied, remove it, its rels, the
|
|
272
|
+
* slide's `notesSlide` rel, and the content-type override — so the designer's
|
|
273
|
+
* template notes never leak.
|
|
274
|
+
*
|
|
275
|
+
* Throws (no silent default) if a slide needs a NEW notes part but the template
|
|
276
|
+
* ships no notes master — synthesizing one is out of scope.
|
|
277
|
+
*/
|
|
278
|
+
export async function applyNotesToSlide(archive, slideNumber, notes, excludeNotes) {
|
|
279
|
+
const notesPath = `ppt/notesSlides/notesSlide${slideNumber}.xml`;
|
|
280
|
+
const notesRelsPath = `ppt/notesSlides/_rels/notesSlide${slideNumber}.xml.rels`;
|
|
281
|
+
const slideRelsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
282
|
+
const notesExists = archive.fileExists(notesPath);
|
|
283
|
+
const shouldWrite = notes !== undefined && notes !== "" && !excludeNotes;
|
|
284
|
+
if (shouldWrite) {
|
|
285
|
+
if (notesExists) {
|
|
286
|
+
// Auto-copied part: overwrite its body; leave rels + content-type as wired.
|
|
287
|
+
await archive.write(notesPath, buildNotesSlideXml(notes));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const masterTarget = await findNotesMaster(archive);
|
|
291
|
+
if (masterTarget === undefined) {
|
|
292
|
+
throw new Error(`Speaker notes: slide ${slideNumber} has notes, but the template ships no notes master ` +
|
|
293
|
+
"(ppt/notesMasters/). Add a notes master to the template (PowerPoint: View → Notes Master), " +
|
|
294
|
+
"or build with notes disabled. Synthesizing a notes master is not supported.");
|
|
295
|
+
}
|
|
296
|
+
await archive.write(notesPath, buildNotesSlideXml(notes));
|
|
297
|
+
await archive.write(notesRelsPath, notesRelsXml(slideNumber, masterTarget));
|
|
298
|
+
const relsDoc = parse(await readString(archive, slideRelsPath));
|
|
299
|
+
appendRel(relsDoc, nextFreeRIdForDoc(relsDoc), REL_TYPE.NotesSlide, `../notesSlides/notesSlide${slideNumber}.xml`);
|
|
300
|
+
await archive.write(slideRelsPath, serialize(relsDoc));
|
|
301
|
+
await addContentTypeOverride(archive, `/${notesPath}`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!notesExists)
|
|
305
|
+
return;
|
|
306
|
+
await archive.remove(notesPath);
|
|
307
|
+
if (archive.fileExists(notesRelsPath))
|
|
308
|
+
await archive.remove(notesRelsPath);
|
|
309
|
+
const relsDoc = parse(await readString(archive, slideRelsPath));
|
|
310
|
+
if (removeNotesRel(relsDoc))
|
|
311
|
+
await archive.write(slideRelsPath, serialize(relsDoc));
|
|
312
|
+
await removeContentTypeOverride(archive, `/${notesPath}`);
|
|
313
|
+
}
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/** How the engine should scale an image inside its picture frame. */
|
|
2
|
-
export declare const FitMode: {
|
|
3
|
-
/** Shrink the picture frame to the image's aspect ratio, centered. */
|
|
4
|
-
readonly Contain: "contain";
|
|
5
|
-
/** Fill the frame; center-crop overflow via srcRect. */
|
|
6
|
-
readonly Cover: "cover";
|
|
7
|
-
};
|
|
8
|
-
export type FitMode = (typeof FitMode)[keyof typeof FitMode];
|
|
9
1
|
/**
|
|
10
2
|
* Fill-strategy discriminator carried on every Slot. Required — there is no
|
|
11
3
|
* silent default. The engine dispatches to fillTemplate / fillText / fillTable /
|
|
@@ -80,37 +72,78 @@ export type TableFill = {
|
|
|
80
72
|
rows: StyledParagraph[][];
|
|
81
73
|
};
|
|
82
74
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
75
|
+
* How a picture is scaled into its frame — the engine's image-sizing directive,
|
|
76
|
+
* mirroring CSS `object-fit`. `contain`: fit the whole image, scale both ways,
|
|
77
|
+
* letterbox. `cover`: fill the frame, centre-crop the overflow. `scale-down`:
|
|
78
|
+
* like contain but never enlarge past native (a small image sits at native size).
|
|
79
|
+
*/
|
|
80
|
+
export declare const ImageFit: {
|
|
81
|
+
readonly Contain: "contain";
|
|
82
|
+
readonly Cover: "cover";
|
|
83
|
+
readonly ScaleDown: "scale-down";
|
|
84
|
+
};
|
|
85
|
+
export type ImageFit = (typeof ImageFit)[keyof typeof ImageFit];
|
|
86
|
+
/**
|
|
87
|
+
* Input to fillImage — a resolved image path plus its object-fit directive.
|
|
88
|
+
* `path` must be absolute; the compiler resolves it (and maps the asset's
|
|
89
|
+
* semantic type to a `fit`) before the ImageFill reaches the engine.
|
|
87
90
|
*/
|
|
88
91
|
export type ImageFill = {
|
|
89
92
|
type: typeof SlotType.Image;
|
|
90
93
|
path: string;
|
|
91
|
-
fit:
|
|
94
|
+
fit: ImageFit;
|
|
92
95
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
/** A shape's absolute position and size, in EMU — the slot's frame. */
|
|
97
|
+
export type Frame = {
|
|
98
|
+
x: number;
|
|
99
|
+
y: number;
|
|
100
|
+
cx: number;
|
|
101
|
+
cy: number;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* A kind of content a slot accepts, and the real template shape that realizes
|
|
105
|
+
* it. `type` is the fill-strategy discriminator; `shapeName` names the shape on
|
|
106
|
+
* `sourceSlide` that carries the specimen styling. When `sourceSlide` equals the
|
|
107
|
+
* layout's `baseSlide` the shape is already on the cloned slide (fill in place);
|
|
108
|
+
* otherwise the shape is transplanted from `sourceSlide` into the slot's frame.
|
|
109
|
+
* `startAt` is a text-specimen concern (leave the first N specimen paragraphs
|
|
110
|
+
* untouched) and only meaningful on a text block.
|
|
111
|
+
*
|
|
112
|
+
* Named `Block` — a kind of content (image / table / text) the way an author
|
|
113
|
+
* thinks of it. Distinct from the compiler's `MarkdownBlock` (a parsed markdown
|
|
114
|
+
* block); different layer, kept separate on purpose.
|
|
115
|
+
*/
|
|
116
|
+
export type Block = {
|
|
97
117
|
type: SlotType;
|
|
98
|
-
|
|
118
|
+
sourceSlide: number;
|
|
119
|
+
shapeName: string;
|
|
99
120
|
startAt?: number;
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* An author-facing fill region. Not welded to one shape+type: a slot owns its
|
|
124
|
+
* `frame` and `accepts` a set of `Block`s; the supplied value's shape selects
|
|
125
|
+
* which block fills. A block whose `sourceSlide === baseSlide` fills in place;
|
|
126
|
+
* any other block is transplanted into the slot's `frame`.
|
|
127
|
+
*/
|
|
128
|
+
export type Slot = {
|
|
129
|
+
key: string;
|
|
130
|
+
frame: Frame;
|
|
131
|
+
accepts: Block[];
|
|
102
132
|
};
|
|
103
133
|
export type Layout = {
|
|
104
134
|
name: string;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
/**
|
|
136
|
+
* The template slide cloned for chrome/background. A block whose `sourceSlide`
|
|
137
|
+
* equals this is filled in place; any other block is transplanted onto the clone.
|
|
138
|
+
*/
|
|
139
|
+
baseSlide: number;
|
|
109
140
|
slots: Slot[];
|
|
110
141
|
};
|
|
111
142
|
export type DeckStep = {
|
|
112
143
|
layout: string;
|
|
113
144
|
content?: Record<string, TextFill | TableFill | ImageFill | TemplateFill>;
|
|
145
|
+
/** Slide-level speaker notes — plain text, one paragraph per line. Not a slot. */
|
|
146
|
+
notes?: string;
|
|
114
147
|
};
|
|
115
148
|
export type Deck = {
|
|
116
149
|
theme: string;
|
package/dist/engine/types.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/** How the engine should scale an image inside its picture frame. */
|
|
2
|
-
export const FitMode = {
|
|
3
|
-
/** Shrink the picture frame to the image's aspect ratio, centered. */
|
|
4
|
-
Contain: "contain",
|
|
5
|
-
/** Fill the frame; center-crop overflow via srcRect. */
|
|
6
|
-
Cover: "cover",
|
|
7
|
-
};
|
|
8
1
|
/**
|
|
9
2
|
* Fill-strategy discriminator carried on every Slot. Required — there is no
|
|
10
3
|
* silent default. The engine dispatches to fillTemplate / fillText / fillTable /
|
|
@@ -20,3 +13,14 @@ export const SlotType = {
|
|
|
20
13
|
/** fillImage: swap the picture's blip and adjust frame geometry. */
|
|
21
14
|
Image: "image",
|
|
22
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* How a picture is scaled into its frame — the engine's image-sizing directive,
|
|
18
|
+
* mirroring CSS `object-fit`. `contain`: fit the whole image, scale both ways,
|
|
19
|
+
* letterbox. `cover`: fill the frame, centre-crop the overflow. `scale-down`:
|
|
20
|
+
* like contain but never enlarge past native (a small image sits at native size).
|
|
21
|
+
*/
|
|
22
|
+
export const ImageFit = {
|
|
23
|
+
Contain: "contain",
|
|
24
|
+
Cover: "cover",
|
|
25
|
+
ScaleDown: "scale-down",
|
|
26
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import { type Config, type ThemeConfig } from "./engine/index.js";
|
|
2
|
-
import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig
|
|
3
|
-
/**
|
|
4
|
-
* Run every compiler-owned resolver over `deck` (highlight code fences,
|
|
5
|
-
* render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
|
|
6
|
-
* values are narrowed to the engine's `TextFill | TableFill | ImageFill |
|
|
7
|
-
* TemplateFill` union. Structurally equivalent to the engine's `Deck` — a
|
|
8
|
-
* caller passes the returned value straight to `generate()` with no cast.
|
|
9
|
-
*
|
|
10
|
-
* Fails fast if `deck.output` is missing: downstream `generate()` requires it,
|
|
11
|
-
* and the CLI populates it before calling `buildDeck`; a programmatic caller
|
|
12
|
-
* that forgot to set it hits the error here instead of a confusing engine-side
|
|
13
|
-
* failure.
|
|
14
|
-
*/
|
|
15
|
-
export declare function resolveDeck(deck: CompilerDeck, config: CompilerConfig): Promise<ResolvedCompilerDeck>;
|
|
1
|
+
import { type Config, type GenerateOptions, type ThemeConfig } from "./engine/index.js";
|
|
2
|
+
import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig } from "./markdown/types.js";
|
|
16
3
|
/**
|
|
17
4
|
* Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
|
|
18
5
|
* Fields are copied cell-by-cell so the boundary is explicit — no casts.
|
|
@@ -28,19 +15,26 @@ export declare function toEngineThemeConfig(config: CompilerThemeConfig): ThemeC
|
|
|
28
15
|
*/
|
|
29
16
|
export declare function toEngineConfig(config: CompilerConfig): Config;
|
|
30
17
|
/**
|
|
31
|
-
* End-to-end build:
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
18
|
+
* End-to-end build: `compileDeck` already produced engine-shaped content (code
|
|
19
|
+
* highlighted, mermaid rendered), so `buildDeck` only asserts an `output` is set
|
|
20
|
+
* and hands the deck to the engine's primitives-only `generate()`. The deck is
|
|
21
|
+
* structurally equivalent to the engine's `Deck` once `output` is present, so no
|
|
22
|
+
* cast is required. `buildDeck` does not itself validate `config` — a
|
|
23
|
+
* programmatic caller assembling a `CompilerConfig` by hand should load it
|
|
24
|
+
* through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
|
|
25
|
+
* fail-fast structural checks the CLI gets.
|
|
26
|
+
*
|
|
27
|
+
* Fails fast if `deck.output` is missing: `generate()` requires it, and the CLI
|
|
28
|
+
* populates it before calling `buildDeck`; a programmatic caller that forgot to
|
|
29
|
+
* set it hits this error instead of a confusing engine-side failure.
|
|
36
30
|
*
|
|
37
31
|
* Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
|
|
38
32
|
* post-write cleanup is needed.
|
|
39
33
|
*/
|
|
40
|
-
export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig): Promise<void>;
|
|
41
|
-
export type { Config, Deck, DeckStep, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
|
|
42
|
-
export {
|
|
34
|
+
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";
|
|
43
37
|
export type { ManifestOptions } from "./manifest.js";
|
|
44
38
|
export { generateManifest } from "./manifest.js";
|
|
45
|
-
export type { AssetCatalog, AssetEntry,
|
|
46
|
-
export {
|
|
39
|
+
export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, Limit, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
|
|
40
|
+
export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
|