node-pptx-templater 1.1.0 → 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 +2 -2
- package/src/core/PPTXTemplater.js +164 -16
- package/src/core/ValidationEngine.js +12 -0
- package/src/managers/HyperlinkManager.js +3 -3
- package/src/managers/TableManager.js +82 -17
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",
|
|
@@ -191,4 +191,4 @@
|
|
|
191
191
|
"LICENSE",
|
|
192
192
|
"CHANGELOG.md"
|
|
193
193
|
]
|
|
194
|
-
}
|
|
194
|
+
}
|
|
@@ -2556,7 +2556,11 @@ class PPTXTemplater {
|
|
|
2556
2556
|
this.#assertLoaded()
|
|
2557
2557
|
const targetIndices = this.#getTargetSlideIndices()
|
|
2558
2558
|
for (const idx of targetIndices) {
|
|
2559
|
-
|
|
2559
|
+
let resolvedOptions = options
|
|
2560
|
+
if (options.alignToCell) {
|
|
2561
|
+
resolvedOptions = this.#resolveAlignToCell(idx, options, shapeId, true)
|
|
2562
|
+
}
|
|
2563
|
+
this.#shapeManager.updateShapePosition(idx, shapeId, resolvedOptions, this.#slideManager)
|
|
2560
2564
|
}
|
|
2561
2565
|
return this
|
|
2562
2566
|
}
|
|
@@ -2650,17 +2654,22 @@ class PPTXTemplater {
|
|
|
2650
2654
|
return this.#shapeManager.validateShape(options)
|
|
2651
2655
|
}
|
|
2652
2656
|
|
|
2653
|
-
|
|
2654
|
-
* Adds a new shape dynamically to the targeted slide(s).
|
|
2655
|
-
*
|
|
2656
|
-
* @param {Object} options Shape configuration options.
|
|
2657
|
-
* @returns {this} The chainable presentation templater instance.
|
|
2658
|
-
*/
|
|
2659
|
-
async addShape(options) {
|
|
2657
|
+
async addShape(typeOrOptions, options = {}) {
|
|
2660
2658
|
this.#assertLoaded()
|
|
2659
|
+
let resolvedOptions = {}
|
|
2660
|
+
if (typeof typeOrOptions === 'string') {
|
|
2661
|
+
resolvedOptions = { ...options, type: typeOrOptions }
|
|
2662
|
+
} else {
|
|
2663
|
+
resolvedOptions = { ...typeOrOptions }
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2661
2666
|
const targetIndices = this.#getTargetSlideIndices()
|
|
2662
2667
|
for (const idx of targetIndices) {
|
|
2663
|
-
|
|
2668
|
+
let finalOptions = resolvedOptions
|
|
2669
|
+
if (resolvedOptions.alignToCell) {
|
|
2670
|
+
finalOptions = this.#resolveAlignToCell(idx, resolvedOptions)
|
|
2671
|
+
}
|
|
2672
|
+
this.#shapeManager.addShape(idx, finalOptions, this.#slideManager)
|
|
2664
2673
|
}
|
|
2665
2674
|
return this
|
|
2666
2675
|
}
|
|
@@ -2676,7 +2685,11 @@ class PPTXTemplater {
|
|
|
2676
2685
|
this.#assertLoaded()
|
|
2677
2686
|
const targetIndices = this.#getTargetSlideIndices()
|
|
2678
2687
|
for (const idx of targetIndices) {
|
|
2679
|
-
|
|
2688
|
+
let resolvedOptions = options
|
|
2689
|
+
if (options.alignToCell) {
|
|
2690
|
+
resolvedOptions = this.#resolveAlignToCell(idx, options, shapeId)
|
|
2691
|
+
}
|
|
2692
|
+
this.#shapeManager.updateShape(idx, shapeId, resolvedOptions, this.#slideManager)
|
|
2680
2693
|
}
|
|
2681
2694
|
return this
|
|
2682
2695
|
}
|
|
@@ -2714,6 +2727,8 @@ class PPTXTemplater {
|
|
|
2714
2727
|
|
|
2715
2728
|
/**
|
|
2716
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.
|
|
2717
2732
|
*
|
|
2718
2733
|
* @param {string} tableId - Table name or shape ID.
|
|
2719
2734
|
* @param {number} rowIndex - 0-based row index.
|
|
@@ -2822,13 +2837,17 @@ class PPTXTemplater {
|
|
|
2822
2837
|
/**
|
|
2823
2838
|
* Retrieves final rendered bounds of a table cell in pixels.
|
|
2824
2839
|
*
|
|
2825
|
-
* @param {string}
|
|
2840
|
+
* @param {string|Object} tableIdOrObj - Table name, shape ID, or table object.
|
|
2826
2841
|
* @param {number} rowIndex - 0-based row index.
|
|
2827
2842
|
* @param {number} colIndex - 0-based column index.
|
|
2828
2843
|
* @returns {Object|null} Cell bounds { x, y, width, height } in pixels, or null.
|
|
2829
2844
|
*/
|
|
2830
|
-
getCellBounds(
|
|
2845
|
+
getCellBounds(tableIdOrObj, rowIndex, colIndex) {
|
|
2831
2846
|
this.#assertLoaded()
|
|
2847
|
+
const tableId =
|
|
2848
|
+
typeof tableIdOrObj === 'object'
|
|
2849
|
+
? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
|
|
2850
|
+
: tableIdOrObj
|
|
2832
2851
|
const targetIndices = this.#getTargetSlideIndices()
|
|
2833
2852
|
for (const idx of targetIndices) {
|
|
2834
2853
|
try {
|
|
@@ -2851,14 +2870,21 @@ class PPTXTemplater {
|
|
|
2851
2870
|
|
|
2852
2871
|
/**
|
|
2853
2872
|
* Retrieves final rendered position of a table cell in pixels.
|
|
2873
|
+
* Optionally calculates centered top-left coordinates for a shape of given dimensions.
|
|
2854
2874
|
*
|
|
2855
|
-
* @param {string}
|
|
2875
|
+
* @param {string|Object} tableIdOrObj - Table name, shape ID, or table object.
|
|
2856
2876
|
* @param {number} rowIndex - 0-based row index.
|
|
2857
2877
|
* @param {number} colIndex - 0-based column index.
|
|
2858
|
-
* @
|
|
2878
|
+
* @param {number|Object} [shapeWidthOrOptions] - Width of the shape in pixels, or options object.
|
|
2879
|
+
* @param {number} [shapeHeight] - Height of the shape in pixels.
|
|
2880
|
+
* @returns {Object|null} Cell position { row, column, x, y, width, height } in pixels, or null.
|
|
2859
2881
|
*/
|
|
2860
|
-
getCellPosition(
|
|
2882
|
+
getCellPosition(tableIdOrObj, rowIndex, colIndex, shapeWidthOrOptions, shapeHeight) {
|
|
2861
2883
|
this.#assertLoaded()
|
|
2884
|
+
const tableId =
|
|
2885
|
+
typeof tableIdOrObj === 'object'
|
|
2886
|
+
? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
|
|
2887
|
+
: tableIdOrObj
|
|
2862
2888
|
const targetIndices = this.#getTargetSlideIndices()
|
|
2863
2889
|
for (const idx of targetIndices) {
|
|
2864
2890
|
try {
|
|
@@ -2867,7 +2893,9 @@ class PPTXTemplater {
|
|
|
2867
2893
|
tableId,
|
|
2868
2894
|
rowIndex,
|
|
2869
2895
|
colIndex,
|
|
2870
|
-
this.#slideManager
|
|
2896
|
+
this.#slideManager,
|
|
2897
|
+
shapeWidthOrOptions,
|
|
2898
|
+
shapeHeight
|
|
2871
2899
|
)
|
|
2872
2900
|
if (pos) return pos
|
|
2873
2901
|
} catch (err) {
|
|
@@ -3548,6 +3576,126 @@ class PPTXTemplater {
|
|
|
3548
3576
|
this.#zOrderManager.normalizeZOrder(targetIdx, this.#slideManager)
|
|
3549
3577
|
return this
|
|
3550
3578
|
}
|
|
3579
|
+
|
|
3580
|
+
/**
|
|
3581
|
+
* Aligns an existing shape to a table cell's position.
|
|
3582
|
+
*
|
|
3583
|
+
* @param {string} shapeId - Unique shape name/id in the template.
|
|
3584
|
+
* @param {string|Object} tableIdOrObj - Table ID string, or table object.
|
|
3585
|
+
* @param {number} rowIndex - 0-based row index.
|
|
3586
|
+
* @param {number} colIndex - 0-based column index.
|
|
3587
|
+
* @param {Object} [options] - Alignment options.
|
|
3588
|
+
* @param {'left'|'center'|'right'} [options.horizontal='center'] - Horizontal alignment.
|
|
3589
|
+
* @param {'top'|'middle'|'bottom'} [options.vertical='middle'] - Vertical alignment.
|
|
3590
|
+
* @returns {this} The chainable presentation templater instance.
|
|
3591
|
+
*/
|
|
3592
|
+
alignShapeToCell(shapeId, tableIdOrObj, rowIndex, colIndex, options = {}) {
|
|
3593
|
+
const tableId =
|
|
3594
|
+
typeof tableIdOrObj === 'object'
|
|
3595
|
+
? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
|
|
3596
|
+
: tableIdOrObj
|
|
3597
|
+
this.updateShapePosition(shapeId, {
|
|
3598
|
+
alignToCell: {
|
|
3599
|
+
table: tableId,
|
|
3600
|
+
row: rowIndex,
|
|
3601
|
+
col: colIndex,
|
|
3602
|
+
horizontal: options.horizontal || 'center',
|
|
3603
|
+
vertical: options.vertical || 'middle',
|
|
3604
|
+
},
|
|
3605
|
+
})
|
|
3606
|
+
return this
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
#resolveAlignToCell(slideIndex, options, shapeId, convertToEmus = false) {
|
|
3610
|
+
const align = options.alignToCell
|
|
3611
|
+
if (!align || !align.table) return options
|
|
3612
|
+
|
|
3613
|
+
const tableId =
|
|
3614
|
+
typeof align.table === 'object'
|
|
3615
|
+
? align.table.id || align.table.name || align.table.tableId
|
|
3616
|
+
: align.table
|
|
3617
|
+
const row = align.row !== undefined ? align.row : 0
|
|
3618
|
+
const col = align.col !== undefined ? align.col : 0
|
|
3619
|
+
|
|
3620
|
+
// Get cell bounds
|
|
3621
|
+
const bounds = this.#tableManager.getCellBounds(
|
|
3622
|
+
slideIndex,
|
|
3623
|
+
tableId,
|
|
3624
|
+
row,
|
|
3625
|
+
col,
|
|
3626
|
+
this.#slideManager
|
|
3627
|
+
)
|
|
3628
|
+
|
|
3629
|
+
if (!bounds) return options
|
|
3630
|
+
|
|
3631
|
+
// Determine shape dimensions
|
|
3632
|
+
let shapeWidth = options.width
|
|
3633
|
+
let shapeHeight = options.height
|
|
3634
|
+
|
|
3635
|
+
if (convertToEmus) {
|
|
3636
|
+
if (shapeWidth !== undefined) shapeWidth = Math.round(shapeWidth / 9525)
|
|
3637
|
+
if (shapeHeight !== undefined) shapeHeight = Math.round(shapeHeight / 9525)
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
if (shapeWidth === undefined || shapeHeight === undefined) {
|
|
3641
|
+
if (options.type === 'square' && options.size !== undefined) {
|
|
3642
|
+
shapeWidth = options.size
|
|
3643
|
+
shapeHeight = options.size
|
|
3644
|
+
} else if (options.type === 'circle' && options.radius !== undefined) {
|
|
3645
|
+
shapeWidth = options.radius * 2
|
|
3646
|
+
shapeHeight = options.radius * 2
|
|
3647
|
+
} else if (shapeId) {
|
|
3648
|
+
// Try getting existing shape dimensions
|
|
3649
|
+
const existing = this.#shapeManager.getShape(slideIndex, shapeId, this.#slideManager)
|
|
3650
|
+
if (existing) {
|
|
3651
|
+
shapeWidth = existing.width
|
|
3652
|
+
shapeHeight = existing.height
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
// Default to fallback dimensions if still undefined
|
|
3658
|
+
if (shapeWidth === undefined) shapeWidth = 100
|
|
3659
|
+
if (shapeHeight === undefined) shapeHeight = 100
|
|
3660
|
+
|
|
3661
|
+
// Align horizontally
|
|
3662
|
+
let horiz = align.horizontal || align.alignX || 'center'
|
|
3663
|
+
horiz = String(horiz).toLowerCase()
|
|
3664
|
+
if (horiz === 'middle') horiz = 'center'
|
|
3665
|
+
|
|
3666
|
+
let x = bounds.x
|
|
3667
|
+
if (horiz === 'center') {
|
|
3668
|
+
x = bounds.x + (bounds.width - shapeWidth) / 2
|
|
3669
|
+
} else if (horiz === 'right') {
|
|
3670
|
+
x = bounds.x + bounds.width - shapeWidth
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// Align vertically
|
|
3674
|
+
let vert = align.vertical || align.alignY || 'middle'
|
|
3675
|
+
vert = String(vert).toLowerCase()
|
|
3676
|
+
if (vert === 'center') vert = 'middle'
|
|
3677
|
+
|
|
3678
|
+
let y = bounds.y
|
|
3679
|
+
if (vert === 'middle') {
|
|
3680
|
+
y = bounds.y + (bounds.height - shapeHeight) / 2
|
|
3681
|
+
} else if (vert === 'bottom') {
|
|
3682
|
+
y = bounds.y + bounds.height - shapeHeight
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
const resolved = { ...options }
|
|
3686
|
+
if (convertToEmus) {
|
|
3687
|
+
resolved.x = Math.round(x * 9525)
|
|
3688
|
+
resolved.y = Math.round(y * 9525)
|
|
3689
|
+
} else {
|
|
3690
|
+
resolved.x = Math.round(x)
|
|
3691
|
+
resolved.y = Math.round(y)
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// Remove alignToCell to prevent it polluting lower levels
|
|
3695
|
+
delete resolved.alignToCell
|
|
3696
|
+
|
|
3697
|
+
return resolved
|
|
3698
|
+
}
|
|
3551
3699
|
}
|
|
3552
3700
|
|
|
3553
3701
|
module.exports = { PPTXTemplater }
|
|
@@ -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(
|
|
@@ -805,7 +805,7 @@ class TableManager {
|
|
|
805
805
|
const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
806
806
|
|
|
807
807
|
const trsArr = tblObj['a:tr'] || []
|
|
808
|
-
const rowHeights =
|
|
808
|
+
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
|
|
809
809
|
|
|
810
810
|
const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
811
811
|
const pr = parent.row
|
|
@@ -843,13 +843,50 @@ class TableManager {
|
|
|
843
843
|
}
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
-
getCellPosition(
|
|
846
|
+
getCellPosition(
|
|
847
|
+
slideIndex,
|
|
848
|
+
tableId,
|
|
849
|
+
rowIndex,
|
|
850
|
+
colIndex,
|
|
851
|
+
slideManager,
|
|
852
|
+
shapeWidthOrOptions,
|
|
853
|
+
shapeHeight
|
|
854
|
+
) {
|
|
847
855
|
const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
856
|
+
if (!bounds) return null
|
|
857
|
+
|
|
858
|
+
let shapeWidth
|
|
859
|
+
let shapeHeightVal
|
|
860
|
+
|
|
861
|
+
if (shapeWidthOrOptions && typeof shapeWidthOrOptions === 'object') {
|
|
862
|
+
shapeWidth =
|
|
863
|
+
shapeWidthOrOptions.width !== undefined
|
|
864
|
+
? shapeWidthOrOptions.width
|
|
865
|
+
: shapeWidthOrOptions.shapeWidth
|
|
866
|
+
shapeHeightVal =
|
|
867
|
+
shapeWidthOrOptions.height !== undefined
|
|
868
|
+
? shapeWidthOrOptions.height
|
|
869
|
+
: shapeWidthOrOptions.shapeHeight
|
|
870
|
+
} else {
|
|
871
|
+
shapeWidth = shapeWidthOrOptions
|
|
872
|
+
shapeHeightVal = shapeHeight
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
let x = bounds.x
|
|
876
|
+
let y = bounds.y
|
|
877
|
+
|
|
878
|
+
if (shapeWidth !== undefined && shapeHeightVal !== undefined) {
|
|
879
|
+
x = Math.round(bounds.x + (bounds.width - shapeWidth) / 2)
|
|
880
|
+
y = Math.round(bounds.y + (bounds.height - shapeHeightVal) / 2)
|
|
881
|
+
}
|
|
882
|
+
|
|
848
883
|
return {
|
|
849
884
|
row: rowIndex,
|
|
850
885
|
column: colIndex,
|
|
851
|
-
x
|
|
852
|
-
y
|
|
886
|
+
x,
|
|
887
|
+
y,
|
|
888
|
+
width: bounds.width,
|
|
889
|
+
height: bounds.height,
|
|
853
890
|
}
|
|
854
891
|
}
|
|
855
892
|
|
|
@@ -1352,8 +1389,17 @@ class TableManager {
|
|
|
1352
1389
|
}
|
|
1353
1390
|
|
|
1354
1391
|
// 2. Determine alignment settings
|
|
1355
|
-
let alignX = config.alignX
|
|
1356
|
-
let alignY = config.alignY
|
|
1392
|
+
let alignX = config.alignX || config.horizontal
|
|
1393
|
+
let alignY = config.alignY || config.vertical
|
|
1394
|
+
|
|
1395
|
+
if (alignX) {
|
|
1396
|
+
alignX = String(alignX).toLowerCase()
|
|
1397
|
+
if (alignX === 'middle') alignX = 'center'
|
|
1398
|
+
}
|
|
1399
|
+
if (alignY) {
|
|
1400
|
+
alignY = String(alignY).toLowerCase()
|
|
1401
|
+
if (alignY === 'center') alignY = 'middle'
|
|
1402
|
+
}
|
|
1357
1403
|
|
|
1358
1404
|
if (config.position) {
|
|
1359
1405
|
switch (config.position) {
|
|
@@ -1444,7 +1490,13 @@ class TableManager {
|
|
|
1444
1490
|
|
|
1445
1491
|
// 4. Boundary Constraints Validation/Enforcement
|
|
1446
1492
|
if (shapeWidth > cellWidth_px) {
|
|
1447
|
-
|
|
1493
|
+
if (alignX === 'center') {
|
|
1494
|
+
shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
|
|
1495
|
+
} else if (alignX === 'right') {
|
|
1496
|
+
shapeLeft = cellLeft_px + cellWidth_px - shapeWidth
|
|
1497
|
+
} else {
|
|
1498
|
+
shapeLeft = cellLeft_px
|
|
1499
|
+
}
|
|
1448
1500
|
} else {
|
|
1449
1501
|
shapeLeft = Math.max(
|
|
1450
1502
|
cellLeft_px,
|
|
@@ -1453,7 +1505,13 @@ class TableManager {
|
|
|
1453
1505
|
}
|
|
1454
1506
|
|
|
1455
1507
|
if (shapeHeight > cellHeight_px) {
|
|
1456
|
-
|
|
1508
|
+
if (alignY === 'middle') {
|
|
1509
|
+
shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
|
|
1510
|
+
} else if (alignY === 'bottom') {
|
|
1511
|
+
shapeTop = cellTop_px + cellHeight_px - shapeHeight
|
|
1512
|
+
} else {
|
|
1513
|
+
shapeTop = cellTop_px
|
|
1514
|
+
}
|
|
1457
1515
|
} else {
|
|
1458
1516
|
shapeTop = Math.max(
|
|
1459
1517
|
cellTop_px,
|
|
@@ -1885,7 +1943,7 @@ class TableManager {
|
|
|
1885
1943
|
const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
1886
1944
|
|
|
1887
1945
|
const trsArr = tblObj['a:tr'] || []
|
|
1888
|
-
const rowHeights =
|
|
1946
|
+
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
|
|
1889
1947
|
|
|
1890
1948
|
const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
1891
1949
|
const pr = parent.row
|
|
@@ -1982,7 +2040,7 @@ class TableManager {
|
|
|
1982
2040
|
const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
1983
2041
|
|
|
1984
2042
|
const trsArr = tblObj['a:tr'] || []
|
|
1985
|
-
const rowHeights =
|
|
2043
|
+
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
|
|
1986
2044
|
|
|
1987
2045
|
const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
1988
2046
|
const pr = parent.row
|
|
@@ -2094,7 +2152,7 @@ class TableManager {
|
|
|
2094
2152
|
}
|
|
2095
2153
|
}
|
|
2096
2154
|
|
|
2097
|
-
#calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
|
|
2155
|
+
#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, writeToXml = true) {
|
|
2098
2156
|
const trsArr = tblObj['a:tr'] || []
|
|
2099
2157
|
if (trsArr.length === 0) return []
|
|
2100
2158
|
|
|
@@ -2105,8 +2163,11 @@ class TableManager {
|
|
|
2105
2163
|
const numRows = trsArr.length
|
|
2106
2164
|
const numCols = colWidths.length
|
|
2107
2165
|
|
|
2108
|
-
// Initialize rowHeights with original height or
|
|
2109
|
-
const rowHeights = trsArr.map(row =>
|
|
2166
|
+
// Initialize rowHeights with original height or a safe minimum floor of 228600 EMUs (~24px/pt)
|
|
2167
|
+
const rowHeights = trsArr.map(row => {
|
|
2168
|
+
const h = parseInt(row['@_h'] || 0, 10)
|
|
2169
|
+
return h > 0 ? h : 228600
|
|
2170
|
+
})
|
|
2110
2171
|
|
|
2111
2172
|
// Helper to get paragraph font size
|
|
2112
2173
|
const getParagraphFontSize = p => {
|
|
@@ -2228,7 +2289,9 @@ class TableManager {
|
|
|
2228
2289
|
}
|
|
2229
2290
|
|
|
2230
2291
|
const totalCellHeight_emu = marT + marB + textHeight_emu
|
|
2231
|
-
|
|
2292
|
+
const rowTemplateHeight = parseInt(row['@_h'] || 0, 10)
|
|
2293
|
+
const minFloor = rowTemplateHeight > 0 ? rowTemplateHeight : 228600
|
|
2294
|
+
cellHeights[r][c] = Math.max(totalCellHeight_emu, minFloor)
|
|
2232
2295
|
}
|
|
2233
2296
|
}
|
|
2234
2297
|
|
|
@@ -2279,8 +2342,10 @@ class TableManager {
|
|
|
2279
2342
|
}
|
|
2280
2343
|
|
|
2281
2344
|
// Update row heights in XML
|
|
2282
|
-
|
|
2283
|
-
|
|
2345
|
+
if (writeToXml) {
|
|
2346
|
+
for (let r = 0; r < numRows; r++) {
|
|
2347
|
+
trsArr[r]['@_h'] = String(rowHeights[r])
|
|
2348
|
+
}
|
|
2284
2349
|
}
|
|
2285
2350
|
|
|
2286
2351
|
return rowHeights
|