node-pptx-templater 1.0.2 → 1.0.4

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 (38) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +175 -327
  3. package/package.json +12 -3
  4. package/src/cli/commands/build.js +30 -31
  5. package/src/cli/commands/debug.js +23 -23
  6. package/src/cli/commands/extract.js +21 -21
  7. package/src/cli/commands/inspect.js +23 -23
  8. package/src/cli/commands/validate.js +17 -17
  9. package/src/cli/index.js +39 -36
  10. package/src/core/OutputWriter.js +79 -78
  11. package/src/core/PPTXTemplater.js +856 -273
  12. package/src/core/TemplateEngine.js +67 -71
  13. package/src/core/ValidationEngine.js +246 -0
  14. package/src/index.js +30 -17
  15. package/src/managers/ChartManager.js +195 -70
  16. package/src/managers/ContentTypesManager.js +49 -45
  17. package/src/managers/HyperlinkManager.js +146 -142
  18. package/src/managers/ImageManager.js +336 -0
  19. package/src/managers/MediaManager.js +62 -81
  20. package/src/managers/RelationshipManager.js +99 -95
  21. package/src/managers/ShapeManager.js +340 -0
  22. package/src/managers/SlideManager.js +408 -311
  23. package/src/managers/TableManager.js +979 -262
  24. package/src/managers/TextManager.js +197 -0
  25. package/src/managers/ZipManager.js +69 -69
  26. package/src/managers/charts/ChartCacheGenerator.js +75 -58
  27. package/src/managers/charts/ChartParser.js +9 -13
  28. package/src/managers/charts/ChartRelationshipManager.js +12 -10
  29. package/src/managers/charts/ChartWorkbookUpdater.js +59 -56
  30. package/src/parsers/XMLParser.js +47 -50
  31. package/src/templates/blankPptx.js +3 -2
  32. package/src/templates/slideTemplate.js +28 -34
  33. package/src/utils/contentTypesHelper.js +40 -54
  34. package/src/utils/errors.js +18 -18
  35. package/src/utils/idUtils.js +16 -14
  36. package/src/utils/logger.js +18 -16
  37. package/src/utils/relationshipUtils.js +19 -20
  38. package/src/utils/xmlUtils.js +26 -26
@@ -24,17 +24,16 @@
24
24
  * - p:cxnSp → Connectors
25
25
  */
26
26
 
27
- const { createLogger } = require('../utils/logger.js');
28
- const { PPTXError, SlideNotFoundError } = require('../utils/errors.js');
29
- const { REL_TYPES } = require('./RelationshipManager.js');
30
- const { buildNewSlideXml } = require('../templates/slideTemplate.js');
31
- const { generateUniqueId } = require('../utils/idUtils.js');
32
- const { remapRelationshipIds } = require('../utils/relationshipUtils.js');
27
+ const { createLogger } = require('../utils/logger.js')
28
+ const { PPTXError, SlideNotFoundError } = require('../utils/errors.js')
29
+ const { REL_TYPES } = require('./RelationshipManager.js')
30
+ const { buildNewSlideXml } = require('../templates/slideTemplate.js')
31
+ const { remapRelationshipIds } = require('../utils/relationshipUtils.js')
33
32
 
34
- const logger = createLogger('SlideManager');
33
+ const logger = createLogger('SlideManager')
35
34
 
36
35
  /** MIME type for PPTX slide parts. */
37
- const SLIDE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml';
36
+ const SLIDE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'
38
37
 
39
38
  /**
40
39
  * @typedef {Object} SlideInfo
@@ -52,37 +51,37 @@ const SLIDE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.presen
52
51
  */
53
52
  class SlideManager {
54
53
  /** @private @type {XMLParser} */
55
- #xmlParser;
54
+ #xmlParser
56
55
  /** @private @type {RelationshipManager} */
57
- #relationshipManager;
56
+ #relationshipManager
58
57
  /** @private @type {ContentTypesManager} */
59
- #contentTypesManager;
58
+ #contentTypesManager
60
59
  /** @private @type {ZipManager} */
61
- #zipManager;
60
+ #zipManager
62
61
 
63
62
  /**
64
63
  * Slide registry: maps 1-based index → SlideInfo.
65
64
  * @private @type {Map<number, SlideInfo>}
66
65
  */
67
- #slides = new Map();
66
+ #slides = new Map()
68
67
 
69
68
  /**
70
69
  * Raw XML cache: maps zipPath → XML string.
71
70
  * @private @type {Map<string, string>}
72
71
  */
73
- #slideXmlCache = new Map();
72
+ #slideXmlCache = new Map()
74
73
 
75
74
  /**
76
75
  * Custom tags: maps tag → array of 1-based indices.
77
76
  * @private @type {Map<string, number[]>}
78
77
  */
79
- #tags = new Map();
78
+ #tags = new Map()
80
79
 
81
80
  /**
82
81
  * Parsed presentation.xml object (cached for modification).
83
82
  * @private @type {Object}
84
83
  */
85
- #presentationObj = null;
84
+ #presentationObj = null
86
85
 
87
86
  /**
88
87
  * @param {XMLParser} xmlParser
@@ -90,9 +89,9 @@ class SlideManager {
90
89
  * @param {ContentTypesManager} contentTypesManager
91
90
  */
92
91
  constructor(xmlParser, relationshipManager, contentTypesManager) {
93
- this.#xmlParser = xmlParser;
94
- this.#relationshipManager = relationshipManager;
95
- this.#contentTypesManager = contentTypesManager;
92
+ this.#xmlParser = xmlParser
93
+ this.#relationshipManager = relationshipManager
94
+ this.#contentTypesManager = contentTypesManager
96
95
  }
97
96
 
98
97
  /**
@@ -102,17 +101,17 @@ class SlideManager {
102
101
  * @returns {Promise<void>}
103
102
  */
104
103
  async initialize(zipManager) {
105
- this.#zipManager = zipManager;
106
- const presentationXml = await zipManager.readFile('ppt/presentation.xml');
104
+ this.#zipManager = zipManager
105
+ const presentationXml = await zipManager.readFile('ppt/presentation.xml')
107
106
 
108
107
  if (!presentationXml) {
109
108
  // If no presentation.xml, create a blank one
110
- await this.#initializeBlankPresentation();
111
- return;
109
+ await this.#initializeBlankPresentation()
110
+ return
112
111
  }
113
112
 
114
- this.#presentationObj = this.#xmlParser.parse(presentationXml, 'presentation.xml');
115
- await this.#discoverSlides(zipManager);
113
+ this.#presentationObj = this.#xmlParser.parse(presentationXml, 'presentation.xml')
114
+ await this.#discoverSlides(zipManager)
116
115
  }
117
116
 
118
117
  /**
@@ -120,47 +119,53 @@ class SlideManager {
120
119
  * @private
121
120
  */
122
121
  async #discoverSlides(zipManager) {
123
- const rels = this.#relationshipManager.getRelationships('ppt/presentation.xml');
124
- const slideRels = rels.filter(r => r.type === REL_TYPES.SLIDE);
122
+ const rels = this.#relationshipManager.getRelationships('ppt/presentation.xml')
123
+ const slideRels = rels.filter(r => r.type === REL_TYPES.SLIDE)
125
124
 
126
125
  // Get slide order from presentation.xml sldIdLst
127
- const sldIdList = this.#xmlParser.findAll(this.#presentationObj, 'p:presentation.p:sldIdLst.p:sldId');
126
+ const sldIdList = this.#xmlParser.findAll(
127
+ this.#presentationObj,
128
+ 'p:presentation.p:sldIdLst.p:sldId'
129
+ )
128
130
 
129
131
  // Map rId → slide info from sldIdLst
130
- const rIdToSlideId = new Map();
132
+ const rIdToSlideId = new Map()
131
133
  for (const sldId of sldIdList) {
132
- const rId = sldId['@_r:id'];
133
- const slideId = sldId['@_id'];
134
- if (rId) rIdToSlideId.set(rId, slideId);
134
+ const rId = sldId['@_r:id']
135
+ const slideId = sldId['@_id']
136
+ if (rId) rIdToSlideId.set(rId, slideId)
135
137
  }
136
138
 
137
139
  // Attempt to read slide titles from docProps/app.xml to preserve them
138
- let slideTitles = [];
140
+ let slideTitles = []
139
141
  try {
140
- const appXml = await zipManager.readFile('docProps/app.xml');
142
+ const appXml = await zipManager.readFile('docProps/app.xml')
141
143
  if (appXml) {
142
- const appObj = this.#xmlParser.parse(appXml, 'app.xml');
143
- const lpstrs = appObj?.Properties?.TitlesOfParts?.['vt:vector']?.['vt:lpstr'];
144
+ const appObj = this.#xmlParser.parse(appXml, 'app.xml')
145
+ const lpstrs = appObj?.Properties?.TitlesOfParts?.['vt:vector']?.['vt:lpstr']
144
146
  if (lpstrs) {
145
- const allLpstrs = Array.isArray(lpstrs) ? lpstrs : [lpstrs];
147
+ const allLpstrs = Array.isArray(lpstrs) ? lpstrs : [lpstrs]
146
148
  // Slide titles are usually the last N items where N = slide count
147
149
  // We take the last slideRels.length items
148
- slideTitles = allLpstrs.slice(-slideRels.length);
150
+ slideTitles = allLpstrs.slice(-slideRels.length)
149
151
  }
150
152
  }
151
153
  } catch (e) {
152
- logger.warn('Failed to parse app.xml for slide titles', e);
154
+ logger.warn('Failed to parse app.xml for slide titles', e)
153
155
  }
154
156
 
155
157
  // Build ordered slide list
156
- let slideIndex = 1;
158
+ let slideIndex = 1
157
159
  for (const sldId of sldIdList) {
158
- const rId = sldId['@_r:id'];
159
- const slideRel = slideRels.find(r => r.id === rId);
160
- if (!slideRel) continue;
160
+ const rId = sldId['@_r:id']
161
+ const slideRel = slideRels.find(r => r.id === rId)
162
+ if (!slideRel) continue
161
163
 
162
164
  // Resolve absolute path from relative target
163
- const zipPath = this.#relationshipManager.resolveTarget('ppt/presentation.xml', slideRel.target);
165
+ const zipPath = this.#relationshipManager.resolveTarget(
166
+ 'ppt/presentation.xml',
167
+ slideRel.target
168
+ )
164
169
 
165
170
  const slideInfo = {
166
171
  index: slideIndex,
@@ -169,13 +174,13 @@ class SlideManager {
169
174
  slideId: rIdToSlideId.get(rId) || String(256 + slideIndex),
170
175
  tags: [],
171
176
  title: slideTitles[slideIndex - 1] || '',
172
- };
177
+ }
173
178
 
174
- this.#slides.set(slideIndex, slideInfo);
175
- slideIndex++;
179
+ this.#slides.set(slideIndex, slideInfo)
180
+ slideIndex++
176
181
  }
177
182
 
178
- logger.debug(`Discovered ${this.#slides.size} slides`);
183
+ logger.debug(`Discovered ${this.#slides.size} slides`)
179
184
  }
180
185
 
181
186
  /**
@@ -183,7 +188,7 @@ class SlideManager {
183
188
  * @returns {number}
184
189
  */
185
190
  get slideCount() {
186
- return this.#slides.size;
191
+ return this.#slides.size
187
192
  }
188
193
 
189
194
  /**
@@ -191,7 +196,7 @@ class SlideManager {
191
196
  * @returns {number[]}
192
197
  */
193
198
  getAllSlideIndices() {
194
- return Array.from(this.#slides.keys()).sort((a, b) => a - b);
199
+ return Array.from(this.#slides.keys()).sort((a, b) => a - b)
195
200
  }
196
201
 
197
202
  /**
@@ -199,7 +204,7 @@ class SlideManager {
199
204
  * @returns {SlideInfo[]}
200
205
  */
201
206
  getAllSlideInfo() {
202
- return this.getAllSlideIndices().map(i => this.#slides.get(i));
207
+ return this.getAllSlideIndices().map(i => this.#slides.get(i))
203
208
  }
204
209
 
205
210
  /**
@@ -212,14 +217,14 @@ class SlideManager {
212
217
  */
213
218
  resolveSlideRef(ref) {
214
219
  if (typeof ref === 'number') {
215
- this.#assertSlideExists(ref);
216
- return [ref];
220
+ this.#assertSlideExists(ref)
221
+ return [ref]
217
222
  }
218
- const taggedIndices = this.#tags.get(ref);
223
+ const taggedIndices = this.#tags.get(ref)
219
224
  if (!taggedIndices || taggedIndices.length === 0) {
220
- throw new SlideNotFoundError(`No slides found with tag: "${ref}"`);
225
+ throw new SlideNotFoundError(`No slides found with tag: "${ref}"`)
221
226
  }
222
- return taggedIndices;
227
+ return taggedIndices
223
228
  }
224
229
 
225
230
  /**
@@ -230,15 +235,15 @@ class SlideManager {
230
235
  * @returns {string} Slide XML content.
231
236
  */
232
237
  getSlideXml(slideIndex) {
233
- this.#assertSlideExists(slideIndex);
234
- const info = this.#slides.get(slideIndex);
238
+ this.#assertSlideExists(slideIndex)
239
+ const info = this.#slides.get(slideIndex)
235
240
 
236
241
  if (this.#slideXmlCache.has(info.zipPath)) {
237
- return this.#slideXmlCache.get(info.zipPath);
242
+ return this.#slideXmlCache.get(info.zipPath)
238
243
  }
239
244
 
240
245
  // This is sync because we pre-load; async callers should use getSlideXmlAsync
241
- throw new PPTXError(`Slide ${slideIndex} XML not pre-loaded. Use getSlideXmlAsync().`);
246
+ throw new PPTXError(`Slide ${slideIndex} XML not pre-loaded. Use getSlideXmlAsync().`)
242
247
  }
243
248
 
244
249
  /**
@@ -248,16 +253,16 @@ class SlideManager {
248
253
  * @returns {Promise<string>}
249
254
  */
250
255
  async getSlideXmlAsync(slideIndex) {
251
- this.#assertSlideExists(slideIndex);
252
- const info = this.#slides.get(slideIndex);
256
+ this.#assertSlideExists(slideIndex)
257
+ const info = this.#slides.get(slideIndex)
253
258
 
254
259
  if (!this.#slideXmlCache.has(info.zipPath)) {
255
- const xml = await this.#zipManager.readFile(info.zipPath);
256
- if (!xml) throw new SlideNotFoundError(`Slide ${slideIndex} XML not found at ${info.zipPath}`);
257
- this.#slideXmlCache.set(info.zipPath, xml);
260
+ const xml = await this.#zipManager.readFile(info.zipPath)
261
+ if (!xml) throw new SlideNotFoundError(`Slide ${slideIndex} XML not found at ${info.zipPath}`)
262
+ this.#slideXmlCache.set(info.zipPath, xml)
258
263
  }
259
264
 
260
- return this.#slideXmlCache.get(info.zipPath);
265
+ return this.#slideXmlCache.get(info.zipPath)
261
266
  }
262
267
 
263
268
  /**
@@ -267,10 +272,10 @@ class SlideManager {
267
272
  * @param {string} xml - New XML content.
268
273
  */
269
274
  setSlideXml(slideIndex, xml) {
270
- this.#assertSlideExists(slideIndex);
271
- const info = this.#slides.get(slideIndex);
272
- this.#slideXmlCache.set(info.zipPath, xml);
273
- this.#zipManager.writeFile(info.zipPath, xml);
275
+ this.#assertSlideExists(slideIndex)
276
+ const info = this.#slides.get(slideIndex)
277
+ this.#slideXmlCache.set(info.zipPath, xml)
278
+ this.#zipManager.writeFile(info.zipPath, xml)
274
279
  }
275
280
 
276
281
  /**
@@ -280,13 +285,13 @@ class SlideManager {
280
285
  * @param {string} tag - Tag string.
281
286
  */
282
287
  tagSlide(slideIndex, tag) {
283
- this.#assertSlideExists(slideIndex);
284
- const info = this.#slides.get(slideIndex);
285
- if (!info.tags.includes(tag)) info.tags.push(tag);
288
+ this.#assertSlideExists(slideIndex)
289
+ const info = this.#slides.get(slideIndex)
290
+ if (!info.tags.includes(tag)) info.tags.push(tag)
286
291
 
287
- if (!this.#tags.has(tag)) this.#tags.set(tag, []);
288
- const tagList = this.#tags.get(tag);
289
- if (!tagList.includes(slideIndex)) tagList.push(slideIndex);
292
+ if (!this.#tags.has(tag)) this.#tags.set(tag, [])
293
+ const tagList = this.#tags.get(tag)
294
+ if (!tagList.includes(slideIndex)) tagList.push(slideIndex)
290
295
  }
291
296
 
292
297
  /**
@@ -296,8 +301,8 @@ class SlideManager {
296
301
  * @returns {SlideInfo}
297
302
  */
298
303
  getSlideInfo(slideIndex) {
299
- this.#assertSlideExists(slideIndex);
300
- return this.#slides.get(slideIndex);
304
+ this.#assertSlideExists(slideIndex)
305
+ return this.#slides.get(slideIndex)
301
306
  }
302
307
 
303
308
  /**
@@ -308,44 +313,42 @@ class SlideManager {
308
313
  * @param {RelationshipManager} relationshipManager
309
314
  * @param {MediaManager} mediaManager
310
315
  */
311
- addNewSlide(options, relationshipManager, mediaManager) {
312
- const newIndex = this.#slides.size + 1;
313
- let nextFileIndex = 1;
316
+ addNewSlide(options, relationshipManager, _mediaManager) {
317
+ const newIndex = this.#slides.size + 1
318
+ let nextFileIndex = 1
314
319
  while (this.#zipManager.hasFile(`ppt/slides/slide${nextFileIndex}.xml`)) {
315
- nextFileIndex++;
320
+ nextFileIndex++
316
321
  }
317
- const slideFileName = `slide${nextFileIndex}.xml`;
318
- const slideZipPath = `ppt/slides/${slideFileName}`;
319
-
320
- // Find the first available layout to reference
321
- const layoutRels = relationshipManager.getRelationshipsByType('ppt/presentation.xml', REL_TYPES.SLIDE_MASTER);
322
- const masterTarget = layoutRels[0]?.target || '../slideMasters/slideMaster1.xml';
322
+ const slideFileName = `slide${nextFileIndex}.xml`
323
+ const slideZipPath = `ppt/slides/${slideFileName}`
323
324
 
324
325
  // Generate the slide XML
325
- const slideXml = buildNewSlideXml(options, newIndex);
326
+ const slideXml = buildNewSlideXml(options, newIndex)
326
327
 
327
328
  // Write the slide XML to the ZIP
328
- this.#zipManager.writeFile(slideZipPath, slideXml);
329
- this.#slideXmlCache.set(slideZipPath, slideXml);
329
+ this.#zipManager.writeFile(slideZipPath, slideXml)
330
+ this.#slideXmlCache.set(slideZipPath, slideXml)
330
331
 
331
332
  // Add slide relationship to presentation.xml.rels
332
333
  const rId = relationshipManager.addRelationship(
333
334
  'ppt/presentation.xml',
334
335
  REL_TYPES.SLIDE,
335
336
  `slides/${slideFileName}`
336
- );
337
+ )
337
338
 
338
339
  // Add slide layout relationship to the new slide's .rels
339
340
  // Reference the first available layout
340
- const layoutRelsAll = this.#zipManager.listFiles('ppt/slideLayouts/').filter(f => f.endsWith('.xml'));
341
- const firstLayout = layoutRelsAll[0] || 'ppt/slideLayouts/slideLayout1.xml';
342
- const relativeLayoutPath = `../slideLayouts/${firstLayout.split('/').pop()}`;
343
- relationshipManager.addRelationship(slideZipPath, REL_TYPES.SLIDE_LAYOUT, relativeLayoutPath);
341
+ const layoutRelsAll = this.#zipManager
342
+ .listFiles('ppt/slideLayouts/')
343
+ .filter(f => f.endsWith('.xml'))
344
+ const firstLayout = layoutRelsAll[0] || 'ppt/slideLayouts/slideLayout1.xml'
345
+ const relativeLayoutPath = `../slideLayouts/${firstLayout.split('/').pop()}`
346
+ relationshipManager.addRelationship(slideZipPath, REL_TYPES.SLIDE_LAYOUT, relativeLayoutPath)
344
347
 
345
348
  // Generate a unique slide ID
346
- const existingIds = Array.from(this.#slides.values()).map(s => parseInt(s.slideId, 10));
347
- const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 255;
348
- const newSlideId = String(maxId + 1);
349
+ const existingIds = Array.from(this.#slides.values()).map(s => parseInt(s.slideId, 10))
350
+ const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 255
351
+ const newSlideId = String(maxId + 1)
349
352
 
350
353
  const slideInfo = {
351
354
  index: newIndex,
@@ -354,17 +357,17 @@ class SlideManager {
354
357
  slideId: newSlideId,
355
358
  tags: [],
356
359
  title: options.title || '',
357
- };
360
+ }
358
361
 
359
- this.#slides.set(newIndex, slideInfo);
362
+ this.#slides.set(newIndex, slideInfo)
360
363
 
361
364
  // Update presentation.xml sldIdLst
362
- this.#addSlideToPresentation(rId, newSlideId);
365
+ this.#addSlideToPresentation(rId, newSlideId)
363
366
 
364
367
  // Update [Content_Types].xml
365
- this.#registerSlideContentType(slideFileName);
368
+ this.#registerSlideContentType(slideFileName)
366
369
 
367
- logger.debug(`Added new slide ${newIndex} at ${slideZipPath}`);
370
+ logger.debug(`Added new slide ${newIndex} at ${slideZipPath}`)
368
371
  }
369
372
 
370
373
  /**
@@ -375,43 +378,41 @@ class SlideManager {
375
378
  * @param {RelationshipManager} relationshipManager
376
379
  */
377
380
  cloneSlide(sourceIndex, atPosition, relationshipManager) {
378
- this.#assertSlideExists(sourceIndex);
379
- const sourceInfo = this.#slides.get(sourceIndex);
381
+ this.#assertSlideExists(sourceIndex)
382
+ const sourceInfo = this.#slides.get(sourceIndex)
380
383
 
381
- const newIndex = this.#slides.size + 1;
382
- let nextFileIndex = 1;
384
+ const newIndex = this.#slides.size + 1
385
+ let nextFileIndex = 1
383
386
  while (this.#zipManager.hasFile(`ppt/slides/slide${nextFileIndex}.xml`)) {
384
- nextFileIndex++;
387
+ nextFileIndex++
385
388
  }
386
- const slideFileName = `slide${nextFileIndex}.xml`;
387
- const slideZipPath = `ppt/slides/${slideFileName}`;
389
+ const slideFileName = `slide${nextFileIndex}.xml`
390
+ const slideZipPath = `ppt/slides/${slideFileName}`
388
391
 
389
392
  // Copy the source XML
390
- let sourceXml = this.getSlideXml(sourceIndex);
393
+ let sourceXml = this.getSlideXml(sourceIndex)
391
394
 
392
395
  // Copy relationships from source slide (excluding notes, which are slide-specific)
393
- const idMap = relationshipManager.copyRelationships(
394
- sourceInfo.zipPath,
395
- slideZipPath,
396
- [REL_TYPES.NOTES_SLIDE]
397
- );
396
+ const idMap = relationshipManager.copyRelationships(sourceInfo.zipPath, slideZipPath, [
397
+ REL_TYPES.NOTES_SLIDE,
398
+ ])
398
399
 
399
400
  // Remap relationship IDs in the cloned XML to match the new targets
400
- sourceXml = remapRelationshipIds(sourceXml, idMap);
401
+ sourceXml = remapRelationshipIds(sourceXml, idMap)
401
402
 
402
- this.#zipManager.writeFile(slideZipPath, sourceXml);
403
- this.#slideXmlCache.set(slideZipPath, sourceXml);
403
+ this.#zipManager.writeFile(slideZipPath, sourceXml)
404
+ this.#slideXmlCache.set(slideZipPath, sourceXml)
404
405
 
405
406
  // Add to presentation.xml
406
407
  const rId = relationshipManager.addRelationship(
407
408
  'ppt/presentation.xml',
408
409
  REL_TYPES.SLIDE,
409
410
  `slides/${slideFileName}`
410
- );
411
+ )
411
412
 
412
- const existingIds = Array.from(this.#slides.values()).map(s => parseInt(s.slideId, 10));
413
- const maxId = Math.max(...existingIds);
414
- const newSlideId = String(maxId + 1);
413
+ const existingIds = Array.from(this.#slides.values()).map(s => parseInt(s.slideId, 10))
414
+ const maxId = Math.max(...existingIds)
415
+ const newSlideId = String(maxId + 1)
415
416
 
416
417
  const slideInfo = {
417
418
  index: newIndex,
@@ -420,13 +421,13 @@ class SlideManager {
420
421
  slideId: newSlideId,
421
422
  tags: [...sourceInfo.tags],
422
423
  title: sourceInfo.title,
423
- };
424
+ }
424
425
 
425
- this.#slides.set(newIndex, slideInfo);
426
- this.#addSlideToPresentation(rId, newSlideId);
427
- this.#registerSlideContentType(slideFileName);
426
+ this.#slides.set(newIndex, slideInfo)
427
+ this.#addSlideToPresentation(rId, newSlideId)
428
+ this.#registerSlideContentType(slideFileName)
428
429
 
429
- logger.debug(`Cloned slide ${sourceIndex} to new slide ${newIndex}`);
430
+ logger.debug(`Cloned slide ${sourceIndex} to new slide ${newIndex}`)
430
431
  }
431
432
 
432
433
  /**
@@ -435,33 +436,33 @@ class SlideManager {
435
436
  * @param {number} slideIndex - 1-based index.
436
437
  */
437
438
  removeSlide(slideIndex) {
438
- this.#assertSlideExists(slideIndex);
439
- const info = this.#slides.get(slideIndex);
439
+ this.#assertSlideExists(slideIndex)
440
+ const info = this.#slides.get(slideIndex)
440
441
 
441
442
  // Remove from ZIP
442
- this.#zipManager.removeFile(info.zipPath);
443
+ this.#zipManager.removeFile(info.zipPath)
443
444
 
444
445
  // Remove its relationships file
445
- const relsFileName = info.zipPath.split('/').pop() + '.rels';
446
- this.#zipManager.removeFile(`ppt/slides/_rels/${relsFileName}`);
446
+ const relsFileName = info.zipPath.split('/').pop() + '.rels'
447
+ this.#zipManager.removeFile(`ppt/slides/_rels/${relsFileName}`)
447
448
 
448
449
  // Remove from cache
449
- this.#slideXmlCache.delete(info.zipPath);
450
+ this.#slideXmlCache.delete(info.zipPath)
450
451
 
451
452
  // Remove relationship from presentation.xml
452
- this.#relationshipManager.removeRelationship('ppt/presentation.xml', info.relationshipId);
453
+ this.#relationshipManager.removeRelationship('ppt/presentation.xml', info.relationshipId)
453
454
 
454
455
  // Remove content type from [Content_Types].xml
455
- this.#contentTypesManager.removeOverride(info.zipPath);
456
+ this.#contentTypesManager.removeOverride(info.zipPath)
456
457
 
457
458
  // Remove from slides map and reindex
458
- this.#slides.delete(slideIndex);
459
- this.#reindexSlides();
459
+ this.#slides.delete(slideIndex)
460
+ this.#reindexSlides()
460
461
 
461
462
  // Update presentation.xml
462
- this.#removeSlideFromPresentation(info.slideId);
463
+ this.#removeSlideFromPresentation(info.slideId)
463
464
 
464
- logger.debug(`Removed slide ${slideIndex}`);
465
+ logger.debug(`Removed slide ${slideIndex}`)
465
466
  }
466
467
 
467
468
  /**
@@ -470,24 +471,26 @@ class SlideManager {
470
471
  * @param {number[]} order - Array of 1-based slide numbers in desired order.
471
472
  */
472
473
  reorderSlides(order) {
473
- const current = this.getAllSlideIndices();
474
+ const current = this.getAllSlideIndices()
474
475
  if (order.length !== current.length) {
475
- throw new PPTXError(`reorderSlides: order array length (${order.length}) must match slide count (${current.length})`);
476
+ throw new PPTXError(
477
+ `reorderSlides: order array length (${order.length}) must match slide count (${current.length})`
478
+ )
476
479
  }
477
480
 
478
- const slidesCopy = new Map(this.#slides);
479
- this.#slides.clear();
481
+ const slidesCopy = new Map(this.#slides)
482
+ this.#slides.clear()
480
483
 
481
484
  order.forEach((oldIndex, newPos) => {
482
- const info = slidesCopy.get(oldIndex);
483
- if (!info) throw new SlideNotFoundError(`Slide ${oldIndex} not found`);
484
- info.index = newPos + 1;
485
- this.#slides.set(newPos + 1, info);
486
- });
485
+ const info = slidesCopy.get(oldIndex)
486
+ if (!info) throw new SlideNotFoundError(`Slide ${oldIndex} not found`)
487
+ info.index = newPos + 1
488
+ this.#slides.set(newPos + 1, info)
489
+ })
487
490
 
488
491
  // Rebuild presentation sldIdLst
489
- this.rebuildPresentationSlideOrder();
490
- logger.debug(`Reordered slides: [${order.join(', ')}]`);
492
+ this.rebuildPresentationSlideOrder()
493
+ logger.debug(`Reordered slides: [${order.join(', ')}]`)
491
494
  }
492
495
 
493
496
  /**
@@ -497,35 +500,35 @@ class SlideManager {
497
500
  * @returns {SlideInfo|null}
498
501
  */
499
502
  resolveSlideInfo(slideRef) {
500
- let index;
503
+ let index
501
504
  if (typeof slideRef === 'number') {
502
- index = slideRef;
505
+ index = slideRef
503
506
  } else {
504
507
  // 1. Try finding by slideId string
505
508
  for (const info of this.#slides.values()) {
506
509
  if (info.slideId === String(slideRef)) {
507
- return info;
510
+ return info
508
511
  }
509
512
  }
510
513
  // 2. Try finding by tag
511
514
  try {
512
- const indices = this.resolveSlideRef(slideRef);
515
+ const indices = this.resolveSlideRef(slideRef)
513
516
  if (indices && indices.length > 0) {
514
- index = indices[0];
517
+ index = indices[0]
515
518
  }
516
519
  } catch (e) {
517
520
  // Fallback: parse as slide index
518
- const parsedNum = parseInt(slideRef, 10);
521
+ const parsedNum = parseInt(slideRef, 10)
519
522
  if (!isNaN(parsedNum)) {
520
- index = parsedNum;
523
+ index = parsedNum
521
524
  }
522
525
  }
523
526
  }
524
527
 
525
528
  if (index !== undefined) {
526
- return this.#slides.get(index) || null;
529
+ return this.#slides.get(index) || null
527
530
  }
528
- return null;
531
+ return null
529
532
  }
530
533
 
531
534
  /**
@@ -538,31 +541,31 @@ class SlideManager {
538
541
  * @returns {Promise<number>} Index of the imported slide.
539
542
  */
540
543
  async importSlide(sourceEngine, slideRef, mediaManager) {
541
- const sourceSlideManager = sourceEngine.slideManager;
542
- const sourceRelManager = sourceEngine.relationshipManager;
543
- const sourceZip = sourceEngine.zipManager;
544
+ const sourceSlideManager = sourceEngine.slideManager
545
+ const sourceRelManager = sourceEngine.relationshipManager
546
+ const sourceZip = sourceEngine.zipManager
544
547
 
545
- const sourceSlideInfo = sourceSlideManager.resolveSlideInfo(slideRef);
548
+ const sourceSlideInfo = sourceSlideManager.resolveSlideInfo(slideRef)
546
549
  if (!sourceSlideInfo) {
547
- throw new SlideNotFoundError(`Source slide "${slideRef}" not found`);
550
+ throw new SlideNotFoundError(`Source slide "${slideRef}" not found`)
548
551
  }
549
552
 
550
- const newIndex = this.#slides.size + 1;
551
- let nextFileIndex = 1;
553
+ const newIndex = this.#slides.size + 1
554
+ let nextFileIndex = 1
552
555
  while (this.#zipManager.hasFile(`ppt/slides/slide${nextFileIndex}.xml`)) {
553
- nextFileIndex++;
556
+ nextFileIndex++
554
557
  }
555
- const slideFileName = `slide${nextFileIndex}.xml`;
556
- const slideZipPath = `ppt/slides/${slideFileName}`;
558
+ const slideFileName = `slide${nextFileIndex}.xml`
559
+ const slideZipPath = `ppt/slides/${slideFileName}`
557
560
 
558
561
  // Read the source slide's XML
559
- let slideXml = await sourceSlideManager.getSlideXmlAsync(sourceSlideInfo.index);
562
+ let slideXml = await sourceSlideManager.getSlideXmlAsync(sourceSlideInfo.index)
560
563
 
561
564
  // Get relationships from the source slide
562
- const sourceRels = sourceRelManager.getRelationships(sourceSlideInfo.zipPath);
565
+ const sourceRels = sourceRelManager.getRelationships(sourceSlideInfo.zipPath)
563
566
 
564
567
  // Map to track old rId -> new rId in the destination slide's .rels file
565
- const idMap = new Map();
568
+ const idMap = new Map()
566
569
 
567
570
  const EXT_TO_MIME_LOCAL = {
568
571
  png: 'image/png',
@@ -573,135 +576,160 @@ class SlideManager {
573
576
  bmp: 'image/bmp',
574
577
  xml: 'application/xml',
575
578
  rels: 'application/vnd.openxmlformats-package.relationships+xml',
576
- };
579
+ }
577
580
 
578
581
  for (const rel of sourceRels) {
579
- const resolvedTarget = sourceRelManager.resolveTarget(sourceSlideInfo.zipPath, rel.target);
582
+ const resolvedTarget = sourceRelManager.resolveTarget(sourceSlideInfo.zipPath, rel.target)
580
583
 
581
584
  if (rel.type === REL_TYPES.SLIDE_LAYOUT) {
582
585
  // Map to destination's slide layout.
583
- const layoutFileName = rel.target.split('/').pop();
584
- const destLayoutPath = `ppt/slideLayouts/${layoutFileName}`;
586
+ const layoutFileName = rel.target.split('/').pop()
587
+ const destLayoutPath = `ppt/slideLayouts/${layoutFileName}`
585
588
 
586
- let targetLayout = `../slideLayouts/${layoutFileName}`;
589
+ let targetLayout = `../slideLayouts/${layoutFileName}`
587
590
  if (!this.#zipManager.hasFile(destLayoutPath)) {
588
591
  // Find first available layout
589
- const layoutFiles = this.#zipManager.listFiles('ppt/slideLayouts/').filter(f => f.endsWith('.xml'));
592
+ const layoutFiles = this.#zipManager
593
+ .listFiles('ppt/slideLayouts/')
594
+ .filter(f => f.endsWith('.xml'))
590
595
  if (layoutFiles.length > 0) {
591
- targetLayout = `../slideLayouts/${layoutFiles[0].split('/').pop()}`;
596
+ targetLayout = `../slideLayouts/${layoutFiles[0].split('/').pop()}`
592
597
  } else {
593
- targetLayout = '../slideLayouts/slideLayout1.xml';
598
+ targetLayout = '../slideLayouts/slideLayout1.xml'
594
599
  }
595
600
  }
596
601
 
597
- const newRId = this.#relationshipManager.addRelationship(slideZipPath, rel.type, targetLayout);
598
- idMap.set(rel.id, newRId);
599
-
602
+ const newRId = this.#relationshipManager.addRelationship(
603
+ slideZipPath,
604
+ rel.type,
605
+ targetLayout
606
+ )
607
+ idMap.set(rel.id, newRId)
600
608
  } else if (rel.type === REL_TYPES.IMAGE) {
601
609
  // Copy media file
602
- const mediaBytes = await sourceZip.readBinaryFile(resolvedTarget);
610
+ const mediaBytes = await sourceZip.readBinaryFile(resolvedTarget)
603
611
  if (mediaBytes) {
604
- const destMediaZipPath = await mediaManager.embedImage(mediaBytes);
605
- const relativeMediaTarget = `../media/${destMediaZipPath.split('/').pop()}`;
606
- const newRId = this.#relationshipManager.addRelationship(slideZipPath, rel.type, relativeMediaTarget);
607
- idMap.set(rel.id, newRId);
612
+ const destMediaZipPath = await mediaManager.embedImage(mediaBytes)
613
+ const relativeMediaTarget = `../media/${destMediaZipPath.split('/').pop()}`
614
+ const newRId = this.#relationshipManager.addRelationship(
615
+ slideZipPath,
616
+ rel.type,
617
+ relativeMediaTarget
618
+ )
619
+ idMap.set(rel.id, newRId)
608
620
  }
609
-
610
621
  } else if (rel.type === REL_TYPES.CHART) {
611
622
  // Copy chart XML and its relationships
612
- const chartXml = await sourceZip.readFile(resolvedTarget);
623
+ const chartXml = await sourceZip.readFile(resolvedTarget)
613
624
  if (chartXml) {
614
- const chartRels = sourceRelManager.getRelationships(resolvedTarget);
625
+ const chartRels = sourceRelManager.getRelationships(resolvedTarget)
615
626
 
616
- let nextChartId = 1;
627
+ let nextChartId = 1
617
628
  while (this.#zipManager.hasFile(`ppt/charts/chart${nextChartId}.xml`)) {
618
- nextChartId++;
629
+ nextChartId++
619
630
  }
620
- const destChartZipPath = `ppt/charts/chart${nextChartId}.xml`;
621
- const chartFileName = `chart${nextChartId}.xml`;
631
+ const destChartZipPath = `ppt/charts/chart${nextChartId}.xml`
632
+ const chartFileName = `chart${nextChartId}.xml`
622
633
 
623
634
  // Handle workbook packages within charts
624
635
  for (const chartRel of chartRels) {
625
- const resolvedChartTarget = sourceRelManager.resolveTarget(resolvedTarget, chartRel.target);
626
- const workbookBytes = await sourceZip.readBinaryFile(resolvedChartTarget);
636
+ const resolvedChartTarget = sourceRelManager.resolveTarget(
637
+ resolvedTarget,
638
+ chartRel.target
639
+ )
640
+ const workbookBytes = await sourceZip.readBinaryFile(resolvedChartTarget)
627
641
 
628
642
  if (workbookBytes) {
629
- const workbookFileName = resolvedChartTarget.split('/').pop();
630
- let nextEmbedId = 1;
631
- let destWorkbookZipPath = `ppt/embeddings/Microsoft_Excel_Worksheet${nextEmbedId}.xlsx`;
643
+ const workbookFileName = resolvedChartTarget.split('/').pop()
644
+ let nextEmbedId = 1
645
+ let destWorkbookZipPath = `ppt/embeddings/Microsoft_Excel_Worksheet${nextEmbedId}.xlsx`
632
646
  if (workbookFileName.endsWith('.bin')) {
633
- destWorkbookZipPath = `ppt/embeddings/oleObject${nextEmbedId}.bin`;
647
+ destWorkbookZipPath = `ppt/embeddings/oleObject${nextEmbedId}.bin`
634
648
  }
635
649
  while (this.#zipManager.hasFile(destWorkbookZipPath)) {
636
- nextEmbedId++;
650
+ nextEmbedId++
637
651
  destWorkbookZipPath = workbookFileName.endsWith('.bin')
638
652
  ? `ppt/embeddings/oleObject${nextEmbedId}.bin`
639
- : `ppt/embeddings/Microsoft_Excel_Worksheet${nextEmbedId}.xlsx`;
653
+ : `ppt/embeddings/Microsoft_Excel_Worksheet${nextEmbedId}.xlsx`
640
654
  }
641
655
 
642
- this.#zipManager.writeBinaryFile(destWorkbookZipPath, workbookBytes);
656
+ this.#zipManager.writeBinaryFile(destWorkbookZipPath, workbookBytes)
643
657
 
644
658
  const workbookContentType = workbookFileName.endsWith('.bin')
645
659
  ? 'application/vnd.ms-office.activeX'
646
- : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
647
- this.#contentTypesManager.addOverride(destWorkbookZipPath, workbookContentType);
648
-
649
- const relativeWorkbookPath = `../embeddings/${destWorkbookZipPath.split('/').pop()}`;
650
- this.#relationshipManager.addRelationship(destChartZipPath, chartRel.type, relativeWorkbookPath);
660
+ : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
661
+ this.#contentTypesManager.addOverride(destWorkbookZipPath, workbookContentType)
662
+
663
+ const relativeWorkbookPath = `../embeddings/${destWorkbookZipPath.split('/').pop()}`
664
+ this.#relationshipManager.addRelationship(
665
+ destChartZipPath,
666
+ chartRel.type,
667
+ relativeWorkbookPath
668
+ )
651
669
  }
652
670
  }
653
671
 
654
- this.#zipManager.writeFile(destChartZipPath, chartXml);
655
- this.#contentTypesManager.addOverride(destChartZipPath, 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml');
656
-
657
- const relativeChartPath = `../charts/${chartFileName}`;
658
- const newRId = this.#relationshipManager.addRelationship(slideZipPath, rel.type, relativeChartPath);
659
- idMap.set(rel.id, newRId);
672
+ this.#zipManager.writeFile(destChartZipPath, chartXml)
673
+ this.#contentTypesManager.addOverride(
674
+ destChartZipPath,
675
+ 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'
676
+ )
677
+
678
+ const relativeChartPath = `../charts/${chartFileName}`
679
+ const newRId = this.#relationshipManager.addRelationship(
680
+ slideZipPath,
681
+ rel.type,
682
+ relativeChartPath
683
+ )
684
+ idMap.set(rel.id, newRId)
660
685
  }
661
-
662
686
  } else if (rel.type === REL_TYPES.HYPERLINK) {
663
687
  const newRId = this.#relationshipManager.addRelationship(
664
688
  slideZipPath,
665
689
  rel.type,
666
690
  rel.target,
667
691
  rel.targetMode
668
- );
669
- idMap.set(rel.id, newRId);
670
-
692
+ )
693
+ idMap.set(rel.id, newRId)
671
694
  } else {
672
695
  // Fallback for notes, themes, styles or custom XML
673
696
  if (rel.target && !rel.target.startsWith('http')) {
674
- const targetBytes = await sourceZip.readBinaryFile(resolvedTarget);
697
+ const targetBytes = await sourceZip.readBinaryFile(resolvedTarget)
675
698
  if (targetBytes && !this.#zipManager.hasFile(resolvedTarget)) {
676
- this.#zipManager.writeBinaryFile(resolvedTarget, targetBytes);
677
- const ext = resolvedTarget.split('.').pop().toLowerCase();
678
- const mime = EXT_TO_MIME_LOCAL[ext] || 'application/octet-stream';
679
- this.#contentTypesManager.addDefault(ext, mime);
699
+ this.#zipManager.writeBinaryFile(resolvedTarget, targetBytes)
700
+ const ext = resolvedTarget.split('.').pop().toLowerCase()
701
+ const mime = EXT_TO_MIME_LOCAL[ext] || 'application/octet-stream'
702
+ this.#contentTypesManager.addDefault(ext, mime)
680
703
  }
681
704
  }
682
- const newRId = this.#relationshipManager.addRelationship(slideZipPath, rel.type, rel.target, rel.targetMode);
683
- idMap.set(rel.id, newRId);
705
+ const newRId = this.#relationshipManager.addRelationship(
706
+ slideZipPath,
707
+ rel.type,
708
+ rel.target,
709
+ rel.targetMode
710
+ )
711
+ idMap.set(rel.id, newRId)
684
712
  }
685
713
  }
686
714
 
687
715
  // Remap all relationship IDs inside the imported slide XML
688
- slideXml = remapRelationshipIds(slideXml, idMap);
716
+ slideXml = remapRelationshipIds(slideXml, idMap)
689
717
 
690
718
  // Save the remapped slide XML to ZIP
691
- this.#zipManager.writeFile(slideZipPath, slideXml);
692
- this.#slideXmlCache.set(slideZipPath, slideXml);
719
+ this.#zipManager.writeFile(slideZipPath, slideXml)
720
+ this.#slideXmlCache.set(slideZipPath, slideXml)
693
721
 
694
722
  // Generate unique Slide ID
695
- const existingIds = Array.from(this.#slides.values()).map(s => parseInt(s.slideId, 10));
696
- const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 255;
697
- const newSlideId = String(maxId + 1);
723
+ const existingIds = Array.from(this.#slides.values()).map(s => parseInt(s.slideId, 10))
724
+ const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 255
725
+ const newSlideId = String(maxId + 1)
698
726
 
699
727
  // Add relationship from presentation.xml
700
728
  const rId = this.#relationshipManager.addRelationship(
701
729
  'ppt/presentation.xml',
702
730
  REL_TYPES.SLIDE,
703
731
  `slides/${slideFileName}`
704
- );
732
+ )
705
733
 
706
734
  const slideInfo = {
707
735
  index: newIndex,
@@ -710,18 +738,18 @@ class SlideManager {
710
738
  slideId: newSlideId,
711
739
  tags: [...sourceSlideInfo.tags],
712
740
  title: sourceSlideInfo.title || '',
713
- };
741
+ }
714
742
 
715
- this.#slides.set(newIndex, slideInfo);
743
+ this.#slides.set(newIndex, slideInfo)
716
744
 
717
745
  // Add entry in presentation.xml sldIdLst
718
- this.#addSlideToPresentation(rId, newSlideId);
746
+ this.#addSlideToPresentation(rId, newSlideId)
719
747
 
720
748
  // Register slide in content types
721
- this.#registerSlideContentType(slideFileName);
749
+ this.#registerSlideContentType(slideFileName)
722
750
 
723
- logger.debug(`Successfully imported slide "${slideRef}" to index ${newIndex}`);
724
- return newIndex;
751
+ logger.debug(`Successfully imported slide "${slideRef}" to index ${newIndex}`)
752
+ return newIndex
725
753
  }
726
754
 
727
755
  /**
@@ -733,24 +761,24 @@ class SlideManager {
733
761
  */
734
762
  async exportSlides(slideIndices, sourceEngine) {
735
763
  // Lazy import to avoid circular dep
736
- const { PPTXTemplater } = require('../core/PPTXTemplater.js');
764
+ const { PPTXTemplater } = require('../core/PPTXTemplater.js')
737
765
 
738
766
  // Create a blank new PPTX
739
- const newEngine = await PPTXTemplater.create();
767
+ const newEngine = await PPTXTemplater.create()
740
768
 
741
769
  // Remove the default slides from the blank template to avoid orphans
742
- const defaultSlides = newEngine.slideManager.getAllSlideIndices();
770
+ const defaultSlides = newEngine.slideManager.getAllSlideIndices()
743
771
  for (const dIdx of defaultSlides.reverse()) {
744
- newEngine.slideManager.removeSlide(dIdx);
772
+ newEngine.slideManager.removeSlide(dIdx)
745
773
  }
746
774
 
747
775
  // Copy selected slides into the new engine
748
776
  for (const idx of slideIndices) {
749
- this.#assertSlideExists(idx);
750
- await newEngine.slideManager.importSlide(sourceEngine, idx, newEngine.mediaManager);
777
+ this.#assertSlideExists(idx)
778
+ await newEngine.slideManager.importSlide(sourceEngine, idx, newEngine.mediaManager)
751
779
  }
752
780
 
753
- return newEngine;
781
+ return newEngine
754
782
  }
755
783
 
756
784
  /**
@@ -761,18 +789,18 @@ class SlideManager {
761
789
  * @returns {ValidationResult}
762
790
  */
763
791
  validateStructure(relationshipManager, zipManager) {
764
- const errors = [];
765
- const warnings = [];
792
+ const errors = []
793
+ const warnings = []
766
794
 
767
795
  for (const [index, info] of this.#slides) {
768
796
  if (!zipManager.hasFile(info.zipPath)) {
769
- errors.push(`Slide ${index}: XML file missing at ${info.zipPath}`);
797
+ errors.push(`Slide ${index}: XML file missing at ${info.zipPath}`)
770
798
  }
771
799
 
772
- const rels = relationshipManager.getRelationships(info.zipPath);
773
- const layoutRel = rels.find(r => r.type === REL_TYPES.SLIDE_LAYOUT);
800
+ const rels = relationshipManager.getRelationships(info.zipPath)
801
+ const layoutRel = rels.find(r => r.type === REL_TYPES.SLIDE_LAYOUT)
774
802
  if (!layoutRel) {
775
- warnings.push(`Slide ${index}: No slide layout relationship found`);
803
+ warnings.push(`Slide ${index}: No slide layout relationship found`)
776
804
  }
777
805
  }
778
806
 
@@ -780,7 +808,7 @@ class SlideManager {
780
808
  valid: errors.length === 0,
781
809
  errors,
782
810
  warnings,
783
- };
811
+ }
784
812
  }
785
813
 
786
814
  /**
@@ -788,9 +816,7 @@ class SlideManager {
788
816
  * @returns {Promise<void>}
789
817
  */
790
818
  async preloadAll() {
791
- await Promise.all(
792
- this.getAllSlideIndices().map(i => this.getSlideXmlAsync(i))
793
- );
819
+ await Promise.all(this.getAllSlideIndices().map(i => this.getSlideXmlAsync(i)))
794
820
  }
795
821
 
796
822
  /**
@@ -798,21 +824,21 @@ class SlideManager {
798
824
  * @private
799
825
  */
800
826
  #addSlideToPresentation(rId, slideId) {
801
- if (!this.#presentationObj) return;
827
+ if (!this.#presentationObj) return
802
828
 
803
- let sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst');
829
+ let sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst')
804
830
  if (!sldIdLst) {
805
- this.#xmlParser.setNode(this.#presentationObj, 'p:presentation.p:sldIdLst', { 'p:sldId': [] });
806
- sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst');
831
+ this.#xmlParser.setNode(this.#presentationObj, 'p:presentation.p:sldIdLst', { 'p:sldId': [] })
832
+ sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst')
807
833
  }
808
834
 
809
- if (!sldIdLst['p:sldId']) sldIdLst['p:sldId'] = [];
835
+ if (!sldIdLst['p:sldId']) sldIdLst['p:sldId'] = []
810
836
  if (!Array.isArray(sldIdLst['p:sldId'])) {
811
- sldIdLst['p:sldId'] = [sldIdLst['p:sldId']];
837
+ sldIdLst['p:sldId'] = [sldIdLst['p:sldId']]
812
838
  }
813
839
 
814
- sldIdLst['p:sldId'].push({ '@_id': slideId, '@_r:id': rId });
815
- this.#flushPresentation();
840
+ sldIdLst['p:sldId'].push({ '@_id': slideId, '@_r:id': rId })
841
+ this.#flushPresentation()
816
842
  }
817
843
 
818
844
  /**
@@ -820,19 +846,18 @@ class SlideManager {
820
846
  * @private
821
847
  */
822
848
  #removeSlideFromPresentation(slideId) {
823
- if (!this.#presentationObj) return;
824
- const sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst');
825
- if (!sldIdLst?.['p:sldId']) return;
849
+ if (!this.#presentationObj) return
850
+ const sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst')
851
+ if (!sldIdLst?.['p:sldId']) return
826
852
 
827
- sldIdLst['p:sldId'] = (Array.isArray(sldIdLst['p:sldId'])
828
- ? sldIdLst['p:sldId']
829
- : [sldIdLst['p:sldId']]
830
- ).filter(s => s['@_id'] !== slideId);
853
+ sldIdLst['p:sldId'] = (
854
+ Array.isArray(sldIdLst['p:sldId']) ? sldIdLst['p:sldId'] : [sldIdLst['p:sldId']]
855
+ ).filter(s => s['@_id'] !== slideId)
831
856
 
832
857
  // Also remove from any PowerPoint sections
833
- this.#removeSlideFromSections(slideId);
858
+ this.#removeSlideFromSections(slideId)
834
859
 
835
- this.#flushPresentation();
860
+ this.#flushPresentation()
836
861
  }
837
862
 
838
863
  /**
@@ -841,28 +866,28 @@ class SlideManager {
841
866
  * @param {string} slideId - Unique slide ID.
842
867
  */
843
868
  #removeSlideFromSections(slideId) {
844
- if (!this.#presentationObj) return;
845
- const extLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:extLst');
846
- if (!extLst?.['p:ext']) return;
869
+ if (!this.#presentationObj) return
870
+ const extLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:extLst')
871
+ if (!extLst?.['p:ext']) return
847
872
 
848
- const exts = Array.isArray(extLst['p:ext']) ? extLst['p:ext'] : [extLst['p:ext']];
873
+ const exts = Array.isArray(extLst['p:ext']) ? extLst['p:ext'] : [extLst['p:ext']]
849
874
  for (const ext of exts) {
850
- const sectionLst = ext['p14:sectionLst'];
851
- if (!sectionLst?.['p14:section']) continue;
875
+ const sectionLst = ext['p14:sectionLst']
876
+ if (!sectionLst?.['p14:section']) continue
852
877
 
853
- const sections = sectionLst['p14:section']; // Guaranteed to be array by XMLParser config
878
+ const sections = sectionLst['p14:section'] // Guaranteed to be array by XMLParser config
854
879
 
855
880
  for (const section of sections) {
856
- const sldIdLst = section['p14:sldIdLst'];
857
- if (!sldIdLst?.['p14:sldId']) continue;
881
+ const sldIdLst = section['p14:sldIdLst']
882
+ if (!sldIdLst?.['p14:sldId']) continue
858
883
 
859
- const sldIds = sldIdLst['p14:sldId']; // Guaranteed to be array by XMLParser config
860
- const targetIdStr = String(slideId);
861
- const filtered = sldIds.filter(s => String(s['@_id']) !== targetIdStr);
884
+ const sldIds = sldIdLst['p14:sldId'] // Guaranteed to be array by XMLParser config
885
+ const targetIdStr = String(slideId)
886
+ const filtered = sldIds.filter(s => String(s['@_id']) !== targetIdStr)
862
887
 
863
888
  if (filtered.length !== sldIds.length) {
864
- logger.debug(`Removing slide ${targetIdStr} from section "${section['@_name']}"`);
865
- section['p14:sldIdLst']['p14:sldId'] = filtered;
889
+ logger.debug(`Removing slide ${targetIdStr} from section "${section['@_name']}"`)
890
+ section['p14:sldIdLst']['p14:sldId'] = filtered
866
891
  }
867
892
  }
868
893
  }
@@ -872,17 +897,17 @@ class SlideManager {
872
897
  * Rebuilds presentation.xml sldIdLst in the current slide order.
873
898
  */
874
899
  rebuildPresentationSlideOrder() {
875
- if (!this.#presentationObj) return;
876
- const sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst');
877
- if (!sldIdLst) return;
900
+ if (!this.#presentationObj) return
901
+ const sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst')
902
+ if (!sldIdLst) return
878
903
 
879
904
  const ordered = this.getAllSlideIndices().map(i => {
880
- const info = this.#slides.get(i);
881
- return { '@_id': info.slideId, '@_r:id': info.relationshipId };
882
- });
905
+ const info = this.#slides.get(i)
906
+ return { '@_id': info.slideId, '@_r:id': info.relationshipId }
907
+ })
883
908
 
884
- sldIdLst['p:sldId'] = ordered;
885
- this.#flushPresentation();
909
+ sldIdLst['p:sldId'] = ordered
910
+ this.#flushPresentation()
886
911
  }
887
912
 
888
913
  /**
@@ -890,12 +915,12 @@ class SlideManager {
890
915
  * @private
891
916
  */
892
917
  #reindexSlides() {
893
- const sorted = Array.from(this.#slides.entries()).sort(([a], [b]) => a - b);
894
- this.#slides.clear();
918
+ const sorted = Array.from(this.#slides.entries()).sort(([a], [b]) => a - b)
919
+ this.#slides.clear()
895
920
  sorted.forEach(([, info], i) => {
896
- info.index = i + 1;
897
- this.#slides.set(i + 1, info);
898
- });
921
+ info.index = i + 1
922
+ this.#slides.set(i + 1, info)
923
+ })
899
924
  }
900
925
 
901
926
  /**
@@ -903,7 +928,7 @@ class SlideManager {
903
928
  * @private
904
929
  */
905
930
  #registerSlideContentType(slideFileName) {
906
- this.#contentTypesManager.addOverride(`ppt/slides/${slideFileName}`, SLIDE_CONTENT_TYPE);
931
+ this.#contentTypesManager.addOverride(`ppt/slides/${slideFileName}`, SLIDE_CONTENT_TYPE)
907
932
  }
908
933
 
909
934
  /**
@@ -911,10 +936,10 @@ class SlideManager {
911
936
  * @private
912
937
  */
913
938
  #flushPresentation() {
914
- if (!this.#presentationObj || !this.#zipManager) return;
915
- const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
916
- const xml = this.#xmlParser.build(this.#presentationObj, declaration);
917
- this.#zipManager.writeFile('ppt/presentation.xml', xml);
939
+ if (!this.#presentationObj || !this.#zipManager) return
940
+ const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
941
+ const xml = this.#xmlParser.build(this.#presentationObj, declaration)
942
+ this.#zipManager.writeFile('ppt/presentation.xml', xml)
918
943
  }
919
944
 
920
945
  /**
@@ -933,8 +958,78 @@ class SlideManager {
933
958
  'p:sldSz': { '@_cx': '9144000', '@_cy': '5143500' },
934
959
  'p:notesSz': { '@_cx': '6858000', '@_cy': '9144000' },
935
960
  },
936
- };
937
- this.#flushPresentation();
961
+ }
962
+ this.#flushPresentation()
963
+ }
964
+
965
+ /**
966
+ * Duplicates a slide.
967
+ *
968
+ * @param {number} slideIndex
969
+ * @param {number} [atPosition]
970
+ * @param {RelationshipManager} relationshipManager
971
+ * @returns {number}
972
+ */
973
+ duplicateSlide(slideIndex, atPosition, relationshipManager) {
974
+ this.cloneSlide(slideIndex, null, relationshipManager)
975
+ const count = this.slideCount
976
+ if (atPosition !== undefined && atPosition !== count) {
977
+ const order = []
978
+ for (let i = 1; i < count; i++) {
979
+ order.push(i)
980
+ }
981
+ order.splice(atPosition - 1, 0, count)
982
+ this.reorderSlides(order)
983
+ return atPosition
984
+ }
985
+ return count
986
+ }
987
+
988
+ /**
989
+ * Moves a slide to a new position.
990
+ *
991
+ * @param {number} fromIndex
992
+ * @param {number} toIndex
993
+ */
994
+ moveSlide(fromIndex, toIndex) {
995
+ this.#assertSlideExists(fromIndex)
996
+ if (toIndex < 1 || toIndex > this.slideCount) {
997
+ throw new PPTXError(`Destination index ${toIndex} out of bounds`)
998
+ }
999
+ const order = this.getAllSlideIndices()
1000
+ const [removed] = order.splice(fromIndex - 1, 1)
1001
+ order.splice(toIndex - 1, 0, removed)
1002
+ this.reorderSlides(order)
1003
+ }
1004
+
1005
+ /**
1006
+ * Inserts a new slide at a specific index.
1007
+ *
1008
+ * @param {number} slideIndex
1009
+ * @param {Object} options
1010
+ * @param {RelationshipManager} relationshipManager
1011
+ * @param {MediaManager} mediaManager
1012
+ */
1013
+ insertSlide(slideIndex, options, relationshipManager, mediaManager) {
1014
+ this.addNewSlide(options, relationshipManager, mediaManager)
1015
+ const count = this.slideCount
1016
+ if (slideIndex !== undefined && slideIndex !== count) {
1017
+ const order = []
1018
+ for (let i = 1; i < count; i++) {
1019
+ order.push(i)
1020
+ }
1021
+ order.splice(slideIndex - 1, 0, count)
1022
+ this.reorderSlides(order)
1023
+ }
1024
+ }
1025
+
1026
+ /**
1027
+ * Gets all slides.
1028
+ *
1029
+ * @returns {SlideInfo[]}
1030
+ */
1031
+ getSlides() {
1032
+ return this.getAllSlideInfo()
938
1033
  }
939
1034
 
940
1035
  /**
@@ -944,9 +1039,11 @@ class SlideManager {
944
1039
  */
945
1040
  #assertSlideExists(index) {
946
1041
  if (!this.#slides.has(index)) {
947
- throw new SlideNotFoundError(`Slide ${index} does not exist. Total slides: ${this.#slides.size}`);
1042
+ throw new SlideNotFoundError(
1043
+ `Slide ${index} does not exist. Total slides: ${this.#slides.size}`
1044
+ )
948
1045
  }
949
1046
  }
950
1047
  }
951
1048
 
952
- module.exports = { SlideManager };
1049
+ module.exports = { SlideManager }