node-pptx-templater 1.0.3 → 1.0.5

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.
@@ -0,0 +1,434 @@
1
+ /**
2
+ * @fileoverview ZOrderManager - Handles slide element Z-order (layer stacking) operations.
3
+ */
4
+
5
+ const { createLogger } = require('../utils/logger.js')
6
+ const { PPTXError } = require('../utils/errors.js')
7
+ const { Z_ORDER_SYMBOL } = require('../parsers/XMLParser.js')
8
+
9
+ const logger = createLogger('ZOrderManager')
10
+
11
+ const drawingTags = new Set(['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp'])
12
+
13
+ function detectElementType(tag, item) {
14
+ if (tag === 'p:sp') {
15
+ const isTxBox =
16
+ item?.['p:nvSpPr']?.['p:cNvSpPr']?.['@_txBox'] === '1' ||
17
+ item?.['p:nvSpPr']?.['p:cNvSpPr']?.['@_txBox'] === true
18
+ const phType = item?.['p:nvSpPr']?.['p:nvPr']?.['p:ph']?.['@_type']
19
+ if (isTxBox || phType === 'title' || phType === 'body' || item?.['p:txBody']) {
20
+ return 'text'
21
+ }
22
+ return 'shape'
23
+ }
24
+ if (tag === 'p:pic') {
25
+ return 'image'
26
+ }
27
+ if (tag === 'p:graphicFrame') {
28
+ const uri = item?.['a:graphic']?.['a:graphicData']?.['@_uri'] || ''
29
+ if (uri.includes('chart')) return 'chart'
30
+ if (uri.includes('table')) return 'table'
31
+ if (uri.includes('diagram')) return 'smartart'
32
+ return 'graphicFrame'
33
+ }
34
+ if (tag === 'p:grpSp') {
35
+ return 'group'
36
+ }
37
+ if (tag === 'p:cxnSp') {
38
+ return 'connector'
39
+ }
40
+ return 'unknown'
41
+ }
42
+
43
+ /**
44
+ * @class ZOrderManager
45
+ * @description Manages stacking layers and z-index of shapes on slides.
46
+ */
47
+ class ZOrderManager {
48
+ /** @private @type {XMLParser} */
49
+ #xmlParser
50
+
51
+ constructor(xmlParser) {
52
+ this.#xmlParser = xmlParser
53
+ }
54
+
55
+ /**
56
+ * Helper to parse Z-order array for a container or initialize it if missing.
57
+ */
58
+ getOrInitZOrder(container) {
59
+ if (!container[Z_ORDER_SYMBOL]) {
60
+ const list = []
61
+ for (const tag of ['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp']) {
62
+ let items = container[tag] || []
63
+ if (!Array.isArray(items)) items = [items]
64
+ for (const item of items) {
65
+ let id = null
66
+ if (tag === 'p:sp') id = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_id']
67
+ else if (tag === 'p:pic') id = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_id']
68
+ else if (tag === 'p:graphicFrame')
69
+ id = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id']
70
+ else if (tag === 'p:grpSp') id = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id']
71
+ else if (tag === 'p:cxnSp') id = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_id']
72
+
73
+ if (id !== undefined && id !== null) {
74
+ list.push(String(id))
75
+ }
76
+ }
77
+ }
78
+ container[Z_ORDER_SYMBOL] = list
79
+ }
80
+ return container[Z_ORDER_SYMBOL]
81
+ }
82
+
83
+ /**
84
+ * Helper to find drawing element and its parent container.
85
+ */
86
+ findObjectByIdOrName(container, targetId) {
87
+ if (!container) return null
88
+
89
+ for (const tag of ['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp']) {
90
+ let items = container[tag] || []
91
+ if (!Array.isArray(items)) items = [items]
92
+ for (const item of items) {
93
+ let id = null
94
+ let name = null
95
+ if (tag === 'p:sp') {
96
+ id = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_id']
97
+ name = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_name']
98
+ } else if (tag === 'p:pic') {
99
+ id = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_id']
100
+ name = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_name']
101
+ } else if (tag === 'p:graphicFrame') {
102
+ id = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id']
103
+ name = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_name']
104
+ } else if (tag === 'p:grpSp') {
105
+ id = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id']
106
+ name = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_name']
107
+ } else if (tag === 'p:cxnSp') {
108
+ id = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_id']
109
+ name = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_name']
110
+ }
111
+
112
+ if (String(id) === String(targetId) || name === targetId) {
113
+ return { tag, obj: item, id: String(id), name, parent: container }
114
+ }
115
+
116
+ if (tag === 'p:grpSp') {
117
+ const res = this.findObjectByIdOrName(item, targetId)
118
+ if (res) return res
119
+ }
120
+ }
121
+ }
122
+ return null
123
+ }
124
+
125
+ /**
126
+ * Retrieves the Z-order sequence of objects on a slide.
127
+ *
128
+ * @param {number} slideIndex
129
+ * @param {SlideManager} slideManager
130
+ * @returns {Array<Object>} List of object metadata in stacking order.
131
+ */
132
+ getObjectOrder(slideIndex, slideManager) {
133
+ const slideXml = slideManager.getSlideXml(slideIndex)
134
+ const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
135
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
136
+ if (!spTree) return []
137
+
138
+ const zOrder = this.getOrInitZOrder(spTree)
139
+
140
+ const drawingElements = new Map()
141
+ for (const tag of ['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp']) {
142
+ let items = spTree[tag] || []
143
+ if (!Array.isArray(items)) items = [items]
144
+ for (const item of items) {
145
+ let id = null
146
+ let name = null
147
+ if (tag === 'p:sp') {
148
+ id = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_id']
149
+ name = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_name']
150
+ } else if (tag === 'p:pic') {
151
+ id = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_id']
152
+ name = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_name']
153
+ } else if (tag === 'p:graphicFrame') {
154
+ id = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id']
155
+ name = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_name']
156
+ } else if (tag === 'p:grpSp') {
157
+ id = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id']
158
+ name = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_name']
159
+ } else if (tag === 'p:cxnSp') {
160
+ id = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_id']
161
+ name = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_name']
162
+ }
163
+
164
+ if (id !== undefined && id !== null) {
165
+ drawingElements.set(String(id), { tag, obj: item, name })
166
+ }
167
+ }
168
+ }
169
+
170
+ const fullZOrder = [...zOrder]
171
+ for (const id of drawingElements.keys()) {
172
+ if (!fullZOrder.includes(id)) {
173
+ fullZOrder.push(id)
174
+ }
175
+ }
176
+
177
+ const result = []
178
+ let zIndex = 1
179
+ for (const id of fullZOrder) {
180
+ const el = drawingElements.get(id)
181
+ if (!el) continue
182
+
183
+ result.push({
184
+ id: el.name || id,
185
+ type: detectElementType(el.tag, el.obj),
186
+ zIndex: zIndex++,
187
+ })
188
+ }
189
+
190
+ return result
191
+ }
192
+
193
+ /**
194
+ * Core reordering orchestrator.
195
+ * Runs the reorder callback on the correct container and saves the slide XML.
196
+ */
197
+ #modifyZOrder(slideIndex, objectId, slideManager, callback) {
198
+ const slideXml = slideManager.getSlideXml(slideIndex)
199
+ const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
200
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
201
+ if (!spTree) {
202
+ throw new PPTXError(`Invalid slide structure for slide ${slideIndex}`)
203
+ }
204
+
205
+ const res = this.findObjectByIdOrName(spTree, objectId)
206
+ if (!res) {
207
+ throw new PPTXError(`Object "${objectId}" not found on slide ${slideIndex}`)
208
+ }
209
+
210
+ const container = res.parent
211
+ const zOrder = this.getOrInitZOrder(container)
212
+
213
+ callback(zOrder, res.id, spTree)
214
+
215
+ const decl = this.#xmlParser.extractDeclaration(slideXml)
216
+ slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
217
+ }
218
+
219
+ bringForward(slideIndex, objectId, slideManager) {
220
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId) => {
221
+ const idx = zOrder.indexOf(elementId)
222
+ if (idx !== -1 && idx < zOrder.length - 1) {
223
+ zOrder[idx] = zOrder[idx + 1]
224
+ zOrder[idx + 1] = elementId
225
+ }
226
+ })
227
+ }
228
+
229
+ sendBackward(slideIndex, objectId, slideManager) {
230
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId) => {
231
+ const idx = zOrder.indexOf(elementId)
232
+ if (idx > 0) {
233
+ zOrder[idx] = zOrder[idx - 1]
234
+ zOrder[idx - 1] = elementId
235
+ }
236
+ })
237
+ }
238
+
239
+ bringToFront(slideIndex, objectId, slideManager) {
240
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId) => {
241
+ const idx = zOrder.indexOf(elementId)
242
+ if (idx !== -1) {
243
+ zOrder.splice(idx, 1)
244
+ zOrder.push(elementId)
245
+ }
246
+ })
247
+ }
248
+
249
+ sendToBack(slideIndex, objectId, slideManager) {
250
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId) => {
251
+ const idx = zOrder.indexOf(elementId)
252
+ if (idx !== -1) {
253
+ zOrder.splice(idx, 1)
254
+ zOrder.unshift(elementId)
255
+ }
256
+ })
257
+ }
258
+
259
+ setZIndex(slideIndex, objectId, zIndex, slideManager) {
260
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId) => {
261
+ const idx = zOrder.indexOf(elementId)
262
+ if (idx !== -1) {
263
+ zOrder.splice(idx, 1)
264
+ const targetIdx = Math.max(0, Math.min(zIndex - 1, zOrder.length))
265
+ zOrder.splice(targetIdx, 0, elementId)
266
+ }
267
+ })
268
+ }
269
+
270
+ moveObjectBefore(slideIndex, objectId, targetId, slideManager) {
271
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId, spTree) => {
272
+ const targetRes = this.findObjectByIdOrName(spTree, targetId)
273
+ if (!targetRes) {
274
+ throw new PPTXError(`Target object "${targetId}" not found on slide ${slideIndex}`)
275
+ }
276
+ if (targetRes.parent !== targetRes.parent) {
277
+ throw new PPTXError('Cannot move elements across different group containers')
278
+ }
279
+
280
+ const idx = zOrder.indexOf(elementId)
281
+ if (idx !== -1) {
282
+ zOrder.splice(idx, 1)
283
+ }
284
+ const targetIdx = zOrder.indexOf(targetRes.id)
285
+ if (targetIdx !== -1) {
286
+ zOrder.splice(targetIdx, 0, elementId)
287
+ } else {
288
+ zOrder.push(elementId)
289
+ }
290
+ })
291
+ }
292
+
293
+ moveObjectAfter(slideIndex, objectId, targetId, slideManager) {
294
+ this.#modifyZOrder(slideIndex, objectId, slideManager, (zOrder, elementId, spTree) => {
295
+ const targetRes = this.findObjectByIdOrName(spTree, targetId)
296
+ if (!targetRes) {
297
+ throw new PPTXError(`Target object "${targetId}" not found on slide ${slideIndex}`)
298
+ }
299
+
300
+ const idx = zOrder.indexOf(elementId)
301
+ if (idx !== -1) {
302
+ zOrder.splice(idx, 1)
303
+ }
304
+ const targetIdx = zOrder.indexOf(targetRes.id)
305
+ if (targetIdx !== -1) {
306
+ zOrder.splice(targetIdx + 1, 0, elementId)
307
+ } else {
308
+ zOrder.push(elementId)
309
+ }
310
+ })
311
+ }
312
+
313
+ reorderObjects(slideIndex, order, slideManager) {
314
+ const slideXml = slideManager.getSlideXml(slideIndex)
315
+ const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
316
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
317
+ if (!spTree) return
318
+
319
+ const zOrder = this.getOrInitZOrder(spTree)
320
+
321
+ // Resolve ordered names/IDs to existing drawing IDs
322
+ const resolvedIds = []
323
+ for (const item of order) {
324
+ const res = this.findObjectByIdOrName(spTree, item)
325
+ if (res && res.parent === spTree) {
326
+ resolvedIds.push(res.id)
327
+ }
328
+ }
329
+
330
+ // Keep track of unspecified IDs
331
+ const unspecifiedIds = zOrder.filter(id => !resolvedIds.includes(id))
332
+
333
+ // Reconstruct the z-order: unspecified bottom, specified top
334
+ spTree[Z_ORDER_SYMBOL] = [...unspecifiedIds, ...resolvedIds]
335
+
336
+ const decl = this.#xmlParser.extractDeclaration(slideXml)
337
+ slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
338
+ }
339
+
340
+ applyZOrder(slideIndex, configs, slideManager) {
341
+ if (!Array.isArray(configs)) return
342
+
343
+ for (const config of configs) {
344
+ if (!config.id) continue
345
+
346
+ if (config.bringToFront) {
347
+ this.bringToFront(slideIndex, config.id, slideManager)
348
+ } else if (config.sendToBack) {
349
+ this.sendToBack(slideIndex, config.id, slideManager)
350
+ } else if (config.bringForward) {
351
+ this.bringForward(slideIndex, config.id, slideManager)
352
+ } else if (config.sendBackward) {
353
+ this.sendBackward(slideIndex, config.id, slideManager)
354
+ } else if (config.zIndex !== undefined) {
355
+ this.setZIndex(slideIndex, config.id, config.zIndex, slideManager)
356
+ }
357
+ }
358
+ }
359
+
360
+ // Layer Utilities
361
+ getTopMostObject(slideIndex, slideManager) {
362
+ const order = this.getObjectOrder(slideIndex, slideManager)
363
+ return order.length > 0 ? order[order.length - 1] : null
364
+ }
365
+
366
+ getBottomMostObject(slideIndex, slideManager) {
367
+ const order = this.getObjectOrder(slideIndex, slideManager)
368
+ return order.length > 0 ? order[0] : null
369
+ }
370
+
371
+ swapObjects(slideIndex, objectId1, objectId2, slideManager) {
372
+ this.#modifyZOrder(slideIndex, objectId1, slideManager, (zOrder, elementId1, spTree) => {
373
+ const res2 = this.findObjectByIdOrName(spTree, objectId2)
374
+ if (!res2) {
375
+ throw new PPTXError(`Object "${objectId2}" not found on slide ${slideIndex}`)
376
+ }
377
+ const elementId2 = res2.id
378
+ const idx1 = zOrder.indexOf(elementId1)
379
+ const idx2 = zOrder.indexOf(elementId2)
380
+ if (idx1 !== -1 && idx2 !== -1) {
381
+ zOrder[idx1] = elementId2
382
+ zOrder[idx2] = elementId1
383
+ }
384
+ })
385
+ }
386
+
387
+ sortObjects(slideIndex, compareFn, slideManager) {
388
+ const slideXml = slideManager.getSlideXml(slideIndex)
389
+ const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
390
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
391
+ if (!spTree) return
392
+
393
+ const zOrder = this.getOrInitZOrder(spTree)
394
+
395
+ // Build complete info list
396
+ const order = this.getObjectOrder(slideIndex, slideManager)
397
+
398
+ // Sort order using compareFn
399
+ order.sort(compareFn)
400
+
401
+ // Map sorted IDs back
402
+ const sortedIds = []
403
+ for (const item of order) {
404
+ // Find ID by name or matching ID in container
405
+ const res = this.findObjectByIdOrName(spTree, item.id)
406
+ if (res && res.parent === spTree) {
407
+ sortedIds.push(res.id)
408
+ }
409
+ }
410
+
411
+ // Preserve unspecified ones at bottom
412
+ const unspecifiedIds = zOrder.filter(id => !sortedIds.includes(id))
413
+ spTree[Z_ORDER_SYMBOL] = [...unspecifiedIds, ...sortedIds]
414
+
415
+ const decl = this.#xmlParser.extractDeclaration(slideXml)
416
+ slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
417
+ }
418
+
419
+ normalizeZOrder(slideIndex, slideManager) {
420
+ const slideXml = slideManager.getSlideXml(slideIndex)
421
+ const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
422
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
423
+ if (!spTree) return
424
+
425
+ // Re-initialize and clean up
426
+ delete spTree[Z_ORDER_SYMBOL]
427
+ this.getOrInitZOrder(spTree)
428
+
429
+ const decl = this.#xmlParser.extractDeclaration(slideXml)
430
+ slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
431
+ }
432
+ }
433
+
434
+ module.exports = { ZOrderManager }
@@ -23,6 +23,107 @@
23
23
  const { XMLParser: FastXMLParser, XMLBuilder } = require('fast-xml-parser')
24
24
  const { PPTXError } = require('../utils/errors.js')
25
25
 
26
+ const Z_ORDER_SYMBOL = Symbol('zOrder')
27
+
28
+ const drawingTags = new Set(['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp'])
29
+
30
+ function findcNvPr(node) {
31
+ const tagName = Object.keys(node).find(k => k !== ':@')
32
+ if (!tagName) return null
33
+ const children = node[tagName]
34
+ if (!Array.isArray(children)) return null
35
+
36
+ const nvPrNode = children.find(child => {
37
+ const k = Object.keys(child).find(key => key !== ':@')
38
+ return (
39
+ k &&
40
+ (k === 'p:nvSpPr' ||
41
+ k === 'p:nvPicPr' ||
42
+ k === 'p:nvGraphicFramePr' ||
43
+ k === 'p:nvGrpSpPr' ||
44
+ k === 'p:nvCxnSpPr')
45
+ )
46
+ })
47
+
48
+ if (nvPrNode) {
49
+ const nvPrTagName = Object.keys(nvPrNode).find(k => k !== ':@')
50
+ const nvPrChildren = nvPrNode[nvPrTagName]
51
+ if (Array.isArray(nvPrChildren)) {
52
+ const cNvPrNode = nvPrChildren.find(child => Object.keys(child).includes('p:cNvPr'))
53
+ if (cNvPrNode) {
54
+ return cNvPrNode[':@']
55
+ }
56
+ }
57
+ }
58
+ return null
59
+ }
60
+
61
+ function buildZOrderMap(orderedNode, containerMap = new Map()) {
62
+ if (!orderedNode) return containerMap
63
+ const tagName = Object.keys(orderedNode).find(k => k !== ':@')
64
+ if (!tagName) return containerMap
65
+ const children = orderedNode[tagName]
66
+ if (!Array.isArray(children)) return containerMap
67
+
68
+ if (tagName === 'p:spTree') {
69
+ const order = []
70
+ for (const child of children) {
71
+ const childTagName = Object.keys(child).find(k => k !== ':@')
72
+ if (drawingTags.has(childTagName)) {
73
+ const attrs = findcNvPr(child)
74
+ if (attrs && attrs['@_id']) {
75
+ order.push(String(attrs['@_id']))
76
+ }
77
+ buildZOrderMap(child, containerMap)
78
+ }
79
+ }
80
+ containerMap.set('root', order)
81
+ } else if (tagName === 'p:grpSp') {
82
+ const attrs = findcNvPr(orderedNode)
83
+ const grpId = attrs ? String(attrs['@_id']) : null
84
+ const order = []
85
+ for (const child of children) {
86
+ const childTagName = Object.keys(child).find(k => k !== ':@')
87
+ if (drawingTags.has(childTagName)) {
88
+ const attrs = findcNvPr(child)
89
+ if (attrs && attrs['@_id']) {
90
+ order.push(String(attrs['@_id']))
91
+ }
92
+ buildZOrderMap(child, containerMap)
93
+ }
94
+ }
95
+ if (grpId) {
96
+ containerMap.set(grpId, order)
97
+ }
98
+ } else {
99
+ for (const child of children) {
100
+ buildZOrderMap(child, containerMap)
101
+ }
102
+ }
103
+
104
+ return containerMap
105
+ }
106
+
107
+ function attachZOrder(normalContainer, containerId, containerMap) {
108
+ if (!normalContainer) return
109
+
110
+ const order = containerMap.get(containerId)
111
+ if (order) {
112
+ normalContainer[Z_ORDER_SYMBOL] = order
113
+ }
114
+
115
+ let grpSps = normalContainer['p:grpSp'] || []
116
+ if (!Array.isArray(grpSps)) grpSps = [grpSps]
117
+
118
+ for (const grpSp of grpSps) {
119
+ const cNvPr = grpSp?.['p:nvGrpSpPr']?.['p:cNvPr']
120
+ const grpId = cNvPr ? String(cNvPr['@_id']) : null
121
+ if (grpId) {
122
+ attachZOrder(grpSp, grpId, containerMap)
123
+ }
124
+ }
125
+ }
126
+
26
127
  /**
27
128
  * Parser configuration for fast-xml-parser.
28
129
  * These settings ensure lossless round-trip XML parsing.
@@ -101,9 +202,22 @@ class XMLParser {
101
202
  */
102
203
  #builder
103
204
 
205
+ /**
206
+ * @private
207
+ * @type {FastXMLParser}
208
+ */
209
+ #orderedParser
210
+
104
211
  constructor() {
105
212
  this.#parser = new FastXMLParser(PARSER_OPTIONS)
106
213
  this.#builder = new XMLBuilder(BUILDER_OPTIONS)
214
+ this.#orderedParser = new FastXMLParser({
215
+ ignoreAttributes: false,
216
+ preserveOrder: true,
217
+ attributeNamePrefix: '@_',
218
+ parseAttributeValue: false,
219
+ parseTagValue: false,
220
+ })
107
221
  }
108
222
 
109
223
  /**
@@ -124,7 +238,31 @@ class XMLParser {
124
238
  }
125
239
 
126
240
  try {
127
- return this.#parser.parse(xmlString)
241
+ const normalObj = this.#parser.parse(xmlString)
242
+
243
+ // Automatically inspect for spTree to build and attach Z-order
244
+ const spTree =
245
+ normalObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
246
+ normalObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
247
+ normalObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
248
+
249
+ if (spTree) {
250
+ try {
251
+ const orderedObj = this.#orderedParser.parse(xmlString)
252
+ const orderedRootNode = orderedObj.find(n => {
253
+ const keys = Object.keys(n)
254
+ return (
255
+ keys.includes('p:sld') || keys.includes('p:sldLayout') || keys.includes('p:sldMaster')
256
+ )
257
+ })
258
+ const containerMap = buildZOrderMap(orderedRootNode)
259
+ attachZOrder(spTree, 'root', containerMap)
260
+ } catch (err) {
261
+ // Fallback gracefully if ordered parsing fails
262
+ }
263
+ }
264
+
265
+ return normalObj
128
266
  } catch (err) {
129
267
  throw new PPTXError(`XML parse error${context ? ` in ${context}` : ''}: ${err.message}`, err)
130
268
  }
@@ -142,13 +280,101 @@ class XMLParser {
142
280
  */
143
281
  build(obj, xmlDeclaration = '') {
144
282
  try {
145
- const xml = this.#builder.build(obj)
283
+ const spTreeObj =
284
+ obj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
285
+ obj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
286
+ obj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
287
+
288
+ let xml = this.#builder.build(obj)
289
+
290
+ if (spTreeObj) {
291
+ const correctSpTreeXml = this.serializeContainer(spTreeObj, 'p:spTree')
292
+ if (xml.includes('<p:spTree/>')) {
293
+ xml = xml.replace('<p:spTree/>', correctSpTreeXml)
294
+ } else {
295
+ xml = xml.replace(/<p:spTree>[\s\S]*<\/p:spTree>/, correctSpTreeXml)
296
+ }
297
+ }
298
+
146
299
  return xmlDeclaration ? `${xmlDeclaration}\n${xml}` : xml
147
300
  } catch (err) {
148
301
  throw new PPTXError(`XML build error: ${err.message}`, err)
149
302
  }
150
303
  }
151
304
 
305
+ /**
306
+ * Helper to serialize drawing containers (p:spTree, p:grpSp) recursively in Z-order.
307
+ */
308
+ serializeContainer(container, containerTagName) {
309
+ const zOrder = container[Z_ORDER_SYMBOL] || []
310
+
311
+ let headerXml = ''
312
+ if (container['p:nvGrpSpPr']) {
313
+ headerXml += this.#builder.build({ 'p:nvGrpSpPr': container['p:nvGrpSpPr'] })
314
+ }
315
+ if (container['p:grpSpPr']) {
316
+ headerXml += this.#builder.build({ 'p:grpSpPr': container['p:grpSpPr'] })
317
+ }
318
+
319
+ // Gather drawing children
320
+ const drawingElements = new Map()
321
+ for (const tag of ['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp']) {
322
+ let items = container[tag] || []
323
+ if (!Array.isArray(items)) items = [items]
324
+ for (const item of items) {
325
+ let id = null
326
+ if (tag === 'p:sp') id = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_id']
327
+ else if (tag === 'p:pic') id = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_id']
328
+ else if (tag === 'p:graphicFrame') id = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id']
329
+ else if (tag === 'p:grpSp') id = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id']
330
+ else if (tag === 'p:cxnSp') id = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_id']
331
+
332
+ if (id !== undefined && id !== null) {
333
+ drawingElements.set(String(id), { tag, obj: item })
334
+ }
335
+ }
336
+ }
337
+
338
+ // Append items not in the explicit Z-order list
339
+ const fullZOrder = [...zOrder]
340
+ for (const id of drawingElements.keys()) {
341
+ if (!fullZOrder.includes(id)) {
342
+ fullZOrder.push(id)
343
+ }
344
+ }
345
+
346
+ // Build children in Z-order
347
+ let childrenXml = ''
348
+ for (const id of fullZOrder) {
349
+ const el = drawingElements.get(id)
350
+ if (!el) continue
351
+
352
+ if (el.tag === 'p:grpSp') {
353
+ childrenXml += this.serializeContainer(el.obj, 'p:grpSp')
354
+ } else {
355
+ childrenXml += this.#builder.build({ [el.tag]: el.obj })
356
+ }
357
+ }
358
+
359
+ // Container attributes
360
+ let attrsStr = ''
361
+ const attrs = {}
362
+ for (const k in container) {
363
+ if (k.startsWith('@_')) {
364
+ attrs[k] = container[k]
365
+ }
366
+ }
367
+ if (Object.keys(attrs).length > 0) {
368
+ const attrXml = this.#builder.build({ [containerTagName]: { ...attrs } })
369
+ const match = attrXml.match(/<[^>]+>/)
370
+ if (match) {
371
+ attrsStr = match[0].slice(containerTagName.length + 1, -1)
372
+ }
373
+ }
374
+
375
+ return `<${containerTagName}${attrsStr}>${headerXml}${childrenXml}</${containerTagName}>`
376
+ }
377
+
152
378
  /**
153
379
  * Extracts the XML declaration line from an XML string.
154
380
  *
@@ -289,4 +515,5 @@ class XMLParser {
289
515
 
290
516
  module.exports = {
291
517
  XMLParser,
518
+ Z_ORDER_SYMBOL,
292
519
  }