node-pptx-templater 1.0.19 → 1.0.21

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.
@@ -66,8 +66,12 @@ class TableManager {
66
66
  * @param {SlideManager} slideManager
67
67
  * @throws {TableNotFoundError} If the table is not found.
68
68
  */
69
- updateTable(slideIndex, tableId, data, slideManager) {
70
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
69
+ updateTable(slideIndex, tableId, data, slideManager, shapeManager) {
70
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
71
+ slideIndex,
72
+ tableId,
73
+ slideManager
74
+ )
71
75
 
72
76
  const trs = tblObj['a:tr'] || []
73
77
  if (trs.length === 0) {
@@ -77,12 +81,14 @@ class TableManager {
77
81
 
78
82
  let rowsData = []
79
83
  let templateMerges = []
84
+ let cellShapes = null
80
85
 
81
86
  if (Array.isArray(data)) {
82
87
  rowsData = data
83
88
  } else if (data && typeof data === 'object') {
84
89
  rowsData = data.rows || []
85
90
  templateMerges = data.merge || []
91
+ cellShapes = data.cellShapes || null
86
92
  }
87
93
 
88
94
  const headerTemplate = trs[0]
@@ -91,80 +97,113 @@ class TableManager {
91
97
  const newRows = []
92
98
  const generatedMerges = []
93
99
 
94
- for (let i = 0; i < rowsData.length; i++) {
95
- const template = i === 0 ? headerTemplate : trs[i] || dataTemplate
96
- const newRow = this.#xmlParser.deepClone(template)
97
- this.#updateRowId(newRow)
98
-
99
- const tcs = newRow['a:tc'] || []
100
- const rowData = rowsData[i]
101
-
102
- for (let j = 0; j < tcs.length; j++) {
103
- const rawCell = rowData && rowData[j] !== undefined ? rowData[j] : ''
104
- let val = ''
105
- let cellOptions = {}
106
-
107
- if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
108
- if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
109
- if (tcs[j]['@_gridSpan']) delete tcs[j]['@_gridSpan']
110
- if (tcs[j]['@_rowSpan']) delete tcs[j]['@_rowSpan']
111
-
112
- if (rawCell && typeof rawCell === 'object') {
113
- val = rawCell.value !== undefined ? rawCell.value : ''
114
- const rowSpan = parseInt(rawCell.rowSpan || 1, 10)
115
- const colSpan = parseInt(rawCell.colSpan || rawCell.gridSpan || 1, 10)
116
- if (rowSpan > 1 || colSpan > 1) {
117
- generatedMerges.push({
118
- startRow: i,
119
- startCol: j,
120
- endRow: i + rowSpan - 1,
121
- endCol: j + colSpan - 1,
122
- })
100
+ const headerNames = (trs[0]['a:tc'] || []).map(cell => this.#getCellText(cell).trim())
101
+ const isObjectRows =
102
+ rowsData.length > 0 && !Array.isArray(rowsData[0]) && typeof rowsData[0] === 'object'
103
+
104
+ if (isObjectRows) {
105
+ // 1. Keep/clone the header row
106
+ const headerRow = this.#xmlParser.deepClone(headerTemplate)
107
+ this.#updateRowId(headerRow)
108
+ newRows.push(headerRow)
109
+
110
+ // 2. Map objects to data rows
111
+ for (let i = 0; i < rowsData.length; i++) {
112
+ const newRow = this.#xmlParser.deepClone(dataTemplate)
113
+ this.#updateRowId(newRow)
114
+
115
+ const tcs = newRow['a:tc'] || []
116
+ const rowObj = rowsData[i]
117
+
118
+ for (let j = 0; j < tcs.length; j++) {
119
+ const headerName = headerNames[j]
120
+ let rawCell = undefined
121
+ if (headerName) {
122
+ if (rowObj[headerName] !== undefined) {
123
+ rawCell = rowObj[headerName]
124
+ } else if (rowObj[headerName.toLowerCase()] !== undefined) {
125
+ rawCell = rowObj[headerName.toLowerCase()]
126
+ }
123
127
  }
124
- cellOptions = rawCell
125
- } else {
126
- val = String(rawCell)
127
- }
128
-
129
- this.#setCellTextObj(tcs[j], val)
130
-
131
- // Apply style properties if specified on the cell object
132
- if (cellOptions.fill) {
133
- if (!tcs[j]['a:tcPr']) tcs[j]['a:tcPr'] = {}
134
- tcs[j]['a:tcPr']['a:solidFill'] = {
135
- 'a:srgbClr': { '@_val': cellOptions.fill },
128
+ if (rawCell === undefined && rowObj[j] !== undefined) {
129
+ rawCell = rowObj[j]
130
+ }
131
+ if (rawCell === undefined) {
132
+ rawCell = ''
136
133
  }
137
- }
138
134
 
139
- if (cellOptions.align) {
140
- const txBody = tcs[j]['a:txBody']
141
- if (txBody && txBody['a:p']) {
142
- const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
143
- for (const p of paras) {
144
- if (!p['a:pPr']) p['a:pPr'] = {}
145
- p['a:pPr']['@_algn'] = cellOptions.align
135
+ let val = ''
136
+ let cellOptions = {}
137
+
138
+ if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
139
+ if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
140
+ if (tcs[j]['@_gridSpan']) delete tcs[j]['@_gridSpan']
141
+ if (tcs[j]['@_rowSpan']) delete tcs[j]['@_rowSpan']
142
+
143
+ if (rawCell && typeof rawCell === 'object') {
144
+ val = rawCell.value !== undefined ? rawCell.value : ''
145
+ const rowSpan = parseInt(rawCell.rowSpan || 1, 10)
146
+ const colSpan = parseInt(rawCell.colSpan || rawCell.gridSpan || 1, 10)
147
+ if (rowSpan > 1 || colSpan > 1) {
148
+ generatedMerges.push({
149
+ startRow: i + 1,
150
+ startCol: j,
151
+ endRow: i + 1 + rowSpan - 1,
152
+ endCol: j + colSpan - 1,
153
+ })
146
154
  }
155
+ cellOptions = rawCell
156
+ } else {
157
+ val = String(rawCell)
147
158
  }
148
- }
149
159
 
150
- if (cellOptions.fontSize) {
151
- const sizeVal = cellOptions.fontSize * 100
152
- const txBody = tcs[j]['a:txBody']
153
- if (txBody && txBody['a:p']) {
154
- const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
155
- for (const p of paras) {
156
- if (p['a:r']) {
157
- const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
158
- for (const r of runs) {
159
- if (!r['a:rPr']) r['a:rPr'] = {}
160
- r['a:rPr']['@_sz'] = String(sizeVal)
161
- }
162
- }
160
+ this.#setCellTextObj(tcs[j], val)
161
+ this.#applyCellOptions(tcs[j], cellOptions)
162
+ }
163
+ newRows.push(newRow)
164
+ }
165
+ } else {
166
+ // 2D array mapping
167
+ for (let i = 0; i < rowsData.length; i++) {
168
+ const template = i === 0 ? headerTemplate : trs[i] || dataTemplate
169
+ const newRow = this.#xmlParser.deepClone(template)
170
+ this.#updateRowId(newRow)
171
+
172
+ const tcs = newRow['a:tc'] || []
173
+ const rowData = rowsData[i]
174
+
175
+ for (let j = 0; j < tcs.length; j++) {
176
+ const rawCell = rowData && rowData[j] !== undefined ? rowData[j] : ''
177
+ let val = ''
178
+ let cellOptions = {}
179
+
180
+ if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
181
+ if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
182
+ if (tcs[j]['@_gridSpan']) delete tcs[j]['@_gridSpan']
183
+ if (tcs[j]['@_rowSpan']) delete tcs[j]['@_rowSpan']
184
+
185
+ if (rawCell && typeof rawCell === 'object') {
186
+ val = rawCell.value !== undefined ? rawCell.value : ''
187
+ const rowSpan = parseInt(rawCell.rowSpan || 1, 10)
188
+ const colSpan = parseInt(rawCell.colSpan || rawCell.gridSpan || 1, 10)
189
+ if (rowSpan > 1 || colSpan > 1) {
190
+ generatedMerges.push({
191
+ startRow: i,
192
+ startCol: j,
193
+ endRow: i + rowSpan - 1,
194
+ endCol: j + colSpan - 1,
195
+ })
163
196
  }
197
+ cellOptions = rawCell
198
+ } else {
199
+ val = String(rawCell)
164
200
  }
201
+
202
+ this.#setCellTextObj(tcs[j], val)
203
+ this.#applyCellOptions(tcs[j], cellOptions)
165
204
  }
205
+ newRows.push(newRow)
166
206
  }
167
- newRows.push(newRow)
168
207
  }
169
208
 
170
209
  tblObj['a:tr'] = newRows
@@ -184,6 +223,23 @@ class TableManager {
184
223
  )
185
224
  }
186
225
 
226
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
227
+
228
+ if (cellShapes) {
229
+ this.#processCellShapes(
230
+ slideIndex,
231
+ tableId,
232
+ resolvedTableId,
233
+ rowsData,
234
+ isObjectRows,
235
+ cellShapes,
236
+ slideManager,
237
+ shapeManager,
238
+ tblObj,
239
+ frameObj
240
+ )
241
+ }
242
+
187
243
  logger.debug(
188
244
  `Updated table "${tableId}" with ${rowsData.length} rows and ${finalMerges.length} merges`
189
245
  )
@@ -197,7 +253,7 @@ class TableManager {
197
253
  * @param {string[]} rowData
198
254
  * @param {SlideManager} slideManager
199
255
  */
200
- addTableRow(slideIndex, tableId, rowData, slideManager) {
256
+ addTableRow(slideIndex, tableId, rowData, slideManager, options = {}) {
201
257
  const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
202
258
 
203
259
  const trs = tblObj['a:tr'] || []
@@ -206,18 +262,69 @@ class TableManager {
206
262
  }
207
263
 
208
264
  const lastRow = trs[trs.length - 1]
209
- const newRow = this.#xmlParser.deepClone(lastRow)
210
- this.#updateRowId(newRow)
265
+ const numCols = lastRow['a:tc']?.length || 0
211
266
 
212
- const tcs = newRow['a:tc'] || []
213
- for (let j = 0; j < tcs.length; j++) {
214
- this.#setCellTextObj(tcs[j], rowData[j] !== undefined ? rowData[j] : '')
215
- // Clear any merged indicators for the new row by default
216
- if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
217
- if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
267
+ // Compute target generated height
268
+ const heights = []
269
+ for (let c = 0; c < numCols; c++) {
270
+ heights.push(this.#getNestedHeight(rowData[c]))
218
271
  }
272
+ const targetHeight = Math.max(1, ...heights)
219
273
 
220
- trs.push(newRow)
274
+ // Expand each column value to targetHeight
275
+ const expandedCols = []
276
+ const strategy = options.mergeStrategy || 'auto'
277
+ for (let c = 0; c < numCols; c++) {
278
+ let colCells = this.#expandCellVal(rowData[c], targetHeight)
279
+ if (strategy === 'none') {
280
+ for (let i = 0; i < colCells.length; i++) {
281
+ if (colCells[i].vMerge) {
282
+ colCells[i] = { value: '', rowSpan: 1 }
283
+ } else {
284
+ colCells[i].rowSpan = 1
285
+ }
286
+ }
287
+ } else if (strategy === 'auto') {
288
+ colCells = this.#applyAutoMerge(colCells)
289
+ }
290
+ expandedCols.push(colCells)
291
+ }
292
+
293
+ // Clone and append rows
294
+ for (let r = 0; r < targetHeight; r++) {
295
+ const newRow = this.#xmlParser.deepClone(lastRow)
296
+ this.#updateRowId(newRow)
297
+
298
+ const tcs = newRow['a:tc'] || []
299
+ for (let c = 0; c < numCols; c++) {
300
+ const cellDef = expandedCols[c][r]
301
+ const tcObj = tcs[c]
302
+
303
+ // Clear any previous merge attributes
304
+ if (tcObj['@_hMerge']) delete tcObj['@_hMerge']
305
+ if (tcObj['@_vMerge']) delete tcObj['@_vMerge']
306
+ if (tcObj['@_gridSpan']) delete tcObj['@_gridSpan']
307
+ if (tcObj['@_rowSpan']) delete tcObj['@_rowSpan']
308
+
309
+ if (cellDef.vMerge) {
310
+ tcObj['@_vMerge'] = '1'
311
+ this.#setCellTextObj(tcObj, '')
312
+ } else {
313
+ let text = cellDef.value
314
+ let cellOpts = {}
315
+ if (cellDef.value && typeof cellDef.value === 'object') {
316
+ text = cellDef.value.value !== undefined ? cellDef.value.value : ''
317
+ cellOpts = cellDef.value
318
+ }
319
+ this.#setCellTextObj(tcObj, text)
320
+ if (cellDef.rowSpan && cellDef.rowSpan > 1 && strategy !== 'none') {
321
+ tcObj['@_rowSpan'] = String(cellDef.rowSpan)
322
+ }
323
+ this.#applyCellOptions(tcObj, cellOpts)
324
+ }
325
+ }
326
+ trs.push(newRow)
327
+ }
221
328
 
222
329
  slideManager.markSlideObjDirty(slideIndex)
223
330
  }
@@ -686,6 +793,68 @@ class TableManager {
686
793
  return { row, col }
687
794
  }
688
795
 
796
+ getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager) {
797
+ const { tblObj, frameObj } = this.#getTableContext(slideIndex, tableId, slideManager)
798
+
799
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
800
+
801
+ const xfrm = frameObj['p:xfrm']
802
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
803
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
804
+
805
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
806
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
807
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
808
+
809
+ const trsArr = tblObj['a:tr'] || []
810
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
811
+
812
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
813
+ const pr = parent.row
814
+ const pc = parent.col
815
+
816
+ let cellLeft = tableX
817
+ for (let idx = 0; idx < pc; idx++) {
818
+ cellLeft += colWidths[idx] || 0
819
+ }
820
+
821
+ let cellTop = tableY
822
+ for (let idx = 0; idx < pr; idx++) {
823
+ cellTop += rowHeights[idx] || 0
824
+ }
825
+
826
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
827
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
828
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
829
+
830
+ let cellWidth = 0
831
+ for (let idx = 0; idx < gridSpan; idx++) {
832
+ cellWidth += colWidths[pc + idx] || 0
833
+ }
834
+
835
+ let cellHeight = 0
836
+ for (let idx = 0; idx < rowSpan; idx++) {
837
+ cellHeight += rowHeights[pr + idx] || 0
838
+ }
839
+
840
+ return {
841
+ x: Math.round(cellLeft / 9525),
842
+ y: Math.round(cellTop / 9525),
843
+ width: Math.round(cellWidth / 9525),
844
+ height: Math.round(cellHeight / 9525),
845
+ }
846
+ }
847
+
848
+ getCellPosition(slideIndex, tableId, rowIndex, colIndex, slideManager) {
849
+ const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
850
+ return {
851
+ row: rowIndex,
852
+ column: colIndex,
853
+ x: bounds.x,
854
+ y: bounds.y,
855
+ }
856
+ }
857
+
689
858
  /**
690
859
  * Splits a merged region containing cell (row, col).
691
860
  */
@@ -1051,11 +1220,838 @@ class TableManager {
1051
1220
 
1052
1221
  #getTableContext(slideIndex, tableId, slideManager) {
1053
1222
  const slideObj = slideManager.getSlideObj(slideIndex)
1054
- const tblObj = this.#findTableObj(slideObj, tableId, slideManager, slideIndex)
1055
- if (!tblObj) {
1223
+ const res = slideManager.getSlideTable(slideIndex, tableId)
1224
+ if (!res || !res.table) {
1056
1225
  throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
1057
1226
  }
1058
- return { slideObj, tblObj }
1227
+ const cNvPr = res.frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
1228
+ const resolvedTableId = cNvPr ? cNvPr['@_name'] || String(cNvPr['@_id']) : tableId
1229
+ return { slideObj, tblObj: res.table, frameObj: res.frame, resolvedTableId }
1230
+ }
1231
+
1232
+ #applyCellOptions(cellObj, cellOptions) {
1233
+ if (cellOptions.fill) {
1234
+ if (!cellObj['a:tcPr']) cellObj['a:tcPr'] = {}
1235
+ cellObj['a:tcPr']['a:solidFill'] = {
1236
+ 'a:srgbClr': { '@_val': cellOptions.fill },
1237
+ }
1238
+ }
1239
+
1240
+ if (cellOptions.align) {
1241
+ const txBody = cellObj['a:txBody']
1242
+ if (txBody && txBody['a:p']) {
1243
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
1244
+ for (const p of paras) {
1245
+ if (!p['a:pPr']) p['a:pPr'] = {}
1246
+ p['a:pPr']['@_algn'] = cellOptions.align
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ if (cellOptions.fontSize) {
1252
+ const sizeVal = cellOptions.fontSize * 100
1253
+ const txBody = cellObj['a:txBody']
1254
+ if (txBody && txBody['a:p']) {
1255
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
1256
+ for (const p of paras) {
1257
+ if (p['a:r']) {
1258
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
1259
+ for (const r of runs) {
1260
+ if (!r['a:rPr']) r['a:rPr'] = {}
1261
+ r['a:rPr']['@_sz'] = String(sizeVal)
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ #expandCellShape(config, cellBounds) {
1270
+ const cellLeft_px = Math.round(cellBounds.left / 9525)
1271
+ const cellTop_px = Math.round(cellBounds.top / 9525)
1272
+ const cellWidth_px = Math.round(cellBounds.width / 9525)
1273
+ const cellHeight_px = Math.round(cellBounds.height / 9525)
1274
+
1275
+ const parseLength = (val, maxVal) => {
1276
+ if (typeof val === 'string' && val.endsWith('%')) {
1277
+ return (parseFloat(val) / 100) * maxVal
1278
+ }
1279
+ return val !== undefined ? parseFloat(val) : undefined
1280
+ }
1281
+
1282
+ const isCellAnchored = config.anchor !== 'slide'
1283
+
1284
+ // 1. Determine bounding box width and height
1285
+ let shapeWidth
1286
+ let shapeHeight
1287
+
1288
+ if (config.type === 'progressBar') {
1289
+ shapeHeight = parseLength(config.height !== undefined ? config.height : 8, cellHeight_px)
1290
+ shapeWidth = parseLength(
1291
+ config.width !== undefined ? config.width : cellWidth_px - 10,
1292
+ cellWidth_px
1293
+ )
1294
+ } else if (config.type === 'badge') {
1295
+ const text = String(config.text !== undefined ? config.text : '')
1296
+ const fontSize = config.textStyle?.fontSize || 10
1297
+ const textWidth = text.length * fontSize * 0.6
1298
+ const paddingX = 12
1299
+ shapeWidth =
1300
+ parseLength(config.width, cellWidth_px) !== undefined
1301
+ ? parseLength(config.width, cellWidth_px)
1302
+ : textWidth + paddingX * 2
1303
+ shapeHeight =
1304
+ parseLength(config.height, cellHeight_px) !== undefined
1305
+ ? parseLength(config.height, cellHeight_px)
1306
+ : fontSize + 12
1307
+ } else if (config.type === 'icon') {
1308
+ const size = parseLength(
1309
+ config.size !== undefined ? config.size : 16,
1310
+ Math.min(cellWidth_px, cellHeight_px)
1311
+ )
1312
+ shapeWidth = size
1313
+ shapeHeight = size
1314
+ } else {
1315
+ shapeWidth = parseLength(config.width, cellWidth_px)
1316
+ if (shapeWidth === undefined) {
1317
+ const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
1318
+ if (sizeVal !== undefined) {
1319
+ shapeWidth = sizeVal
1320
+ } else {
1321
+ const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
1322
+ if (radiusVal !== undefined) {
1323
+ shapeWidth = radiusVal * 2
1324
+ } else {
1325
+ shapeWidth = 12 // default
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ shapeHeight = parseLength(config.height, cellHeight_px)
1331
+ if (shapeHeight === undefined) {
1332
+ const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
1333
+ if (sizeVal !== undefined) {
1334
+ shapeHeight = sizeVal
1335
+ } else {
1336
+ const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
1337
+ if (radiusVal !== undefined) {
1338
+ shapeHeight = radiusVal * 2
1339
+ } else {
1340
+ shapeHeight = 12 // default
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ // 2. Determine alignment settings
1347
+ let alignX = config.alignX
1348
+ let alignY = config.alignY
1349
+
1350
+ if (config.position) {
1351
+ switch (config.position) {
1352
+ case 'top-left':
1353
+ if (!alignX) alignX = 'left'
1354
+ if (!alignY) alignY = 'top'
1355
+ break
1356
+ case 'top-center':
1357
+ case 'top':
1358
+ if (!alignX) alignX = 'center'
1359
+ if (!alignY) alignY = 'top'
1360
+ break
1361
+ case 'top-right':
1362
+ if (!alignX) alignX = 'right'
1363
+ if (!alignY) alignY = 'top'
1364
+ break
1365
+ case 'middle-left':
1366
+ case 'left':
1367
+ if (!alignX) alignX = 'left'
1368
+ if (!alignY) alignY = 'middle'
1369
+ break
1370
+ case 'center':
1371
+ case 'middle-center':
1372
+ if (!alignX) alignX = 'center'
1373
+ if (!alignY) alignY = 'middle'
1374
+ break
1375
+ case 'middle-right':
1376
+ case 'right':
1377
+ if (!alignX) alignX = 'right'
1378
+ if (!alignY) alignY = 'middle'
1379
+ break
1380
+ case 'bottom-left':
1381
+ if (!alignX) alignX = 'left'
1382
+ if (!alignY) alignY = 'bottom'
1383
+ break
1384
+ case 'bottom-center':
1385
+ case 'bottom':
1386
+ if (!alignX) alignX = 'center'
1387
+ if (!alignY) alignY = 'bottom'
1388
+ break
1389
+ case 'bottom-right':
1390
+ if (!alignX) alignX = 'right'
1391
+ if (!alignY) alignY = 'bottom'
1392
+ break
1393
+ }
1394
+ }
1395
+
1396
+ if (alignX && !alignY && config.y === undefined) {
1397
+ alignY = 'middle'
1398
+ }
1399
+ if (alignY && !alignX && config.x === undefined) {
1400
+ alignX = 'center'
1401
+ }
1402
+
1403
+ if (!alignX && !alignY && config.x === undefined && config.y === undefined) {
1404
+ alignX = 'center'
1405
+ alignY = 'middle'
1406
+ }
1407
+
1408
+ // 3. Compute coordinates
1409
+ let shapeLeft = cellLeft_px
1410
+ let shapeTop = cellTop_px
1411
+
1412
+ if (isCellAnchored) {
1413
+ if (alignX === 'left') {
1414
+ shapeLeft = cellLeft_px + (config.x !== undefined ? config.x : 5)
1415
+ } else if (alignX === 'center') {
1416
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + (config.x || 0)
1417
+ } else if (alignX === 'right') {
1418
+ shapeLeft =
1419
+ cellLeft_px + cellWidth_px - shapeWidth - (config.x !== undefined ? config.x : 5)
1420
+ } else {
1421
+ shapeLeft =
1422
+ cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - shapeWidth) / 2)
1423
+ }
1424
+
1425
+ if (alignY === 'top') {
1426
+ shapeTop = cellTop_px + (config.y !== undefined ? config.y : 5)
1427
+ } else if (alignY === 'middle') {
1428
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + (config.y || 0)
1429
+ } else if (alignY === 'bottom') {
1430
+ shapeTop =
1431
+ cellTop_px + cellHeight_px - shapeHeight - (config.y !== undefined ? config.y : 5)
1432
+ } else {
1433
+ shapeTop =
1434
+ cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - shapeHeight) / 2)
1435
+ }
1436
+
1437
+ // 4. Boundary Constraints Validation/Enforcement
1438
+ if (shapeWidth > cellWidth_px) {
1439
+ shapeLeft = cellLeft_px
1440
+ } else {
1441
+ shapeLeft = Math.max(
1442
+ cellLeft_px,
1443
+ Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1444
+ )
1445
+ }
1446
+
1447
+ if (shapeHeight > cellHeight_px) {
1448
+ shapeTop = cellTop_px
1449
+ } else {
1450
+ shapeTop = Math.max(
1451
+ cellTop_px,
1452
+ Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight)
1453
+ )
1454
+ }
1455
+ } else {
1456
+ shapeLeft = config.x || 0
1457
+ shapeTop = config.y || 0
1458
+ }
1459
+
1460
+ // 5. Expand individual sub-elements / custom shapes
1461
+ if (config.type === 'progressBar') {
1462
+ const value = config.value !== undefined ? config.value : 0
1463
+ const max = config.max !== undefined ? config.max : 100
1464
+ const fill = config.fill || '#3B82F6'
1465
+ const bgFill = config.backgroundFill || '#E5E7EB'
1466
+
1467
+ const shapes = []
1468
+ shapes.push({
1469
+ type: 'roundedRectangle',
1470
+ fill: bgFill,
1471
+ x: shapeLeft,
1472
+ y: shapeTop,
1473
+ width: shapeWidth,
1474
+ height: shapeHeight,
1475
+ borderRadius: shapeHeight / 2,
1476
+ zIndex: config.zIndex,
1477
+ })
1478
+
1479
+ const pct = Math.min(1, Math.max(0, value / max))
1480
+ if (pct > 0) {
1481
+ const filledWidth = shapeWidth * pct
1482
+ shapes.push({
1483
+ type: 'roundedRectangle',
1484
+ fill: fill,
1485
+ x: shapeLeft,
1486
+ y: shapeTop,
1487
+ width: filledWidth,
1488
+ height: shapeHeight,
1489
+ borderRadius: shapeHeight / 2,
1490
+ zIndex: (config.zIndex || 0) + 1,
1491
+ })
1492
+ }
1493
+ return shapes
1494
+ }
1495
+
1496
+ if (config.type === 'badge') {
1497
+ const text = String(config.text !== undefined ? config.text : '')
1498
+ const fontSize = config.textStyle?.fontSize || 10
1499
+ return [
1500
+ {
1501
+ type: 'roundedRectangle',
1502
+ fill: config.fill || '#10B981',
1503
+ borderRadius: shapeHeight / 2,
1504
+ x: shapeLeft,
1505
+ y: shapeTop,
1506
+ width: shapeWidth,
1507
+ height: shapeHeight,
1508
+ text: text,
1509
+ textStyle: {
1510
+ color: config.textStyle?.color || '#FFFFFF',
1511
+ fontSize: fontSize,
1512
+ bold: config.textStyle?.bold !== undefined ? config.textStyle.bold : true,
1513
+ align: 'center',
1514
+ },
1515
+ border: config.border,
1516
+ transparency: config.transparency,
1517
+ shadow: config.shadow,
1518
+ rotation: config.rotation,
1519
+ zIndex: config.zIndex,
1520
+ },
1521
+ ]
1522
+ }
1523
+
1524
+ if (config.type === 'icon') {
1525
+ const iconFill = config.fill
1526
+ const fontSize = Math.round(shapeWidth * 0.8)
1527
+
1528
+ let baseConfig = null
1529
+ switch (config.icon) {
1530
+ case 'check':
1531
+ baseConfig = {
1532
+ type: 'rectangle',
1533
+ fill: 'none',
1534
+ border: null,
1535
+ width: shapeWidth,
1536
+ height: shapeHeight,
1537
+ text: '✔',
1538
+ textStyle: {
1539
+ color: iconFill || '#10B981',
1540
+ bold: true,
1541
+ fontSize: fontSize,
1542
+ align: 'center',
1543
+ },
1544
+ }
1545
+ break
1546
+ case 'cross':
1547
+ baseConfig = {
1548
+ type: 'rectangle',
1549
+ fill: 'none',
1550
+ border: null,
1551
+ width: shapeWidth,
1552
+ height: shapeHeight,
1553
+ text: '✘',
1554
+ textStyle: {
1555
+ color: iconFill || '#EF4444',
1556
+ bold: true,
1557
+ fontSize: fontSize,
1558
+ align: 'center',
1559
+ },
1560
+ }
1561
+ break
1562
+ case 'warning':
1563
+ baseConfig = {
1564
+ type: 'triangle',
1565
+ fill: iconFill || '#F59E0B',
1566
+ border: null,
1567
+ width: shapeWidth,
1568
+ height: shapeHeight,
1569
+ text: '!',
1570
+ textStyle: {
1571
+ color: '#FFFFFF',
1572
+ bold: true,
1573
+ fontSize: Math.round(fontSize * 0.7),
1574
+ align: 'center',
1575
+ },
1576
+ }
1577
+ break
1578
+ case 'info':
1579
+ baseConfig = {
1580
+ type: 'circle',
1581
+ fill: iconFill || '#3B82F6',
1582
+ border: null,
1583
+ radius: shapeWidth / 2,
1584
+ text: 'i',
1585
+ textStyle: {
1586
+ color: '#FFFFFF',
1587
+ bold: true,
1588
+ fontSize: Math.round(fontSize * 0.7),
1589
+ align: 'center',
1590
+ },
1591
+ }
1592
+ break
1593
+ case 'star':
1594
+ baseConfig = {
1595
+ type: 'star5',
1596
+ fill: iconFill || '#FBBF24',
1597
+ border: null,
1598
+ width: shapeWidth,
1599
+ height: shapeHeight,
1600
+ }
1601
+ break
1602
+ case 'up':
1603
+ baseConfig = {
1604
+ type: 'upArrow',
1605
+ fill: iconFill || '#10B981',
1606
+ border: null,
1607
+ width: shapeWidth,
1608
+ height: shapeHeight,
1609
+ }
1610
+ break
1611
+ case 'down':
1612
+ baseConfig = {
1613
+ type: 'downArrow',
1614
+ fill: iconFill || '#EF4444',
1615
+ border: null,
1616
+ width: shapeWidth,
1617
+ height: shapeHeight,
1618
+ }
1619
+ break
1620
+ case 'arrow-right':
1621
+ baseConfig = {
1622
+ type: 'rightArrow',
1623
+ fill: iconFill || '#3B82F6',
1624
+ border: null,
1625
+ width: shapeWidth,
1626
+ height: shapeHeight,
1627
+ }
1628
+ break
1629
+ case 'arrow-left':
1630
+ baseConfig = {
1631
+ type: 'leftArrow',
1632
+ fill: iconFill || '#3B82F6',
1633
+ border: null,
1634
+ width: shapeWidth,
1635
+ height: shapeHeight,
1636
+ }
1637
+ break
1638
+ default:
1639
+ return []
1640
+ }
1641
+
1642
+ baseConfig.x = shapeLeft
1643
+ baseConfig.y = shapeTop
1644
+ baseConfig.zIndex = config.zIndex
1645
+ if (config.border) baseConfig.border = config.border
1646
+ if (config.transparency !== undefined) baseConfig.transparency = config.transparency
1647
+ if (config.shadow !== undefined) baseConfig.shadow = config.shadow
1648
+ if (config.rotation !== undefined) baseConfig.rotation = config.rotation
1649
+
1650
+ return [baseConfig]
1651
+ }
1652
+
1653
+ const expanded = Object.assign({}, config, {
1654
+ x: shapeLeft,
1655
+ y: shapeTop,
1656
+ width: shapeWidth,
1657
+ height: shapeHeight,
1658
+ })
1659
+
1660
+ if (expanded.type === 'circle' && expanded.radius === undefined) {
1661
+ expanded.radius = shapeWidth / 2
1662
+ }
1663
+ if (expanded.type === 'square' && expanded.size === undefined) {
1664
+ expanded.size = shapeWidth
1665
+ }
1666
+
1667
+ return [expanded]
1668
+ }
1669
+
1670
+ #processCellShapes(
1671
+ slideIndex,
1672
+ tableId,
1673
+ resolvedTableId,
1674
+ rowsData,
1675
+ isObjectRows,
1676
+ cellShapes,
1677
+ slideManager,
1678
+ shapeManager,
1679
+ tblObj,
1680
+ frameObj
1681
+ ) {
1682
+ if (!cellShapes || !shapeManager) return
1683
+
1684
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1685
+ const prefixToDelete = `cellshape_${resolvedTableId}_`
1686
+ const existingNames = shapes
1687
+ .map(s => s.name)
1688
+ .filter(name => name && name.startsWith(prefixToDelete))
1689
+
1690
+ for (const name of existingNames) {
1691
+ try {
1692
+ shapeManager.deleteShape(slideIndex, name, slideManager)
1693
+ } catch (err) {
1694
+ logger.warn(`Failed to delete existing cell shape "${name}": ${err.message}`)
1695
+ }
1696
+ }
1697
+
1698
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1699
+
1700
+ const xfrm = frameObj['p:xfrm']
1701
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1702
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1703
+
1704
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1705
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1706
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1707
+
1708
+ const trsArr = tblObj['a:tr'] || []
1709
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1710
+
1711
+ const getCellBounds = (r, c) => {
1712
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
1713
+ const pr = parent.row
1714
+ const pc = parent.col
1715
+
1716
+ let cellLeft = tableX
1717
+ for (let idx = 0; idx < pc; idx++) {
1718
+ cellLeft += colWidths[idx] || 0
1719
+ }
1720
+
1721
+ let cellTop = tableY
1722
+ for (let idx = 0; idx < pr; idx++) {
1723
+ cellTop += rowHeights[idx] || 0
1724
+ }
1725
+
1726
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1727
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1728
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1729
+
1730
+ let cellWidth = 0
1731
+ for (let idx = 0; idx < gridSpan; idx++) {
1732
+ cellWidth += colWidths[pc + idx] || 0
1733
+ }
1734
+
1735
+ let cellHeight = 0
1736
+ for (let idx = 0; idx < rowSpan; idx++) {
1737
+ cellHeight += rowHeights[pr + idx] || 0
1738
+ }
1739
+
1740
+ return {
1741
+ left: cellLeft,
1742
+ top: cellTop,
1743
+ width: cellWidth,
1744
+ height: cellHeight,
1745
+ }
1746
+ }
1747
+
1748
+ const shapesToCreate = []
1749
+ const headerNames = (trsArr[0]?.['a:tc'] || []).map(cell => this.#getCellText(cell).trim())
1750
+
1751
+ for (let i = 0; i < rowsData.length; i++) {
1752
+ const rowData = rowsData[i]
1753
+ const finalRowIndex = isObjectRows ? i + 1 : i
1754
+
1755
+ const numCols = trsArr[finalRowIndex]?.['a:tc']?.length || 0
1756
+ for (let j = 0; j < numCols; j++) {
1757
+ const headerName = headerNames[j]
1758
+ let shapeFn = null
1759
+
1760
+ if (headerName) {
1761
+ shapeFn = cellShapes[headerName] || cellShapes[headerName.toLowerCase()]
1762
+ }
1763
+ if (!shapeFn) {
1764
+ shapeFn = cellShapes[j]
1765
+ }
1766
+
1767
+ if (typeof shapeFn !== 'function') continue
1768
+
1769
+ let configs = shapeFn(rowData, i)
1770
+ if (!configs) continue
1771
+
1772
+ if (!Array.isArray(configs)) {
1773
+ configs = [configs]
1774
+ }
1775
+
1776
+ configs.forEach((config, shapeIdx) => {
1777
+ shapesToCreate.push({
1778
+ config,
1779
+ rowIndex: finalRowIndex,
1780
+ colIndex: j,
1781
+ shapeIndex: shapeIdx,
1782
+ })
1783
+ })
1784
+ }
1785
+ }
1786
+
1787
+ shapesToCreate.sort((a, b) => (a.config.zIndex || 0) - (b.config.zIndex || 0))
1788
+
1789
+ shapesToCreate.forEach(item => {
1790
+ const bounds = getCellBounds(item.rowIndex, item.colIndex)
1791
+ const expandedConfigs = this.#expandCellShape(item.config, bounds)
1792
+
1793
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
1794
+ const finalShapeIndex =
1795
+ expandedConfigs.length > 1 ? `${item.shapeIndex}_${expIdx}` : item.shapeIndex
1796
+ expandedConfig.id = `cellshape_${resolvedTableId}_${item.rowIndex}_${item.colIndex}_${finalShapeIndex}`
1797
+
1798
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1799
+ })
1800
+ })
1801
+ }
1802
+
1803
+ getTableRows(slideIndex, tableId, options = {}, slideManager) {
1804
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
1805
+ const trs = tblObj['a:tr'] || []
1806
+ if (trs.length === 0) {
1807
+ return options.includeMetadata
1808
+ ? { rows: [], rowCount: 0, columnCount: 0, mergedCells: [] }
1809
+ : []
1810
+ }
1811
+
1812
+ const numRows = trs.length
1813
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1814
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1815
+ const numCols = gridColsArr.length
1816
+
1817
+ // Extract all raw cell text, resolving merges to their parent's text
1818
+ const matrix = []
1819
+ for (let r = 0; r < numRows; r++) {
1820
+ const rowCells = []
1821
+ for (let c = 0; c < numCols; c++) {
1822
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
1823
+ const cell = trs[parent.row]?.['a:tc']?.[parent.col]
1824
+ const text = cell ? this.#getCellText(cell) : ''
1825
+ rowCells.push(text)
1826
+ }
1827
+ matrix.push(rowCells)
1828
+ }
1829
+
1830
+ // Header names are extracted from the first row (index 0)
1831
+ const headerNames = matrix[0].map((hText, cIdx) => {
1832
+ const cleaned = hText.trim()
1833
+ return cleaned || `column${cIdx + 1}`
1834
+ })
1835
+
1836
+ // Compute the data rows (excluding the header row at index 0)
1837
+ const dataRows = matrix.slice(1)
1838
+
1839
+ let rowsResult = []
1840
+ if (options.raw) {
1841
+ rowsResult = dataRows
1842
+ } else {
1843
+ for (const rowCells of dataRows) {
1844
+ const rowObj = {}
1845
+ for (let c = 0; c < numCols; c++) {
1846
+ const key = headerNames[c]
1847
+ rowObj[key] = rowCells[c] || ''
1848
+ }
1849
+ rowsResult.push(rowObj)
1850
+ }
1851
+ }
1852
+
1853
+ if (options.includeMetadata) {
1854
+ const mergedCells = this.getMergedCells(slideIndex, tableId, slideManager)
1855
+ return {
1856
+ rows: rowsResult,
1857
+ rowCount: numRows,
1858
+ columnCount: numCols,
1859
+ mergedCells,
1860
+ }
1861
+ }
1862
+
1863
+ return rowsResult
1864
+ }
1865
+
1866
+ addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
1867
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1868
+ slideIndex,
1869
+ tableId,
1870
+ slideManager
1871
+ )
1872
+
1873
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1874
+
1875
+ const xfrm = frameObj['p:xfrm']
1876
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1877
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1878
+
1879
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1880
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1881
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1882
+
1883
+ const trsArr = tblObj['a:tr'] || []
1884
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1885
+
1886
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
1887
+ const pr = parent.row
1888
+ const pc = parent.col
1889
+
1890
+ let cellLeft = tableX
1891
+ for (let idx = 0; idx < pc; idx++) {
1892
+ cellLeft += colWidths[idx] || 0
1893
+ }
1894
+
1895
+ let cellTop = tableY
1896
+ for (let idx = 0; idx < pr; idx++) {
1897
+ cellTop += rowHeights[idx] || 0
1898
+ }
1899
+
1900
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1901
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1902
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1903
+
1904
+ let cellWidth = 0
1905
+ for (let idx = 0; idx < gridSpan; idx++) {
1906
+ cellWidth += colWidths[pc + idx] || 0
1907
+ }
1908
+
1909
+ let cellHeight = 0
1910
+ for (let idx = 0; idx < rowSpan; idx++) {
1911
+ cellHeight += rowHeights[pr + idx] || 0
1912
+ }
1913
+
1914
+ const bounds = { left: cellLeft, top: cellTop, width: cellWidth, height: cellHeight }
1915
+
1916
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1917
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_`
1918
+ let maxShapeIndex = -1
1919
+ for (const s of shapes) {
1920
+ if (s.name && s.name.startsWith(prefix)) {
1921
+ const remaining = s.name.slice(prefix.length)
1922
+ const parts = remaining.split('_')
1923
+ const idxVal = parseInt(parts[0], 10)
1924
+ if (!isNaN(idxVal) && idxVal > maxShapeIndex) {
1925
+ maxShapeIndex = idxVal
1926
+ }
1927
+ }
1928
+ }
1929
+ const nextShapeIndex = maxShapeIndex + 1
1930
+
1931
+ const expandedConfigs = this.#expandCellShape(options, bounds)
1932
+
1933
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
1934
+ const finalShapeIndex =
1935
+ expandedConfigs.length > 1 ? `${nextShapeIndex}_${expIdx}` : nextShapeIndex
1936
+ expandedConfig.id = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${finalShapeIndex}`
1937
+
1938
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1939
+ })
1940
+ }
1941
+
1942
+ updateCellShape(
1943
+ slideIndex,
1944
+ tableId,
1945
+ rowIndex,
1946
+ colIndex,
1947
+ shapeIndex,
1948
+ options,
1949
+ slideManager,
1950
+ shapeManager
1951
+ ) {
1952
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1953
+ slideIndex,
1954
+ tableId,
1955
+ slideManager
1956
+ )
1957
+
1958
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1959
+
1960
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1961
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
1962
+ const matchingShapes = shapes.filter(
1963
+ s => s.name && (s.name === prefix || s.name.startsWith(prefix + '_'))
1964
+ )
1965
+
1966
+ if (matchingShapes.length === 0) {
1967
+ throw new PPTXError(`Cell shape "${shapeIndex}" not found in cell (${rowIndex}, ${colIndex})`)
1968
+ }
1969
+
1970
+ for (const s of matchingShapes) {
1971
+ shapeManager.deleteShape(slideIndex, s.name, slideManager)
1972
+ }
1973
+
1974
+ const xfrm = frameObj['p:xfrm']
1975
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1976
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1977
+
1978
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1979
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1980
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1981
+
1982
+ const trsArr = tblObj['a:tr'] || []
1983
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1984
+
1985
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
1986
+ const pr = parent.row
1987
+ const pc = parent.col
1988
+
1989
+ let cellLeft = tableX
1990
+ for (let idx = 0; idx < pc; idx++) {
1991
+ cellLeft += colWidths[idx] || 0
1992
+ }
1993
+
1994
+ let cellTop = tableY
1995
+ for (let idx = 0; idx < pr; idx++) {
1996
+ cellTop += rowHeights[idx] || 0
1997
+ }
1998
+
1999
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
2000
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
2001
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
2002
+
2003
+ let cellWidth = 0
2004
+ for (let idx = 0; idx < gridSpan; idx++) {
2005
+ cellWidth += colWidths[pc + idx] || 0
2006
+ }
2007
+
2008
+ let cellHeight = 0
2009
+ for (let idx = 0; idx < rowSpan; idx++) {
2010
+ cellHeight += rowHeights[pr + idx] || 0
2011
+ }
2012
+
2013
+ const bounds = { left: cellLeft, top: cellTop, width: cellWidth, height: cellHeight }
2014
+
2015
+ const expandedConfigs = this.#expandCellShape(options, bounds)
2016
+
2017
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
2018
+ const finalShapeIndex = expandedConfigs.length > 1 ? `${shapeIndex}_${expIdx}` : shapeIndex
2019
+ expandedConfig.id = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${finalShapeIndex}`
2020
+
2021
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
2022
+ })
2023
+ }
2024
+
2025
+ removeCellShape(slideIndex, tableId, rowIndex, colIndex, shapeIndex, slideManager, shapeManager) {
2026
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2027
+
2028
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
2029
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
2030
+ const matchingShapes = shapes.filter(
2031
+ s => s.name && (s.name === prefix || s.name.startsWith(prefix + '_'))
2032
+ )
2033
+
2034
+ if (matchingShapes.length === 0) {
2035
+ throw new PPTXError(`Cell shape "${shapeIndex}" not found in cell (${rowIndex}, ${colIndex})`)
2036
+ }
2037
+
2038
+ for (const s of matchingShapes) {
2039
+ shapeManager.deleteShape(slideIndex, s.name, slideManager)
2040
+ }
2041
+ }
2042
+
2043
+ getCellShape(slideIndex, tableId, rowIndex, colIndex, shapeIndex, slideManager, shapeManager) {
2044
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2045
+
2046
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
2047
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
2048
+ const primaryShape = shapes.find(
2049
+ s => s.name === prefix || s.name === `${prefix}_0` || s.name === `${prefix}_1`
2050
+ )
2051
+
2052
+ if (!primaryShape) return null
2053
+
2054
+ return shapeManager.getShape(slideIndex, primaryShape.name, slideManager)
1059
2055
  }
1060
2056
 
1061
2057
  /**
@@ -1096,6 +2092,297 @@ class TableManager {
1096
2092
  }
1097
2093
  }
1098
2094
 
2095
+ #calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
2096
+ const trsArr = tblObj['a:tr'] || []
2097
+ if (trsArr.length === 0) return []
2098
+
2099
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2100
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2101
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2102
+
2103
+ const numRows = trsArr.length
2104
+ const numCols = colWidths.length
2105
+
2106
+ // Initialize rowHeights with original height or 0
2107
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2108
+
2109
+ // Helper to get paragraph font size
2110
+ const getParagraphFontSize = p => {
2111
+ let maxSz = 14 // default 14pt
2112
+ if (p['a:pPr']?.['a:defRPr']?.['@_sz']) {
2113
+ maxSz = parseInt(p['a:pPr']['a:defRPr']['@_sz'], 10) / 100
2114
+ }
2115
+ if (p['a:r']) {
2116
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
2117
+ for (const r of runs) {
2118
+ if (r['a:rPr']?.['@_sz']) {
2119
+ const szVal = parseInt(r['a:rPr']['@_sz'], 10) / 100
2120
+ if (szVal > maxSz) {
2121
+ maxSz = szVal
2122
+ }
2123
+ }
2124
+ }
2125
+ }
2126
+ return maxSz
2127
+ }
2128
+
2129
+ // Helper to wrap text
2130
+ const wrapText = (text, availWidth_px, fontSize) => {
2131
+ const charWidth = fontSize * 0.65
2132
+ const words = text.split(/(\s+)/)
2133
+ let linesCount = 0
2134
+ let currentLineLen = 0
2135
+
2136
+ for (const word of words) {
2137
+ if (!word) continue
2138
+ const wordWidth = word.length * charWidth
2139
+ if (wordWidth > availWidth_px) {
2140
+ if (currentLineLen > 0) {
2141
+ linesCount++
2142
+ currentLineLen = 0
2143
+ }
2144
+ let remainingWidth = wordWidth
2145
+ while (remainingWidth > 0) {
2146
+ linesCount++
2147
+ remainingWidth -= availWidth_px
2148
+ }
2149
+ } else {
2150
+ if (currentLineLen + wordWidth > availWidth_px) {
2151
+ linesCount++
2152
+ currentLineLen = word.trim() ? wordWidth : 0
2153
+ } else {
2154
+ currentLineLen += wordWidth
2155
+ }
2156
+ }
2157
+ }
2158
+ if (currentLineLen > 0 || linesCount === 0) {
2159
+ linesCount++
2160
+ }
2161
+ return linesCount
2162
+ }
2163
+
2164
+ // Helper to get cell margins
2165
+ const getCellMargins = cell => {
2166
+ const tcPr = cell['a:tcPr']
2167
+ const marL = tcPr?.['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
2168
+ const marR = tcPr?.['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
2169
+ const marT = tcPr?.['@_marT'] !== undefined ? parseInt(tcPr['@_marT'], 10) : 45720
2170
+ const marB = tcPr?.['@_marB'] !== undefined ? parseInt(tcPr['@_marB'], 10) : 45720
2171
+ return { marL, marR, marT, marB }
2172
+ }
2173
+
2174
+ // Calculate required height for each cell
2175
+ const cellHeights = Array.from({ length: numRows }, () => new Array(numCols).fill(0))
2176
+
2177
+ for (let r = 0; r < numRows; r++) {
2178
+ const row = trsArr[r]
2179
+ const tcs = row['a:tc'] || []
2180
+ for (let c = 0; c < numCols; c++) {
2181
+ const cell = tcs[c]
2182
+ if (!cell || cell['@_hMerge'] || cell['@_vMerge']) continue
2183
+
2184
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
2185
+ const gridSpan = cell['@_gridSpan'] ? parseInt(cell['@_gridSpan'], 10) : 1
2186
+
2187
+ // Calculate cell width
2188
+ let cellWidth = 0
2189
+ for (let idx = 0; idx < gridSpan; idx++) {
2190
+ cellWidth += colWidths[parent.col + idx] || 0
2191
+ }
2192
+
2193
+ const { marL, marR, marT, marB } = getCellMargins(cell)
2194
+ const availWidth = cellWidth - marL - marR
2195
+ const availWidth_px = Math.max(1, availWidth / 9525)
2196
+
2197
+ // Calculate text height
2198
+ const txBody = cell['a:txBody']
2199
+ let textHeight_emu = 0
2200
+ if (txBody) {
2201
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
2202
+ for (const p of paras) {
2203
+ const fontSize = getParagraphFontSize(p)
2204
+ let pText = ''
2205
+ if (p['a:r']) {
2206
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
2207
+ for (const r of runs) {
2208
+ if (r['a:t']) {
2209
+ pText += String(r['a:t'])
2210
+ }
2211
+ }
2212
+ }
2213
+
2214
+ const linesCount = wrapText(pText, availWidth_px, fontSize)
2215
+ const lineHeight_emu = fontSize * 20780 // 1.4 line height multiplier
2216
+
2217
+ let pHeight_emu = linesCount * lineHeight_emu
2218
+ if (p['a:pPr']?.['a:spcBef']?.['a:spcPts']?.['@_val']) {
2219
+ pHeight_emu += parseInt(p['a:pPr']['a:spcBef']['a:spcPts']['@_val'], 10) * 127
2220
+ }
2221
+ if (p['a:pPr']?.['a:spcAft']?.['a:spcPts']?.['@_val']) {
2222
+ pHeight_emu += parseInt(p['a:pPr']['a:spcAft']['a:spcPts']['@_val'], 10) * 127
2223
+ }
2224
+ textHeight_emu += pHeight_emu
2225
+ }
2226
+ }
2227
+
2228
+ const totalCellHeight_emu = marT + marB + textHeight_emu
2229
+ cellHeights[r][c] = totalCellHeight_emu
2230
+ }
2231
+ }
2232
+
2233
+ // Now resolve row heights based on required cell heights
2234
+ // First, non-vertically-merged cells define row heights directly
2235
+ for (let r = 0; r < numRows; r++) {
2236
+ let maxCellHeight = rowHeights[r] // Start with original template height as floor
2237
+ const row = trsArr[r]
2238
+ const tcs = row['a:tc'] || []
2239
+ for (let c = 0; c < numCols; c++) {
2240
+ const cell = tcs[c]
2241
+ if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
2242
+ const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
2243
+ if (rowSpan === 1) {
2244
+ if (cellHeights[r][c] > maxCellHeight) {
2245
+ maxCellHeight = cellHeights[r][c]
2246
+ }
2247
+ }
2248
+ }
2249
+ rowHeights[r] = maxCellHeight
2250
+ }
2251
+
2252
+ // Next, adjust for vertically merged cells (rowSpan > 1)
2253
+ for (let r = 0; r < numRows; r++) {
2254
+ const row = trsArr[r]
2255
+ const tcs = row['a:tc'] || []
2256
+ for (let c = 0; c < numCols; c++) {
2257
+ const cell = tcs[c]
2258
+ if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
2259
+ const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
2260
+ if (rowSpan > 1) {
2261
+ const reqHeight = cellHeights[r][c]
2262
+ // Sum currently allocated row heights for spanned rows
2263
+ let currentSpanHeight = 0
2264
+ for (let idx = 0; idx < rowSpan; idx++) {
2265
+ currentSpanHeight += rowHeights[r + idx] || 0
2266
+ }
2267
+ if (reqHeight > currentSpanHeight) {
2268
+ // Distribute the extra required height equally across all spanned rows
2269
+ const diff = reqHeight - currentSpanHeight
2270
+ const extraPerRow = Math.ceil(diff / rowSpan)
2271
+ for (let idx = 0; idx < rowSpan; idx++) {
2272
+ rowHeights[r + idx] += extraPerRow
2273
+ }
2274
+ }
2275
+ }
2276
+ }
2277
+ }
2278
+
2279
+ // Update row heights in XML
2280
+ for (let r = 0; r < numRows; r++) {
2281
+ trsArr[r]['@_h'] = String(rowHeights[r])
2282
+ }
2283
+
2284
+ return rowHeights
2285
+ }
2286
+
2287
+ #getNestedHeight(val) {
2288
+ if (Array.isArray(val)) {
2289
+ if (val.length === 0) return 1
2290
+ return val.reduce((sum, item) => sum + this.#getNestedHeight(item), 0)
2291
+ }
2292
+ return 1
2293
+ }
2294
+
2295
+ #expandCellVal(val, targetHeight) {
2296
+ if (!Array.isArray(val)) {
2297
+ const res = []
2298
+ res.push({ value: val !== undefined ? val : '', rowSpan: targetHeight })
2299
+ for (let i = 1; i < targetHeight; i++) {
2300
+ res.push({ vMerge: true })
2301
+ }
2302
+ return res
2303
+ }
2304
+
2305
+ if (val.length === 0) {
2306
+ const res = []
2307
+ res.push({ value: '', rowSpan: targetHeight })
2308
+ for (let i = 1; i < targetHeight; i++) {
2309
+ res.push({ vMerge: true })
2310
+ }
2311
+ return res
2312
+ }
2313
+
2314
+ const itemHeights = val.map(item => this.#getNestedHeight(item))
2315
+ const currentSum = itemHeights.reduce((a, b) => a + b, 0)
2316
+
2317
+ const allocatedHeights = []
2318
+ let remaining = targetHeight
2319
+ for (let i = 0; i < val.length; i++) {
2320
+ const share = Math.round((itemHeights[i] / currentSum) * targetHeight)
2321
+ allocatedHeights.push(share)
2322
+ remaining -= share
2323
+ }
2324
+
2325
+ if (remaining !== 0) {
2326
+ let idx = 0
2327
+ while (remaining > 0) {
2328
+ allocatedHeights[idx % allocatedHeights.length]++
2329
+ remaining--
2330
+ idx++
2331
+ }
2332
+ while (remaining < 0) {
2333
+ let reduced = false
2334
+ for (let i = 0; i < allocatedHeights.length; i++) {
2335
+ const actualIdx = (idx + i) % allocatedHeights.length
2336
+ if (allocatedHeights[actualIdx] > 1) {
2337
+ allocatedHeights[actualIdx]--
2338
+ remaining++
2339
+ reduced = true
2340
+ break
2341
+ }
2342
+ }
2343
+ if (!reduced) break
2344
+ idx++
2345
+ }
2346
+ }
2347
+
2348
+ const result = []
2349
+ for (let i = 0; i < val.length; i++) {
2350
+ result.push(...this.#expandCellVal(val[i], allocatedHeights[i]))
2351
+ }
2352
+ return result
2353
+ }
2354
+
2355
+ #applyAutoMerge(cells) {
2356
+ const result = [...cells]
2357
+ let i = 0
2358
+ while (i < result.length) {
2359
+ const cell = result[i]
2360
+ if (cell.vMerge) {
2361
+ i++
2362
+ continue
2363
+ }
2364
+ let count = 1
2365
+ let j = i + 1
2366
+ while (
2367
+ j < result.length &&
2368
+ !result[j].vMerge &&
2369
+ result[j].value === cell.value &&
2370
+ cell.value !== ''
2371
+ ) {
2372
+ count++
2373
+ j++
2374
+ }
2375
+ if (count > 1) {
2376
+ cell.rowSpan = count
2377
+ for (let k = i + 1; k < j; k++) {
2378
+ result[k] = { vMerge: true }
2379
+ }
2380
+ }
2381
+ i = j
2382
+ }
2383
+ return result
2384
+ }
2385
+
1099
2386
  #generateRandomUint32() {
1100
2387
  return Math.floor(Math.random() * 4294967296)
1101
2388
  }