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.
Files changed (37) hide show
  1. package/README.md +336 -281
  2. package/package.json +6 -6
  3. package/src/cli/commands/build.js +32 -31
  4. package/src/cli/commands/debug.js +25 -24
  5. package/src/cli/commands/extract.js +23 -21
  6. package/src/cli/commands/inspect.js +25 -23
  7. package/src/cli/commands/validate.js +19 -17
  8. package/src/cli/index.js +45 -43
  9. package/src/core/OutputWriter.js +81 -78
  10. package/src/core/PPTXTemplater.js +859 -274
  11. package/src/core/TemplateEngine.js +69 -71
  12. package/src/core/ValidationEngine.js +246 -0
  13. package/src/index.js +51 -15
  14. package/src/managers/ChartManager.js +197 -70
  15. package/src/managers/ContentTypesManager.js +51 -45
  16. package/src/managers/HyperlinkManager.js +148 -142
  17. package/src/managers/ImageManager.js +336 -0
  18. package/src/managers/MediaManager.js +64 -81
  19. package/src/managers/RelationshipManager.js +102 -96
  20. package/src/managers/ShapeManager.js +340 -0
  21. package/src/managers/SlideManager.js +410 -311
  22. package/src/managers/TableManager.js +981 -262
  23. package/src/managers/TextManager.js +197 -0
  24. package/src/managers/ZipManager.js +71 -69
  25. package/src/managers/charts/ChartCacheGenerator.js +77 -58
  26. package/src/managers/charts/ChartParser.js +11 -13
  27. package/src/managers/charts/ChartRelationshipManager.js +14 -10
  28. package/src/managers/charts/ChartWorkbookUpdater.js +61 -56
  29. package/src/parsers/XMLParser.js +50 -49
  30. package/src/templates/blankPptx.js +3 -1
  31. package/src/templates/slideTemplate.js +31 -32
  32. package/src/utils/contentTypesHelper.js +41 -53
  33. package/src/utils/errors.js +33 -23
  34. package/src/utils/idUtils.js +23 -15
  35. package/src/utils/logger.js +21 -15
  36. package/src/utils/relationshipUtils.js +28 -22
  37. 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 }