node-pptx-templater 1.0.3 → 1.0.5

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,7 +1,7 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.3",
4
- "description": "Low-level PowerPoint OpenXML template engine for Node.js generate and edit PPTX files directly through XML manipulation without relying on PowerPoint generation libraries.",
3
+ "version": "1.0.5",
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",
7
7
  "exports": {
@@ -43,7 +43,16 @@
43
43
  "report",
44
44
  "generator",
45
45
  "nodejs",
46
- "esm"
46
+ "powerpoint-automation",
47
+ "pptx-template-engine",
48
+ "merge-cells",
49
+ "table-merge",
50
+ "chart-update",
51
+ "dynamic-slides",
52
+ "mail-merge",
53
+ "report-generator",
54
+ "xml-manipulation",
55
+ "pptx-editor"
47
56
  ],
48
57
  "author": {
49
58
  "name": "node-pptx-templater contributors"
@@ -36,6 +36,7 @@ const { RelationshipManager } = require('../managers/RelationshipManager.js')
36
36
  const { ShapeManager } = require('../managers/ShapeManager.js')
37
37
  const { ImageManager } = require('../managers/ImageManager.js')
38
38
  const { TextManager } = require('../managers/TextManager.js')
39
+ const { ZOrderManager } = require('../managers/ZOrderManager.js')
39
40
  const { ValidationEngine } = require('./ValidationEngine.js')
40
41
  const { OutputWriter } = require('./OutputWriter.js')
41
42
  const { TemplateEngine } = require('./TemplateEngine.js')
@@ -121,6 +122,12 @@ class PPTXTemplater {
121
122
  */
122
123
  #textManager
123
124
 
125
+ /**
126
+ * @private
127
+ * @type {ZOrderManager}
128
+ */
129
+ #zOrderManager
130
+
124
131
  /**
125
132
  * @private
126
133
  * @type {RelationshipManager}
@@ -169,6 +176,7 @@ class PPTXTemplater {
169
176
  this.#imageManager = new ImageManager(this.#xmlParser)
170
177
  this.#textManager = new TextManager(this.#xmlParser)
171
178
  this.#templateEngine = new TemplateEngine(this.#xmlParser)
179
+ this.#zOrderManager = new ZOrderManager(this.#xmlParser)
172
180
  this.#outputWriter = new OutputWriter(this.#zipManager, this.#contentTypesManager)
173
181
  }
174
182
 
@@ -253,6 +261,8 @@ class PPTXTemplater {
253
261
  await this.#contentTypesManager.initialize(this.#zipManager)
254
262
  await this.#relationshipManager.initialize(this.#zipManager)
255
263
  await this.#slideManager.initialize(this.#zipManager)
264
+ // Pre-load all slide XML so synchronous operations work on the blank template's existing slides
265
+ await this.#slideManager.preloadAll()
256
266
  await this.#chartManager.initialize(this.#zipManager)
257
267
  await this.#mediaManager.initialize(this.#zipManager)
258
268
  this.#loaded = true
@@ -1541,6 +1551,266 @@ class PPTXTemplater {
1541
1551
  get mediaManager() {
1542
1552
  return this.#mediaManager
1543
1553
  }
1554
+
1555
+ // Z-Order / Layer Management APIs
1556
+
1557
+ /**
1558
+ * Moves slide element one layer forward.
1559
+ */
1560
+ bringForward(optionsOrId) {
1561
+ this.#assertLoaded()
1562
+ let slideIndex, objectId
1563
+ if (typeof optionsOrId === 'object' && optionsOrId !== null) {
1564
+ slideIndex =
1565
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1566
+ objectId = optionsOrId.objectId
1567
+ } else {
1568
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1569
+ objectId = optionsOrId
1570
+ }
1571
+ this.#zOrderManager.bringForward(slideIndex, objectId, this.#slideManager)
1572
+ return this
1573
+ }
1574
+
1575
+ /**
1576
+ * Moves slide element one layer backward.
1577
+ */
1578
+ sendBackward(optionsOrId) {
1579
+ this.#assertLoaded()
1580
+ let slideIndex, objectId
1581
+ if (typeof optionsOrId === 'object' && optionsOrId !== null) {
1582
+ slideIndex =
1583
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1584
+ objectId = optionsOrId.objectId
1585
+ } else {
1586
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1587
+ objectId = optionsOrId
1588
+ }
1589
+ this.#zOrderManager.sendBackward(slideIndex, objectId, this.#slideManager)
1590
+ return this
1591
+ }
1592
+
1593
+ /**
1594
+ * Moves slide element above all other objects.
1595
+ */
1596
+ bringToFront(optionsOrId) {
1597
+ this.#assertLoaded()
1598
+ let slideIndex, objectId
1599
+ if (typeof optionsOrId === 'object' && optionsOrId !== null) {
1600
+ slideIndex =
1601
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1602
+ objectId = optionsOrId.objectId
1603
+ } else {
1604
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1605
+ objectId = optionsOrId
1606
+ }
1607
+ this.#zOrderManager.bringToFront(slideIndex, objectId, this.#slideManager)
1608
+ return this
1609
+ }
1610
+
1611
+ /**
1612
+ * Moves slide element behind all other objects.
1613
+ */
1614
+ sendToBack(optionsOrId) {
1615
+ this.#assertLoaded()
1616
+ let slideIndex, objectId
1617
+ if (typeof optionsOrId === 'object' && optionsOrId !== null) {
1618
+ slideIndex =
1619
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1620
+ objectId = optionsOrId.objectId
1621
+ } else {
1622
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1623
+ objectId = optionsOrId
1624
+ }
1625
+ this.#zOrderManager.sendToBack(slideIndex, objectId, this.#slideManager)
1626
+ return this
1627
+ }
1628
+
1629
+ /**
1630
+ * Moves slide element to the specific 1-based stacking position.
1631
+ */
1632
+ setZIndex(optionsOrId, zIndex) {
1633
+ this.#assertLoaded()
1634
+ let slideIndex, objectId, targetZIndex
1635
+ if (
1636
+ typeof optionsOrId === 'object' &&
1637
+ optionsOrId !== null &&
1638
+ optionsOrId.zIndex !== undefined
1639
+ ) {
1640
+ slideIndex =
1641
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1642
+ objectId = optionsOrId.objectId
1643
+ targetZIndex = optionsOrId.zIndex
1644
+ } else {
1645
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1646
+ objectId = optionsOrId
1647
+ targetZIndex = zIndex
1648
+ }
1649
+ this.#zOrderManager.setZIndex(slideIndex, objectId, targetZIndex, this.#slideManager)
1650
+ return this
1651
+ }
1652
+
1653
+ /**
1654
+ * Moves slide element directly before (below) a target element.
1655
+ */
1656
+ moveObjectBefore(optionsOrId, targetId) {
1657
+ this.#assertLoaded()
1658
+ let slideIndex, objectId, target
1659
+ if (
1660
+ typeof optionsOrId === 'object' &&
1661
+ optionsOrId !== null &&
1662
+ optionsOrId.targetId !== undefined
1663
+ ) {
1664
+ slideIndex =
1665
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1666
+ objectId = optionsOrId.objectId
1667
+ target = optionsOrId.targetId
1668
+ } else {
1669
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1670
+ objectId = optionsOrId
1671
+ target = targetId
1672
+ }
1673
+ this.#zOrderManager.moveObjectBefore(slideIndex, objectId, target, this.#slideManager)
1674
+ return this
1675
+ }
1676
+
1677
+ /**
1678
+ * Moves slide element directly after (above) a target element.
1679
+ */
1680
+ moveObjectAfter(optionsOrId, targetId) {
1681
+ this.#assertLoaded()
1682
+ let slideIndex, objectId, target
1683
+ if (
1684
+ typeof optionsOrId === 'object' &&
1685
+ optionsOrId !== null &&
1686
+ optionsOrId.targetId !== undefined
1687
+ ) {
1688
+ slideIndex =
1689
+ optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
1690
+ objectId = optionsOrId.objectId
1691
+ target = optionsOrId.targetId
1692
+ } else {
1693
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1694
+ objectId = optionsOrId
1695
+ target = targetId
1696
+ }
1697
+ this.#zOrderManager.moveObjectAfter(slideIndex, objectId, target, this.#slideManager)
1698
+ return this
1699
+ }
1700
+
1701
+ /**
1702
+ * Reorders slide objects exactly as specified in the array.
1703
+ */
1704
+ reorderObjects(optionsOrOrder) {
1705
+ this.#assertLoaded()
1706
+ let slideIndex, order
1707
+ if (
1708
+ typeof optionsOrOrder === 'object' &&
1709
+ optionsOrOrder !== null &&
1710
+ Array.isArray(optionsOrOrder.order)
1711
+ ) {
1712
+ slideIndex =
1713
+ optionsOrOrder.slide !== undefined
1714
+ ? optionsOrOrder.slide
1715
+ : this.#getTargetSlideIndices()[0] || 1
1716
+ order = optionsOrOrder.order
1717
+ } else {
1718
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1719
+ order = optionsOrOrder
1720
+ }
1721
+ this.#zOrderManager.reorderObjects(slideIndex, order, this.#slideManager)
1722
+ return this
1723
+ }
1724
+
1725
+ /**
1726
+ * Gets the ordered metadata of all objects on the slide.
1727
+ */
1728
+ getObjectOrder(slideIndex) {
1729
+ this.#assertLoaded()
1730
+ const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
1731
+ return this.#zOrderManager.getObjectOrder(targetIdx, this.#slideManager)
1732
+ }
1733
+
1734
+ /**
1735
+ * Applies bulk template configurations for slide elements stacking layers.
1736
+ */
1737
+ applyZOrder(slideOrConfigs, configsOption) {
1738
+ this.#assertLoaded()
1739
+ let slideIndex, configs
1740
+ if (Array.isArray(slideOrConfigs)) {
1741
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1742
+ configs = slideOrConfigs
1743
+ } else {
1744
+ slideIndex = slideOrConfigs
1745
+ configs = configsOption
1746
+ }
1747
+ this.#zOrderManager.applyZOrder(slideIndex, configs, this.#slideManager)
1748
+ return this
1749
+ }
1750
+
1751
+ /**
1752
+ * Retrieves the info of the top-most object on the slide.
1753
+ */
1754
+ getTopMostObject(slideIndex) {
1755
+ this.#assertLoaded()
1756
+ const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
1757
+ return this.#zOrderManager.getTopMostObject(targetIdx, this.#slideManager)
1758
+ }
1759
+
1760
+ /**
1761
+ * Retrieves the info of the bottom-most object on the slide.
1762
+ */
1763
+ getBottomMostObject(slideIndex) {
1764
+ this.#assertLoaded()
1765
+ const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
1766
+ return this.#zOrderManager.getBottomMostObject(targetIdx, this.#slideManager)
1767
+ }
1768
+
1769
+ /**
1770
+ * Swaps stacking positions of two slide objects.
1771
+ */
1772
+ swapObjects(slideIndexOrId1, id1OrId2, id2) {
1773
+ this.#assertLoaded()
1774
+ let slideIndex, objectId1, objectId2
1775
+ if (id2 !== undefined) {
1776
+ slideIndex = slideIndexOrId1
1777
+ objectId1 = id1OrId2
1778
+ objectId2 = id2
1779
+ } else {
1780
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1781
+ objectId1 = slideIndexOrId1
1782
+ objectId2 = id1OrId2
1783
+ }
1784
+ this.#zOrderManager.swapObjects(slideIndex, objectId1, objectId2, this.#slideManager)
1785
+ return this
1786
+ }
1787
+
1788
+ /**
1789
+ * Sorts stacking order using a custom comparison function.
1790
+ */
1791
+ sortObjects(slideIndexOrCompareFn, compareFnOption) {
1792
+ this.#assertLoaded()
1793
+ let slideIndex, compareFn
1794
+ if (typeof slideIndexOrCompareFn === 'function') {
1795
+ slideIndex = this.#getTargetSlideIndices()[0] || 1
1796
+ compareFn = slideIndexOrCompareFn
1797
+ } else {
1798
+ slideIndex = slideIndexOrCompareFn
1799
+ compareFn = compareFnOption
1800
+ }
1801
+ this.#zOrderManager.sortObjects(slideIndex, compareFn, this.#slideManager)
1802
+ return this
1803
+ }
1804
+
1805
+ /**
1806
+ * Cleans up and normalizes stacking order consistency.
1807
+ */
1808
+ normalizeZOrder(slideIndex) {
1809
+ this.#assertLoaded()
1810
+ const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
1811
+ this.#zOrderManager.normalizeZOrder(targetIdx, this.#slideManager)
1812
+ return this
1813
+ }
1544
1814
  }
1545
1815
 
1546
1816
  module.exports = { PPTXTemplater }
@@ -105,6 +105,11 @@ class ValidationEngine {
105
105
  )
106
106
  }
107
107
  }
108
+
109
+ // Verify Z-order and duplicate IDs
110
+ const zOrderResult = this.validateObjectOrder(ppt, slideIndex)
111
+ errors.push(...zOrderResult.errors)
112
+ warnings.push(...zOrderResult.warnings)
108
113
  } catch (err) {
109
114
  errors.push(`Slide ${slideIndex} validation error: ${err.message}`)
110
115
  }
@@ -241,6 +246,78 @@ class ValidationEngine {
241
246
  warnings,
242
247
  }
243
248
  }
249
+
250
+ /**
251
+ * Audits the shape tree structure for duplicate drawing element IDs.
252
+ */
253
+ static validateObjectOrder(ppt, slideIndex) {
254
+ const errors = []
255
+ const warnings = []
256
+
257
+ try {
258
+ const slideXml = ppt.slideManager.getSlideXml(slideIndex)
259
+ const slideObj = ppt.xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
260
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
261
+
262
+ if (!spTree) {
263
+ errors.push(`Slide ${slideIndex} shape tree not found`)
264
+ return { valid: false, errors, warnings }
265
+ }
266
+
267
+ const idToTags = new Map()
268
+
269
+ const checkIdsRecursive = container => {
270
+ if (!container) return
271
+
272
+ for (const tag of ['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp']) {
273
+ let items = container[tag] || []
274
+ if (!Array.isArray(items)) items = [items]
275
+ for (const item of items) {
276
+ let id = null
277
+ let name = null
278
+ if (tag === 'p:sp') {
279
+ id = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_id']
280
+ name = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_name']
281
+ } else if (tag === 'p:pic') {
282
+ id = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_id']
283
+ name = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_name']
284
+ } else if (tag === 'p:graphicFrame') {
285
+ id = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id']
286
+ name = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_name']
287
+ } else if (tag === 'p:grpSp') {
288
+ id = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id']
289
+ name = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_name']
290
+ checkIdsRecursive(item)
291
+ } else if (tag === 'p:cxnSp') {
292
+ id = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_id']
293
+ name = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_name']
294
+ }
295
+
296
+ if (id !== undefined && id !== null) {
297
+ const strId = String(id)
298
+ if (idToTags.has(strId)) {
299
+ errors.push(
300
+ `Duplicate drawing object ID "${strId}" found in slide ${slideIndex} (name: "${name}")`
301
+ )
302
+ } else {
303
+ idToTags.set(strId, tag)
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ checkIdsRecursive(spTree)
311
+ } catch (err) {
312
+ errors.push(`Slide ${slideIndex} shape tree validation error: ${err.message}`)
313
+ }
314
+
315
+ return {
316
+ valid: errors.length === 0,
317
+ errors,
318
+ warnings,
319
+ }
320
+ }
244
321
  }
245
322
 
246
323
  module.exports = { ValidationEngine }
package/src/index.js CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  const { PPTXTemplater } = require('./core/PPTXTemplater.js')
28
28
  const { ZipManager } = require('./managers/ZipManager.js')
29
- const { XMLParser } = require('./parsers/XMLParser.js')
29
+ const { XMLParser, Z_ORDER_SYMBOL } = require('./parsers/XMLParser.js')
30
30
  const { SlideManager } = require('./managers/SlideManager.js')
31
31
  const { ChartManager } = require('./managers/ChartManager.js')
32
32
  const { TableManager } = require('./managers/TableManager.js')
@@ -55,6 +55,7 @@ module.exports = {
55
55
  PPTXTemplater,
56
56
  ZipManager,
57
57
  XMLParser,
58
+ Z_ORDER_SYMBOL,
58
59
  SlideManager,
59
60
  ChartManager,
60
61
  TableManager,