node-pptx-templater 2.0.0-alpha.1 → 2.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/OutputWriter.js +15 -2
- package/src/core/PPTXTemplater.js +21 -5
- package/src/core/ValidationEngine.js +40 -0
- package/src/managers/ContentTypesManager.js +13 -0
- package/src/managers/MediaManager.js +38 -0
- package/src/managers/RelationshipManager.js +21 -0
- package/src/managers/SlideManager.js +951 -60
- package/src/utils/relationshipUtils.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.2",
|
|
4
4
|
"description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"module": "./src/index.mjs",
|
package/src/core/OutputWriter.js
CHANGED
|
@@ -25,6 +25,8 @@ class OutputWriter {
|
|
|
25
25
|
#zipManager
|
|
26
26
|
/** @private @type {ContentTypesManager} */
|
|
27
27
|
#contentTypesManager
|
|
28
|
+
/** @private @type {RelationshipManager} */
|
|
29
|
+
#relationshipManager
|
|
28
30
|
|
|
29
31
|
/** @type {boolean} */
|
|
30
32
|
debugZip = false
|
|
@@ -32,10 +34,12 @@ class OutputWriter {
|
|
|
32
34
|
/**
|
|
33
35
|
* @param {ZipManager} zipManager
|
|
34
36
|
* @param {ContentTypesManager} contentTypesManager
|
|
37
|
+
* @param {RelationshipManager} [relationshipManager]
|
|
35
38
|
*/
|
|
36
|
-
constructor(zipManager, contentTypesManager) {
|
|
39
|
+
constructor(zipManager, contentTypesManager, relationshipManager = null) {
|
|
37
40
|
this.#zipManager = zipManager
|
|
38
41
|
this.#contentTypesManager = contentTypesManager
|
|
42
|
+
this.#relationshipManager = relationshipManager
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/**
|
|
@@ -71,9 +75,18 @@ class OutputWriter {
|
|
|
71
75
|
if (slideManager && typeof slideManager.flush === 'function') {
|
|
72
76
|
slideManager.flush()
|
|
73
77
|
}
|
|
78
|
+
// Complete all pending async mutations (slide duplication, chart writes, etc.)
|
|
79
|
+
// BEFORE structural normalization so sldIdLst/rels match the final slide set.
|
|
80
|
+
await zipManager.waitForPendingWrites()
|
|
81
|
+
if (
|
|
82
|
+
slideManager &&
|
|
83
|
+
typeof slideManager.normalizeStructure === 'function' &&
|
|
84
|
+
this.#relationshipManager
|
|
85
|
+
) {
|
|
86
|
+
await slideManager.normalizeStructure(this.#relationshipManager, this.#contentTypesManager)
|
|
87
|
+
}
|
|
74
88
|
await this.#flushAllSlides(slideManager, zipManager)
|
|
75
89
|
this.#contentTypesManager.flush(zipManager)
|
|
76
|
-
await zipManager.waitForPendingWrites()
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/**
|
|
@@ -186,7 +186,11 @@ class PPTXTemplater {
|
|
|
186
186
|
this.#textManager = new TextManager(this.#xmlParser)
|
|
187
187
|
this.#templateEngine = new TemplateEngine(this.#xmlParser)
|
|
188
188
|
this.#zOrderManager = new ZOrderManager(this.#xmlParser)
|
|
189
|
-
this.#outputWriter = new OutputWriter(
|
|
189
|
+
this.#outputWriter = new OutputWriter(
|
|
190
|
+
this.#zipManager,
|
|
191
|
+
this.#contentTypesManager,
|
|
192
|
+
this.#relationshipManager
|
|
193
|
+
)
|
|
190
194
|
|
|
191
195
|
this.#profiler = {
|
|
192
196
|
enabled: false,
|
|
@@ -966,8 +970,14 @@ class PPTXTemplater {
|
|
|
966
970
|
*/
|
|
967
971
|
cloneSlide(sourceSlideNumber, atPosition) {
|
|
968
972
|
this.#assertLoaded()
|
|
969
|
-
this.#slideManager.cloneSlide(
|
|
970
|
-
|
|
973
|
+
const promise = this.#slideManager.cloneSlide(
|
|
974
|
+
sourceSlideNumber,
|
|
975
|
+
atPosition,
|
|
976
|
+
this.#relationshipManager,
|
|
977
|
+
this.#mediaManager
|
|
978
|
+
)
|
|
979
|
+
this.#zipManager.addPendingPromise(promise)
|
|
980
|
+
return promise.then(() => this)
|
|
971
981
|
}
|
|
972
982
|
|
|
973
983
|
/**
|
|
@@ -1467,8 +1477,14 @@ class PPTXTemplater {
|
|
|
1467
1477
|
*/
|
|
1468
1478
|
duplicateSlide(slideIndex, atPosition) {
|
|
1469
1479
|
this.#assertLoaded()
|
|
1470
|
-
this.#slideManager.duplicateSlide(
|
|
1471
|
-
|
|
1480
|
+
const promise = this.#slideManager.duplicateSlide(
|
|
1481
|
+
slideIndex,
|
|
1482
|
+
atPosition,
|
|
1483
|
+
this.#relationshipManager,
|
|
1484
|
+
this.#mediaManager
|
|
1485
|
+
)
|
|
1486
|
+
this.#zipManager.addPendingPromise(promise)
|
|
1487
|
+
return promise.then(() => this)
|
|
1472
1488
|
}
|
|
1473
1489
|
|
|
1474
1490
|
/**
|
|
@@ -38,6 +38,11 @@ class ValidationEngine {
|
|
|
38
38
|
errors.push(...presRelResult.errors.map(e => `Presentation relationship error: ${e}`))
|
|
39
39
|
warnings.push(...presRelResult.warnings.map(w => `Presentation relationship warning: ${w}`))
|
|
40
40
|
|
|
41
|
+
// 3. Verify sldIdLst r:id values resolve to slide relationships in presentation.xml.rels
|
|
42
|
+
const sldIdResult = this.validatePresentationSlideIds(ppt)
|
|
43
|
+
errors.push(...sldIdResult.errors)
|
|
44
|
+
warnings.push(...sldIdResult.warnings)
|
|
45
|
+
|
|
41
46
|
return {
|
|
42
47
|
valid: errors.length === 0,
|
|
43
48
|
errors,
|
|
@@ -206,6 +211,41 @@ class ValidationEngine {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Ensures each p:sldId entry references a valid slide relationship in presentation.xml.rels.
|
|
216
|
+
*
|
|
217
|
+
* @param {PPTXTemplater} ppt
|
|
218
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
219
|
+
*/
|
|
220
|
+
static validatePresentationSlideIds(ppt) {
|
|
221
|
+
const errors = []
|
|
222
|
+
const warnings = []
|
|
223
|
+
|
|
224
|
+
const slides = ppt.slideManager.getAllSlideInfo()
|
|
225
|
+
if (slides.length === 0) {
|
|
226
|
+
return { valid: true, errors, warnings }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const presRels = ppt.relationshipManager
|
|
230
|
+
.getRelationships('ppt/presentation.xml')
|
|
231
|
+
.filter(r => r.type.endsWith('/slide'))
|
|
232
|
+
const slideRelIds = new Set(presRels.map(r => r.id))
|
|
233
|
+
|
|
234
|
+
for (const slide of slides) {
|
|
235
|
+
if (!slideRelIds.has(slide.relationshipId)) {
|
|
236
|
+
errors.push(
|
|
237
|
+
`Slide ${slide.index} relationshipId "${slide.relationshipId}" is not defined in presentation.xml.rels`
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
valid: errors.length === 0,
|
|
244
|
+
errors,
|
|
245
|
+
warnings,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
209
249
|
/**
|
|
210
250
|
* Validates relationship mappings for a specific part.
|
|
211
251
|
*
|
|
@@ -150,6 +150,19 @@ class ContentTypesManager {
|
|
|
150
150
|
return this.#contentTypesObj.Types.Override.some(o => o['@_PartName'] === normalizedPart)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Gets the override content type for a specific part name, if registered.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} partName - Absolute or relative path.
|
|
157
|
+
* @returns {string|null}
|
|
158
|
+
*/
|
|
159
|
+
getOverrideContentType(partName) {
|
|
160
|
+
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}`
|
|
161
|
+
const overrides = this.#contentTypesObj?.Types?.Override || []
|
|
162
|
+
const existing = overrides.find(o => o['@_PartName'] === normalizedPart)
|
|
163
|
+
return existing ? existing['@_ContentType'] : null
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
/**
|
|
154
167
|
* Serializes back to [Content_Types].xml and writes to ZIP.
|
|
155
168
|
*
|
|
@@ -234,6 +234,44 @@ class MediaManager {
|
|
|
234
234
|
return zipPath
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Copies an existing media part to a new unique file without deduplication.
|
|
239
|
+
* Used when cloning slides so the copy does not share media relationships with the source.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} sourceZipPath - Absolute ZIP path to the source media file.
|
|
242
|
+
* @returns {Promise<string>} ZIP path of the new media file.
|
|
243
|
+
*/
|
|
244
|
+
async copyMediaAsNewPart(sourceZipPath) {
|
|
245
|
+
const data = await this.#zipManager.readBinaryFile(sourceZipPath)
|
|
246
|
+
if (!data) {
|
|
247
|
+
throw new PPTXError(`Cannot clone slide: media not found at ${sourceZipPath}`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const fileName = sourceZipPath.split('/').pop()
|
|
251
|
+
const ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : 'png'
|
|
252
|
+
const mimeType = EXT_TO_MIME[ext] || 'application/octet-stream'
|
|
253
|
+
|
|
254
|
+
let mediaId = this.#nextMediaId
|
|
255
|
+
let zipPath = `ppt/media/image${mediaId}.${ext}`
|
|
256
|
+
while (this.#zipManager.hasFile(zipPath)) {
|
|
257
|
+
mediaId++
|
|
258
|
+
zipPath = `ppt/media/image${mediaId}.${ext}`
|
|
259
|
+
}
|
|
260
|
+
this.#nextMediaId = mediaId + 1
|
|
261
|
+
|
|
262
|
+
this.#zipManager.writeBinaryFile(zipPath, data)
|
|
263
|
+
this.#mediaRegistry.set(zipPath, {
|
|
264
|
+
zipPath,
|
|
265
|
+
hash: null,
|
|
266
|
+
mimeType,
|
|
267
|
+
size: data.length,
|
|
268
|
+
})
|
|
269
|
+
this.#registerContentType(ext, mimeType)
|
|
270
|
+
|
|
271
|
+
logger.debug(`Cloned media to new part: ${zipPath}`)
|
|
272
|
+
return zipPath
|
|
273
|
+
}
|
|
274
|
+
|
|
237
275
|
/**
|
|
238
276
|
* Generates the slide XML snippet for an image element.
|
|
239
277
|
*
|
|
@@ -407,6 +407,27 @@ class RelationshipManager {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Sets the relationships array for a part in memory.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} partPath
|
|
414
|
+
* @param {Relationship[]} rels
|
|
415
|
+
*/
|
|
416
|
+
setRelationships(partPath, rels) {
|
|
417
|
+
const key = this.#getNormalizedKey(partPath)
|
|
418
|
+
this.#relationships.set(key, rels)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Flushes the relationships of a part.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} partPath
|
|
425
|
+
*/
|
|
426
|
+
flushRelationships(partPath) {
|
|
427
|
+
const key = this.#getNormalizedKey(partPath)
|
|
428
|
+
this.#flushRels(key, partPath)
|
|
429
|
+
}
|
|
430
|
+
|
|
410
431
|
/**
|
|
411
432
|
* Scans all relationships and removes those pointing to missing internal targets.
|
|
412
433
|
* This is part of the repair functionality.
|