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.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/package.json +83 -0
- package/src/cli/commands/build.js +79 -0
- package/src/cli/commands/debug.js +46 -0
- package/src/cli/commands/extract.js +42 -0
- package/src/cli/commands/inspect.js +39 -0
- package/src/cli/commands/validate.js +36 -0
- package/src/cli/index.js +132 -0
- package/src/core/OutputWriter.js +181 -0
- package/src/core/PPTXTemplater.js +961 -0
- package/src/core/TemplateEngine.js +321 -0
- package/src/index.js +43 -0
- package/src/managers/ChartManager.js +317 -0
- package/src/managers/ContentTypesManager.js +160 -0
- package/src/managers/HyperlinkManager.js +451 -0
- package/src/managers/MediaManager.js +307 -0
- package/src/managers/RelationshipManager.js +401 -0
- package/src/managers/SlideManager.js +950 -0
- package/src/managers/TableManager.js +416 -0
- package/src/managers/ZipManager.js +298 -0
- package/src/managers/charts/ChartCacheGenerator.js +156 -0
- package/src/managers/charts/ChartParser.js +43 -0
- package/src/managers/charts/ChartRelationshipManager.js +33 -0
- package/src/managers/charts/ChartWorkbookUpdater.js +130 -0
- package/src/parsers/XMLParser.js +291 -0
- package/src/templates/blankPptx.js +1 -0
- package/src/templates/slideTemplate.js +314 -0
- package/src/utils/contentTypesHelper.js +149 -0
- package/src/utils/errors.js +129 -0
- package/src/utils/idUtils.js +54 -0
- package/src/utils/logger.js +113 -0
- package/src/utils/relationshipUtils.js +89 -0
- 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
|
+
}
|