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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ShapeManager - Handles shape text replacement, cloning, deletion, and enumeration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { createLogger } = require('../utils/logger.js')
|
|
6
|
+
const { PPTXError } = require('../utils/errors.js')
|
|
7
|
+
|
|
8
|
+
const logger = createLogger('ShapeManager')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @class ShapeManager
|
|
12
|
+
* @description Manages shape elements inside PPTX slides.
|
|
13
|
+
*/
|
|
14
|
+
class ShapeManager {
|
|
15
|
+
/** @private @type {XMLParser} */
|
|
16
|
+
#xmlParser
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {XMLParser} xmlParser
|
|
20
|
+
*/
|
|
21
|
+
constructor(xmlParser) {
|
|
22
|
+
this.#xmlParser = xmlParser
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Updates shape text content.
|
|
27
|
+
*
|
|
28
|
+
* @param {number} slideIndex
|
|
29
|
+
* @param {string} shapeId
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @param {SlideManager} slideManager
|
|
32
|
+
*/
|
|
33
|
+
updateShapeText(slideIndex, shapeId, text, slideManager) {
|
|
34
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
35
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
36
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
37
|
+
const res = this.findShapeRecursive(spTree, shapeId)
|
|
38
|
+
|
|
39
|
+
if (!res) {
|
|
40
|
+
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.#setShapeTextObj(res.shape, text)
|
|
44
|
+
|
|
45
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
46
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
47
|
+
logger.debug(`Updated text for shape "${shapeId}" on slide ${slideIndex}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clones a shape and adds it with offsets.
|
|
52
|
+
*
|
|
53
|
+
* @param {number} slideIndex
|
|
54
|
+
* @param {string} shapeId
|
|
55
|
+
* @param {string} newShapeId
|
|
56
|
+
* @param {Object} options
|
|
57
|
+
* @param {SlideManager} slideManager
|
|
58
|
+
*/
|
|
59
|
+
cloneShape(slideIndex, shapeId, newShapeId, options = {}, slideManager) {
|
|
60
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
61
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
62
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
63
|
+
const res = this.findShapeRecursive(spTree, shapeId)
|
|
64
|
+
|
|
65
|
+
if (!res) {
|
|
66
|
+
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const newShape = this.#xmlParser.deepClone(res.shape)
|
|
70
|
+
const cNvPr = newShape['p:nvSpPr']?.['p:cNvPr']
|
|
71
|
+
|
|
72
|
+
const existingIds = this.#getAllShapeIds(spTree)
|
|
73
|
+
const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 1000
|
|
74
|
+
const newId = maxId + 1
|
|
75
|
+
|
|
76
|
+
if (cNvPr) {
|
|
77
|
+
cNvPr['@_id'] = String(newId)
|
|
78
|
+
cNvPr['@_name'] = newShapeId
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const xfrm = newShape['p:spPr']?.['a:xfrm']
|
|
82
|
+
if (xfrm) {
|
|
83
|
+
if (options.offsetX !== undefined) {
|
|
84
|
+
const dx =
|
|
85
|
+
options.offsetX < 100 ? Math.round(options.offsetX * 914400) : Math.round(options.offsetX)
|
|
86
|
+
const x = parseInt(xfrm['a:off']?.['@_x'] || 0, 10) + dx
|
|
87
|
+
if (!xfrm['a:off']) xfrm['a:off'] = {}
|
|
88
|
+
xfrm['a:off']['@_x'] = String(x)
|
|
89
|
+
}
|
|
90
|
+
if (options.offsetY !== undefined) {
|
|
91
|
+
const dy =
|
|
92
|
+
options.offsetY < 100 ? Math.round(options.offsetY * 914400) : Math.round(options.offsetY)
|
|
93
|
+
const y = parseInt(xfrm['a:off']?.['@_y'] || 0, 10) + dy
|
|
94
|
+
if (!xfrm['a:off']) xfrm['a:off'] = {}
|
|
95
|
+
xfrm['a:off']['@_y'] = String(y)
|
|
96
|
+
}
|
|
97
|
+
if (options.width !== undefined) {
|
|
98
|
+
const cx =
|
|
99
|
+
options.width < 100 ? Math.round(options.width * 914400) : Math.round(options.width)
|
|
100
|
+
if (!xfrm['a:ext']) xfrm['a:ext'] = {}
|
|
101
|
+
xfrm['a:ext']['@_cx'] = String(cx)
|
|
102
|
+
}
|
|
103
|
+
if (options.height !== undefined) {
|
|
104
|
+
const cy =
|
|
105
|
+
options.height < 100 ? Math.round(options.height * 914400) : Math.round(options.height)
|
|
106
|
+
if (!xfrm['a:ext']) xfrm['a:ext'] = {}
|
|
107
|
+
xfrm['a:ext']['@_cy'] = String(cy)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const parent = res.parent
|
|
112
|
+
if (!parent['p:sp']) parent['p:sp'] = []
|
|
113
|
+
if (!Array.isArray(parent['p:sp'])) {
|
|
114
|
+
parent['p:sp'] = [parent['p:sp']]
|
|
115
|
+
}
|
|
116
|
+
parent['p:sp'].push(newShape)
|
|
117
|
+
|
|
118
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
119
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
120
|
+
logger.debug(`Cloned shape "${shapeId}" as "${newShapeId}"`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Deletes a shape from the slide.
|
|
125
|
+
*
|
|
126
|
+
* @param {number} slideIndex
|
|
127
|
+
* @param {string} shapeId
|
|
128
|
+
* @param {SlideManager} slideManager
|
|
129
|
+
*/
|
|
130
|
+
deleteShape(slideIndex, shapeId, slideManager) {
|
|
131
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
132
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
133
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
134
|
+
const res = this.findShapeRecursive(spTree, shapeId)
|
|
135
|
+
|
|
136
|
+
if (!res) {
|
|
137
|
+
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const parent = res.parent
|
|
141
|
+
if (parent['p:sp']) {
|
|
142
|
+
if (Array.isArray(parent['p:sp'])) {
|
|
143
|
+
parent['p:sp'] = parent['p:sp'].filter(s => s !== res.shape)
|
|
144
|
+
} else if (parent['p:sp'] === res.shape) {
|
|
145
|
+
delete parent['p:sp']
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
150
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
151
|
+
logger.debug(`Deleted shape "${shapeId}" from slide ${slideIndex}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets all shapes inside a slide.
|
|
156
|
+
*
|
|
157
|
+
* @param {number} slideIndex
|
|
158
|
+
* @param {SlideManager} slideManager
|
|
159
|
+
* @returns {Array<Object>}
|
|
160
|
+
*/
|
|
161
|
+
getShapes(slideIndex, slideManager) {
|
|
162
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
163
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
164
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
165
|
+
|
|
166
|
+
const shapesInfo = []
|
|
167
|
+
this.#collectShapesInfo(spTree, shapesInfo)
|
|
168
|
+
return shapesInfo
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Helper to recursively scan a container for shapes.
|
|
173
|
+
*/
|
|
174
|
+
findShapeRecursive(container, shapeId) {
|
|
175
|
+
if (!container) return null
|
|
176
|
+
|
|
177
|
+
let shapes = container['p:sp'] || []
|
|
178
|
+
if (!Array.isArray(shapes)) shapes = [shapes]
|
|
179
|
+
|
|
180
|
+
for (const shape of shapes) {
|
|
181
|
+
const cNvPr = shape?.['p:nvSpPr']?.['p:cNvPr']
|
|
182
|
+
if (cNvPr) {
|
|
183
|
+
if (cNvPr['@_name'] === shapeId || String(cNvPr['@_id']) === shapeId) {
|
|
184
|
+
return { shape, parent: container, type: 'sp' }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let groups = container['p:grpSp'] || []
|
|
190
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
191
|
+
|
|
192
|
+
for (const group of groups) {
|
|
193
|
+
const res = this.findShapeRecursive(group, shapeId)
|
|
194
|
+
if (res) return res
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#setShapeTextObj(shape, text) {
|
|
201
|
+
const val = text === undefined || text === null ? '' : String(text)
|
|
202
|
+
|
|
203
|
+
if (!shape['p:txBody']) {
|
|
204
|
+
shape['p:txBody'] = {
|
|
205
|
+
'a:bodyPr': {},
|
|
206
|
+
'a:lstStyle': {},
|
|
207
|
+
'a:p': [],
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const txBody = shape['p:txBody']
|
|
212
|
+
if (!txBody['a:p']) {
|
|
213
|
+
txBody['a:p'] = []
|
|
214
|
+
}
|
|
215
|
+
if (!Array.isArray(txBody['a:p'])) {
|
|
216
|
+
txBody['a:p'] = [txBody['a:p']]
|
|
217
|
+
}
|
|
218
|
+
if (txBody['a:p'].length === 0) {
|
|
219
|
+
txBody['a:p'].push({})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lines = val.split(/\r?\n/)
|
|
223
|
+
const templatePara = txBody['a:p'][0]
|
|
224
|
+
const newParas = []
|
|
225
|
+
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
const p = this.#xmlParser.deepClone(templatePara)
|
|
228
|
+
if (!p['a:r']) {
|
|
229
|
+
p['a:r'] = []
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(p['a:r'])) {
|
|
232
|
+
p['a:r'] = [p['a:r']]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (p['a:r'].length === 0) {
|
|
236
|
+
p['a:r'].push({ 'a:t': line })
|
|
237
|
+
} else {
|
|
238
|
+
const firstRun = p['a:r'][0]
|
|
239
|
+
firstRun['a:t'] = line
|
|
240
|
+
p['a:r'] = [firstRun]
|
|
241
|
+
}
|
|
242
|
+
newParas.push(p)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
txBody['a:p'] = newParas
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#getAllShapeIds(container) {
|
|
249
|
+
const ids = []
|
|
250
|
+
if (!container) return ids
|
|
251
|
+
|
|
252
|
+
let shapes = container['p:sp'] || []
|
|
253
|
+
if (!Array.isArray(shapes)) shapes = [shapes]
|
|
254
|
+
for (const s of shapes) {
|
|
255
|
+
const id = parseInt(s?.['p:nvSpPr']?.['p:cNvPr']?.['@_id'], 10)
|
|
256
|
+
if (!isNaN(id)) ids.push(id)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let pics = container['p:pic'] || []
|
|
260
|
+
if (!Array.isArray(pics)) pics = [pics]
|
|
261
|
+
for (const p of pics) {
|
|
262
|
+
const id = parseInt(p?.['p:nvPicPr']?.['p:cNvPr']?.['@_id'], 10)
|
|
263
|
+
if (!isNaN(id)) ids.push(id)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let frames = container['p:graphicFrame'] || []
|
|
267
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
268
|
+
for (const f of frames) {
|
|
269
|
+
const id = parseInt(f?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id'], 10)
|
|
270
|
+
if (!isNaN(id)) ids.push(id)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let groups = container['p:grpSp'] || []
|
|
274
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
275
|
+
for (const g of groups) {
|
|
276
|
+
const id = parseInt(g?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id'], 10)
|
|
277
|
+
if (!isNaN(id)) ids.push(id)
|
|
278
|
+
ids.push(...this.#getAllShapeIds(g))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return ids
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#collectShapesInfo(container, results) {
|
|
285
|
+
if (!container) return
|
|
286
|
+
|
|
287
|
+
let shapes = container['p:sp'] || []
|
|
288
|
+
if (!Array.isArray(shapes)) shapes = [shapes]
|
|
289
|
+
|
|
290
|
+
for (const shape of shapes) {
|
|
291
|
+
const cNvPr = shape?.['p:nvSpPr']?.['p:cNvPr']
|
|
292
|
+
if (!cNvPr) continue
|
|
293
|
+
|
|
294
|
+
const name = cNvPr['@_name']
|
|
295
|
+
const id = String(cNvPr['@_id'])
|
|
296
|
+
|
|
297
|
+
let text = ''
|
|
298
|
+
const txBody = shape['p:txBody']
|
|
299
|
+
if (txBody && txBody['a:p']) {
|
|
300
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
301
|
+
const textParts = []
|
|
302
|
+
for (const p of paras) {
|
|
303
|
+
if (p['a:r']) {
|
|
304
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
305
|
+
for (const r of runs) {
|
|
306
|
+
if (r['a:t']) textParts.push(String(r['a:t']))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
text = textParts.join('\n')
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const xfrm = shape['p:spPr']?.['a:xfrm']
|
|
314
|
+
const position = xfrm
|
|
315
|
+
? {
|
|
316
|
+
x: parseInt(xfrm['a:off']?.['@_x'] || 0, 10),
|
|
317
|
+
y: parseInt(xfrm['a:off']?.['@_y'] || 0, 10),
|
|
318
|
+
cx: parseInt(xfrm['a:ext']?.['@_cx'] || 0, 10),
|
|
319
|
+
cy: parseInt(xfrm['a:ext']?.['@_cy'] || 0, 10),
|
|
320
|
+
}
|
|
321
|
+
: null
|
|
322
|
+
|
|
323
|
+
results.push({
|
|
324
|
+
type: 'shape',
|
|
325
|
+
id,
|
|
326
|
+
name,
|
|
327
|
+
text,
|
|
328
|
+
position,
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let groups = container['p:grpSp'] || []
|
|
333
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
334
|
+
for (const g of groups) {
|
|
335
|
+
this.#collectShapesInfo(g, results)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = { ShapeManager }
|