easy-template-x 6.2.0 → 6.2.1
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 +1 -1
- package/dist/cjs/easy-template-x.cjs +167 -89
- package/dist/es/easy-template-x.mjs +167 -90
- package/dist/types/compilation/delimiterSearcher.d.ts +0 -1
- package/dist/types/compilation/templateContext.d.ts +5 -0
- package/dist/types/plugins/image/imagePlugin.d.ts +1 -0
- package/dist/types/xml/index.d.ts +4 -3
- package/dist/types/xml/xml.d.ts +3 -0
- package/dist/types/xml/xmlTreeIterator.d.ts +10 -0
- package/package.json +1 -1
- package/src/compilation/delimiterSearcher.ts +29 -56
- package/src/compilation/templateContext.ts +9 -0
- package/src/office/contentTypesFile.ts +18 -10
- package/src/plugins/image/imagePlugin.ts +50 -12
- package/src/templateHandler.ts +5 -1
- package/src/xml/index.ts +4 -3
- package/src/xml/xml.ts +22 -3
- package/src/xml/xmlTreeIterator.ts +67 -0
package/README.md
CHANGED
|
@@ -491,6 +491,71 @@ const XmlNodeType = Object.freeze({
|
|
|
491
491
|
const TEXT_NODE_NAME = '#text'; // see: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName
|
|
492
492
|
const COMMENT_NODE_NAME = '#comment';
|
|
493
493
|
|
|
494
|
+
class XmlDepthTracker {
|
|
495
|
+
depth = 0;
|
|
496
|
+
constructor(maxDepth) {
|
|
497
|
+
this.maxDepth = maxDepth;
|
|
498
|
+
}
|
|
499
|
+
increment() {
|
|
500
|
+
this.depth++;
|
|
501
|
+
if (this.depth > this.maxDepth) {
|
|
502
|
+
throw new MaxXmlDepthError(this.maxDepth);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
decrement() {
|
|
506
|
+
this.depth--;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
class XmlTreeIterator {
|
|
511
|
+
get node() {
|
|
512
|
+
return this._current;
|
|
513
|
+
}
|
|
514
|
+
constructor(initial, maxDepth) {
|
|
515
|
+
if (!initial) {
|
|
516
|
+
throw new InternalError("Initial node is required");
|
|
517
|
+
}
|
|
518
|
+
if (!maxDepth) {
|
|
519
|
+
throw new InternalError("Max depth is required");
|
|
520
|
+
}
|
|
521
|
+
this._current = initial;
|
|
522
|
+
this.depthTracker = new XmlDepthTracker(maxDepth);
|
|
523
|
+
}
|
|
524
|
+
next() {
|
|
525
|
+
if (!this._current) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
this._current = this.findNextNode(this._current);
|
|
529
|
+
return this._current;
|
|
530
|
+
}
|
|
531
|
+
setCurrent(node) {
|
|
532
|
+
this._current = node;
|
|
533
|
+
}
|
|
534
|
+
findNextNode(node) {
|
|
535
|
+
// Children
|
|
536
|
+
if (node.childNodes && node.childNodes.length) {
|
|
537
|
+
this.depthTracker.increment();
|
|
538
|
+
return node.childNodes[0];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Siblings
|
|
542
|
+
if (node.nextSibling) return node.nextSibling;
|
|
543
|
+
|
|
544
|
+
// Parent sibling
|
|
545
|
+
while (node.parentNode) {
|
|
546
|
+
if (node.parentNode.nextSibling) {
|
|
547
|
+
this.depthTracker.decrement();
|
|
548
|
+
return node.parentNode.nextSibling;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Go up
|
|
552
|
+
this.depthTracker.decrement();
|
|
553
|
+
node = node.parentNode;
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
494
559
|
class XmlUtils {
|
|
495
560
|
parser = new Parser();
|
|
496
561
|
create = new Create();
|
|
@@ -690,16 +755,19 @@ let Query$1 = class Query {
|
|
|
690
755
|
isTextNode(node) {
|
|
691
756
|
if (node.nodeType === XmlNodeType.Text || node.nodeName === TEXT_NODE_NAME) {
|
|
692
757
|
if (!(node.nodeType === XmlNodeType.Text && node.nodeName === TEXT_NODE_NAME)) {
|
|
693
|
-
throw new
|
|
758
|
+
throw new InternalError(`Invalid text node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
|
|
694
759
|
}
|
|
695
760
|
return true;
|
|
696
761
|
}
|
|
697
762
|
return false;
|
|
698
763
|
}
|
|
764
|
+
isGeneralNode(node) {
|
|
765
|
+
return node.nodeType === XmlNodeType.General;
|
|
766
|
+
}
|
|
699
767
|
isCommentNode(node) {
|
|
700
768
|
if (node.nodeType === XmlNodeType.Comment || node.nodeName === COMMENT_NODE_NAME) {
|
|
701
769
|
if (!(node.nodeType === XmlNodeType.Comment && node.nodeName === COMMENT_NODE_NAME)) {
|
|
702
|
-
throw new
|
|
770
|
+
throw new InternalError(`Invalid comment node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
|
|
703
771
|
}
|
|
704
772
|
return true;
|
|
705
773
|
}
|
|
@@ -778,6 +846,17 @@ let Query$1 = class Query {
|
|
|
778
846
|
range.push(lastNode);
|
|
779
847
|
return range;
|
|
780
848
|
}
|
|
849
|
+
descendants(node, maxDepth, predicate) {
|
|
850
|
+
const result = [];
|
|
851
|
+
const it = new XmlTreeIterator(node, maxDepth);
|
|
852
|
+
while (it.node) {
|
|
853
|
+
if (predicate(it.node)) {
|
|
854
|
+
result.push(it.node);
|
|
855
|
+
}
|
|
856
|
+
it.next();
|
|
857
|
+
}
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
781
860
|
};
|
|
782
861
|
let Modify$1 = class Modify {
|
|
783
862
|
/**
|
|
@@ -1030,22 +1109,6 @@ function recursiveRemoveEmptyTextNodes(node) {
|
|
|
1030
1109
|
}
|
|
1031
1110
|
const xml = new XmlUtils();
|
|
1032
1111
|
|
|
1033
|
-
class XmlDepthTracker {
|
|
1034
|
-
depth = 0;
|
|
1035
|
-
constructor(maxDepth) {
|
|
1036
|
-
this.maxDepth = maxDepth;
|
|
1037
|
-
}
|
|
1038
|
-
increment() {
|
|
1039
|
-
this.depth++;
|
|
1040
|
-
if (this.depth > this.maxDepth) {
|
|
1041
|
-
throw new MaxXmlDepthError(this.maxDepth);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
decrement() {
|
|
1045
|
-
this.depth--;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
1112
|
/**
|
|
1050
1113
|
* The types of relationships that can be created in a docx file.
|
|
1051
1114
|
* A non-comprehensive list.
|
|
@@ -1149,14 +1212,18 @@ class ContentTypesFile {
|
|
|
1149
1212
|
this.zip = zip;
|
|
1150
1213
|
}
|
|
1151
1214
|
async ensureContentType(mime) {
|
|
1152
|
-
//
|
|
1215
|
+
// Parse the content types file
|
|
1153
1216
|
await this.parseContentTypesFile();
|
|
1154
1217
|
|
|
1155
|
-
// already exists
|
|
1218
|
+
// Mime type already exists
|
|
1156
1219
|
if (this.contentTypes[mime]) return;
|
|
1157
1220
|
|
|
1158
|
-
//
|
|
1221
|
+
// Extension already exists
|
|
1222
|
+
// Unfortunately, this can happen in real life so we need to handle it.
|
|
1159
1223
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1224
|
+
if (Object.values(this.contentTypes).includes(extension)) return;
|
|
1225
|
+
|
|
1226
|
+
// Add new node
|
|
1160
1227
|
const typeNode = xml.create.generalNode('Default');
|
|
1161
1228
|
typeNode.attributes = {
|
|
1162
1229
|
"Extension": extension,
|
|
@@ -1164,9 +1231,9 @@ class ContentTypesFile {
|
|
|
1164
1231
|
};
|
|
1165
1232
|
this.root.childNodes.push(typeNode);
|
|
1166
1233
|
|
|
1167
|
-
//
|
|
1234
|
+
// Update state
|
|
1168
1235
|
this.addedNew = true;
|
|
1169
|
-
this.contentTypes[mime] =
|
|
1236
|
+
this.contentTypes[mime] = extension;
|
|
1170
1237
|
}
|
|
1171
1238
|
async count() {
|
|
1172
1239
|
await this.parseContentTypesFile();
|
|
@@ -1178,7 +1245,7 @@ class ContentTypesFile {
|
|
|
1178
1245
|
* Called automatically by the holding `Docx` before exporting.
|
|
1179
1246
|
*/
|
|
1180
1247
|
async save() {
|
|
1181
|
-
//
|
|
1248
|
+
// Not change - no need to save
|
|
1182
1249
|
if (!this.addedNew) return;
|
|
1183
1250
|
const xmlContent = xml.parser.serializeFile(this.root);
|
|
1184
1251
|
this.zip.setFile(ContentTypesFile.contentTypesFilePath, xmlContent);
|
|
@@ -1197,7 +1264,9 @@ class ContentTypesFile {
|
|
|
1197
1264
|
const genNode = node;
|
|
1198
1265
|
const contentTypeAttribute = genNode.attributes['ContentType'];
|
|
1199
1266
|
if (!contentTypeAttribute) continue;
|
|
1200
|
-
|
|
1267
|
+
const extensionAttribute = genNode.attributes['Extension'];
|
|
1268
|
+
if (!extensionAttribute) continue;
|
|
1269
|
+
this.contentTypes[contentTypeAttribute] = extensionAttribute;
|
|
1201
1270
|
}
|
|
1202
1271
|
}
|
|
1203
1272
|
}
|
|
@@ -2168,30 +2237,30 @@ class DelimiterSearcher {
|
|
|
2168
2237
|
|
|
2169
2238
|
const delimiters = [];
|
|
2170
2239
|
const match = new MatchState();
|
|
2171
|
-
const
|
|
2240
|
+
const it = new XmlTreeIterator(node, this.maxXmlDepth);
|
|
2172
2241
|
let lookForOpenDelimiter = true;
|
|
2173
|
-
while (node) {
|
|
2242
|
+
while (it.node) {
|
|
2174
2243
|
// Reset state on paragraph transition
|
|
2175
|
-
if (officeMarkup.query.isParagraphNode(node)) {
|
|
2244
|
+
if (officeMarkup.query.isParagraphNode(it.node)) {
|
|
2176
2245
|
match.reset();
|
|
2177
2246
|
}
|
|
2178
2247
|
|
|
2179
2248
|
// Skip irrelevant nodes
|
|
2180
|
-
if (!this.shouldSearchNode(
|
|
2181
|
-
|
|
2249
|
+
if (!this.shouldSearchNode(it)) {
|
|
2250
|
+
it.next();
|
|
2182
2251
|
continue;
|
|
2183
2252
|
}
|
|
2184
2253
|
|
|
2185
2254
|
// Search delimiters in text nodes
|
|
2186
|
-
match.openNodes.push(node);
|
|
2255
|
+
match.openNodes.push(it.node);
|
|
2187
2256
|
let textIndex = 0;
|
|
2188
|
-
while (textIndex < node.textContent.length) {
|
|
2257
|
+
while (textIndex < it.node.textContent.length) {
|
|
2189
2258
|
const delimiterPattern = lookForOpenDelimiter ? this.startDelimiter : this.endDelimiter;
|
|
2190
|
-
const char = node.textContent[textIndex];
|
|
2259
|
+
const char = it.node.textContent[textIndex];
|
|
2191
2260
|
|
|
2192
2261
|
// No match
|
|
2193
2262
|
if (char !== delimiterPattern[match.delimiterIndex]) {
|
|
2194
|
-
|
|
2263
|
+
textIndex = this.noMatch(it, textIndex, match);
|
|
2195
2264
|
textIndex++;
|
|
2196
2265
|
continue;
|
|
2197
2266
|
}
|
|
@@ -2209,14 +2278,14 @@ class DelimiterSearcher {
|
|
|
2209
2278
|
}
|
|
2210
2279
|
|
|
2211
2280
|
// Full delimiter match
|
|
2212
|
-
[
|
|
2281
|
+
[textIndex, lookForOpenDelimiter] = this.fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters);
|
|
2213
2282
|
textIndex++;
|
|
2214
2283
|
}
|
|
2215
|
-
|
|
2284
|
+
it.next();
|
|
2216
2285
|
}
|
|
2217
2286
|
return delimiters;
|
|
2218
2287
|
}
|
|
2219
|
-
noMatch(
|
|
2288
|
+
noMatch(it, textIndex, match) {
|
|
2220
2289
|
//
|
|
2221
2290
|
// Go back to first open node
|
|
2222
2291
|
//
|
|
@@ -2226,25 +2295,26 @@ class DelimiterSearcher {
|
|
|
2226
2295
|
// Delimiter is '{!' and template text contains the string '{{!'
|
|
2227
2296
|
//
|
|
2228
2297
|
if (match.firstMatchIndex !== -1) {
|
|
2229
|
-
node = first(match.openNodes);
|
|
2298
|
+
const node = first(match.openNodes);
|
|
2299
|
+
it.setCurrent(node);
|
|
2230
2300
|
textIndex = match.firstMatchIndex;
|
|
2231
2301
|
}
|
|
2232
2302
|
|
|
2233
2303
|
// Update state
|
|
2234
2304
|
match.reset();
|
|
2235
|
-
if (textIndex < node.textContent.length - 1) {
|
|
2236
|
-
match.openNodes.push(node);
|
|
2305
|
+
if (textIndex < it.node.textContent.length - 1) {
|
|
2306
|
+
match.openNodes.push(it.node);
|
|
2237
2307
|
}
|
|
2238
|
-
return
|
|
2308
|
+
return textIndex;
|
|
2239
2309
|
}
|
|
2240
|
-
fullMatch(
|
|
2310
|
+
fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters) {
|
|
2241
2311
|
// Move all delimiters characters to the same text node
|
|
2242
2312
|
if (match.openNodes.length > 1) {
|
|
2243
2313
|
const firstNode = first(match.openNodes);
|
|
2244
2314
|
const lastNode = last(match.openNodes);
|
|
2245
2315
|
officeMarkup.modify.joinTextNodesRange(firstNode, lastNode);
|
|
2246
|
-
textIndex += firstNode.textContent.length - node.textContent.length;
|
|
2247
|
-
|
|
2316
|
+
textIndex += firstNode.textContent.length - it.node.textContent.length;
|
|
2317
|
+
it.setCurrent(firstNode);
|
|
2248
2318
|
}
|
|
2249
2319
|
|
|
2250
2320
|
// Store delimiter
|
|
@@ -2254,41 +2324,18 @@ class DelimiterSearcher {
|
|
|
2254
2324
|
// Update state
|
|
2255
2325
|
lookForOpenDelimiter = !lookForOpenDelimiter;
|
|
2256
2326
|
match.reset();
|
|
2257
|
-
if (textIndex < node.textContent.length - 1) {
|
|
2258
|
-
match.openNodes.push(node);
|
|
2327
|
+
if (textIndex < it.node.textContent.length - 1) {
|
|
2328
|
+
match.openNodes.push(it.node);
|
|
2259
2329
|
}
|
|
2260
|
-
return [
|
|
2330
|
+
return [textIndex, lookForOpenDelimiter];
|
|
2261
2331
|
}
|
|
2262
|
-
shouldSearchNode(
|
|
2263
|
-
if (!xml.query.isTextNode(node)) return false;
|
|
2264
|
-
if (!node.textContent) return false;
|
|
2265
|
-
if (!node.parentNode) return false;
|
|
2266
|
-
if (!officeMarkup.query.isTextNode(node.parentNode)) return false;
|
|
2332
|
+
shouldSearchNode(it) {
|
|
2333
|
+
if (!xml.query.isTextNode(it.node)) return false;
|
|
2334
|
+
if (!it.node.textContent) return false;
|
|
2335
|
+
if (!it.node.parentNode) return false;
|
|
2336
|
+
if (!officeMarkup.query.isTextNode(it.node.parentNode)) return false;
|
|
2267
2337
|
return true;
|
|
2268
2338
|
}
|
|
2269
|
-
findNextNode(node, depth) {
|
|
2270
|
-
// Children
|
|
2271
|
-
if (node.childNodes && node.childNodes.length) {
|
|
2272
|
-
depth.increment();
|
|
2273
|
-
return node.childNodes[0];
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
// Siblings
|
|
2277
|
-
if (node.nextSibling) return node.nextSibling;
|
|
2278
|
-
|
|
2279
|
-
// Parent sibling
|
|
2280
|
-
while (node.parentNode) {
|
|
2281
|
-
if (node.parentNode.nextSibling) {
|
|
2282
|
-
depth.decrement();
|
|
2283
|
-
return node.parentNode.nextSibling;
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
// Go up
|
|
2287
|
-
depth.decrement();
|
|
2288
|
-
node = node.parentNode;
|
|
2289
|
-
}
|
|
2290
|
-
return null;
|
|
2291
|
-
}
|
|
2292
2339
|
createDelimiterMark(match, isOpenDelimiter) {
|
|
2293
2340
|
return {
|
|
2294
2341
|
index: match.firstMatchIndex,
|
|
@@ -2529,17 +2576,6 @@ class TemplatePlugin {
|
|
|
2529
2576
|
}
|
|
2530
2577
|
}
|
|
2531
2578
|
|
|
2532
|
-
/**
|
|
2533
|
-
* Apparently it is not that important for the ID to be unique...
|
|
2534
|
-
* Word displays two images correctly even if they both have the same ID.
|
|
2535
|
-
* Further more, Word will assign each a unique ID upon saving (it assigns
|
|
2536
|
-
* consecutive integers starting with 1).
|
|
2537
|
-
*
|
|
2538
|
-
* Note: The same principal applies to image names.
|
|
2539
|
-
*
|
|
2540
|
-
* Tested in Word v1908
|
|
2541
|
-
*/
|
|
2542
|
-
let nextImageId = 1;
|
|
2543
2579
|
class ImagePlugin extends TemplatePlugin {
|
|
2544
2580
|
contentType = 'image';
|
|
2545
2581
|
async simpleTagReplacements(tag, data, context) {
|
|
@@ -2556,12 +2592,49 @@ class ImagePlugin extends TemplatePlugin {
|
|
|
2556
2592
|
await context.docx.contentTypes.ensureContentType(content.format);
|
|
2557
2593
|
|
|
2558
2594
|
// Create the xml markup
|
|
2559
|
-
const imageId =
|
|
2595
|
+
const imageId = await this.getNextImageId(context);
|
|
2560
2596
|
const imageXml = this.createMarkup(imageId, relId, content);
|
|
2561
2597
|
const wordTextNode = officeMarkup.query.containingTextNode(tag.xmlTextNode);
|
|
2562
2598
|
xml.modify.insertAfter(imageXml, wordTextNode);
|
|
2563
2599
|
officeMarkup.modify.removeTag(tag.xmlTextNode);
|
|
2564
2600
|
}
|
|
2601
|
+
async getNextImageId(context) {
|
|
2602
|
+
// Init plugin context.
|
|
2603
|
+
if (!context.pluginContext[this.contentType]) {
|
|
2604
|
+
context.pluginContext[this.contentType] = {};
|
|
2605
|
+
}
|
|
2606
|
+
if (!context.pluginContext[this.contentType]) {
|
|
2607
|
+
context.pluginContext[this.contentType] = {};
|
|
2608
|
+
}
|
|
2609
|
+
const pluginContext = context.pluginContext[this.contentType];
|
|
2610
|
+
if (!pluginContext.lastDrawingObjectId) {
|
|
2611
|
+
pluginContext.lastDrawingObjectId = {};
|
|
2612
|
+
}
|
|
2613
|
+
const lastIdMap = pluginContext.lastDrawingObjectId;
|
|
2614
|
+
|
|
2615
|
+
// Get next image ID if already initialized.
|
|
2616
|
+
if (lastIdMap[context.currentPart.path]) {
|
|
2617
|
+
lastIdMap[context.currentPart.path]++;
|
|
2618
|
+
return lastIdMap[context.currentPart.path];
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// Init next image ID.
|
|
2622
|
+
const partRoot = await context.currentPart.xmlRoot();
|
|
2623
|
+
const maxDepth = context.options.maxXmlDepth;
|
|
2624
|
+
|
|
2625
|
+
// Get all existing doc props IDs
|
|
2626
|
+
// (docPr stands for "Drawing Object Non-Visual Properties", which isn't
|
|
2627
|
+
// exactly a good acronym but that's how it's called nevertheless)
|
|
2628
|
+
const docProps = xml.query.descendants(partRoot, maxDepth, node => {
|
|
2629
|
+
return xml.query.isGeneralNode(node) && node.nodeName === 'wp:docPr';
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
// Start counting from the current max
|
|
2633
|
+
const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
|
|
2634
|
+
const maxId = Math.max(...ids, 0);
|
|
2635
|
+
lastIdMap[context.currentPart.path] = maxId + 1;
|
|
2636
|
+
return lastIdMap[context.currentPart.path];
|
|
2637
|
+
}
|
|
2565
2638
|
createMarkup(imageId, relId, content) {
|
|
2566
2639
|
// http://officeopenxml.com/drwPicInline.php
|
|
2567
2640
|
|
|
@@ -4618,7 +4691,7 @@ class TemplateHandler {
|
|
|
4618
4691
|
/**
|
|
4619
4692
|
* Version number of the `easy-template-x` library.
|
|
4620
4693
|
*/
|
|
4621
|
-
version = "6.2.
|
|
4694
|
+
version = "6.2.1" ;
|
|
4622
4695
|
constructor(options) {
|
|
4623
4696
|
this.options = new TemplateHandlerOptions(options);
|
|
4624
4697
|
|
|
@@ -4666,7 +4739,11 @@ class TemplateHandler {
|
|
|
4666
4739
|
scopeData.scopeDataResolver = this.options.scopeDataResolver;
|
|
4667
4740
|
const context = {
|
|
4668
4741
|
docx,
|
|
4669
|
-
currentPart: null
|
|
4742
|
+
currentPart: null,
|
|
4743
|
+
pluginContext: {},
|
|
4744
|
+
options: {
|
|
4745
|
+
maxXmlDepth: this.options.maxXmlDepth
|
|
4746
|
+
}
|
|
4670
4747
|
};
|
|
4671
4748
|
const contentParts = await docx.getContentParts();
|
|
4672
4749
|
for (const part of contentParts) {
|
|
@@ -4806,6 +4883,7 @@ exports.UnsupportedFileTypeError = UnsupportedFileTypeError;
|
|
|
4806
4883
|
exports.Xlsx = Xlsx;
|
|
4807
4884
|
exports.XmlDepthTracker = XmlDepthTracker;
|
|
4808
4885
|
exports.XmlNodeType = XmlNodeType;
|
|
4886
|
+
exports.XmlTreeIterator = XmlTreeIterator;
|
|
4809
4887
|
exports.XmlUtils = XmlUtils;
|
|
4810
4888
|
exports.Zip = Zip;
|
|
4811
4889
|
exports.ZipObject = ZipObject;
|