node-pptx-templater 1.0.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +415 -0
  4. package/package.json +83 -0
  5. package/src/cli/commands/build.js +79 -0
  6. package/src/cli/commands/debug.js +46 -0
  7. package/src/cli/commands/extract.js +42 -0
  8. package/src/cli/commands/inspect.js +39 -0
  9. package/src/cli/commands/validate.js +36 -0
  10. package/src/cli/index.js +132 -0
  11. package/src/core/OutputWriter.js +181 -0
  12. package/src/core/PPTXTemplater.js +961 -0
  13. package/src/core/TemplateEngine.js +321 -0
  14. package/src/index.js +43 -0
  15. package/src/managers/ChartManager.js +317 -0
  16. package/src/managers/ContentTypesManager.js +160 -0
  17. package/src/managers/HyperlinkManager.js +451 -0
  18. package/src/managers/MediaManager.js +307 -0
  19. package/src/managers/RelationshipManager.js +401 -0
  20. package/src/managers/SlideManager.js +950 -0
  21. package/src/managers/TableManager.js +416 -0
  22. package/src/managers/ZipManager.js +298 -0
  23. package/src/managers/charts/ChartCacheGenerator.js +156 -0
  24. package/src/managers/charts/ChartParser.js +43 -0
  25. package/src/managers/charts/ChartRelationshipManager.js +33 -0
  26. package/src/managers/charts/ChartWorkbookUpdater.js +130 -0
  27. package/src/parsers/XMLParser.js +291 -0
  28. package/src/templates/blankPptx.js +1 -0
  29. package/src/templates/slideTemplate.js +314 -0
  30. package/src/utils/contentTypesHelper.js +149 -0
  31. package/src/utils/errors.js +129 -0
  32. package/src/utils/idUtils.js +54 -0
  33. package/src/utils/logger.js +113 -0
  34. package/src/utils/relationshipUtils.js +89 -0
  35. package/src/utils/xmlUtils.js +115 -0
@@ -0,0 +1,961 @@
1
+ /**
2
+ * @fileoverview PPTXTemplater - The main orchestrator class.
3
+ *
4
+ * This is the primary public API. It coordinates all sub-managers
5
+ * (ZipManager, SlideManager, ChartManager, etc.) and exposes a
6
+ * fluent, chainable interface for template manipulation.
7
+ *
8
+ * OpenXML PPTX Structure:
9
+ * ├── [Content_Types].xml — lists all parts and their MIME types
10
+ * ├── _rels/.rels — root relationships (points to presentation)
11
+ * ├── ppt/
12
+ * │ ├── presentation.xml — slide order, slide masters references
13
+ * │ ├── _rels/presentation.xml.rels
14
+ * │ ├── slides/
15
+ * │ │ ├── slide1.xml — individual slide content
16
+ * │ │ └── _rels/slide1.xml.rels
17
+ * │ ├── slideLayouts/ — layout templates (title, content, etc.)
18
+ * │ ├── slideMasters/ — master slide designs
19
+ * │ ├── theme/ — color/font themes
20
+ * │ ├── charts/ — embedded chart XML
21
+ * │ └── media/ — embedded images/videos
22
+ * └── docProps/
23
+ * ├── core.xml — author, title, etc.
24
+ * └── app.xml — application metadata
25
+ */
26
+
27
+ import { ZipManager } from '../managers/ZipManager.js';
28
+ import { XMLParser } from '../parsers/XMLParser.js';
29
+ import { ContentTypesManager } from '../managers/ContentTypesManager.js';
30
+ import { SlideManager } from '../managers/SlideManager.js';
31
+ import { ChartManager } from '../managers/ChartManager.js';
32
+ import { TableManager } from '../managers/TableManager.js';
33
+ import { HyperlinkManager } from '../managers/HyperlinkManager.js';
34
+ import { MediaManager } from '../managers/MediaManager.js';
35
+ import { RelationshipManager } from '../managers/RelationshipManager.js';
36
+ import { OutputWriter } from './OutputWriter.js';
37
+ import { TemplateEngine } from './TemplateEngine.js';
38
+ import { createLogger } from '../utils/logger.js';
39
+ import { PPTXError } from '../utils/errors.js';
40
+
41
+ const logger = createLogger('PPTXTemplater');
42
+
43
+ /**
44
+ * @class PPTXTemplater
45
+ * @description Main engine class for PPTX template manipulation.
46
+ *
47
+ * @example
48
+ * const ppt = await PPTXTemplater.load('template.pptx');
49
+ * ppt.useSlide(1);
50
+ * ppt.replaceText({ '{{title}}': 'My Report' });
51
+ * await ppt.saveToFile('./output/report.pptx');
52
+ */
53
+ export class PPTXTemplater {
54
+ /**
55
+ * @private
56
+ * @type {ZipManager}
57
+ */
58
+ #zipManager;
59
+
60
+ /**
61
+ * @private
62
+ * @type {XMLParser}
63
+ */
64
+ #xmlParser;
65
+
66
+ /**
67
+ * @private
68
+ * @type {ContentTypesManager}
69
+ */
70
+ #contentTypesManager;
71
+
72
+ /**
73
+ * @private
74
+ * @type {SlideManager}
75
+ */
76
+ #slideManager;
77
+
78
+ /**
79
+ * @private
80
+ * @type {ChartManager}
81
+ */
82
+ #chartManager;
83
+
84
+ /**
85
+ * @private
86
+ * @type {TableManager}
87
+ */
88
+ #tableManager;
89
+
90
+ /**
91
+ * @private
92
+ * @type {HyperlinkManager}
93
+ */
94
+ #hyperlinkManager;
95
+
96
+ /**
97
+ * @private
98
+ * @type {MediaManager}
99
+ */
100
+ #mediaManager;
101
+
102
+ /**
103
+ * @private
104
+ * @type {RelationshipManager}
105
+ */
106
+ #relationshipManager;
107
+
108
+ /**
109
+ * @private
110
+ * @type {OutputWriter}
111
+ */
112
+ #outputWriter;
113
+
114
+ /**
115
+ * @private
116
+ * @type {TemplateEngine}
117
+ */
118
+ #templateEngine;
119
+
120
+ /**
121
+ * @private
122
+ * @type {number[]} - Currently selected slide indices (1-based)
123
+ */
124
+ #selectedSlides = [];
125
+
126
+ /**
127
+ * @private
128
+ * @type {boolean}
129
+ */
130
+ #loaded = false;
131
+
132
+ constructor() {
133
+ this.#xmlParser = new XMLParser();
134
+ this.#zipManager = new ZipManager();
135
+ this.#contentTypesManager = new ContentTypesManager(this.#xmlParser);
136
+ this.#relationshipManager = new RelationshipManager(this.#xmlParser);
137
+ this.#slideManager = new SlideManager(this.#xmlParser, this.#relationshipManager, this.#contentTypesManager);
138
+ this.#chartManager = new ChartManager(this.#xmlParser, this.#contentTypesManager);
139
+ this.#tableManager = new TableManager(this.#xmlParser);
140
+ this.#hyperlinkManager = new HyperlinkManager(this.#xmlParser, this.#relationshipManager);
141
+ this.#mediaManager = new MediaManager(this.#contentTypesManager);
142
+ this.#templateEngine = new TemplateEngine(this.#xmlParser);
143
+ this.#outputWriter = new OutputWriter(this.#zipManager, this.#contentTypesManager);
144
+ }
145
+
146
+ /**
147
+ * Loads a PPTX template from a file path or buffer.
148
+ *
149
+ * @static
150
+ * @param {string|Buffer} source - Path to PPTX file or Buffer containing PPTX data.
151
+ * @returns {Promise<PPTXTemplater>} Initialized engine instance.
152
+ * @throws {PPTXError} If the file cannot be read or is not a valid PPTX.
153
+ *
154
+ * @example
155
+ * // From file path
156
+ * const ppt = await PPTXTemplater.load('./template.pptx');
157
+ *
158
+ * // From buffer
159
+ * const buffer = fs.readFileSync('./template.pptx');
160
+ * const ppt = await PPTXTemplater.load(buffer);
161
+ */
162
+ static async load(source) {
163
+ const engine = new PPTXTemplater();
164
+ await engine.#initialize(source);
165
+ return engine;
166
+ }
167
+
168
+ /**
169
+ * Creates a new blank PPTX from scratch.
170
+ *
171
+ * @static
172
+ * @returns {Promise<PPTXTemplater>} Engine instance with a blank PPTX.
173
+ *
174
+ * @example
175
+ * const ppt = await PPTXTemplater.create();
176
+ * ppt.addSlide({ title: 'First Slide' });
177
+ * await ppt.saveToFile('./new.pptx');
178
+ */
179
+ static async create() {
180
+ const engine = new PPTXTemplater();
181
+ await engine.#initializeBlank();
182
+ return engine;
183
+ }
184
+
185
+ /**
186
+ * Initializes the engine by loading a PPTX file/buffer.
187
+ * @private
188
+ * @param {string|Buffer} source
189
+ */
190
+ async #initialize(source) {
191
+ logger.debug(`Loading PPTX from ${typeof source === 'string' ? source : 'buffer'}`);
192
+
193
+ // Load and extract the ZIP archive (PPTX is just a ZIP)
194
+ await this.#zipManager.load(source);
195
+
196
+ // Initialize content types manager first!
197
+ await this.#contentTypesManager.initialize(this.#zipManager);
198
+
199
+ // Parse the core presentation relationships and structure
200
+ await this.#relationshipManager.initialize(this.#zipManager);
201
+
202
+ // Load all slide references from presentation.xml
203
+ await this.#slideManager.initialize(this.#zipManager);
204
+
205
+ // Pre-load all slide XML into cache to allow synchronous operations like replaceText()
206
+ await this.#slideManager.preloadAll();
207
+
208
+ // Initialize chart manager with zip context
209
+ await this.#chartManager.initialize(this.#zipManager);
210
+
211
+ // Deduplicate and index media files
212
+ await this.#mediaManager.initialize(this.#zipManager);
213
+
214
+ this.#loaded = true;
215
+ logger.debug(`Loaded ${this.#slideManager.slideCount} slides successfully`);
216
+ }
217
+
218
+ /**
219
+ * Initializes a blank PPTX structure from embedded template XML.
220
+ * @private
221
+ */
222
+ async #initializeBlank() {
223
+ await this.#zipManager.createBlank();
224
+ await this.#contentTypesManager.initialize(this.#zipManager);
225
+ await this.#relationshipManager.initialize(this.#zipManager);
226
+ await this.#slideManager.initialize(this.#zipManager);
227
+ await this.#chartManager.initialize(this.#zipManager);
228
+ await this.#mediaManager.initialize(this.#zipManager);
229
+ this.#loaded = true;
230
+ }
231
+
232
+ /**
233
+ * Asserts the engine is loaded before performing operations.
234
+ * @private
235
+ */
236
+ #assertLoaded() {
237
+ if (!this.#loaded) {
238
+ throw new PPTXError('Engine not initialized. Call PPTXTemplater.load() first.');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Selects one or more slides to work on.
244
+ * All subsequent operations (replaceText, updateChart, etc.) apply to these slides.
245
+ * If not called, operations apply to ALL slides.
246
+ *
247
+ * @param {...number|string} slideRefs - Slide numbers (1-based), IDs, or tags.
248
+ * @returns {PPTXTemplater} this (chainable)
249
+ *
250
+ * @example
251
+ * ppt.useSlide(1); // Select slide 1
252
+ * ppt.useSlide(1, 3, 5); // Select slides 1, 3, and 5
253
+ * ppt.useSlide('intro'); // Select by custom tag
254
+ */
255
+ useSlide(...slideRefs) {
256
+ this.#assertLoaded();
257
+ this.#selectedSlides = slideRefs;
258
+ logger.debug(`Selected slides: ${slideRefs.join(', ')}`);
259
+ return this;
260
+ }
261
+
262
+ /**
263
+ * Selects all slides.
264
+ * @returns {PPTXTemplater} this (chainable)
265
+ */
266
+ useAllSlides() {
267
+ this.#assertLoaded();
268
+ this.#selectedSlides = [];
269
+ return this;
270
+ }
271
+
272
+ /**
273
+ * Returns the resolved slide indices based on #selectedSlides.
274
+ * If nothing is selected, returns all slide indices.
275
+ * @private
276
+ * @returns {number[]} Array of 1-based slide indices.
277
+ */
278
+ #getTargetSlideIndices() {
279
+ if (this.#selectedSlides.length === 0) {
280
+ return this.#slideManager.getAllSlideIndices();
281
+ }
282
+ return this.#selectedSlides.flatMap(ref => {
283
+ if (typeof ref === 'number') return [ref];
284
+ // Resolve by tag or ID
285
+ return this.#slideManager.resolveSlideRef(ref);
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Replaces template placeholders (e.g., {{key}}) with values in the selected slides.
291
+ * Works inside text boxes, titles, grouped shapes, tables, and shapes.
292
+ *
293
+ * @param {Object.<string, string>} replacements - Map of placeholder → replacement value.
294
+ * @returns {PPTXTemplater} this (chainable)
295
+ *
296
+ * @example
297
+ * ppt.replaceText({
298
+ * '{{title}}': 'Quarterly Report',
299
+ * '{{year}}': '2026',
300
+ * '{{company}}': 'Acme Corp'
301
+ * });
302
+ */
303
+ replaceText(replacements) {
304
+ this.#assertLoaded();
305
+ const targetIndices = this.#getTargetSlideIndices();
306
+
307
+ for (const slideIndex of targetIndices) {
308
+ const slideXml = this.#slideManager.getSlideXml(slideIndex);
309
+ const updated = this.#templateEngine.replaceTextInXml(slideXml, replacements);
310
+ this.#slideManager.setSlideXml(slideIndex, updated);
311
+ }
312
+
313
+ logger.debug(`Replaced ${Object.keys(replacements).length} placeholder(s) in ${targetIndices.length} slide(s)`);
314
+ return this;
315
+ }
316
+
317
+ /**
318
+ * Updates chart data in the selected slide(s).
319
+ * Finds charts by their name/ID and updates categories, series, and values.
320
+ * Preserves original chart styles, themes, and formatting.
321
+ *
322
+ * @param {string} chartId - Chart name or relationship ID.
323
+ * @param {ChartData} data - New chart data.
324
+ * @param {string[]} data.categories - Category labels (X-axis).
325
+ * @param {SeriesData[]} data.series - Data series array.
326
+ * @param {string} data.series[].name - Series name.
327
+ * @param {number[]} data.series[].values - Data values.
328
+ * @returns {PPTXTemplater} this (chainable)
329
+ *
330
+ * @example
331
+ * ppt.updateChart('sales-chart', {
332
+ * categories: ['Jan', 'Feb', 'Mar'],
333
+ * series: [{ name: 'Revenue', values: [120, 150, 180] }]
334
+ * });
335
+ */
336
+ updateChart(chartId, data) {
337
+ this.#assertLoaded();
338
+ const targetIndices = this.#getTargetSlideIndices();
339
+
340
+ for (const slideIndex of targetIndices) {
341
+ this.#chartManager.updateChart(
342
+ slideIndex,
343
+ chartId,
344
+ data,
345
+ this.#slideManager,
346
+ this.#relationshipManager
347
+ );
348
+ }
349
+
350
+ logger.debug(`Updated chart "${chartId}" in ${targetIndices.length} slide(s)`);
351
+ return this;
352
+ }
353
+
354
+ /**
355
+ * Replaces table rows with new data in the selected slide(s).
356
+ * Preserves borders, merged cells, fonts, colors, and alignment from the template.
357
+ *
358
+ * @param {string} tableId - Table name or shape ID.
359
+ * @param {string[][]} rows - 2D array of cell values (row × col).
360
+ * @returns {PPTXTemplater} this (chainable)
361
+ *
362
+ * @example
363
+ * ppt.updateTable('employees-table', [
364
+ * ['Name', 'Role', 'Department'],
365
+ * ['John', 'Engineer', 'Platform'],
366
+ * ['Jane', 'Designer', 'Product']
367
+ * ]);
368
+ */
369
+ updateTable(tableId, rows) {
370
+ this.#assertLoaded();
371
+ const targetIndices = this.#getTargetSlideIndices();
372
+
373
+ for (const slideIndex of targetIndices) {
374
+ this.#tableManager.updateTable(slideIndex, tableId, rows, this.#slideManager);
375
+ }
376
+
377
+ logger.debug(`Updated table "${tableId}" in ${targetIndices.length} slide(s)`);
378
+ return this;
379
+ }
380
+
381
+ /**
382
+ * Adds or replaces a hyperlink on a text run or shape.
383
+ *
384
+ * @param {HyperlinkOptions} options - Hyperlink configuration.
385
+ * @param {string} options.text - Text to find and make clickable.
386
+ * @param {string} options.url - Target URL.
387
+ * @param {string} [options.tooltip] - Optional tooltip.
388
+ * @returns {PPTXTemplater} this (chainable)
389
+ *
390
+ * @example
391
+ * ppt.addHyperlink({ text: 'Open Website', url: 'https://example.com' });
392
+ */
393
+ addHyperlink(options) {
394
+ this.#assertLoaded();
395
+ const targetIndices = this.#getTargetSlideIndices();
396
+
397
+ for (const slideIndex of targetIndices) {
398
+ this.#hyperlinkManager.addExternalHyperlink(
399
+ slideIndex,
400
+ options,
401
+ this.#slideManager,
402
+ this.#relationshipManager
403
+ );
404
+ }
405
+
406
+ return this;
407
+ }
408
+
409
+ /**
410
+ * Adds an inter-slide hyperlink to a specific text element.
411
+ *
412
+ * @param {Object} options - Link configuration.
413
+ * @param {number} options.sourceSlide - Source slide number (1-based).
414
+ * @param {number} options.targetSlide - Destination slide number (1-based).
415
+ * @param {string} options.element - Text element to make clickable.
416
+ * @returns {PPTXTemplater} this (chainable)
417
+ */
418
+ addSlideLink(options) {
419
+ this.#assertLoaded();
420
+ const { sourceSlide, targetSlide, element } = options;
421
+
422
+ // Fallback: If no element text is provided, link the slide number (legacy behavior)
423
+ if (!element) {
424
+ this.#hyperlinkManager.addSlideHyperlink(
425
+ sourceSlide,
426
+ targetSlide,
427
+ this.#slideManager,
428
+ this.#relationshipManager
429
+ );
430
+ } else {
431
+ // Add a slide hyperlink on specific text
432
+ this.#hyperlinkManager.addTextSlideLink(
433
+ sourceSlide,
434
+ element,
435
+ targetSlide,
436
+ this.#slideManager,
437
+ this.#relationshipManager
438
+ );
439
+ }
440
+ return this;
441
+ }
442
+
443
+ /**
444
+ * Adds an inter-slide hyperlink to an image.
445
+ *
446
+ * @param {Object} options
447
+ * @param {number} options.slide - Source slide number.
448
+ * @param {string} options.imageId - Image name/id to make clickable.
449
+ * @param {number} options.targetSlide - Destination slide number.
450
+ * @returns {PPTXTemplater} this
451
+ */
452
+ addImageLink(options) {
453
+ this.#assertLoaded();
454
+ this.#hyperlinkManager.addShapeSlideLink(
455
+ options.slide,
456
+ options.imageId,
457
+ options.targetSlide,
458
+ this.#slideManager,
459
+ this.#relationshipManager
460
+ );
461
+ return this;
462
+ }
463
+
464
+ /**
465
+ * Adds an inter-slide hyperlink to a shape.
466
+ *
467
+ * @param {Object} options
468
+ * @param {number} options.slide - Source slide number.
469
+ * @param {string} options.shapeId - Shape name/id to make clickable.
470
+ * @param {number} options.targetSlide - Destination slide number.
471
+ * @returns {PPTXTemplater} this
472
+ */
473
+ addShapeLink(options) {
474
+ this.#assertLoaded();
475
+ this.#hyperlinkManager.addShapeSlideLink(
476
+ options.slide,
477
+ options.shapeId,
478
+ options.targetSlide,
479
+ this.#slideManager,
480
+ this.#relationshipManager
481
+ );
482
+ return this;
483
+ }
484
+
485
+ /**
486
+ * Adds a special navigation link (next, previous, first, last slide) to a text element.
487
+ *
488
+ * @param {Object} options
489
+ * @param {number} options.slide - Source slide number (1-based).
490
+ * @param {string} options.element - Text element to make clickable.
491
+ * @param {'next'|'previous'|'first'|'last'} options.action - Navigation action type.
492
+ * @returns {PPTXTemplater} this (chainable)
493
+ */
494
+ addTextNavigationLink(options) {
495
+ this.#assertLoaded();
496
+ const { slide, element, action } = options;
497
+ this.#hyperlinkManager.addTextNavigationLink(slide, element, action, this.#slideManager);
498
+ return this;
499
+ }
500
+
501
+ /**
502
+ * Adds a special navigation link (next, previous, first, last slide) to a shape or image.
503
+ *
504
+ * @param {Object} options
505
+ * @param {number} options.slide - Source slide number (1-based).
506
+ * @param {string} options.shapeId - Shape name/id to make clickable.
507
+ * @param {'next'|'previous'|'first'|'last'} options.action - Navigation action type.
508
+ * @returns {PPTXTemplater} this (chainable)
509
+ */
510
+ addShapeNavigationLink(options) {
511
+ this.#assertLoaded();
512
+ const { slide, shapeId, action } = options;
513
+ this.#hyperlinkManager.addShapeNavigationLink(slide, shapeId, action, this.#slideManager);
514
+ return this;
515
+ }
516
+
517
+ /**
518
+ * Adds a new slide to the presentation.
519
+ * Automatically generates required XML and relationship entries.
520
+ *
521
+ * @param {NewSlideOptions} options - Slide definition.
522
+ * @param {string} [options.title] - Slide title text.
523
+ * @param {string} [options.layout] - Layout name to use (default: 'blank').
524
+ * @param {SlideElement[]} [options.elements] - Elements to add to the slide.
525
+ * @returns {PPTXTemplater} this (chainable)
526
+ *
527
+ * @example
528
+ * ppt.addSlide({
529
+ * title: 'New Slide',
530
+ * elements: [
531
+ * { type: 'text', value: 'Hello World', x: 100, y: 200 },
532
+ * { type: 'image', src: './logo.png', x: 500, y: 100, width: 200, height: 150 }
533
+ * ]
534
+ * });
535
+ */
536
+ addSlide(options = {}) {
537
+ this.#assertLoaded();
538
+ this.#slideManager.addNewSlide(options, this.#relationshipManager, this.#mediaManager);
539
+ logger.debug(`Added new slide: "${options.title || 'Untitled'}"`);
540
+ return this;
541
+ }
542
+
543
+ /**
544
+ * Clones an existing slide and appends it to the end (or at a position).
545
+ *
546
+ * @param {number} sourceSlideNumber - 1-based source slide number.
547
+ * @param {number} [atPosition] - Optional position to insert (1-based). Default: append.
548
+ * @returns {PPTXTemplater} this (chainable)
549
+ */
550
+ cloneSlide(sourceSlideNumber, atPosition) {
551
+ this.#assertLoaded();
552
+ this.#slideManager.cloneSlide(sourceSlideNumber, atPosition, this.#relationshipManager);
553
+ return this;
554
+ }
555
+
556
+ /**
557
+ * Removes a slide from the presentation.
558
+ *
559
+ * @param {number} slideNumber - 1-based slide number to remove.
560
+ * @returns {PPTXTemplater} this (chainable)
561
+ */
562
+ removeSlide(slideNumber) {
563
+ this.#assertLoaded();
564
+ this.#slideManager.removeSlide(slideNumber);
565
+ return this;
566
+ }
567
+
568
+ /**
569
+ * Reorders slides in the presentation.
570
+ *
571
+ * @param {number[]} order - Array of 1-based slide numbers in desired order.
572
+ * @returns {PPTXTemplater} this (chainable)
573
+ *
574
+ * @example
575
+ * ppt.reorderSlides([3, 1, 2]); // Move slide 3 to position 1
576
+ */
577
+ reorderSlides(order) {
578
+ this.#assertLoaded();
579
+ this.#slideManager.reorderSlides(order);
580
+ return this;
581
+ }
582
+
583
+ /**
584
+ * Tags a slide with a custom string identifier for later selection.
585
+ *
586
+ * @param {number} slideNumber - 1-based slide number.
587
+ * @param {string} tag - Custom tag string.
588
+ * @returns {PPTXTemplater} this (chainable)
589
+ *
590
+ * @example
591
+ * ppt.tagSlide(1, 'intro');
592
+ * ppt.useSlide('intro').replaceText({ '{{title}}': 'Hello' });
593
+ */
594
+ tagSlide(slideNumber, tag) {
595
+ this.#assertLoaded();
596
+ this.#slideManager.tagSlide(slideNumber, tag);
597
+ return this;
598
+ }
599
+
600
+ /**
601
+ * Exports selected slides to a new standalone PPTX engine.
602
+ * Useful for creating "slide decks" from a master template.
603
+ *
604
+ * @param {...number} slideNumbers - 1-based slide numbers to export.
605
+ * @returns {Promise<PPTXTemplater>} New engine with only the selected slides.
606
+ *
607
+ * @example
608
+ * const subset = await ppt.exportSlides(1, 3, 5);
609
+ * await subset.saveToFile('./subset.pptx');
610
+ */
611
+ async exportSlides(...slideNumbers) {
612
+ this.#assertLoaded();
613
+ return this.#slideManager.exportSlides(slideNumbers, this);
614
+ }
615
+
616
+ /**
617
+ * Imports a single slide from another PPTXTemplater instance into this presentation.
618
+ * Preserves all slide layouts, charts, relationships, and embedded media.
619
+ *
620
+ * @param {PPTXTemplater} sourceEngine - Source PPTXTemplater instance.
621
+ * @param {number|string} slideRef - Slide index (1-based), ID, or custom tag.
622
+ * @returns {Promise<PPTXTemplater>} this (chainable)
623
+ */
624
+ async importSlideFrom(sourceEngine, slideRef) {
625
+ this.#assertLoaded();
626
+ await this.#slideManager.importSlide(sourceEngine, slideRef, this.#mediaManager);
627
+ return this;
628
+ }
629
+
630
+ /**
631
+ * Imports selected slides from the current template, discarding the rest.
632
+ * The remaining slides are reordered to match the provided array.
633
+ * Preserves all layouts, themes, relationships, and embedded media.
634
+ *
635
+ * @param {number[]} slideIndices - Array of 1-based slide indices to keep.
636
+ * @returns {PPTXTemplater} this (chainable)
637
+ *
638
+ * @example
639
+ * ppt.importSlides([1, 3, 5]);
640
+ */
641
+ importSlides(slideIndices) {
642
+ this.#assertLoaded();
643
+ const slidesToKeep = slideIndices.map(i => this.#slideManager.getSlideInfo(i).slideId);
644
+
645
+ // Remove unneeded slides from highest to lowest index to avoid shifting issues
646
+ const allIndices = this.#slideManager.getAllSlideIndices();
647
+ for (let i = allIndices.length; i >= 1; i--) {
648
+ const info = this.#slideManager.getSlideInfo(i);
649
+ if (!slidesToKeep.includes(info.slideId)) {
650
+ this.#slideManager.removeSlide(i);
651
+ }
652
+ }
653
+
654
+ // Calculate new target order based on the requested slideIndices
655
+ const currentOrder = this.#slideManager.getAllSlideIndices().map(i => this.#slideManager.getSlideInfo(i).slideId);
656
+
657
+ const newOrder = slidesToKeep.map(id => {
658
+ return currentOrder.indexOf(id) + 1;
659
+ });
660
+
661
+ // Only reorder if needed
662
+ if (newOrder.join(',') !== currentOrder.map((_, i) => i + 1).join(',')) {
663
+ this.#slideManager.reorderSlides(newOrder);
664
+ }
665
+
666
+ logger.debug(`Imported ${slideIndices.length} slide(s).`);
667
+ return this;
668
+ }
669
+
670
+ /**
671
+ * Returns presentation metadata (title, author, slide count, etc.)
672
+ *
673
+ * @returns {PresentationInfo} Metadata object.
674
+ */
675
+ getInfo() {
676
+ this.#assertLoaded();
677
+ return {
678
+ slideCount: this.#slideManager.slideCount,
679
+ title: this.#zipManager.getCoreProperty('dc:title') || '',
680
+ author: this.#zipManager.getCoreProperty('dc:creator') || '',
681
+ created: this.#zipManager.getCoreProperty('dcterms:created') || '',
682
+ modified: this.#zipManager.getCoreProperty('dcterms:modified') || '',
683
+ slides: this.#slideManager.getAllSlideInfo(),
684
+ mediaCount: this.#mediaManager.mediaCount,
685
+ };
686
+ }
687
+
688
+ /**
689
+ * Validates the XML structure of the current PPTX.
690
+ * Reports issues with relationship IDs, missing parts, etc.
691
+ *
692
+ * @returns {ValidationResult} Object with `valid`, `errors`, and `warnings` arrays.
693
+ */
694
+ validate() {
695
+ this.#assertLoaded();
696
+ return this.#slideManager.validateStructure(this.#relationshipManager, this.#zipManager);
697
+ }
698
+
699
+ /**
700
+ * Repairs corrupted OpenXML structure, relationships, and content types.
701
+ * Removes orphan relationships, rebuilds slide references, and fixes missing entries.
702
+ *
703
+ * @returns {Promise<PPTXTemplater>} this (chainable)
704
+ */
705
+ async repair() {
706
+ this.#assertLoaded();
707
+
708
+ // 1. Rebuild presentation.xml slide mappings
709
+ this.#slideManager.rebuildPresentationSlideOrder();
710
+
711
+ // 2. Remove orphan relationships
712
+ this.#relationshipManager.removeOrphanRelationships(this.#zipManager);
713
+
714
+ logger.info('PPTX repair complete.');
715
+ return this;
716
+ }
717
+
718
+ /**
719
+ * Logs all relationships across the presentation to the console for debugging.
720
+ * @returns {PPTXTemplater} this (chainable)
721
+ */
722
+ debugRelationships() {
723
+ this.#assertLoaded();
724
+ const files = this.#zipManager.listFiles('').filter(f => f.endsWith('.rels'));
725
+ console.log('=== Relationship Graph ===');
726
+ for (const file of files) {
727
+ console.log(`\n${file}:`);
728
+ const rels = this.#relationshipManager.getRelationships(file.replace('_rels/', '').replace('.rels', ''));
729
+ rels.forEach(r => console.log(` - ${r.id} [${r.type.split('/').pop()}] -> ${r.target}`));
730
+ }
731
+ return this;
732
+ }
733
+
734
+ /**
735
+ * Inspects a specific slide's structure and relationships.
736
+ * @param {number} slideIndex - 1-based slide index.
737
+ * @returns {PPTXTemplater} this (chainable)
738
+ */
739
+ inspectSlide(slideIndex) {
740
+ this.#assertLoaded();
741
+ const info = this.#slideManager.getSlideInfo(slideIndex);
742
+ const xml = this.#slideManager.getSlideXml(slideIndex);
743
+ const rels = this.#relationshipManager.getRelationships(info.zipPath);
744
+
745
+ console.log(`=== Slide ${slideIndex} Inspection ===`);
746
+ console.log(`Path: ${info.zipPath}`);
747
+ console.log(`ID: ${info.slideId}`);
748
+ console.log(`rId: ${info.relationshipId}`);
749
+ console.log(`Title: ${info.title}`);
750
+ console.log(`XML Size: ${xml.length} characters`);
751
+ console.log(`Relationships (${rels.length}):`);
752
+ rels.forEach(r => console.log(` - ${r.id} [${r.type.split('/').pop()}] -> ${r.target}`));
753
+
754
+ return this;
755
+ }
756
+
757
+ /**
758
+ * Inspects and logs the raw XML of any file in the ZIP.
759
+ * @param {string} xmlPath - Path inside the ZIP (e.g., 'ppt/slides/slide1.xml')
760
+ * @returns {Promise<PPTXTemplater>} this (chainable)
761
+ */
762
+ async inspectXML(xmlPath) {
763
+ this.#assertLoaded();
764
+ const xml = await this.#zipManager.readFile(xmlPath);
765
+ console.log(`=== XML Inspection: ${xmlPath} ===`);
766
+ if (!xml) {
767
+ console.log('(File not found or empty)');
768
+ } else {
769
+ console.log(xml.substring(0, 1500) + (xml.length > 1500 ? '...\n[Truncated]' : ''));
770
+ }
771
+ return this;
772
+ }
773
+
774
+ /**
775
+ * Validates all charts in the presentation to ensure they are not corrupted.
776
+ * Checks XML, caches, and embedded workbook references.
777
+ *
778
+ * @returns {Promise<Object>} Validation results for charts.
779
+ */
780
+ async validateCharts() {
781
+ this.#assertLoaded();
782
+ const issues = { valid: true, errors: [], warnings: [] };
783
+
784
+ // We lazy import ChartRelationshipManager so we don't circularly depend if not needed
785
+ const { ChartRelationshipManager } = await import('../managers/charts/ChartRelationshipManager.js');
786
+
787
+ const chartFiles = this.#zipManager.listFiles('ppt/charts/')
788
+ .filter(f => {
789
+ const name = f.split('/').pop();
790
+ return name.startsWith('chart') && name.endsWith('.xml') && !f.includes('_rels');
791
+ });
792
+
793
+ for (const chartPath of chartFiles) {
794
+ const relIssues = ChartRelationshipManager.validateChartRelationships(this.#relationshipManager, this.#zipManager, chartPath);
795
+ issues.errors.push(...relIssues.errors);
796
+ issues.warnings.push(...relIssues.warnings);
797
+ }
798
+
799
+ if (issues.errors.length > 0) issues.valid = false;
800
+ return issues;
801
+ }
802
+
803
+ /**
804
+ * Repairs common chart corruption issues such as broken caches,
805
+ * missing embedded workbooks, or orphan nodes.
806
+ *
807
+ * @returns {Promise<PPTXTemplater>} this
808
+ */
809
+ async repairCharts() {
810
+ this.#assertLoaded();
811
+ logger.info('Repairing charts...');
812
+
813
+ // Check all charts for missing embedded workbooks
814
+ const chartFiles = this.#zipManager.listFiles('ppt/charts/')
815
+ .filter(f => {
816
+ const name = f.split('/').pop();
817
+ return name.startsWith('chart') && name.endsWith('.xml') && !f.includes('_rels');
818
+ });
819
+ for (const chartPath of chartFiles) {
820
+ const rels = this.#relationshipManager.getRelationshipsByType(chartPath, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/package');
821
+ for (const rel of rels) {
822
+ const xlsxPath = this.#relationshipManager.resolveTarget(chartPath, rel.target);
823
+ if (!this.#zipManager.hasFile(xlsxPath)) {
824
+ logger.warn(`Chart ${chartPath} has broken workbook reference ${rel.id}, removing to prevent repair mode.`);
825
+ this.#relationshipManager.removeRelationship(chartPath, rel.id);
826
+
827
+ // Also strip c:externalData from chart XML to prevent PowerPoint looking for it
828
+ const xml = await this.#zipManager.readFile(chartPath);
829
+ if (xml) {
830
+ const updated = xml.replace(/<c:externalData[^>]*r:id="[^"]*"[^>]*>/, '').replace(/<\/c:externalData>/, '');
831
+ this.#zipManager.writeFile(chartPath, updated);
832
+ }
833
+ }
834
+ }
835
+ }
836
+
837
+ return this;
838
+ }
839
+
840
+ /**
841
+ * Inspects a specific chart's metadata and structure.
842
+ *
843
+ * @param {string} chartId
844
+ */
845
+ inspectChart(chartId) {
846
+ this.#assertLoaded();
847
+ console.log(`=== Chart Inspection: ${chartId} ===`);
848
+ // Find chart across all slides to get info
849
+ let found = false;
850
+ for (const i of this.#slideManager.getAllSlideIndices()) {
851
+ try {
852
+ const info = this.#chartManager.getChartsInSlide(i, this.#slideManager, this.#relationshipManager);
853
+ const chart = info.find(c => c.zipPath.toLowerCase().includes(chartId.toLowerCase()) || c.rId === chartId);
854
+ if (chart) {
855
+ console.log(`Found on Slide ${i}`);
856
+ console.log(`ZIP Path: ${chart.zipPath}`);
857
+ console.log(`Relationship ID: ${chart.rId}`);
858
+ found = true;
859
+ break;
860
+ }
861
+ } catch (e) {}
862
+ }
863
+ if (!found) console.log('Chart not found.');
864
+ return this;
865
+ }
866
+
867
+ /**
868
+ * Inspects and logs the raw XML of a chart file.
869
+ *
870
+ * @param {string} chartFileName
871
+ */
872
+ async inspectChartXML(chartFileName) {
873
+ const fullPath = chartFileName.includes('/') ? chartFileName : `ppt/charts/${chartFileName}`;
874
+ await this.inspectXML(fullPath);
875
+ return this;
876
+ }
877
+
878
+ /**
879
+ * Logs all chart relationships.
880
+ */
881
+ debugChartRelationships() {
882
+ this.#assertLoaded();
883
+ console.log('=== Chart Relationships ===');
884
+ const chartFiles = this.#zipManager.listFiles('ppt/charts/')
885
+ .filter(f => {
886
+ const name = f.split('/').pop();
887
+ return name.startsWith('chart') && name.endsWith('.xml') && !f.includes('_rels');
888
+ });
889
+ for (const chartPath of chartFiles) {
890
+ console.log(`\n${chartPath}:`);
891
+ const rels = this.#relationshipManager.getRelationships(chartPath);
892
+ rels.forEach(r => console.log(` - ${r.id} [${r.type.split('/').pop()}] -> ${r.target}`));
893
+ }
894
+ return this;
895
+ }
896
+
897
+ /**
898
+ * Saves the modified PPTX to a file on disk.
899
+ *
900
+ * @param {string} filePath - Output file path (e.g., './output/report.pptx').
901
+ * @returns {Promise<void>}
902
+ *
903
+ * @example
904
+ * await ppt.saveToFile('./output/report.pptx');
905
+ */
906
+ async saveToFile(filePath) {
907
+ this.#assertLoaded();
908
+ await this.#outputWriter.saveToFile(filePath, this.#slideManager, this.#zipManager);
909
+ logger.info(`Saved PPTX to ${filePath}`);
910
+ }
911
+
912
+ /**
913
+ * Returns the PPTX content as a Node.js Buffer.
914
+ * Useful for HTTP responses, email attachments, etc.
915
+ *
916
+ * @returns {Promise<Buffer>} Buffer containing PPTX binary data.
917
+ *
918
+ * @example
919
+ * const buffer = await ppt.toBuffer();
920
+ * res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.presentationml.presentation');
921
+ * res.send(buffer);
922
+ */
923
+ async toBuffer() {
924
+ this.#assertLoaded();
925
+ return this.#outputWriter.toBuffer(this.#slideManager, this.#zipManager);
926
+ }
927
+
928
+ /**
929
+ * Returns the PPTX content as a readable Node.js Stream.
930
+ * Ideal for streaming large presentations to HTTP responses.
931
+ *
932
+ * @returns {Promise<NodeJS.ReadableStream>} Readable stream of PPTX data.
933
+ *
934
+ * @example
935
+ * const stream = await ppt.toStream();
936
+ * stream.pipe(res);
937
+ */
938
+ async toStream() {
939
+ this.#assertLoaded();
940
+ return this.#outputWriter.toStream(this.#slideManager, this.#zipManager);
941
+ }
942
+
943
+ /**
944
+ * Returns the total number of slides in the loaded presentation.
945
+ * @type {number}
946
+ */
947
+ get slideCount() {
948
+ return this.#slideManager.slideCount;
949
+ }
950
+
951
+ // --- Public Getters for Internal Managers ---
952
+ get zipManager() { return this.#zipManager; }
953
+ get xmlParser() { return this.#xmlParser; }
954
+ get contentTypesManager() { return this.#contentTypesManager; }
955
+ get relationshipManager() { return this.#relationshipManager; }
956
+ get slideManager() { return this.#slideManager; }
957
+ get chartManager() { return this.#chartManager; }
958
+ get tableManager() { return this.#tableManager; }
959
+ get hyperlinkManager() { return this.#hyperlinkManager; }
960
+ get mediaManager() { return this.#mediaManager; }
961
+ }