node-pptx-templater 1.1.4 → 1.1.6

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 CHANGED
@@ -5,22 +5,6 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.1.3] - 2026-06-22
9
-
10
- ### Fixed
11
-
12
- - **Layout Preservation in `addCellShape()`**: Fixed a critical layout bug where querying cell bounds or adding cell shapes dynamically (via `addCellShape()` or `updateCellShape()`) mutated table row heights inside the XML. The sizing/positioning logic now runs in a layout-only mode, keeping the table XML completely untouched and respecting original template custom row heights.
13
-
14
- ## [1.1.2] - 2026-06-22
15
-
16
- ### Fixed
17
-
18
- - **Slide Link Relationships Target Paths**: Fixed PowerPoint "Repair Mode" errors when adding slide-to-slide hyperlinks (via `addSlideLink` or `addImageLink`). The relative target path resolves correctly to the sibling filename `slideX.xml` instead of using the redundant parent prefix `../slides/slideX.xml` (which exited and re-entered the same directory, violating PowerPoint's relative path resolution rules).
19
-
20
- ### Added
21
-
22
- - **Redundant Traversal Validation**: Added automated check in `ValidationEngine` to identify and error on redundant relative path traversals inside relationship files (e.g. referencing `../slides/slideX.xml` from a source slide file already located in `ppt/slides/`).
23
-
24
8
  ## [1.1.0] - 2026-06-12
25
9
 
26
10
  ### Added
package/README.md CHANGED
@@ -172,51 +172,27 @@ const meta = await ppt.getTableRows('SalesTable', { includeMetadata: true });
172
172
  ```
173
173
 
174
174
  ### Table Cell Shapes
175
- Cell shapes are overlay graphics anchored within table cells. They are positioned absolutely based on cell bounds, and **never** modify row heights, column widths, cell margins, or trigger table reflow.
176
-
177
- #### Merged Cell Support
178
- When targeting a merged cell (spanned using `rowSpan` or `colSpan`), `addCellShape()` dynamically resolves the **actual rendered bounds** of the entire merged cell region (summing the width and height of the spanned rows/columns). The shape is positioned and aligned relative to this final merged boundary. Any offset `x` and `y` specified is interpreted relative to the top-left corner of the resolved merged cell region.
179
-
180
- #### Alignment & Presets
181
- Shapes can be aligned dynamically within the cell (or merged region) using preset positions or explicit alignments:
182
- * **Presets**: Use `position` values like `'top-left'`, `'center'`, `'bottom-right'`, etc.
183
- * **Alignment Options**: Set `alignX: 'left' | 'center' | 'right'` and `alignY: 'top' | 'middle' | 'bottom'`. If no alignments or offsets are provided, shapes default to true centering (`'center'`/`'middle'`).
184
- * **Offsets & Padding**: Offsets (`x`, `y`) are applied relative to the resolved alignment. For instance, when `alignX: 'right'` is used, the shape is placed at `cellRight - shapeWidth - x` (defaulting to a `5px` padding if `x` is omitted).
175
+ Cell shapes are overlay graphics anchored within table cells. They are positioned absolutely based on cell bounds, and **never** modify row heights, column widths, cell margins, or trigger table reflow. Offsets (`x`, `y`) are relative to the cell's top-left corner. Oversized shapes are scaled down proportionally to fit inside the cell.
185
176
 
186
177
  ```javascript
187
- // Add a status icon centered in a merged cell (rowSpan=2, colSpan=2)
188
- await ppt.addCellShape('StatusTable', 1, 1, {
178
+ // Add a simple indicator
179
+ await ppt.addCellShape('SalesTable', 2, 1, {
189
180
  type: 'circle',
190
181
  width: 12,
191
182
  height: 12,
192
- fill: '#10B981', // Green status dot
193
- alignX: 'center',
194
- alignY: 'middle' // Visually centered in the merged cell
183
+ fill: '#10B981' // Green status dot
195
184
  });
196
185
 
197
- // Add a badge with text and explicit offsets inside a merged cell
198
- await ppt.addCellShape('StatusTable', 1, 1, {
186
+ // Add a badge with text and custom offsets
187
+ await ppt.addCellShape('SalesTable', 1, 2, {
199
188
  type: 'badge',
200
- text: 'Urgent',
201
- fill: '#EF4444',
202
- alignX: 'left',
203
- alignY: 'top',
204
- x: 5,
205
- y: 3,
189
+ text: 'Active',
190
+ fill: '#3B82F6',
191
+ x: 4,
192
+ y: 2,
206
193
  width: 50,
207
194
  height: 16
208
195
  });
209
-
210
- // Add an indicator with bottom-right alignment and custom padding
211
- await ppt.addCellShape('StatusTable', 1, 1, {
212
- type: 'icon',
213
- icon: 'up',
214
- size: 14,
215
- alignX: 'right',
216
- alignY: 'bottom',
217
- x: 4,
218
- y: 2
219
- });
220
196
  ```
221
197
 
222
198
  ### XML Folder Workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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",
@@ -37,8 +37,6 @@
37
37
  "example:table-extraction": "node examples/table-extraction.js",
38
38
  "example:nested-rows": "node examples/nested-table-rows.js",
39
39
  "example:xml-folder": "node examples/xml-folder-workflow.js",
40
- "example:shape-cells": "node examples/shape-cells.js",
41
- "example:all": "node scripts/run-examples-check.js",
42
40
  "prepublishOnly": "npm run lint && npm run test"
43
41
  },
44
42
  "keywords": [
@@ -473,10 +473,14 @@ class PPTXTemplater {
473
473
  throw new PPTXError(`Destination is a file: ${outputPath}`)
474
474
  }
475
475
  const files = fs.readdirSync(resolvedOut)
476
- if (files.length > 0 && !options.overwrite) {
477
- throw new PPTXError(
478
- `Destination directory "${outputPath}" is not empty. Set overwrite: true to overwrite.`
479
- )
476
+ if (files.length > 0) {
477
+ if (!options.overwrite) {
478
+ throw new PPTXError(
479
+ `Destination directory "${outputPath}" is not empty. Set overwrite: true to overwrite.`
480
+ )
481
+ } else {
482
+ await fs.emptyDir(resolvedOut)
483
+ }
480
484
  }
481
485
  } else {
482
486
  await fs.ensureDir(resolvedOut)
@@ -1569,30 +1573,11 @@ class PPTXTemplater {
1569
1573
  }
1570
1574
 
1571
1575
  /**
1572
- * Appends one or more rows to a table. Supports flat arrays, nested arrays
1573
- * for rowspan-merged cells, and **Shape Cell** configuration objects that insert
1574
- * a graphic indicator directly inside a cell.
1575
- *
1576
- * ### Shape Cell Object
1577
- * Instead of a string value you may pass an object with the following keys:
1578
- *
1579
- * | Key | Type | Default | Description |
1580
- * |-----|------|---------|-------------|
1581
- * | `type` | `string` | **required** | Shape preset: `'circle'`, `'square'`, `'rectangle'`, `'triangle'`, `'diamond'`, `'hexagon'`, `'line'`. |
1582
- * | `text` | `string` | `''` | Optional label rendered next to the shape. |
1583
- * | `color` | `string` | `'#4CAF50'` | Fill colour of the shape (hex). |
1584
- * | `width` | `number` | `14` | Shape width in points. |
1585
- * | `height` | `number` | `14` | Shape height in points. |
1586
- * | `position` | `string` | `'center'` | Alignment inside the cell: `'center'`, `'left'`, `'right'`, `'top'`, `'bottom'`, `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`. |
1587
- * | `offsetX` | `number` | `0` | Additional horizontal offset from the resolved anchor (pt). |
1588
- * | `offsetY` | `number` | `0` | Additional vertical offset from the resolved anchor (pt). |
1589
- *
1590
- * The shape is added as a floating overlay anchored to the cell; the table
1591
- * dimensions (row heights, column widths) are **never mutated**.
1576
+ * Appends one or more rows to a table. Supports flat arrays and nested arrays
1577
+ * for rowspan-merged cells.
1592
1578
  *
1593
1579
  * @param {string} tableId - Table name or shape ID.
1594
- * @param {Array<string|Array<string>|Object>} rowData - Row data. Each element may be
1595
- * a plain string, a nested array (rowspan), or a Shape Cell configuration object.
1580
+ * @param {Array<string|Array<string>>} rowData - Row data. Nested arrays create rowspan cells.
1596
1581
  * @param {Object} [options] - Row insertion options.
1597
1582
  * @param {'rowspan'|'auto'|'none'} [options.mergeStrategy='rowspan'] - How to handle nested arrays.
1598
1583
  * `'rowspan'` creates OpenXML vertical spans, `'auto'` merges identical adjacent values,
@@ -1605,20 +1590,6 @@ class PPTXTemplater {
1605
1590
  *
1606
1591
  * // Nested row with rowspan
1607
1592
  * ppt.addTableRow('SalesTable', ['Region', ['Q1', 'Q2'], '$5K'], { mergeStrategy: 'rowspan' });
1608
- *
1609
- * // KPI indicator – green circle placed in the cell centre, no label
1610
- * await ppt.addTableRow('StatusTable', [
1611
- * 'Alice',
1612
- * 'Engineering',
1613
- * { type: 'circle', color: '#4CAF50', width: 12, height: 12, position: 'center' },
1614
- * ]);
1615
- *
1616
- * // Status badge – shape with accompanying text label, aligned to the left
1617
- * await ppt.addTableRow('StatusTable', [
1618
- * 'Bob',
1619
- * 'Design',
1620
- * { type: 'circle', color: '#F44336', text: 'Blocked', position: 'left' },
1621
- * ]);
1622
1593
  */
1623
1594
  addTableRow(tableId, rowData, options = {}) {
1624
1595
  this.#assertLoaded()
@@ -433,6 +433,14 @@ class ShapeManager {
433
433
  if (!options) return options
434
434
  const normalized = { ...options }
435
435
 
436
+ // Fallback mapping between id and name for compatibility
437
+ if (normalized.id === undefined && normalized.name !== undefined) {
438
+ normalized.id = normalized.name
439
+ }
440
+ if (normalized.name === undefined && normalized.id !== undefined) {
441
+ normalized.name = normalized.id
442
+ }
443
+
436
444
  // 1. Map color alias to fill
437
445
  const fillVal = options.fill !== undefined ? options.fill : options.color
438
446
  if (fillVal !== undefined) {
@@ -1719,40 +1719,52 @@ class TableManager {
1719
1719
  }
1720
1720
 
1721
1721
  // 2. Determine alignment settings
1722
- let alignX = undefined
1723
- let alignY = undefined
1724
-
1725
- const pos = String(config.position || '')
1726
- .toLowerCase()
1727
- .trim()
1728
- if (pos) {
1729
- if (pos === 'left') {
1730
- alignX = 'left'
1731
- alignY = 'middle'
1732
- } else if (pos === 'right') {
1733
- alignX = 'right'
1734
- alignY = 'middle'
1735
- } else if (pos === 'center' || pos === 'middle') {
1736
- alignX = 'center'
1737
- alignY = 'middle'
1738
- } else if (pos === 'top') {
1739
- alignX = 'center'
1740
- alignY = 'top'
1741
- } else if (pos === 'bottom') {
1742
- alignX = 'center'
1743
- alignY = 'bottom'
1744
- } else if (pos === 'top-left') {
1745
- alignX = 'left'
1746
- alignY = 'top'
1747
- } else if (pos === 'top-right') {
1748
- alignX = 'right'
1749
- alignY = 'top'
1750
- } else if (pos === 'bottom-left') {
1751
- alignX = 'left'
1752
- alignY = 'bottom'
1753
- } else if (pos === 'bottom-right') {
1754
- alignX = 'right'
1755
- alignY = 'bottom'
1722
+ let alignX = config.alignX
1723
+ let alignY = config.alignY
1724
+
1725
+ if (config.position) {
1726
+ switch (config.position) {
1727
+ case 'top-left':
1728
+ if (!alignX) alignX = 'left'
1729
+ if (!alignY) alignY = 'top'
1730
+ break
1731
+ case 'top-center':
1732
+ case 'top':
1733
+ if (!alignX) alignX = 'center'
1734
+ if (!alignY) alignY = 'top'
1735
+ break
1736
+ case 'top-right':
1737
+ if (!alignX) alignX = 'right'
1738
+ if (!alignY) alignY = 'top'
1739
+ break
1740
+ case 'middle-left':
1741
+ case 'left':
1742
+ if (!alignX) alignX = 'left'
1743
+ if (!alignY) alignY = 'middle'
1744
+ break
1745
+ case 'center':
1746
+ case 'middle-center':
1747
+ if (!alignX) alignX = 'center'
1748
+ if (!alignY) alignY = 'middle'
1749
+ break
1750
+ case 'middle-right':
1751
+ case 'right':
1752
+ if (!alignX) alignX = 'right'
1753
+ if (!alignY) alignY = 'middle'
1754
+ break
1755
+ case 'bottom-left':
1756
+ if (!alignX) alignX = 'left'
1757
+ if (!alignY) alignY = 'bottom'
1758
+ break
1759
+ case 'bottom-center':
1760
+ case 'bottom':
1761
+ if (!alignX) alignX = 'center'
1762
+ if (!alignY) alignY = 'bottom'
1763
+ break
1764
+ case 'bottom-right':
1765
+ if (!alignX) alignX = 'right'
1766
+ if (!alignY) alignY = 'bottom'
1767
+ break
1756
1768
  }
1757
1769
  }
1758
1770
 
@@ -2057,7 +2069,7 @@ class TableManager {
2057
2069
  slideManager,
2058
2070
  shapeManager,
2059
2071
  tblObj,
2060
- _frameObj
2072
+ frameObj
2061
2073
  ) {
2062
2074
  if (!cellShapes || !shapeManager) return
2063
2075
 
@@ -2075,6 +2087,54 @@ class TableManager {
2075
2087
  }
2076
2088
  }
2077
2089
 
2090
+ const xfrm = frameObj['p:xfrm']
2091
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
2092
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
2093
+
2094
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2095
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2096
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2097
+
2098
+ const trsArr = tblObj['a:tr'] || []
2099
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2100
+
2101
+ const getCellBounds = (r, c) => {
2102
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
2103
+ const pr = parent.row
2104
+ const pc = parent.col
2105
+
2106
+ let cellLeft = tableX
2107
+ for (let idx = 0; idx < pc; idx++) {
2108
+ cellLeft += colWidths[idx] || 0
2109
+ }
2110
+
2111
+ let cellTop = tableY
2112
+ for (let idx = 0; idx < pr; idx++) {
2113
+ cellTop += rowHeights[idx] || 0
2114
+ }
2115
+
2116
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
2117
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
2118
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
2119
+
2120
+ let cellWidth = 0
2121
+ for (let idx = 0; idx < gridSpan; idx++) {
2122
+ cellWidth += colWidths[pc + idx] || 0
2123
+ }
2124
+
2125
+ let cellHeight = 0
2126
+ for (let idx = 0; idx < rowSpan; idx++) {
2127
+ cellHeight += rowHeights[pr + idx] || 0
2128
+ }
2129
+
2130
+ return {
2131
+ left: cellLeft,
2132
+ top: cellTop,
2133
+ width: cellWidth,
2134
+ height: cellHeight,
2135
+ }
2136
+ }
2137
+
2078
2138
  const shapesToCreate = []
2079
2139
  const headerNames = (tblObj['a:tr']?.[0]?.['a:tc'] || []).map(cell =>
2080
2140
  this.#getCellText(cell).trim()
@@ -2204,7 +2264,45 @@ class TableManager {
2204
2264
  }
2205
2265
 
2206
2266
  addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
2207
- const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2267
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
2268
+ slideIndex,
2269
+ tableId,
2270
+ slideManager
2271
+ )
2272
+
2273
+ const xfrm = frameObj['p:xfrm']
2274
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
2275
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
2276
+
2277
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2278
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2279
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2280
+
2281
+ const trsArr = tblObj['a:tr'] || []
2282
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2283
+
2284
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
2285
+ const pr = parent.row
2286
+ const pc = parent.col
2287
+
2288
+ let cellLeft = tableX
2289
+ for (let idx = 0; idx < pc; idx++) {
2290
+ cellLeft += colWidths[idx] || 0
2291
+ }
2292
+
2293
+ let cellTop = tableY
2294
+ for (let idx = 0; idx < pr; idx++) {
2295
+ cellTop += rowHeights[idx] || 0
2296
+ }
2297
+
2298
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
2299
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
2300
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
2301
+
2302
+ let cellWidth = 0
2303
+ for (let idx = 0; idx < gridSpan; idx++) {
2304
+ cellWidth += colWidths[pc + idx] || 0
2305
+ }
2208
2306
 
2209
2307
  const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
2210
2308
  if (!bounds) {
@@ -2259,7 +2357,11 @@ class TableManager {
2259
2357
  slideManager,
2260
2358
  shapeManager
2261
2359
  ) {
2262
- const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2360
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
2361
+ slideIndex,
2362
+ tableId,
2363
+ slideManager
2364
+ )
2263
2365
 
2264
2366
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
2265
2367
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`