node-pptx-templater 1.0.18 → 1.0.20

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,21 @@ class TableManager {
184
223
  )
185
224
  }
186
225
 
226
+ if (cellShapes) {
227
+ this.#processCellShapes(
228
+ slideIndex,
229
+ tableId,
230
+ resolvedTableId,
231
+ rowsData,
232
+ isObjectRows,
233
+ cellShapes,
234
+ slideManager,
235
+ shapeManager,
236
+ tblObj,
237
+ frameObj
238
+ )
239
+ }
240
+
187
241
  logger.debug(
188
242
  `Updated table "${tableId}" with ${rowsData.length} rows and ${finalMerges.length} merges`
189
243
  )
@@ -1051,11 +1105,647 @@ class TableManager {
1051
1105
 
1052
1106
  #getTableContext(slideIndex, tableId, slideManager) {
1053
1107
  const slideObj = slideManager.getSlideObj(slideIndex)
1054
- const tblObj = this.#findTableObj(slideObj, tableId, slideManager, slideIndex)
1055
- if (!tblObj) {
1108
+ const res = slideManager.getSlideTable(slideIndex, tableId)
1109
+ if (!res || !res.table) {
1056
1110
  throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
1057
1111
  }
1058
- return { slideObj, tblObj }
1112
+ const cNvPr = res.frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
1113
+ const resolvedTableId = cNvPr ? cNvPr['@_name'] || String(cNvPr['@_id']) : tableId
1114
+ return { slideObj, tblObj: res.table, frameObj: res.frame, resolvedTableId }
1115
+ }
1116
+
1117
+ #applyCellOptions(cellObj, cellOptions) {
1118
+ if (cellOptions.fill) {
1119
+ if (!cellObj['a:tcPr']) cellObj['a:tcPr'] = {}
1120
+ cellObj['a:tcPr']['a:solidFill'] = {
1121
+ 'a:srgbClr': { '@_val': cellOptions.fill },
1122
+ }
1123
+ }
1124
+
1125
+ if (cellOptions.align) {
1126
+ const txBody = cellObj['a:txBody']
1127
+ if (txBody && txBody['a:p']) {
1128
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
1129
+ for (const p of paras) {
1130
+ if (!p['a:pPr']) p['a:pPr'] = {}
1131
+ p['a:pPr']['@_algn'] = cellOptions.align
1132
+ }
1133
+ }
1134
+ }
1135
+
1136
+ if (cellOptions.fontSize) {
1137
+ const sizeVal = cellOptions.fontSize * 100
1138
+ const txBody = cellObj['a:txBody']
1139
+ if (txBody && txBody['a:p']) {
1140
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
1141
+ for (const p of paras) {
1142
+ if (p['a:r']) {
1143
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
1144
+ for (const r of runs) {
1145
+ if (!r['a:rPr']) r['a:rPr'] = {}
1146
+ r['a:rPr']['@_sz'] = String(sizeVal)
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ #expandCellShape(config, cellBounds) {
1155
+ const cellLeft_px = cellBounds.left / 9525
1156
+ const cellTop_px = cellBounds.top / 9525
1157
+ const cellWidth_px = cellBounds.width / 9525
1158
+ const cellHeight_px = cellBounds.height / 9525
1159
+
1160
+ if (config.type === 'progressBar') {
1161
+ const value = config.value !== undefined ? config.value : 0
1162
+ const max = config.max !== undefined ? config.max : 100
1163
+ const fill = config.fill || '#3B82F6'
1164
+ const bgFill = config.backgroundFill || '#E5E7EB'
1165
+ const pbHeight = config.height !== undefined ? config.height : 8
1166
+ const pbWidth = config.width !== undefined ? config.width : cellWidth_px - 10
1167
+
1168
+ const pbX = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - pbWidth) / 2)
1169
+ const pbY = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - pbHeight) / 2)
1170
+
1171
+ const shapes = []
1172
+ shapes.push({
1173
+ type: 'roundedRectangle',
1174
+ fill: bgFill,
1175
+ x: pbX,
1176
+ y: pbY,
1177
+ width: pbWidth,
1178
+ height: pbHeight,
1179
+ borderRadius: pbHeight / 2,
1180
+ zIndex: config.zIndex,
1181
+ })
1182
+
1183
+ const pct = Math.min(1, Math.max(0, value / max))
1184
+ if (pct > 0) {
1185
+ const filledWidth = pbWidth * pct
1186
+ shapes.push({
1187
+ type: 'roundedRectangle',
1188
+ fill: fill,
1189
+ x: pbX,
1190
+ y: pbY,
1191
+ width: filledWidth,
1192
+ height: pbHeight,
1193
+ borderRadius: pbHeight / 2,
1194
+ zIndex: (config.zIndex || 0) + 1,
1195
+ })
1196
+ }
1197
+ return shapes
1198
+ }
1199
+
1200
+ if (config.type === 'badge') {
1201
+ const text = String(config.text !== undefined ? config.text : '')
1202
+ const fontSize = config.textStyle?.fontSize || 10
1203
+ const textWidth = text.length * fontSize * 0.6
1204
+ const paddingX = 12
1205
+ const badgeWidth = config.width !== undefined ? config.width : textWidth + paddingX * 2
1206
+ const badgeHeight = config.height !== undefined ? config.height : fontSize + 12
1207
+
1208
+ const x = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - badgeWidth) / 2)
1209
+ const y = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - badgeHeight) / 2)
1210
+
1211
+ return [
1212
+ {
1213
+ type: 'roundedRectangle',
1214
+ fill: config.fill || '#10B981',
1215
+ borderRadius: badgeHeight / 2,
1216
+ x: x,
1217
+ y: y,
1218
+ width: badgeWidth,
1219
+ height: badgeHeight,
1220
+ text: text,
1221
+ textStyle: {
1222
+ color: config.textStyle?.color || '#FFFFFF',
1223
+ fontSize: fontSize,
1224
+ bold: config.textStyle?.bold !== undefined ? config.textStyle.bold : true,
1225
+ align: 'center',
1226
+ },
1227
+ border: config.border,
1228
+ transparency: config.transparency,
1229
+ shadow: config.shadow,
1230
+ rotation: config.rotation,
1231
+ zIndex: config.zIndex,
1232
+ },
1233
+ ]
1234
+ }
1235
+
1236
+ if (config.type === 'icon') {
1237
+ const size = config.size || 16
1238
+ const iconFill = config.fill
1239
+ const fontSize = Math.round(size * 0.8)
1240
+
1241
+ let baseConfig = null
1242
+ switch (config.icon) {
1243
+ case 'check':
1244
+ baseConfig = {
1245
+ type: 'rectangle',
1246
+ fill: 'none',
1247
+ border: null,
1248
+ width: size,
1249
+ height: size,
1250
+ text: '✔',
1251
+ textStyle: {
1252
+ color: iconFill || '#10B981',
1253
+ bold: true,
1254
+ fontSize: fontSize,
1255
+ align: 'center',
1256
+ },
1257
+ }
1258
+ break
1259
+ case 'cross':
1260
+ baseConfig = {
1261
+ type: 'rectangle',
1262
+ fill: 'none',
1263
+ border: null,
1264
+ width: size,
1265
+ height: size,
1266
+ text: '✘',
1267
+ textStyle: {
1268
+ color: iconFill || '#EF4444',
1269
+ bold: true,
1270
+ fontSize: fontSize,
1271
+ align: 'center',
1272
+ },
1273
+ }
1274
+ break
1275
+ case 'warning':
1276
+ baseConfig = {
1277
+ type: 'triangle',
1278
+ fill: iconFill || '#F59E0B',
1279
+ border: null,
1280
+ width: size,
1281
+ height: size,
1282
+ text: '!',
1283
+ textStyle: {
1284
+ color: '#FFFFFF',
1285
+ bold: true,
1286
+ fontSize: Math.round(fontSize * 0.7),
1287
+ align: 'center',
1288
+ },
1289
+ }
1290
+ break
1291
+ case 'info':
1292
+ baseConfig = {
1293
+ type: 'circle',
1294
+ fill: iconFill || '#3B82F6',
1295
+ border: null,
1296
+ radius: size / 2,
1297
+ text: 'i',
1298
+ textStyle: {
1299
+ color: '#FFFFFF',
1300
+ bold: true,
1301
+ fontSize: Math.round(fontSize * 0.7),
1302
+ align: 'center',
1303
+ },
1304
+ }
1305
+ break
1306
+ case 'star':
1307
+ baseConfig = {
1308
+ type: 'star5',
1309
+ fill: iconFill || '#FBBF24',
1310
+ border: null,
1311
+ width: size,
1312
+ height: size,
1313
+ }
1314
+ break
1315
+ case 'up':
1316
+ baseConfig = {
1317
+ type: 'upArrow',
1318
+ fill: iconFill || '#10B981',
1319
+ border: null,
1320
+ width: size,
1321
+ height: size,
1322
+ }
1323
+ break
1324
+ case 'down':
1325
+ baseConfig = {
1326
+ type: 'downArrow',
1327
+ fill: iconFill || '#EF4444',
1328
+ border: null,
1329
+ width: size,
1330
+ height: size,
1331
+ }
1332
+ break
1333
+ case 'arrow-right':
1334
+ baseConfig = {
1335
+ type: 'rightArrow',
1336
+ fill: iconFill || '#3B82F6',
1337
+ border: null,
1338
+ width: size,
1339
+ height: size,
1340
+ }
1341
+ break
1342
+ case 'arrow-left':
1343
+ baseConfig = {
1344
+ type: 'leftArrow',
1345
+ fill: iconFill || '#3B82F6',
1346
+ border: null,
1347
+ width: size,
1348
+ height: size,
1349
+ }
1350
+ break
1351
+ default:
1352
+ return []
1353
+ }
1354
+
1355
+ const x = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - size) / 2)
1356
+ const y = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - size) / 2)
1357
+
1358
+ baseConfig.x = x
1359
+ baseConfig.y = y
1360
+ baseConfig.zIndex = config.zIndex
1361
+ if (config.border) baseConfig.border = config.border
1362
+ if (config.transparency !== undefined) baseConfig.transparency = config.transparency
1363
+ if (config.shadow !== undefined) baseConfig.shadow = config.shadow
1364
+ if (config.rotation !== undefined) baseConfig.rotation = config.rotation
1365
+
1366
+ return [baseConfig]
1367
+ }
1368
+
1369
+ const shapeWidth =
1370
+ config.width !== undefined
1371
+ ? config.width
1372
+ : config.size !== undefined
1373
+ ? config.size
1374
+ : config.radius !== undefined
1375
+ ? config.radius * 2
1376
+ : 12
1377
+ const shapeHeight =
1378
+ config.height !== undefined
1379
+ ? config.height
1380
+ : config.size !== undefined
1381
+ ? config.size
1382
+ : config.radius !== undefined
1383
+ ? config.radius * 2
1384
+ : 12
1385
+
1386
+ let shapeLeft = cellLeft_px
1387
+ let shapeTop = cellTop_px
1388
+
1389
+ if (config.position === 'center') {
1390
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1391
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1392
+ } else if (config.position === 'left') {
1393
+ shapeLeft = cellLeft_px + 5
1394
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1395
+ } else if (config.position === 'right') {
1396
+ shapeLeft = cellLeft_px + cellWidth_px - shapeWidth - 5
1397
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1398
+ } else if (config.position === 'top') {
1399
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1400
+ shapeTop = cellTop_px + 5
1401
+ } else if (config.position === 'bottom') {
1402
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1403
+ shapeTop = cellTop_px + cellHeight_px - shapeHeight - 5
1404
+ } else {
1405
+ if (config.x === undefined && config.y === undefined) {
1406
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1407
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1408
+ }
1409
+ }
1410
+
1411
+ if (config.x !== undefined) {
1412
+ shapeLeft += config.x
1413
+ }
1414
+ if (config.y !== undefined) {
1415
+ shapeTop += config.y
1416
+ }
1417
+
1418
+ const expanded = Object.assign({}, config, {
1419
+ x: shapeLeft,
1420
+ y: shapeTop,
1421
+ })
1422
+
1423
+ if (expanded.type === 'circle' && expanded.radius === undefined) {
1424
+ expanded.radius = shapeWidth / 2
1425
+ }
1426
+ if (expanded.type === 'square' && expanded.size === undefined) {
1427
+ expanded.size = shapeWidth
1428
+ }
1429
+
1430
+ return [expanded]
1431
+ }
1432
+
1433
+ #processCellShapes(
1434
+ slideIndex,
1435
+ tableId,
1436
+ resolvedTableId,
1437
+ rowsData,
1438
+ isObjectRows,
1439
+ cellShapes,
1440
+ slideManager,
1441
+ shapeManager,
1442
+ tblObj,
1443
+ frameObj
1444
+ ) {
1445
+ if (!cellShapes || !shapeManager) return
1446
+
1447
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1448
+ const prefixToDelete = `cellshape_${resolvedTableId}_`
1449
+ const existingNames = shapes
1450
+ .map(s => s.name)
1451
+ .filter(name => name && name.startsWith(prefixToDelete))
1452
+
1453
+ for (const name of existingNames) {
1454
+ try {
1455
+ shapeManager.deleteShape(slideIndex, name, slideManager)
1456
+ } catch (err) {
1457
+ logger.warn(`Failed to delete existing cell shape "${name}": ${err.message}`)
1458
+ }
1459
+ }
1460
+
1461
+ const xfrm = frameObj['p:xfrm']
1462
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1463
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1464
+
1465
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1466
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1467
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1468
+
1469
+ const trsArr = tblObj['a:tr'] || []
1470
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1471
+
1472
+ const getCellBounds = (r, c) => {
1473
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
1474
+ const pr = parent.row
1475
+ const pc = parent.col
1476
+
1477
+ let cellLeft = tableX
1478
+ for (let idx = 0; idx < pc; idx++) {
1479
+ cellLeft += colWidths[idx] || 0
1480
+ }
1481
+
1482
+ let cellTop = tableY
1483
+ for (let idx = 0; idx < pr; idx++) {
1484
+ cellTop += rowHeights[idx] || 0
1485
+ }
1486
+
1487
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1488
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1489
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1490
+
1491
+ let cellWidth = 0
1492
+ for (let idx = 0; idx < gridSpan; idx++) {
1493
+ cellWidth += colWidths[pc + idx] || 0
1494
+ }
1495
+
1496
+ let cellHeight = 0
1497
+ for (let idx = 0; idx < rowSpan; idx++) {
1498
+ cellHeight += rowHeights[pr + idx] || 0
1499
+ }
1500
+
1501
+ return {
1502
+ left: cellLeft,
1503
+ top: cellTop,
1504
+ width: cellWidth,
1505
+ height: cellHeight,
1506
+ }
1507
+ }
1508
+
1509
+ const shapesToCreate = []
1510
+ const headerNames = (trsArr[0]?.['a:tc'] || []).map(cell => this.#getCellText(cell).trim())
1511
+
1512
+ for (let i = 0; i < rowsData.length; i++) {
1513
+ const rowData = rowsData[i]
1514
+ const finalRowIndex = isObjectRows ? i + 1 : i
1515
+
1516
+ const numCols = trsArr[finalRowIndex]?.['a:tc']?.length || 0
1517
+ for (let j = 0; j < numCols; j++) {
1518
+ const headerName = headerNames[j]
1519
+ let shapeFn = null
1520
+
1521
+ if (headerName) {
1522
+ shapeFn = cellShapes[headerName] || cellShapes[headerName.toLowerCase()]
1523
+ }
1524
+ if (!shapeFn) {
1525
+ shapeFn = cellShapes[j]
1526
+ }
1527
+
1528
+ if (typeof shapeFn !== 'function') continue
1529
+
1530
+ let configs = shapeFn(rowData, i)
1531
+ if (!configs) continue
1532
+
1533
+ if (!Array.isArray(configs)) {
1534
+ configs = [configs]
1535
+ }
1536
+
1537
+ configs.forEach((config, shapeIdx) => {
1538
+ shapesToCreate.push({
1539
+ config,
1540
+ rowIndex: finalRowIndex,
1541
+ colIndex: j,
1542
+ shapeIndex: shapeIdx,
1543
+ })
1544
+ })
1545
+ }
1546
+ }
1547
+
1548
+ shapesToCreate.sort((a, b) => (a.config.zIndex || 0) - (b.config.zIndex || 0))
1549
+
1550
+ shapesToCreate.forEach(item => {
1551
+ const bounds = getCellBounds(item.rowIndex, item.colIndex)
1552
+ const expandedConfigs = this.#expandCellShape(item.config, bounds)
1553
+
1554
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
1555
+ const finalShapeIndex =
1556
+ expandedConfigs.length > 1 ? `${item.shapeIndex}_${expIdx}` : item.shapeIndex
1557
+ expandedConfig.id = `cellshape_${resolvedTableId}_${item.rowIndex}_${item.colIndex}_${finalShapeIndex}`
1558
+
1559
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1560
+ })
1561
+ })
1562
+ }
1563
+
1564
+ addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
1565
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1566
+ slideIndex,
1567
+ tableId,
1568
+ slideManager
1569
+ )
1570
+
1571
+ const xfrm = frameObj['p:xfrm']
1572
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1573
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1574
+
1575
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1576
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1577
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1578
+
1579
+ const trsArr = tblObj['a:tr'] || []
1580
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1581
+
1582
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
1583
+ const pr = parent.row
1584
+ const pc = parent.col
1585
+
1586
+ let cellLeft = tableX
1587
+ for (let idx = 0; idx < pc; idx++) {
1588
+ cellLeft += colWidths[idx] || 0
1589
+ }
1590
+
1591
+ let cellTop = tableY
1592
+ for (let idx = 0; idx < pr; idx++) {
1593
+ cellTop += rowHeights[idx] || 0
1594
+ }
1595
+
1596
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1597
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1598
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1599
+
1600
+ let cellWidth = 0
1601
+ for (let idx = 0; idx < gridSpan; idx++) {
1602
+ cellWidth += colWidths[pc + idx] || 0
1603
+ }
1604
+
1605
+ let cellHeight = 0
1606
+ for (let idx = 0; idx < rowSpan; idx++) {
1607
+ cellHeight += rowHeights[pr + idx] || 0
1608
+ }
1609
+
1610
+ const bounds = { left: cellLeft, top: cellTop, width: cellWidth, height: cellHeight }
1611
+
1612
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1613
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_`
1614
+ let maxShapeIndex = -1
1615
+ for (const s of shapes) {
1616
+ if (s.name && s.name.startsWith(prefix)) {
1617
+ const remaining = s.name.slice(prefix.length)
1618
+ const parts = remaining.split('_')
1619
+ const idxVal = parseInt(parts[0], 10)
1620
+ if (!isNaN(idxVal) && idxVal > maxShapeIndex) {
1621
+ maxShapeIndex = idxVal
1622
+ }
1623
+ }
1624
+ }
1625
+ const nextShapeIndex = maxShapeIndex + 1
1626
+
1627
+ const expandedConfigs = this.#expandCellShape(options, bounds)
1628
+
1629
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
1630
+ const finalShapeIndex =
1631
+ expandedConfigs.length > 1 ? `${nextShapeIndex}_${expIdx}` : nextShapeIndex
1632
+ expandedConfig.id = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${finalShapeIndex}`
1633
+
1634
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1635
+ })
1636
+ }
1637
+
1638
+ updateCellShape(
1639
+ slideIndex,
1640
+ tableId,
1641
+ rowIndex,
1642
+ colIndex,
1643
+ shapeIndex,
1644
+ options,
1645
+ slideManager,
1646
+ shapeManager
1647
+ ) {
1648
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1649
+ slideIndex,
1650
+ tableId,
1651
+ slideManager
1652
+ )
1653
+
1654
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1655
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
1656
+ const matchingShapes = shapes.filter(
1657
+ s => s.name && (s.name === prefix || s.name.startsWith(prefix + '_'))
1658
+ )
1659
+
1660
+ if (matchingShapes.length === 0) {
1661
+ throw new PPTXError(`Cell shape "${shapeIndex}" not found in cell (${rowIndex}, ${colIndex})`)
1662
+ }
1663
+
1664
+ for (const s of matchingShapes) {
1665
+ shapeManager.deleteShape(slideIndex, s.name, slideManager)
1666
+ }
1667
+
1668
+ const xfrm = frameObj['p:xfrm']
1669
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1670
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1671
+
1672
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1673
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1674
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1675
+
1676
+ const trsArr = tblObj['a:tr'] || []
1677
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1678
+
1679
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
1680
+ const pr = parent.row
1681
+ const pc = parent.col
1682
+
1683
+ let cellLeft = tableX
1684
+ for (let idx = 0; idx < pc; idx++) {
1685
+ cellLeft += colWidths[idx] || 0
1686
+ }
1687
+
1688
+ let cellTop = tableY
1689
+ for (let idx = 0; idx < pr; idx++) {
1690
+ cellTop += rowHeights[idx] || 0
1691
+ }
1692
+
1693
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1694
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1695
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1696
+
1697
+ let cellWidth = 0
1698
+ for (let idx = 0; idx < gridSpan; idx++) {
1699
+ cellWidth += colWidths[pc + idx] || 0
1700
+ }
1701
+
1702
+ let cellHeight = 0
1703
+ for (let idx = 0; idx < rowSpan; idx++) {
1704
+ cellHeight += rowHeights[pr + idx] || 0
1705
+ }
1706
+
1707
+ const bounds = { left: cellLeft, top: cellTop, width: cellWidth, height: cellHeight }
1708
+
1709
+ const expandedConfigs = this.#expandCellShape(options, bounds)
1710
+
1711
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
1712
+ const finalShapeIndex = expandedConfigs.length > 1 ? `${shapeIndex}_${expIdx}` : shapeIndex
1713
+ expandedConfig.id = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${finalShapeIndex}`
1714
+
1715
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1716
+ })
1717
+ }
1718
+
1719
+ removeCellShape(slideIndex, tableId, rowIndex, colIndex, shapeIndex, slideManager, shapeManager) {
1720
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
1721
+
1722
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1723
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
1724
+ const matchingShapes = shapes.filter(
1725
+ s => s.name && (s.name === prefix || s.name.startsWith(prefix + '_'))
1726
+ )
1727
+
1728
+ if (matchingShapes.length === 0) {
1729
+ throw new PPTXError(`Cell shape "${shapeIndex}" not found in cell (${rowIndex}, ${colIndex})`)
1730
+ }
1731
+
1732
+ for (const s of matchingShapes) {
1733
+ shapeManager.deleteShape(slideIndex, s.name, slideManager)
1734
+ }
1735
+ }
1736
+
1737
+ getCellShape(slideIndex, tableId, rowIndex, colIndex, shapeIndex, slideManager, shapeManager) {
1738
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
1739
+
1740
+ const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
1741
+ const shapes = shapeManager.getShapes(slideIndex, slideManager)
1742
+ const primaryShape = shapes.find(
1743
+ s => s.name === prefix || s.name === `${prefix}_0` || s.name === `${prefix}_1`
1744
+ )
1745
+
1746
+ if (!primaryShape) return null
1747
+
1748
+ return shapeManager.getShape(slideIndex, primaryShape.name, slideManager)
1059
1749
  }
1060
1750
 
1061
1751
  /**