ppt-template-reuse 0.4.0-rc.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +8 -0
  3. package/README.md +233 -0
  4. package/adapters/README.md +13 -0
  5. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  6. package/adapters/claude/.mcp.json +8 -0
  7. package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
  8. package/adapters/kimi/kimi.plugin.json +12 -0
  9. package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
  10. package/adapters/kimi-work/mcp.json +8 -0
  11. package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
  12. package/adapters/workbuddy/mcp.json +8 -0
  13. package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
  14. package/package.json +53 -0
  15. package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
  16. package/plugins/ppt-template-reuse/.mcp.json +9 -0
  17. package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
  18. package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
  19. package/skills/ppt-template-reuse/SKILL.md +105 -0
  20. package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
  21. package/src/cli/ppt-reuse.mjs +203 -0
  22. package/src/core/adaptive-planner.mjs +1192 -0
  23. package/src/core/content-audit.mjs +573 -0
  24. package/src/core/frame-map.mjs +91 -0
  25. package/src/core/index.mjs +38 -0
  26. package/src/core/io.mjs +81 -0
  27. package/src/core/legacy-capsule-import.mjs +382 -0
  28. package/src/core/ooxml-editor.mjs +753 -0
  29. package/src/core/paths.mjs +60 -0
  30. package/src/core/planner-lib.mjs +59 -0
  31. package/src/core/pptx-package.mjs +227 -0
  32. package/src/core/renderer.mjs +402 -0
  33. package/src/core/source-extractor.mjs +412 -0
  34. package/src/core/template-inspector.mjs +489 -0
  35. package/src/core/template-intelligence.mjs +675 -0
  36. package/src/core/universal-engine.mjs +1075 -0
  37. package/src/index.mjs +2 -0
  38. package/src/install/installer.mjs +293 -0
  39. package/src/mcp/server.mjs +377 -0
@@ -0,0 +1,753 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ attr,
6
+ attrQName,
7
+ elements,
8
+ loadPptx,
9
+ localName,
10
+ parseXml,
11
+ relationshipPart,
12
+ relationships,
13
+ serializeXml,
14
+ shapeRecords,
15
+ slideRecords,
16
+ textOf,
17
+ xmlFrom,
18
+ } from "./pptx-package.mjs";
19
+ import { fingerprint, sha256File } from "./io.mjs";
20
+
21
+ const REL_NS =
22
+ "http://schemas.openxmlformats.org/package/2006/relationships";
23
+ const P_NS =
24
+ "http://schemas.openxmlformats.org/presentationml/2006/main";
25
+ const A_NS =
26
+ "http://schemas.openxmlformats.org/drawingml/2006/main";
27
+ const R_NS =
28
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
29
+ const CT_NS =
30
+ "http://schemas.openxmlformats.org/package/2006/content-types";
31
+ const FIXED_DATE = new Date("2000-01-01T00:00:00.000Z");
32
+
33
+ function xmlEscape(value) {
34
+ return String(value ?? "")
35
+ .replace(/&/g, "&")
36
+ .replace(/</g, "&lt;")
37
+ .replace(/>/g, "&gt;")
38
+ .replace(/"/g, "&quot;")
39
+ .replace(/'/g, "&apos;");
40
+ }
41
+
42
+ function relationshipDocument(document) {
43
+ return (
44
+ document ||
45
+ parseXml(
46
+ `<Relationships xmlns="${REL_NS}"></Relationships>`,
47
+ )
48
+ );
49
+ }
50
+
51
+ function contentTypesDocument(xml) {
52
+ return parseXml(xml);
53
+ }
54
+
55
+ function ensureOverride(contentTypes, partName, contentType) {
56
+ const normalized = partName.startsWith("/") ? partName : `/${partName}`;
57
+ const existing = elements(contentTypes, "Override").find(
58
+ (node) => attr(node, "PartName") === normalized,
59
+ );
60
+ if (existing) {
61
+ existing.setAttribute("ContentType", contentType);
62
+ return;
63
+ }
64
+ const node = contentTypes.createElementNS(CT_NS, "Override");
65
+ node.setAttribute("PartName", normalized);
66
+ node.setAttribute("ContentType", contentType);
67
+ contentTypes.documentElement.appendChild(node);
68
+ }
69
+
70
+ function ensureDefault(contentTypes, extension, contentType) {
71
+ const existing = elements(contentTypes, "Default").find(
72
+ (node) => attr(node, "Extension") === extension,
73
+ );
74
+ if (existing) return;
75
+ const node = contentTypes.createElementNS(CT_NS, "Default");
76
+ node.setAttribute("Extension", extension);
77
+ node.setAttribute("ContentType", contentType);
78
+ contentTypes.documentElement.appendChild(node);
79
+ }
80
+
81
+ function removeOverrides(contentTypes, predicate) {
82
+ for (const node of elements(contentTypes, "Override")) {
83
+ if (predicate(String(attr(node, "PartName") || ""))) {
84
+ node.parentNode.removeChild(node);
85
+ }
86
+ }
87
+ }
88
+
89
+ function pruneOrphanedSlideParts(zip, contentTypes, outputSlideCount) {
90
+ const removed = [];
91
+ const shouldRemove = (name) => {
92
+ const slide = name.match(
93
+ /^ppt\/slides\/(?:_rels\/)?slide(\d+)\.xml(?:\.rels)?$/,
94
+ );
95
+ if (slide) return Number(slide[1]) > outputSlideCount;
96
+ const notes = name.match(
97
+ /^ppt\/notesSlides\/(?:_rels\/)?notesSlide(\d+)\.xml(?:\.rels)?$/,
98
+ );
99
+ return notes ? Number(notes[1]) > outputSlideCount : false;
100
+ };
101
+ for (const name of Object.keys(zip.files)) {
102
+ if (!shouldRemove(name)) continue;
103
+ zip.remove(name);
104
+ removed.push(name);
105
+ }
106
+ removeOverrides(contentTypes, (partName) => {
107
+ const normalized = partName.replace(/^\//, "");
108
+ return shouldRemove(normalized);
109
+ });
110
+ return removed.sort();
111
+ }
112
+
113
+ function relationshipById(document, relationshipId) {
114
+ return elements(document, "Relationship").find(
115
+ (node) => attr(node, "Id") === relationshipId,
116
+ );
117
+ }
118
+
119
+ function removeRelationshipsByType(document, suffix) {
120
+ for (const node of elements(document, "Relationship")) {
121
+ if (String(attr(node, "Type") || "").endsWith(suffix)) {
122
+ node.parentNode.removeChild(node);
123
+ }
124
+ }
125
+ }
126
+
127
+ function appendRelationship(document, { id, type, target }) {
128
+ const node = document.createElementNS(REL_NS, "Relationship");
129
+ node.setAttribute("Id", id);
130
+ node.setAttribute("Type", type);
131
+ node.setAttribute("Target", target);
132
+ document.documentElement.appendChild(node);
133
+ return node;
134
+ }
135
+
136
+ function findShapeNode(slideDocument, target) {
137
+ const shapes = shapeRecords(slideDocument);
138
+ const match =
139
+ shapes.find(
140
+ (shape) =>
141
+ target.sourceShapeId !== undefined &&
142
+ String(shape.id) === String(target.sourceShapeId),
143
+ ) ||
144
+ shapes.find(
145
+ (shape) =>
146
+ target.sourceElementName &&
147
+ shape.name === target.sourceElementName,
148
+ );
149
+ if (!match) {
150
+ throw new Error(
151
+ `Cannot resolve ${target.slotId} by shape id/name on source slide`,
152
+ );
153
+ }
154
+ return match;
155
+ }
156
+
157
+ function rewriteText(shape, value) {
158
+ let textNodes = elements(shape.node, "t");
159
+ if (!textNodes.length) {
160
+ const textBody = elements(shape.node, "txBody")[0];
161
+ if (!textBody) {
162
+ throw new Error(`Shape ${shape.id} is not an editable text shape`);
163
+ }
164
+ let paragraph = elements(textBody, "p")[0];
165
+ if (!paragraph) {
166
+ paragraph = shape.node.ownerDocument.createElementNS(A_NS, "a:p");
167
+ textBody.appendChild(paragraph);
168
+ }
169
+ const run = shape.node.ownerDocument.createElementNS(A_NS, "a:r");
170
+ const textNode = shape.node.ownerDocument.createElementNS(A_NS, "a:t");
171
+ const endProperties = elements(paragraph, "endParaRPr")[0];
172
+ run.appendChild(textNode);
173
+ if (endProperties) paragraph.insertBefore(run, endProperties);
174
+ else paragraph.appendChild(run);
175
+ textNodes = [textNode];
176
+ }
177
+ textNodes[0].textContent = String(value ?? "");
178
+ textNodes[0].setAttribute("xml:space", "preserve");
179
+ for (const node of textNodes.slice(1)) node.textContent = "";
180
+ }
181
+
182
+ function replaceTable(shape, value) {
183
+ const rows = value?.values;
184
+ if (!Array.isArray(rows)) {
185
+ throw new Error(`Table target ${shape.id} requires value.values`);
186
+ }
187
+ const cells = elements(shape.node, "tc");
188
+ const flat = rows.flat();
189
+ if (flat.length !== cells.length) {
190
+ throw new Error(
191
+ `Table target ${shape.id} has ${cells.length} cells but received ${flat.length}`,
192
+ );
193
+ }
194
+ cells.forEach((cell, index) => {
195
+ const textNodes = elements(cell, "t");
196
+ if (!textNodes.length) throw new Error(`Table cell ${index + 1} has no text run`);
197
+ textNodes[0].textContent = String(flat[index] ?? "");
198
+ for (const node of textNodes.slice(1)) node.textContent = "";
199
+ });
200
+ }
201
+
202
+ function chartSeriesNodes(chartDocument) {
203
+ return elements(chartDocument, "ser").filter((node) =>
204
+ ["barChart", "lineChart", "pieChart", "areaChart", "scatterChart"].includes(
205
+ localName(node.parentNode),
206
+ ),
207
+ );
208
+ }
209
+
210
+ function replaceCache(cacheParent, values, type) {
211
+ const cache =
212
+ elements(cacheParent, type === "number" ? "numCache" : "strCache")[0];
213
+ if (!cache) return false;
214
+ for (const point of elements(cache, "pt")) point.parentNode.removeChild(point);
215
+ const pointCount = elements(cache, "ptCount")[0];
216
+ if (pointCount) pointCount.setAttribute("val", String(values.length));
217
+ values.forEach((value, index) => {
218
+ const point = cache.ownerDocument.createElementNS(
219
+ "http://schemas.openxmlformats.org/drawingml/2006/chart",
220
+ "c:pt",
221
+ );
222
+ point.setAttribute("idx", String(index));
223
+ const valueNode = cache.ownerDocument.createElementNS(
224
+ "http://schemas.openxmlformats.org/drawingml/2006/chart",
225
+ "c:v",
226
+ );
227
+ valueNode.appendChild(
228
+ cache.ownerDocument.createTextNode(String(value ?? "")),
229
+ );
230
+ point.appendChild(valueNode);
231
+ cache.appendChild(point);
232
+ });
233
+ return true;
234
+ }
235
+
236
+ function updateChartXml(chartXml, payload) {
237
+ const document = parseXml(chartXml);
238
+ const seriesNodes = chartSeriesNodes(document);
239
+ if (!Array.isArray(payload?.series) || payload.series.length !== seriesNodes.length) {
240
+ throw new Error(
241
+ `Chart series mismatch: expected ${seriesNodes.length}, received ${payload?.series?.length || 0}`,
242
+ );
243
+ }
244
+ seriesNodes.forEach((series, index) => {
245
+ const next = payload.series[index];
246
+ const titleParent = elements(series, "tx")[0];
247
+ if (titleParent) {
248
+ replaceCache(titleParent, [String(next.name || "")], "string");
249
+ const directValue = elements(titleParent, "v")[0];
250
+ if (directValue) directValue.textContent = String(next.name || "");
251
+ }
252
+ const categories = payload.categories || next.categories || [];
253
+ const categoryParent = elements(series, "cat")[0];
254
+ if (categoryParent) replaceCache(categoryParent, categories, "string");
255
+ const valueParent = elements(series, "val")[0];
256
+ if (valueParent) replaceCache(valueParent, next.values || [], "number");
257
+ });
258
+ return serializeXml(document);
259
+ }
260
+
261
+ function contentTypeForImage(extension) {
262
+ return (
263
+ {
264
+ png: "image/png",
265
+ jpg: "image/jpeg",
266
+ jpeg: "image/jpeg",
267
+ gif: "image/gif",
268
+ svg: "image/svg+xml",
269
+ webp: "image/webp",
270
+ }[extension] || "application/octet-stream"
271
+ );
272
+ }
273
+
274
+ async function replaceImage({
275
+ zip,
276
+ slideDocument,
277
+ slideRels,
278
+ shape,
279
+ target,
280
+ outputSlide,
281
+ targetIndex,
282
+ contentTypes,
283
+ }) {
284
+ const sourcePath =
285
+ typeof target.value === "string" ? target.value : target.value?.path;
286
+ if (!sourcePath) throw new Error(`Image target ${target.slotId} has no path`);
287
+ const resolved = path.resolve(sourcePath);
288
+ const bytes = await fs.readFile(resolved);
289
+ const extension = path.extname(resolved).slice(1).toLowerCase() || "png";
290
+ const blip = elements(shape.node, "blip")[0];
291
+ let relationshipId =
292
+ attrQName(blip, "r:embed") || attr(blip, "embed") || shape.relationshipId;
293
+ let relationship = relationshipId
294
+ ? relationshipById(slideRels, relationshipId)
295
+ : undefined;
296
+ if (!relationship) {
297
+ relationshipId = `rIdV04Image${targetIndex}`;
298
+ while (relationshipById(slideRels, relationshipId)) {
299
+ relationshipId = `${relationshipId}x`;
300
+ }
301
+ relationship = appendRelationship(slideRels, {
302
+ id: relationshipId,
303
+ type: `${R_NS}/image`,
304
+ target: "",
305
+ });
306
+ const shapeProperties = elements(shape.node, "spPr")[0];
307
+ if (!shapeProperties) {
308
+ throw new Error(`Image target ${target.slotId} has no shape properties`);
309
+ }
310
+ for (const child of Array.from(shapeProperties.childNodes || [])) {
311
+ if (
312
+ child.nodeType === 1 &&
313
+ ["noFill", "solidFill", "gradFill", "pattFill", "blipFill"].includes(
314
+ localName(child),
315
+ )
316
+ ) {
317
+ shapeProperties.removeChild(child);
318
+ }
319
+ }
320
+ const blipFill = slideDocument.createElementNS(A_NS, "a:blipFill");
321
+ const nextBlip = slideDocument.createElementNS(A_NS, "a:blip");
322
+ nextBlip.setAttributeNS(R_NS, "r:embed", relationshipId);
323
+ const stretch = slideDocument.createElementNS(A_NS, "a:stretch");
324
+ stretch.appendChild(slideDocument.createElementNS(A_NS, "a:fillRect"));
325
+ blipFill.appendChild(nextBlip);
326
+ blipFill.appendChild(stretch);
327
+ shapeProperties.appendChild(blipFill);
328
+ for (const textNode of elements(shape.node, "t")) textNode.textContent = "";
329
+ }
330
+ const mediaPart = `ppt/media/v04-slide-${outputSlide}-${targetIndex}.${extension}`;
331
+ zip.file(mediaPart, bytes, { date: FIXED_DATE });
332
+ relationship.setAttribute(
333
+ "Target",
334
+ `../media/${path.posix.basename(mediaPart)}`,
335
+ );
336
+ ensureDefault(contentTypes, extension, contentTypeForImage(extension));
337
+ return { sourcePath: resolved, mediaPart };
338
+ }
339
+
340
+ async function updateChart({
341
+ zip,
342
+ sourceSlidePart,
343
+ slideRels,
344
+ shape,
345
+ target,
346
+ outputSlide,
347
+ targetIndex,
348
+ sourceEntries,
349
+ contentTypes,
350
+ }) {
351
+ const chartNode = elements(shape.node, "chart")[0];
352
+ const relationshipId =
353
+ attrQName(chartNode, "r:id") || attr(chartNode, "id") || shape.relationshipId;
354
+ const relationship = relationshipById(slideRels, relationshipId);
355
+ if (!relationship) throw new Error(`Chart relationship ${relationshipId} is missing`);
356
+ const sourceChartPart = path.posix.normalize(
357
+ path.posix.join(
358
+ path.posix.dirname(sourceSlidePart),
359
+ attr(relationship, "Target"),
360
+ ),
361
+ );
362
+ const sourceChartXml = sourceEntries.get(sourceChartPart)?.text;
363
+ if (!sourceChartXml) throw new Error(`Chart part is missing: ${sourceChartPart}`);
364
+ const chartPart = `ppt/charts/chart-v04-${outputSlide}-${targetIndex}.xml`;
365
+ zip.file(chartPart, updateChartXml(sourceChartXml, target.value), {
366
+ date: FIXED_DATE,
367
+ });
368
+ relationship.setAttribute(
369
+ "Target",
370
+ `../charts/${path.posix.basename(chartPart)}`,
371
+ );
372
+ ensureOverride(
373
+ contentTypes,
374
+ chartPart,
375
+ "application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
376
+ );
377
+ return { sourceChartPart, chartPart, workbookSynced: false };
378
+ }
379
+
380
+ function generatedNotesSlideXml(originalText, generatedText) {
381
+ const combined = [originalText, generatedText ? `[生成讲稿]\n${generatedText}` : ""]
382
+ .filter(Boolean)
383
+ .join("\n\n");
384
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
385
+ <p:notes xmlns:a="${A_NS}" xmlns:r="${R_NS}" xmlns:p="${P_NS}">
386
+ <p:cSld><p:spTree>
387
+ <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
388
+ <p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>
389
+ <p:sp>
390
+ <p:nvSpPr><p:cNvPr id="2" name="Notes Text Placeholder 1"/><p:cNvSpPr txBox="1"/><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr>
391
+ <p:spPr/>
392
+ <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="zh-CN"/><a:t xml:space="preserve">${xmlEscape(combined)}</a:t></a:r><a:endParaRPr lang="zh-CN"/></a:p></p:txBody>
393
+ </p:sp>
394
+ </p:spTree></p:cSld>
395
+ <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
396
+ </p:notes>`;
397
+ }
398
+
399
+ function notesMasterXml() {
400
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
401
+ <p:notesMaster xmlns:a="${A_NS}" xmlns:r="${R_NS}" xmlns:p="${P_NS}">
402
+ <p:cSld><p:spTree>
403
+ <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
404
+ <p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>
405
+ </p:spTree></p:cSld>
406
+ <p:clrMap accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" bg1="lt1" bg2="lt2" folHlink="folHlink" hlink="hlink" tx1="dk1" tx2="dk2"/>
407
+ <p:hf hdr="0" ftr="0" dt="0" sldNum="0"/>
408
+ <p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles>
409
+ </p:notesMaster>`;
410
+ }
411
+
412
+ async function ensureNotesMaster({
413
+ zip,
414
+ contentTypes,
415
+ presentation,
416
+ presentationRels,
417
+ }) {
418
+ const existing = Object.keys(zip.files)
419
+ .filter((name) => /^ppt\/notesMasters\/notesMaster\d+\.xml$/.test(name))
420
+ .sort()[0];
421
+ if (existing) return existing;
422
+ const partName = "ppt/notesMasters/notesMaster1.xml";
423
+ zip.file(partName, notesMasterXml(), { date: FIXED_DATE });
424
+ zip.file(
425
+ relationshipPart(partName),
426
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${REL_NS}"></Relationships>`,
427
+ { date: FIXED_DATE },
428
+ );
429
+ ensureOverride(
430
+ contentTypes,
431
+ partName,
432
+ "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml",
433
+ );
434
+ const relId = "rIdNotesMasterV04";
435
+ appendRelationship(presentationRels, {
436
+ id: relId,
437
+ type: `${R_NS}/notesMaster`,
438
+ target: "notesMasters/notesMaster1.xml",
439
+ });
440
+ let list = elements(presentation, "notesMasterIdLst")[0];
441
+ if (!list) {
442
+ list = presentation.createElementNS(P_NS, "p:notesMasterIdLst");
443
+ presentation.documentElement.appendChild(list);
444
+ }
445
+ const idNode = presentation.createElementNS(P_NS, "p:notesMasterId");
446
+ idNode.setAttributeNS(R_NS, "r:id", relId);
447
+ list.appendChild(idNode);
448
+ return partName;
449
+ }
450
+
451
+ async function originalNotesText(zip, sourceSlidePart, sourceRels) {
452
+ const record = elements(sourceRels, "Relationship").find((node) =>
453
+ String(attr(node, "Type") || "").endsWith("/notesSlide"),
454
+ );
455
+ if (!record) return "";
456
+ const notesPart = path.posix.normalize(
457
+ path.posix.join(path.posix.dirname(sourceSlidePart), attr(record, "Target")),
458
+ );
459
+ const entry = zip.file(notesPart);
460
+ if (!entry) return "";
461
+ return textOf(parseXml(await entry.async("string")));
462
+ }
463
+
464
+ async function attachNotes({
465
+ zip,
466
+ slidePart,
467
+ slideRels,
468
+ notesMasterPart,
469
+ outputSlide,
470
+ originalText,
471
+ generatedText,
472
+ contentTypes,
473
+ }) {
474
+ removeRelationshipsByType(slideRels, "/notesSlide");
475
+ const notesPart = `ppt/notesSlides/notesSlide${outputSlide}.xml`;
476
+ zip.file(notesPart, generatedNotesSlideXml(originalText, generatedText), {
477
+ date: FIXED_DATE,
478
+ });
479
+ zip.file(
480
+ relationshipPart(notesPart),
481
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${REL_NS}"><Relationship Id="rId1" Type="${R_NS}/notesMaster" Target="../notesMasters/${path.posix.basename(notesMasterPart)}"/><Relationship Id="rId2" Type="${R_NS}/slide" Target="../slides/${path.posix.basename(slidePart)}"/></Relationships>`,
482
+ { date: FIXED_DATE },
483
+ );
484
+ appendRelationship(slideRels, {
485
+ id: "rIdNotesV04",
486
+ type: `${R_NS}/notesSlide`,
487
+ target: `../notesSlides/${path.posix.basename(notesPart)}`,
488
+ });
489
+ ensureOverride(
490
+ contentTypes,
491
+ notesPart,
492
+ "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml",
493
+ );
494
+ }
495
+
496
+ async function sourceEntryMap(zip, slides) {
497
+ const map = new Map();
498
+ for (const slide of slides) {
499
+ const slideXml = await xmlFrom(zip, slide.partName);
500
+ const relPart = relationshipPart(slide.partName);
501
+ const relXml = zip.file(relPart)
502
+ ? await zip.file(relPart).async("string")
503
+ : `<Relationships xmlns="${REL_NS}"></Relationships>`;
504
+ map.set(slide.partName, { text: slideXml });
505
+ map.set(relPart, { text: relXml });
506
+ const rels = parseXml(relXml);
507
+ for (const node of elements(rels, "Relationship")) {
508
+ if (attr(node, "TargetMode") === "External") continue;
509
+ const targetPart = path.posix.normalize(
510
+ path.posix.join(path.posix.dirname(slide.partName), attr(node, "Target")),
511
+ );
512
+ const entry = zip.file(targetPart);
513
+ if (entry && !map.has(targetPart)) {
514
+ const binary = !/\.(xml|rels)$/i.test(targetPart);
515
+ map.set(targetPart, {
516
+ ...(binary
517
+ ? { bytes: await entry.async("nodebuffer") }
518
+ : { text: await entry.async("string") }),
519
+ });
520
+ }
521
+ }
522
+ }
523
+ return map;
524
+ }
525
+
526
+ export async function editPptxNative({
527
+ templatePath,
528
+ frameMap,
529
+ outputPath,
530
+ progress,
531
+ progressSnapshotPath,
532
+ }) {
533
+ if (frameMap.fidelityMode === "hybrid-locked") {
534
+ throw new Error(
535
+ "hybrid-locked authoring requires reviewed overlay slots; automatic flattened-page authoring is blocked",
536
+ );
537
+ }
538
+ const zip = await loadPptx(templatePath);
539
+ const order = await slideRecords(zip);
540
+ const sourceByNumber = new Map(
541
+ order.slides.map((slide) => [slide.slideNumber, slide]),
542
+ );
543
+ const sourceEntries = await sourceEntryMap(zip, order.slides);
544
+ const contentTypes = contentTypesDocument(
545
+ await xmlFrom(zip, "[Content_Types].xml"),
546
+ );
547
+ const presentation = order.presentation;
548
+ const presentationRels = relationshipDocument(
549
+ order.presentationRels.document,
550
+ );
551
+ const notesMasterPart = await ensureNotesMaster({
552
+ zip,
553
+ contentTypes,
554
+ presentation,
555
+ presentationRels,
556
+ });
557
+ ensureOverride(
558
+ contentTypes,
559
+ "ppt/presentation.xml",
560
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
561
+ );
562
+
563
+ const slideList = elements(presentation, "sldIdLst")[0];
564
+ if (!slideList) throw new Error("PPTX presentation has no slide list");
565
+ for (const child of Array.from(slideList.childNodes || [])) {
566
+ if (child.nodeType === 1 && localName(child) === "sldId") {
567
+ slideList.removeChild(child);
568
+ }
569
+ }
570
+ removeRelationshipsByType(presentationRels, "/slide");
571
+
572
+ const editLog = [];
573
+ for (const [index, slideMap] of frameMap.outputSlides.entries()) {
574
+ const outputSlide = index + 1;
575
+ const source = sourceByNumber.get(Number(slideMap.sourceSlide));
576
+ if (!source) throw new Error(`Missing source slide ${slideMap.sourceSlide}`);
577
+ const sourceSlideXml = sourceEntries.get(source.partName)?.text;
578
+ const sourceRelPart = relationshipPart(source.partName);
579
+ const sourceRelXml = sourceEntries.get(sourceRelPart)?.text;
580
+ const slideDocument = parseXml(sourceSlideXml);
581
+ const slideRels = relationshipDocument(parseXml(sourceRelXml));
582
+ const originalNotes = await originalNotesText(
583
+ zip,
584
+ source.partName,
585
+ slideRels,
586
+ );
587
+ removeRelationshipsByType(slideRels, "/notesSlide");
588
+
589
+ for (const [targetIndex, target] of (slideMap.editTargets || []).entries()) {
590
+ const shape = findShapeNode(slideDocument, target);
591
+ let evidence = {};
592
+ if (target.action === "rewrite" && target.type === "text") {
593
+ rewriteText(shape, target.value);
594
+ } else if (
595
+ ["replace", "update-data"].includes(target.action) &&
596
+ target.type === "table"
597
+ ) {
598
+ replaceTable(shape, target.value);
599
+ } else if (target.action === "replace" && target.type === "image") {
600
+ evidence = await replaceImage({
601
+ zip,
602
+ slideDocument,
603
+ slideRels,
604
+ shape,
605
+ target,
606
+ outputSlide,
607
+ targetIndex: targetIndex + 1,
608
+ contentTypes,
609
+ });
610
+ } else if (
611
+ ["replace", "update-data"].includes(target.action) &&
612
+ target.type === "chart"
613
+ ) {
614
+ const error = new Error(
615
+ `Chart target ${target.slotId} is blocked until both the visible chart cache and embedded workbook can be synchronized`,
616
+ );
617
+ error.code = "CHART_WORKBOOK_SYNC_REQUIRED";
618
+ throw error;
619
+ } else {
620
+ throw new Error(
621
+ `Unsupported native edit ${target.action}/${target.type} for ${target.slotId}`,
622
+ );
623
+ }
624
+ editLog.push({
625
+ outputSlide,
626
+ sourceSlide: slideMap.sourceSlide,
627
+ slotId: target.slotId,
628
+ action: target.action,
629
+ type: target.type,
630
+ valueFingerprint: fingerprint(target.value),
631
+ ...evidence,
632
+ });
633
+ }
634
+
635
+ const slidePart = `ppt/slides/slide${outputSlide}.xml`;
636
+ await attachNotes({
637
+ zip,
638
+ slidePart,
639
+ slideRels,
640
+ notesMasterPart,
641
+ outputSlide,
642
+ originalText: originalNotes,
643
+ generatedText: slideMap.speakerNotes,
644
+ contentTypes,
645
+ });
646
+ zip.file(slidePart, serializeXml(slideDocument), { date: FIXED_DATE });
647
+ zip.file(relationshipPart(slidePart), serializeXml(slideRels), {
648
+ date: FIXED_DATE,
649
+ });
650
+ ensureOverride(
651
+ contentTypes,
652
+ slidePart,
653
+ "application/vnd.openxmlformats-officedocument.presentationml.slide+xml",
654
+ );
655
+ const relId = `rIdV04Slide${outputSlide}`;
656
+ appendRelationship(presentationRels, {
657
+ id: relId,
658
+ type: `${R_NS}/slide`,
659
+ target: `slides/slide${outputSlide}.xml`,
660
+ });
661
+ const slideIdNode = presentation.createElementNS(P_NS, "p:sldId");
662
+ slideIdNode.setAttribute("id", String(255 + outputSlide));
663
+ slideIdNode.setAttributeNS(R_NS, "r:id", relId);
664
+ slideList.appendChild(slideIdNode);
665
+ if (progressSnapshotPath) {
666
+ zip.file("ppt/presentation.xml", serializeXml(presentation), {
667
+ date: FIXED_DATE,
668
+ });
669
+ zip.file(
670
+ "ppt/_rels/presentation.xml.rels",
671
+ serializeXml(presentationRels),
672
+ { date: FIXED_DATE },
673
+ );
674
+ zip.file("[Content_Types].xml", serializeXml(contentTypes), {
675
+ date: FIXED_DATE,
676
+ });
677
+ const snapshotBytes = await zip.generateAsync({
678
+ type: "nodebuffer",
679
+ compression: "DEFLATE",
680
+ compressionOptions: { level: 6 },
681
+ platform: "UNIX",
682
+ });
683
+ await fs.mkdir(path.dirname(progressSnapshotPath), { recursive: true });
684
+ await fs.writeFile(progressSnapshotPath, snapshotBytes);
685
+ }
686
+ await progress?.({
687
+ event: "slide-authored",
688
+ outputSlide,
689
+ sourceSlide: Number(slideMap.sourceSlide),
690
+ slideCount: frameMap.outputSlides.length,
691
+ snapshotPptx: progressSnapshotPath
692
+ ? path.resolve(progressSnapshotPath)
693
+ : undefined,
694
+ });
695
+ }
696
+
697
+ zip.file("ppt/presentation.xml", serializeXml(presentation), {
698
+ date: FIXED_DATE,
699
+ });
700
+ zip.file(
701
+ "ppt/_rels/presentation.xml.rels",
702
+ serializeXml(presentationRels),
703
+ { date: FIXED_DATE },
704
+ );
705
+ const removedOrphanedParts = pruneOrphanedSlideParts(
706
+ zip,
707
+ contentTypes,
708
+ frameMap.outputSlides.length,
709
+ );
710
+ zip.file("[Content_Types].xml", serializeXml(contentTypes), {
711
+ date: FIXED_DATE,
712
+ });
713
+ for (const entry of Object.values(zip.files)) entry.date = FIXED_DATE;
714
+ const bytes = await zip.generateAsync({
715
+ type: "nodebuffer",
716
+ compression: "DEFLATE",
717
+ compressionOptions: { level: 9 },
718
+ platform: "UNIX",
719
+ });
720
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
721
+ await fs.writeFile(outputPath, bytes);
722
+ return {
723
+ schemaVersion: 1,
724
+ output: path.resolve(outputPath),
725
+ sha256: await sha256File(outputPath),
726
+ slideCount: frameMap.outputSlides.length,
727
+ editLog,
728
+ removedOrphanedParts,
729
+ };
730
+ }
731
+
732
+ export async function inspectPptxPackage(pptxPath) {
733
+ const zip = await loadPptx(pptxPath);
734
+ const order = await slideRecords(zip);
735
+ const notes = Object.keys(zip.files).filter((name) =>
736
+ /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(name),
737
+ );
738
+ return {
739
+ path: path.resolve(pptxPath),
740
+ sha256: await sha256File(pptxPath),
741
+ slideCount: order.slides.length,
742
+ notesSlideCount: notes.length,
743
+ macroParts: Object.keys(zip.files).filter((name) => /vbaProject/i.test(name)),
744
+ externalRelationships: (
745
+ await Promise.all(
746
+ order.slides.map(async (slide) => relationships(zip, slide.partName)),
747
+ )
748
+ )
749
+ .flatMap((record) => record.records)
750
+ .filter((record) => record.targetMode === "External")
751
+ .map((record) => record.target),
752
+ };
753
+ }