node-pptx-templater 1.1.1 → 1.1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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
+
8
24
  ## [1.1.0] - 2026-06-12
9
25
 
10
26
  ### Added
package/README.md CHANGED
@@ -172,27 +172,51 @@ 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. Offsets (`x`, `y`) are relative to the cell's top-left corner. Oversized shapes are scaled down proportionally to fit inside the cell.
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).
176
185
 
177
186
  ```javascript
178
- // Add a simple indicator
179
- await ppt.addCellShape('SalesTable', 2, 1, {
187
+ // Add a status icon centered in a merged cell (rowSpan=2, colSpan=2)
188
+ await ppt.addCellShape('StatusTable', 1, 1, {
180
189
  type: 'circle',
181
190
  width: 12,
182
191
  height: 12,
183
- fill: '#10B981' // Green status dot
192
+ fill: '#10B981', // Green status dot
193
+ alignX: 'center',
194
+ alignY: 'middle' // Visually centered in the merged cell
184
195
  });
185
196
 
186
- // Add a badge with text and custom offsets
187
- await ppt.addCellShape('SalesTable', 1, 2, {
197
+ // Add a badge with text and explicit offsets inside a merged cell
198
+ await ppt.addCellShape('StatusTable', 1, 1, {
188
199
  type: 'badge',
189
- text: 'Active',
190
- fill: '#3B82F6',
191
- x: 4,
192
- y: 2,
200
+ text: 'Urgent',
201
+ fill: '#EF4444',
202
+ alignX: 'left',
203
+ alignY: 'top',
204
+ x: 5,
205
+ y: 3,
193
206
  width: 50,
194
207
  height: 16
195
208
  });
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
+ });
196
220
  ```
197
221
 
198
222
  ### XML Folder Workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.1.1",
3
+ "version": "1.1.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
  "type": "commonjs",
@@ -2727,6 +2727,8 @@ class PPTXTemplater {
2727
2727
 
2728
2728
  /**
2729
2729
  * Dynamically adds a shape inside a table cell based on cell coordinates.
2730
+ * Cell shapes are overlay graphics anchored independently of the table layout,
2731
+ * and adding a cell shape never modifies row heights, column widths, or table dimensions.
2730
2732
  *
2731
2733
  * @param {string} tableId - Table name or shape ID.
2732
2734
  * @param {number} rowIndex - 0-based row index.
@@ -237,6 +237,18 @@ class ValidationEngine {
237
237
  if (!ppt.zipManager.hasFile(resolved)) {
238
238
  errors.push(`Relationship ${rel.id} points to non-existent file: ${resolved}`)
239
239
  }
240
+
241
+ // Catch redundant relative path traversals (e.g., '../slides/slide1.xml' from 'ppt/slides/slide2.xml')
242
+ const sourceDir = partPath.split('/').slice(0, -1).join('/')
243
+ const resolvedDir = resolved.split('/').slice(0, -1).join('/')
244
+ if (sourceDir === resolvedDir) {
245
+ const expectedTarget = resolved.split('/').pop()
246
+ if (rel.target !== expectedTarget) {
247
+ errors.push(
248
+ `Relationship "${rel.id}" in ${relsPath} uses redundant relative target "${rel.target}" (expected "${expectedTarget}")`
249
+ )
250
+ }
251
+ }
240
252
  }
241
253
  }
242
254
 
@@ -100,7 +100,7 @@ class HyperlinkManager {
100
100
  const targetInfo = slideManager.getSlideInfo(targetSlideIndex)
101
101
 
102
102
  // Build relative target path from source to target slide
103
- const relativePath = `../slides/${targetInfo.zipPath.split('/').pop()}`
103
+ const relativePath = targetInfo.zipPath.split('/').pop()
104
104
 
105
105
  // Add relationship pointing to the target slide
106
106
  const rId = relationshipManager.addRelationship(
@@ -130,7 +130,7 @@ class HyperlinkManager {
130
130
  const sourceInfo = slideManager.getSlideInfo(sourceSlideIndex)
131
131
  const targetInfo = slideManager.getSlideInfo(targetSlideIndex)
132
132
 
133
- const relativePath = `../slides/${targetInfo.zipPath.split('/').pop()}`
133
+ const relativePath = targetInfo.zipPath.split('/').pop()
134
134
  const rId = relationshipManager.addRelationship(
135
135
  sourceInfo.zipPath,
136
136
  REL_TYPES.SLIDE,
@@ -167,7 +167,7 @@ class HyperlinkManager {
167
167
  const sourceInfo = slideManager.getSlideInfo(sourceSlideIndex)
168
168
  const targetInfo = slideManager.getSlideInfo(targetSlideIndex)
169
169
 
170
- const relativePath = `../slides/${targetInfo.zipPath.split('/').pop()}`
170
+ const relativePath = targetInfo.zipPath.split('/').pop()
171
171
  const rId = relationshipManager.addRelationship(
172
172
  sourceInfo.zipPath,
173
173
  REL_TYPES.SLIDE,
@@ -223,7 +223,7 @@ class TableManager {
223
223
  )
224
224
  }
225
225
 
226
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
226
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
227
227
 
228
228
  if (cellShapes) {
229
229
  this.#processCellShapes(
@@ -796,8 +796,6 @@ class TableManager {
796
796
  getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager) {
797
797
  const { tblObj, frameObj } = this.#getTableContext(slideIndex, tableId, slideManager)
798
798
 
799
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
800
-
801
799
  const xfrm = frameObj['p:xfrm']
802
800
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
803
801
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -807,7 +805,7 @@ class TableManager {
807
805
  const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
808
806
 
809
807
  const trsArr = tblObj['a:tr'] || []
810
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
808
+ const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
811
809
 
812
810
  const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
813
811
  const pr = parent.row
@@ -1936,8 +1934,6 @@ class TableManager {
1936
1934
  slideManager
1937
1935
  )
1938
1936
 
1939
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1940
-
1941
1937
  const xfrm = frameObj['p:xfrm']
1942
1938
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1943
1939
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -1947,7 +1943,7 @@ class TableManager {
1947
1943
  const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1948
1944
 
1949
1945
  const trsArr = tblObj['a:tr'] || []
1950
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1946
+ const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
1951
1947
 
1952
1948
  const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
1953
1949
  const pr = parent.row
@@ -2021,8 +2017,6 @@ class TableManager {
2021
2017
  slideManager
2022
2018
  )
2023
2019
 
2024
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
2025
-
2026
2020
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
2027
2021
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
2028
2022
  const matchingShapes = shapes.filter(
@@ -2046,7 +2040,7 @@ class TableManager {
2046
2040
  const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2047
2041
 
2048
2042
  const trsArr = tblObj['a:tr'] || []
2049
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2043
+ const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
2050
2044
 
2051
2045
  const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
2052
2046
  const pr = parent.row
@@ -2158,7 +2152,7 @@ class TableManager {
2158
2152
  }
2159
2153
  }
2160
2154
 
2161
- #calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
2155
+ #calculateRowHeights(slideIndex, tableId, slideManager, tblObj, writeToXml = true) {
2162
2156
  const trsArr = tblObj['a:tr'] || []
2163
2157
  if (trsArr.length === 0) return []
2164
2158
 
@@ -2172,7 +2166,7 @@ class TableManager {
2172
2166
  // Initialize rowHeights with original height or a safe minimum floor of 228600 EMUs (~24px/pt)
2173
2167
  const rowHeights = trsArr.map(row => {
2174
2168
  const h = parseInt(row['@_h'] || 0, 10)
2175
- return Math.max(h, 228600)
2169
+ return h > 0 ? h : 228600
2176
2170
  })
2177
2171
 
2178
2172
  // Helper to get paragraph font size
@@ -2295,7 +2289,9 @@ class TableManager {
2295
2289
  }
2296
2290
 
2297
2291
  const totalCellHeight_emu = marT + marB + textHeight_emu
2298
- cellHeights[r][c] = Math.max(totalCellHeight_emu, 228600)
2292
+ const rowTemplateHeight = parseInt(row['@_h'] || 0, 10)
2293
+ const minFloor = rowTemplateHeight > 0 ? rowTemplateHeight : 228600
2294
+ cellHeights[r][c] = Math.max(totalCellHeight_emu, minFloor)
2299
2295
  }
2300
2296
  }
2301
2297
 
@@ -2346,8 +2342,10 @@ class TableManager {
2346
2342
  }
2347
2343
 
2348
2344
  // Update row heights in XML
2349
- for (let r = 0; r < numRows; r++) {
2350
- trsArr[r]['@_h'] = String(rowHeights[r])
2345
+ if (writeToXml) {
2346
+ for (let r = 0; r < numRows; r++) {
2347
+ trsArr[r]['@_h'] = String(rowHeights[r])
2348
+ }
2351
2349
  }
2352
2350
 
2353
2351
  return rowHeights