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