node-pptx-templater 1.0.4 → 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.
- package/CHANGELOG.md +33 -0
- package/README.md +107 -0
- package/package.json +1 -1
- package/src/core/PPTXTemplater.js +270 -0
- package/src/core/ValidationEngine.js +77 -0
- package/src/index.js +2 -1
- package/src/managers/ZOrderManager.js +434 -0
- package/src/parsers/XMLParser.js +229 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.5] - 2026-06-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Z-Order (Layer Management) System**: Full stacking control for all slide drawing objects — shapes, images, charts, tables, groups, connectors, and SmartArt. Directly manipulates the OpenXML `<p:spTree>` element order, matching PowerPoint's native Bring Forward / Send Backward behavior exactly. New APIs:
|
|
12
|
+
- `getObjectOrder(slideIndex)` — Returns ordered metadata (id, type, zIndex) for every element on a slide, bottom-to-top.
|
|
13
|
+
- `bringForward(options)` — Moves an object one layer up in the stack.
|
|
14
|
+
- `sendBackward(options)` — Moves an object one layer down.
|
|
15
|
+
- `bringToFront(options)` — Moves an object to the very top of the stack.
|
|
16
|
+
- `sendToBack(options)` — Moves an object to the very bottom of the stack.
|
|
17
|
+
- `setZIndex(options)` — Places an object at an exact 1-based stacking position.
|
|
18
|
+
- `moveObjectBefore(options)` — Positions an object immediately below a named target.
|
|
19
|
+
- `moveObjectAfter(options)` — Positions an object immediately above a named target.
|
|
20
|
+
- `reorderObjects(options)` — Full bulk reorder of the slide stack from a given array.
|
|
21
|
+
- `applyZOrder(slideIndex, configs)` — Applies multiple stacking rules sequentially in one call.
|
|
22
|
+
- `swapObjects(slideIndex, id1, id2)` — Exchanges two objects' positions.
|
|
23
|
+
- `sortObjects(slideIndex, compareFn)` — Sorts the stack using a custom comparator.
|
|
24
|
+
- `getTopMostObject(slideIndex)` / `getBottomMostObject(slideIndex)` — Inspection helpers.
|
|
25
|
+
- `normalizeZOrder(slideIndex)` — Re-derives and resets internal Z-order state from the current XML.
|
|
26
|
+
- **Z_ORDER_SYMBOL Export**: The `Z_ORDER_SYMBOL` is now exported from `src/index.js` for advanced integrations.
|
|
27
|
+
- **ZOrderManager**: New dedicated manager class (`src/managers/ZOrderManager.js`) encapsulating all layer logic.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **`PPTXTemplater.create()` synchronous readiness**: Added `preloadAll()` call to `#initializeBlank()`. Previously, the blank PPTX template's pre-existing slides were registered but their XML was not cached, causing all synchronous operations (including ZOrderManager) to throw `"Slide N XML not pre-loaded"`.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **`XMLParser` hybrid parsing**: Added a secondary `preserveOrder: true` fast-xml-parser pass that runs during `parse()` whenever a slide `<p:spTree>` is detected. Extracts DOM element order and attaches it via `Z_ORDER_SYMBOL` to each container. The `build()` method uses a new `serializeContainer()` recursive function to serialize containers in Z_ORDER_SYMBOL order, injecting the result back into the output XML.
|
|
34
|
+
- **`ValidationEngine`**: `validate()` now audits the shape tree for duplicate shape IDs, reporting them as errors.
|
|
35
|
+
|
|
36
|
+
### Tests
|
|
37
|
+
- Added 12 new integration tests in `tests/integration/ZOrder.test.js` covering all Z-order operations.
|
|
38
|
+
- Total test count increased from 96 → 108 (all passing).
|
|
39
|
+
|
|
8
40
|
## [1.0.3] - 2026-06-02
|
|
9
41
|
|
|
10
42
|
### Added
|
|
@@ -58,6 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
58
90
|
- Private class fields (`#field`) for encapsulation
|
|
59
91
|
- Modular architecture following SOLID principles
|
|
60
92
|
|
|
93
|
+
[1.0.4]: https://github.com/jsuyog2/node-pptx-templater/compare/v1.0.3...v1.0.4
|
|
61
94
|
[1.0.3]: https://github.com/jsuyog2/node-pptx-templater/compare/v1.0.2...v1.0.3
|
|
62
95
|
[1.0.2]: https://github.com/jsuyog2/node-pptx-templater/compare/v1.0.1...v1.0.2
|
|
63
96
|
[1.0.1]: https://github.com/jsuyog2/node-pptx-templater/compare/v1.0.0...v1.0.1
|
package/README.md
CHANGED
|
@@ -211,6 +211,113 @@ ppt.updateChartTitle('sales-chart', 'Revenue Growth (2026)');
|
|
|
211
211
|
|
|
212
212
|
---
|
|
213
213
|
|
|
214
|
+
### Z-Order (Layer Management)
|
|
215
|
+
|
|
216
|
+
Control the stacking order of shapes, images, charts, tables, groups, and connectors on any slide — just like PowerPoint's **Bring Forward / Send Backward** panel. The Z-order directly maps to the XML element order inside the slide's `<p:spTree>`, which is what PowerPoint reads when rendering.
|
|
217
|
+
|
|
218
|
+
All operations accept either an **options object** with a `slide` key or can be chained after `useSlide()`:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
// Option A — explicit slide number
|
|
222
|
+
ppt.bringForward({ slide: 2, objectId: 'logo' });
|
|
223
|
+
|
|
224
|
+
// Option B — fluent chain
|
|
225
|
+
ppt.useSlide(2).bringForward('logo');
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `getObjectOrder(slideIndex)`
|
|
229
|
+
Returns a sorted array describing every drawing element on the slide, bottom-to-top.
|
|
230
|
+
```js
|
|
231
|
+
const layers = ppt.getObjectOrder(1);
|
|
232
|
+
// → [{ id: 'Background', type: 'shape', zIndex: 1 }, ...]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### `bringForward(options)` / `sendBackward(options)`
|
|
236
|
+
Move an object one layer up or down.
|
|
237
|
+
```js
|
|
238
|
+
ppt.bringForward({ slide: 1, objectId: 'logo' });
|
|
239
|
+
ppt.sendBackward({ slide: 1, objectId: 'logo' });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### `bringToFront(options)` / `sendToBack(options)`
|
|
243
|
+
Move an object to the very top or very bottom of the stack.
|
|
244
|
+
```js
|
|
245
|
+
ppt.bringToFront({ slide: 1, objectId: 'logo' });
|
|
246
|
+
ppt.sendToBack({ slide: 1, objectId: 'background' });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### `setZIndex(options)`
|
|
250
|
+
Place an object at an exact 1-based stacking position.
|
|
251
|
+
```js
|
|
252
|
+
ppt.setZIndex({ slide: 1, objectId: 'logo', zIndex: 3 });
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `moveObjectBefore(options)` / `moveObjectAfter(options)`
|
|
256
|
+
Position an object immediately below or above a specific target.
|
|
257
|
+
```js
|
|
258
|
+
ppt.moveObjectBefore({ slide: 1, objectId: 'overlay', targetId: 'chart' });
|
|
259
|
+
ppt.moveObjectAfter({ slide: 1, objectId: 'label', targetId: 'chart' });
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `reorderObjects(options)`
|
|
263
|
+
Bulk-reorder the entire slide stack by specifying all object names in desired bottom-to-top order.
|
|
264
|
+
```js
|
|
265
|
+
ppt.reorderObjects({
|
|
266
|
+
slide: 1,
|
|
267
|
+
order: ['background', 'chart', 'logo', 'title']
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `applyZOrder(slideIndex, configs)`
|
|
272
|
+
Apply multiple stacking rules in a single call. Operations are executed sequentially.
|
|
273
|
+
```js
|
|
274
|
+
ppt.applyZOrder(1, [
|
|
275
|
+
{ id: 'background', sendToBack: true },
|
|
276
|
+
{ id: 'overlay', zIndex: 2 },
|
|
277
|
+
{ id: 'logo', bringToFront: true },
|
|
278
|
+
]);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### `swapObjects(slideIndex, objectId1, objectId2)`
|
|
282
|
+
Exchange the stacking positions of two objects.
|
|
283
|
+
```js
|
|
284
|
+
ppt.swapObjects(1, 'logo', 'chart');
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `sortObjects(slideIndex, compareFn)`
|
|
288
|
+
Sort the layer stack using a custom comparator (receives `{ id, type, zIndex }` objects).
|
|
289
|
+
```js
|
|
290
|
+
// Alphabetical ascending by name
|
|
291
|
+
ppt.sortObjects(1, (a, b) => a.id.localeCompare(b.id));
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `getTopMostObject(slideIndex)` / `getBottomMostObject(slideIndex)`
|
|
295
|
+
Retrieve metadata for the topmost or bottommost element.
|
|
296
|
+
```js
|
|
297
|
+
const top = ppt.getTopMostObject(1); // { id: 'logo', type: 'image', zIndex: 5 }
|
|
298
|
+
const bottom = ppt.getBottomMostObject(1); // { id: 'background', type: 'shape', zIndex: 1 }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### `normalizeZOrder(slideIndex)`
|
|
302
|
+
Re-derives the Z-order directly from the current XML element order. Useful after manual XML edits or imports to reset the internal ordering state.
|
|
303
|
+
```js
|
|
304
|
+
ppt.normalizeZOrder(1);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Supported element types:**
|
|
308
|
+
|
|
309
|
+
| PowerPoint Type | XML Tag | `type` Value |
|
|
310
|
+
|:---|:---|:---|
|
|
311
|
+
| Shape / Text Box | `p:sp` | `shape` / `text` |
|
|
312
|
+
| Image | `p:pic` | `image` |
|
|
313
|
+
| Chart | `p:graphicFrame` + chart URI | `chart` |
|
|
314
|
+
| Table | `p:graphicFrame` + table URI | `table` |
|
|
315
|
+
| SmartArt | `p:graphicFrame` + diagram URI | `smartart` |
|
|
316
|
+
| Group | `p:grpSp` | `group` |
|
|
317
|
+
| Connector | `p:cxnSp` | `connector` |
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
214
321
|
## ⚡ Performance Benchmarks
|
|
215
322
|
|
|
216
323
|
Tested on a standard 50-slide enterprise presentation template:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -36,6 +36,7 @@ const { RelationshipManager } = require('../managers/RelationshipManager.js')
|
|
|
36
36
|
const { ShapeManager } = require('../managers/ShapeManager.js')
|
|
37
37
|
const { ImageManager } = require('../managers/ImageManager.js')
|
|
38
38
|
const { TextManager } = require('../managers/TextManager.js')
|
|
39
|
+
const { ZOrderManager } = require('../managers/ZOrderManager.js')
|
|
39
40
|
const { ValidationEngine } = require('./ValidationEngine.js')
|
|
40
41
|
const { OutputWriter } = require('./OutputWriter.js')
|
|
41
42
|
const { TemplateEngine } = require('./TemplateEngine.js')
|
|
@@ -121,6 +122,12 @@ class PPTXTemplater {
|
|
|
121
122
|
*/
|
|
122
123
|
#textManager
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @private
|
|
127
|
+
* @type {ZOrderManager}
|
|
128
|
+
*/
|
|
129
|
+
#zOrderManager
|
|
130
|
+
|
|
124
131
|
/**
|
|
125
132
|
* @private
|
|
126
133
|
* @type {RelationshipManager}
|
|
@@ -169,6 +176,7 @@ class PPTXTemplater {
|
|
|
169
176
|
this.#imageManager = new ImageManager(this.#xmlParser)
|
|
170
177
|
this.#textManager = new TextManager(this.#xmlParser)
|
|
171
178
|
this.#templateEngine = new TemplateEngine(this.#xmlParser)
|
|
179
|
+
this.#zOrderManager = new ZOrderManager(this.#xmlParser)
|
|
172
180
|
this.#outputWriter = new OutputWriter(this.#zipManager, this.#contentTypesManager)
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -253,6 +261,8 @@ class PPTXTemplater {
|
|
|
253
261
|
await this.#contentTypesManager.initialize(this.#zipManager)
|
|
254
262
|
await this.#relationshipManager.initialize(this.#zipManager)
|
|
255
263
|
await this.#slideManager.initialize(this.#zipManager)
|
|
264
|
+
// Pre-load all slide XML so synchronous operations work on the blank template's existing slides
|
|
265
|
+
await this.#slideManager.preloadAll()
|
|
256
266
|
await this.#chartManager.initialize(this.#zipManager)
|
|
257
267
|
await this.#mediaManager.initialize(this.#zipManager)
|
|
258
268
|
this.#loaded = true
|
|
@@ -1541,6 +1551,266 @@ class PPTXTemplater {
|
|
|
1541
1551
|
get mediaManager() {
|
|
1542
1552
|
return this.#mediaManager
|
|
1543
1553
|
}
|
|
1554
|
+
|
|
1555
|
+
// Z-Order / Layer Management APIs
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Moves slide element one layer forward.
|
|
1559
|
+
*/
|
|
1560
|
+
bringForward(optionsOrId) {
|
|
1561
|
+
this.#assertLoaded()
|
|
1562
|
+
let slideIndex, objectId
|
|
1563
|
+
if (typeof optionsOrId === 'object' && optionsOrId !== null) {
|
|
1564
|
+
slideIndex =
|
|
1565
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1566
|
+
objectId = optionsOrId.objectId
|
|
1567
|
+
} else {
|
|
1568
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1569
|
+
objectId = optionsOrId
|
|
1570
|
+
}
|
|
1571
|
+
this.#zOrderManager.bringForward(slideIndex, objectId, this.#slideManager)
|
|
1572
|
+
return this
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* Moves slide element one layer backward.
|
|
1577
|
+
*/
|
|
1578
|
+
sendBackward(optionsOrId) {
|
|
1579
|
+
this.#assertLoaded()
|
|
1580
|
+
let slideIndex, objectId
|
|
1581
|
+
if (typeof optionsOrId === 'object' && optionsOrId !== null) {
|
|
1582
|
+
slideIndex =
|
|
1583
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1584
|
+
objectId = optionsOrId.objectId
|
|
1585
|
+
} else {
|
|
1586
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1587
|
+
objectId = optionsOrId
|
|
1588
|
+
}
|
|
1589
|
+
this.#zOrderManager.sendBackward(slideIndex, objectId, this.#slideManager)
|
|
1590
|
+
return this
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Moves slide element above all other objects.
|
|
1595
|
+
*/
|
|
1596
|
+
bringToFront(optionsOrId) {
|
|
1597
|
+
this.#assertLoaded()
|
|
1598
|
+
let slideIndex, objectId
|
|
1599
|
+
if (typeof optionsOrId === 'object' && optionsOrId !== null) {
|
|
1600
|
+
slideIndex =
|
|
1601
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1602
|
+
objectId = optionsOrId.objectId
|
|
1603
|
+
} else {
|
|
1604
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1605
|
+
objectId = optionsOrId
|
|
1606
|
+
}
|
|
1607
|
+
this.#zOrderManager.bringToFront(slideIndex, objectId, this.#slideManager)
|
|
1608
|
+
return this
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Moves slide element behind all other objects.
|
|
1613
|
+
*/
|
|
1614
|
+
sendToBack(optionsOrId) {
|
|
1615
|
+
this.#assertLoaded()
|
|
1616
|
+
let slideIndex, objectId
|
|
1617
|
+
if (typeof optionsOrId === 'object' && optionsOrId !== null) {
|
|
1618
|
+
slideIndex =
|
|
1619
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1620
|
+
objectId = optionsOrId.objectId
|
|
1621
|
+
} else {
|
|
1622
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1623
|
+
objectId = optionsOrId
|
|
1624
|
+
}
|
|
1625
|
+
this.#zOrderManager.sendToBack(slideIndex, objectId, this.#slideManager)
|
|
1626
|
+
return this
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Moves slide element to the specific 1-based stacking position.
|
|
1631
|
+
*/
|
|
1632
|
+
setZIndex(optionsOrId, zIndex) {
|
|
1633
|
+
this.#assertLoaded()
|
|
1634
|
+
let slideIndex, objectId, targetZIndex
|
|
1635
|
+
if (
|
|
1636
|
+
typeof optionsOrId === 'object' &&
|
|
1637
|
+
optionsOrId !== null &&
|
|
1638
|
+
optionsOrId.zIndex !== undefined
|
|
1639
|
+
) {
|
|
1640
|
+
slideIndex =
|
|
1641
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1642
|
+
objectId = optionsOrId.objectId
|
|
1643
|
+
targetZIndex = optionsOrId.zIndex
|
|
1644
|
+
} else {
|
|
1645
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1646
|
+
objectId = optionsOrId
|
|
1647
|
+
targetZIndex = zIndex
|
|
1648
|
+
}
|
|
1649
|
+
this.#zOrderManager.setZIndex(slideIndex, objectId, targetZIndex, this.#slideManager)
|
|
1650
|
+
return this
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Moves slide element directly before (below) a target element.
|
|
1655
|
+
*/
|
|
1656
|
+
moveObjectBefore(optionsOrId, targetId) {
|
|
1657
|
+
this.#assertLoaded()
|
|
1658
|
+
let slideIndex, objectId, target
|
|
1659
|
+
if (
|
|
1660
|
+
typeof optionsOrId === 'object' &&
|
|
1661
|
+
optionsOrId !== null &&
|
|
1662
|
+
optionsOrId.targetId !== undefined
|
|
1663
|
+
) {
|
|
1664
|
+
slideIndex =
|
|
1665
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1666
|
+
objectId = optionsOrId.objectId
|
|
1667
|
+
target = optionsOrId.targetId
|
|
1668
|
+
} else {
|
|
1669
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1670
|
+
objectId = optionsOrId
|
|
1671
|
+
target = targetId
|
|
1672
|
+
}
|
|
1673
|
+
this.#zOrderManager.moveObjectBefore(slideIndex, objectId, target, this.#slideManager)
|
|
1674
|
+
return this
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Moves slide element directly after (above) a target element.
|
|
1679
|
+
*/
|
|
1680
|
+
moveObjectAfter(optionsOrId, targetId) {
|
|
1681
|
+
this.#assertLoaded()
|
|
1682
|
+
let slideIndex, objectId, target
|
|
1683
|
+
if (
|
|
1684
|
+
typeof optionsOrId === 'object' &&
|
|
1685
|
+
optionsOrId !== null &&
|
|
1686
|
+
optionsOrId.targetId !== undefined
|
|
1687
|
+
) {
|
|
1688
|
+
slideIndex =
|
|
1689
|
+
optionsOrId.slide !== undefined ? optionsOrId.slide : this.#getTargetSlideIndices()[0] || 1
|
|
1690
|
+
objectId = optionsOrId.objectId
|
|
1691
|
+
target = optionsOrId.targetId
|
|
1692
|
+
} else {
|
|
1693
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1694
|
+
objectId = optionsOrId
|
|
1695
|
+
target = targetId
|
|
1696
|
+
}
|
|
1697
|
+
this.#zOrderManager.moveObjectAfter(slideIndex, objectId, target, this.#slideManager)
|
|
1698
|
+
return this
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Reorders slide objects exactly as specified in the array.
|
|
1703
|
+
*/
|
|
1704
|
+
reorderObjects(optionsOrOrder) {
|
|
1705
|
+
this.#assertLoaded()
|
|
1706
|
+
let slideIndex, order
|
|
1707
|
+
if (
|
|
1708
|
+
typeof optionsOrOrder === 'object' &&
|
|
1709
|
+
optionsOrOrder !== null &&
|
|
1710
|
+
Array.isArray(optionsOrOrder.order)
|
|
1711
|
+
) {
|
|
1712
|
+
slideIndex =
|
|
1713
|
+
optionsOrOrder.slide !== undefined
|
|
1714
|
+
? optionsOrOrder.slide
|
|
1715
|
+
: this.#getTargetSlideIndices()[0] || 1
|
|
1716
|
+
order = optionsOrOrder.order
|
|
1717
|
+
} else {
|
|
1718
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1719
|
+
order = optionsOrOrder
|
|
1720
|
+
}
|
|
1721
|
+
this.#zOrderManager.reorderObjects(slideIndex, order, this.#slideManager)
|
|
1722
|
+
return this
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Gets the ordered metadata of all objects on the slide.
|
|
1727
|
+
*/
|
|
1728
|
+
getObjectOrder(slideIndex) {
|
|
1729
|
+
this.#assertLoaded()
|
|
1730
|
+
const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
|
|
1731
|
+
return this.#zOrderManager.getObjectOrder(targetIdx, this.#slideManager)
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Applies bulk template configurations for slide elements stacking layers.
|
|
1736
|
+
*/
|
|
1737
|
+
applyZOrder(slideOrConfigs, configsOption) {
|
|
1738
|
+
this.#assertLoaded()
|
|
1739
|
+
let slideIndex, configs
|
|
1740
|
+
if (Array.isArray(slideOrConfigs)) {
|
|
1741
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1742
|
+
configs = slideOrConfigs
|
|
1743
|
+
} else {
|
|
1744
|
+
slideIndex = slideOrConfigs
|
|
1745
|
+
configs = configsOption
|
|
1746
|
+
}
|
|
1747
|
+
this.#zOrderManager.applyZOrder(slideIndex, configs, this.#slideManager)
|
|
1748
|
+
return this
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Retrieves the info of the top-most object on the slide.
|
|
1753
|
+
*/
|
|
1754
|
+
getTopMostObject(slideIndex) {
|
|
1755
|
+
this.#assertLoaded()
|
|
1756
|
+
const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
|
|
1757
|
+
return this.#zOrderManager.getTopMostObject(targetIdx, this.#slideManager)
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Retrieves the info of the bottom-most object on the slide.
|
|
1762
|
+
*/
|
|
1763
|
+
getBottomMostObject(slideIndex) {
|
|
1764
|
+
this.#assertLoaded()
|
|
1765
|
+
const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
|
|
1766
|
+
return this.#zOrderManager.getBottomMostObject(targetIdx, this.#slideManager)
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Swaps stacking positions of two slide objects.
|
|
1771
|
+
*/
|
|
1772
|
+
swapObjects(slideIndexOrId1, id1OrId2, id2) {
|
|
1773
|
+
this.#assertLoaded()
|
|
1774
|
+
let slideIndex, objectId1, objectId2
|
|
1775
|
+
if (id2 !== undefined) {
|
|
1776
|
+
slideIndex = slideIndexOrId1
|
|
1777
|
+
objectId1 = id1OrId2
|
|
1778
|
+
objectId2 = id2
|
|
1779
|
+
} else {
|
|
1780
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1781
|
+
objectId1 = slideIndexOrId1
|
|
1782
|
+
objectId2 = id1OrId2
|
|
1783
|
+
}
|
|
1784
|
+
this.#zOrderManager.swapObjects(slideIndex, objectId1, objectId2, this.#slideManager)
|
|
1785
|
+
return this
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Sorts stacking order using a custom comparison function.
|
|
1790
|
+
*/
|
|
1791
|
+
sortObjects(slideIndexOrCompareFn, compareFnOption) {
|
|
1792
|
+
this.#assertLoaded()
|
|
1793
|
+
let slideIndex, compareFn
|
|
1794
|
+
if (typeof slideIndexOrCompareFn === 'function') {
|
|
1795
|
+
slideIndex = this.#getTargetSlideIndices()[0] || 1
|
|
1796
|
+
compareFn = slideIndexOrCompareFn
|
|
1797
|
+
} else {
|
|
1798
|
+
slideIndex = slideIndexOrCompareFn
|
|
1799
|
+
compareFn = compareFnOption
|
|
1800
|
+
}
|
|
1801
|
+
this.#zOrderManager.sortObjects(slideIndex, compareFn, this.#slideManager)
|
|
1802
|
+
return this
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Cleans up and normalizes stacking order consistency.
|
|
1807
|
+
*/
|
|
1808
|
+
normalizeZOrder(slideIndex) {
|
|
1809
|
+
this.#assertLoaded()
|
|
1810
|
+
const targetIdx = slideIndex !== undefined ? slideIndex : this.#getTargetSlideIndices()[0] || 1
|
|
1811
|
+
this.#zOrderManager.normalizeZOrder(targetIdx, this.#slideManager)
|
|
1812
|
+
return this
|
|
1813
|
+
}
|
|
1544
1814
|
}
|
|
1545
1815
|
|
|
1546
1816
|
module.exports = { PPTXTemplater }
|
|
@@ -105,6 +105,11 @@ class ValidationEngine {
|
|
|
105
105
|
)
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
// Verify Z-order and duplicate IDs
|
|
110
|
+
const zOrderResult = this.validateObjectOrder(ppt, slideIndex)
|
|
111
|
+
errors.push(...zOrderResult.errors)
|
|
112
|
+
warnings.push(...zOrderResult.warnings)
|
|
108
113
|
} catch (err) {
|
|
109
114
|
errors.push(`Slide ${slideIndex} validation error: ${err.message}`)
|
|
110
115
|
}
|
|
@@ -241,6 +246,78 @@ class ValidationEngine {
|
|
|
241
246
|
warnings,
|
|
242
247
|
}
|
|
243
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Audits the shape tree structure for duplicate drawing element IDs.
|
|
252
|
+
*/
|
|
253
|
+
static validateObjectOrder(ppt, slideIndex) {
|
|
254
|
+
const errors = []
|
|
255
|
+
const warnings = []
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const slideXml = ppt.slideManager.getSlideXml(slideIndex)
|
|
259
|
+
const slideObj = ppt.xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
260
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
261
|
+
|
|
262
|
+
if (!spTree) {
|
|
263
|
+
errors.push(`Slide ${slideIndex} shape tree not found`)
|
|
264
|
+
return { valid: false, errors, warnings }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const idToTags = new Map()
|
|
268
|
+
|
|
269
|
+
const checkIdsRecursive = container => {
|
|
270
|
+
if (!container) return
|
|
271
|
+
|
|
272
|
+
for (const tag of ['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp']) {
|
|
273
|
+
let items = container[tag] || []
|
|
274
|
+
if (!Array.isArray(items)) items = [items]
|
|
275
|
+
for (const item of items) {
|
|
276
|
+
let id = null
|
|
277
|
+
let name = null
|
|
278
|
+
if (tag === 'p:sp') {
|
|
279
|
+
id = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_id']
|
|
280
|
+
name = item?.['p:nvSpPr']?.['p:cNvPr']?.['@_name']
|
|
281
|
+
} else if (tag === 'p:pic') {
|
|
282
|
+
id = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_id']
|
|
283
|
+
name = item?.['p:nvPicPr']?.['p:cNvPr']?.['@_name']
|
|
284
|
+
} else if (tag === 'p:graphicFrame') {
|
|
285
|
+
id = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id']
|
|
286
|
+
name = item?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_name']
|
|
287
|
+
} else if (tag === 'p:grpSp') {
|
|
288
|
+
id = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id']
|
|
289
|
+
name = item?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_name']
|
|
290
|
+
checkIdsRecursive(item)
|
|
291
|
+
} else if (tag === 'p:cxnSp') {
|
|
292
|
+
id = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_id']
|
|
293
|
+
name = item?.['p:nvCxnSpPr']?.['p:cNvPr']?.['@_name']
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (id !== undefined && id !== null) {
|
|
297
|
+
const strId = String(id)
|
|
298
|
+
if (idToTags.has(strId)) {
|
|
299
|
+
errors.push(
|
|
300
|
+
`Duplicate drawing object ID "${strId}" found in slide ${slideIndex} (name: "${name}")`
|
|
301
|
+
)
|
|
302
|
+
} else {
|
|
303
|
+
idToTags.set(strId, tag)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
checkIdsRecursive(spTree)
|
|
311
|
+
} catch (err) {
|
|
312
|
+
errors.push(`Slide ${slideIndex} shape tree validation error: ${err.message}`)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
valid: errors.length === 0,
|
|
317
|
+
errors,
|
|
318
|
+
warnings,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
244
321
|
}
|
|
245
322
|
|
|
246
323
|
module.exports = { ValidationEngine }
|
package/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
const { PPTXTemplater } = require('./core/PPTXTemplater.js')
|
|
28
28
|
const { ZipManager } = require('./managers/ZipManager.js')
|
|
29
|
-
const { XMLParser } = require('./parsers/XMLParser.js')
|
|
29
|
+
const { XMLParser, Z_ORDER_SYMBOL } = require('./parsers/XMLParser.js')
|
|
30
30
|
const { SlideManager } = require('./managers/SlideManager.js')
|
|
31
31
|
const { ChartManager } = require('./managers/ChartManager.js')
|
|
32
32
|
const { TableManager } = require('./managers/TableManager.js')
|
|
@@ -55,6 +55,7 @@ module.exports = {
|
|
|
55
55
|
PPTXTemplater,
|
|
56
56
|
ZipManager,
|
|
57
57
|
XMLParser,
|
|
58
|
+
Z_ORDER_SYMBOL,
|
|
58
59
|
SlideManager,
|
|
59
60
|
ChartManager,
|
|
60
61
|
TableManager,
|
|
@@ -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 }
|
package/src/parsers/XMLParser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|