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.
- package/CHANGELOG.md +28 -3
- package/README.md +175 -327
- package/package.json +12 -3
- package/src/cli/commands/build.js +30 -31
- package/src/cli/commands/debug.js +23 -23
- package/src/cli/commands/extract.js +21 -21
- package/src/cli/commands/inspect.js +23 -23
- package/src/cli/commands/validate.js +17 -17
- package/src/cli/index.js +39 -36
- package/src/core/OutputWriter.js +79 -78
- package/src/core/PPTXTemplater.js +856 -273
- package/src/core/TemplateEngine.js +67 -71
- package/src/core/ValidationEngine.js +246 -0
- package/src/index.js +30 -17
- package/src/managers/ChartManager.js +195 -70
- package/src/managers/ContentTypesManager.js +49 -45
- package/src/managers/HyperlinkManager.js +146 -142
- package/src/managers/ImageManager.js +336 -0
- package/src/managers/MediaManager.js +62 -81
- package/src/managers/RelationshipManager.js +99 -95
- package/src/managers/ShapeManager.js +340 -0
- package/src/managers/SlideManager.js +408 -311
- package/src/managers/TableManager.js +979 -262
- package/src/managers/TextManager.js +197 -0
- package/src/managers/ZipManager.js +69 -69
- package/src/managers/charts/ChartCacheGenerator.js +75 -58
- package/src/managers/charts/ChartParser.js +9 -13
- package/src/managers/charts/ChartRelationshipManager.js +12 -10
- package/src/managers/charts/ChartWorkbookUpdater.js +59 -56
- package/src/parsers/XMLParser.js +47 -50
- package/src/templates/blankPptx.js +3 -2
- package/src/templates/slideTemplate.js +28 -34
- package/src/utils/contentTypesHelper.js +40 -54
- package/src/utils/errors.js +18 -18
- package/src/utils/idUtils.js +16 -14
- package/src/utils/logger.js +18 -16
- package/src/utils/relationshipUtils.js +19 -20
- 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 {
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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(
|
|
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
|
|
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(
|
|
598
|
-
|
|
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(
|
|
607
|
-
|
|
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(
|
|
626
|
-
|
|
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(
|
|
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(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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(
|
|
683
|
-
|
|
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'] = (
|
|
828
|
-
? sldIdLst['p:sldId']
|
|
829
|
-
|
|
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']
|
|
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']
|
|
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(
|
|
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 }
|