node-pptx-templater 1.0.1 → 1.0.3
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 +336 -281
- package/package.json +6 -6
- package/src/cli/commands/build.js +32 -31
- package/src/cli/commands/debug.js +25 -24
- package/src/cli/commands/extract.js +23 -21
- package/src/cli/commands/inspect.js +25 -23
- package/src/cli/commands/validate.js +19 -17
- package/src/cli/index.js +45 -43
- package/src/core/OutputWriter.js +81 -78
- package/src/core/PPTXTemplater.js +859 -274
- package/src/core/TemplateEngine.js +69 -71
- package/src/core/ValidationEngine.js +246 -0
- package/src/index.js +51 -15
- package/src/managers/ChartManager.js +197 -70
- package/src/managers/ContentTypesManager.js +51 -45
- package/src/managers/HyperlinkManager.js +148 -142
- package/src/managers/ImageManager.js +336 -0
- package/src/managers/MediaManager.js +64 -81
- package/src/managers/RelationshipManager.js +102 -96
- package/src/managers/ShapeManager.js +340 -0
- package/src/managers/SlideManager.js +410 -311
- package/src/managers/TableManager.js +981 -262
- package/src/managers/TextManager.js +197 -0
- package/src/managers/ZipManager.js +71 -69
- package/src/managers/charts/ChartCacheGenerator.js +77 -58
- package/src/managers/charts/ChartParser.js +11 -13
- package/src/managers/charts/ChartRelationshipManager.js +14 -10
- package/src/managers/charts/ChartWorkbookUpdater.js +61 -56
- package/src/parsers/XMLParser.js +50 -49
- package/src/templates/blankPptx.js +3 -1
- package/src/templates/slideTemplate.js +31 -32
- package/src/utils/contentTypesHelper.js +41 -53
- package/src/utils/errors.js +33 -23
- package/src/utils/idUtils.js +23 -15
- package/src/utils/logger.js +21 -15
- package/src/utils/relationshipUtils.js +28 -22
- package/src/utils/xmlUtils.js +37 -29
|
@@ -30,42 +30,29 @@
|
|
|
30
30
|
* </a:graphic>
|
|
31
31
|
* </p:graphicFrame>
|
|
32
32
|
*
|
|
33
|
-
* EMU (English Metric Units):
|
|
34
|
-
* - 1 inch = 914400 EMU
|
|
35
|
-
* - 1 pt = 12700 EMU
|
|
36
|
-
* - 1 cm = 360000 EMU
|
|
37
|
-
*
|
|
38
33
|
* Key challenge: Preserving cell formatting while replacing text.
|
|
39
34
|
* We ONLY modify the <a:t> text nodes, keeping all <a:tcPr>, <a:rPr>, etc.
|
|
40
|
-
*
|
|
41
|
-
* Merged cells use:
|
|
42
|
-
* - <a:tc gridSpan="2"> → horizontal merge (spans 2 columns)
|
|
43
|
-
* - <a:tc rowSpan="2"> → vertical merge (spans 2 rows)
|
|
44
|
-
* - <a:tc hMerge="1"> → continuation of horizontal merge
|
|
45
|
-
* - <a:tc vMerge="1"> → continuation of vertical merge
|
|
35
|
+
* And critical to avoid PPT corruption: update the val of <a16:rowId> in each cloned row's <a:extLst>.
|
|
46
36
|
*/
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
const { createLogger } = require('../utils/logger.js')
|
|
39
|
+
const { TableNotFoundError, PPTXError } = require('../utils/errors.js')
|
|
50
40
|
|
|
51
|
-
const logger = createLogger('TableManager')
|
|
41
|
+
const logger = createLogger('TableManager')
|
|
52
42
|
|
|
53
43
|
/**
|
|
54
44
|
* @class TableManager
|
|
55
|
-
* @description Handles table
|
|
56
|
-
*
|
|
57
|
-
* The key design principle is "preserve formatting, replace content".
|
|
58
|
-
* We never touch table styles, borders, or fonts — only the text.
|
|
45
|
+
* @description Handles table operations in PPTX templates.
|
|
59
46
|
*/
|
|
60
|
-
|
|
47
|
+
class TableManager {
|
|
61
48
|
/** @private @type {XMLParser} */
|
|
62
|
-
#xmlParser
|
|
49
|
+
#xmlParser
|
|
63
50
|
|
|
64
51
|
/**
|
|
65
52
|
* @param {XMLParser} xmlParser
|
|
66
53
|
*/
|
|
67
54
|
constructor(xmlParser) {
|
|
68
|
-
this.#xmlParser = xmlParser
|
|
55
|
+
this.#xmlParser = xmlParser
|
|
69
56
|
}
|
|
70
57
|
|
|
71
58
|
/**
|
|
@@ -74,343 +61,1075 @@ export class TableManager {
|
|
|
74
61
|
* Preserves all formatting properties.
|
|
75
62
|
*
|
|
76
63
|
* @param {number} slideIndex - 1-based slide index.
|
|
77
|
-
* @param {string} tableId - Table name
|
|
64
|
+
* @param {string} tableId - Table name or shape ID.
|
|
78
65
|
* @param {string[][]} rows - 2D array of cell values [row][col].
|
|
79
66
|
* @param {SlideManager} slideManager
|
|
80
67
|
* @throws {TableNotFoundError} If the table is not found.
|
|
81
68
|
*/
|
|
82
|
-
updateTable(slideIndex, tableId,
|
|
83
|
-
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
84
|
-
const
|
|
69
|
+
updateTable(slideIndex, tableId, data, slideManager) {
|
|
70
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
71
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
72
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
73
|
+
if (!tblObj) {
|
|
74
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
75
|
+
}
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
const trs = tblObj['a:tr'] || []
|
|
78
|
+
if (trs.length === 0) {
|
|
79
|
+
logger.warn('No rows found in table XML template')
|
|
80
|
+
return
|
|
88
81
|
}
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
let rowsData = []
|
|
84
|
+
let templateMerges = []
|
|
85
|
+
|
|
86
|
+
if (Array.isArray(data)) {
|
|
87
|
+
rowsData = data
|
|
88
|
+
} else if (data && typeof data === 'object') {
|
|
89
|
+
rowsData = data.rows || []
|
|
90
|
+
templateMerges = data.merge || []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const headerTemplate = trs[0]
|
|
94
|
+
const dataTemplate = trs[1] || trs[0]
|
|
95
|
+
|
|
96
|
+
const newRows = []
|
|
97
|
+
const generatedMerges = []
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < rowsData.length; i++) {
|
|
100
|
+
const template = i === 0 ? headerTemplate : trs[i] || dataTemplate
|
|
101
|
+
const newRow = this.#xmlParser.deepClone(template)
|
|
102
|
+
this.#updateRowId(newRow)
|
|
103
|
+
|
|
104
|
+
const tcs = newRow['a:tc'] || []
|
|
105
|
+
const rowData = rowsData[i]
|
|
106
|
+
|
|
107
|
+
for (let j = 0; j < tcs.length; j++) {
|
|
108
|
+
const rawCell = rowData && rowData[j] !== undefined ? rowData[j] : ''
|
|
109
|
+
let val = ''
|
|
110
|
+
let cellOptions = {}
|
|
111
|
+
|
|
112
|
+
if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
|
|
113
|
+
if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
|
|
114
|
+
if (tcs[j]['@_gridSpan']) delete tcs[j]['@_gridSpan']
|
|
115
|
+
if (tcs[j]['@_rowSpan']) delete tcs[j]['@_rowSpan']
|
|
116
|
+
|
|
117
|
+
if (rawCell && typeof rawCell === 'object') {
|
|
118
|
+
val = rawCell.value !== undefined ? rawCell.value : ''
|
|
119
|
+
const rowSpan = parseInt(rawCell.rowSpan || 1, 10)
|
|
120
|
+
const colSpan = parseInt(rawCell.colSpan || rawCell.gridSpan || 1, 10)
|
|
121
|
+
if (rowSpan > 1 || colSpan > 1) {
|
|
122
|
+
generatedMerges.push({
|
|
123
|
+
startRow: i,
|
|
124
|
+
startCol: j,
|
|
125
|
+
endRow: i + rowSpan - 1,
|
|
126
|
+
endCol: j + colSpan - 1,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
cellOptions = rawCell
|
|
130
|
+
} else {
|
|
131
|
+
val = String(rawCell)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.#setCellTextObj(tcs[j], val)
|
|
135
|
+
|
|
136
|
+
// Apply style properties if specified on the cell object
|
|
137
|
+
if (cellOptions.fill) {
|
|
138
|
+
if (!tcs[j]['a:tcPr']) tcs[j]['a:tcPr'] = {}
|
|
139
|
+
tcs[j]['a:tcPr']['a:solidFill'] = {
|
|
140
|
+
'a:srgbClr': { '@_val': cellOptions.fill },
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (cellOptions.align) {
|
|
145
|
+
const txBody = tcs[j]['a:txBody']
|
|
146
|
+
if (txBody && txBody['a:p']) {
|
|
147
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
148
|
+
for (const p of paras) {
|
|
149
|
+
if (!p['a:pPr']) p['a:pPr'] = {}
|
|
150
|
+
p['a:pPr']['@_algn'] = cellOptions.align
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (cellOptions.fontSize) {
|
|
156
|
+
const sizeVal = cellOptions.fontSize * 100
|
|
157
|
+
const txBody = tcs[j]['a:txBody']
|
|
158
|
+
if (txBody && txBody['a:p']) {
|
|
159
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
160
|
+
for (const p of paras) {
|
|
161
|
+
if (p['a:r']) {
|
|
162
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
163
|
+
for (const r of runs) {
|
|
164
|
+
if (!r['a:rPr']) r['a:rPr'] = {}
|
|
165
|
+
r['a:rPr']['@_sz'] = String(sizeVal)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
newRows.push(newRow)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tblObj['a:tr'] = newRows
|
|
176
|
+
|
|
177
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
178
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
179
|
+
|
|
180
|
+
const finalMerges = [...templateMerges, ...generatedMerges]
|
|
181
|
+
for (const merge of finalMerges) {
|
|
182
|
+
this.mergeCells(
|
|
183
|
+
slideIndex,
|
|
184
|
+
tableId,
|
|
185
|
+
merge.startRow,
|
|
186
|
+
merge.startCol,
|
|
187
|
+
merge.endRow,
|
|
188
|
+
merge.endCol,
|
|
189
|
+
slideManager
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
logger.debug(
|
|
194
|
+
`Updated table "${tableId}" with ${rowsData.length} rows and ${finalMerges.length} merges`
|
|
195
|
+
)
|
|
92
196
|
}
|
|
93
197
|
|
|
94
198
|
/**
|
|
95
|
-
*
|
|
96
|
-
* Returns null if the table was not found.
|
|
199
|
+
* Adds a row at the end of the table.
|
|
97
200
|
*
|
|
98
|
-
* @
|
|
99
|
-
* @param {string}
|
|
100
|
-
* @param {string}
|
|
101
|
-
* @param {
|
|
102
|
-
* @returns {string|null} Updated XML or null if not found.
|
|
201
|
+
* @param {number} slideIndex
|
|
202
|
+
* @param {string} tableId
|
|
203
|
+
* @param {string[]} rowData
|
|
204
|
+
* @param {SlideManager} slideManager
|
|
103
205
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Strategy 1: Find by name attribute
|
|
117
|
-
const namePattern = new RegExp(`name="${this.#escapeRegex(tableId)}"`, 'g');
|
|
118
|
-
const nameMatch = namePattern.exec(slideXml);
|
|
119
|
-
|
|
120
|
-
if (nameMatch) {
|
|
121
|
-
// Find the graphicFrame containing this name
|
|
122
|
-
const frameStart = slideXml.lastIndexOf('<p:graphicFrame>', nameMatch.index);
|
|
123
|
-
const frameEnd = this.#findClosingTag(slideXml, '</p:graphicFrame>', nameMatch.index);
|
|
124
|
-
|
|
125
|
-
if (frameStart !== -1 && frameEnd !== -1) {
|
|
126
|
-
const frameXml = slideXml.substring(frameStart, frameEnd + '</p:graphicFrame>'.length);
|
|
127
|
-
const updatedFrame = this.#updateTableRows(frameXml, rows);
|
|
128
|
-
|
|
129
|
-
updatedXml = slideXml.substring(0, frameStart) + updatedFrame +
|
|
130
|
-
slideXml.substring(frameEnd + '</p:graphicFrame>'.length);
|
|
131
|
-
found = true;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Strategy 2: Find by shape ID (tableId is numeric)
|
|
136
|
-
if (!found && /^\d+$/.test(tableId)) {
|
|
137
|
-
const idPattern = new RegExp(`id="${tableId}"`, 'g');
|
|
138
|
-
const idMatch = idPattern.exec(slideXml);
|
|
139
|
-
if (idMatch) {
|
|
140
|
-
const frameStart = slideXml.lastIndexOf('<p:graphicFrame>', idMatch.index);
|
|
141
|
-
const frameEnd = this.#findClosingTag(slideXml, '</p:graphicFrame>', idMatch.index);
|
|
142
|
-
|
|
143
|
-
if (frameStart !== -1 && frameEnd !== -1) {
|
|
144
|
-
const frameXml = slideXml.substring(frameStart, frameEnd + '</p:graphicFrame>'.length);
|
|
145
|
-
const updatedFrame = this.#updateTableRows(frameXml, rows);
|
|
146
|
-
updatedXml = slideXml.substring(0, frameStart) + updatedFrame +
|
|
147
|
-
slideXml.substring(frameEnd + '</p:graphicFrame>'.length);
|
|
148
|
-
found = true;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
206
|
+
addTableRow(slideIndex, tableId, rowData, slideManager) {
|
|
207
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
208
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
209
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
210
|
+
if (!tblObj) {
|
|
211
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const trs = tblObj['a:tr'] || []
|
|
215
|
+
if (trs.length === 0) {
|
|
216
|
+
throw new PPTXError('No rows to clone from')
|
|
151
217
|
}
|
|
152
218
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const frameStart = slideXml.lastIndexOf('<p:graphicFrame>', tableStart);
|
|
157
|
-
const frameEnd = this.#findClosingTag(slideXml, '</p:graphicFrame>', tableStart);
|
|
219
|
+
const lastRow = trs[trs.length - 1]
|
|
220
|
+
const newRow = this.#xmlParser.deepClone(lastRow)
|
|
221
|
+
this.#updateRowId(newRow)
|
|
158
222
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
223
|
+
const tcs = newRow['a:tc'] || []
|
|
224
|
+
for (let j = 0; j < tcs.length; j++) {
|
|
225
|
+
this.#setCellTextObj(tcs[j], rowData[j] !== undefined ? rowData[j] : '')
|
|
226
|
+
// Clear any merged indicators for the new row by default
|
|
227
|
+
if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
|
|
228
|
+
if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
|
|
166
229
|
}
|
|
167
230
|
|
|
168
|
-
|
|
231
|
+
trs.push(newRow)
|
|
232
|
+
|
|
233
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
234
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
169
235
|
}
|
|
170
236
|
|
|
171
237
|
/**
|
|
172
|
-
*
|
|
173
|
-
* Preserves the first row's styling as a template for new rows.
|
|
238
|
+
* Removes a table row by index.
|
|
174
239
|
*
|
|
175
|
-
* @
|
|
176
|
-
* @param {string}
|
|
177
|
-
* @param {
|
|
178
|
-
* @
|
|
240
|
+
* @param {number} slideIndex
|
|
241
|
+
* @param {string} tableId
|
|
242
|
+
* @param {number} rowIndex - 0-based row index.
|
|
243
|
+
* @param {SlideManager} slideManager
|
|
179
244
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
245
|
+
removeTableRow(slideIndex, tableId, rowIndex, slideManager) {
|
|
246
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
247
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
248
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
249
|
+
if (!tblObj) {
|
|
250
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
251
|
+
}
|
|
183
252
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
253
|
+
const trs = tblObj['a:tr'] || []
|
|
254
|
+
if (rowIndex < 0 || rowIndex >= trs.length) {
|
|
255
|
+
throw new PPTXError(`Row index ${rowIndex} out of bounds`)
|
|
187
256
|
}
|
|
188
257
|
|
|
189
|
-
|
|
190
|
-
const headerTemplate = existingRows[0];
|
|
191
|
-
const dataTemplate = existingRows[1] || existingRows[0];
|
|
258
|
+
trs.splice(rowIndex, 1)
|
|
192
259
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return this.#buildRow(template, rowData);
|
|
197
|
-
}).join('');
|
|
260
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
261
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
262
|
+
}
|
|
198
263
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Inserts a table row at a specific index.
|
|
266
|
+
*
|
|
267
|
+
* @param {number} slideIndex
|
|
268
|
+
* @param {string} tableId
|
|
269
|
+
* @param {number} rowIndex
|
|
270
|
+
* @param {string[]} rowData
|
|
271
|
+
* @param {SlideManager} slideManager
|
|
272
|
+
*/
|
|
273
|
+
insertTableRow(slideIndex, tableId, rowIndex, rowData, slideManager) {
|
|
274
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
275
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
276
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
277
|
+
if (!tblObj) {
|
|
278
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const trs = tblObj['a:tr'] || []
|
|
282
|
+
if (rowIndex < 0 || rowIndex > trs.length) {
|
|
283
|
+
throw new PPTXError(`Row index ${rowIndex} out of bounds`)
|
|
284
|
+
}
|
|
203
285
|
|
|
204
|
-
//
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
286
|
+
// Use row above or first row as template
|
|
287
|
+
const templateIndex = Math.max(0, rowIndex - 1)
|
|
288
|
+
const template = trs[templateIndex] || trs[0]
|
|
289
|
+
if (!template) {
|
|
290
|
+
throw new PPTXError('No rows to insert and copy from')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const newRow = this.#xmlParser.deepClone(template)
|
|
294
|
+
this.#updateRowId(newRow)
|
|
209
295
|
|
|
210
|
-
|
|
211
|
-
|
|
296
|
+
const tcs = newRow['a:tc'] || []
|
|
297
|
+
for (let j = 0; j < tcs.length; j++) {
|
|
298
|
+
this.#setCellTextObj(tcs[j], rowData[j] !== undefined ? rowData[j] : '')
|
|
299
|
+
if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
|
|
300
|
+
if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
|
|
212
301
|
}
|
|
213
302
|
|
|
214
|
-
|
|
215
|
-
tblXml.substring(0, firstRowStart) +
|
|
216
|
-
newRowsXml +
|
|
217
|
-
tblXml.substring(lastRowEnd);
|
|
303
|
+
trs.splice(rowIndex, 0, newRow)
|
|
218
304
|
|
|
219
|
-
|
|
305
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
306
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
220
307
|
}
|
|
221
308
|
|
|
222
309
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* @param {
|
|
226
|
-
* @
|
|
310
|
+
* Clones a row and inserts it at another index.
|
|
311
|
+
*
|
|
312
|
+
* @param {number} slideIndex
|
|
313
|
+
* @param {string} tableId
|
|
314
|
+
* @param {number} sourceRowIndex
|
|
315
|
+
* @param {number} targetRowIndex
|
|
316
|
+
* @param {SlideManager} slideManager
|
|
227
317
|
*/
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
318
|
+
cloneTableRow(slideIndex, tableId, sourceRowIndex, targetRowIndex, slideManager) {
|
|
319
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
320
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
321
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
322
|
+
if (!tblObj) {
|
|
323
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const trs = tblObj['a:tr'] || []
|
|
327
|
+
if (sourceRowIndex < 0 || sourceRowIndex >= trs.length) {
|
|
328
|
+
throw new PPTXError(`Source row index ${sourceRowIndex} out of bounds`)
|
|
329
|
+
}
|
|
330
|
+
if (targetRowIndex < 0 || targetRowIndex > trs.length) {
|
|
331
|
+
throw new PPTXError(`Target row index ${targetRowIndex} out of bounds`)
|
|
332
|
+
}
|
|
231
333
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
334
|
+
const template = trs[sourceRowIndex]
|
|
335
|
+
const newRow = this.#xmlParser.deepClone(template)
|
|
336
|
+
this.#updateRowId(newRow)
|
|
235
337
|
|
|
236
|
-
|
|
237
|
-
if (rowEnd === -1) break;
|
|
338
|
+
trs.splice(targetRowIndex, 0, newRow)
|
|
238
339
|
|
|
239
|
-
|
|
240
|
-
|
|
340
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
341
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Updates a single cell text and formatting.
|
|
346
|
+
*
|
|
347
|
+
* @param {number} slideIndex
|
|
348
|
+
* @param {string} tableId
|
|
349
|
+
* @param {number} rowIndex
|
|
350
|
+
* @param {number} colIndex
|
|
351
|
+
* @param {string} value
|
|
352
|
+
* @param {Object} options
|
|
353
|
+
* @param {SlideManager} slideManager
|
|
354
|
+
*/
|
|
355
|
+
updateCell(slideIndex, tableId, rowIndex, colIndex, value, options = {}, slideManager) {
|
|
356
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
357
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
358
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
359
|
+
if (!tblObj) {
|
|
360
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const row = tblObj['a:tr']?.[rowIndex]
|
|
364
|
+
if (!row) {
|
|
365
|
+
throw new PPTXError(`Row index ${rowIndex} out of bounds`)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const cell = row['a:tc']?.[colIndex]
|
|
369
|
+
if (!cell) {
|
|
370
|
+
throw new PPTXError(`Column index ${colIndex} out of bounds`)
|
|
241
371
|
}
|
|
242
372
|
|
|
243
|
-
|
|
373
|
+
this.#setCellTextObj(cell, value)
|
|
374
|
+
|
|
375
|
+
if (options.fill) {
|
|
376
|
+
if (!cell['a:tcPr']) cell['a:tcPr'] = {}
|
|
377
|
+
cell['a:tcPr']['a:solidFill'] = {
|
|
378
|
+
'a:srgbClr': { '@_val': options.fill },
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (options.align) {
|
|
383
|
+
const txBody = cell['a:txBody']
|
|
384
|
+
if (txBody && txBody['a:p']) {
|
|
385
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
386
|
+
for (const p of paras) {
|
|
387
|
+
if (!p['a:pPr']) p['a:pPr'] = {}
|
|
388
|
+
p['a:pPr']['@_algn'] = options.align
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (options.fontSize) {
|
|
394
|
+
const sizeVal = options.fontSize * 100
|
|
395
|
+
const txBody = cell['a:txBody']
|
|
396
|
+
if (txBody && txBody['a:p']) {
|
|
397
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
398
|
+
for (const p of paras) {
|
|
399
|
+
if (p['a:r']) {
|
|
400
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
401
|
+
for (const r of runs) {
|
|
402
|
+
if (!r['a:rPr']) r['a:rPr'] = {}
|
|
403
|
+
r['a:rPr']['@_sz'] = String(sizeVal)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
411
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
244
412
|
}
|
|
245
413
|
|
|
246
414
|
/**
|
|
247
|
-
*
|
|
415
|
+
* Validates if a merge region can be applied to the table.
|
|
248
416
|
*
|
|
249
|
-
* @
|
|
250
|
-
* @param {string}
|
|
251
|
-
* @param {
|
|
252
|
-
* @
|
|
417
|
+
* @param {number} slideIndex
|
|
418
|
+
* @param {string} tableId
|
|
419
|
+
* @param {number} startRow
|
|
420
|
+
* @param {number} startCol
|
|
421
|
+
* @param {number} endRow
|
|
422
|
+
* @param {number} endCol
|
|
423
|
+
* @param {SlideManager} slideManager
|
|
424
|
+
* @returns {Object} Validation report { valid: boolean, errors: string[] }
|
|
253
425
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
426
|
+
validateMergeRegion(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
|
|
427
|
+
const errors = []
|
|
428
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
429
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
430
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
431
|
+
if (!tblObj) {
|
|
432
|
+
errors.push(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
433
|
+
return { valid: false, errors }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const trs = tblObj['a:tr'] || []
|
|
437
|
+
const numRows = trs.length
|
|
438
|
+
const numCols = trs[0]?.['a:tc']?.length || 0
|
|
439
|
+
|
|
440
|
+
if (startRow < 0 || startRow >= numRows) {
|
|
441
|
+
errors.push(`startRow ${startRow} is out of bounds (table has ${numRows} rows)`)
|
|
442
|
+
}
|
|
443
|
+
if (endRow < 0 || endRow >= numRows) {
|
|
444
|
+
errors.push(`endRow ${endRow} is out of bounds (table has ${numRows} rows)`)
|
|
445
|
+
}
|
|
446
|
+
if (startCol < 0 || startCol >= numCols) {
|
|
447
|
+
errors.push(`startCol ${startCol} is out of bounds (table has ${numCols} cols)`)
|
|
448
|
+
}
|
|
449
|
+
if (endCol < 0 || endCol >= numCols) {
|
|
450
|
+
errors.push(`endCol ${endCol} is out of bounds (table has ${numCols} cols)`)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (errors.length > 0) {
|
|
454
|
+
return { valid: false, errors }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (endRow < startRow) {
|
|
458
|
+
errors.push(`endRow (${endRow}) cannot be less than startRow (${startRow})`)
|
|
459
|
+
}
|
|
460
|
+
if (endCol < startCol) {
|
|
461
|
+
errors.push(`endCol (${endCol}) cannot be less than startCol (${startCol})`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (errors.length > 0) {
|
|
465
|
+
return { valid: false, errors }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const existingMerges = this.getMergedCells(slideIndex, tableId, slideManager)
|
|
469
|
+
for (const R of existingMerges) {
|
|
470
|
+
const overlap =
|
|
471
|
+
startRow <= R.endRow && endRow >= R.startRow && startCol <= R.endCol && endCol >= R.startCol
|
|
472
|
+
if (overlap) {
|
|
473
|
+
errors.push(
|
|
474
|
+
`Requested merge region (${startRow}, ${startCol}) to (${endRow}, ${endCol}) overlaps with existing merged region (${R.startRow}, ${R.startCol}) to (${R.endRow}, ${R.endCol})`
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
valid: errors.length === 0,
|
|
481
|
+
errors,
|
|
482
|
+
}
|
|
279
483
|
}
|
|
280
484
|
|
|
281
485
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* @param {
|
|
285
|
-
* @
|
|
486
|
+
* Merges cells in a table.
|
|
487
|
+
*
|
|
488
|
+
* @param {number} slideIndex
|
|
489
|
+
* @param {string} tableId
|
|
490
|
+
* @param {number} startRow
|
|
491
|
+
* @param {number} startCol
|
|
492
|
+
* @param {number} endRow
|
|
493
|
+
* @param {number} endCol
|
|
494
|
+
* @param {SlideManager} slideManager
|
|
286
495
|
*/
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
496
|
+
mergeCells(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
|
|
497
|
+
const validation = this.validateMergeRegion(
|
|
498
|
+
slideIndex,
|
|
499
|
+
tableId,
|
|
500
|
+
startRow,
|
|
501
|
+
startCol,
|
|
502
|
+
endRow,
|
|
503
|
+
endCol,
|
|
504
|
+
slideManager
|
|
505
|
+
)
|
|
506
|
+
if (!validation.valid) {
|
|
507
|
+
throw new PPTXError(`Invalid merge region: ${validation.errors.join('; ')}`)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
511
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
512
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
513
|
+
const trs = tblObj['a:tr'] || []
|
|
514
|
+
|
|
515
|
+
const allTexts = []
|
|
516
|
+
|
|
517
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
518
|
+
const row = trs[r]
|
|
519
|
+
if (!row) continue
|
|
520
|
+
const tcs = row['a:tc'] || []
|
|
290
521
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
522
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
523
|
+
const cell = tcs[c]
|
|
524
|
+
if (!cell) continue
|
|
525
|
+
|
|
526
|
+
if (r === startRow && c === startCol) {
|
|
527
|
+
const text = this.#getCellText(cell)
|
|
528
|
+
if (text) allTexts.push(text)
|
|
529
|
+
if (cell['@_hMerge'] !== undefined) delete cell['@_hMerge']
|
|
530
|
+
if (cell['@_vMerge'] !== undefined) delete cell['@_vMerge']
|
|
531
|
+
} else {
|
|
532
|
+
const text = this.#getCellText(cell)
|
|
533
|
+
if (text) allTexts.push(text)
|
|
534
|
+
|
|
535
|
+
if (cell['@_gridSpan'] !== undefined) delete cell['@_gridSpan']
|
|
536
|
+
if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
|
|
537
|
+
|
|
538
|
+
if (r === startRow) {
|
|
539
|
+
cell['@_hMerge'] = '1'
|
|
540
|
+
if (cell['@_vMerge'] !== undefined) delete cell['@_vMerge']
|
|
541
|
+
} else if (c === startCol) {
|
|
542
|
+
cell['@_vMerge'] = '1'
|
|
543
|
+
if (cell['@_hMerge'] !== undefined) delete cell['@_hMerge']
|
|
544
|
+
} else {
|
|
545
|
+
cell['@_hMerge'] = '1'
|
|
546
|
+
cell['@_vMerge'] = '1'
|
|
547
|
+
}
|
|
548
|
+
this.#setCellTextObj(cell, '')
|
|
549
|
+
}
|
|
295
550
|
}
|
|
296
|
-
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const originCell = trs[startRow]['a:tc'][startCol]
|
|
554
|
+
if (endCol > startCol) {
|
|
555
|
+
originCell['@_gridSpan'] = String(endCol - startCol + 1)
|
|
556
|
+
}
|
|
557
|
+
if (endRow > startRow) {
|
|
558
|
+
originCell['@_rowSpan'] = String(endRow - startRow + 1)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const combinedText = allTexts.filter(t => t.trim() !== '').join('\n')
|
|
562
|
+
this.#setCellTextObj(originCell, combinedText)
|
|
563
|
+
|
|
564
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
565
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Unmerges cells in a table.
|
|
570
|
+
*
|
|
571
|
+
* @param {number} slideIndex
|
|
572
|
+
* @param {string} tableId
|
|
573
|
+
* @param {number} startRow
|
|
574
|
+
* @param {number} startCol
|
|
575
|
+
* @param {number} endRow
|
|
576
|
+
* @param {number} endCol
|
|
577
|
+
* @param {SlideManager} slideManager
|
|
578
|
+
*/
|
|
579
|
+
unmergeCells(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
|
|
580
|
+
let actualSlideManager = slideManager
|
|
581
|
+
let actualEndRow = endRow
|
|
582
|
+
let actualEndCol = endCol
|
|
297
583
|
|
|
298
|
-
|
|
299
|
-
|
|
584
|
+
if (typeof endRow === 'object' && endRow.getSlideXml) {
|
|
585
|
+
actualSlideManager = endRow
|
|
586
|
+
actualEndRow = undefined
|
|
587
|
+
actualEndCol = undefined
|
|
588
|
+
}
|
|
300
589
|
|
|
301
|
-
|
|
302
|
-
|
|
590
|
+
const slideXml = actualSlideManager.getSlideXml(slideIndex)
|
|
591
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
592
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
593
|
+
if (!tblObj) {
|
|
594
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
303
595
|
}
|
|
304
596
|
|
|
305
|
-
|
|
597
|
+
const trs = tblObj['a:tr'] || []
|
|
598
|
+
|
|
599
|
+
if (actualEndRow === undefined || actualEndCol === undefined) {
|
|
600
|
+
const R = this.getMergeRegion(slideIndex, tableId, startRow, startCol, actualSlideManager)
|
|
601
|
+
if (!R) return
|
|
602
|
+
|
|
603
|
+
for (let r = R.startRow; r <= R.endRow; r++) {
|
|
604
|
+
const rowObj = trs[r]
|
|
605
|
+
if (!rowObj) continue
|
|
606
|
+
const tcs = rowObj['a:tc'] || []
|
|
607
|
+
for (let c = R.startCol; c <= R.endCol; c++) {
|
|
608
|
+
const cell = tcs[c]
|
|
609
|
+
if (!cell) continue
|
|
610
|
+
if (cell['@_hMerge'] !== undefined) delete cell['@_hMerge']
|
|
611
|
+
if (cell['@_vMerge'] !== undefined) delete cell['@_vMerge']
|
|
612
|
+
if (cell['@_gridSpan'] !== undefined) delete cell['@_gridSpan']
|
|
613
|
+
if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
for (let r = startRow; r <= actualEndRow; r++) {
|
|
618
|
+
const rowObj = trs[r]
|
|
619
|
+
if (!rowObj) continue
|
|
620
|
+
const tcs = rowObj['a:tc'] || []
|
|
621
|
+
for (let c = startCol; c <= actualEndCol; c++) {
|
|
622
|
+
const cell = tcs[c]
|
|
623
|
+
if (!cell) continue
|
|
624
|
+
if (cell['@_hMerge'] !== undefined) delete cell['@_hMerge']
|
|
625
|
+
if (cell['@_vMerge'] !== undefined) delete cell['@_vMerge']
|
|
626
|
+
if (cell['@_gridSpan'] !== undefined) delete cell['@_gridSpan']
|
|
627
|
+
if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
633
|
+
actualSlideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
306
634
|
}
|
|
307
635
|
|
|
308
636
|
/**
|
|
309
|
-
*
|
|
310
|
-
* Preserves all formatting (borders, colors, fonts) and only changes <a:t> content.
|
|
637
|
+
* Scans the table grid and returns all merged regions.
|
|
311
638
|
*
|
|
312
|
-
* @
|
|
313
|
-
* @param {string}
|
|
314
|
-
* @param {
|
|
315
|
-
* @returns {
|
|
639
|
+
* @param {number} slideIndex
|
|
640
|
+
* @param {string} tableId
|
|
641
|
+
* @param {SlideManager} slideManager
|
|
642
|
+
* @returns {Array<Object>} List of merged region coordinates
|
|
316
643
|
*/
|
|
317
|
-
|
|
318
|
-
const
|
|
644
|
+
getMergedCells(slideIndex, tableId, slideManager) {
|
|
645
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
646
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
647
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
648
|
+
if (!tblObj) {
|
|
649
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
650
|
+
}
|
|
319
651
|
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
652
|
+
const trs = tblObj['a:tr'] || []
|
|
653
|
+
const merged = []
|
|
654
|
+
|
|
655
|
+
for (let r = 0; r < trs.length; r++) {
|
|
656
|
+
const row = trs[r]
|
|
657
|
+
const tcs = row['a:tc'] || []
|
|
658
|
+
for (let c = 0; c < tcs.length; c++) {
|
|
659
|
+
const cell = tcs[c]
|
|
660
|
+
if (!cell) continue
|
|
661
|
+
|
|
662
|
+
const gridSpan = parseInt(cell['@_gridSpan'] || 1, 10)
|
|
663
|
+
const rowSpan = parseInt(cell['@_rowSpan'] || 1, 10)
|
|
664
|
+
|
|
665
|
+
if (gridSpan > 1 || rowSpan > 1) {
|
|
666
|
+
merged.push({
|
|
667
|
+
startRow: r,
|
|
668
|
+
startCol: c,
|
|
669
|
+
endRow: r + rowSpan - 1,
|
|
670
|
+
endCol: c + gridSpan - 1,
|
|
671
|
+
})
|
|
672
|
+
}
|
|
673
|
+
}
|
|
325
674
|
}
|
|
675
|
+
return merged
|
|
676
|
+
}
|
|
326
677
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
678
|
+
/**
|
|
679
|
+
* Helper to get clean string content of a cell.
|
|
680
|
+
*/
|
|
681
|
+
#getCellText(cellObj) {
|
|
682
|
+
const txBody = cellObj?.['a:txBody']
|
|
683
|
+
if (!txBody) return ''
|
|
684
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
685
|
+
const text = []
|
|
686
|
+
for (const p of paras) {
|
|
687
|
+
let pText = ''
|
|
688
|
+
if (p['a:r']) {
|
|
689
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
690
|
+
for (const r of runs) {
|
|
691
|
+
if (r['a:t']) {
|
|
692
|
+
pText += String(r['a:t'])
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
text.push(pText)
|
|
331
697
|
}
|
|
698
|
+
return text.join('\n')
|
|
699
|
+
}
|
|
332
700
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Returns true if the cell is part of any merged region.
|
|
703
|
+
*/
|
|
704
|
+
isMergedCell(slideIndex, tableId, row, col, slideManager) {
|
|
705
|
+
return this.getMergeRegion(slideIndex, tableId, row, col, slideManager) !== null
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Returns the region coordinate containing cell (row, col).
|
|
710
|
+
*/
|
|
711
|
+
getMergeRegion(slideIndex, tableId, row, col, slideManager) {
|
|
712
|
+
const merges = this.getMergedCells(slideIndex, tableId, slideManager)
|
|
713
|
+
for (const R of merges) {
|
|
714
|
+
if (row >= R.startRow && row <= R.endRow && col >= R.startCol && col <= R.endCol) {
|
|
715
|
+
return R
|
|
716
|
+
}
|
|
340
717
|
}
|
|
718
|
+
return null
|
|
719
|
+
}
|
|
341
720
|
|
|
342
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Returns origin cell coordinates.
|
|
723
|
+
*/
|
|
724
|
+
getMergeParent(slideIndex, tableId, row, col, slideManager) {
|
|
725
|
+
const R = this.getMergeRegion(slideIndex, tableId, row, col, slideManager)
|
|
726
|
+
if (R) {
|
|
727
|
+
return { row: R.startRow, col: R.startCol }
|
|
728
|
+
}
|
|
729
|
+
return { row, col }
|
|
343
730
|
}
|
|
344
731
|
|
|
345
732
|
/**
|
|
346
|
-
*
|
|
347
|
-
* @private
|
|
348
|
-
* @param {string} xml
|
|
349
|
-
* @param {string} closingTag
|
|
350
|
-
* @param {number} searchFrom
|
|
351
|
-
* @returns {number} Index of closing tag or -1.
|
|
733
|
+
* Splits a merged region containing cell (row, col).
|
|
352
734
|
*/
|
|
353
|
-
|
|
354
|
-
|
|
735
|
+
splitMergedRegion(slideIndex, tableId, row, col, slideManager) {
|
|
736
|
+
this.unmergeCells(slideIndex, tableId, row, col, slideManager)
|
|
355
737
|
}
|
|
356
738
|
|
|
357
739
|
/**
|
|
358
|
-
*
|
|
359
|
-
* @private
|
|
360
|
-
* @param {string} str
|
|
361
|
-
* @returns {string}
|
|
740
|
+
* Clones a merged region.
|
|
362
741
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
742
|
+
cloneMergedRegion(slideIndex, tableId, row, col, targetRow, targetCol, slideManager) {
|
|
743
|
+
const R = this.getMergeRegion(slideIndex, tableId, row, col, slideManager)
|
|
744
|
+
if (!R) return
|
|
745
|
+
|
|
746
|
+
const rowSpan = R.endRow - R.startRow + 1
|
|
747
|
+
const colSpan = R.endCol - R.startCol + 1
|
|
748
|
+
|
|
749
|
+
const targetEndRow = targetRow + rowSpan - 1
|
|
750
|
+
const targetEndCol = targetCol + colSpan - 1
|
|
751
|
+
|
|
752
|
+
this.mergeCells(
|
|
753
|
+
slideIndex,
|
|
754
|
+
tableId,
|
|
755
|
+
targetRow,
|
|
756
|
+
targetCol,
|
|
757
|
+
targetEndRow,
|
|
758
|
+
targetEndCol,
|
|
759
|
+
slideManager
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
763
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
764
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
765
|
+
if (!tblObj) return
|
|
766
|
+
|
|
767
|
+
const trs = tblObj['a:tr'] || []
|
|
768
|
+
const srcCell = trs[R.startRow]?.['a:tc']?.[R.startCol]
|
|
769
|
+
const destCell = trs[targetRow]?.['a:tc']?.[targetCol]
|
|
770
|
+
|
|
771
|
+
if (srcCell && destCell) {
|
|
772
|
+
if (srcCell['a:tcPr']) {
|
|
773
|
+
destCell['a:tcPr'] = this.#xmlParser.deepClone(srcCell['a:tcPr'])
|
|
774
|
+
}
|
|
775
|
+
if (srcCell['a:txBody']) {
|
|
776
|
+
destCell['a:txBody'] = this.#xmlParser.deepClone(srcCell['a:txBody'])
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
781
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
365
782
|
}
|
|
366
783
|
|
|
367
784
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* @param {
|
|
371
|
-
* @
|
|
785
|
+
* Auto-fits table columns based on text length.
|
|
786
|
+
*
|
|
787
|
+
* @param {number} slideIndex
|
|
788
|
+
* @param {string} tableId
|
|
789
|
+
* @param {SlideManager} slideManager
|
|
372
790
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
791
|
+
autoFitTable(slideIndex, tableId, slideManager) {
|
|
792
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
793
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
794
|
+
const tblObj = this.#findTableObj(slideObj, tableId)
|
|
795
|
+
if (!tblObj) {
|
|
796
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const trs = tblObj['a:tr'] || []
|
|
800
|
+
const gridCols = tblObj['a:tblGrid']?.['a:gridCol']
|
|
801
|
+
if (!gridCols || trs.length === 0) return
|
|
802
|
+
|
|
803
|
+
const numCols = gridCols.length
|
|
804
|
+
const maxLens = new Array(numCols).fill(0)
|
|
805
|
+
|
|
806
|
+
for (const row of trs) {
|
|
807
|
+
const tcs = row['a:tc'] || []
|
|
808
|
+
for (let c = 0; c < numCols; c++) {
|
|
809
|
+
const cell = tcs[c]
|
|
810
|
+
if (!cell || cell['@_hMerge'] || cell['@_vMerge']) continue
|
|
811
|
+
|
|
812
|
+
const txBody = cell['a:txBody']
|
|
813
|
+
if (txBody) {
|
|
814
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
815
|
+
let len = 0
|
|
816
|
+
for (const p of paras) {
|
|
817
|
+
if (p['a:r']) {
|
|
818
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
819
|
+
for (const r of runs) {
|
|
820
|
+
if (r['a:t']) {
|
|
821
|
+
len += String(r['a:t']).length
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (len > maxLens[c]) {
|
|
827
|
+
maxLens[c] = len
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
for (let c = 0; c < numCols; c++) {
|
|
834
|
+
const width = Math.max(1000000, maxLens[c] * 120000)
|
|
835
|
+
gridCols[c]['@_w'] = String(width)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
839
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
380
840
|
}
|
|
381
841
|
|
|
382
842
|
/**
|
|
383
|
-
*
|
|
843
|
+
* Resizes the table width and height.
|
|
844
|
+
*
|
|
845
|
+
* @param {number} slideIndex
|
|
846
|
+
* @param {string} tableId
|
|
847
|
+
* @param {number} width - New width in EMUs or inches.
|
|
848
|
+
* @param {number} height - New height in EMUs or inches.
|
|
849
|
+
* @param {SlideManager} slideManager
|
|
850
|
+
*/
|
|
851
|
+
resizeTable(slideIndex, tableId, width, height, slideManager) {
|
|
852
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
853
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
854
|
+
|
|
855
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
856
|
+
if (!spTree) return
|
|
857
|
+
|
|
858
|
+
let frames = spTree['p:graphicFrame'] || []
|
|
859
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
860
|
+
|
|
861
|
+
let targetFrame = null
|
|
862
|
+
let tbl = null
|
|
863
|
+
|
|
864
|
+
for (const frame of frames) {
|
|
865
|
+
tbl = frame?.['a:graphic']?.['a:graphicData']?.['a:tbl']
|
|
866
|
+
if (!tbl) continue
|
|
867
|
+
|
|
868
|
+
const cNvPr = frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
|
|
869
|
+
if (!cNvPr) continue
|
|
870
|
+
|
|
871
|
+
const name = cNvPr['@_name']
|
|
872
|
+
const id = String(cNvPr['@_id'])
|
|
873
|
+
|
|
874
|
+
if (tableId === 'first' || name === tableId || id === tableId) {
|
|
875
|
+
targetFrame = frame
|
|
876
|
+
break
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (!targetFrame || !tbl) {
|
|
881
|
+
throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const emuWidth = width < 100 ? Math.round(width * 914400) : Math.round(width)
|
|
885
|
+
const emuHeight = height < 100 ? Math.round(height * 914400) : Math.round(height)
|
|
886
|
+
|
|
887
|
+
if (!targetFrame['p:xfrm']) targetFrame['p:xfrm'] = {}
|
|
888
|
+
if (!targetFrame['p:xfrm']['a:ext']) targetFrame['p:xfrm']['a:ext'] = {}
|
|
889
|
+
targetFrame['p:xfrm']['a:ext']['@_cx'] = String(emuWidth)
|
|
890
|
+
targetFrame['p:xfrm']['a:ext']['@_cy'] = String(emuHeight)
|
|
891
|
+
|
|
892
|
+
const gridCols = tbl['a:tblGrid']?.['a:gridCol']
|
|
893
|
+
if (gridCols && gridCols.length > 0) {
|
|
894
|
+
let currentTotalWidth = 0
|
|
895
|
+
for (const col of gridCols) {
|
|
896
|
+
currentTotalWidth += parseInt(col['@_w'] || 0, 10)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (currentTotalWidth > 0) {
|
|
900
|
+
const ratio = emuWidth / currentTotalWidth
|
|
901
|
+
for (const col of gridCols) {
|
|
902
|
+
const w = parseInt(col['@_w'] || 0, 10)
|
|
903
|
+
col['@_w'] = String(Math.round(w * ratio))
|
|
904
|
+
}
|
|
905
|
+
} else {
|
|
906
|
+
const evenWidth = Math.round(emuWidth / gridCols.length)
|
|
907
|
+
for (const col of gridCols) {
|
|
908
|
+
col['@_w'] = String(evenWidth)
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const trs = tbl['a:tr'] || []
|
|
914
|
+
if (trs.length > 0) {
|
|
915
|
+
let currentTotalHeight = 0
|
|
916
|
+
for (const row of trs) {
|
|
917
|
+
currentTotalHeight += parseInt(row['@_h'] || 0, 10)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (currentTotalHeight > 0) {
|
|
921
|
+
const ratio = emuHeight / currentTotalHeight
|
|
922
|
+
for (const row of trs) {
|
|
923
|
+
const h = parseInt(row['@_h'] || 0, 10)
|
|
924
|
+
row['@_h'] = String(Math.round(h * ratio))
|
|
925
|
+
}
|
|
926
|
+
} else {
|
|
927
|
+
const evenHeight = Math.round(emuHeight / trs.length)
|
|
928
|
+
for (const row of trs) {
|
|
929
|
+
row['@_h'] = String(evenHeight)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
935
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Inspects all tables in a slide.
|
|
384
940
|
*
|
|
385
941
|
* @param {number} slideIndex
|
|
386
942
|
* @param {SlideManager} slideManager
|
|
387
943
|
* @returns {Array<{name: string, id: string, rows: number, cols: number}>}
|
|
388
944
|
*/
|
|
389
945
|
inspectTables(slideIndex, slideManager) {
|
|
390
|
-
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
391
|
-
const
|
|
946
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
947
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
948
|
+
const tables = []
|
|
949
|
+
|
|
950
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
951
|
+
if (!spTree) return []
|
|
392
952
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
let match;
|
|
953
|
+
let frames = spTree['p:graphicFrame'] || []
|
|
954
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
396
955
|
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
if (!
|
|
956
|
+
for (const frame of frames) {
|
|
957
|
+
const tbl = frame?.['a:graphic']?.['a:graphicData']?.['a:tbl']
|
|
958
|
+
if (!tbl) continue
|
|
400
959
|
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
|
|
960
|
+
const cNvPr = frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
|
|
961
|
+
const name = cNvPr ? cNvPr['@_name'] : 'unnamed'
|
|
962
|
+
const id = cNvPr ? String(cNvPr['@_id']) : 'unknown'
|
|
963
|
+
|
|
964
|
+
const trs = tbl['a:tr'] || []
|
|
965
|
+
const cols = trs[0]?.['a:tc']?.length || 0
|
|
405
966
|
|
|
406
967
|
tables.push({
|
|
407
|
-
name
|
|
408
|
-
id
|
|
409
|
-
rows:
|
|
968
|
+
name,
|
|
969
|
+
id,
|
|
970
|
+
rows: trs.length,
|
|
410
971
|
cols,
|
|
411
|
-
})
|
|
972
|
+
})
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return tables
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Helper to set cell text in parsed object structure.
|
|
980
|
+
*/
|
|
981
|
+
#setCellTextObj(cellObj, text) {
|
|
982
|
+
const val = text === undefined || text === null ? '' : String(text)
|
|
983
|
+
|
|
984
|
+
if (!cellObj['a:txBody']) {
|
|
985
|
+
cellObj['a:txBody'] = {
|
|
986
|
+
'a:bodyPr': {},
|
|
987
|
+
'a:lstStyle': {},
|
|
988
|
+
'a:p': [],
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const txBody = cellObj['a:txBody']
|
|
993
|
+
|
|
994
|
+
if (!txBody['a:p']) {
|
|
995
|
+
txBody['a:p'] = []
|
|
996
|
+
}
|
|
997
|
+
if (!Array.isArray(txBody['a:p'])) {
|
|
998
|
+
txBody['a:p'] = [txBody['a:p']]
|
|
999
|
+
}
|
|
1000
|
+
if (txBody['a:p'].length === 0) {
|
|
1001
|
+
txBody['a:p'].push({})
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const lines = val.split(/\r?\n/)
|
|
1005
|
+
const templatePara = txBody['a:p'][0]
|
|
1006
|
+
const newParas = []
|
|
1007
|
+
|
|
1008
|
+
for (const line of lines) {
|
|
1009
|
+
const p = this.#xmlParser.deepClone(templatePara)
|
|
1010
|
+
|
|
1011
|
+
if (!p['a:r']) {
|
|
1012
|
+
p['a:r'] = []
|
|
1013
|
+
}
|
|
1014
|
+
if (!Array.isArray(p['a:r'])) {
|
|
1015
|
+
p['a:r'] = [p['a:r']]
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (p['a:r'].length === 0) {
|
|
1019
|
+
p['a:r'].push({ 'a:t': line })
|
|
1020
|
+
} else {
|
|
1021
|
+
const firstRun = p['a:r'][0]
|
|
1022
|
+
firstRun['a:t'] = line
|
|
1023
|
+
p['a:r'] = [firstRun]
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Ensure strict schema ordering for DrawingML paragraph elements:
|
|
1027
|
+
// 1. a:pPr (paragraph properties)
|
|
1028
|
+
// 2. a:r / a:br / a:fld (text runs, breaks, fields)
|
|
1029
|
+
// 3. a:endParaRPr (end paragraph run properties)
|
|
1030
|
+
const pOrdered = {}
|
|
1031
|
+
if (p['a:pPr'] !== undefined) {
|
|
1032
|
+
pOrdered['a:pPr'] = p['a:pPr']
|
|
1033
|
+
}
|
|
1034
|
+
if (p['a:r'] !== undefined) {
|
|
1035
|
+
pOrdered['a:r'] = p['a:r']
|
|
1036
|
+
}
|
|
1037
|
+
if (p['a:br'] !== undefined) {
|
|
1038
|
+
pOrdered['a:br'] = p['a:br']
|
|
1039
|
+
}
|
|
1040
|
+
if (p['a:fld'] !== undefined) {
|
|
1041
|
+
pOrdered['a:fld'] = p['a:fld']
|
|
1042
|
+
}
|
|
1043
|
+
for (const key of Object.keys(p)) {
|
|
1044
|
+
if (
|
|
1045
|
+
key !== 'a:pPr' &&
|
|
1046
|
+
key !== 'a:r' &&
|
|
1047
|
+
key !== 'a:br' &&
|
|
1048
|
+
key !== 'a:fld' &&
|
|
1049
|
+
key !== 'a:endParaRPr'
|
|
1050
|
+
) {
|
|
1051
|
+
pOrdered[key] = p[key]
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (p['a:endParaRPr'] !== undefined) {
|
|
1055
|
+
pOrdered['a:endParaRPr'] = p['a:endParaRPr']
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
newParas.push(pOrdered)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
txBody['a:p'] = newParas
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Helper to find a table element inside a slide parsed object.
|
|
1066
|
+
*/
|
|
1067
|
+
#findTableObj(slideObj, tableId) {
|
|
1068
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
1069
|
+
if (!spTree) return null
|
|
1070
|
+
|
|
1071
|
+
let frames = spTree['p:graphicFrame'] || []
|
|
1072
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
1073
|
+
|
|
1074
|
+
for (const frame of frames) {
|
|
1075
|
+
const tbl = frame?.['a:graphic']?.['a:graphicData']?.['a:tbl']
|
|
1076
|
+
if (!tbl) continue
|
|
1077
|
+
|
|
1078
|
+
const cNvPr = frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
|
|
1079
|
+
if (!cNvPr) continue
|
|
1080
|
+
|
|
1081
|
+
const name = cNvPr['@_name']
|
|
1082
|
+
const id = String(cNvPr['@_id'])
|
|
1083
|
+
|
|
1084
|
+
if (tableId === 'first' || name === tableId || id === tableId) {
|
|
1085
|
+
return tbl
|
|
1086
|
+
}
|
|
412
1087
|
}
|
|
413
1088
|
|
|
414
|
-
return
|
|
1089
|
+
return null
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Generates a new rowId for the given row object.
|
|
1094
|
+
*/
|
|
1095
|
+
#updateRowId(rowObj) {
|
|
1096
|
+
const randomVal = String(this.#generateRandomUint32())
|
|
1097
|
+
if (rowObj['a:extLst']?.['a:ext']) {
|
|
1098
|
+
const ext = rowObj['a:extLst']['a:ext']
|
|
1099
|
+
const exts = Array.isArray(ext) ? ext : [ext]
|
|
1100
|
+
let updated = false
|
|
1101
|
+
for (const e of exts) {
|
|
1102
|
+
if (e['a16:rowId']) {
|
|
1103
|
+
e['a16:rowId']['@_val'] = randomVal
|
|
1104
|
+
updated = true
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (!updated) {
|
|
1108
|
+
exts.push({
|
|
1109
|
+
'@_uri': '{0D108BD9-81ED-4DB2-BD59-A6C34878D82A}',
|
|
1110
|
+
'a16:rowId': {
|
|
1111
|
+
'@_xmlns:a16': 'http://schemas.microsoft.com/office/drawing/2014/main',
|
|
1112
|
+
'@_val': randomVal,
|
|
1113
|
+
},
|
|
1114
|
+
})
|
|
1115
|
+
rowObj['a:extLst']['a:ext'] = exts
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
rowObj['a:extLst'] = {
|
|
1119
|
+
'a:ext': {
|
|
1120
|
+
'@_uri': '{0D108BD9-81ED-4DB2-BD59-A6C34878D82A}',
|
|
1121
|
+
'a16:rowId': {
|
|
1122
|
+
'@_xmlns:a16': 'http://schemas.microsoft.com/office/drawing/2014/main',
|
|
1123
|
+
'@_val': randomVal,
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
#generateRandomUint32() {
|
|
1131
|
+
return Math.floor(Math.random() * 4294967296)
|
|
415
1132
|
}
|
|
416
1133
|
}
|
|
1134
|
+
|
|
1135
|
+
module.exports = { TableManager }
|