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 CHANGED
@@ -14,7 +14,7 @@ Generate docx documents from templates, in Node or in the browser.
14
14
  >
15
15
  > ✓ PDF support
16
16
  > ✓ REST API integration
17
- > ✓ Zapier integration
17
+ > ✓ Zapier and Make integration
18
18
  > ✓ Live preview functionality
19
19
  > ✓ Easy-to-use UI
20
20
  >
@@ -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 Error(`Invalid text node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
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 Error(`Invalid comment node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
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
- // parse the content types file
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
- // add new
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
- // update state
1234
+ // Update state
1168
1235
  this.addedNew = true;
1169
- this.contentTypes[mime] = true;
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
- // not change - no need to save
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
- this.contentTypes[contentTypeAttribute] = true;
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 depth = new XmlDepthTracker(this.maxXmlDepth);
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(node)) {
2181
- node = this.findNextNode(node, depth);
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
- [node, textIndex] = this.noMatch(node, textIndex, match);
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
- [node, textIndex, lookForOpenDelimiter] = this.fullMatch(node, textIndex, lookForOpenDelimiter, match, delimiters);
2281
+ [textIndex, lookForOpenDelimiter] = this.fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters);
2213
2282
  textIndex++;
2214
2283
  }
2215
- node = this.findNextNode(node, depth);
2284
+ it.next();
2216
2285
  }
2217
2286
  return delimiters;
2218
2287
  }
2219
- noMatch(node, textIndex, match) {
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 [node, textIndex];
2308
+ return textIndex;
2239
2309
  }
2240
- fullMatch(node, textIndex, lookForOpenDelimiter, match, delimiters) {
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
- node = firstNode;
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 [node, textIndex, lookForOpenDelimiter];
2330
+ return [textIndex, lookForOpenDelimiter];
2261
2331
  }
2262
- shouldSearchNode(node) {
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 = nextImageId++;
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.0" ;
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;