node-pptx-templater 1.0.20 → 1.1.0

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.
@@ -223,6 +223,8 @@ class TableManager {
223
223
  )
224
224
  }
225
225
 
226
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
227
+
226
228
  if (cellShapes) {
227
229
  this.#processCellShapes(
228
230
  slideIndex,
@@ -251,7 +253,7 @@ class TableManager {
251
253
  * @param {string[]} rowData
252
254
  * @param {SlideManager} slideManager
253
255
  */
254
- addTableRow(slideIndex, tableId, rowData, slideManager) {
256
+ addTableRow(slideIndex, tableId, rowData, slideManager, options = {}) {
255
257
  const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
256
258
 
257
259
  const trs = tblObj['a:tr'] || []
@@ -260,18 +262,69 @@ class TableManager {
260
262
  }
261
263
 
262
264
  const lastRow = trs[trs.length - 1]
263
- const newRow = this.#xmlParser.deepClone(lastRow)
264
- this.#updateRowId(newRow)
265
+ const numCols = lastRow['a:tc']?.length || 0
265
266
 
266
- const tcs = newRow['a:tc'] || []
267
- for (let j = 0; j < tcs.length; j++) {
268
- this.#setCellTextObj(tcs[j], rowData[j] !== undefined ? rowData[j] : '')
269
- // Clear any merged indicators for the new row by default
270
- if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
271
- 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]))
272
271
  }
272
+ const targetHeight = Math.max(1, ...heights)
273
273
 
274
- 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
+ }
275
328
 
276
329
  slideManager.markSlideObjDirty(slideIndex)
277
330
  }
@@ -740,6 +793,66 @@ class TableManager {
740
793
  return { row, col }
741
794
  }
742
795
 
796
+ getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager) {
797
+ const { tblObj, frameObj } = this.#getTableContext(slideIndex, tableId, slideManager)
798
+
799
+ const xfrm = frameObj['p:xfrm']
800
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
801
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
802
+
803
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
804
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
805
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
806
+
807
+ const trsArr = tblObj['a:tr'] || []
808
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
809
+
810
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
811
+ const pr = parent.row
812
+ const pc = parent.col
813
+
814
+ let cellLeft = tableX
815
+ for (let idx = 0; idx < pc; idx++) {
816
+ cellLeft += colWidths[idx] || 0
817
+ }
818
+
819
+ let cellTop = tableY
820
+ for (let idx = 0; idx < pr; idx++) {
821
+ cellTop += rowHeights[idx] || 0
822
+ }
823
+
824
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
825
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
826
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
827
+
828
+ let cellWidth = 0
829
+ for (let idx = 0; idx < gridSpan; idx++) {
830
+ cellWidth += colWidths[pc + idx] || 0
831
+ }
832
+
833
+ let cellHeight = 0
834
+ for (let idx = 0; idx < rowSpan; idx++) {
835
+ cellHeight += rowHeights[pr + idx] || 0
836
+ }
837
+
838
+ return {
839
+ x: Math.round(cellLeft / 9525),
840
+ y: Math.round(cellTop / 9525),
841
+ width: Math.round(cellWidth / 9525),
842
+ height: Math.round(cellHeight / 9525),
843
+ }
844
+ }
845
+
846
+ getCellPosition(slideIndex, tableId, rowIndex, colIndex, slideManager) {
847
+ const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
848
+ return {
849
+ row: rowIndex,
850
+ column: colIndex,
851
+ x: bounds.x,
852
+ y: bounds.y,
853
+ }
854
+ }
855
+
743
856
  /**
744
857
  * Splits a merged region containing cell (row, col).
745
858
  */
@@ -1152,45 +1265,236 @@ class TableManager {
1152
1265
  }
1153
1266
 
1154
1267
  #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
1268
+ const cellLeft_px = Math.round(cellBounds.left / 9525)
1269
+ const cellTop_px = Math.round(cellBounds.top / 9525)
1270
+ const cellWidth_px = Math.round(cellBounds.width / 9525)
1271
+ const cellHeight_px = Math.round(cellBounds.height / 9525)
1272
+
1273
+ const parseLength = (val, maxVal) => {
1274
+ if (typeof val === 'string' && val.endsWith('%')) {
1275
+ return (parseFloat(val) / 100) * maxVal
1276
+ }
1277
+ return val !== undefined ? parseFloat(val) : undefined
1278
+ }
1159
1279
 
1280
+ const isCellAnchored = config.anchor !== 'slide'
1281
+
1282
+ // 1. Determine bounding box width and height
1283
+ let shapeWidth
1284
+ let shapeHeight
1285
+
1286
+ if (config.type === 'progressBar') {
1287
+ shapeHeight = parseLength(config.height !== undefined ? config.height : 8, cellHeight_px)
1288
+ shapeWidth = parseLength(
1289
+ config.width !== undefined ? config.width : cellWidth_px - 10,
1290
+ cellWidth_px
1291
+ )
1292
+ } else if (config.type === 'badge') {
1293
+ const text = String(config.text !== undefined ? config.text : '')
1294
+ const fontSize = config.textStyle?.fontSize || 10
1295
+ const textWidth = text.length * fontSize * 0.6
1296
+ const paddingX = 12
1297
+ shapeWidth =
1298
+ parseLength(config.width, cellWidth_px) !== undefined
1299
+ ? parseLength(config.width, cellWidth_px)
1300
+ : textWidth + paddingX * 2
1301
+ shapeHeight =
1302
+ parseLength(config.height, cellHeight_px) !== undefined
1303
+ ? parseLength(config.height, cellHeight_px)
1304
+ : fontSize + 12
1305
+ } else if (config.type === 'icon') {
1306
+ const size = parseLength(
1307
+ config.size !== undefined ? config.size : 16,
1308
+ Math.min(cellWidth_px, cellHeight_px)
1309
+ )
1310
+ shapeWidth = size
1311
+ shapeHeight = size
1312
+ } else {
1313
+ shapeWidth = parseLength(config.width, cellWidth_px)
1314
+ if (shapeWidth === undefined) {
1315
+ const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
1316
+ if (sizeVal !== undefined) {
1317
+ shapeWidth = sizeVal
1318
+ } else {
1319
+ const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
1320
+ if (radiusVal !== undefined) {
1321
+ shapeWidth = radiusVal * 2
1322
+ } else {
1323
+ shapeWidth = 12 // default
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ shapeHeight = parseLength(config.height, cellHeight_px)
1329
+ if (shapeHeight === undefined) {
1330
+ const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
1331
+ if (sizeVal !== undefined) {
1332
+ shapeHeight = sizeVal
1333
+ } else {
1334
+ const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
1335
+ if (radiusVal !== undefined) {
1336
+ shapeHeight = radiusVal * 2
1337
+ } else {
1338
+ shapeHeight = 12 // default
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ // Scale shape down proportionally to fit inside the cell if it exceeds the cell dimensions
1345
+ if (shapeWidth > cellWidth_px || shapeHeight > cellHeight_px) {
1346
+ logger.warn(
1347
+ `Shape width (${shapeWidth}px) or height (${shapeHeight}px) exceeds cell dimensions (${cellWidth_px}px x ${cellHeight_px}px). Scaling shape to fit.`
1348
+ )
1349
+ const scale = Math.min(cellWidth_px / shapeWidth, cellHeight_px / shapeHeight)
1350
+ shapeWidth = Math.max(1, Math.floor(shapeWidth * scale))
1351
+ shapeHeight = Math.max(1, Math.floor(shapeHeight * scale))
1352
+ }
1353
+
1354
+ // 2. Determine alignment settings
1355
+ let alignX = config.alignX
1356
+ let alignY = config.alignY
1357
+
1358
+ if (config.position) {
1359
+ switch (config.position) {
1360
+ case 'top-left':
1361
+ if (!alignX) alignX = 'left'
1362
+ if (!alignY) alignY = 'top'
1363
+ break
1364
+ case 'top-center':
1365
+ case 'top':
1366
+ if (!alignX) alignX = 'center'
1367
+ if (!alignY) alignY = 'top'
1368
+ break
1369
+ case 'top-right':
1370
+ if (!alignX) alignX = 'right'
1371
+ if (!alignY) alignY = 'top'
1372
+ break
1373
+ case 'middle-left':
1374
+ case 'left':
1375
+ if (!alignX) alignX = 'left'
1376
+ if (!alignY) alignY = 'middle'
1377
+ break
1378
+ case 'center':
1379
+ case 'middle-center':
1380
+ if (!alignX) alignX = 'center'
1381
+ if (!alignY) alignY = 'middle'
1382
+ break
1383
+ case 'middle-right':
1384
+ case 'right':
1385
+ if (!alignX) alignX = 'right'
1386
+ if (!alignY) alignY = 'middle'
1387
+ break
1388
+ case 'bottom-left':
1389
+ if (!alignX) alignX = 'left'
1390
+ if (!alignY) alignY = 'bottom'
1391
+ break
1392
+ case 'bottom-center':
1393
+ case 'bottom':
1394
+ if (!alignX) alignX = 'center'
1395
+ if (!alignY) alignY = 'bottom'
1396
+ break
1397
+ case 'bottom-right':
1398
+ if (!alignX) alignX = 'right'
1399
+ if (!alignY) alignY = 'bottom'
1400
+ break
1401
+ }
1402
+ }
1403
+
1404
+ if (alignX && !alignY && config.y === undefined) {
1405
+ alignY = 'middle'
1406
+ }
1407
+ if (alignY && !alignX && config.x === undefined) {
1408
+ alignX = 'center'
1409
+ }
1410
+
1411
+ if (!alignX && !alignY && config.x === undefined && config.y === undefined) {
1412
+ alignX = 'center'
1413
+ alignY = 'middle'
1414
+ }
1415
+
1416
+ // 3. Compute coordinates
1417
+ let shapeLeft = cellLeft_px
1418
+ let shapeTop = cellTop_px
1419
+
1420
+ if (isCellAnchored) {
1421
+ if (alignX === 'left') {
1422
+ shapeLeft = cellLeft_px + (config.x !== undefined ? config.x : 5)
1423
+ } else if (alignX === 'center') {
1424
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + (config.x || 0)
1425
+ } else if (alignX === 'right') {
1426
+ shapeLeft =
1427
+ cellLeft_px + cellWidth_px - shapeWidth - (config.x !== undefined ? config.x : 5)
1428
+ } else {
1429
+ shapeLeft =
1430
+ cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - shapeWidth) / 2)
1431
+ }
1432
+
1433
+ if (alignY === 'top') {
1434
+ shapeTop = cellTop_px + (config.y !== undefined ? config.y : 5)
1435
+ } else if (alignY === 'middle') {
1436
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + (config.y || 0)
1437
+ } else if (alignY === 'bottom') {
1438
+ shapeTop =
1439
+ cellTop_px + cellHeight_px - shapeHeight - (config.y !== undefined ? config.y : 5)
1440
+ } else {
1441
+ shapeTop =
1442
+ cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - shapeHeight) / 2)
1443
+ }
1444
+
1445
+ // 4. Boundary Constraints Validation/Enforcement
1446
+ if (shapeWidth > cellWidth_px) {
1447
+ shapeLeft = cellLeft_px
1448
+ } else {
1449
+ shapeLeft = Math.max(
1450
+ cellLeft_px,
1451
+ Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1452
+ )
1453
+ }
1454
+
1455
+ if (shapeHeight > cellHeight_px) {
1456
+ shapeTop = cellTop_px
1457
+ } else {
1458
+ shapeTop = Math.max(
1459
+ cellTop_px,
1460
+ Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight)
1461
+ )
1462
+ }
1463
+ } else {
1464
+ shapeLeft = config.x || 0
1465
+ shapeTop = config.y || 0
1466
+ }
1467
+
1468
+ // 5. Expand individual sub-elements / custom shapes
1160
1469
  if (config.type === 'progressBar') {
1161
1470
  const value = config.value !== undefined ? config.value : 0
1162
1471
  const max = config.max !== undefined ? config.max : 100
1163
1472
  const fill = config.fill || '#3B82F6'
1164
1473
  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
1474
 
1171
1475
  const shapes = []
1172
1476
  shapes.push({
1173
1477
  type: 'roundedRectangle',
1174
1478
  fill: bgFill,
1175
- x: pbX,
1176
- y: pbY,
1177
- width: pbWidth,
1178
- height: pbHeight,
1179
- borderRadius: pbHeight / 2,
1479
+ x: shapeLeft,
1480
+ y: shapeTop,
1481
+ width: shapeWidth,
1482
+ height: shapeHeight,
1483
+ borderRadius: shapeHeight / 2,
1180
1484
  zIndex: config.zIndex,
1181
1485
  })
1182
1486
 
1183
1487
  const pct = Math.min(1, Math.max(0, value / max))
1184
1488
  if (pct > 0) {
1185
- const filledWidth = pbWidth * pct
1489
+ const filledWidth = shapeWidth * pct
1186
1490
  shapes.push({
1187
1491
  type: 'roundedRectangle',
1188
1492
  fill: fill,
1189
- x: pbX,
1190
- y: pbY,
1493
+ x: shapeLeft,
1494
+ y: shapeTop,
1191
1495
  width: filledWidth,
1192
- height: pbHeight,
1193
- borderRadius: pbHeight / 2,
1496
+ height: shapeHeight,
1497
+ borderRadius: shapeHeight / 2,
1194
1498
  zIndex: (config.zIndex || 0) + 1,
1195
1499
  })
1196
1500
  }
@@ -1200,23 +1504,15 @@ class TableManager {
1200
1504
  if (config.type === 'badge') {
1201
1505
  const text = String(config.text !== undefined ? config.text : '')
1202
1506
  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
1507
  return [
1212
1508
  {
1213
1509
  type: 'roundedRectangle',
1214
1510
  fill: config.fill || '#10B981',
1215
- borderRadius: badgeHeight / 2,
1216
- x: x,
1217
- y: y,
1218
- width: badgeWidth,
1219
- height: badgeHeight,
1511
+ borderRadius: shapeHeight / 2,
1512
+ x: shapeLeft,
1513
+ y: shapeTop,
1514
+ width: shapeWidth,
1515
+ height: shapeHeight,
1220
1516
  text: text,
1221
1517
  textStyle: {
1222
1518
  color: config.textStyle?.color || '#FFFFFF',
@@ -1234,9 +1530,8 @@ class TableManager {
1234
1530
  }
1235
1531
 
1236
1532
  if (config.type === 'icon') {
1237
- const size = config.size || 16
1238
1533
  const iconFill = config.fill
1239
- const fontSize = Math.round(size * 0.8)
1534
+ const fontSize = Math.round(shapeWidth * 0.8)
1240
1535
 
1241
1536
  let baseConfig = null
1242
1537
  switch (config.icon) {
@@ -1245,8 +1540,8 @@ class TableManager {
1245
1540
  type: 'rectangle',
1246
1541
  fill: 'none',
1247
1542
  border: null,
1248
- width: size,
1249
- height: size,
1543
+ width: shapeWidth,
1544
+ height: shapeHeight,
1250
1545
  text: '✔',
1251
1546
  textStyle: {
1252
1547
  color: iconFill || '#10B981',
@@ -1261,8 +1556,8 @@ class TableManager {
1261
1556
  type: 'rectangle',
1262
1557
  fill: 'none',
1263
1558
  border: null,
1264
- width: size,
1265
- height: size,
1559
+ width: shapeWidth,
1560
+ height: shapeHeight,
1266
1561
  text: '✘',
1267
1562
  textStyle: {
1268
1563
  color: iconFill || '#EF4444',
@@ -1277,8 +1572,8 @@ class TableManager {
1277
1572
  type: 'triangle',
1278
1573
  fill: iconFill || '#F59E0B',
1279
1574
  border: null,
1280
- width: size,
1281
- height: size,
1575
+ width: shapeWidth,
1576
+ height: shapeHeight,
1282
1577
  text: '!',
1283
1578
  textStyle: {
1284
1579
  color: '#FFFFFF',
@@ -1293,7 +1588,7 @@ class TableManager {
1293
1588
  type: 'circle',
1294
1589
  fill: iconFill || '#3B82F6',
1295
1590
  border: null,
1296
- radius: size / 2,
1591
+ radius: shapeWidth / 2,
1297
1592
  text: 'i',
1298
1593
  textStyle: {
1299
1594
  color: '#FFFFFF',
@@ -1308,8 +1603,8 @@ class TableManager {
1308
1603
  type: 'star5',
1309
1604
  fill: iconFill || '#FBBF24',
1310
1605
  border: null,
1311
- width: size,
1312
- height: size,
1606
+ width: shapeWidth,
1607
+ height: shapeHeight,
1313
1608
  }
1314
1609
  break
1315
1610
  case 'up':
@@ -1317,8 +1612,8 @@ class TableManager {
1317
1612
  type: 'upArrow',
1318
1613
  fill: iconFill || '#10B981',
1319
1614
  border: null,
1320
- width: size,
1321
- height: size,
1615
+ width: shapeWidth,
1616
+ height: shapeHeight,
1322
1617
  }
1323
1618
  break
1324
1619
  case 'down':
@@ -1326,8 +1621,8 @@ class TableManager {
1326
1621
  type: 'downArrow',
1327
1622
  fill: iconFill || '#EF4444',
1328
1623
  border: null,
1329
- width: size,
1330
- height: size,
1624
+ width: shapeWidth,
1625
+ height: shapeHeight,
1331
1626
  }
1332
1627
  break
1333
1628
  case 'arrow-right':
@@ -1335,8 +1630,8 @@ class TableManager {
1335
1630
  type: 'rightArrow',
1336
1631
  fill: iconFill || '#3B82F6',
1337
1632
  border: null,
1338
- width: size,
1339
- height: size,
1633
+ width: shapeWidth,
1634
+ height: shapeHeight,
1340
1635
  }
1341
1636
  break
1342
1637
  case 'arrow-left':
@@ -1344,19 +1639,16 @@ class TableManager {
1344
1639
  type: 'leftArrow',
1345
1640
  fill: iconFill || '#3B82F6',
1346
1641
  border: null,
1347
- width: size,
1348
- height: size,
1642
+ width: shapeWidth,
1643
+ height: shapeHeight,
1349
1644
  }
1350
1645
  break
1351
1646
  default:
1352
1647
  return []
1353
1648
  }
1354
1649
 
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
1650
+ baseConfig.x = shapeLeft
1651
+ baseConfig.y = shapeTop
1360
1652
  baseConfig.zIndex = config.zIndex
1361
1653
  if (config.border) baseConfig.border = config.border
1362
1654
  if (config.transparency !== undefined) baseConfig.transparency = config.transparency
@@ -1366,58 +1658,11 @@ class TableManager {
1366
1658
  return [baseConfig]
1367
1659
  }
1368
1660
 
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
1661
  const expanded = Object.assign({}, config, {
1419
1662
  x: shapeLeft,
1420
1663
  y: shapeTop,
1664
+ width: shapeWidth,
1665
+ height: shapeHeight,
1421
1666
  })
1422
1667
 
1423
1668
  if (expanded.type === 'circle' && expanded.radius === undefined) {
@@ -1561,6 +1806,69 @@ class TableManager {
1561
1806
  })
1562
1807
  }
1563
1808
 
1809
+ getTableRows(slideIndex, tableId, options = {}, slideManager) {
1810
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
1811
+ const trs = tblObj['a:tr'] || []
1812
+ if (trs.length === 0) {
1813
+ return options.includeMetadata
1814
+ ? { rows: [], rowCount: 0, columnCount: 0, mergedCells: [] }
1815
+ : []
1816
+ }
1817
+
1818
+ const numRows = trs.length
1819
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1820
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1821
+ const numCols = gridColsArr.length
1822
+
1823
+ // Extract all raw cell text, resolving merges to their parent's text
1824
+ const matrix = []
1825
+ for (let r = 0; r < numRows; r++) {
1826
+ const rowCells = []
1827
+ for (let c = 0; c < numCols; c++) {
1828
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
1829
+ const cell = trs[parent.row]?.['a:tc']?.[parent.col]
1830
+ const text = cell ? this.#getCellText(cell) : ''
1831
+ rowCells.push(text)
1832
+ }
1833
+ matrix.push(rowCells)
1834
+ }
1835
+
1836
+ // Header names are extracted from the first row (index 0)
1837
+ const headerNames = matrix[0].map((hText, cIdx) => {
1838
+ const cleaned = hText.trim()
1839
+ return cleaned || `column${cIdx + 1}`
1840
+ })
1841
+
1842
+ // Compute the data rows (excluding the header row at index 0)
1843
+ const dataRows = matrix.slice(1)
1844
+
1845
+ let rowsResult = []
1846
+ if (options.raw) {
1847
+ rowsResult = dataRows
1848
+ } else {
1849
+ for (const rowCells of dataRows) {
1850
+ const rowObj = {}
1851
+ for (let c = 0; c < numCols; c++) {
1852
+ const key = headerNames[c]
1853
+ rowObj[key] = rowCells[c] || ''
1854
+ }
1855
+ rowsResult.push(rowObj)
1856
+ }
1857
+ }
1858
+
1859
+ if (options.includeMetadata) {
1860
+ const mergedCells = this.getMergedCells(slideIndex, tableId, slideManager)
1861
+ return {
1862
+ rows: rowsResult,
1863
+ rowCount: numRows,
1864
+ columnCount: numCols,
1865
+ mergedCells,
1866
+ }
1867
+ }
1868
+
1869
+ return rowsResult
1870
+ }
1871
+
1564
1872
  addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
1565
1873
  const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1566
1874
  slideIndex,
@@ -1786,6 +2094,297 @@ class TableManager {
1786
2094
  }
1787
2095
  }
1788
2096
 
2097
+ #calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
2098
+ const trsArr = tblObj['a:tr'] || []
2099
+ if (trsArr.length === 0) return []
2100
+
2101
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2102
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2103
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2104
+
2105
+ const numRows = trsArr.length
2106
+ const numCols = colWidths.length
2107
+
2108
+ // Initialize rowHeights with original height or 0
2109
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2110
+
2111
+ // Helper to get paragraph font size
2112
+ const getParagraphFontSize = p => {
2113
+ let maxSz = 14 // default 14pt
2114
+ if (p['a:pPr']?.['a:defRPr']?.['@_sz']) {
2115
+ maxSz = parseInt(p['a:pPr']['a:defRPr']['@_sz'], 10) / 100
2116
+ }
2117
+ if (p['a:r']) {
2118
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
2119
+ for (const r of runs) {
2120
+ if (r['a:rPr']?.['@_sz']) {
2121
+ const szVal = parseInt(r['a:rPr']['@_sz'], 10) / 100
2122
+ if (szVal > maxSz) {
2123
+ maxSz = szVal
2124
+ }
2125
+ }
2126
+ }
2127
+ }
2128
+ return maxSz
2129
+ }
2130
+
2131
+ // Helper to wrap text
2132
+ const wrapText = (text, availWidth_px, fontSize) => {
2133
+ const charWidth = fontSize * 0.65
2134
+ const words = text.split(/(\s+)/)
2135
+ let linesCount = 0
2136
+ let currentLineLen = 0
2137
+
2138
+ for (const word of words) {
2139
+ if (!word) continue
2140
+ const wordWidth = word.length * charWidth
2141
+ if (wordWidth > availWidth_px) {
2142
+ if (currentLineLen > 0) {
2143
+ linesCount++
2144
+ currentLineLen = 0
2145
+ }
2146
+ let remainingWidth = wordWidth
2147
+ while (remainingWidth > 0) {
2148
+ linesCount++
2149
+ remainingWidth -= availWidth_px
2150
+ }
2151
+ } else {
2152
+ if (currentLineLen + wordWidth > availWidth_px) {
2153
+ linesCount++
2154
+ currentLineLen = word.trim() ? wordWidth : 0
2155
+ } else {
2156
+ currentLineLen += wordWidth
2157
+ }
2158
+ }
2159
+ }
2160
+ if (currentLineLen > 0 || linesCount === 0) {
2161
+ linesCount++
2162
+ }
2163
+ return linesCount
2164
+ }
2165
+
2166
+ // Helper to get cell margins
2167
+ const getCellMargins = cell => {
2168
+ const tcPr = cell['a:tcPr']
2169
+ const marL = tcPr?.['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
2170
+ const marR = tcPr?.['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
2171
+ const marT = tcPr?.['@_marT'] !== undefined ? parseInt(tcPr['@_marT'], 10) : 45720
2172
+ const marB = tcPr?.['@_marB'] !== undefined ? parseInt(tcPr['@_marB'], 10) : 45720
2173
+ return { marL, marR, marT, marB }
2174
+ }
2175
+
2176
+ // Calculate required height for each cell
2177
+ const cellHeights = Array.from({ length: numRows }, () => new Array(numCols).fill(0))
2178
+
2179
+ for (let r = 0; r < numRows; r++) {
2180
+ const row = trsArr[r]
2181
+ const tcs = row['a:tc'] || []
2182
+ for (let c = 0; c < numCols; c++) {
2183
+ const cell = tcs[c]
2184
+ if (!cell || cell['@_hMerge'] || cell['@_vMerge']) continue
2185
+
2186
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
2187
+ const gridSpan = cell['@_gridSpan'] ? parseInt(cell['@_gridSpan'], 10) : 1
2188
+
2189
+ // Calculate cell width
2190
+ let cellWidth = 0
2191
+ for (let idx = 0; idx < gridSpan; idx++) {
2192
+ cellWidth += colWidths[parent.col + idx] || 0
2193
+ }
2194
+
2195
+ const { marL, marR, marT, marB } = getCellMargins(cell)
2196
+ const availWidth = cellWidth - marL - marR
2197
+ const availWidth_px = Math.max(1, availWidth / 9525)
2198
+
2199
+ // Calculate text height
2200
+ const txBody = cell['a:txBody']
2201
+ let textHeight_emu = 0
2202
+ if (txBody) {
2203
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
2204
+ for (const p of paras) {
2205
+ const fontSize = getParagraphFontSize(p)
2206
+ let pText = ''
2207
+ if (p['a:r']) {
2208
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
2209
+ for (const r of runs) {
2210
+ if (r['a:t']) {
2211
+ pText += String(r['a:t'])
2212
+ }
2213
+ }
2214
+ }
2215
+
2216
+ const linesCount = wrapText(pText, availWidth_px, fontSize)
2217
+ const lineHeight_emu = fontSize * 20780 // 1.4 line height multiplier
2218
+
2219
+ let pHeight_emu = linesCount * lineHeight_emu
2220
+ if (p['a:pPr']?.['a:spcBef']?.['a:spcPts']?.['@_val']) {
2221
+ pHeight_emu += parseInt(p['a:pPr']['a:spcBef']['a:spcPts']['@_val'], 10) * 127
2222
+ }
2223
+ if (p['a:pPr']?.['a:spcAft']?.['a:spcPts']?.['@_val']) {
2224
+ pHeight_emu += parseInt(p['a:pPr']['a:spcAft']['a:spcPts']['@_val'], 10) * 127
2225
+ }
2226
+ textHeight_emu += pHeight_emu
2227
+ }
2228
+ }
2229
+
2230
+ const totalCellHeight_emu = marT + marB + textHeight_emu
2231
+ cellHeights[r][c] = totalCellHeight_emu
2232
+ }
2233
+ }
2234
+
2235
+ // Now resolve row heights based on required cell heights
2236
+ // First, non-vertically-merged cells define row heights directly
2237
+ for (let r = 0; r < numRows; r++) {
2238
+ let maxCellHeight = rowHeights[r] // Start with original template height as floor
2239
+ const row = trsArr[r]
2240
+ const tcs = row['a:tc'] || []
2241
+ for (let c = 0; c < numCols; c++) {
2242
+ const cell = tcs[c]
2243
+ if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
2244
+ const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
2245
+ if (rowSpan === 1) {
2246
+ if (cellHeights[r][c] > maxCellHeight) {
2247
+ maxCellHeight = cellHeights[r][c]
2248
+ }
2249
+ }
2250
+ }
2251
+ rowHeights[r] = maxCellHeight
2252
+ }
2253
+
2254
+ // Next, adjust for vertically merged cells (rowSpan > 1)
2255
+ for (let r = 0; r < numRows; r++) {
2256
+ const row = trsArr[r]
2257
+ const tcs = row['a:tc'] || []
2258
+ for (let c = 0; c < numCols; c++) {
2259
+ const cell = tcs[c]
2260
+ if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
2261
+ const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
2262
+ if (rowSpan > 1) {
2263
+ const reqHeight = cellHeights[r][c]
2264
+ // Sum currently allocated row heights for spanned rows
2265
+ let currentSpanHeight = 0
2266
+ for (let idx = 0; idx < rowSpan; idx++) {
2267
+ currentSpanHeight += rowHeights[r + idx] || 0
2268
+ }
2269
+ if (reqHeight > currentSpanHeight) {
2270
+ // Distribute the extra required height equally across all spanned rows
2271
+ const diff = reqHeight - currentSpanHeight
2272
+ const extraPerRow = Math.ceil(diff / rowSpan)
2273
+ for (let idx = 0; idx < rowSpan; idx++) {
2274
+ rowHeights[r + idx] += extraPerRow
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+
2281
+ // Update row heights in XML
2282
+ for (let r = 0; r < numRows; r++) {
2283
+ trsArr[r]['@_h'] = String(rowHeights[r])
2284
+ }
2285
+
2286
+ return rowHeights
2287
+ }
2288
+
2289
+ #getNestedHeight(val) {
2290
+ if (Array.isArray(val)) {
2291
+ if (val.length === 0) return 1
2292
+ return val.reduce((sum, item) => sum + this.#getNestedHeight(item), 0)
2293
+ }
2294
+ return 1
2295
+ }
2296
+
2297
+ #expandCellVal(val, targetHeight) {
2298
+ if (!Array.isArray(val)) {
2299
+ const res = []
2300
+ res.push({ value: val !== undefined ? val : '', rowSpan: targetHeight })
2301
+ for (let i = 1; i < targetHeight; i++) {
2302
+ res.push({ vMerge: true })
2303
+ }
2304
+ return res
2305
+ }
2306
+
2307
+ if (val.length === 0) {
2308
+ const res = []
2309
+ res.push({ value: '', rowSpan: targetHeight })
2310
+ for (let i = 1; i < targetHeight; i++) {
2311
+ res.push({ vMerge: true })
2312
+ }
2313
+ return res
2314
+ }
2315
+
2316
+ const itemHeights = val.map(item => this.#getNestedHeight(item))
2317
+ const currentSum = itemHeights.reduce((a, b) => a + b, 0)
2318
+
2319
+ const allocatedHeights = []
2320
+ let remaining = targetHeight
2321
+ for (let i = 0; i < val.length; i++) {
2322
+ const share = Math.round((itemHeights[i] / currentSum) * targetHeight)
2323
+ allocatedHeights.push(share)
2324
+ remaining -= share
2325
+ }
2326
+
2327
+ if (remaining !== 0) {
2328
+ let idx = 0
2329
+ while (remaining > 0) {
2330
+ allocatedHeights[idx % allocatedHeights.length]++
2331
+ remaining--
2332
+ idx++
2333
+ }
2334
+ while (remaining < 0) {
2335
+ let reduced = false
2336
+ for (let i = 0; i < allocatedHeights.length; i++) {
2337
+ const actualIdx = (idx + i) % allocatedHeights.length
2338
+ if (allocatedHeights[actualIdx] > 1) {
2339
+ allocatedHeights[actualIdx]--
2340
+ remaining++
2341
+ reduced = true
2342
+ break
2343
+ }
2344
+ }
2345
+ if (!reduced) break
2346
+ idx++
2347
+ }
2348
+ }
2349
+
2350
+ const result = []
2351
+ for (let i = 0; i < val.length; i++) {
2352
+ result.push(...this.#expandCellVal(val[i], allocatedHeights[i]))
2353
+ }
2354
+ return result
2355
+ }
2356
+
2357
+ #applyAutoMerge(cells) {
2358
+ const result = [...cells]
2359
+ let i = 0
2360
+ while (i < result.length) {
2361
+ const cell = result[i]
2362
+ if (cell.vMerge) {
2363
+ i++
2364
+ continue
2365
+ }
2366
+ let count = 1
2367
+ let j = i + 1
2368
+ while (
2369
+ j < result.length &&
2370
+ !result[j].vMerge &&
2371
+ result[j].value === cell.value &&
2372
+ cell.value !== ''
2373
+ ) {
2374
+ count++
2375
+ j++
2376
+ }
2377
+ if (count > 1) {
2378
+ cell.rowSpan = count
2379
+ for (let k = i + 1; k < j; k++) {
2380
+ result[k] = { vMerge: true }
2381
+ }
2382
+ }
2383
+ i = j
2384
+ }
2385
+ return result
2386
+ }
2387
+
1789
2388
  #generateRandomUint32() {
1790
2389
  return Math.floor(Math.random() * 4294967296)
1791
2390
  }