node-pptx-templater 1.0.11 → 1.0.13
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
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
|
"type": "commonjs",
|
package/src/core/OutputWriter.js
CHANGED
|
@@ -107,8 +107,6 @@ class OutputWriter {
|
|
|
107
107
|
* @returns {Promise<void>}
|
|
108
108
|
*/
|
|
109
109
|
async #flushAllSlides(slideManager, zipManager) {
|
|
110
|
-
// SlideManager already writes to zipManager via setSlideXml,
|
|
111
|
-
// so this is mostly a no-op with a validation step.
|
|
112
110
|
const info = slideManager.getAllSlideInfo()
|
|
113
111
|
|
|
114
112
|
for (const slide of info) {
|
|
@@ -117,64 +115,58 @@ class OutputWriter {
|
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
|
|
120
|
-
//
|
|
118
|
+
// Change this block to await the process completely
|
|
121
119
|
if (zipManager.hasFile('docProps/app.xml')) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
oldSlideTitlesCount = parseInt(countVar['vt:i4'], 10) || 0
|
|
144
|
-
countVar['vt:i4'] = info.length
|
|
145
|
-
}
|
|
146
|
-
break
|
|
120
|
+
// AWAIT this process entirely before exiting the function!
|
|
121
|
+
await zipManager.rawZip
|
|
122
|
+
.file('docProps/app.xml')
|
|
123
|
+
.async('text')
|
|
124
|
+
.then(content => {
|
|
125
|
+
const parser = new XMLParser()
|
|
126
|
+
const appObj = parser.parse(content, 'app.xml')
|
|
127
|
+
const properties = appObj.Properties
|
|
128
|
+
|
|
129
|
+
if (properties) {
|
|
130
|
+
properties.Slides = info.length
|
|
131
|
+
|
|
132
|
+
let oldSlideTitlesCount = 0
|
|
133
|
+
const variants = properties.HeadingPairs?.['vt:vector']?.['vt:variant']
|
|
134
|
+
if (Array.isArray(variants)) {
|
|
135
|
+
for (let i = 0; i < variants.length; i++) {
|
|
136
|
+
if (variants[i]['vt:lpstr'] === 'Slide Titles') {
|
|
137
|
+
const countVar = variants[i + 1]
|
|
138
|
+
if (countVar) {
|
|
139
|
+
oldSlideTitlesCount = parseInt(countVar['vt:i4'], 10) || 0
|
|
140
|
+
countVar['vt:i4'] = info.length
|
|
147
141
|
}
|
|
142
|
+
break
|
|
148
143
|
}
|
|
149
144
|
}
|
|
145
|
+
}
|
|
150
146
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (lpstrs)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Remove the old slide titles (which are at the end)
|
|
159
|
-
if (oldSlideTitlesCount > 0 && lpstrs.length >= oldSlideTitlesCount) {
|
|
160
|
-
lpstrs = lpstrs.slice(0, lpstrs.length - oldSlideTitlesCount)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Append new slide titles
|
|
164
|
-
const newSlideTitles = info.map(slide => slide.title || `Slide ${slide.index}`)
|
|
165
|
-
lpstrs.push(...newSlideTitles)
|
|
166
|
-
|
|
167
|
-
titlesVector['vt:lpstr'] = lpstrs
|
|
168
|
-
titlesVector['@_size'] = String(lpstrs.length)
|
|
147
|
+
const titlesVector = properties.TitlesOfParts?.['vt:vector']
|
|
148
|
+
if (titlesVector) {
|
|
149
|
+
let lpstrs = titlesVector['vt:lpstr']
|
|
150
|
+
if (lpstrs) {
|
|
151
|
+
if (!Array.isArray(lpstrs)) lpstrs = [lpstrs]
|
|
152
|
+
if (oldSlideTitlesCount > 0 && lpstrs.length >= oldSlideTitlesCount) {
|
|
153
|
+
lpstrs = lpstrs.slice(0, lpstrs.length - oldSlideTitlesCount)
|
|
169
154
|
}
|
|
170
|
-
|
|
155
|
+
const newSlideTitles = info.map(slide => slide.title || `Slide ${slide.index}`)
|
|
156
|
+
lpstrs.push(...newSlideTitles)
|
|
171
157
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
158
|
+
titlesVector['vt:lpstr'] = lpstrs
|
|
159
|
+
titlesVector['@_size'] = String(lpstrs.length)
|
|
160
|
+
}
|
|
175
161
|
}
|
|
176
|
-
|
|
177
|
-
|
|
162
|
+
|
|
163
|
+
const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
164
|
+
const updatedXml = parser.build(appObj, declaration)
|
|
165
|
+
|
|
166
|
+
// Writing it safely here now that the function block is strictly sequential
|
|
167
|
+
zipManager.writeFile('docProps/app.xml', updatedXml)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
178
170
|
}
|
|
179
171
|
|
|
180
172
|
logger.debug(`Flushed ${info.length} slide(s) to ZIP`)
|
|
@@ -69,24 +69,43 @@ class ShapeManager {
|
|
|
69
69
|
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
if (!res.shape['p:spPr']) {
|
|
73
|
+
res.shape['p:spPr'] = {}
|
|
74
|
+
}
|
|
75
|
+
if (!res.shape['p:spPr']['a:xfrm']) {
|
|
76
|
+
res.shape['p:spPr']['a:xfrm'] = {}
|
|
77
|
+
}
|
|
78
|
+
const xfrm = res.shape['p:spPr']['a:xfrm']
|
|
79
|
+
|
|
80
|
+
if (!xfrm['a:off']) {
|
|
81
|
+
xfrm['a:off'] = {}
|
|
82
|
+
}
|
|
83
|
+
if (!xfrm['a:ext']) {
|
|
84
|
+
xfrm['a:ext'] = {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options.x !== undefined) {
|
|
88
|
+
xfrm['a:off']['@_x'] = String(Math.round(options.x))
|
|
89
|
+
} else if (xfrm['a:off']['@_x'] === undefined) {
|
|
90
|
+
xfrm['a:off']['@_x'] = '0'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (options.y !== undefined) {
|
|
94
|
+
xfrm['a:off']['@_y'] = String(Math.round(options.y))
|
|
95
|
+
} else if (xfrm['a:off']['@_y'] === undefined) {
|
|
96
|
+
xfrm['a:off']['@_y'] = '0'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.width !== undefined) {
|
|
100
|
+
xfrm['a:ext']['@_cx'] = String(Math.round(options.width))
|
|
101
|
+
} else if (xfrm['a:ext']['@_cx'] === undefined) {
|
|
102
|
+
xfrm['a:ext']['@_cx'] = '0'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (options.height !== undefined) {
|
|
106
|
+
xfrm['a:ext']['@_cy'] = String(Math.round(options.height))
|
|
107
|
+
} else if (xfrm['a:ext']['@_cy'] === undefined) {
|
|
108
|
+
xfrm['a:ext']['@_cy'] = '0'
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
@@ -293,16 +293,17 @@ class ChartCacheGenerator {
|
|
|
293
293
|
if (countMatch) pointsCount = parseInt(countMatch[1], 10)
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
-
if (pointsCount === 0 && options.labels) {
|
|
297
|
-
pointsCount = options.labels.length
|
|
298
|
-
}
|
|
299
296
|
|
|
300
|
-
// Parse existing styling and flags from the current <c:dLbls> block
|
|
301
297
|
let existingTxPr = ''
|
|
302
298
|
let existingDLblPos = ''
|
|
303
299
|
let existingNumFmt = ''
|
|
304
300
|
let existingSpPr = ''
|
|
301
|
+
let existingExtLst = ''
|
|
302
|
+
let existingShowLeaderLines = ''
|
|
305
303
|
const existingDLblSpPrs = {}
|
|
304
|
+
const existingDLblTxPrs = {}
|
|
305
|
+
const existingDLblLayouts = {}
|
|
306
|
+
const existingDLblPositions = {}
|
|
306
307
|
const existingShowTags = {}
|
|
307
308
|
|
|
308
309
|
const dLblsMatch = /<c:dLbls>([\s\S]*?)<\/c:dLbls>/.exec(content)
|
|
@@ -324,8 +325,16 @@ class ChartCacheGenerator {
|
|
|
324
325
|
if (spPrMatch) {
|
|
325
326
|
existingSpPr = spPrMatch[1]
|
|
326
327
|
}
|
|
328
|
+
const extLstMatch = /(<c:extLst>[\s\S]*?<\/c:extLst>)/.exec(dLblsContent)
|
|
329
|
+
if (extLstMatch) {
|
|
330
|
+
existingExtLst = extLstMatch[1]
|
|
331
|
+
}
|
|
332
|
+
const showLeaderMatch = /(<c:showLeaderLines\s+[^>]*\/>)/.exec(dLblsContent)
|
|
333
|
+
if (showLeaderMatch) {
|
|
334
|
+
existingShowLeaderLines = showLeaderMatch[1]
|
|
335
|
+
}
|
|
327
336
|
|
|
328
|
-
// Parse individual <c:dLbl>
|
|
337
|
+
// Parse individual <c:dLbl> properties: fills, text styling, layouts, and positions
|
|
329
338
|
const dLblPattern = /<c:dLbl>([\s\S]*?)<\/c:dLbl>/g
|
|
330
339
|
let dLblMatch
|
|
331
340
|
while ((dLblMatch = dLblPattern.exec(dLblsContent)) !== null) {
|
|
@@ -337,6 +346,18 @@ class ChartCacheGenerator {
|
|
|
337
346
|
if (dLblSpPrMatch) {
|
|
338
347
|
existingDLblSpPrs[idx] = dLblSpPrMatch[1]
|
|
339
348
|
}
|
|
349
|
+
const dLblTxPrMatch = /(<c:txPr>[\s\S]*?<\/c:txPr>)/.exec(dLblContent)
|
|
350
|
+
if (dLblTxPrMatch) {
|
|
351
|
+
existingDLblTxPrs[idx] = dLblTxPrMatch[1]
|
|
352
|
+
}
|
|
353
|
+
const dLblLayoutMatch = /(<c:layout>[\s\S]*?<\/c:layout>|<c:layout\/>)/.exec(dLblContent)
|
|
354
|
+
if (dLblLayoutMatch) {
|
|
355
|
+
existingDLblLayouts[idx] = dLblLayoutMatch[1]
|
|
356
|
+
}
|
|
357
|
+
const dLblPosMatch = /(<c:dLblPos\s+[^>]*\/>)/.exec(dLblContent)
|
|
358
|
+
if (dLblPosMatch) {
|
|
359
|
+
existingDLblPositions[idx] = dLblPosMatch[1]
|
|
360
|
+
}
|
|
340
361
|
}
|
|
341
362
|
}
|
|
342
363
|
|
|
@@ -367,7 +388,12 @@ class ChartCacheGenerator {
|
|
|
367
388
|
existingNumFmt,
|
|
368
389
|
existingShowTags,
|
|
369
390
|
existingSpPr,
|
|
370
|
-
existingDLblSpPrs
|
|
391
|
+
existingDLblSpPrs,
|
|
392
|
+
existingDLblTxPrs,
|
|
393
|
+
existingDLblLayouts,
|
|
394
|
+
existingDLblPositions,
|
|
395
|
+
existingExtLst,
|
|
396
|
+
existingShowLeaderLines
|
|
371
397
|
)
|
|
372
398
|
|
|
373
399
|
let updatedContent = content
|
|
@@ -397,7 +423,12 @@ class ChartCacheGenerator {
|
|
|
397
423
|
existingNumFmt = '',
|
|
398
424
|
existingShowTags = {},
|
|
399
425
|
existingSpPr = '',
|
|
400
|
-
existingDLblSpPrs = {}
|
|
426
|
+
existingDLblSpPrs = {},
|
|
427
|
+
existingDLblTxPrs = {},
|
|
428
|
+
existingDLblLayouts = {},
|
|
429
|
+
existingDLblPositions = {},
|
|
430
|
+
existingExtLst = '',
|
|
431
|
+
existingShowLeaderLines = ''
|
|
401
432
|
) {
|
|
402
433
|
const {
|
|
403
434
|
labels,
|
|
@@ -463,6 +494,11 @@ class ChartCacheGenerator {
|
|
|
463
494
|
xml += `<c:dLbl>`
|
|
464
495
|
xml += `<c:idx val="${i}"/>`
|
|
465
496
|
|
|
497
|
+
// Restore per-point layout (manual position offsets) from template
|
|
498
|
+
if (existingDLblLayouts && existingDLblLayouts[i]) {
|
|
499
|
+
xml += existingDLblLayouts[i]
|
|
500
|
+
}
|
|
501
|
+
|
|
466
502
|
if (labelsFromCells && !template) {
|
|
467
503
|
const range = ChartWorkbookUpdater.parseCellRange(labelsFromCells)
|
|
468
504
|
const startColNum = ChartWorkbookUpdater.colLetterToNum(range.startCol)
|
|
@@ -510,8 +546,9 @@ class ChartCacheGenerator {
|
|
|
510
546
|
let bodyPr = '<a:bodyPr/>'
|
|
511
547
|
let pPrXml = ''
|
|
512
548
|
let rPrXml = ''
|
|
513
|
-
|
|
514
|
-
|
|
549
|
+
const dLblTxPr = (existingDLblTxPrs && existingDLblTxPrs[i]) || existingTxPr
|
|
550
|
+
if (dLblTxPr) {
|
|
551
|
+
const extracted = this.extractTxPrParts(dLblTxPr)
|
|
515
552
|
bodyPr = extracted.bodyPr
|
|
516
553
|
pPrXml = extracted.pPrXml
|
|
517
554
|
rPrXml = extracted.rPrXml
|
|
@@ -556,12 +593,16 @@ class ChartCacheGenerator {
|
|
|
556
593
|
|
|
557
594
|
if (labelStyle) {
|
|
558
595
|
xml += this.generateTxPrXml(labelStyle)
|
|
596
|
+
} else if (existingDLblTxPrs && existingDLblTxPrs[i]) {
|
|
597
|
+
xml += existingDLblTxPrs[i]
|
|
559
598
|
} else if (existingTxPr) {
|
|
560
599
|
xml += existingTxPr
|
|
561
600
|
}
|
|
562
601
|
|
|
563
602
|
if (openxmlPos) {
|
|
564
603
|
xml += `<c:dLblPos val="${openxmlPos}"/>`
|
|
604
|
+
} else if (existingDLblPositions && existingDLblPositions[i]) {
|
|
605
|
+
xml += existingDLblPositions[i]
|
|
565
606
|
} else if (existingDLblPos) {
|
|
566
607
|
xml += existingDLblPos
|
|
567
608
|
}
|
|
@@ -648,6 +689,14 @@ class ChartCacheGenerator {
|
|
|
648
689
|
xml += `<c:showBubbleSize val="0"/>`
|
|
649
690
|
}
|
|
650
691
|
|
|
692
|
+
// Restore showLeaderLines and extension list from template
|
|
693
|
+
if (existingShowLeaderLines) {
|
|
694
|
+
xml += existingShowLeaderLines
|
|
695
|
+
}
|
|
696
|
+
if (existingExtLst) {
|
|
697
|
+
xml += existingExtLst
|
|
698
|
+
}
|
|
699
|
+
|
|
651
700
|
xml += '</c:dLbls>'
|
|
652
701
|
return xml
|
|
653
702
|
}
|