node-pptx-templater 1.1.3 → 1.1.6

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.
@@ -48,6 +48,19 @@ class TableManager {
48
48
  /** @private @type {XMLParser} */
49
49
  #xmlParser
50
50
 
51
+ /**
52
+ * In-memory registry mapping cellshape names to their original config.
53
+ * Used to reposition shapes after table structure mutations (row removal,
54
+ * insertion, merge, etc.).
55
+ *
56
+ * Key: shape name (e.g. "cellshape_Table_2_5_0")
57
+ * Value: { slideIndex, resolvedTableId, tableId, rowIndex, colIndex, shapeIndex, config }
58
+ *
59
+ * @private
60
+ * @type {Map<string, {slideIndex: number, resolvedTableId: string, tableId: string, rowIndex: number, colIndex: number, shapeIndex: string|number, config: Object}>}
61
+ */
62
+ #cellShapeAnchors = new Map()
63
+
51
64
  /**
52
65
  * @param {XMLParser} xmlParser
53
66
  */
@@ -253,7 +266,15 @@ class TableManager {
253
266
  * @param {string[]} rowData
254
267
  * @param {SlideManager} slideManager
255
268
  */
256
- addTableRow(slideIndex, tableId, rowData, slideManager, options = {}) {
269
+ addTableRow(slideIndex, tableId, rowData, slideManager, shapeManagerOrOptions, options = {}) {
270
+ let shapeManager = null
271
+ let actualOptions = options
272
+ if (shapeManagerOrOptions && typeof shapeManagerOrOptions.getShapes === 'function') {
273
+ shapeManager = shapeManagerOrOptions
274
+ } else if (shapeManagerOrOptions && typeof shapeManagerOrOptions === 'object') {
275
+ actualOptions = shapeManagerOrOptions
276
+ }
277
+
257
278
  const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
258
279
 
259
280
  const trs = tblObj['a:tr'] || []
@@ -261,6 +282,10 @@ class TableManager {
261
282
  throw new PPTXError('No rows to clone from')
262
283
  }
263
284
 
285
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
286
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
287
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
288
+
264
289
  const lastRow = trs[trs.length - 1]
265
290
  const numCols = lastRow['a:tc']?.length || 0
266
291
 
@@ -273,7 +298,7 @@ class TableManager {
273
298
 
274
299
  // Expand each column value to targetHeight
275
300
  const expandedCols = []
276
- const strategy = options.mergeStrategy || 'auto'
301
+ const strategy = actualOptions.mergeStrategy || 'auto'
277
302
  for (let c = 0; c < numCols; c++) {
278
303
  let colCells = this.#expandCellVal(rowData[c], targetHeight)
279
304
  if (strategy === 'none') {
@@ -290,6 +315,12 @@ class TableManager {
290
315
  expandedCols.push(colCells)
291
316
  }
292
317
 
318
+ const startRowIndex = trs.length
319
+ const shapeCellsToCreate = []
320
+ const isShapeConfig = val => {
321
+ return val && typeof val === 'object' && typeof val.type === 'string'
322
+ }
323
+
293
324
  // Clone and append rows
294
325
  for (let r = 0; r < targetHeight; r++) {
295
326
  const newRow = this.#xmlParser.deepClone(lastRow)
@@ -312,10 +343,117 @@ class TableManager {
312
343
  } else {
313
344
  let text = cellDef.value
314
345
  let cellOpts = {}
315
- if (cellDef.value && typeof cellDef.value === 'object') {
346
+
347
+ if (isShapeConfig(cellDef.value)) {
348
+ const config = cellDef.value
349
+ const globalRowIndex = startRowIndex + r
350
+
351
+ const isStandardShape = [
352
+ 'circle',
353
+ 'square',
354
+ 'rectangle',
355
+ 'triangle',
356
+ 'diamond',
357
+ 'hexagon',
358
+ 'line',
359
+ ].includes(config.type)
360
+
361
+ const shapeConfig = { ...config }
362
+
363
+ if (isStandardShape) {
364
+ text =
365
+ config.text !== undefined
366
+ ? config.text
367
+ : config.value !== undefined
368
+ ? config.value
369
+ : ''
370
+ delete shapeConfig.text // DO NOT render text inside the shape overlay!
371
+ } else {
372
+ text = '' // for badges/icons/progressBars, cell text is empty!
373
+ }
374
+
375
+ shapeCellsToCreate.push({
376
+ rowIndex: globalRowIndex,
377
+ colIndex: c,
378
+ config: shapeConfig,
379
+ })
380
+
381
+ cellOpts = {}
382
+ if (config.cellFill) cellOpts.fill = config.cellFill
383
+ if (config.cellAlign) cellOpts.align = config.cellAlign
384
+
385
+ // Estimate shape dimensions to set margins
386
+ const colWidth_emu = colWidths[c] || 0
387
+ const colWidth_px = colWidth_emu / 9525
388
+
389
+ const parseLength = (val, maxVal) => {
390
+ if (typeof val === 'string' && val.endsWith('%')) {
391
+ return (parseFloat(val) / 100) * maxVal
392
+ }
393
+ return val !== undefined ? parseFloat(val) : undefined
394
+ }
395
+
396
+ let shapeWidth = 12
397
+ let shapeHeight = 12
398
+
399
+ if (config.width !== undefined) {
400
+ shapeWidth = parseLength(config.width, colWidth_px) || 12
401
+ } else if (config.size !== undefined) {
402
+ shapeWidth = parseLength(config.size, colWidth_px) || 12
403
+ } else if (config.radius !== undefined) {
404
+ shapeWidth = (parseLength(config.radius, colWidth_px) || 6) * 2
405
+ }
406
+
407
+ if (config.height !== undefined) {
408
+ shapeHeight = parseLength(config.height, 50) || 12
409
+ } else if (config.size !== undefined) {
410
+ shapeHeight = parseLength(config.size, 50) || 12
411
+ } else if (config.radius !== undefined) {
412
+ shapeHeight = (parseLength(config.radius, 25) || 6) * 2
413
+ }
414
+
415
+ if (isStandardShape && text !== '') {
416
+ const position = config.position || (config.text ? 'left' : 'center')
417
+ const tcPr = tcObj['a:tcPr'] || {}
418
+ const currentMarL =
419
+ tcPr['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
420
+ const currentMarR =
421
+ tcPr['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
422
+ const currentMarT =
423
+ tcPr['@_marT'] !== undefined ? parseInt(tcPr['@_marT'], 10) : 45720
424
+ const currentMarB =
425
+ tcPr['@_marB'] !== undefined ? parseInt(tcPr['@_marB'], 10) : 45720
426
+
427
+ tcObj['a:tcPr'] = tcObj['a:tcPr'] || {}
428
+
429
+ const isLeft = position.includes('left') || position === 'left'
430
+ const isRight = position.includes('right') || position === 'right'
431
+ const isTop = position === 'top' || position.startsWith('top-')
432
+ const isBottom = position === 'bottom' || position.startsWith('bottom-')
433
+
434
+ if (isLeft) {
435
+ tcObj['a:tcPr']['@_marL'] = String(
436
+ currentMarL + Math.round(shapeWidth * 9525) + 57150
437
+ )
438
+ } else if (isRight) {
439
+ tcObj['a:tcPr']['@_marR'] = String(
440
+ currentMarR + Math.round(shapeWidth * 9525) + 57150
441
+ )
442
+ } else if (isTop) {
443
+ tcObj['a:tcPr']['@_marT'] = String(
444
+ currentMarT + Math.round(shapeHeight * 9525) + 57150
445
+ )
446
+ } else if (isBottom) {
447
+ tcObj['a:tcPr']['@_marB'] = String(
448
+ currentMarB + Math.round(shapeHeight * 9525) + 57150
449
+ )
450
+ }
451
+ }
452
+ } else if (cellDef.value && typeof cellDef.value === 'object') {
316
453
  text = cellDef.value.value !== undefined ? cellDef.value.value : ''
317
454
  cellOpts = cellDef.value
318
455
  }
456
+
319
457
  this.#setCellTextObj(tcObj, text)
320
458
  if (cellDef.rowSpan && cellDef.rowSpan > 1 && strategy !== 'none') {
321
459
  tcObj['@_rowSpan'] = String(cellDef.rowSpan)
@@ -327,6 +465,25 @@ class TableManager {
327
465
  }
328
466
 
329
467
  slideManager.markSlideObjDirty(slideIndex)
468
+
469
+ if (shapeCellsToCreate.length > 0 && shapeManager) {
470
+ for (const item of shapeCellsToCreate) {
471
+ const resolvedConfig = { ...item.config }
472
+ if (!resolvedConfig.position) {
473
+ resolvedConfig.position = resolvedConfig.text ? 'left' : 'center'
474
+ }
475
+
476
+ this.addCellShape(
477
+ slideIndex,
478
+ tableId,
479
+ item.rowIndex,
480
+ item.colIndex,
481
+ resolvedConfig,
482
+ slideManager,
483
+ shapeManager
484
+ )
485
+ }
486
+ }
330
487
  }
331
488
 
332
489
  /**
@@ -337,8 +494,8 @@ class TableManager {
337
494
  * @param {number} rowIndex - 0-based row index.
338
495
  * @param {SlideManager} slideManager
339
496
  */
340
- removeTableRow(slideIndex, tableId, rowIndex, slideManager) {
341
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
497
+ removeTableRow(slideIndex, tableId, rowIndex, slideManager, shapeManager = null) {
498
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
342
499
 
343
500
  const trs = tblObj['a:tr'] || []
344
501
  if (rowIndex < 0 || rowIndex >= trs.length) {
@@ -348,6 +505,18 @@ class TableManager {
348
505
  trs.splice(rowIndex, 1)
349
506
 
350
507
  slideManager.markSlideObjDirty(slideIndex)
508
+
509
+ if (shapeManager) {
510
+ this.#adjustCellShapesAfterRowShift(
511
+ slideIndex,
512
+ resolvedTableId,
513
+ tableId,
514
+ rowIndex,
515
+ -1,
516
+ slideManager,
517
+ shapeManager
518
+ )
519
+ }
351
520
  }
352
521
 
353
522
  /**
@@ -359,8 +528,8 @@ class TableManager {
359
528
  * @param {string[]} rowData
360
529
  * @param {SlideManager} slideManager
361
530
  */
362
- insertTableRow(slideIndex, tableId, rowIndex, rowData, slideManager) {
363
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
531
+ insertTableRow(slideIndex, tableId, rowIndex, rowData, slideManager, shapeManager = null) {
532
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
364
533
 
365
534
  const trs = tblObj['a:tr'] || []
366
535
  if (rowIndex < 0 || rowIndex > trs.length) {
@@ -387,6 +556,18 @@ class TableManager {
387
556
  trs.splice(rowIndex, 0, newRow)
388
557
 
389
558
  slideManager.markSlideObjDirty(slideIndex)
559
+
560
+ if (shapeManager) {
561
+ this.#adjustCellShapesAfterRowShift(
562
+ slideIndex,
563
+ resolvedTableId,
564
+ tableId,
565
+ rowIndex,
566
+ +1,
567
+ slideManager,
568
+ shapeManager
569
+ )
570
+ }
390
571
  }
391
572
 
392
573
  /**
@@ -398,8 +579,15 @@ class TableManager {
398
579
  * @param {number} targetRowIndex
399
580
  * @param {SlideManager} slideManager
400
581
  */
401
- cloneTableRow(slideIndex, tableId, sourceRowIndex, targetRowIndex, slideManager) {
402
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
582
+ cloneTableRow(
583
+ slideIndex,
584
+ tableId,
585
+ sourceRowIndex,
586
+ targetRowIndex,
587
+ slideManager,
588
+ shapeManager = null
589
+ ) {
590
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
403
591
 
404
592
  const trs = tblObj['a:tr'] || []
405
593
  if (sourceRowIndex < 0 || sourceRowIndex >= trs.length) {
@@ -416,6 +604,18 @@ class TableManager {
416
604
  trs.splice(targetRowIndex, 0, newRow)
417
605
 
418
606
  slideManager.markSlideObjDirty(slideIndex)
607
+
608
+ if (shapeManager) {
609
+ this.#adjustCellShapesAfterRowShift(
610
+ slideIndex,
611
+ resolvedTableId,
612
+ tableId,
613
+ targetRowIndex,
614
+ +1,
615
+ slideManager,
616
+ shapeManager
617
+ )
618
+ }
419
619
  }
420
620
 
421
621
  /**
@@ -563,7 +763,16 @@ class TableManager {
563
763
  * @param {number} endCol
564
764
  * @param {SlideManager} slideManager
565
765
  */
566
- mergeCells(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
766
+ mergeCells(
767
+ slideIndex,
768
+ tableId,
769
+ startRow,
770
+ startCol,
771
+ endRow,
772
+ endCol,
773
+ slideManager,
774
+ shapeManager = null
775
+ ) {
567
776
  const validation = this.validateMergeRegion(
568
777
  slideIndex,
569
778
  tableId,
@@ -577,7 +786,7 @@ class TableManager {
577
786
  throw new PPTXError(`Invalid merge region: ${validation.errors.join('; ')}`)
578
787
  }
579
788
 
580
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
789
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
581
790
  const trs = tblObj['a:tr'] || []
582
791
 
583
792
  const allTexts = []
@@ -630,6 +839,20 @@ class TableManager {
630
839
  this.#setCellTextObj(originCell, combinedText)
631
840
 
632
841
  slideManager.markSlideObjDirty(slideIndex)
842
+
843
+ if (shapeManager) {
844
+ this.#repositionCellShapesInRegion(
845
+ slideIndex,
846
+ tableId,
847
+ resolvedTableId,
848
+ startRow,
849
+ startCol,
850
+ endRow,
851
+ endCol,
852
+ slideManager,
853
+ shapeManager
854
+ )
855
+ }
633
856
  }
634
857
 
635
858
  /**
@@ -643,7 +866,16 @@ class TableManager {
643
866
  * @param {number} endCol
644
867
  * @param {SlideManager} slideManager
645
868
  */
646
- unmergeCells(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
869
+ unmergeCells(
870
+ slideIndex,
871
+ tableId,
872
+ startRow,
873
+ startCol,
874
+ endRow,
875
+ endCol,
876
+ slideManager,
877
+ shapeManager = null
878
+ ) {
647
879
  let actualSlideManager = slideManager
648
880
  let actualEndRow = endRow
649
881
  let actualEndCol = endCol
@@ -679,6 +911,21 @@ class TableManager {
679
911
  if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
680
912
  }
681
913
  }
914
+
915
+ if (shapeManager) {
916
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, actualSlideManager)
917
+ this.#repositionCellShapesInRegion(
918
+ slideIndex,
919
+ tableId,
920
+ resolvedTableId,
921
+ R.startRow,
922
+ R.startCol,
923
+ R.endRow,
924
+ R.endCol,
925
+ actualSlideManager,
926
+ shapeManager
927
+ )
928
+ }
682
929
  } else {
683
930
  for (let r = startRow; r <= actualEndRow; r++) {
684
931
  const rowObj = trs[r]
@@ -693,6 +940,21 @@ class TableManager {
693
940
  if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
694
941
  }
695
942
  }
943
+
944
+ if (shapeManager) {
945
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, actualSlideManager)
946
+ this.#repositionCellShapesInRegion(
947
+ slideIndex,
948
+ tableId,
949
+ resolvedTableId,
950
+ startRow,
951
+ startCol,
952
+ actualEndRow,
953
+ actualEndCol,
954
+ actualSlideManager,
955
+ shapeManager
956
+ )
957
+ }
696
958
  }
697
959
 
698
960
  actualSlideManager.markSlideObjDirty(slideIndex)
@@ -714,25 +976,89 @@ class TableManager {
714
976
  }
715
977
 
716
978
  const trs = tblObj['a:tr'] || []
979
+ const numRows = trs.length
980
+ if (numRows === 0) return []
981
+ const numCols = trs[0]['a:tc']?.length || 0
982
+
717
983
  const merged = []
984
+ const visited = Array.from({ length: numRows }, () => Array(numCols).fill(false))
718
985
 
719
- for (let r = 0; r < trs.length; r++) {
986
+ for (let r = 0; r < numRows; r++) {
720
987
  const row = trs[r]
721
988
  const tcs = row['a:tc'] || []
722
989
  for (let c = 0; c < tcs.length; c++) {
990
+ if (visited[r][c]) continue
723
991
  const cell = tcs[c]
724
992
  if (!cell) continue
725
993
 
726
- const gridSpan = parseInt(cell['@_gridSpan'] || 1, 10)
727
- const rowSpan = parseInt(cell['@_rowSpan'] || 1, 10)
994
+ const isVMerged =
995
+ cell['@_vMerge'] === '1' || cell['@_vMerge'] === 'true' || cell['@_vMerge'] === true
996
+ const isHMerged =
997
+ cell['@_hMerge'] === '1' || cell['@_hMerge'] === 'true' || cell['@_hMerge'] === true
728
998
 
729
- if (gridSpan > 1 || rowSpan > 1) {
999
+ if (isVMerged || isHMerged) {
1000
+ continue
1001
+ }
1002
+
1003
+ // Determine colSpan
1004
+ let colSpan = 1
1005
+ if (cell['@_gridSpan'] !== undefined) {
1006
+ colSpan = parseInt(cell['@_gridSpan'], 10)
1007
+ } else {
1008
+ let nextCol = c + 1
1009
+ while (nextCol < numCols) {
1010
+ const nextCell = tcs[nextCol]
1011
+ if (
1012
+ nextCell &&
1013
+ (nextCell['@_hMerge'] === '1' ||
1014
+ nextCell['@_hMerge'] === 'true' ||
1015
+ nextCell['@_hMerge'] === true)
1016
+ ) {
1017
+ colSpan++
1018
+ nextCol++
1019
+ } else {
1020
+ break
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ // Determine rowSpan
1026
+ let rowSpan = 1
1027
+ if (cell['@_rowSpan'] !== undefined) {
1028
+ rowSpan = parseInt(cell['@_rowSpan'], 10)
1029
+ } else {
1030
+ let nextRow = r + 1
1031
+ while (nextRow < numRows) {
1032
+ const nextCell = trs[nextRow]['a:tc']?.[c]
1033
+ if (
1034
+ nextCell &&
1035
+ (nextCell['@_vMerge'] === '1' ||
1036
+ nextCell['@_vMerge'] === 'true' ||
1037
+ nextCell['@_vMerge'] === true)
1038
+ ) {
1039
+ rowSpan++
1040
+ nextRow++
1041
+ } else {
1042
+ break
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ if (colSpan > 1 || rowSpan > 1) {
730
1048
  merged.push({
731
1049
  startRow: r,
732
1050
  startCol: c,
733
1051
  endRow: r + rowSpan - 1,
734
- endCol: c + gridSpan - 1,
1052
+ endCol: c + colSpan - 1,
735
1053
  })
1054
+
1055
+ for (let i = r; i < r + rowSpan; i++) {
1056
+ for (let j = c; j < c + colSpan; j++) {
1057
+ if (i < numRows && j < numCols) {
1058
+ visited[i][j] = true
1059
+ }
1060
+ }
1061
+ }
736
1062
  }
737
1063
  }
738
1064
  }
@@ -804,12 +1130,20 @@ class TableManager {
804
1130
  const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
805
1131
  const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
806
1132
 
807
- const trsArr = tblObj['a:tr'] || []
808
1133
  const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
809
1134
 
810
- const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
811
- const pr = parent.row
812
- const pc = parent.col
1135
+ const R = this.getMergeRegion(slideIndex, tableId, rowIndex, colIndex, slideManager)
1136
+ let pr = rowIndex
1137
+ let pc = colIndex
1138
+ let gridSpan = 1
1139
+ let rowSpan = 1
1140
+
1141
+ if (R) {
1142
+ pr = R.startRow
1143
+ pc = R.startCol
1144
+ gridSpan = R.endCol - R.startCol + 1
1145
+ rowSpan = R.endRow - R.startRow + 1
1146
+ }
813
1147
 
814
1148
  let cellLeft = tableX
815
1149
  for (let idx = 0; idx < pc; idx++) {
@@ -821,10 +1155,6 @@ class TableManager {
821
1155
  cellTop += rowHeights[idx] || 0
822
1156
  }
823
1157
 
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
1158
  let cellWidth = 0
829
1159
  for (let idx = 0; idx < gridSpan; idx++) {
830
1160
  cellWidth += colWidths[pc + idx] || 0
@@ -1389,17 +1719,8 @@ class TableManager {
1389
1719
  }
1390
1720
 
1391
1721
  // 2. Determine alignment settings
1392
- let alignX = config.alignX || config.horizontal
1393
- let alignY = config.alignY || config.vertical
1394
-
1395
- if (alignX) {
1396
- alignX = String(alignX).toLowerCase()
1397
- if (alignX === 'middle') alignX = 'center'
1398
- }
1399
- if (alignY) {
1400
- alignY = String(alignY).toLowerCase()
1401
- if (alignY === 'center') alignY = 'middle'
1402
- }
1722
+ let alignX = config.alignX
1723
+ let alignY = config.alignY
1403
1724
 
1404
1725
  if (config.position) {
1405
1726
  switch (config.position) {
@@ -1447,16 +1768,33 @@ class TableManager {
1447
1768
  }
1448
1769
  }
1449
1770
 
1450
- if (alignX && !alignY && config.y === undefined) {
1451
- alignY = 'middle'
1771
+ if (!alignX) {
1772
+ const ax = config.alignX || config.horizontal
1773
+ if (ax) {
1774
+ alignX = String(ax).toLowerCase().trim()
1775
+ if (alignX === 'middle') alignX = 'center'
1776
+ }
1452
1777
  }
1453
- if (alignY && !alignX && config.x === undefined) {
1454
- alignX = 'center'
1778
+ if (!alignY) {
1779
+ const ay = config.alignY || config.vertical
1780
+ if (ay) {
1781
+ alignY = String(ay).toLowerCase().trim()
1782
+ if (alignY === 'center') alignY = 'middle'
1783
+ }
1455
1784
  }
1456
1785
 
1457
- if (!alignX && !alignY && config.x === undefined && config.y === undefined) {
1458
- alignX = 'center'
1786
+ if (!alignX && !alignY) {
1787
+ if (config.x !== undefined || config.y !== undefined) {
1788
+ alignX = 'left'
1789
+ alignY = 'top'
1790
+ } else {
1791
+ alignX = 'center'
1792
+ alignY = 'middle'
1793
+ }
1794
+ } else if (alignX && !alignY) {
1459
1795
  alignY = 'middle'
1796
+ } else if (alignY && !alignX) {
1797
+ alignX = 'center'
1460
1798
  }
1461
1799
 
1462
1800
  // 3. Compute coordinates
@@ -1464,60 +1802,48 @@ class TableManager {
1464
1802
  let shapeTop = cellTop_px
1465
1803
 
1466
1804
  if (isCellAnchored) {
1805
+ let dx = 0
1806
+ const hasOffsetValX =
1807
+ config.offsetX !== undefined || config.xOffset !== undefined || config.x !== undefined
1808
+ if (config.offsetX !== undefined) dx = parseFloat(config.offsetX)
1809
+ else if (config.xOffset !== undefined) dx = parseFloat(config.xOffset)
1810
+ else if (config.x !== undefined) dx = parseFloat(config.x)
1811
+
1812
+ let dy = 0
1813
+ const hasOffsetValY =
1814
+ config.offsetY !== undefined || config.yOffset !== undefined || config.y !== undefined
1815
+ if (config.offsetY !== undefined) dy = parseFloat(config.offsetY)
1816
+ else if (config.yOffset !== undefined) dy = parseFloat(config.yOffset)
1817
+ else if (config.y !== undefined) dy = parseFloat(config.y)
1818
+
1819
+ shapeLeft = cellLeft_px
1467
1820
  if (alignX === 'left') {
1468
- shapeLeft = cellLeft_px + (config.x !== undefined ? config.x : 5)
1821
+ const padding = hasOffsetValX ? dx : 5
1822
+ shapeLeft = cellLeft_px + padding
1469
1823
  } else if (alignX === 'center') {
1470
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + (config.x || 0)
1824
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + dx
1471
1825
  } else if (alignX === 'right') {
1472
- shapeLeft =
1473
- cellLeft_px + cellWidth_px - shapeWidth - (config.x !== undefined ? config.x : 5)
1474
- } else {
1475
- shapeLeft =
1476
- cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - shapeWidth) / 2)
1826
+ const padding = hasOffsetValX ? dx : 5
1827
+ shapeLeft = cellLeft_px + cellWidth_px - shapeWidth - padding
1477
1828
  }
1478
1829
 
1830
+ shapeTop = cellTop_px
1479
1831
  if (alignY === 'top') {
1480
- shapeTop = cellTop_px + (config.y !== undefined ? config.y : 5)
1832
+ const padding = hasOffsetValY ? dy : 5
1833
+ shapeTop = cellTop_px + padding
1481
1834
  } else if (alignY === 'middle') {
1482
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + (config.y || 0)
1835
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + dy
1483
1836
  } else if (alignY === 'bottom') {
1484
- shapeTop =
1485
- cellTop_px + cellHeight_px - shapeHeight - (config.y !== undefined ? config.y : 5)
1486
- } else {
1487
- shapeTop =
1488
- cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - shapeHeight) / 2)
1837
+ const padding = hasOffsetValY ? dy : 5
1838
+ shapeTop = cellTop_px + cellHeight_px - shapeHeight - padding
1489
1839
  }
1490
1840
 
1491
1841
  // 4. Boundary Constraints Validation/Enforcement
1492
- if (shapeWidth > cellWidth_px) {
1493
- if (alignX === 'center') {
1494
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1495
- } else if (alignX === 'right') {
1496
- shapeLeft = cellLeft_px + cellWidth_px - shapeWidth
1497
- } else {
1498
- shapeLeft = cellLeft_px
1499
- }
1500
- } else {
1501
- shapeLeft = Math.max(
1502
- cellLeft_px,
1503
- Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1504
- )
1505
- }
1506
-
1507
- if (shapeHeight > cellHeight_px) {
1508
- if (alignY === 'middle') {
1509
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1510
- } else if (alignY === 'bottom') {
1511
- shapeTop = cellTop_px + cellHeight_px - shapeHeight
1512
- } else {
1513
- shapeTop = cellTop_px
1514
- }
1515
- } else {
1516
- shapeTop = Math.max(
1517
- cellTop_px,
1518
- Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight)
1519
- )
1520
- }
1842
+ shapeLeft = Math.max(
1843
+ cellLeft_px,
1844
+ Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1845
+ )
1846
+ shapeTop = Math.max(cellTop_px, Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight))
1521
1847
  } else {
1522
1848
  shapeLeft = config.x || 0
1523
1849
  shapeTop = config.y || 0
@@ -1743,7 +2069,7 @@ class TableManager {
1743
2069
  slideManager,
1744
2070
  shapeManager,
1745
2071
  tblObj,
1746
- _frameObj
2072
+ frameObj
1747
2073
  ) {
1748
2074
  if (!cellShapes || !shapeManager) return
1749
2075
 
@@ -1761,6 +2087,54 @@ class TableManager {
1761
2087
  }
1762
2088
  }
1763
2089
 
2090
+ const xfrm = frameObj['p:xfrm']
2091
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
2092
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
2093
+
2094
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2095
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2096
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2097
+
2098
+ const trsArr = tblObj['a:tr'] || []
2099
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2100
+
2101
+ const getCellBounds = (r, c) => {
2102
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
2103
+ const pr = parent.row
2104
+ const pc = parent.col
2105
+
2106
+ let cellLeft = tableX
2107
+ for (let idx = 0; idx < pc; idx++) {
2108
+ cellLeft += colWidths[idx] || 0
2109
+ }
2110
+
2111
+ let cellTop = tableY
2112
+ for (let idx = 0; idx < pr; idx++) {
2113
+ cellTop += rowHeights[idx] || 0
2114
+ }
2115
+
2116
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
2117
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
2118
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
2119
+
2120
+ let cellWidth = 0
2121
+ for (let idx = 0; idx < gridSpan; idx++) {
2122
+ cellWidth += colWidths[pc + idx] || 0
2123
+ }
2124
+
2125
+ let cellHeight = 0
2126
+ for (let idx = 0; idx < rowSpan; idx++) {
2127
+ cellHeight += rowHeights[pr + idx] || 0
2128
+ }
2129
+
2130
+ return {
2131
+ left: cellLeft,
2132
+ top: cellTop,
2133
+ width: cellWidth,
2134
+ height: cellHeight,
2135
+ }
2136
+ }
2137
+
1764
2138
  const shapesToCreate = []
1765
2139
  const headerNames = (tblObj['a:tr']?.[0]?.['a:tc'] || []).map(cell =>
1766
2140
  this.#getCellText(cell).trim()
@@ -1890,7 +2264,45 @@ class TableManager {
1890
2264
  }
1891
2265
 
1892
2266
  addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
1893
- const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2267
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
2268
+ slideIndex,
2269
+ tableId,
2270
+ slideManager
2271
+ )
2272
+
2273
+ const xfrm = frameObj['p:xfrm']
2274
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
2275
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
2276
+
2277
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2278
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2279
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2280
+
2281
+ const trsArr = tblObj['a:tr'] || []
2282
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2283
+
2284
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
2285
+ const pr = parent.row
2286
+ const pc = parent.col
2287
+
2288
+ let cellLeft = tableX
2289
+ for (let idx = 0; idx < pc; idx++) {
2290
+ cellLeft += colWidths[idx] || 0
2291
+ }
2292
+
2293
+ let cellTop = tableY
2294
+ for (let idx = 0; idx < pr; idx++) {
2295
+ cellTop += rowHeights[idx] || 0
2296
+ }
2297
+
2298
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
2299
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
2300
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
2301
+
2302
+ let cellWidth = 0
2303
+ for (let idx = 0; idx < gridSpan; idx++) {
2304
+ cellWidth += colWidths[pc + idx] || 0
2305
+ }
1894
2306
 
1895
2307
  const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
1896
2308
  if (!bounds) {
@@ -1920,6 +2332,18 @@ class TableManager {
1920
2332
  expandedConfig.id = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${finalShapeIndex}`
1921
2333
 
1922
2334
  shapeManager.addShape(slideIndex, expandedConfig, slideManager)
2335
+
2336
+ // Register this shape's original config so it can be repositioned after
2337
+ // any subsequent table mutations (row removal, insertion, merge, etc.)
2338
+ this.#cellShapeAnchors.set(expandedConfig.id, {
2339
+ slideIndex,
2340
+ resolvedTableId,
2341
+ tableId,
2342
+ rowIndex,
2343
+ colIndex,
2344
+ shapeIndex: finalShapeIndex,
2345
+ config: { ...options },
2346
+ })
1923
2347
  })
1924
2348
  }
1925
2349
 
@@ -1933,7 +2357,11 @@ class TableManager {
1933
2357
  slideManager,
1934
2358
  shapeManager
1935
2359
  ) {
1936
- const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2360
+ const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
2361
+ slideIndex,
2362
+ tableId,
2363
+ slideManager
2364
+ )
1937
2365
 
1938
2366
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
1939
2367
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
@@ -1979,6 +2407,8 @@ class TableManager {
1979
2407
 
1980
2408
  for (const s of matchingShapes) {
1981
2409
  shapeManager.deleteShape(slideIndex, s.name, slideManager)
2410
+ // Deregister from anchor registry
2411
+ this.#cellShapeAnchors.delete(s.name)
1982
2412
  }
1983
2413
  }
1984
2414
 
@@ -1996,6 +2426,188 @@ class TableManager {
1996
2426
  return shapeManager.getShape(slideIndex, primaryShape.name, slideManager)
1997
2427
  }
1998
2428
 
2429
+ /**
2430
+ * Adjusts all registered cell shapes for a table after a row is removed (delta=-1)
2431
+ * or inserted (delta=+1) at `pivotRowIndex`.
2432
+ *
2433
+ * - For delta=-1 and shapes at pivotRowIndex: the shape is deleted (its row is gone).
2434
+ * - For shapes at rows that shifted: delete the old shape and re-add it at the new
2435
+ * row index so that `getCellBounds` can compute correct coordinates from the
2436
+ * updated table layout.
2437
+ *
2438
+ * @private
2439
+ */
2440
+ #adjustCellShapesAfterRowShift(
2441
+ slideIndex,
2442
+ resolvedTableId,
2443
+ tableId,
2444
+ pivotRowIndex,
2445
+ delta,
2446
+ slideManager,
2447
+ shapeManager
2448
+ ) {
2449
+ if (!shapeManager) return
2450
+
2451
+ // Collect entries first (avoid mutating map while iterating)
2452
+ const toDelete = []
2453
+ const toReindex = []
2454
+
2455
+ for (const [name, anchor] of this.#cellShapeAnchors) {
2456
+ if (anchor.slideIndex !== slideIndex || anchor.resolvedTableId !== resolvedTableId) continue
2457
+
2458
+ if (delta < 0 && anchor.rowIndex === pivotRowIndex) {
2459
+ // Row was removed — delete the shape
2460
+ toDelete.push(name)
2461
+ } else if (delta < 0 && anchor.rowIndex > pivotRowIndex) {
2462
+ // Row shifted up (removal below pivot)
2463
+ toReindex.push({ name, anchor, newRowIndex: anchor.rowIndex + delta })
2464
+ } else if (delta > 0 && anchor.rowIndex >= pivotRowIndex) {
2465
+ // Row shifted down (insertion at or above this row)
2466
+ toReindex.push({ name, anchor, newRowIndex: anchor.rowIndex + delta })
2467
+ }
2468
+ }
2469
+
2470
+ // Delete shapes for the removed row
2471
+ for (const name of toDelete) {
2472
+ try {
2473
+ shapeManager.deleteShape(slideIndex, name, slideManager)
2474
+ } catch (e) {
2475
+ logger.warn(`Failed to delete cell shape "${name}": ${e.message}`)
2476
+ }
2477
+ this.#cellShapeAnchors.delete(name)
2478
+ }
2479
+
2480
+ // Sort toReindex: first by newRowIndex, then by colIndex, then by shapeIndex (base index)
2481
+ toReindex.sort((a, b) => {
2482
+ if (a.newRowIndex !== b.newRowIndex) {
2483
+ return a.newRowIndex - b.newRowIndex
2484
+ }
2485
+ if (a.anchor.colIndex !== b.anchor.colIndex) {
2486
+ return a.anchor.colIndex - b.anchor.colIndex
2487
+ }
2488
+ const aBase =
2489
+ typeof a.anchor.shapeIndex === 'string'
2490
+ ? parseInt(a.anchor.shapeIndex.split('_')[0], 10)
2491
+ : a.anchor.shapeIndex
2492
+ const bBase =
2493
+ typeof b.anchor.shapeIndex === 'string'
2494
+ ? parseInt(b.anchor.shapeIndex.split('_')[0], 10)
2495
+ : b.anchor.shapeIndex
2496
+ return aBase - bBase
2497
+ })
2498
+
2499
+ // Phase 1: delete all old shapes to prevent collisions when re-adding
2500
+ for (const { name } of toReindex) {
2501
+ try {
2502
+ shapeManager.deleteShape(slideIndex, name, slideManager)
2503
+ } catch (e) {
2504
+ logger.warn(`Failed to delete cell shape "${name}" during reindex: ${e.message}`)
2505
+ }
2506
+ this.#cellShapeAnchors.delete(name)
2507
+ }
2508
+
2509
+ // Phase 2: re-add all shapes at their newRowIndex
2510
+ for (const { anchor, newRowIndex } of toReindex) {
2511
+ try {
2512
+ this.addCellShape(
2513
+ slideIndex,
2514
+ anchor.tableId,
2515
+ newRowIndex,
2516
+ anchor.colIndex,
2517
+ anchor.config,
2518
+ slideManager,
2519
+ shapeManager
2520
+ )
2521
+ } catch (e) {
2522
+ logger.warn(
2523
+ `Failed to re-add cell shape for (${newRowIndex}, ${anchor.colIndex}): ${e.message}`
2524
+ )
2525
+ }
2526
+ }
2527
+ }
2528
+
2529
+ /**
2530
+ * Repositions all registered cell shapes that fall within a table region
2531
+ * (e.g. after a merge or unmerge). Shapes in the region are deleted and
2532
+ * re-added targeting `(startRow, startCol)` so their coordinates are
2533
+ * recomputed against the merged cell's full bounding box.
2534
+ *
2535
+ * @private
2536
+ */
2537
+ #repositionCellShapesInRegion(
2538
+ slideIndex,
2539
+ tableId,
2540
+ resolvedTableId,
2541
+ startRow,
2542
+ startCol,
2543
+ endRow,
2544
+ endCol,
2545
+ slideManager,
2546
+ shapeManager
2547
+ ) {
2548
+ if (!shapeManager) return
2549
+
2550
+ const toReposition = []
2551
+
2552
+ for (const [name, anchor] of this.#cellShapeAnchors) {
2553
+ if (anchor.slideIndex !== slideIndex || anchor.resolvedTableId !== resolvedTableId) continue
2554
+ if (
2555
+ anchor.rowIndex >= startRow &&
2556
+ anchor.rowIndex <= endRow &&
2557
+ anchor.colIndex >= startCol &&
2558
+ anchor.colIndex <= endCol
2559
+ ) {
2560
+ toReposition.push({ name, anchor })
2561
+ }
2562
+ }
2563
+
2564
+ // Sort toReposition: first by original rowIndex, then by colIndex, then by shapeIndex (base index)
2565
+ toReposition.sort((a, b) => {
2566
+ if (a.anchor.rowIndex !== b.anchor.rowIndex) {
2567
+ return a.anchor.rowIndex - b.anchor.rowIndex
2568
+ }
2569
+ if (a.anchor.colIndex !== b.anchor.colIndex) {
2570
+ return a.anchor.colIndex - b.anchor.colIndex
2571
+ }
2572
+ const aBase =
2573
+ typeof a.anchor.shapeIndex === 'string'
2574
+ ? parseInt(a.anchor.shapeIndex.split('_')[0], 10)
2575
+ : a.anchor.shapeIndex
2576
+ const bBase =
2577
+ typeof b.anchor.shapeIndex === 'string'
2578
+ ? parseInt(b.anchor.shapeIndex.split('_')[0], 10)
2579
+ : b.anchor.shapeIndex
2580
+ return aBase - bBase
2581
+ })
2582
+
2583
+ // Phase 1: delete all old shapes first
2584
+ for (const { name } of toReposition) {
2585
+ try {
2586
+ shapeManager.deleteShape(slideIndex, name, slideManager)
2587
+ } catch (e) {
2588
+ logger.warn(`Failed to delete cell shape "${name}" during reposition: ${e.message}`)
2589
+ }
2590
+ this.#cellShapeAnchors.delete(name)
2591
+ }
2592
+
2593
+ // Phase 2: re-add all shapes targeting startRow, startCol
2594
+ for (const { anchor } of toReposition) {
2595
+ try {
2596
+ this.addCellShape(
2597
+ slideIndex,
2598
+ anchor.tableId,
2599
+ startRow,
2600
+ startCol,
2601
+ anchor.config,
2602
+ slideManager,
2603
+ shapeManager
2604
+ )
2605
+ } catch (e) {
2606
+ logger.warn(`Failed to reposition cell shape for (${startRow}, ${startCol}): ${e.message}`)
2607
+ }
2608
+ }
2609
+ }
2610
+
1999
2611
  /**
2000
2612
  * Generates a new rowId for the given row object.
2001
2613
  */