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 +16 -0
- package/README.md +34 -10
- package/package.json +1 -1
- package/src/core/PPTXTemplater.js +2 -0
- package/src/core/ValidationEngine.js +12 -0
- package/src/managers/HyperlinkManager.js +3 -3
- package/src/managers/TableManager.js +13 -15
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.
|
|
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
|
|
179
|
-
await ppt.addCellShape('
|
|
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
|
|
187
|
-
await ppt.addCellShape('
|
|
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: '
|
|
190
|
-
fill: '#
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2350
|
-
|
|
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
|