node-pptx-templater 1.0.6 → 1.0.8
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 +258 -2
- package/package.json +1 -1
- package/src/core/PPTXTemplater.js +92 -1
- package/src/core/TemplateEngine.js +126 -0
- package/src/core/ValidationEngine.js +179 -0
- package/src/managers/ChartManager.js +383 -21
- package/src/managers/TextManager.js +271 -0
- package/src/managers/charts/ChartCacheGenerator.js +427 -1
- package/src/managers/charts/ChartWorkbookUpdater.js +204 -33
|
@@ -20,14 +20,22 @@ class ChartWorkbookUpdater {
|
|
|
20
20
|
// Look for sheet1.xml
|
|
21
21
|
const sheetPath = 'xl/worksheets/sheet1.xml'
|
|
22
22
|
if (!zip.file(sheetPath)) {
|
|
23
|
-
logger.warn('sheet1.xml not found in embedded workbook
|
|
24
|
-
|
|
23
|
+
logger.warn('sheet1.xml not found in embedded workbook')
|
|
24
|
+
return workbookData
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const sheetXml = await zip.file(sheetPath).async('text')
|
|
28
|
+
const sharedStrings = await this.getSharedStrings(zip)
|
|
29
|
+
const cells = this.parseWorksheetCells(sheetXml, sharedStrings)
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
+
// Update categories, series values, and custom labels in the cell grid
|
|
32
|
+
this.#updateCellGrid(cells, data)
|
|
33
|
+
|
|
34
|
+
// Serialize cells to sheet XML
|
|
35
|
+
const updatedSheetXml = this.#serializeSheetXml(sheetXml, cells)
|
|
36
|
+
zip.file(sheetPath, updatedSheetXml)
|
|
37
|
+
|
|
38
|
+
// Clean up any existing Excel tables
|
|
31
39
|
const tableFiles = Object.keys(zip.files).filter(f => f.startsWith('xl/tables/'))
|
|
32
40
|
tableFiles.forEach(f => zip.remove(f))
|
|
33
41
|
|
|
@@ -56,50 +64,180 @@ class ChartWorkbookUpdater {
|
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
static
|
|
67
|
+
static async getSharedStrings(zip) {
|
|
68
|
+
const sstFile = zip.file('xl/sharedStrings.xml')
|
|
69
|
+
if (!sstFile) return []
|
|
70
|
+
const xml = await sstFile.async('text')
|
|
71
|
+
const strings = []
|
|
72
|
+
const pattern = /<si>([\s\S]*?)<\/si>/g
|
|
73
|
+
let match
|
|
74
|
+
while ((match = pattern.exec(xml)) !== null) {
|
|
75
|
+
const siContent = match[1]
|
|
76
|
+
const tPattern = /<t\b[^>]*>([^<]*)<\/t>/g
|
|
77
|
+
let tMatch
|
|
78
|
+
let textVal = ''
|
|
79
|
+
while ((tMatch = tPattern.exec(siContent)) !== null) {
|
|
80
|
+
textVal += tMatch[1]
|
|
81
|
+
}
|
|
82
|
+
strings.push(textVal)
|
|
83
|
+
}
|
|
84
|
+
return strings
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static parseWorksheetCells(sheetXml, sharedStrings) {
|
|
88
|
+
const cells = {}
|
|
89
|
+
const cellPattern = /<c r="([A-Z]+\d+)"([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g
|
|
90
|
+
let match
|
|
91
|
+
while ((match = cellPattern.exec(sheetXml)) !== null) {
|
|
92
|
+
const ref = match[1]
|
|
93
|
+
const attrs = match[2]
|
|
94
|
+
const content = match[3] || ''
|
|
95
|
+
|
|
96
|
+
const tMatch = /t="([^"]*)"/.exec(attrs)
|
|
97
|
+
const t = tMatch ? tMatch[1] : null
|
|
98
|
+
|
|
99
|
+
let val = ''
|
|
100
|
+
if (t === 'inlineStr') {
|
|
101
|
+
const tValMatch = /<t\b[^>]*>([^<]*)<\/t>/.exec(content)
|
|
102
|
+
val = tValMatch ? tValMatch[1] : ''
|
|
103
|
+
} else if (t === 's') {
|
|
104
|
+
const vMatch = /<v>(\d+)<\/v>/.exec(content)
|
|
105
|
+
if (vMatch) {
|
|
106
|
+
const idx = parseInt(vMatch[1], 10)
|
|
107
|
+
val = sharedStrings[idx] !== undefined ? sharedStrings[idx] : ''
|
|
108
|
+
}
|
|
109
|
+
} else if (content) {
|
|
110
|
+
const vMatch = /<v>([^<]*)<\/v>/.exec(content)
|
|
111
|
+
if (vMatch) {
|
|
112
|
+
val = vMatch[1]
|
|
113
|
+
if (val !== '' && !isNaN(val)) {
|
|
114
|
+
val = Number(val)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
cells[ref] = val
|
|
119
|
+
}
|
|
120
|
+
return cells
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static #updateCellGrid(cells, data) {
|
|
60
124
|
const { categories = [], series = [] } = data
|
|
61
125
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
const
|
|
126
|
+
// Clear cells that are outside the new category/series grid
|
|
127
|
+
const maxRow = categories.length + 1
|
|
128
|
+
const maxCol = series.length // Column A is 0, Column B is 1, etc.
|
|
65
129
|
|
|
66
|
-
const
|
|
67
|
-
|
|
130
|
+
for (const ref of Object.keys(cells)) {
|
|
131
|
+
const match = /^([A-Z]+)(\d+)$/.exec(ref)
|
|
132
|
+
if (match) {
|
|
133
|
+
const colLetter = match[1]
|
|
134
|
+
const row = parseInt(match[2], 10)
|
|
135
|
+
const col = this.colLetterToNum(colLetter)
|
|
136
|
+
if (row > maxRow || col > maxCol) {
|
|
137
|
+
delete cells[ref]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
68
141
|
|
|
69
|
-
|
|
142
|
+
// 1. Write Header A1 as empty
|
|
143
|
+
cells['A1'] = ''
|
|
70
144
|
|
|
71
|
-
// Row 1
|
|
72
|
-
sheetData += '<row r="1">'
|
|
73
|
-
sheetData += `<c r="A1" t="inlineStr"><is><t></t></is></c>`
|
|
145
|
+
// 2. Write series titles in Row 1 (B1, C1, etc.)
|
|
74
146
|
series.forEach((ser, i) => {
|
|
75
147
|
const colLetter = this.getColumnLetter(i + 1)
|
|
76
|
-
|
|
148
|
+
cells[`${colLetter}1`] = ser.name || ''
|
|
77
149
|
})
|
|
78
|
-
sheetData += '</row>'
|
|
79
150
|
|
|
80
|
-
//
|
|
151
|
+
// 3. Write categories in column A (A2, A3, etc.)
|
|
81
152
|
categories.forEach((cat, rowIndex) => {
|
|
82
|
-
|
|
153
|
+
cells[`A${rowIndex + 2}`] = String(cat)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// 4. Write series values
|
|
157
|
+
series.forEach((ser, colIndex) => {
|
|
158
|
+
const colLetter = this.getColumnLetter(colIndex + 1)
|
|
159
|
+
if (ser.values) {
|
|
160
|
+
ser.values.forEach((val, rowIndex) => {
|
|
161
|
+
cells[`${colLetter}${rowIndex + 2}`] = val !== undefined ? val : null
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// 5. Write custom data labels
|
|
167
|
+
series.forEach(ser => {
|
|
168
|
+
if (ser.labels && ser.labelsFromCells) {
|
|
169
|
+
const range = this.parseCellRange(ser.labelsFromCells)
|
|
170
|
+
const startColNum = this.colLetterToNum(range.startCol)
|
|
171
|
+
ser.labels.forEach((lbl, i) => {
|
|
172
|
+
let cellRef
|
|
173
|
+
if (range.startRow === range.endRow) {
|
|
174
|
+
cellRef = `${this.numToColLetter(startColNum + i)}${range.startRow}`
|
|
175
|
+
} else {
|
|
176
|
+
cellRef = `${range.startCol}${range.startRow + i}`
|
|
177
|
+
}
|
|
178
|
+
cells[cellRef] = lbl
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static #serializeSheetXml(sheetXml, cells) {
|
|
185
|
+
// Group cells by row
|
|
186
|
+
const rows = {}
|
|
187
|
+
for (const [ref, val] of Object.entries(cells)) {
|
|
188
|
+
const rowMatch = /\d+$/.exec(ref)
|
|
189
|
+
if (!rowMatch) continue
|
|
190
|
+
const r = parseInt(rowMatch[0], 10)
|
|
191
|
+
if (!rows[r]) rows[r] = []
|
|
192
|
+
rows[r].push(ref)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Sort rows ascending
|
|
196
|
+
const sortedRowKeys = Object.keys(rows)
|
|
197
|
+
.map(Number)
|
|
198
|
+
.sort((a, b) => a - b)
|
|
199
|
+
|
|
200
|
+
let maxRow = 1
|
|
201
|
+
let maxColNum = 0
|
|
202
|
+
|
|
203
|
+
let sheetData = '<sheetData>'
|
|
204
|
+
for (const r of sortedRowKeys) {
|
|
205
|
+
if (r > maxRow) maxRow = r
|
|
83
206
|
sheetData += `<row r="${r}">`
|
|
84
|
-
sheetData += `<c r="A${r}" t="inlineStr"><is><t>${this.#escapeXml(String(cat))}</t></is></c>`
|
|
85
207
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
208
|
+
// Sort cells in this row by column letters
|
|
209
|
+
const sortedRefs = rows[r].sort((a, b) => {
|
|
210
|
+
const colA = /^[A-Z]+/.exec(a)[0]
|
|
211
|
+
const colB = /^[A-Z]+/.exec(b)[0]
|
|
212
|
+
if (colA.length !== colB.length) return colA.length - colB.length
|
|
213
|
+
return colA.localeCompare(colB)
|
|
90
214
|
})
|
|
91
|
-
sheetData += '</row>'
|
|
92
|
-
})
|
|
93
215
|
|
|
216
|
+
for (const ref of sortedRefs) {
|
|
217
|
+
const colLetter = /^[A-Z]+/.exec(ref)[0]
|
|
218
|
+
const colNum = this.colLetterToNum(colLetter)
|
|
219
|
+
if (colNum > maxColNum) maxColNum = colNum
|
|
220
|
+
|
|
221
|
+
const val = cells[ref]
|
|
222
|
+
if (val === null || val === undefined) {
|
|
223
|
+
sheetData += `<c r="${ref}" t="inlineStr"><is><t></t></is></c>`
|
|
224
|
+
} else if (typeof val === 'number') {
|
|
225
|
+
sheetData += `<c r="${ref}"><v>${val}</v></c>`
|
|
226
|
+
} else {
|
|
227
|
+
sheetData += `<c r="${ref}" t="inlineStr"><is><t>${this.#escapeXml(String(val))}</t></is></c>`
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
sheetData += '</row>'
|
|
231
|
+
}
|
|
94
232
|
sheetData += '</sheetData>'
|
|
95
233
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
234
|
+
const maxColLetter = this.numToColLetter(maxColNum)
|
|
235
|
+
const newDimension = `<dimension ref="A1:${maxColLetter}${maxRow}"/>`
|
|
236
|
+
|
|
237
|
+
let updatedXml = sheetXml.replace(/<sheetData>[\s\S]*?<\/sheetData>/, sheetData)
|
|
238
|
+
updatedXml = updatedXml.replace(/<dimension ref="[^"]*"\/>/, newDimension)
|
|
239
|
+
|
|
240
|
+
return updatedXml
|
|
103
241
|
}
|
|
104
242
|
|
|
105
243
|
static getColumnLetter(colIndex) {
|
|
@@ -111,6 +249,39 @@ class ChartWorkbookUpdater {
|
|
|
111
249
|
return letter
|
|
112
250
|
}
|
|
113
251
|
|
|
252
|
+
static colLetterToNum(letter) {
|
|
253
|
+
let num = 0
|
|
254
|
+
for (let i = 0; i < letter.length; i++) {
|
|
255
|
+
num = num * 26 + (letter.charCodeAt(i) - 64)
|
|
256
|
+
}
|
|
257
|
+
return num - 1
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static numToColLetter(num) {
|
|
261
|
+
return this.getColumnLetter(num)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static parseCellRange(rangeStr) {
|
|
265
|
+
const parts = rangeStr.split('!')
|
|
266
|
+
const range = parts.length > 1 ? parts[1] : parts[0]
|
|
267
|
+
const cleanRange = range.replace(/\$/g, '')
|
|
268
|
+
const [start, end] = cleanRange.split(':')
|
|
269
|
+
|
|
270
|
+
const startCol = /^[A-Z]+/.exec(start)[0]
|
|
271
|
+
const startRow = parseInt(/\d+$/.exec(start)[0], 10)
|
|
272
|
+
|
|
273
|
+
const endCol = end ? /^[A-Z]+/.exec(end)[0] : startCol
|
|
274
|
+
const endRow = end ? parseInt(/\d+$/.exec(end)[0], 10) : startRow
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
sheetName: parts.length > 1 ? parts[0] : 'Sheet1',
|
|
278
|
+
startCol,
|
|
279
|
+
startRow,
|
|
280
|
+
endCol,
|
|
281
|
+
endRow,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
114
285
|
static getFormulaRange(sheetName, startRow, startCol, endRow, endCol) {
|
|
115
286
|
const startLetter = this.getColumnLetter(startCol)
|
|
116
287
|
const endLetter = this.getColumnLetter(endCol)
|