node-pptx-templater 1.0.15 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/package.json +1 -1
- package/src/core/OutputWriter.js +59 -55
- package/src/core/PPTXTemplater.js +133 -0
- package/src/index.js +1 -0
- package/src/managers/ChartManager.js +11 -34
- package/src/managers/ZipManager.js +283 -41
package/README.md
CHANGED
|
@@ -112,6 +112,67 @@ async function generateReport() {
|
|
|
112
112
|
generateReport().catch(console.error);
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
## 📂 PowerPoint XML Folder Templates
|
|
116
|
+
|
|
117
|
+
Instead of loading and saving standard compiled `.pptx` ZIP files, the library natively supports working with uncompressed PowerPoint OpenXML directories. This is extremely useful for server environments and development setups, bypassing ZIP compression/decompression overhead and resolving relationships relative to the unzipped structure.
|
|
118
|
+
|
|
119
|
+
### 1. Load from XML Folder Template
|
|
120
|
+
|
|
121
|
+
You can load a template directly from a folder directory or `presentation.xml` entry point:
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const { PPTXTemplater, PPTXTemplate } = require('node-pptx-templater');
|
|
125
|
+
|
|
126
|
+
// Load using the directory root path (auto-detects ppt/presentation.xml)
|
|
127
|
+
const ppt = await PPTXTemplater.load('./monthly-template-folder');
|
|
128
|
+
|
|
129
|
+
// Load using fromPresentationXml with a configuration object
|
|
130
|
+
const ppt2 = await PPTXTemplate.fromPresentationXml({
|
|
131
|
+
presentation: './ppt/presentation.xml',
|
|
132
|
+
root: './template'
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 2. Save/Export directly to XML Folder
|
|
137
|
+
|
|
138
|
+
You can export the modified presentation back to an uncompressed folder structure on disk:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
await ppt.saveToFolder('./output-template-folder');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This generates:
|
|
145
|
+
```text
|
|
146
|
+
output-template-folder/
|
|
147
|
+
├── [Content_Types].xml
|
|
148
|
+
├── _rels/
|
|
149
|
+
├── ppt/
|
|
150
|
+
│ ├── presentation.xml
|
|
151
|
+
│ ├── _rels/
|
|
152
|
+
│ ├── slides/
|
|
153
|
+
│ ├── slideLayouts/
|
|
154
|
+
│ ├── slideMasters/
|
|
155
|
+
│ └── theme/
|
|
156
|
+
└── docProps/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 3. Folder Mode Performance Benefits
|
|
160
|
+
|
|
161
|
+
Our benchmark results compare standard ZIP-based templates with uncompressed XML folder workflows:
|
|
162
|
+
* **Concurrency Throughput**: Up to **1.4x faster** under parallel request stress due to eliminated ZIP compression CPU locks.
|
|
163
|
+
* **Heap Memory Footprint**: Reduces memory overhead by avoiding full in-memory ZIP archives.
|
|
164
|
+
|
|
165
|
+
### 4. Validation
|
|
166
|
+
|
|
167
|
+
Ensure XML directory templates are correct and contain no orphan relations:
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
const report = await ppt.validatePresentationXml();
|
|
171
|
+
if (!report.valid) {
|
|
172
|
+
console.error('Errors found:', report.errors);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
115
176
|
---
|
|
116
177
|
|
|
117
178
|
## 📋 OpenXML Presentation Architecture
|
|
@@ -1245,6 +1306,40 @@ Saves the modified PPTX to a file on disk.
|
|
|
1245
1306
|
ppt.useSlide(1).saveToFile(filePath, options = {});
|
|
1246
1307
|
```
|
|
1247
1308
|
|
|
1309
|
+
#### `save(filePath, options = {})`
|
|
1310
|
+
Saves the presentation. Equivalent to saveToFile.
|
|
1311
|
+
|
|
1312
|
+
* **Arguments**:
|
|
1313
|
+
* `filePath` (`string`): Output file path.
|
|
1314
|
+
* `[options]` (`Object`): Save options.
|
|
1315
|
+
* **Returns**: `Promise<void>` -
|
|
1316
|
+
|
|
1317
|
+
```javascript
|
|
1318
|
+
await ppt.save('output.pptx');
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
#### `saveXml(folderPath)`
|
|
1322
|
+
Saves the modified presentation XML structures directly to a folder.
|
|
1323
|
+
|
|
1324
|
+
* **Arguments**:
|
|
1325
|
+
* `folderPath` (`string`): Target directory path.
|
|
1326
|
+
* **Returns**: `Promise<void>` -
|
|
1327
|
+
|
|
1328
|
+
```javascript
|
|
1329
|
+
ppt.useSlide(1).saveXml(folderPath);
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
#### `saveToFolder(folderPath)`
|
|
1333
|
+
Saves the modified presentation XML structures directly to a folder.
|
|
1334
|
+
|
|
1335
|
+
* **Arguments**:
|
|
1336
|
+
* `folderPath` (`string`): Target directory path.
|
|
1337
|
+
* **Returns**: `Promise<void>` -
|
|
1338
|
+
|
|
1339
|
+
```javascript
|
|
1340
|
+
await ppt.saveToFolder('./output-template');
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1248
1343
|
#### `toBuffer()`
|
|
1249
1344
|
Returns the PPTX content as a Node.js Buffer.
|
|
1250
1345
|
|
|
@@ -1263,6 +1358,16 @@ Returns the PPTX content as a readable Node.js Stream.
|
|
|
1263
1358
|
ppt.useSlide(1).toStream();
|
|
1264
1359
|
```
|
|
1265
1360
|
|
|
1361
|
+
#### `validatePresentationXml()`
|
|
1362
|
+
Performs validation specifically on PowerPoint XML folder contents/relationships.
|
|
1363
|
+
|
|
1364
|
+
* **Returns**: `Promise<{valid: boolean, errors: string[], warnings: string[]` -
|
|
1365
|
+
|
|
1366
|
+
```javascript
|
|
1367
|
+
const report = await ppt.validatePresentationXml();
|
|
1368
|
+
if (!report.valid) console.error(report.errors);
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1266
1371
|
#### `slideCount()`
|
|
1267
1372
|
Returns the total number of slides in the loaded presentation. @type {number}
|
|
1268
1373
|
|
|
@@ -1282,6 +1387,15 @@ OpenXML relationship IDs follow the format rId1, rId2, rId3, ... They must be un
|
|
|
1282
1387
|
ppt.useSlide(1).function();
|
|
1283
1388
|
```
|
|
1284
1389
|
|
|
1390
|
+
#### `fromPresentationXml(())`
|
|
1391
|
+
Delegates core actions to slide element sub-managers.
|
|
1392
|
+
|
|
1393
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
1394
|
+
|
|
1395
|
+
```javascript
|
|
1396
|
+
const ppt = await PPTXTemplate.fromPresentationXml('./template-folder');
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1285
1399
|
#### `validatePresentation(())`
|
|
1286
1400
|
Delegates core actions to slide element sub-managers.
|
|
1287
1401
|
|
|
@@ -1309,6 +1423,24 @@ Delegates core actions to slide element sub-managers.
|
|
|
1309
1423
|
ppt.useSlide(1).validateTable(());
|
|
1310
1424
|
```
|
|
1311
1425
|
|
|
1426
|
+
#### `validateArchive(())`
|
|
1427
|
+
Delegates core actions to slide element sub-managers.
|
|
1428
|
+
|
|
1429
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
1430
|
+
|
|
1431
|
+
```javascript
|
|
1432
|
+
ppt.useSlide(1).validateArchive(());
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
#### `enableDebugZip(())`
|
|
1436
|
+
Delegates core actions to slide element sub-managers.
|
|
1437
|
+
|
|
1438
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
1439
|
+
|
|
1440
|
+
```javascript
|
|
1441
|
+
ppt.useSlide(1).enableDebugZip(());
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1312
1444
|
#### `validateRelationships(())`
|
|
1313
1445
|
Delegates core actions to slide element sub-managers.
|
|
1314
1446
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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",
|
package/src/core/OutputWriter.js
CHANGED
|
@@ -67,15 +67,28 @@ class OutputWriter {
|
|
|
67
67
|
* @param {ZipManager} zipManager
|
|
68
68
|
* @returns {Promise<Buffer>}
|
|
69
69
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Flushes all pending changes from all managers into the ZipManager.
|
|
72
|
+
*
|
|
73
|
+
* @param {SlideManager} slideManager
|
|
74
|
+
* @param {ZipManager} zipManager
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
async flush(slideManager, zipManager) {
|
|
72
78
|
await this.#flushAllSlides(slideManager, zipManager)
|
|
73
|
-
|
|
74
|
-
// Flush Content Types safely
|
|
75
79
|
this.#contentTypesManager.flush(zipManager)
|
|
76
|
-
|
|
77
|
-
// Wait for any queued asynchronous writes (like content types, media hashing)
|
|
78
80
|
await zipManager.waitForPendingWrites()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the PPTX as a Node.js Buffer.
|
|
85
|
+
*
|
|
86
|
+
* @param {SlideManager} slideManager
|
|
87
|
+
* @param {ZipManager} zipManager
|
|
88
|
+
* @returns {Promise<Buffer>}
|
|
89
|
+
*/
|
|
90
|
+
async toBuffer(slideManager, zipManager) {
|
|
91
|
+
await this.flush(slideManager, zipManager)
|
|
79
92
|
|
|
80
93
|
const buffer = await zipManager.toBuffer()
|
|
81
94
|
logger.debug(`Generated buffer: ${(buffer.length / 1024).toFixed(1)} KB`)
|
|
@@ -95,12 +108,7 @@ class OutputWriter {
|
|
|
95
108
|
* @returns {Promise<Readable>}
|
|
96
109
|
*/
|
|
97
110
|
async toStream(slideManager, zipManager) {
|
|
98
|
-
await this
|
|
99
|
-
|
|
100
|
-
// Flush Content Types safely
|
|
101
|
-
this.#contentTypesManager.flush(zipManager)
|
|
102
|
-
|
|
103
|
-
await zipManager.waitForPendingWrites()
|
|
111
|
+
await this.flush(slideManager, zipManager)
|
|
104
112
|
const nodeStream = await zipManager.toStream()
|
|
105
113
|
|
|
106
114
|
if (this.debugZip) {
|
|
@@ -182,58 +190,54 @@ class OutputWriter {
|
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
192
|
|
|
185
|
-
// Change this block to await the process completely
|
|
186
193
|
if (zipManager.hasFile('docProps/app.xml')) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
let
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (countVar) {
|
|
206
|
-
oldSlideTitlesCount = parseInt(countVar['vt:i4'], 10) || 0
|
|
207
|
-
countVar['vt:i4'] = info.length
|
|
208
|
-
}
|
|
209
|
-
break
|
|
194
|
+
const content = await zipManager.readFile('docProps/app.xml')
|
|
195
|
+
if (content) {
|
|
196
|
+
const parser = new XMLParser()
|
|
197
|
+
const appObj = parser.parse(content, 'app.xml')
|
|
198
|
+
const properties = appObj.Properties
|
|
199
|
+
|
|
200
|
+
if (properties) {
|
|
201
|
+
properties.Slides = info.length
|
|
202
|
+
|
|
203
|
+
let oldSlideTitlesCount = 0
|
|
204
|
+
const variants = properties.HeadingPairs?.['vt:vector']?.['vt:variant']
|
|
205
|
+
if (Array.isArray(variants)) {
|
|
206
|
+
for (let i = 0; i < variants.length; i++) {
|
|
207
|
+
if (variants[i]['vt:lpstr'] === 'Slide Titles') {
|
|
208
|
+
const countVar = variants[i + 1]
|
|
209
|
+
if (countVar) {
|
|
210
|
+
oldSlideTitlesCount = parseInt(countVar['vt:i4'], 10) || 0
|
|
211
|
+
countVar['vt:i4'] = info.length
|
|
210
212
|
}
|
|
213
|
+
break
|
|
211
214
|
}
|
|
212
215
|
}
|
|
216
|
+
}
|
|
213
217
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
const newSlideTitles = info.map(slide => slide.title || `Slide ${slide.index}`)
|
|
223
|
-
lpstrs.push(...newSlideTitles)
|
|
224
|
-
|
|
225
|
-
titlesVector['vt:lpstr'] = lpstrs
|
|
226
|
-
titlesVector['@_size'] = String(lpstrs.length)
|
|
218
|
+
const titlesVector = properties.TitlesOfParts?.['vt:vector']
|
|
219
|
+
if (titlesVector) {
|
|
220
|
+
let lpstrs = titlesVector['vt:lpstr']
|
|
221
|
+
if (lpstrs) {
|
|
222
|
+
if (!Array.isArray(lpstrs)) lpstrs = [lpstrs]
|
|
223
|
+
if (oldSlideTitlesCount > 0 && lpstrs.length >= oldSlideTitlesCount) {
|
|
224
|
+
lpstrs = lpstrs.slice(0, lpstrs.length - oldSlideTitlesCount)
|
|
227
225
|
}
|
|
226
|
+
const newSlideTitles = info.map(slide => slide.title || `Slide ${slide.index}`)
|
|
227
|
+
lpstrs.push(...newSlideTitles)
|
|
228
|
+
|
|
229
|
+
titlesVector['vt:lpstr'] = lpstrs
|
|
230
|
+
titlesVector['@_size'] = String(lpstrs.length)
|
|
228
231
|
}
|
|
232
|
+
}
|
|
229
233
|
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
235
|
+
const updatedXml = parser.build(appObj, declaration)
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
// Writing it safely here now that the function block is strictly sequential
|
|
238
|
+
zipManager.writeFile('docProps/app.xml', updatedXml)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
logger.debug(`Flushed ${info.length} slide(s) to ZIP`)
|
|
@@ -202,6 +202,17 @@ class PPTXTemplater {
|
|
|
202
202
|
return engine
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Loads a template from a PowerPoint XML Presentation format.
|
|
207
|
+
*
|
|
208
|
+
* @static
|
|
209
|
+
* @param {string|Object} options - Path to presentation.xml, folder root, or configuration object.
|
|
210
|
+
* @returns {Promise<PPTXTemplater>} Initialized engine instance.
|
|
211
|
+
*/
|
|
212
|
+
static async fromPresentationXml(options) {
|
|
213
|
+
return PPTXTemplater.load(options)
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
/**
|
|
206
217
|
* Creates a new blank PPTX from scratch.
|
|
207
218
|
*
|
|
@@ -992,6 +1003,40 @@ class PPTXTemplater {
|
|
|
992
1003
|
logger.info(`Saved PPTX to ${filePath}`)
|
|
993
1004
|
}
|
|
994
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* Saves the presentation. Equivalent to saveToFile.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} filePath - Output file path.
|
|
1010
|
+
* @param {Object} [options] - Save options.
|
|
1011
|
+
* @returns {Promise<void>}
|
|
1012
|
+
*/
|
|
1013
|
+
async save(filePath, options = {}) {
|
|
1014
|
+
return this.saveToFile(filePath, options)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Saves the modified presentation XML structures directly to a folder.
|
|
1019
|
+
*
|
|
1020
|
+
* @param {string} folderPath - Target directory path.
|
|
1021
|
+
* @returns {Promise<void>}
|
|
1022
|
+
*/
|
|
1023
|
+
async saveXml(folderPath) {
|
|
1024
|
+
this.#assertLoaded()
|
|
1025
|
+
await this.#outputWriter.flush(this.#slideManager, this.#zipManager)
|
|
1026
|
+
await this.#zipManager.toFolder(folderPath)
|
|
1027
|
+
logger.info(`Saved XML presentation to folder ${folderPath}`)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Saves the modified presentation XML structures directly to a folder.
|
|
1032
|
+
*
|
|
1033
|
+
* @param {string} folderPath - Target directory path.
|
|
1034
|
+
* @returns {Promise<void>}
|
|
1035
|
+
*/
|
|
1036
|
+
async saveToFolder(folderPath) {
|
|
1037
|
+
return this.saveXml(folderPath)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
995
1040
|
/**
|
|
996
1041
|
* Returns the PPTX content as a Node.js Buffer.
|
|
997
1042
|
*
|
|
@@ -1738,6 +1783,94 @@ class PPTXTemplater {
|
|
|
1738
1783
|
return await ValidationEngine.validatePresentation(this)
|
|
1739
1784
|
}
|
|
1740
1785
|
|
|
1786
|
+
/**
|
|
1787
|
+
* Performs validation specifically on PowerPoint XML folder contents/relationships.
|
|
1788
|
+
*
|
|
1789
|
+
* @returns {Promise<{valid: boolean, errors: string[], warnings: string[]}>} Validation report.
|
|
1790
|
+
*/
|
|
1791
|
+
async validatePresentationXml() {
|
|
1792
|
+
this.#assertLoaded()
|
|
1793
|
+
const errors = []
|
|
1794
|
+
const warnings = []
|
|
1795
|
+
|
|
1796
|
+
try {
|
|
1797
|
+
const presResult = await this.validatePresentation()
|
|
1798
|
+
errors.push(...presResult.errors)
|
|
1799
|
+
warnings.push(...presResult.warnings)
|
|
1800
|
+
} catch (err) {
|
|
1801
|
+
errors.push(`Presentation validation error: ${err.message}`)
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
try {
|
|
1805
|
+
await this.validateArchive()
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
errors.push(err.message)
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
if (this.#zipManager.hasFile('[Content_Types].xml')) {
|
|
1811
|
+
try {
|
|
1812
|
+
const ctXml = await this.#zipManager.readFile('[Content_Types].xml')
|
|
1813
|
+
const ctObj = this.#xmlParser.parse(ctXml, '[Content_Types].xml')
|
|
1814
|
+
const overrides = ctObj?.Types?.Override || []
|
|
1815
|
+
const overrideList = Array.isArray(overrides) ? overrides : [overrides]
|
|
1816
|
+
|
|
1817
|
+
for (const override of overrideList) {
|
|
1818
|
+
const partName = override['@_PartName']
|
|
1819
|
+
const contentType = override['@_ContentType']
|
|
1820
|
+
if (partName && contentType) {
|
|
1821
|
+
const cleanPath = partName.startsWith('/') ? partName.substring(1) : partName
|
|
1822
|
+
if (!this.#zipManager.hasFile(cleanPath)) {
|
|
1823
|
+
errors.push(`Content types override refers to missing file: ${cleanPath}`)
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
errors.push(`Invalid [Content_Types].xml structure: ${err.message}`)
|
|
1829
|
+
}
|
|
1830
|
+
} else {
|
|
1831
|
+
errors.push('Missing [Content_Types].xml')
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
const slideInfo = this.#slideManager.getAllSlideInfo()
|
|
1835
|
+
for (const slide of slideInfo) {
|
|
1836
|
+
const rels = this.#relationshipManager.getRelationships(slide.zipPath)
|
|
1837
|
+
const layoutRel = rels.find(
|
|
1838
|
+
r =>
|
|
1839
|
+
r.type ===
|
|
1840
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout'
|
|
1841
|
+
)
|
|
1842
|
+
if (layoutRel) {
|
|
1843
|
+
const layoutPath = this.#relationshipManager.resolveTarget(slide.zipPath, layoutRel.target)
|
|
1844
|
+
if (!this.#zipManager.hasFile(layoutPath)) {
|
|
1845
|
+
errors.push(`Slide ${slide.index} refers to missing slideLayout: ${layoutPath}`)
|
|
1846
|
+
} else {
|
|
1847
|
+
const layoutRels = this.#relationshipManager.getRelationships(layoutPath)
|
|
1848
|
+
const masterRel = layoutRels.find(
|
|
1849
|
+
r =>
|
|
1850
|
+
r.type ===
|
|
1851
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster'
|
|
1852
|
+
)
|
|
1853
|
+
if (masterRel) {
|
|
1854
|
+
const masterPath = this.#relationshipManager.resolveTarget(layoutPath, masterRel.target)
|
|
1855
|
+
if (!this.#zipManager.hasFile(masterPath)) {
|
|
1856
|
+
errors.push(`Slide layout ${layoutPath} refers to missing slideMaster: ${masterPath}`)
|
|
1857
|
+
}
|
|
1858
|
+
} else {
|
|
1859
|
+
warnings.push(`Slide layout ${layoutPath} has no slideMaster relationship`)
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
errors.push(`Slide ${slide.index} has no slideLayout relationship`)
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
return {
|
|
1868
|
+
valid: errors.length === 0,
|
|
1869
|
+
errors,
|
|
1870
|
+
warnings,
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1741
1874
|
async validateSlide(slideIndex) {
|
|
1742
1875
|
this.#assertLoaded()
|
|
1743
1876
|
return await ValidationEngine.validateSlide(this, slideIndex)
|
package/src/index.js
CHANGED
|
@@ -303,10 +303,9 @@ class ChartManager {
|
|
|
303
303
|
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
304
304
|
for (const rel of rels) {
|
|
305
305
|
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
306
|
+
const buffer = await this.#zipManager.readBinaryFile(xlsxPath)
|
|
307
|
+
if (buffer) {
|
|
308
308
|
console.log(`Found embedded workbook: ${xlsxPath}`)
|
|
309
|
-
const buffer = await xlsxData.async('nodebuffer')
|
|
310
309
|
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, cleanNumericData)
|
|
311
310
|
if (updatedXlsx) {
|
|
312
311
|
console.log(`Writing updated workbook to: ${xlsxPath}, size: ${updatedXlsx.length}`)
|
|
@@ -573,9 +572,8 @@ class ChartManager {
|
|
|
573
572
|
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
574
573
|
for (const rel of rels) {
|
|
575
574
|
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
576
|
-
const
|
|
577
|
-
if (
|
|
578
|
-
const buffer = await xlsxData.async('nodebuffer')
|
|
575
|
+
const buffer = await this.#zipManager.readBinaryFile(xlsxPath)
|
|
576
|
+
if (buffer) {
|
|
579
577
|
const zip = await JSZip.loadAsync(buffer)
|
|
580
578
|
const sheetFile = zip.file('xl/worksheets/sheet1.xml')
|
|
581
579
|
if (sheetFile) {
|
|
@@ -656,9 +654,8 @@ class ChartManager {
|
|
|
656
654
|
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
657
655
|
for (const rel of rels) {
|
|
658
656
|
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
659
|
-
const
|
|
660
|
-
if (
|
|
661
|
-
const buffer = await xlsxData.async('nodebuffer')
|
|
657
|
+
const buffer = await this.#zipManager.readBinaryFile(xlsxPath)
|
|
658
|
+
if (buffer) {
|
|
662
659
|
const workbookData = {
|
|
663
660
|
categories,
|
|
664
661
|
series: series.map((ser, idx) => {
|
|
@@ -697,31 +694,11 @@ class ChartManager {
|
|
|
697
694
|
getChartType(slideIndex, chartId, slideManager, relationshipManager) {
|
|
698
695
|
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
699
696
|
if (!chartInfo) return 'unknown'
|
|
700
|
-
const cachedXml = this.#zipManager.
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// We can't do async inside synchronous getChartType, but wait: we preloaded them!
|
|
706
|
-
// Since it's preloaded, it is in #xmlCache of zipManager.
|
|
707
|
-
// Let's see if we can get it from xmlCache
|
|
708
|
-
const path = chartInfo.zipPath.replace(/\\/g, '/')
|
|
709
|
-
const xml = this.#zipManager.hasFile(path)
|
|
710
|
-
? this.#zipManager.rawZip.file(path).async('text')
|
|
711
|
-
: null
|
|
712
|
-
// Actually, we can return the detected type from the file's text.
|
|
713
|
-
// Wait, is getChartType needed? We can make it async or use cached xml.
|
|
714
|
-
// Let's implement it asynchronously to be 100% correct, or read from cache!
|
|
715
|
-
// Let's see:
|
|
716
|
-
const xmlText = this.#zipManager.rawZip.file(path)
|
|
717
|
-
? String(this.#zipManager.rawZip.file(path)._data)
|
|
718
|
-
: ''
|
|
719
|
-
// Wait, JSZip's internal _data might not be fully text. Let's make getChartTypeAsync or just read the cache.
|
|
720
|
-
// Since they were all loaded into cache during initialization:
|
|
721
|
-
const xmlFromCache = this.#zipManager.rawZip.file(path)
|
|
722
|
-
? this.#zipManager.rawZip.file(path).name
|
|
723
|
-
: '' // wait, let's just make it async or check xmlCache
|
|
724
|
-
return 'bar' // fallback or default for type check, or we can make it async!
|
|
697
|
+
const cachedXml = this.#zipManager.readCachedFile(chartInfo.zipPath)
|
|
698
|
+
if (cachedXml) {
|
|
699
|
+
return this.#detectChartType(cachedXml)
|
|
700
|
+
}
|
|
701
|
+
return 'unknown'
|
|
725
702
|
}
|
|
726
703
|
|
|
727
704
|
async getChartTypeAsync(slideIndex, chartId, slideManager, relationshipManager) {
|
|
@@ -53,32 +53,140 @@ class ZipManager {
|
|
|
53
53
|
#coreProperties = new Map()
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
* @private
|
|
57
|
+
* @type {boolean}
|
|
58
|
+
*/
|
|
59
|
+
#isFolderMode = false
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @private
|
|
63
|
+
* @type {string|null}
|
|
61
64
|
*/
|
|
65
|
+
#folderRoot = null
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @private
|
|
69
|
+
* @type {Set<string>}
|
|
70
|
+
*/
|
|
71
|
+
#folderFiles = new Set()
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @private
|
|
75
|
+
* @type {Map<string, Buffer|Uint8Array>}
|
|
76
|
+
*/
|
|
77
|
+
#dirtyBinaryFiles = new Map()
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @private
|
|
81
|
+
* @type {Set<string>}
|
|
82
|
+
*/
|
|
83
|
+
#removedFiles = new Set()
|
|
84
|
+
|
|
62
85
|
async load(source) {
|
|
63
86
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
const path = require('path')
|
|
88
|
+
const fs = require('fs-extra')
|
|
89
|
+
|
|
90
|
+
let isFolder = false
|
|
91
|
+
let rootDir = null
|
|
92
|
+
let presentationPath = null
|
|
93
|
+
|
|
94
|
+
if (typeof source === 'object' && source !== null && (source.presentation || source.root)) {
|
|
95
|
+
isFolder = true
|
|
96
|
+
presentationPath = source.presentation
|
|
97
|
+
rootDir = source.root
|
|
98
|
+
} else if (typeof source === 'string') {
|
|
99
|
+
try {
|
|
100
|
+
const stat = fs.statSync(source)
|
|
101
|
+
if (stat.isDirectory()) {
|
|
102
|
+
isFolder = true
|
|
103
|
+
rootDir = source
|
|
104
|
+
} else if (source.endsWith('.xml')) {
|
|
105
|
+
isFolder = true
|
|
106
|
+
presentationPath = source
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
if (source.endsWith('.xml')) {
|
|
110
|
+
isFolder = true
|
|
111
|
+
presentationPath = source
|
|
112
|
+
}
|
|
113
|
+
}
|
|
74
114
|
}
|
|
75
115
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
if (isFolder) {
|
|
117
|
+
this.#isFolderMode = true
|
|
118
|
+
|
|
119
|
+
// Resolve presentationPath and rootDir
|
|
120
|
+
if (presentationPath && !rootDir) {
|
|
121
|
+
const resolvedPresentation = path.resolve(presentationPath)
|
|
122
|
+
const dir = path.dirname(resolvedPresentation)
|
|
123
|
+
if (path.basename(dir).toLowerCase() === 'ppt') {
|
|
124
|
+
rootDir = path.dirname(dir)
|
|
125
|
+
} else {
|
|
126
|
+
rootDir = dir
|
|
127
|
+
}
|
|
128
|
+
} else if (rootDir && !presentationPath) {
|
|
129
|
+
const candidates = [
|
|
130
|
+
path.join(rootDir, 'ppt/presentation.xml'),
|
|
131
|
+
path.join(rootDir, 'presentation.xml'),
|
|
132
|
+
]
|
|
133
|
+
for (const cand of candidates) {
|
|
134
|
+
if (fs.existsSync(cand)) {
|
|
135
|
+
presentationPath = cand
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!presentationPath) {
|
|
140
|
+
presentationPath = path.join(rootDir, 'ppt/presentation.xml')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.#folderRoot = path.resolve(rootDir)
|
|
145
|
+
logger.debug(`Loading from uncompressed OpenXML folder: ${this.#folderRoot}`)
|
|
146
|
+
|
|
147
|
+
// Populate #folderFiles recursively
|
|
148
|
+
const getFiles = async (dir, baseDir) => {
|
|
149
|
+
const results = []
|
|
150
|
+
if (!(await fs.pathExists(dir))) return results
|
|
151
|
+
const list = await fs.readdir(dir)
|
|
152
|
+
for (const file of list) {
|
|
153
|
+
const filePath = path.join(dir, file)
|
|
154
|
+
const stat = await fs.stat(filePath)
|
|
155
|
+
if (stat && stat.isDirectory()) {
|
|
156
|
+
results.push(...(await getFiles(filePath, baseDir)))
|
|
157
|
+
} else {
|
|
158
|
+
const rel = path.relative(baseDir, filePath).replace(/\\/g, '/')
|
|
159
|
+
results.push(rel)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const files = await getFiles(this.#folderRoot, this.#folderRoot)
|
|
166
|
+
this.#folderFiles = new Set(files)
|
|
167
|
+
|
|
168
|
+
await this.#loadCoreProperties()
|
|
169
|
+
logger.debug(`Folder loaded successfully. Files: ${this.#folderFiles.size}`)
|
|
170
|
+
} else {
|
|
171
|
+
let data
|
|
172
|
+
if (typeof source === 'string') {
|
|
173
|
+
logger.debug(`Reading file: ${source}`)
|
|
174
|
+
data = await fsExtra.readFile(source)
|
|
175
|
+
} else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
176
|
+
data = source
|
|
177
|
+
} else {
|
|
178
|
+
throw new PPTXError(
|
|
179
|
+
`Invalid source type: ${typeof source}. Expected string path or Buffer.`
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.#zip = await JSZip.loadAsync(data)
|
|
184
|
+
await this.#loadCoreProperties()
|
|
185
|
+
logger.debug(`ZIP loaded successfully. Files: ${Object.keys(this.#zip.files).length}`)
|
|
186
|
+
}
|
|
79
187
|
} catch (err) {
|
|
80
188
|
if (err instanceof PPTXError) throw err
|
|
81
|
-
throw new PPTXError(`Failed to load
|
|
189
|
+
throw new PPTXError(`Failed to load template: ${err.message}`, err)
|
|
82
190
|
}
|
|
83
191
|
}
|
|
84
192
|
|
|
@@ -113,6 +221,19 @@ class ZipManager {
|
|
|
113
221
|
return this.#dirtyFiles.get(normalPath)
|
|
114
222
|
}
|
|
115
223
|
|
|
224
|
+
if (this.#isFolderMode) {
|
|
225
|
+
const path = require('path')
|
|
226
|
+
const fs = require('fs-extra')
|
|
227
|
+
const diskPath = path.join(this.#folderRoot, normalPath)
|
|
228
|
+
if (!(await fs.pathExists(diskPath))) {
|
|
229
|
+
logger.debug(`File not found in folder: ${normalPath}`)
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
const content = await fs.readFile(diskPath, 'utf8')
|
|
233
|
+
this.#xmlCache.set(normalPath, content)
|
|
234
|
+
return content
|
|
235
|
+
}
|
|
236
|
+
|
|
116
237
|
const file = this.#zip.file(normalPath)
|
|
117
238
|
if (!file) {
|
|
118
239
|
logger.debug(`File not found in ZIP: ${normalPath}`)
|
|
@@ -124,6 +245,20 @@ class ZipManager {
|
|
|
124
245
|
return content
|
|
125
246
|
}
|
|
126
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Synchronously reads a cached text file.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} zipPath - Path within the ZIP.
|
|
252
|
+
* @returns {string|null} Cached content or null.
|
|
253
|
+
*/
|
|
254
|
+
readCachedFile(zipPath) {
|
|
255
|
+
const normalPath = zipPath.replace(/\\/g, '/')
|
|
256
|
+
if (this.#dirtyFiles.has(normalPath)) {
|
|
257
|
+
return this.#dirtyFiles.get(normalPath)
|
|
258
|
+
}
|
|
259
|
+
return this.#xmlCache.get(normalPath) || null
|
|
260
|
+
}
|
|
261
|
+
|
|
127
262
|
/**
|
|
128
263
|
* Reads a binary file from the ZIP archive.
|
|
129
264
|
*
|
|
@@ -132,36 +267,39 @@ class ZipManager {
|
|
|
132
267
|
*/
|
|
133
268
|
async readBinaryFile(zipPath) {
|
|
134
269
|
const normalPath = zipPath.replace(/\\/g, '/')
|
|
270
|
+
if (this.#dirtyBinaryFiles.has(normalPath)) {
|
|
271
|
+
return this.#dirtyBinaryFiles.get(normalPath)
|
|
272
|
+
}
|
|
273
|
+
if (this.#isFolderMode) {
|
|
274
|
+
const path = require('path')
|
|
275
|
+
const fs = require('fs-extra')
|
|
276
|
+
const diskPath = path.join(this.#folderRoot, normalPath)
|
|
277
|
+
if (!(await fs.pathExists(diskPath))) return null
|
|
278
|
+
return fs.readFile(diskPath)
|
|
279
|
+
}
|
|
135
280
|
const file = this.#zip.file(normalPath)
|
|
136
281
|
if (!file) return null
|
|
137
282
|
return file.async('uint8array')
|
|
138
283
|
}
|
|
139
284
|
|
|
140
|
-
/**
|
|
141
|
-
* Writes (or overwrites) a text file in the ZIP archive.
|
|
142
|
-
* Changes are buffered and applied when generating the output ZIP.
|
|
143
|
-
*
|
|
144
|
-
* @param {string} zipPath - Path within the ZIP.
|
|
145
|
-
* @param {string} content - UTF-8 string content.
|
|
146
|
-
*/
|
|
147
285
|
writeFile(zipPath, content) {
|
|
148
286
|
const normalPath = zipPath.replace(/\\/g, '/')
|
|
149
287
|
this.#dirtyFiles.set(normalPath, content)
|
|
150
288
|
this.#xmlCache.set(normalPath, content)
|
|
151
|
-
|
|
152
|
-
this.#zip
|
|
289
|
+
this.#removedFiles.delete(normalPath)
|
|
290
|
+
if (this.#zip) {
|
|
291
|
+
this.#zip.file(normalPath, content)
|
|
292
|
+
}
|
|
153
293
|
logger.debug(`Queued write: ${normalPath}`)
|
|
154
294
|
}
|
|
155
295
|
|
|
156
|
-
/**
|
|
157
|
-
* Writes a binary file to the ZIP archive.
|
|
158
|
-
*
|
|
159
|
-
* @param {string} zipPath - Path within the ZIP.
|
|
160
|
-
* @param {Buffer|Uint8Array} data - Binary data.
|
|
161
|
-
*/
|
|
162
296
|
writeBinaryFile(zipPath, data) {
|
|
163
297
|
const normalPath = zipPath.replace(/\\/g, '/')
|
|
164
|
-
this.#
|
|
298
|
+
this.#dirtyBinaryFiles.set(normalPath, data)
|
|
299
|
+
this.#removedFiles.delete(normalPath)
|
|
300
|
+
if (this.#zip) {
|
|
301
|
+
this.#zip.file(normalPath, data)
|
|
302
|
+
}
|
|
165
303
|
logger.debug(`Queued binary write: ${normalPath}`)
|
|
166
304
|
}
|
|
167
305
|
|
|
@@ -197,9 +335,13 @@ class ZipManager {
|
|
|
197
335
|
*/
|
|
198
336
|
removeFile(zipPath) {
|
|
199
337
|
const normalPath = zipPath.replace(/\\/g, '/')
|
|
200
|
-
this.#
|
|
338
|
+
this.#removedFiles.add(normalPath)
|
|
201
339
|
this.#xmlCache.delete(normalPath)
|
|
202
340
|
this.#dirtyFiles.delete(normalPath)
|
|
341
|
+
this.#dirtyBinaryFiles.delete(normalPath)
|
|
342
|
+
if (this.#zip) {
|
|
343
|
+
this.#zip.remove(normalPath)
|
|
344
|
+
}
|
|
203
345
|
}
|
|
204
346
|
|
|
205
347
|
/**
|
|
@@ -210,6 +352,11 @@ class ZipManager {
|
|
|
210
352
|
*/
|
|
211
353
|
hasFile(zipPath) {
|
|
212
354
|
const normalPath = zipPath.replace(/\\/g, '/')
|
|
355
|
+
if (this.#removedFiles.has(normalPath)) return false
|
|
356
|
+
if (this.#dirtyFiles.has(normalPath) || this.#dirtyBinaryFiles.has(normalPath)) return true
|
|
357
|
+
if (this.#isFolderMode) {
|
|
358
|
+
return this.#folderFiles.has(normalPath)
|
|
359
|
+
}
|
|
213
360
|
return this.#zip.file(normalPath) !== null
|
|
214
361
|
}
|
|
215
362
|
|
|
@@ -220,6 +367,14 @@ class ZipManager {
|
|
|
220
367
|
* @returns {string[]} Array of matching file paths.
|
|
221
368
|
*/
|
|
222
369
|
listFiles(prefix = '') {
|
|
370
|
+
if (this.#isFolderMode) {
|
|
371
|
+
const allFiles = new Set([
|
|
372
|
+
...this.#folderFiles,
|
|
373
|
+
...this.#dirtyFiles.keys(),
|
|
374
|
+
...this.#dirtyBinaryFiles.keys(),
|
|
375
|
+
])
|
|
376
|
+
return Array.from(allFiles).filter(f => !this.#removedFiles.has(f) && f.startsWith(prefix))
|
|
377
|
+
}
|
|
223
378
|
return Object.keys(this.#zip.files).filter(f => !this.#zip.files[f].dir && f.startsWith(prefix))
|
|
224
379
|
}
|
|
225
380
|
|
|
@@ -230,6 +385,7 @@ class ZipManager {
|
|
|
230
385
|
* @returns {Promise<Buffer>} Compressed PPTX as a Buffer.
|
|
231
386
|
*/
|
|
232
387
|
async toBuffer() {
|
|
388
|
+
await this.#ensureZipForExport()
|
|
233
389
|
return this.#zip.generateAsync({
|
|
234
390
|
type: 'nodebuffer',
|
|
235
391
|
compression: 'STORE',
|
|
@@ -237,12 +393,8 @@ class ZipManager {
|
|
|
237
393
|
})
|
|
238
394
|
}
|
|
239
395
|
|
|
240
|
-
/**
|
|
241
|
-
* Generates the final ZIP archive as a readable Stream.
|
|
242
|
-
*
|
|
243
|
-
* @returns {Promise<NodeJS.ReadableStream>}
|
|
244
|
-
*/
|
|
245
396
|
async toStream() {
|
|
397
|
+
await this.#ensureZipForExport()
|
|
246
398
|
return this.#zip.generateNodeStream({
|
|
247
399
|
type: 'nodebuffer',
|
|
248
400
|
compression: 'STORE',
|
|
@@ -296,6 +448,7 @@ class ZipManager {
|
|
|
296
448
|
* @throws {PPTXError} If any validation issue is found.
|
|
297
449
|
*/
|
|
298
450
|
async validateArchive() {
|
|
451
|
+
await this.#ensureZipForExport()
|
|
299
452
|
const files = this.#zip.files
|
|
300
453
|
const errors = []
|
|
301
454
|
const seenPaths = new Set()
|
|
@@ -383,6 +536,95 @@ class ZipManager {
|
|
|
383
536
|
* Returns the raw JSZip instance (for advanced use cases).
|
|
384
537
|
* @returns {JSZip}
|
|
385
538
|
*/
|
|
539
|
+
/**
|
|
540
|
+
* Saves the presentation to a folder structure on the filesystem.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} destPath - Target directory path.
|
|
543
|
+
* @returns {Promise<void>}
|
|
544
|
+
*/
|
|
545
|
+
async toFolder(destPath) {
|
|
546
|
+
const path = require('path')
|
|
547
|
+
const fs = require('fs-extra')
|
|
548
|
+
await fs.ensureDir(destPath)
|
|
549
|
+
|
|
550
|
+
if (this.#isFolderMode) {
|
|
551
|
+
const allFiles = new Set([
|
|
552
|
+
...this.#folderFiles,
|
|
553
|
+
...this.#dirtyFiles.keys(),
|
|
554
|
+
...this.#dirtyBinaryFiles.keys(),
|
|
555
|
+
])
|
|
556
|
+
|
|
557
|
+
for (const relPath of allFiles) {
|
|
558
|
+
if (this.#removedFiles.has(relPath)) {
|
|
559
|
+
const targetPath = path.join(destPath, relPath)
|
|
560
|
+
await fs.remove(targetPath)
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const targetPath = path.join(destPath, relPath)
|
|
565
|
+
await fs.ensureDir(path.dirname(targetPath))
|
|
566
|
+
|
|
567
|
+
if (this.#dirtyFiles.has(relPath)) {
|
|
568
|
+
await fs.writeFile(targetPath, this.#dirtyFiles.get(relPath), 'utf8')
|
|
569
|
+
} else if (this.#dirtyBinaryFiles.has(relPath)) {
|
|
570
|
+
await fs.writeFile(targetPath, this.#dirtyBinaryFiles.get(relPath))
|
|
571
|
+
} else {
|
|
572
|
+
const srcPath = path.join(this.#folderRoot, relPath)
|
|
573
|
+
await fs.copy(srcPath, targetPath)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
const files = this.#zip.files
|
|
578
|
+
for (const [name, file] of Object.entries(files)) {
|
|
579
|
+
if (file.dir) continue
|
|
580
|
+
const targetPath = path.join(destPath, name)
|
|
581
|
+
await fs.ensureDir(path.dirname(targetPath))
|
|
582
|
+
const buffer = await file.async('nodebuffer')
|
|
583
|
+
await fs.writeFile(targetPath, buffer)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async #ensureZipForExport() {
|
|
589
|
+
if (this.#zip) return this.#zip
|
|
590
|
+
|
|
591
|
+
const JSZip = require('jszip')
|
|
592
|
+
const path = require('path')
|
|
593
|
+
const fs = require('fs-extra')
|
|
594
|
+
|
|
595
|
+
const zip = new JSZip()
|
|
596
|
+
|
|
597
|
+
// 1. Read all files from the original folder structure (that are not removed)
|
|
598
|
+
for (const relPath of this.#folderFiles) {
|
|
599
|
+
if (this.#removedFiles.has(relPath)) continue
|
|
600
|
+
|
|
601
|
+
if (this.#dirtyFiles.has(relPath)) {
|
|
602
|
+
zip.file(relPath, this.#dirtyFiles.get(relPath))
|
|
603
|
+
} else if (this.#dirtyBinaryFiles.has(relPath)) {
|
|
604
|
+
zip.file(relPath, this.#dirtyBinaryFiles.get(relPath))
|
|
605
|
+
} else {
|
|
606
|
+
const diskPath = path.join(this.#folderRoot, relPath)
|
|
607
|
+
const data = await fs.readFile(diskPath)
|
|
608
|
+
zip.file(relPath, data)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 2. Write any new files that were added (and not already in folderFiles)
|
|
613
|
+
for (const [relPath, content] of this.#dirtyFiles.entries()) {
|
|
614
|
+
if (!this.#folderFiles.has(relPath) && !this.#removedFiles.has(relPath)) {
|
|
615
|
+
zip.file(relPath, content)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const [relPath, data] of this.#dirtyBinaryFiles.entries()) {
|
|
619
|
+
if (!this.#folderFiles.has(relPath) && !this.#removedFiles.has(relPath)) {
|
|
620
|
+
zip.file(relPath, data)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
this.#zip = zip
|
|
625
|
+
return zip
|
|
626
|
+
}
|
|
627
|
+
|
|
386
628
|
get rawZip() {
|
|
387
629
|
return this.#zip
|
|
388
630
|
}
|