node-pptx-templater 1.1.10 → 2.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/managers/ChartManager.js +3 -3
- package/src/managers/RelationshipManager.js +10 -0
- package/src/managers/ShapeManager.js +10 -34
- package/src/managers/SlideManager.js +61 -10
- package/src/managers/TableManager.js +2 -2
- package/src/managers/charts/ChartWorkbookUpdater.js +0 -1
- package/src/utils/relationshipUtils.js +8 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.0",
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"node-pptx-templater": "
|
|
18
|
+
"node-pptx-templater": "src/cli/index.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "vitest run",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"license": "MIT",
|
|
168
168
|
"repository": {
|
|
169
169
|
"type": "git",
|
|
170
|
-
"url": "https://github.com/jsuyog2/node-pptx-templater.git"
|
|
170
|
+
"url": "git+https://github.com/jsuyog2/node-pptx-templater.git"
|
|
171
171
|
},
|
|
172
172
|
"bugs": {
|
|
173
173
|
"url": "https://github.com/jsuyog2/node-pptx-templater/issues"
|
|
@@ -209,4 +209,4 @@
|
|
|
209
209
|
"publishConfig": {
|
|
210
210
|
"access": "public"
|
|
211
211
|
}
|
|
212
|
-
}
|
|
212
|
+
}
|
|
@@ -303,14 +303,14 @@ class ChartManager {
|
|
|
303
303
|
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
304
304
|
const buffer = await this.#zipManager.readBinaryFile(xlsxPath)
|
|
305
305
|
if (buffer) {
|
|
306
|
-
|
|
306
|
+
logger.debug(`Found embedded workbook: ${xlsxPath}`)
|
|
307
307
|
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, cleanNumericData)
|
|
308
308
|
if (updatedXlsx) {
|
|
309
|
-
|
|
309
|
+
logger.debug(`Writing updated workbook to: ${xlsxPath}, size: ${updatedXlsx.length}`)
|
|
310
310
|
this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
|
|
311
311
|
}
|
|
312
312
|
} else {
|
|
313
|
-
|
|
313
|
+
logger.debug(`Could not find workbook at: ${xlsxPath}`)
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
}
|
|
@@ -244,6 +244,16 @@ class RelationshipManager {
|
|
|
244
244
|
this.#flushRels(key, partPath)
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Completely removes all relationships for a part from the cache.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} partPath - ZIP path of the part.
|
|
251
|
+
*/
|
|
252
|
+
deleteRelationships(partPath) {
|
|
253
|
+
const key = this.#getNormalizedKey(partPath)
|
|
254
|
+
this.#relationships.delete(key)
|
|
255
|
+
}
|
|
256
|
+
|
|
247
257
|
/**
|
|
248
258
|
* Updates the target of an existing relationship.
|
|
249
259
|
*
|
|
@@ -863,6 +863,10 @@ class ShapeManager {
|
|
|
863
863
|
normalized.rotation !== undefined ? ` rot="${Math.round(normalized.rotation * 60000)}"` : ''
|
|
864
864
|
|
|
865
865
|
// Text box body properties
|
|
866
|
+
// NOTE: p:txBody is REQUIRED on every p:sp element per the OOXML spec.
|
|
867
|
+
// PowerPoint will trigger a repair dialog if it is missing, even for purely
|
|
868
|
+
// graphical shapes (circles, rectangles used as visual indicators, etc.).
|
|
869
|
+
// When no text is provided we emit an empty body with an end-paragraph run.
|
|
866
870
|
let txBodyXml = ''
|
|
867
871
|
if (normalized.text !== undefined && normalized.text !== null) {
|
|
868
872
|
const textStyle = normalized.textStyle || {}
|
|
@@ -886,46 +890,18 @@ class ShapeManager {
|
|
|
886
890
|
const lines = String(normalized.text).split(/\r?\n/)
|
|
887
891
|
const paragraphsXml = lines
|
|
888
892
|
.map(line => {
|
|
889
|
-
return `<a:p
|
|
890
|
-
${alignAttr}
|
|
891
|
-
<a:r>
|
|
892
|
-
<a:rPr lang="en-US" sz="${fontSizeVal}"${boldAttr}${italicAttr}>
|
|
893
|
-
${colorFill}
|
|
894
|
-
</a:rPr>
|
|
895
|
-
<a:t>${escapeXml(line)}</a:t>
|
|
896
|
-
</a:r>
|
|
897
|
-
</a:p>`
|
|
893
|
+
return `<a:p>${alignAttr}<a:r><a:rPr lang="en-US" sz="${fontSizeVal}"${boldAttr}${italicAttr}>${colorFill}</a:rPr><a:t>${escapeXml(line)}</a:t></a:r></a:p>`
|
|
898
894
|
})
|
|
899
895
|
.join('')
|
|
900
896
|
|
|
901
|
-
txBodyXml = `<p:txBody
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
<a:lstStyle/>
|
|
906
|
-
${paragraphsXml}
|
|
907
|
-
</p:txBody>`
|
|
897
|
+
txBodyXml = `<p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:normAutofit/></a:bodyPr><a:lstStyle/>${paragraphsXml}</p:txBody>`
|
|
898
|
+
} else {
|
|
899
|
+
// Graphical-only shape: emit a minimal empty txBody to satisfy the OOXML schema
|
|
900
|
+
txBodyXml = `<p:txBody><a:bodyPr rtlCol="0"/><a:lstStyle/><a:p><a:endParaRPr lang="en-US" dirty="0"/></a:p></p:txBody>`
|
|
908
901
|
}
|
|
909
902
|
|
|
910
903
|
// Build shape XML block
|
|
911
|
-
const shapeXml = `<p:sp
|
|
912
|
-
<p:nvSpPr>
|
|
913
|
-
<p:cNvPr id="${newId}" name="${escapeXml(name)}"/>
|
|
914
|
-
<p:cNvSpPr/>
|
|
915
|
-
<p:nvPr/>
|
|
916
|
-
</p:nvSpPr>
|
|
917
|
-
<p:spPr>
|
|
918
|
-
<a:xfrm${rotAttr}>
|
|
919
|
-
<a:off x="${xEmu}" y="${yEmu}"/>
|
|
920
|
-
<a:ext cx="${wEmu}" cy="${hEmu}"/>
|
|
921
|
-
</a:xfrm>
|
|
922
|
-
<a:prstGeom prst="${preset}">${avLstXml}</a:prstGeom>
|
|
923
|
-
${fillXml}
|
|
924
|
-
${borderXml}
|
|
925
|
-
${shadowXml}
|
|
926
|
-
</p:spPr>
|
|
927
|
-
${txBodyXml}
|
|
928
|
-
</p:sp>`
|
|
904
|
+
const shapeXml = `<p:sp><p:nvSpPr><p:cNvPr id="${newId}" name="${escapeXml(name)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm${rotAttr}><a:off x="${xEmu}" y="${yEmu}"/><a:ext cx="${wEmu}" cy="${hEmu}"/></a:xfrm><a:prstGeom prst="${preset}">${avLstXml}</a:prstGeom>${fillXml}${borderXml}${shadowXml}</p:spPr>${txBodyXml}</p:sp>`
|
|
929
905
|
|
|
930
906
|
const parsed = this.#xmlParser.parse(shapeXml, 'shape.xml')['p:sp']
|
|
931
907
|
const shapeObj = Array.isArray(parsed) ? parsed[0] : parsed
|
|
@@ -433,7 +433,7 @@ class SlideManager {
|
|
|
433
433
|
cloneSlide(sourceIndex, atPosition, relationshipManager) {
|
|
434
434
|
this.#assertSlideExists(sourceIndex)
|
|
435
435
|
const sourceInfo = this.#slides.get(sourceIndex)
|
|
436
|
-
|
|
436
|
+
logger.debug('Source Slide Info:', sourceInfo)
|
|
437
437
|
|
|
438
438
|
const newIndex = this.#slides.size + 1
|
|
439
439
|
let nextFileIndex = 1
|
|
@@ -445,21 +445,18 @@ class SlideManager {
|
|
|
445
445
|
|
|
446
446
|
// Copy the source XML
|
|
447
447
|
let sourceXml = this.getSlideXml(sourceIndex)
|
|
448
|
-
|
|
448
|
+
logger.debug('Source XML length:', sourceXml ? sourceXml.length : 0)
|
|
449
449
|
|
|
450
450
|
// Copy relationships
|
|
451
451
|
const sourceRels = relationshipManager.getRelationships(sourceInfo.zipPath)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
relationshipManager.getRelsPath(sourceInfo.zipPath)
|
|
455
|
-
)
|
|
456
|
-
console.log('[DEBUG] Source Rels found:', sourceRels)
|
|
452
|
+
logger.debug('Source Rels Path searched:', relationshipManager.getRelsPath(sourceInfo.zipPath))
|
|
453
|
+
logger.debug('Source Rels found:', sourceRels)
|
|
457
454
|
|
|
458
455
|
// Copy relationships from source slide (excluding notes, which are slide-specific)
|
|
459
456
|
const idMap = relationshipManager.copyRelationships(sourceInfo.zipPath, slideZipPath, [
|
|
460
457
|
REL_TYPES.NOTES_SLIDE,
|
|
461
458
|
])
|
|
462
|
-
|
|
459
|
+
logger.debug('Copied relationship ID map:', Array.from(idMap.entries()))
|
|
463
460
|
|
|
464
461
|
// Remap relationship IDs in the cloned XML to match the new targets
|
|
465
462
|
sourceXml = remapRelationshipIds(sourceXml, idMap)
|
|
@@ -498,7 +495,7 @@ class SlideManager {
|
|
|
498
495
|
tableMap: new Map(),
|
|
499
496
|
chartMap: new Map(),
|
|
500
497
|
})
|
|
501
|
-
this.#addSlideToPresentation(rId, newSlideId)
|
|
498
|
+
this.#addSlideToPresentation(rId, newSlideId, sourceInfo.slideId)
|
|
502
499
|
this.#registerSlideContentType(slideFileName)
|
|
503
500
|
|
|
504
501
|
logger.debug(`Cloned slide ${sourceIndex} to new slide ${newIndex}`)
|
|
@@ -527,6 +524,9 @@ class SlideManager {
|
|
|
527
524
|
// Remove relationship from presentation.xml
|
|
528
525
|
this.#relationshipManager.removeRelationship('ppt/presentation.xml', info.relationshipId)
|
|
529
526
|
|
|
527
|
+
// Remove relationships cache
|
|
528
|
+
this.#relationshipManager.deleteRelationships(info.zipPath)
|
|
529
|
+
|
|
530
530
|
// Remove content type from [Content_Types].xml
|
|
531
531
|
this.#contentTypesManager.removeOverride(info.zipPath)
|
|
532
532
|
|
|
@@ -909,11 +909,61 @@ class SlideManager {
|
|
|
909
909
|
await Promise.all(this.getAllSlideIndices().map(i => this.getSlideXmlAsync(i)))
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Adds a slide ID to sections in presentation.xml.
|
|
914
|
+
* @private
|
|
915
|
+
*/
|
|
916
|
+
#addSlideToSections(slideId, sourceSlideId = null) {
|
|
917
|
+
if (!this.#presentationObj) return
|
|
918
|
+
const extLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:extLst')
|
|
919
|
+
if (!extLst?.['p:ext']) return
|
|
920
|
+
|
|
921
|
+
const exts = Array.isArray(extLst['p:ext']) ? extLst['p:ext'] : [extLst['p:ext']]
|
|
922
|
+
for (const ext of exts) {
|
|
923
|
+
const sectionLst = ext['p14:sectionLst']
|
|
924
|
+
if (!sectionLst?.['p14:section']) continue
|
|
925
|
+
|
|
926
|
+
const sections = sectionLst['p14:section']
|
|
927
|
+
const targetIdStr = String(slideId)
|
|
928
|
+
const sourceIdStr = sourceSlideId ? String(sourceSlideId) : null
|
|
929
|
+
|
|
930
|
+
if (sourceIdStr) {
|
|
931
|
+
for (const section of sections) {
|
|
932
|
+
const sldIdLst = section['p14:sldIdLst']
|
|
933
|
+
if (!sldIdLst?.['p14:sldId']) continue
|
|
934
|
+
|
|
935
|
+
const sldIds = sldIdLst['p14:sldId']
|
|
936
|
+
const idx = sldIds.findIndex(s => String(s['@_id']) === sourceIdStr)
|
|
937
|
+
if (idx !== -1) {
|
|
938
|
+
logger.debug(
|
|
939
|
+
`Inserting slide ${targetIdStr} after slide ${sourceIdStr} in section "${section['@_name']}"`
|
|
940
|
+
)
|
|
941
|
+
sldIds.splice(idx + 1, 0, { '@_id': targetIdStr })
|
|
942
|
+
return
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (sections.length > 0) {
|
|
948
|
+
const lastSection = sections[sections.length - 1]
|
|
949
|
+
if (!lastSection['p14:sldIdLst']) {
|
|
950
|
+
lastSection['p14:sldIdLst'] = { 'p14:sldId': [] }
|
|
951
|
+
}
|
|
952
|
+
if (!lastSection['p14:sldIdLst']['p14:sldId']) {
|
|
953
|
+
lastSection['p14:sldIdLst']['p14:sldId'] = []
|
|
954
|
+
}
|
|
955
|
+
const sldIds = lastSection['p14:sldIdLst']['p14:sldId']
|
|
956
|
+
logger.debug(`Appending slide ${targetIdStr} to last section "${lastSection['@_name']}"`)
|
|
957
|
+
sldIds.push({ '@_id': targetIdStr })
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
912
962
|
/**
|
|
913
963
|
* Updates the presentation.xml sldIdLst with a new slide entry.
|
|
914
964
|
* @private
|
|
915
965
|
*/
|
|
916
|
-
#addSlideToPresentation(rId, slideId) {
|
|
966
|
+
#addSlideToPresentation(rId, slideId, sourceSlideId = null) {
|
|
917
967
|
if (!this.#presentationObj) return
|
|
918
968
|
|
|
919
969
|
let sldIdLst = this.#xmlParser.getNode(this.#presentationObj, 'p:presentation.p:sldIdLst')
|
|
@@ -928,6 +978,7 @@ class SlideManager {
|
|
|
928
978
|
}
|
|
929
979
|
|
|
930
980
|
sldIdLst['p:sldId'].push({ '@_id': slideId, '@_r:id': rId })
|
|
981
|
+
this.#addSlideToSections(slideId, sourceSlideId)
|
|
931
982
|
this.#flushPresentation()
|
|
932
983
|
}
|
|
933
984
|
|
|
@@ -1166,8 +1166,8 @@ class TableManager {
|
|
|
1166
1166
|
const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
|
|
1167
1167
|
const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
|
|
1168
1168
|
|
|
1169
|
-
const colWidths = this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj,
|
|
1170
|
-
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj,
|
|
1169
|
+
const colWidths = this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, false)
|
|
1170
|
+
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
|
|
1171
1171
|
|
|
1172
1172
|
const R = this.getMergeRegion(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
1173
1173
|
let pr = rowIndex
|
|
@@ -58,7 +58,6 @@ class ChartWorkbookUpdater {
|
|
|
58
58
|
compressionOptions: { level: 6 },
|
|
59
59
|
})
|
|
60
60
|
} catch (err) {
|
|
61
|
-
console.error('Failed to update embedded workbook', err)
|
|
62
61
|
logger.error('Failed to update embedded workbook', err)
|
|
63
62
|
return workbookData // Return original if failed
|
|
64
63
|
}
|
|
@@ -67,24 +67,15 @@ function isValidRelationshipId(str) {
|
|
|
67
67
|
* remapRelationshipIds(xml, new Map([['rId1', 'rId5'], ['rId2', 'rId6']]));
|
|
68
68
|
*/
|
|
69
69
|
function remapRelationshipIds(xml, idMap) {
|
|
70
|
-
|
|
70
|
+
if (!idMap || idMap.size === 0) return xml
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (pre) return `${pre}${newId}${post}`
|
|
80
|
-
return match.replace(oldId, newId)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// Simple global replace as fallback
|
|
84
|
-
updated = updated.split(`"${oldId}"`).join(`"${newId}"`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return updated
|
|
72
|
+
// Perform single-pass replacement of relationship IDs inside quotes to prevent clobbering
|
|
73
|
+
return xml.replace(/([\'"])(rId\d+)([\'"])/g, (match, openQuote, id, closeQuote) => {
|
|
74
|
+
if (idMap.has(id)) {
|
|
75
|
+
return `${openQuote}${idMap.get(id)}${closeQuote}`
|
|
76
|
+
}
|
|
77
|
+
return match
|
|
78
|
+
})
|
|
88
79
|
}
|
|
89
80
|
|
|
90
81
|
module.exports = {
|