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
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
* We update both to ensure compatibility with both cached and live data.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const { createLogger } = require('../utils/logger.js')
|
|
43
|
+
const { ChartNotFoundError } = require('../utils/errors.js')
|
|
44
|
+
const { REL_TYPES } = require('./RelationshipManager.js')
|
|
45
|
+
const { ChartWorkbookUpdater } = require('./charts/ChartWorkbookUpdater.js')
|
|
46
|
+
const { ChartCacheGenerator } = require('./charts/ChartCacheGenerator.js')
|
|
47
47
|
|
|
48
|
-
const logger = createLogger('ChartManager')
|
|
48
|
+
const logger = createLogger('ChartManager')
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Supported chart types and their XML element names.
|
|
@@ -60,7 +60,7 @@ const CHART_TYPE_MAP = {
|
|
|
60
60
|
radar: 'c:radarChart',
|
|
61
61
|
bubble: 'c:bubbleChart',
|
|
62
62
|
stock: 'c:stockChart',
|
|
63
|
-
}
|
|
63
|
+
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* @class ChartManager
|
|
@@ -69,23 +69,23 @@ const CHART_TYPE_MAP = {
|
|
|
69
69
|
* Unlike high-level charting libraries, this manager edits the raw OpenXML
|
|
70
70
|
* chart structure, allowing full control while preserving styles and themes.
|
|
71
71
|
*/
|
|
72
|
-
|
|
72
|
+
class ChartManager {
|
|
73
73
|
/** @private @type {XMLParser} */
|
|
74
|
-
#xmlParser
|
|
74
|
+
#xmlParser
|
|
75
75
|
/** @private @type {ZipManager} */
|
|
76
|
-
#zipManager
|
|
76
|
+
#zipManager
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Cache of chart ZIP paths: maps chartName → { zipPath, slideIndex }
|
|
80
80
|
* @private @type {Map<string, { zipPath: string, slideIndex: number }>}
|
|
81
81
|
*/
|
|
82
|
-
#chartRegistry = new Map()
|
|
82
|
+
#chartRegistry = new Map()
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* @param {XMLParser} xmlParser
|
|
86
86
|
*/
|
|
87
87
|
constructor(xmlParser) {
|
|
88
|
-
this.#xmlParser = xmlParser
|
|
88
|
+
this.#xmlParser = xmlParser
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -95,16 +95,18 @@ export class ChartManager {
|
|
|
95
95
|
* @returns {Promise<void>}
|
|
96
96
|
*/
|
|
97
97
|
async initialize(zipManager) {
|
|
98
|
-
this.#zipManager = zipManager
|
|
99
|
-
const chartFiles = zipManager
|
|
98
|
+
this.#zipManager = zipManager
|
|
99
|
+
const chartFiles = zipManager
|
|
100
|
+
.listFiles('ppt/charts/')
|
|
101
|
+
.filter(f => f.endsWith('.xml') && !f.includes('_rels'))
|
|
100
102
|
|
|
101
103
|
for (const chartPath of chartFiles) {
|
|
102
104
|
// Chart name is inferred from file name
|
|
103
|
-
const chartName = chartPath.split('/').pop().replace('.xml', '')
|
|
104
|
-
this.#chartRegistry.set(chartName, { zipPath: chartPath, slideIndex: null })
|
|
105
|
+
const chartName = chartPath.split('/').pop().replace('.xml', '')
|
|
106
|
+
this.#chartRegistry.set(chartName, { zipPath: chartPath, slideIndex: null })
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
logger.debug(`Found ${chartFiles.length} chart file(s)`)
|
|
109
|
+
logger.debug(`Found ${chartFiles.length} chart file(s)`)
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/**
|
|
@@ -118,14 +120,14 @@ export class ChartManager {
|
|
|
118
120
|
* @throws {ChartNotFoundError} If the chart cannot be found.
|
|
119
121
|
*/
|
|
120
122
|
updateChart(slideIndex, chartId, data, slideManager, relationshipManager) {
|
|
121
|
-
const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
123
|
+
const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
122
124
|
|
|
123
125
|
if (!chartInfo) {
|
|
124
|
-
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
126
|
+
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
logger.debug(`Updating chart "${chartId}" at ${chartInfo.zipPath}`)
|
|
128
|
-
this.#updateChartXml(chartInfo.zipPath, data, relationshipManager)
|
|
129
|
+
logger.debug(`Updating chart "${chartId}" at ${chartInfo.zipPath}`)
|
|
130
|
+
this.#updateChartXml(chartInfo.zipPath, data, relationshipManager)
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
/**
|
|
@@ -137,13 +139,13 @@ export class ChartManager {
|
|
|
137
139
|
* @returns {Array<{name: string, zipPath: string}>}
|
|
138
140
|
*/
|
|
139
141
|
getChartsInSlide(slideIndex, slideManager, relationshipManager) {
|
|
140
|
-
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
141
|
-
const rels = relationshipManager.getRelationshipsByType(slideInfo.zipPath, REL_TYPES.CHART)
|
|
142
|
+
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
143
|
+
const rels = relationshipManager.getRelationshipsByType(slideInfo.zipPath, REL_TYPES.CHART)
|
|
142
144
|
|
|
143
145
|
return rels.map(rel => {
|
|
144
|
-
const chartPath = relationshipManager.resolveTarget(slideInfo.zipPath, rel.target)
|
|
145
|
-
return { rId: rel.id, zipPath: chartPath }
|
|
146
|
-
})
|
|
146
|
+
const chartPath = relationshipManager.resolveTarget(slideInfo.zipPath, rel.target)
|
|
147
|
+
return { rId: rel.id, zipPath: chartPath }
|
|
148
|
+
})
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
/**
|
|
@@ -157,58 +159,58 @@ export class ChartManager {
|
|
|
157
159
|
* @returns {{ zipPath: string }|null}
|
|
158
160
|
*/
|
|
159
161
|
#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager) {
|
|
160
|
-
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
161
|
-
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
162
|
+
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
163
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
162
164
|
|
|
163
165
|
// Strategy 1: Look for shape with matching name (cNvPr name attribute)
|
|
164
166
|
const shapeNamePattern = new RegExp(
|
|
165
167
|
`<p:cNvPr[^>]*name="${chartId}"[^>]*>(?:.*?)<c:chart[^>]*r:id="(rId\\d+)"`,
|
|
166
168
|
's'
|
|
167
|
-
)
|
|
168
|
-
const rIdMatch = shapeNamePattern.exec(slideXml)
|
|
169
|
+
)
|
|
170
|
+
const rIdMatch = shapeNamePattern.exec(slideXml)
|
|
169
171
|
|
|
170
172
|
// Strategy 2: Find graphicFrame shapes and match chart rIds
|
|
171
173
|
if (!rIdMatch) {
|
|
172
|
-
const chartRIdPattern = /<c:chart[^>]*r:id="(rId\d+)"/g
|
|
173
|
-
let chartMatch
|
|
174
|
+
const chartRIdPattern = /<c:chart[^>]*r:id="(rId\d+)"/g
|
|
175
|
+
let chartMatch
|
|
174
176
|
while ((chartMatch = chartRIdPattern.exec(slideXml)) !== null) {
|
|
175
|
-
const rId = chartMatch[1]
|
|
176
|
-
const rel = relationshipManager.getRelationshipById(slideInfo.zipPath, rId)
|
|
177
|
+
const rId = chartMatch[1]
|
|
178
|
+
const rel = relationshipManager.getRelationshipById(slideInfo.zipPath, rId)
|
|
177
179
|
if (rel) {
|
|
178
|
-
const chartPath = relationshipManager.resolveTarget(slideInfo.zipPath, rel.target)
|
|
180
|
+
const chartPath = relationshipManager.resolveTarget(slideInfo.zipPath, rel.target)
|
|
179
181
|
// Check if chart file name matches chartId
|
|
180
182
|
if (chartPath.includes(chartId) || rel.id === chartId) {
|
|
181
|
-
return { zipPath: chartPath }
|
|
183
|
+
return { zipPath: chartPath }
|
|
182
184
|
}
|
|
183
185
|
// For the first chart found, if chartId looks like a chart file name
|
|
184
186
|
if (chartId.startsWith('chart')) {
|
|
185
|
-
return { zipPath: chartPath }
|
|
187
|
+
return { zipPath: chartPath }
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
if (rIdMatch) {
|
|
192
|
-
const rId = rIdMatch[1]
|
|
193
|
-
const rel = relationshipManager.getRelationshipById(slideInfo.zipPath, rId)
|
|
194
|
+
const rId = rIdMatch[1]
|
|
195
|
+
const rel = relationshipManager.getRelationshipById(slideInfo.zipPath, rId)
|
|
194
196
|
if (rel) {
|
|
195
|
-
const chartPath = relationshipManager.resolveTarget(slideInfo.zipPath, rel.target)
|
|
196
|
-
return { zipPath: chartPath }
|
|
197
|
+
const chartPath = relationshipManager.resolveTarget(slideInfo.zipPath, rel.target)
|
|
198
|
+
return { zipPath: chartPath }
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
// Strategy 3: Direct chart registry lookup
|
|
201
203
|
if (this.#chartRegistry.has(chartId)) {
|
|
202
|
-
return this.#chartRegistry.get(chartId)
|
|
204
|
+
return this.#chartRegistry.get(chartId)
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
// Strategy 4: Try chartN naming convention
|
|
206
|
-
const chartPath = `ppt/charts/${chartId}.xml
|
|
208
|
+
const chartPath = `ppt/charts/${chartId}.xml`
|
|
207
209
|
if (this.#zipManager.hasFile(chartPath)) {
|
|
208
|
-
return { zipPath: chartPath }
|
|
210
|
+
return { zipPath: chartPath }
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
return null
|
|
213
|
+
return null
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
/**
|
|
@@ -222,13 +224,13 @@ export class ChartManager {
|
|
|
222
224
|
*/
|
|
223
225
|
#updateChartXml(chartZipPath, data, relationshipManager) {
|
|
224
226
|
if (!this.#zipManager.hasFile(chartZipPath)) {
|
|
225
|
-
throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
227
|
+
throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
// Register async update to ensure it completes before saving
|
|
229
231
|
this.#zipManager.addPendingPromise(
|
|
230
232
|
this.updateChartAsync(chartZipPath, data, relationshipManager)
|
|
231
|
-
)
|
|
233
|
+
)
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
/**
|
|
@@ -241,29 +243,29 @@ export class ChartManager {
|
|
|
241
243
|
*/
|
|
242
244
|
async updateChartAsync(chartZipPath, data, relationshipManager) {
|
|
243
245
|
// 1. Read Chart XML
|
|
244
|
-
const xml = await this.#zipManager.readFile(chartZipPath)
|
|
245
|
-
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
246
|
+
const xml = await this.#zipManager.readFile(chartZipPath)
|
|
247
|
+
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
246
248
|
|
|
247
249
|
// 2. Apply Chart XML Updates
|
|
248
|
-
const updatedXml = this.#applyChartData(xml, data, chartZipPath)
|
|
249
|
-
this.#zipManager.writeFile(chartZipPath, updatedXml)
|
|
250
|
+
const updatedXml = this.#applyChartData(xml, data, chartZipPath)
|
|
251
|
+
this.#zipManager.writeFile(chartZipPath, updatedXml)
|
|
250
252
|
|
|
251
253
|
// 3. Find and Update Embedded Workbook
|
|
252
254
|
if (relationshipManager) {
|
|
253
|
-
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
255
|
+
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
254
256
|
for (const rel of rels) {
|
|
255
|
-
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
256
|
-
const xlsxData = this.#zipManager.rawZip.file(xlsxPath)
|
|
257
|
+
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
258
|
+
const xlsxData = this.#zipManager.rawZip.file(xlsxPath)
|
|
257
259
|
if (xlsxData) {
|
|
258
|
-
console.log(`Found embedded workbook: ${xlsxPath}`)
|
|
259
|
-
const buffer = await xlsxData.async('nodebuffer')
|
|
260
|
-
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, data)
|
|
260
|
+
console.log(`Found embedded workbook: ${xlsxPath}`)
|
|
261
|
+
const buffer = await xlsxData.async('nodebuffer')
|
|
262
|
+
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, data)
|
|
261
263
|
if (updatedXlsx) {
|
|
262
|
-
console.log(`Writing updated workbook to: ${xlsxPath}, size: ${updatedXlsx.length}`)
|
|
263
|
-
this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
|
|
264
|
+
console.log(`Writing updated workbook to: ${xlsxPath}, size: ${updatedXlsx.length}`)
|
|
265
|
+
this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
|
|
264
266
|
}
|
|
265
267
|
} else {
|
|
266
|
-
console.log(`Could not find workbook at: ${xlsxPath}`)
|
|
268
|
+
console.log(`Could not find workbook at: ${xlsxPath}`)
|
|
267
269
|
}
|
|
268
270
|
}
|
|
269
271
|
}
|
|
@@ -279,27 +281,150 @@ export class ChartManager {
|
|
|
279
281
|
* @returns {string} Updated XML.
|
|
280
282
|
*/
|
|
281
283
|
#applyChartData(xml, data, context) {
|
|
282
|
-
const { categories, series } = data
|
|
284
|
+
const { categories, series } = data
|
|
283
285
|
|
|
284
286
|
// Detect chart type
|
|
285
|
-
const chartType = this.#detectChartType(xml)
|
|
286
|
-
logger.debug(`Updating ${chartType} chart at ${context}`)
|
|
287
|
+
const chartType = this.#detectChartType(xml)
|
|
288
|
+
logger.debug(`Updating ${chartType} chart at ${context}`)
|
|
287
289
|
|
|
288
|
-
let updatedXml = xml
|
|
290
|
+
let updatedXml = xml
|
|
289
291
|
|
|
290
292
|
if (series && series.length > 0) {
|
|
291
|
-
updatedXml = ChartCacheGenerator.appendDynamicSeries(updatedXml, series.length)
|
|
293
|
+
updatedXml = ChartCacheGenerator.appendDynamicSeries(updatedXml, series.length)
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
if (categories && categories.length > 0) {
|
|
295
|
-
updatedXml = ChartCacheGenerator.updateCategories(updatedXml, categories)
|
|
297
|
+
updatedXml = ChartCacheGenerator.updateCategories(updatedXml, categories)
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
if (series && series.length > 0) {
|
|
299
|
-
updatedXml = ChartCacheGenerator.updateSeries(
|
|
301
|
+
updatedXml = ChartCacheGenerator.updateSeries(
|
|
302
|
+
updatedXml,
|
|
303
|
+
series,
|
|
304
|
+
categories ? categories.length : null
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return updatedXml
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Updates only chart categories.
|
|
313
|
+
*/
|
|
314
|
+
updateChartCategories(slideIndex, chartId, categories, slideManager, relationshipManager) {
|
|
315
|
+
const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
316
|
+
if (!chartInfo) {
|
|
317
|
+
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
318
|
+
}
|
|
319
|
+
this.#zipManager.addPendingPromise(
|
|
320
|
+
this.updateChartCategoriesAsync(chartInfo.zipPath, categories, relationshipManager)
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async updateChartCategoriesAsync(chartZipPath, categories, relationshipManager) {
|
|
325
|
+
const xml = await this.#zipManager.readFile(chartZipPath)
|
|
326
|
+
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
327
|
+
const data = this.#extractChartData(xml)
|
|
328
|
+
data.categories = categories
|
|
329
|
+
await this.updateChartAsync(chartZipPath, data, relationshipManager)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Replaces a specific chart series.
|
|
334
|
+
*/
|
|
335
|
+
replaceChartSeries(
|
|
336
|
+
slideIndex,
|
|
337
|
+
chartId,
|
|
338
|
+
seriesIndex,
|
|
339
|
+
newSeriesData,
|
|
340
|
+
slideManager,
|
|
341
|
+
relationshipManager
|
|
342
|
+
) {
|
|
343
|
+
const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
344
|
+
if (!chartInfo) {
|
|
345
|
+
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
346
|
+
}
|
|
347
|
+
this.#zipManager.addPendingPromise(
|
|
348
|
+
this.replaceChartSeriesAsync(
|
|
349
|
+
chartInfo.zipPath,
|
|
350
|
+
seriesIndex,
|
|
351
|
+
newSeriesData,
|
|
352
|
+
relationshipManager
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async replaceChartSeriesAsync(chartZipPath, seriesIndex, newSeriesData, relationshipManager) {
|
|
358
|
+
const xml = await this.#zipManager.readFile(chartZipPath)
|
|
359
|
+
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
360
|
+
const data = this.#extractChartData(xml)
|
|
361
|
+
data.series[seriesIndex] = newSeriesData
|
|
362
|
+
await this.updateChartAsync(chartZipPath, data, relationshipManager)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Updates the chart title.
|
|
367
|
+
*/
|
|
368
|
+
updateChartTitle(slideIndex, chartId, title, slideManager, relationshipManager) {
|
|
369
|
+
const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
370
|
+
if (!chartInfo) {
|
|
371
|
+
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
300
372
|
}
|
|
373
|
+
this.#zipManager.addPendingPromise(this.updateChartTitleAsync(chartInfo.zipPath, title))
|
|
374
|
+
}
|
|
301
375
|
|
|
302
|
-
|
|
376
|
+
async updateChartTitleAsync(chartZipPath, title) {
|
|
377
|
+
const xml = await this.#zipManager.readFile(chartZipPath)
|
|
378
|
+
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
379
|
+
const updatedXml = ChartCacheGenerator.updateTitle(xml, title)
|
|
380
|
+
this.#zipManager.writeFile(chartZipPath, updatedXml)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Helper to extract data from chart XML.
|
|
385
|
+
*/
|
|
386
|
+
#extractChartData(xml) {
|
|
387
|
+
const categories = []
|
|
388
|
+
const series = []
|
|
389
|
+
|
|
390
|
+
const catMatch = /<c:cat>([\s\S]*?)<\/c:cat>/.exec(xml)
|
|
391
|
+
if (catMatch) {
|
|
392
|
+
const catXml = catMatch[1]
|
|
393
|
+
const ptPattern = /<c:pt idx="\d+">\s*<c:v>([^<]*)<\/c:v>/g
|
|
394
|
+
let match
|
|
395
|
+
while ((match = ptPattern.exec(catXml)) !== null) {
|
|
396
|
+
categories.push(match[1])
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const serPattern = /<c:ser>([\s\S]*?)<\/c:ser>/g
|
|
401
|
+
let serMatch
|
|
402
|
+
let idx = 0
|
|
403
|
+
while ((serMatch = serPattern.exec(xml)) !== null) {
|
|
404
|
+
const serXml = serMatch[1]
|
|
405
|
+
|
|
406
|
+
let name = `Series ${idx + 1}`
|
|
407
|
+
const txMatch = /<c:tx>([\s\S]*?)<\/c:tx>/.exec(serXml)
|
|
408
|
+
if (txMatch) {
|
|
409
|
+
const nameValMatch = /<c:v>([^<]*)<\/c:v>/.exec(txMatch[1])
|
|
410
|
+
if (nameValMatch) name = nameValMatch[1]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const values = []
|
|
414
|
+
const valMatch = /<c:val>([\s\S]*?)<\/c:val>/.exec(serXml)
|
|
415
|
+
if (valMatch) {
|
|
416
|
+
const ptPattern = /<c:pt idx="\d+">\s*<c:v>([^<]*)<\/c:v>/g
|
|
417
|
+
let match
|
|
418
|
+
while ((match = ptPattern.exec(valMatch[1])) !== null) {
|
|
419
|
+
values.push(Number(match[1]) || 0)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
series.push({ name, values })
|
|
424
|
+
idx++
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { categories, series }
|
|
303
428
|
}
|
|
304
429
|
|
|
305
430
|
/**
|
|
@@ -310,8 +435,10 @@ export class ChartManager {
|
|
|
310
435
|
*/
|
|
311
436
|
#detectChartType(xml) {
|
|
312
437
|
for (const [name, element] of Object.entries(CHART_TYPE_MAP)) {
|
|
313
|
-
if (xml.includes(element)) return name
|
|
438
|
+
if (xml.includes(element)) return name
|
|
314
439
|
}
|
|
315
|
-
return 'unknown'
|
|
440
|
+
return 'unknown'
|
|
316
441
|
}
|
|
317
442
|
}
|
|
443
|
+
|
|
444
|
+
module.exports = { ChartManager }
|
|
@@ -4,25 +4,25 @@
|
|
|
4
4
|
* Implements structured, XML-safe manipulation of the OPC manifest.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const { createLogger } = require('../utils/logger.js')
|
|
8
|
+
const { PPTXError } = require('../utils/errors.js')
|
|
9
9
|
|
|
10
|
-
const logger = createLogger('ContentTypesManager')
|
|
10
|
+
const logger = createLogger('ContentTypesManager')
|
|
11
11
|
|
|
12
|
-
const TYPES_XML_PATH = '[Content_Types].xml'
|
|
12
|
+
const TYPES_XML_PATH = '[Content_Types].xml'
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
class ContentTypesManager {
|
|
15
15
|
/** @private @type {XMLParser} */
|
|
16
|
-
#xmlParser
|
|
16
|
+
#xmlParser
|
|
17
17
|
|
|
18
18
|
/** @private @type {Object} */
|
|
19
|
-
#contentTypesObj = null
|
|
19
|
+
#contentTypesObj = null
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* @param {XMLParser} xmlParser
|
|
23
23
|
*/
|
|
24
24
|
constructor(xmlParser) {
|
|
25
|
-
this.#xmlParser = xmlParser
|
|
25
|
+
this.#xmlParser = xmlParser
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -32,36 +32,38 @@ export class ContentTypesManager {
|
|
|
32
32
|
* @returns {Promise<void>}
|
|
33
33
|
*/
|
|
34
34
|
async initialize(zipManager) {
|
|
35
|
-
const content = await zipManager.readFile(TYPES_XML_PATH)
|
|
35
|
+
const content = await zipManager.readFile(TYPES_XML_PATH)
|
|
36
36
|
if (!content) {
|
|
37
|
-
throw new PPTXError(`${TYPES_XML_PATH} is missing from the archive.`)
|
|
37
|
+
throw new PPTXError(`${TYPES_XML_PATH} is missing from the archive.`)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
this.#contentTypesObj = this.#xmlParser.parse(content, TYPES_XML_PATH)
|
|
40
|
+
this.#contentTypesObj = this.#xmlParser.parse(content, TYPES_XML_PATH)
|
|
41
41
|
|
|
42
42
|
// Ensure structure is correct
|
|
43
43
|
if (!this.#contentTypesObj.Types) {
|
|
44
44
|
this.#contentTypesObj.Types = {
|
|
45
45
|
'@_xmlns': 'http://schemas.openxmlformats.org/package/2006/content-types',
|
|
46
46
|
Default: [],
|
|
47
|
-
Override: []
|
|
48
|
-
}
|
|
47
|
+
Override: [],
|
|
48
|
+
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Ensure array properties
|
|
52
52
|
if (!this.#contentTypesObj.Types.Default) {
|
|
53
|
-
this.#contentTypesObj.Types.Default = []
|
|
53
|
+
this.#contentTypesObj.Types.Default = []
|
|
54
54
|
} else if (!Array.isArray(this.#contentTypesObj.Types.Default)) {
|
|
55
|
-
this.#contentTypesObj.Types.Default = [this.#contentTypesObj.Types.Default]
|
|
55
|
+
this.#contentTypesObj.Types.Default = [this.#contentTypesObj.Types.Default]
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (!this.#contentTypesObj.Types.Override) {
|
|
59
|
-
this.#contentTypesObj.Types.Override = []
|
|
59
|
+
this.#contentTypesObj.Types.Override = []
|
|
60
60
|
} else if (!Array.isArray(this.#contentTypesObj.Types.Override)) {
|
|
61
|
-
this.#contentTypesObj.Types.Override = [this.#contentTypesObj.Types.Override]
|
|
61
|
+
this.#contentTypesObj.Types.Override = [this.#contentTypesObj.Types.Override]
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
logger.debug(
|
|
64
|
+
logger.debug(
|
|
65
|
+
`Loaded [Content_Types].xml with ${this.#contentTypesObj.Types.Default.length} Defaults and ${this.#contentTypesObj.Types.Override.length} Overrides`
|
|
66
|
+
)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -71,18 +73,18 @@ export class ContentTypesManager {
|
|
|
71
73
|
* @param {string} contentType - The MIME type.
|
|
72
74
|
*/
|
|
73
75
|
addDefault(extension, contentType) {
|
|
74
|
-
const extLower = extension.toLowerCase()
|
|
75
|
-
const defaults = this.#contentTypesObj.Types.Default
|
|
76
|
+
const extLower = extension.toLowerCase()
|
|
77
|
+
const defaults = this.#contentTypesObj.Types.Default
|
|
76
78
|
|
|
77
|
-
const existing = defaults.find(d => d['@_Extension']?.toLowerCase() === extLower)
|
|
79
|
+
const existing = defaults.find(d => d['@_Extension']?.toLowerCase() === extLower)
|
|
78
80
|
if (existing) {
|
|
79
|
-
existing['@_ContentType'] = contentType
|
|
81
|
+
existing['@_ContentType'] = contentType
|
|
80
82
|
} else {
|
|
81
83
|
defaults.push({
|
|
82
84
|
'@_Extension': extLower,
|
|
83
|
-
'@_ContentType': contentType
|
|
84
|
-
})
|
|
85
|
-
logger.debug(`Registered default content type for extension .${extLower} -> ${contentType}`)
|
|
85
|
+
'@_ContentType': contentType,
|
|
86
|
+
})
|
|
87
|
+
logger.debug(`Registered default content type for extension .${extLower} -> ${contentType}`)
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -93,18 +95,18 @@ export class ContentTypesManager {
|
|
|
93
95
|
* @param {string} contentType - The MIME type.
|
|
94
96
|
*/
|
|
95
97
|
addOverride(partName, contentType) {
|
|
96
|
-
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}
|
|
97
|
-
const overrides = this.#contentTypesObj.Types.Override
|
|
98
|
+
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}`
|
|
99
|
+
const overrides = this.#contentTypesObj.Types.Override
|
|
98
100
|
|
|
99
|
-
const existing = overrides.find(o => o['@_PartName'] === normalizedPart)
|
|
101
|
+
const existing = overrides.find(o => o['@_PartName'] === normalizedPart)
|
|
100
102
|
if (existing) {
|
|
101
|
-
existing['@_ContentType'] = contentType
|
|
103
|
+
existing['@_ContentType'] = contentType
|
|
102
104
|
} else {
|
|
103
105
|
overrides.push({
|
|
104
106
|
'@_PartName': normalizedPart,
|
|
105
|
-
'@_ContentType': contentType
|
|
106
|
-
})
|
|
107
|
-
logger.debug(`Registered override content type for ${normalizedPart} -> ${contentType}`)
|
|
107
|
+
'@_ContentType': contentType,
|
|
108
|
+
})
|
|
109
|
+
logger.debug(`Registered override content type for ${normalizedPart} -> ${contentType}`)
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -114,13 +116,13 @@ export class ContentTypesManager {
|
|
|
114
116
|
* @param {string} partName - Absolute part path (e.g., '/ppt/slides/slide1.xml').
|
|
115
117
|
*/
|
|
116
118
|
removeOverride(partName) {
|
|
117
|
-
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}
|
|
118
|
-
const overrides = this.#contentTypesObj.Types.Override
|
|
119
|
+
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}`
|
|
120
|
+
const overrides = this.#contentTypesObj.Types.Override
|
|
119
121
|
|
|
120
|
-
const filtered = overrides.filter(o => o['@_PartName'] !== normalizedPart)
|
|
122
|
+
const filtered = overrides.filter(o => o['@_PartName'] !== normalizedPart)
|
|
121
123
|
if (filtered.length !== overrides.length) {
|
|
122
|
-
this.#contentTypesObj.Types.Override = filtered
|
|
123
|
-
logger.debug(`Removed content type override for ${normalizedPart}`)
|
|
124
|
+
this.#contentTypesObj.Types.Override = filtered
|
|
125
|
+
logger.debug(`Removed content type override for ${normalizedPart}`)
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -131,8 +133,10 @@ export class ContentTypesManager {
|
|
|
131
133
|
* @returns {boolean}
|
|
132
134
|
*/
|
|
133
135
|
hasDefault(extension) {
|
|
134
|
-
const extLower = extension.toLowerCase()
|
|
135
|
-
return this.#contentTypesObj.Types.Default.some(
|
|
136
|
+
const extLower = extension.toLowerCase()
|
|
137
|
+
return this.#contentTypesObj.Types.Default.some(
|
|
138
|
+
d => d['@_Extension']?.toLowerCase() === extLower
|
|
139
|
+
)
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
/**
|
|
@@ -142,8 +146,8 @@ export class ContentTypesManager {
|
|
|
142
146
|
* @returns {boolean}
|
|
143
147
|
*/
|
|
144
148
|
hasOverride(partName) {
|
|
145
|
-
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}
|
|
146
|
-
return this.#contentTypesObj.Types.Override.some(o => o['@_PartName'] === normalizedPart)
|
|
149
|
+
const normalizedPart = partName.startsWith('/') ? partName : `/${partName}`
|
|
150
|
+
return this.#contentTypesObj.Types.Override.some(o => o['@_PartName'] === normalizedPart)
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
/**
|
|
@@ -152,9 +156,11 @@ export class ContentTypesManager {
|
|
|
152
156
|
* @param {ZipManager} zipManager
|
|
153
157
|
*/
|
|
154
158
|
flush(zipManager) {
|
|
155
|
-
const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
156
|
-
const xml = this.#xmlParser.build(this.#contentTypesObj, declaration)
|
|
157
|
-
zipManager.writeFile(TYPES_XML_PATH, xml)
|
|
158
|
-
logger.debug(`Flushed ${TYPES_XML_PATH}`)
|
|
159
|
+
const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
160
|
+
const xml = this.#xmlParser.build(this.#contentTypesObj, declaration)
|
|
161
|
+
zipManager.writeFile(TYPES_XML_PATH, xml)
|
|
162
|
+
logger.debug(`Flushed ${TYPES_XML_PATH}`)
|
|
159
163
|
}
|
|
160
164
|
}
|
|
165
|
+
|
|
166
|
+
module.exports = { ContentTypesManager }
|