node-pptx-templater 1.0.17 → 1.0.19

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.
@@ -67,12 +67,7 @@ class TableManager {
67
67
  * @throws {TableNotFoundError} If the table is not found.
68
68
  */
69
69
  updateTable(slideIndex, tableId, data, slideManager) {
70
- const slideXml = slideManager.getSlideXml(slideIndex)
71
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
72
- const tblObj = this.#findTableObj(slideObj, tableId)
73
- if (!tblObj) {
74
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
75
- }
70
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
76
71
 
77
72
  const trs = tblObj['a:tr'] || []
78
73
  if (trs.length === 0) {
@@ -174,8 +169,7 @@ class TableManager {
174
169
 
175
170
  tblObj['a:tr'] = newRows
176
171
 
177
- const decl = this.#xmlParser.extractDeclaration(slideXml)
178
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
172
+ slideManager.markSlideObjDirty(slideIndex)
179
173
 
180
174
  const finalMerges = [...templateMerges, ...generatedMerges]
181
175
  for (const merge of finalMerges) {
@@ -204,12 +198,7 @@ class TableManager {
204
198
  * @param {SlideManager} slideManager
205
199
  */
206
200
  addTableRow(slideIndex, tableId, rowData, slideManager) {
207
- const slideXml = slideManager.getSlideXml(slideIndex)
208
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
209
- const tblObj = this.#findTableObj(slideObj, tableId)
210
- if (!tblObj) {
211
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
212
- }
201
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
213
202
 
214
203
  const trs = tblObj['a:tr'] || []
215
204
  if (trs.length === 0) {
@@ -230,8 +219,7 @@ class TableManager {
230
219
 
231
220
  trs.push(newRow)
232
221
 
233
- const decl = this.#xmlParser.extractDeclaration(slideXml)
234
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
222
+ slideManager.markSlideObjDirty(slideIndex)
235
223
  }
236
224
 
237
225
  /**
@@ -243,12 +231,7 @@ class TableManager {
243
231
  * @param {SlideManager} slideManager
244
232
  */
245
233
  removeTableRow(slideIndex, tableId, rowIndex, slideManager) {
246
- const slideXml = slideManager.getSlideXml(slideIndex)
247
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
248
- const tblObj = this.#findTableObj(slideObj, tableId)
249
- if (!tblObj) {
250
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
251
- }
234
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
252
235
 
253
236
  const trs = tblObj['a:tr'] || []
254
237
  if (rowIndex < 0 || rowIndex >= trs.length) {
@@ -257,8 +240,7 @@ class TableManager {
257
240
 
258
241
  trs.splice(rowIndex, 1)
259
242
 
260
- const decl = this.#xmlParser.extractDeclaration(slideXml)
261
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
243
+ slideManager.markSlideObjDirty(slideIndex)
262
244
  }
263
245
 
264
246
  /**
@@ -271,12 +253,7 @@ class TableManager {
271
253
  * @param {SlideManager} slideManager
272
254
  */
273
255
  insertTableRow(slideIndex, tableId, rowIndex, rowData, slideManager) {
274
- const slideXml = slideManager.getSlideXml(slideIndex)
275
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
276
- const tblObj = this.#findTableObj(slideObj, tableId)
277
- if (!tblObj) {
278
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
279
- }
256
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
280
257
 
281
258
  const trs = tblObj['a:tr'] || []
282
259
  if (rowIndex < 0 || rowIndex > trs.length) {
@@ -302,8 +279,7 @@ class TableManager {
302
279
 
303
280
  trs.splice(rowIndex, 0, newRow)
304
281
 
305
- const decl = this.#xmlParser.extractDeclaration(slideXml)
306
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
282
+ slideManager.markSlideObjDirty(slideIndex)
307
283
  }
308
284
 
309
285
  /**
@@ -316,12 +292,7 @@ class TableManager {
316
292
  * @param {SlideManager} slideManager
317
293
  */
318
294
  cloneTableRow(slideIndex, tableId, sourceRowIndex, targetRowIndex, slideManager) {
319
- const slideXml = slideManager.getSlideXml(slideIndex)
320
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
321
- const tblObj = this.#findTableObj(slideObj, tableId)
322
- if (!tblObj) {
323
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
324
- }
295
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
325
296
 
326
297
  const trs = tblObj['a:tr'] || []
327
298
  if (sourceRowIndex < 0 || sourceRowIndex >= trs.length) {
@@ -337,8 +308,7 @@ class TableManager {
337
308
 
338
309
  trs.splice(targetRowIndex, 0, newRow)
339
310
 
340
- const decl = this.#xmlParser.extractDeclaration(slideXml)
341
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
311
+ slideManager.markSlideObjDirty(slideIndex)
342
312
  }
343
313
 
344
314
  /**
@@ -353,12 +323,7 @@ class TableManager {
353
323
  * @param {SlideManager} slideManager
354
324
  */
355
325
  updateCell(slideIndex, tableId, rowIndex, colIndex, value, options = {}, slideManager) {
356
- const slideXml = slideManager.getSlideXml(slideIndex)
357
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
358
- const tblObj = this.#findTableObj(slideObj, tableId)
359
- if (!tblObj) {
360
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
361
- }
326
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
362
327
 
363
328
  const row = tblObj['a:tr']?.[rowIndex]
364
329
  if (!row) {
@@ -407,8 +372,7 @@ class TableManager {
407
372
  }
408
373
  }
409
374
 
410
- const decl = this.#xmlParser.extractDeclaration(slideXml)
411
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
375
+ slideManager.markSlideObjDirty(slideIndex)
412
376
  }
413
377
 
414
378
  /**
@@ -425,9 +389,8 @@ class TableManager {
425
389
  */
426
390
  validateMergeRegion(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
427
391
  const errors = []
428
- const slideXml = slideManager.getSlideXml(slideIndex)
429
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
430
- const tblObj = this.#findTableObj(slideObj, tableId)
392
+ const slideObj = slideManager.getSlideObj(slideIndex)
393
+ const tblObj = this.#findTableObj(slideObj, tableId, slideManager, slideIndex)
431
394
  if (!tblObj) {
432
395
  errors.push(`Table "${tableId}" not found in slide ${slideIndex}`)
433
396
  return { valid: false, errors }
@@ -507,9 +470,7 @@ class TableManager {
507
470
  throw new PPTXError(`Invalid merge region: ${validation.errors.join('; ')}`)
508
471
  }
509
472
 
510
- const slideXml = slideManager.getSlideXml(slideIndex)
511
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
512
- const tblObj = this.#findTableObj(slideObj, tableId)
473
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
513
474
  const trs = tblObj['a:tr'] || []
514
475
 
515
476
  const allTexts = []
@@ -561,8 +522,7 @@ class TableManager {
561
522
  const combinedText = allTexts.filter(t => t.trim() !== '').join('\n')
562
523
  this.#setCellTextObj(originCell, combinedText)
563
524
 
564
- const decl = this.#xmlParser.extractDeclaration(slideXml)
565
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
525
+ slideManager.markSlideObjDirty(slideIndex)
566
526
  }
567
527
 
568
528
  /**
@@ -587,9 +547,8 @@ class TableManager {
587
547
  actualEndCol = undefined
588
548
  }
589
549
 
590
- const slideXml = actualSlideManager.getSlideXml(slideIndex)
591
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
592
- const tblObj = this.#findTableObj(slideObj, tableId)
550
+ const slideObj = actualSlideManager.getSlideObj(slideIndex)
551
+ const tblObj = this.#findTableObj(slideObj, tableId, actualSlideManager, slideIndex)
593
552
  if (!tblObj) {
594
553
  throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
595
554
  }
@@ -629,8 +588,7 @@ class TableManager {
629
588
  }
630
589
  }
631
590
 
632
- const decl = this.#xmlParser.extractDeclaration(slideXml)
633
- actualSlideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
591
+ actualSlideManager.markSlideObjDirty(slideIndex)
634
592
  }
635
593
 
636
594
  /**
@@ -642,9 +600,8 @@ class TableManager {
642
600
  * @returns {Array<Object>} List of merged region coordinates
643
601
  */
644
602
  getMergedCells(slideIndex, tableId, slideManager) {
645
- const slideXml = slideManager.getSlideXml(slideIndex)
646
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
647
- const tblObj = this.#findTableObj(slideObj, tableId)
603
+ const slideObj = slideManager.getSlideObj(slideIndex)
604
+ const tblObj = this.#findTableObj(slideObj, tableId, slideManager, slideIndex)
648
605
  if (!tblObj) {
649
606
  throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
650
607
  }
@@ -759,9 +716,8 @@ class TableManager {
759
716
  slideManager
760
717
  )
761
718
 
762
- const slideXml = slideManager.getSlideXml(slideIndex)
763
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
764
- const tblObj = this.#findTableObj(slideObj, tableId)
719
+ const slideObj = slideManager.getSlideObj(slideIndex)
720
+ const tblObj = this.#findTableObj(slideObj, tableId, slideManager, slideIndex)
765
721
  if (!tblObj) return
766
722
 
767
723
  const trs = tblObj['a:tr'] || []
@@ -777,8 +733,7 @@ class TableManager {
777
733
  }
778
734
  }
779
735
 
780
- const decl = this.#xmlParser.extractDeclaration(slideXml)
781
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
736
+ slideManager.markSlideObjDirty(slideIndex)
782
737
  }
783
738
 
784
739
  /**
@@ -789,12 +744,7 @@ class TableManager {
789
744
  * @param {SlideManager} slideManager
790
745
  */
791
746
  autoFitTable(slideIndex, tableId, slideManager) {
792
- const slideXml = slideManager.getSlideXml(slideIndex)
793
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
794
- const tblObj = this.#findTableObj(slideObj, tableId)
795
- if (!tblObj) {
796
- throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
797
- }
747
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
798
748
 
799
749
  const trs = tblObj['a:tr'] || []
800
750
  const gridCols = tblObj['a:tblGrid']?.['a:gridCol']
@@ -835,8 +785,7 @@ class TableManager {
835
785
  gridCols[c]['@_w'] = String(width)
836
786
  }
837
787
 
838
- const decl = this.#xmlParser.extractDeclaration(slideXml)
839
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
788
+ slideManager.markSlideObjDirty(slideIndex)
840
789
  }
841
790
 
842
791
  /**
@@ -849,10 +798,12 @@ class TableManager {
849
798
  * @param {SlideManager} slideManager
850
799
  */
851
800
  resizeTable(slideIndex, tableId, width, height, slideManager) {
852
- const slideXml = slideManager.getSlideXml(slideIndex)
853
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
801
+ const slideObj = slideManager.getSlideObj(slideIndex)
854
802
 
855
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
803
+ const spTree =
804
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
805
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
806
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
856
807
  if (!spTree) return
857
808
 
858
809
  let frames = spTree['p:graphicFrame'] || []
@@ -931,8 +882,7 @@ class TableManager {
931
882
  }
932
883
  }
933
884
 
934
- const decl = this.#xmlParser.extractDeclaration(slideXml)
935
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
885
+ slideManager.markSlideObjDirty(slideIndex)
936
886
  }
937
887
 
938
888
  /**
@@ -943,11 +893,13 @@ class TableManager {
943
893
  * @returns {Array<{name: string, id: string, rows: number, cols: number}>}
944
894
  */
945
895
  inspectTables(slideIndex, slideManager) {
946
- const slideXml = slideManager.getSlideXml(slideIndex)
947
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
896
+ const slideObj = slideManager.getSlideObj(slideIndex)
948
897
  const tables = []
949
898
 
950
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
899
+ const spTree =
900
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
901
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
902
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
951
903
  if (!spTree) return []
952
904
 
953
905
  let frames = spTree['p:graphicFrame'] || []
@@ -1064,8 +1016,16 @@ class TableManager {
1064
1016
  /**
1065
1017
  * Helper to find a table element inside a slide parsed object.
1066
1018
  */
1067
- #findTableObj(slideObj, tableId) {
1068
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
1019
+ #findTableObj(slideObj, tableId, slideManager, slideIndex) {
1020
+ if (slideManager && slideIndex !== undefined) {
1021
+ const res = slideManager.getSlideTable(slideIndex, tableId)
1022
+ return res ? res.table : null
1023
+ }
1024
+
1025
+ const spTree =
1026
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
1027
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
1028
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
1069
1029
  if (!spTree) return null
1070
1030
 
1071
1031
  let frames = spTree['p:graphicFrame'] || []
@@ -1089,6 +1049,15 @@ class TableManager {
1089
1049
  return null
1090
1050
  }
1091
1051
 
1052
+ #getTableContext(slideIndex, tableId, slideManager) {
1053
+ const slideObj = slideManager.getSlideObj(slideIndex)
1054
+ const tblObj = this.#findTableObj(slideObj, tableId, slideManager, slideIndex)
1055
+ if (!tblObj) {
1056
+ throw new TableNotFoundError(`Table "${tableId}" not found in slide ${slideIndex}`)
1057
+ }
1058
+ return { slideObj, tblObj }
1059
+ }
1060
+
1092
1061
  /**
1093
1062
  * Generates a new rowId for the given row object.
1094
1063
  */
@@ -2,14 +2,9 @@
2
2
  * @fileoverview ZOrderManager - Handles slide element Z-order (layer stacking) operations.
3
3
  */
4
4
 
5
- const { createLogger } = require('../utils/logger.js')
6
5
  const { PPTXError } = require('../utils/errors.js')
7
6
  const { Z_ORDER_SYMBOL } = require('../parsers/XMLParser.js')
8
7
 
9
- const logger = createLogger('ZOrderManager')
10
-
11
- const drawingTags = new Set(['p:sp', 'p:pic', 'p:graphicFrame', 'p:grpSp', 'p:cxnSp'])
12
-
13
8
  function detectElementType(tag, item) {
14
9
  if (tag === 'p:sp') {
15
10
  const isTxBox =
@@ -82,6 +82,18 @@ class ZipManager {
82
82
  */
83
83
  #removedFiles = new Set()
84
84
 
85
+ /**
86
+ * @private
87
+ * @type {Map<string, { type: string, content: string|Buffer|Uint8Array }>|null}
88
+ */
89
+ #cachedFiles = null
90
+
91
+ async loadFromCache(cachedFilesMap) {
92
+ this.#cachedFiles = cachedFilesMap
93
+ await this.#loadCoreProperties()
94
+ logger.debug(`Loaded from cache. Files: ${cachedFilesMap.size}`)
95
+ }
96
+
85
97
  async load(source) {
86
98
  try {
87
99
  const path = require('path')
@@ -221,6 +233,19 @@ class ZipManager {
221
233
  return this.#dirtyFiles.get(normalPath)
222
234
  }
223
235
 
236
+ if (this.#cachedFiles && this.#cachedFiles.has(normalPath)) {
237
+ const entry = this.#cachedFiles.get(normalPath)
238
+ let content
239
+ if (entry.type === 'text') {
240
+ content = entry.content
241
+ } else {
242
+ const { TextDecoder } = require('util')
243
+ content = new TextDecoder('utf-8').decode(entry.content)
244
+ }
245
+ this.#xmlCache.set(normalPath, content)
246
+ return content
247
+ }
248
+
224
249
  if (this.#isFolderMode) {
225
250
  const path = require('path')
226
251
  const fs = require('fs-extra')
@@ -234,6 +259,7 @@ class ZipManager {
234
259
  return content
235
260
  }
236
261
 
262
+ if (!this.#zip) return null
237
263
  const file = this.#zip.file(normalPath)
238
264
  if (!file) {
239
265
  logger.debug(`File not found in ZIP: ${normalPath}`)
@@ -256,7 +282,22 @@ class ZipManager {
256
282
  if (this.#dirtyFiles.has(normalPath)) {
257
283
  return this.#dirtyFiles.get(normalPath)
258
284
  }
259
- return this.#xmlCache.get(normalPath) || null
285
+ if (this.#xmlCache.has(normalPath)) {
286
+ return this.#xmlCache.get(normalPath)
287
+ }
288
+ if (this.#cachedFiles && this.#cachedFiles.has(normalPath)) {
289
+ const entry = this.#cachedFiles.get(normalPath)
290
+ let content
291
+ if (entry.type === 'text') {
292
+ content = entry.content
293
+ } else {
294
+ const { TextDecoder } = require('util')
295
+ content = new TextDecoder('utf-8').decode(entry.content)
296
+ }
297
+ this.#xmlCache.set(normalPath, content)
298
+ return content
299
+ }
300
+ return null
260
301
  }
261
302
 
262
303
  /**
@@ -270,6 +311,10 @@ class ZipManager {
270
311
  if (this.#dirtyBinaryFiles.has(normalPath)) {
271
312
  return this.#dirtyBinaryFiles.get(normalPath)
272
313
  }
314
+ if (this.#cachedFiles && this.#cachedFiles.has(normalPath)) {
315
+ const entry = this.#cachedFiles.get(normalPath)
316
+ return entry.content
317
+ }
273
318
  if (this.#isFolderMode) {
274
319
  const path = require('path')
275
320
  const fs = require('fs-extra')
@@ -277,6 +322,7 @@ class ZipManager {
277
322
  if (!(await fs.pathExists(diskPath))) return null
278
323
  return fs.readFile(diskPath)
279
324
  }
325
+ if (!this.#zip) return null
280
326
  const file = this.#zip.file(normalPath)
281
327
  if (!file) return null
282
328
  return file.async('uint8array')
@@ -354,10 +400,11 @@ class ZipManager {
354
400
  const normalPath = zipPath.replace(/\\/g, '/')
355
401
  if (this.#removedFiles.has(normalPath)) return false
356
402
  if (this.#dirtyFiles.has(normalPath) || this.#dirtyBinaryFiles.has(normalPath)) return true
403
+ if (this.#cachedFiles && this.#cachedFiles.has(normalPath)) return true
357
404
  if (this.#isFolderMode) {
358
405
  return this.#folderFiles.has(normalPath)
359
406
  }
360
- return this.#zip.file(normalPath) !== null
407
+ return this.#zip && this.#zip.file(normalPath) !== null
361
408
  }
362
409
 
363
410
  /**
@@ -367,6 +414,14 @@ class ZipManager {
367
414
  * @returns {string[]} Array of matching file paths.
368
415
  */
369
416
  listFiles(prefix = '') {
417
+ if (this.#cachedFiles) {
418
+ const allFiles = new Set([
419
+ ...this.#cachedFiles.keys(),
420
+ ...this.#dirtyFiles.keys(),
421
+ ...this.#dirtyBinaryFiles.keys(),
422
+ ])
423
+ return Array.from(allFiles).filter(f => !this.#removedFiles.has(f) && f.startsWith(prefix))
424
+ }
370
425
  if (this.#isFolderMode) {
371
426
  const allFiles = new Set([
372
427
  ...this.#folderFiles,
@@ -375,6 +430,7 @@ class ZipManager {
375
430
  ])
376
431
  return Array.from(allFiles).filter(f => !this.#removedFiles.has(f) && f.startsWith(prefix))
377
432
  }
433
+ if (!this.#zip) return []
378
434
  return Object.keys(this.#zip.files).filter(f => !this.#zip.files[f].dir && f.startsWith(prefix))
379
435
  }
380
436
 
@@ -384,23 +440,43 @@ class ZipManager {
384
440
  *
385
441
  * @returns {Promise<Buffer>} Compressed PPTX as a Buffer.
386
442
  */
387
- async toBuffer() {
443
+ async toBuffer(options = {}) {
388
444
  await this.#ensureZipForExport()
389
- return this.#zip.generateAsync({
390
- type: 'nodebuffer',
391
- compression: 'DEFLATE',
392
- compressionOptions: { level: 6 },
393
- })
445
+ const zipOptions = this.#getZipOptions(options)
446
+ return this.#zip.generateAsync(zipOptions)
394
447
  }
395
448
 
396
- async toStream() {
449
+ async toStream(options = {}) {
397
450
  await this.#ensureZipForExport()
398
- return this.#zip.generateNodeStream({
451
+ const zipOptions = this.#getZipOptions(options)
452
+ zipOptions.streamFiles = true
453
+ return this.#zip.generateNodeStream(zipOptions)
454
+ }
455
+
456
+ #getZipOptions(options = {}) {
457
+ const compression = options.compression || 'balanced'
458
+ let method = 'DEFLATE'
459
+ let level = 6
460
+
461
+ if (compression === 'none' || compression === 'store') {
462
+ method = 'STORE'
463
+ level = 0
464
+ } else if (compression === 'fast') {
465
+ method = 'DEFLATE'
466
+ level = 1
467
+ } else if (compression === 'balanced') {
468
+ method = 'DEFLATE'
469
+ level = 6
470
+ } else if (compression === 'maximum') {
471
+ method = 'DEFLATE'
472
+ level = 9
473
+ }
474
+
475
+ return {
399
476
  type: 'nodebuffer',
400
- compression: 'DEFLATE',
401
- compressionOptions: { level: 6 },
402
- streamFiles: true,
403
- })
477
+ compression: method,
478
+ compressionOptions: method === 'DEFLATE' ? { level } : undefined,
479
+ }
404
480
  }
405
481
 
406
482
  /**
@@ -594,6 +670,36 @@ class ZipManager {
594
670
 
595
671
  const zip = new JSZip()
596
672
 
673
+ if (this.#cachedFiles) {
674
+ // 1. Read all files from cache (that are not removed)
675
+ for (const [relPath, entry] of this.#cachedFiles.entries()) {
676
+ if (this.#removedFiles.has(relPath)) continue
677
+
678
+ if (this.#dirtyFiles.has(relPath)) {
679
+ zip.file(relPath, this.#dirtyFiles.get(relPath))
680
+ } else if (this.#dirtyBinaryFiles.has(relPath)) {
681
+ zip.file(relPath, this.#dirtyBinaryFiles.get(relPath))
682
+ } else {
683
+ zip.file(relPath, entry.content)
684
+ }
685
+ }
686
+
687
+ // 2. Write any new files that were added (and not already in cache)
688
+ for (const [relPath, content] of this.#dirtyFiles.entries()) {
689
+ if (!this.#cachedFiles.has(relPath) && !this.#removedFiles.has(relPath)) {
690
+ zip.file(relPath, content)
691
+ }
692
+ }
693
+ for (const [relPath, data] of this.#dirtyBinaryFiles.entries()) {
694
+ if (!this.#cachedFiles.has(relPath) && !this.#removedFiles.has(relPath)) {
695
+ zip.file(relPath, data)
696
+ }
697
+ }
698
+
699
+ this.#zip = zip
700
+ return zip
701
+ }
702
+
597
703
  // 1. Read all files from the original folder structure (that are not removed)
598
704
  for (const relPath of this.#folderFiles) {
599
705
  if (this.#removedFiles.has(relPath)) continue
@@ -184,7 +184,7 @@ class ChartWorkbookUpdater {
184
184
  static #serializeSheetXml(sheetXml, cells) {
185
185
  // Group cells by row
186
186
  const rows = {}
187
- for (const [ref, val] of Object.entries(cells)) {
187
+ for (const ref of Object.keys(cells)) {
188
188
  const rowMatch = /\d+$/.exec(ref)
189
189
  if (!rowMatch) continue
190
190
  const r = parseInt(rowMatch[0], 10)
@@ -70,10 +70,9 @@ class ContentTypesHelper {
70
70
  */
71
71
  addMediaDefault(zipManager, extension, mimeType) {
72
72
  this.#updateQueue = this.#updateQueue.then(async () => {
73
- const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
74
- if (!xmlFile) return
73
+ const content = await zipManager.readFile('[Content_Types].xml')
74
+ if (!content) return
75
75
 
76
- const content = await xmlFile.async('text')
77
76
  const entry = `Extension="${extension}" ContentType="${mimeType}"`
78
77
  if (!content.includes(entry)) {
79
78
  const updated = content.replace('</Types>', ` <Default ${entry}/>\n</Types>`)
@@ -92,12 +91,11 @@ class ContentTypesHelper {
92
91
  */
93
92
  #addOverride(zipManager, partName, contentType) {
94
93
  this.#updateQueue = this.#updateQueue.then(async () => {
95
- const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
96
- if (!xmlFile) {
94
+ const content = await zipManager.readFile('[Content_Types].xml')
95
+ if (!content) {
97
96
  logger.warn('[Content_Types].xml not found')
98
97
  return
99
98
  }
100
- const content = await xmlFile.async('text')
101
99
  const entry = `PartName="${partName}"`
102
100
  if (!content.includes(entry)) {
103
101
  const override = `<Override PartName="${partName}" ContentType="${contentType}"/>`
@@ -115,12 +113,11 @@ class ContentTypesHelper {
115
113
  */
116
114
  #removeOverride(zipManager, partName) {
117
115
  this.#updateQueue = this.#updateQueue.then(async () => {
118
- const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
119
- if (!xmlFile) {
116
+ const content = await zipManager.readFile('[Content_Types].xml')
117
+ if (!content) {
120
118
  logger.warn('[Content_Types].xml not found')
121
119
  return
122
120
  }
123
- const content = await xmlFile.async('text')
124
121
  const regex = new RegExp(`<Override[^>]*PartName="${partName}"[^>]*/>\\s*`, 'g')
125
122
  if (regex.test(content)) {
126
123
  const updated = content.replace(regex, '')