@cj-tech-master/excelts 5.0.3 → 5.0.4-canary.20260125235458.742dade
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/dist/browser/modules/archive/unzip/zip-parser.d.ts +10 -0
- package/dist/browser/modules/archive/unzip/zip-parser.js +24 -0
- package/dist/browser/modules/archive/zip/index.d.ts +5 -0
- package/dist/browser/modules/archive/zip/index.js +8 -4
- package/dist/browser/modules/archive/zip/zip-bytes.d.ts +5 -0
- package/dist/browser/modules/archive/zip/zip-bytes.js +33 -11
- package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +1 -0
- package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +9 -5
- package/dist/cjs/modules/archive/unzip/zip-parser.js +24 -0
- package/dist/cjs/modules/archive/zip/index.js +8 -4
- package/dist/cjs/modules/archive/zip/zip-bytes.js +33 -11
- package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +9 -5
- package/dist/esm/modules/archive/unzip/zip-parser.js +24 -0
- package/dist/esm/modules/archive/zip/index.js +8 -4
- package/dist/esm/modules/archive/zip/zip-bytes.js +33 -11
- package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +9 -5
- package/dist/iife/excelts.iife.js +54 -17
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +5 -5
- package/dist/types/modules/archive/unzip/zip-parser.d.ts +10 -0
- package/dist/types/modules/archive/zip/index.d.ts +5 -0
- package/dist/types/modules/archive/zip/zip-bytes.d.ts +5 -0
- package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +1 -0
- package/package.json +1 -1
|
@@ -32,6 +32,16 @@ export declare class ZipParser {
|
|
|
32
32
|
* Check if entry exists
|
|
33
33
|
*/
|
|
34
34
|
hasEntry(path: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Get the number of child entries in a directory.
|
|
37
|
+
*
|
|
38
|
+
* Returns the count of entries whose paths start with the directory prefix,
|
|
39
|
+
* excluding the directory entry itself. For non-directory entries, returns 0.
|
|
40
|
+
*
|
|
41
|
+
* @param path - Directory path (with or without trailing slash)
|
|
42
|
+
* @returns Number of child entries
|
|
43
|
+
*/
|
|
44
|
+
getChildCount(path: string): number;
|
|
35
45
|
/**
|
|
36
46
|
* List all file paths
|
|
37
47
|
*/
|
|
@@ -275,6 +275,30 @@ export class ZipParser {
|
|
|
275
275
|
hasEntry(path) {
|
|
276
276
|
return this.entryMap.has(path);
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the number of child entries in a directory.
|
|
280
|
+
*
|
|
281
|
+
* Returns the count of entries whose paths start with the directory prefix,
|
|
282
|
+
* excluding the directory entry itself. For non-directory entries, returns 0.
|
|
283
|
+
*
|
|
284
|
+
* @param path - Directory path (with or without trailing slash)
|
|
285
|
+
* @returns Number of child entries
|
|
286
|
+
*/
|
|
287
|
+
getChildCount(path) {
|
|
288
|
+
const entry = this.entryMap.get(path);
|
|
289
|
+
if (!entry?.isDirectory) {
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
// Ensure prefix ends with "/" for correct matching
|
|
293
|
+
const prefix = entry.path.endsWith("/") ? entry.path : entry.path + "/";
|
|
294
|
+
let count = 0;
|
|
295
|
+
for (const e of this.entries) {
|
|
296
|
+
if (e.path.startsWith(prefix) && e !== entry) {
|
|
297
|
+
count++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return count;
|
|
301
|
+
}
|
|
278
302
|
/**
|
|
279
303
|
* List all file paths
|
|
280
304
|
*/
|
|
@@ -22,6 +22,11 @@ export interface ZipOptions {
|
|
|
22
22
|
* If false, always follow `level` (DEFLATE when level > 0).
|
|
23
23
|
*/
|
|
24
24
|
smartStore?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* If true, entries are written in their original input order.
|
|
27
|
+
* If false (default), entries are sorted alphabetically by name.
|
|
28
|
+
*/
|
|
29
|
+
noSort?: boolean;
|
|
25
30
|
}
|
|
26
31
|
export interface ZipEntryOptions {
|
|
27
32
|
level?: number;
|
|
@@ -15,7 +15,8 @@ export class ZipArchive {
|
|
|
15
15
|
timestamps: options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS),
|
|
16
16
|
comment: options.comment,
|
|
17
17
|
modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
|
|
18
|
-
smartStore: options.smartStore ?? true
|
|
18
|
+
smartStore: options.smartStore ?? true,
|
|
19
|
+
noSort: options.noSort ?? false
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
add(name, source, options) {
|
|
@@ -105,7 +106,8 @@ export class ZipArchive {
|
|
|
105
106
|
timestamps: this._options.timestamps,
|
|
106
107
|
modTime: this._options.modTime,
|
|
107
108
|
comment: this._options.comment,
|
|
108
|
-
smartStore: this._options.smartStore
|
|
109
|
+
smartStore: this._options.smartStore,
|
|
110
|
+
noSort: this._options.noSort
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
113
|
const entries = await Promise.all(this._entries.map(async (e) => ({
|
|
@@ -120,7 +122,8 @@ export class ZipArchive {
|
|
|
120
122
|
timestamps: this._options.timestamps,
|
|
121
123
|
modTime: this._options.modTime,
|
|
122
124
|
comment: this._options.comment,
|
|
123
|
-
smartStore: this._options.smartStore
|
|
125
|
+
smartStore: this._options.smartStore,
|
|
126
|
+
noSort: this._options.noSort
|
|
124
127
|
});
|
|
125
128
|
}
|
|
126
129
|
return collect(this.stream());
|
|
@@ -145,7 +148,8 @@ export class ZipArchive {
|
|
|
145
148
|
timestamps: this._options.timestamps,
|
|
146
149
|
modTime: this._options.modTime,
|
|
147
150
|
comment: this._options.comment,
|
|
148
|
-
smartStore: this._options.smartStore
|
|
151
|
+
smartStore: this._options.smartStore,
|
|
152
|
+
noSort: this._options.noSort
|
|
149
153
|
});
|
|
150
154
|
}
|
|
151
155
|
async pipeTo(sink) {
|
|
@@ -42,6 +42,11 @@ export interface ZipOptions extends CompressOptions {
|
|
|
42
42
|
* - default `timestamps` becomes "dos" (no UTC extra field)
|
|
43
43
|
*/
|
|
44
44
|
reproducible?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* If true, entries are written in their original input order.
|
|
47
|
+
* If false (default), entries are sorted alphabetically by name.
|
|
48
|
+
*/
|
|
49
|
+
noSort?: boolean;
|
|
45
50
|
/**
|
|
46
51
|
* Max number of entries to compress concurrently in `createZip()`.
|
|
47
52
|
* This helps avoid zlib threadpool saturation / memory spikes with many files.
|
|
@@ -93,13 +93,23 @@ function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
|
|
|
93
93
|
offset
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Sort entries alphabetically by name (case-insensitive).
|
|
98
|
+
* Uses Schwartzian transform to avoid repeated decoding during sort.
|
|
99
|
+
*/
|
|
100
|
+
function sortEntriesByName(entries) {
|
|
101
|
+
if (entries.length <= 1) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
const decorated = entries.map(entry => ({
|
|
106
|
+
entry,
|
|
107
|
+
sortKey: decoder.decode(entry.name).toLowerCase()
|
|
108
|
+
}));
|
|
109
|
+
decorated.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
110
|
+
for (let i = 0; i < decorated.length; i++) {
|
|
111
|
+
entries[i] = decorated[i].entry;
|
|
112
|
+
}
|
|
103
113
|
}
|
|
104
114
|
function finalizeZip(processedEntries, zipComment) {
|
|
105
115
|
// Assemble ZIP into a single buffer to reduce allocations and copying.
|
|
@@ -161,6 +171,7 @@ function finalizeZip(processedEntries, zipComment) {
|
|
|
161
171
|
*/
|
|
162
172
|
export async function createZip(entries, options = {}) {
|
|
163
173
|
const reproducible = options.reproducible ?? false;
|
|
174
|
+
const noSort = options.noSort ?? false;
|
|
164
175
|
const level = options.level ?? DEFAULT_ZIP_LEVEL;
|
|
165
176
|
const smartStore = options.smartStore ?? true;
|
|
166
177
|
const concurrency = options.concurrency ?? 4;
|
|
@@ -196,7 +207,10 @@ export async function createZip(entries, options = {}) {
|
|
|
196
207
|
});
|
|
197
208
|
await Promise.all(workers);
|
|
198
209
|
}
|
|
199
|
-
|
|
210
|
+
if (!noSort) {
|
|
211
|
+
sortEntriesByName(processedEntries);
|
|
212
|
+
}
|
|
213
|
+
// Compute offsets after sorting.
|
|
200
214
|
let currentOffset = 0;
|
|
201
215
|
for (let i = 0; i < processedEntries.length; i++) {
|
|
202
216
|
const processedEntry = processedEntries[i];
|
|
@@ -212,6 +226,7 @@ export async function createZip(entries, options = {}) {
|
|
|
212
226
|
*/
|
|
213
227
|
export function createZipSync(entries, options = {}) {
|
|
214
228
|
const reproducible = options.reproducible ?? false;
|
|
229
|
+
const noSort = options.noSort ?? false;
|
|
215
230
|
const level = options.level ?? DEFAULT_ZIP_LEVEL;
|
|
216
231
|
const smartStore = options.smartStore ?? true;
|
|
217
232
|
const timestamps = options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS);
|
|
@@ -224,7 +239,6 @@ export function createZipSync(entries, options = {}) {
|
|
|
224
239
|
};
|
|
225
240
|
const thresholdBytes = options.thresholdBytes;
|
|
226
241
|
const processedEntries = [];
|
|
227
|
-
let currentOffset = 0;
|
|
228
242
|
for (const entry of entries) {
|
|
229
243
|
const entryLevel = entry.level ?? level;
|
|
230
244
|
const compressOptions = {
|
|
@@ -232,8 +246,16 @@ export function createZipSync(entries, options = {}) {
|
|
|
232
246
|
thresholdBytes
|
|
233
247
|
};
|
|
234
248
|
const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
processedEntries.push(buildProcessedEntry(entry, 0, settings, compressedData, deflate));
|
|
250
|
+
}
|
|
251
|
+
if (!noSort) {
|
|
252
|
+
sortEntriesByName(processedEntries);
|
|
253
|
+
}
|
|
254
|
+
// Compute offsets after sorting.
|
|
255
|
+
let currentOffset = 0;
|
|
256
|
+
for (const processedEntry of processedEntries) {
|
|
257
|
+
processedEntry.offset = currentOffset;
|
|
258
|
+
currentOffset += computeLocalRecordSize(processedEntry);
|
|
237
259
|
}
|
|
238
260
|
return finalizeZip(processedEntries, zipComment);
|
|
239
261
|
}
|
|
@@ -71,7 +71,7 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
71
71
|
* Render loaded pivot cache definition (preserving original structure)
|
|
72
72
|
*/
|
|
73
73
|
renderLoaded(xmlStream, model) {
|
|
74
|
-
const { cacheFields, sourceRef, sourceSheet, recordCount } = model;
|
|
74
|
+
const { cacheFields, sourceRef, sourceSheet, sourceTableName, recordCount } = model;
|
|
75
75
|
xmlStream.openXml(XmlStream.StdDocAttributes);
|
|
76
76
|
xmlStream.openNode(this.tag, {
|
|
77
77
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
@@ -83,10 +83,13 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
83
83
|
recordCount: recordCount || cacheFields.length + 1
|
|
84
84
|
});
|
|
85
85
|
xmlStream.openNode("cacheSource", { type: "worksheet" });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// worksheetSource supports two reference styles:
|
|
87
|
+
// 1. name: references a named Table
|
|
88
|
+
// 2. ref + sheet: references a cell range on a worksheet
|
|
89
|
+
const worksheetSourceAttrs = sourceTableName
|
|
90
|
+
? { name: sourceTableName }
|
|
91
|
+
: { ref: sourceRef, sheet: sourceSheet };
|
|
92
|
+
xmlStream.leafNode("worksheetSource", worksheetSourceAttrs);
|
|
90
93
|
xmlStream.closeNode();
|
|
91
94
|
xmlStream.openNode("cacheFields", { count: cacheFields.length });
|
|
92
95
|
xmlStream.writeXml(cacheFields
|
|
@@ -124,6 +127,7 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
124
127
|
if (this.inCacheSource && this.model) {
|
|
125
128
|
this.model.sourceRef = attributes.ref;
|
|
126
129
|
this.model.sourceSheet = attributes.sheet;
|
|
130
|
+
this.model.sourceTableName = attributes.name;
|
|
127
131
|
}
|
|
128
132
|
break;
|
|
129
133
|
case "cacheFields":
|
|
@@ -278,6 +278,30 @@ class ZipParser {
|
|
|
278
278
|
hasEntry(path) {
|
|
279
279
|
return this.entryMap.has(path);
|
|
280
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Get the number of child entries in a directory.
|
|
283
|
+
*
|
|
284
|
+
* Returns the count of entries whose paths start with the directory prefix,
|
|
285
|
+
* excluding the directory entry itself. For non-directory entries, returns 0.
|
|
286
|
+
*
|
|
287
|
+
* @param path - Directory path (with or without trailing slash)
|
|
288
|
+
* @returns Number of child entries
|
|
289
|
+
*/
|
|
290
|
+
getChildCount(path) {
|
|
291
|
+
const entry = this.entryMap.get(path);
|
|
292
|
+
if (!entry?.isDirectory) {
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
// Ensure prefix ends with "/" for correct matching
|
|
296
|
+
const prefix = entry.path.endsWith("/") ? entry.path : entry.path + "/";
|
|
297
|
+
let count = 0;
|
|
298
|
+
for (const e of this.entries) {
|
|
299
|
+
if (e.path.startsWith(prefix) && e !== entry) {
|
|
300
|
+
count++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return count;
|
|
304
|
+
}
|
|
281
305
|
/**
|
|
282
306
|
* List all file paths
|
|
283
307
|
*/
|
|
@@ -19,7 +19,8 @@ class ZipArchive {
|
|
|
19
19
|
timestamps: options.timestamps ?? (reproducible ? "dos" : defaults_1.DEFAULT_ZIP_TIMESTAMPS),
|
|
20
20
|
comment: options.comment,
|
|
21
21
|
modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
|
|
22
|
-
smartStore: options.smartStore ?? true
|
|
22
|
+
smartStore: options.smartStore ?? true,
|
|
23
|
+
noSort: options.noSort ?? false
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
add(name, source, options) {
|
|
@@ -109,7 +110,8 @@ class ZipArchive {
|
|
|
109
110
|
timestamps: this._options.timestamps,
|
|
110
111
|
modTime: this._options.modTime,
|
|
111
112
|
comment: this._options.comment,
|
|
112
|
-
smartStore: this._options.smartStore
|
|
113
|
+
smartStore: this._options.smartStore,
|
|
114
|
+
noSort: this._options.noSort
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
const entries = await Promise.all(this._entries.map(async (e) => ({
|
|
@@ -124,7 +126,8 @@ class ZipArchive {
|
|
|
124
126
|
timestamps: this._options.timestamps,
|
|
125
127
|
modTime: this._options.modTime,
|
|
126
128
|
comment: this._options.comment,
|
|
127
|
-
smartStore: this._options.smartStore
|
|
129
|
+
smartStore: this._options.smartStore,
|
|
130
|
+
noSort: this._options.noSort
|
|
128
131
|
});
|
|
129
132
|
}
|
|
130
133
|
return (0, archive_sink_1.collect)(this.stream());
|
|
@@ -149,7 +152,8 @@ class ZipArchive {
|
|
|
149
152
|
timestamps: this._options.timestamps,
|
|
150
153
|
modTime: this._options.modTime,
|
|
151
154
|
comment: this._options.comment,
|
|
152
|
-
smartStore: this._options.smartStore
|
|
155
|
+
smartStore: this._options.smartStore,
|
|
156
|
+
noSort: this._options.noSort
|
|
153
157
|
});
|
|
154
158
|
}
|
|
155
159
|
async pipeTo(sink) {
|
|
@@ -96,13 +96,23 @@ function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
|
|
|
96
96
|
offset
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Sort entries alphabetically by name (case-insensitive).
|
|
101
|
+
* Uses Schwartzian transform to avoid repeated decoding during sort.
|
|
102
|
+
*/
|
|
103
|
+
function sortEntriesByName(entries) {
|
|
104
|
+
if (entries.length <= 1) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const decoder = new TextDecoder();
|
|
108
|
+
const decorated = entries.map(entry => ({
|
|
109
|
+
entry,
|
|
110
|
+
sortKey: decoder.decode(entry.name).toLowerCase()
|
|
111
|
+
}));
|
|
112
|
+
decorated.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
113
|
+
for (let i = 0; i < decorated.length; i++) {
|
|
114
|
+
entries[i] = decorated[i].entry;
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
117
|
function finalizeZip(processedEntries, zipComment) {
|
|
108
118
|
// Assemble ZIP into a single buffer to reduce allocations and copying.
|
|
@@ -164,6 +174,7 @@ function finalizeZip(processedEntries, zipComment) {
|
|
|
164
174
|
*/
|
|
165
175
|
async function createZip(entries, options = {}) {
|
|
166
176
|
const reproducible = options.reproducible ?? false;
|
|
177
|
+
const noSort = options.noSort ?? false;
|
|
167
178
|
const level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
|
|
168
179
|
const smartStore = options.smartStore ?? true;
|
|
169
180
|
const concurrency = options.concurrency ?? 4;
|
|
@@ -199,7 +210,10 @@ async function createZip(entries, options = {}) {
|
|
|
199
210
|
});
|
|
200
211
|
await Promise.all(workers);
|
|
201
212
|
}
|
|
202
|
-
|
|
213
|
+
if (!noSort) {
|
|
214
|
+
sortEntriesByName(processedEntries);
|
|
215
|
+
}
|
|
216
|
+
// Compute offsets after sorting.
|
|
203
217
|
let currentOffset = 0;
|
|
204
218
|
for (let i = 0; i < processedEntries.length; i++) {
|
|
205
219
|
const processedEntry = processedEntries[i];
|
|
@@ -215,6 +229,7 @@ async function createZip(entries, options = {}) {
|
|
|
215
229
|
*/
|
|
216
230
|
function createZipSync(entries, options = {}) {
|
|
217
231
|
const reproducible = options.reproducible ?? false;
|
|
232
|
+
const noSort = options.noSort ?? false;
|
|
218
233
|
const level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
|
|
219
234
|
const smartStore = options.smartStore ?? true;
|
|
220
235
|
const timestamps = options.timestamps ?? (reproducible ? "dos" : defaults_1.DEFAULT_ZIP_TIMESTAMPS);
|
|
@@ -227,7 +242,6 @@ function createZipSync(entries, options = {}) {
|
|
|
227
242
|
};
|
|
228
243
|
const thresholdBytes = options.thresholdBytes;
|
|
229
244
|
const processedEntries = [];
|
|
230
|
-
let currentOffset = 0;
|
|
231
245
|
for (const entry of entries) {
|
|
232
246
|
const entryLevel = entry.level ?? level;
|
|
233
247
|
const compressOptions = {
|
|
@@ -235,8 +249,16 @@ function createZipSync(entries, options = {}) {
|
|
|
235
249
|
thresholdBytes
|
|
236
250
|
};
|
|
237
251
|
const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
processedEntries.push(buildProcessedEntry(entry, 0, settings, compressedData, deflate));
|
|
253
|
+
}
|
|
254
|
+
if (!noSort) {
|
|
255
|
+
sortEntriesByName(processedEntries);
|
|
256
|
+
}
|
|
257
|
+
// Compute offsets after sorting.
|
|
258
|
+
let currentOffset = 0;
|
|
259
|
+
for (const processedEntry of processedEntries) {
|
|
260
|
+
processedEntry.offset = currentOffset;
|
|
261
|
+
currentOffset += computeLocalRecordSize(processedEntry);
|
|
240
262
|
}
|
|
241
263
|
return finalizeZip(processedEntries, zipComment);
|
|
242
264
|
}
|
|
@@ -74,7 +74,7 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
74
74
|
* Render loaded pivot cache definition (preserving original structure)
|
|
75
75
|
*/
|
|
76
76
|
renderLoaded(xmlStream, model) {
|
|
77
|
-
const { cacheFields, sourceRef, sourceSheet, recordCount } = model;
|
|
77
|
+
const { cacheFields, sourceRef, sourceSheet, sourceTableName, recordCount } = model;
|
|
78
78
|
xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
|
|
79
79
|
xmlStream.openNode(this.tag, {
|
|
80
80
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
@@ -86,10 +86,13 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
86
86
|
recordCount: recordCount || cacheFields.length + 1
|
|
87
87
|
});
|
|
88
88
|
xmlStream.openNode("cacheSource", { type: "worksheet" });
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// worksheetSource supports two reference styles:
|
|
90
|
+
// 1. name: references a named Table
|
|
91
|
+
// 2. ref + sheet: references a cell range on a worksheet
|
|
92
|
+
const worksheetSourceAttrs = sourceTableName
|
|
93
|
+
? { name: sourceTableName }
|
|
94
|
+
: { ref: sourceRef, sheet: sourceSheet };
|
|
95
|
+
xmlStream.leafNode("worksheetSource", worksheetSourceAttrs);
|
|
93
96
|
xmlStream.closeNode();
|
|
94
97
|
xmlStream.openNode("cacheFields", { count: cacheFields.length });
|
|
95
98
|
xmlStream.writeXml(cacheFields
|
|
@@ -127,6 +130,7 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
127
130
|
if (this.inCacheSource && this.model) {
|
|
128
131
|
this.model.sourceRef = attributes.ref;
|
|
129
132
|
this.model.sourceSheet = attributes.sheet;
|
|
133
|
+
this.model.sourceTableName = attributes.name;
|
|
130
134
|
}
|
|
131
135
|
break;
|
|
132
136
|
case "cacheFields":
|
|
@@ -275,6 +275,30 @@ export class ZipParser {
|
|
|
275
275
|
hasEntry(path) {
|
|
276
276
|
return this.entryMap.has(path);
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the number of child entries in a directory.
|
|
280
|
+
*
|
|
281
|
+
* Returns the count of entries whose paths start with the directory prefix,
|
|
282
|
+
* excluding the directory entry itself. For non-directory entries, returns 0.
|
|
283
|
+
*
|
|
284
|
+
* @param path - Directory path (with or without trailing slash)
|
|
285
|
+
* @returns Number of child entries
|
|
286
|
+
*/
|
|
287
|
+
getChildCount(path) {
|
|
288
|
+
const entry = this.entryMap.get(path);
|
|
289
|
+
if (!entry?.isDirectory) {
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
// Ensure prefix ends with "/" for correct matching
|
|
293
|
+
const prefix = entry.path.endsWith("/") ? entry.path : entry.path + "/";
|
|
294
|
+
let count = 0;
|
|
295
|
+
for (const e of this.entries) {
|
|
296
|
+
if (e.path.startsWith(prefix) && e !== entry) {
|
|
297
|
+
count++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return count;
|
|
301
|
+
}
|
|
278
302
|
/**
|
|
279
303
|
* List all file paths
|
|
280
304
|
*/
|
|
@@ -15,7 +15,8 @@ export class ZipArchive {
|
|
|
15
15
|
timestamps: options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS),
|
|
16
16
|
comment: options.comment,
|
|
17
17
|
modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
|
|
18
|
-
smartStore: options.smartStore ?? true
|
|
18
|
+
smartStore: options.smartStore ?? true,
|
|
19
|
+
noSort: options.noSort ?? false
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
add(name, source, options) {
|
|
@@ -105,7 +106,8 @@ export class ZipArchive {
|
|
|
105
106
|
timestamps: this._options.timestamps,
|
|
106
107
|
modTime: this._options.modTime,
|
|
107
108
|
comment: this._options.comment,
|
|
108
|
-
smartStore: this._options.smartStore
|
|
109
|
+
smartStore: this._options.smartStore,
|
|
110
|
+
noSort: this._options.noSort
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
113
|
const entries = await Promise.all(this._entries.map(async (e) => ({
|
|
@@ -120,7 +122,8 @@ export class ZipArchive {
|
|
|
120
122
|
timestamps: this._options.timestamps,
|
|
121
123
|
modTime: this._options.modTime,
|
|
122
124
|
comment: this._options.comment,
|
|
123
|
-
smartStore: this._options.smartStore
|
|
125
|
+
smartStore: this._options.smartStore,
|
|
126
|
+
noSort: this._options.noSort
|
|
124
127
|
});
|
|
125
128
|
}
|
|
126
129
|
return collect(this.stream());
|
|
@@ -145,7 +148,8 @@ export class ZipArchive {
|
|
|
145
148
|
timestamps: this._options.timestamps,
|
|
146
149
|
modTime: this._options.modTime,
|
|
147
150
|
comment: this._options.comment,
|
|
148
|
-
smartStore: this._options.smartStore
|
|
151
|
+
smartStore: this._options.smartStore,
|
|
152
|
+
noSort: this._options.noSort
|
|
149
153
|
});
|
|
150
154
|
}
|
|
151
155
|
async pipeTo(sink) {
|
|
@@ -93,13 +93,23 @@ function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
|
|
|
93
93
|
offset
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Sort entries alphabetically by name (case-insensitive).
|
|
98
|
+
* Uses Schwartzian transform to avoid repeated decoding during sort.
|
|
99
|
+
*/
|
|
100
|
+
function sortEntriesByName(entries) {
|
|
101
|
+
if (entries.length <= 1) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
const decorated = entries.map(entry => ({
|
|
106
|
+
entry,
|
|
107
|
+
sortKey: decoder.decode(entry.name).toLowerCase()
|
|
108
|
+
}));
|
|
109
|
+
decorated.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
110
|
+
for (let i = 0; i < decorated.length; i++) {
|
|
111
|
+
entries[i] = decorated[i].entry;
|
|
112
|
+
}
|
|
103
113
|
}
|
|
104
114
|
function finalizeZip(processedEntries, zipComment) {
|
|
105
115
|
// Assemble ZIP into a single buffer to reduce allocations and copying.
|
|
@@ -161,6 +171,7 @@ function finalizeZip(processedEntries, zipComment) {
|
|
|
161
171
|
*/
|
|
162
172
|
export async function createZip(entries, options = {}) {
|
|
163
173
|
const reproducible = options.reproducible ?? false;
|
|
174
|
+
const noSort = options.noSort ?? false;
|
|
164
175
|
const level = options.level ?? DEFAULT_ZIP_LEVEL;
|
|
165
176
|
const smartStore = options.smartStore ?? true;
|
|
166
177
|
const concurrency = options.concurrency ?? 4;
|
|
@@ -196,7 +207,10 @@ export async function createZip(entries, options = {}) {
|
|
|
196
207
|
});
|
|
197
208
|
await Promise.all(workers);
|
|
198
209
|
}
|
|
199
|
-
|
|
210
|
+
if (!noSort) {
|
|
211
|
+
sortEntriesByName(processedEntries);
|
|
212
|
+
}
|
|
213
|
+
// Compute offsets after sorting.
|
|
200
214
|
let currentOffset = 0;
|
|
201
215
|
for (let i = 0; i < processedEntries.length; i++) {
|
|
202
216
|
const processedEntry = processedEntries[i];
|
|
@@ -212,6 +226,7 @@ export async function createZip(entries, options = {}) {
|
|
|
212
226
|
*/
|
|
213
227
|
export function createZipSync(entries, options = {}) {
|
|
214
228
|
const reproducible = options.reproducible ?? false;
|
|
229
|
+
const noSort = options.noSort ?? false;
|
|
215
230
|
const level = options.level ?? DEFAULT_ZIP_LEVEL;
|
|
216
231
|
const smartStore = options.smartStore ?? true;
|
|
217
232
|
const timestamps = options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS);
|
|
@@ -224,7 +239,6 @@ export function createZipSync(entries, options = {}) {
|
|
|
224
239
|
};
|
|
225
240
|
const thresholdBytes = options.thresholdBytes;
|
|
226
241
|
const processedEntries = [];
|
|
227
|
-
let currentOffset = 0;
|
|
228
242
|
for (const entry of entries) {
|
|
229
243
|
const entryLevel = entry.level ?? level;
|
|
230
244
|
const compressOptions = {
|
|
@@ -232,8 +246,16 @@ export function createZipSync(entries, options = {}) {
|
|
|
232
246
|
thresholdBytes
|
|
233
247
|
};
|
|
234
248
|
const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
processedEntries.push(buildProcessedEntry(entry, 0, settings, compressedData, deflate));
|
|
250
|
+
}
|
|
251
|
+
if (!noSort) {
|
|
252
|
+
sortEntriesByName(processedEntries);
|
|
253
|
+
}
|
|
254
|
+
// Compute offsets after sorting.
|
|
255
|
+
let currentOffset = 0;
|
|
256
|
+
for (const processedEntry of processedEntries) {
|
|
257
|
+
processedEntry.offset = currentOffset;
|
|
258
|
+
currentOffset += computeLocalRecordSize(processedEntry);
|
|
237
259
|
}
|
|
238
260
|
return finalizeZip(processedEntries, zipComment);
|
|
239
261
|
}
|
|
@@ -71,7 +71,7 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
71
71
|
* Render loaded pivot cache definition (preserving original structure)
|
|
72
72
|
*/
|
|
73
73
|
renderLoaded(xmlStream, model) {
|
|
74
|
-
const { cacheFields, sourceRef, sourceSheet, recordCount } = model;
|
|
74
|
+
const { cacheFields, sourceRef, sourceSheet, sourceTableName, recordCount } = model;
|
|
75
75
|
xmlStream.openXml(XmlStream.StdDocAttributes);
|
|
76
76
|
xmlStream.openNode(this.tag, {
|
|
77
77
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
@@ -83,10 +83,13 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
83
83
|
recordCount: recordCount || cacheFields.length + 1
|
|
84
84
|
});
|
|
85
85
|
xmlStream.openNode("cacheSource", { type: "worksheet" });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// worksheetSource supports two reference styles:
|
|
87
|
+
// 1. name: references a named Table
|
|
88
|
+
// 2. ref + sheet: references a cell range on a worksheet
|
|
89
|
+
const worksheetSourceAttrs = sourceTableName
|
|
90
|
+
? { name: sourceTableName }
|
|
91
|
+
: { ref: sourceRef, sheet: sourceSheet };
|
|
92
|
+
xmlStream.leafNode("worksheetSource", worksheetSourceAttrs);
|
|
90
93
|
xmlStream.closeNode();
|
|
91
94
|
xmlStream.openNode("cacheFields", { count: cacheFields.length });
|
|
92
95
|
xmlStream.writeXml(cacheFields
|
|
@@ -124,6 +127,7 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
124
127
|
if (this.inCacheSource && this.model) {
|
|
125
128
|
this.model.sourceRef = attributes.ref;
|
|
126
129
|
this.model.sourceSheet = attributes.sheet;
|
|
130
|
+
this.model.sourceTableName = attributes.name;
|
|
127
131
|
}
|
|
128
132
|
break;
|
|
129
133
|
case "cacheFields":
|