easy-template-x 6.2.0 → 6.2.2

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.
@@ -402,6 +402,10 @@ function stringValue(val) {
402
402
  function normalizeDoubleQuotes(text) {
403
403
  return text.replace(nonStandardDoubleQuotesRegex, standardDoubleQuotes);
404
404
  }
405
+ function countOccurrences(text, substring) {
406
+ // https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
407
+ return (text.match(new RegExp(substring, 'g')) || []).length;
408
+ }
405
409
 
406
410
  class JsZipHelper {
407
411
  static toJsZipOutputType(binaryOrType) {
@@ -489,6 +493,71 @@ const XmlNodeType = Object.freeze({
489
493
  const TEXT_NODE_NAME = '#text'; // see: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName
490
494
  const COMMENT_NODE_NAME = '#comment';
491
495
 
496
+ class XmlDepthTracker {
497
+ depth = 0;
498
+ constructor(maxDepth) {
499
+ this.maxDepth = maxDepth;
500
+ }
501
+ increment() {
502
+ this.depth++;
503
+ if (this.depth > this.maxDepth) {
504
+ throw new MaxXmlDepthError(this.maxDepth);
505
+ }
506
+ }
507
+ decrement() {
508
+ this.depth--;
509
+ }
510
+ }
511
+
512
+ class XmlTreeIterator {
513
+ get node() {
514
+ return this._current;
515
+ }
516
+ constructor(initial, maxDepth) {
517
+ if (!initial) {
518
+ throw new InternalError("Initial node is required");
519
+ }
520
+ if (!maxDepth) {
521
+ throw new InternalError("Max depth is required");
522
+ }
523
+ this._current = initial;
524
+ this.depthTracker = new XmlDepthTracker(maxDepth);
525
+ }
526
+ next() {
527
+ if (!this._current) {
528
+ return null;
529
+ }
530
+ this._current = this.findNextNode(this._current);
531
+ return this._current;
532
+ }
533
+ setCurrent(node) {
534
+ this._current = node;
535
+ }
536
+ findNextNode(node) {
537
+ // Children
538
+ if (node.childNodes && node.childNodes.length) {
539
+ this.depthTracker.increment();
540
+ return node.childNodes[0];
541
+ }
542
+
543
+ // Siblings
544
+ if (node.nextSibling) return node.nextSibling;
545
+
546
+ // Parent sibling
547
+ while (node.parentNode) {
548
+ if (node.parentNode.nextSibling) {
549
+ this.depthTracker.decrement();
550
+ return node.parentNode.nextSibling;
551
+ }
552
+
553
+ // Go up
554
+ this.depthTracker.decrement();
555
+ node = node.parentNode;
556
+ }
557
+ return null;
558
+ }
559
+ }
560
+
492
561
  class XmlUtils {
493
562
  parser = new Parser();
494
563
  create = new Create();
@@ -688,16 +757,19 @@ let Query$1 = class Query {
688
757
  isTextNode(node) {
689
758
  if (node.nodeType === XmlNodeType.Text || node.nodeName === TEXT_NODE_NAME) {
690
759
  if (!(node.nodeType === XmlNodeType.Text && node.nodeName === TEXT_NODE_NAME)) {
691
- throw new Error(`Invalid text node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
760
+ throw new InternalError(`Invalid text node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
692
761
  }
693
762
  return true;
694
763
  }
695
764
  return false;
696
765
  }
766
+ isGeneralNode(node) {
767
+ return node.nodeType === XmlNodeType.General;
768
+ }
697
769
  isCommentNode(node) {
698
770
  if (node.nodeType === XmlNodeType.Comment || node.nodeName === COMMENT_NODE_NAME) {
699
771
  if (!(node.nodeType === XmlNodeType.Comment && node.nodeName === COMMENT_NODE_NAME)) {
700
- throw new Error(`Invalid comment node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
772
+ throw new InternalError(`Invalid comment node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
701
773
  }
702
774
  return true;
703
775
  }
@@ -776,6 +848,17 @@ let Query$1 = class Query {
776
848
  range.push(lastNode);
777
849
  return range;
778
850
  }
851
+ descendants(node, maxDepth, predicate) {
852
+ const result = [];
853
+ const it = new XmlTreeIterator(node, maxDepth);
854
+ while (it.node) {
855
+ if (predicate(it.node)) {
856
+ result.push(it.node);
857
+ }
858
+ it.next();
859
+ }
860
+ return result;
861
+ }
779
862
  };
780
863
  let Modify$1 = class Modify {
781
864
  /**
@@ -1028,22 +1111,6 @@ function recursiveRemoveEmptyTextNodes(node) {
1028
1111
  }
1029
1112
  const xml = new XmlUtils();
1030
1113
 
1031
- class XmlDepthTracker {
1032
- depth = 0;
1033
- constructor(maxDepth) {
1034
- this.maxDepth = maxDepth;
1035
- }
1036
- increment() {
1037
- this.depth++;
1038
- if (this.depth > this.maxDepth) {
1039
- throw new MaxXmlDepthError(this.maxDepth);
1040
- }
1041
- }
1042
- decrement() {
1043
- this.depth--;
1044
- }
1045
- }
1046
-
1047
1114
  /**
1048
1115
  * The types of relationships that can be created in a docx file.
1049
1116
  * A non-comprehensive list.
@@ -1147,14 +1214,17 @@ class ContentTypesFile {
1147
1214
  this.zip = zip;
1148
1215
  }
1149
1216
  async ensureContentType(mime) {
1150
- // parse the content types file
1217
+ // Parse the content types file
1151
1218
  await this.parseContentTypesFile();
1152
1219
 
1153
- // already exists
1154
- if (this.contentTypes[mime]) return;
1155
-
1156
- // add new
1220
+ // Extension already exists
1221
+ //
1222
+ // Multiple extensions may map to the same mime type, but a single
1223
+ // extension must only map to one mime type.
1157
1224
  const extension = MimeTypeHelper.getDefaultExtension(mime);
1225
+ if (this.contentTypes[extension]) return;
1226
+
1227
+ // Add new node
1158
1228
  const typeNode = xml.create.generalNode('Default');
1159
1229
  typeNode.attributes = {
1160
1230
  "Extension": extension,
@@ -1162,13 +1232,13 @@ class ContentTypesFile {
1162
1232
  };
1163
1233
  this.root.childNodes.push(typeNode);
1164
1234
 
1165
- // update state
1235
+ // Update state
1166
1236
  this.addedNew = true;
1167
- this.contentTypes[mime] = true;
1237
+ this.contentTypes[extension] = mime;
1168
1238
  }
1169
- async count() {
1239
+ async xmlString() {
1170
1240
  await this.parseContentTypesFile();
1171
- return this.root.childNodes.filter(node => !xml.query.isTextNode(node)).length;
1241
+ return xml.parser.serializeFile(this.root);
1172
1242
  }
1173
1243
 
1174
1244
  /**
@@ -1176,7 +1246,7 @@ class ContentTypesFile {
1176
1246
  * Called automatically by the holding `Docx` before exporting.
1177
1247
  */
1178
1248
  async save() {
1179
- // not change - no need to save
1249
+ // Not change - no need to save
1180
1250
  if (!this.addedNew) return;
1181
1251
  const xmlContent = xml.parser.serializeFile(this.root);
1182
1252
  this.zip.setFile(ContentTypesFile.contentTypesFilePath, xmlContent);
@@ -1195,7 +1265,9 @@ class ContentTypesFile {
1195
1265
  const genNode = node;
1196
1266
  const contentTypeAttribute = genNode.attributes['ContentType'];
1197
1267
  if (!contentTypeAttribute) continue;
1198
- this.contentTypes[contentTypeAttribute] = true;
1268
+ const extensionAttribute = genNode.attributes['Extension'];
1269
+ if (!extensionAttribute) continue;
1270
+ this.contentTypes[extensionAttribute] = contentTypeAttribute;
1199
1271
  }
1200
1272
  }
1201
1273
  }
@@ -1215,39 +1287,38 @@ class MediaFiles {
1215
1287
  * Returns the media file path.
1216
1288
  */
1217
1289
  async add(mediaFile, mime) {
1218
- // check if already added
1290
+ // Check if already added
1219
1291
  if (this.files.has(mediaFile)) return this.files.get(mediaFile);
1220
1292
 
1221
- // hash existing media files
1293
+ // Hash existing media files
1222
1294
  await this.hashMediaFiles();
1223
1295
 
1224
- // hash the new file
1296
+ // Hash the new file
1225
1297
  // Note: Even though hashing the base64 string may seem inefficient
1226
1298
  // (requires extra step in some cases) in practice it is significantly
1227
1299
  // faster than hashing a 'binarystring'.
1228
1300
  const base64 = await Binary.toBase64(mediaFile);
1229
1301
  const hash = sha1(base64);
1230
1302
 
1231
- // check if file already exists
1232
- // note: this can be optimized by keeping both mapping by filename as well as by hash
1303
+ // Check if file already exists
1304
+ // Note: this can be optimized by keeping both mapping by filename as well as by hash
1233
1305
  let path = Object.keys(this.hashes).find(p => this.hashes[p] === hash);
1234
1306
  if (path) return path;
1235
1307
 
1236
- // generate unique media file name
1308
+ // Generate unique media file name
1309
+ const baseFilename = this.baseFilename(mime);
1237
1310
  const extension = MimeTypeHelper.getDefaultExtension(mime);
1238
1311
  do {
1239
1312
  this.nextFileId++;
1240
- path = `${MediaFiles.mediaDir}/media${this.nextFileId}.${extension}`;
1313
+ path = `${MediaFiles.mediaDir}/${baseFilename}${this.nextFileId}.${extension}`;
1241
1314
  } while (this.hashes[path]);
1242
1315
 
1243
- // add media to zip
1244
- await this.zip.setFile(path, mediaFile);
1316
+ // Add media to zip
1317
+ this.zip.setFile(path, mediaFile);
1245
1318
 
1246
- // add media to our lookups
1319
+ // Add media to our lookups
1247
1320
  this.hashes[path] = hash;
1248
1321
  this.files.set(mediaFile, path);
1249
-
1250
- // return
1251
1322
  return path;
1252
1323
  }
1253
1324
  async count() {
@@ -1263,9 +1334,15 @@ class MediaFiles {
1263
1334
  if (!filename) continue;
1264
1335
  const fileData = await this.zip.getFile(path).getContentBase64();
1265
1336
  const fileHash = sha1(fileData);
1266
- this.hashes[filename] = fileHash;
1337
+ this.hashes[path] = fileHash;
1267
1338
  }
1268
1339
  }
1340
+ baseFilename(mime) {
1341
+ // Naive heuristic.
1342
+ // May need to be modified if we're going to support more mime types.
1343
+ const parts = mime.split('/');
1344
+ return parts[0];
1345
+ }
1269
1346
  }
1270
1347
 
1271
1348
  /**
@@ -2166,30 +2243,30 @@ class DelimiterSearcher {
2166
2243
 
2167
2244
  const delimiters = [];
2168
2245
  const match = new MatchState();
2169
- const depth = new XmlDepthTracker(this.maxXmlDepth);
2246
+ const it = new XmlTreeIterator(node, this.maxXmlDepth);
2170
2247
  let lookForOpenDelimiter = true;
2171
- while (node) {
2248
+ while (it.node) {
2172
2249
  // Reset state on paragraph transition
2173
- if (officeMarkup.query.isParagraphNode(node)) {
2250
+ if (officeMarkup.query.isParagraphNode(it.node)) {
2174
2251
  match.reset();
2175
2252
  }
2176
2253
 
2177
2254
  // Skip irrelevant nodes
2178
- if (!this.shouldSearchNode(node)) {
2179
- node = this.findNextNode(node, depth);
2255
+ if (!this.shouldSearchNode(it)) {
2256
+ it.next();
2180
2257
  continue;
2181
2258
  }
2182
2259
 
2183
2260
  // Search delimiters in text nodes
2184
- match.openNodes.push(node);
2261
+ match.openNodes.push(it.node);
2185
2262
  let textIndex = 0;
2186
- while (textIndex < node.textContent.length) {
2263
+ while (textIndex < it.node.textContent.length) {
2187
2264
  const delimiterPattern = lookForOpenDelimiter ? this.startDelimiter : this.endDelimiter;
2188
- const char = node.textContent[textIndex];
2265
+ const char = it.node.textContent[textIndex];
2189
2266
 
2190
2267
  // No match
2191
2268
  if (char !== delimiterPattern[match.delimiterIndex]) {
2192
- [node, textIndex] = this.noMatch(node, textIndex, match);
2269
+ textIndex = this.noMatch(it, textIndex, match);
2193
2270
  textIndex++;
2194
2271
  continue;
2195
2272
  }
@@ -2207,14 +2284,14 @@ class DelimiterSearcher {
2207
2284
  }
2208
2285
 
2209
2286
  // Full delimiter match
2210
- [node, textIndex, lookForOpenDelimiter] = this.fullMatch(node, textIndex, lookForOpenDelimiter, match, delimiters);
2287
+ [textIndex, lookForOpenDelimiter] = this.fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters);
2211
2288
  textIndex++;
2212
2289
  }
2213
- node = this.findNextNode(node, depth);
2290
+ it.next();
2214
2291
  }
2215
2292
  return delimiters;
2216
2293
  }
2217
- noMatch(node, textIndex, match) {
2294
+ noMatch(it, textIndex, match) {
2218
2295
  //
2219
2296
  // Go back to first open node
2220
2297
  //
@@ -2224,25 +2301,26 @@ class DelimiterSearcher {
2224
2301
  // Delimiter is '{!' and template text contains the string '{{!'
2225
2302
  //
2226
2303
  if (match.firstMatchIndex !== -1) {
2227
- node = first(match.openNodes);
2304
+ const node = first(match.openNodes);
2305
+ it.setCurrent(node);
2228
2306
  textIndex = match.firstMatchIndex;
2229
2307
  }
2230
2308
 
2231
2309
  // Update state
2232
2310
  match.reset();
2233
- if (textIndex < node.textContent.length - 1) {
2234
- match.openNodes.push(node);
2311
+ if (textIndex < it.node.textContent.length - 1) {
2312
+ match.openNodes.push(it.node);
2235
2313
  }
2236
- return [node, textIndex];
2314
+ return textIndex;
2237
2315
  }
2238
- fullMatch(node, textIndex, lookForOpenDelimiter, match, delimiters) {
2316
+ fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters) {
2239
2317
  // Move all delimiters characters to the same text node
2240
2318
  if (match.openNodes.length > 1) {
2241
2319
  const firstNode = first(match.openNodes);
2242
2320
  const lastNode = last(match.openNodes);
2243
2321
  officeMarkup.modify.joinTextNodesRange(firstNode, lastNode);
2244
- textIndex += firstNode.textContent.length - node.textContent.length;
2245
- node = firstNode;
2322
+ textIndex += firstNode.textContent.length - it.node.textContent.length;
2323
+ it.setCurrent(firstNode);
2246
2324
  }
2247
2325
 
2248
2326
  // Store delimiter
@@ -2252,41 +2330,18 @@ class DelimiterSearcher {
2252
2330
  // Update state
2253
2331
  lookForOpenDelimiter = !lookForOpenDelimiter;
2254
2332
  match.reset();
2255
- if (textIndex < node.textContent.length - 1) {
2256
- match.openNodes.push(node);
2333
+ if (textIndex < it.node.textContent.length - 1) {
2334
+ match.openNodes.push(it.node);
2257
2335
  }
2258
- return [node, textIndex, lookForOpenDelimiter];
2336
+ return [textIndex, lookForOpenDelimiter];
2259
2337
  }
2260
- shouldSearchNode(node) {
2261
- if (!xml.query.isTextNode(node)) return false;
2262
- if (!node.textContent) return false;
2263
- if (!node.parentNode) return false;
2264
- if (!officeMarkup.query.isTextNode(node.parentNode)) return false;
2338
+ shouldSearchNode(it) {
2339
+ if (!xml.query.isTextNode(it.node)) return false;
2340
+ if (!it.node.textContent) return false;
2341
+ if (!it.node.parentNode) return false;
2342
+ if (!officeMarkup.query.isTextNode(it.node.parentNode)) return false;
2265
2343
  return true;
2266
2344
  }
2267
- findNextNode(node, depth) {
2268
- // Children
2269
- if (node.childNodes && node.childNodes.length) {
2270
- depth.increment();
2271
- return node.childNodes[0];
2272
- }
2273
-
2274
- // Siblings
2275
- if (node.nextSibling) return node.nextSibling;
2276
-
2277
- // Parent sibling
2278
- while (node.parentNode) {
2279
- if (node.parentNode.nextSibling) {
2280
- depth.decrement();
2281
- return node.parentNode.nextSibling;
2282
- }
2283
-
2284
- // Go up
2285
- depth.decrement();
2286
- node = node.parentNode;
2287
- }
2288
- return null;
2289
- }
2290
2345
  createDelimiterMark(match, isOpenDelimiter) {
2291
2346
  return {
2292
2347
  index: match.firstMatchIndex,
@@ -2527,17 +2582,6 @@ class TemplatePlugin {
2527
2582
  }
2528
2583
  }
2529
2584
 
2530
- /**
2531
- * Apparently it is not that important for the ID to be unique...
2532
- * Word displays two images correctly even if they both have the same ID.
2533
- * Further more, Word will assign each a unique ID upon saving (it assigns
2534
- * consecutive integers starting with 1).
2535
- *
2536
- * Note: The same principal applies to image names.
2537
- *
2538
- * Tested in Word v1908
2539
- */
2540
- let nextImageId = 1;
2541
2585
  class ImagePlugin extends TemplatePlugin {
2542
2586
  contentType = 'image';
2543
2587
  async simpleTagReplacements(tag, data, context) {
@@ -2554,12 +2598,47 @@ class ImagePlugin extends TemplatePlugin {
2554
2598
  await context.docx.contentTypes.ensureContentType(content.format);
2555
2599
 
2556
2600
  // Create the xml markup
2557
- const imageId = nextImageId++;
2601
+ const imageId = await this.getNextImageId(context);
2558
2602
  const imageXml = this.createMarkup(imageId, relId, content);
2559
2603
  const wordTextNode = officeMarkup.query.containingTextNode(tag.xmlTextNode);
2560
2604
  xml.modify.insertAfter(imageXml, wordTextNode);
2561
2605
  officeMarkup.modify.removeTag(tag.xmlTextNode);
2562
2606
  }
2607
+ async getNextImageId(context) {
2608
+ // Init plugin context.
2609
+ if (!context.pluginContext[this.contentType]) {
2610
+ context.pluginContext[this.contentType] = {};
2611
+ }
2612
+ const pluginContext = context.pluginContext[this.contentType];
2613
+ if (!pluginContext.lastDrawingObjectId) {
2614
+ pluginContext.lastDrawingObjectId = {};
2615
+ }
2616
+ const lastIdMap = pluginContext.lastDrawingObjectId;
2617
+ const lastIdKey = context.currentPart.path;
2618
+
2619
+ // Get next image ID if already initialized.
2620
+ if (lastIdMap[lastIdKey]) {
2621
+ lastIdMap[lastIdKey]++;
2622
+ return lastIdMap[lastIdKey];
2623
+ }
2624
+
2625
+ // Init next image ID.
2626
+ const partRoot = await context.currentPart.xmlRoot();
2627
+ const maxDepth = context.options.maxXmlDepth;
2628
+
2629
+ // Get all existing doc props IDs
2630
+ // (docPr stands for "Drawing Object Non-Visual Properties", which isn't
2631
+ // exactly a good acronym but that's how it's called nevertheless)
2632
+ const docProps = xml.query.descendants(partRoot, maxDepth, node => {
2633
+ return xml.query.isGeneralNode(node) && node.nodeName === 'wp:docPr';
2634
+ });
2635
+
2636
+ // Start counting from the current max
2637
+ const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
2638
+ const maxId = Math.max(...ids, 0);
2639
+ lastIdMap[lastIdKey] = maxId + 1;
2640
+ return lastIdMap[lastIdKey];
2641
+ }
2563
2642
  createMarkup(imageId, relId, content) {
2564
2643
  // http://officeopenxml.com/drwPicInline.php
2565
2644
 
@@ -4616,7 +4695,7 @@ class TemplateHandler {
4616
4695
  /**
4617
4696
  * Version number of the `easy-template-x` library.
4618
4697
  */
4619
- version = "6.2.0" ;
4698
+ version = "6.2.2" ;
4620
4699
  constructor(options) {
4621
4700
  this.options = new TemplateHandlerOptions(options);
4622
4701
 
@@ -4664,7 +4743,11 @@ class TemplateHandler {
4664
4743
  scopeData.scopeDataResolver = this.options.scopeDataResolver;
4665
4744
  const context = {
4666
4745
  docx,
4667
- currentPart: null
4746
+ currentPart: null,
4747
+ pluginContext: {},
4748
+ options: {
4749
+ maxXmlDepth: this.options.maxXmlDepth
4750
+ }
4668
4751
  };
4669
4752
  const contentParts = await docx.getContentParts();
4670
4753
  for (const part of contentParts) {
@@ -4754,4 +4837,4 @@ class TemplateHandler {
4754
4837
  }
4755
4838
  }
4756
4839
 
4757
- export { Base64, Binary, COMMENT_NODE_NAME, DelimiterSearcher, Delimiters, Docx, ImagePlugin, InternalArgumentMissingError, InternalError, LOOP_CONTENT_TYPE, LinkPlugin, LoopPlugin, MalformedFileError, MaxXmlDepthError, MimeType, MimeTypeHelper, MissingCloseDelimiterError, MissingStartDelimiterError, OfficeMarkup, OmlAttribute, OmlNode, OpenXmlPart, Path, PluginContent, RawXmlPlugin, Regex, RelType, Relationship, ScopeData, TEXT_CONTENT_TYPE, TEXT_NODE_NAME, TagDisposition, TagOptionsParseError, TagParser, TemplateCompiler, TemplateDataError, TemplateExtension, TemplateHandler, TemplateHandlerOptions, TemplatePlugin, TemplateSyntaxError, TextPlugin, UnclosedTagError, UnidentifiedFileTypeError, UnknownContentTypeError, UnopenedTagError, UnsupportedFileTypeError, Xlsx, XmlDepthTracker, XmlNodeType, XmlUtils, Zip, ZipObject, createDefaultPlugins, first, inheritsFrom, isNumber, isPromiseLike, last, normalizeDoubleQuotes, officeMarkup, pushMany, sha1, stringValue, toDictionary, xml };
4840
+ export { Base64, Binary, COMMENT_NODE_NAME, ChartPlugin, DelimiterSearcher, Delimiters, Docx, ImagePlugin, InternalArgumentMissingError, InternalError, LOOP_CONTENT_TYPE, LinkPlugin, LoopPlugin, MalformedFileError, MaxXmlDepthError, MimeType, MimeTypeHelper, MissingCloseDelimiterError, MissingStartDelimiterError, OfficeMarkup, OmlAttribute, OmlNode, OpenXmlPart, Path, PluginContent, RawXmlPlugin, Regex, RelType, Relationship, ScopeData, TEXT_CONTENT_TYPE, TEXT_NODE_NAME, TagDisposition, TagOptionsParseError, TagParser, TemplateCompiler, TemplateDataError, TemplateExtension, TemplateHandler, TemplateHandlerOptions, TemplatePlugin, TemplateSyntaxError, TextPlugin, UnclosedTagError, UnidentifiedFileTypeError, UnknownContentTypeError, UnopenedTagError, UnsupportedFileTypeError, Xlsx, XmlDepthTracker, XmlNodeType, XmlTreeIterator, XmlUtils, Zip, ZipObject, countOccurrences, createDefaultPlugins, first, inheritsFrom, isNumber, isPromiseLike, last, normalizeDoubleQuotes, officeMarkup, pushMany, sha1, stringValue, toDictionary, xml };
@@ -8,6 +8,5 @@ export declare class DelimiterSearcher {
8
8
  private noMatch;
9
9
  private fullMatch;
10
10
  private shouldSearchNode;
11
- private findNextNode;
12
11
  private createDelimiterMark;
13
12
  }
@@ -2,4 +2,9 @@ import { Docx, OpenXmlPart } from '../office';
2
2
  export interface TemplateContext {
3
3
  docx: Docx;
4
4
  currentPart: OpenXmlPart;
5
+ pluginContext: Record<string, any>;
6
+ options: TemplateOptions;
7
+ }
8
+ export interface TemplateOptions {
9
+ maxXmlDepth: number;
5
10
  }
@@ -8,7 +8,7 @@ export declare class ContentTypesFile {
8
8
  private readonly zip;
9
9
  constructor(zip: Zip);
10
10
  ensureContentType(mime: MimeType): Promise<void>;
11
- count(): Promise<number>;
11
+ xmlString(): Promise<string>;
12
12
  save(): Promise<void>;
13
13
  private parseContentTypesFile;
14
14
  }
@@ -11,4 +11,5 @@ export declare class MediaFiles {
11
11
  add(mediaFile: Binary, mime: MimeType): Promise<string>;
12
12
  count(): Promise<number>;
13
13
  private hashMediaFiles;
14
+ private baseFilename;
14
15
  }
@@ -3,6 +3,7 @@ import { TemplatePlugin } from "src/plugins/templatePlugin";
3
3
  export declare class ImagePlugin extends TemplatePlugin {
4
4
  readonly contentType = "image";
5
5
  simpleTagReplacements(tag: Tag, data: ScopeData, context: TemplateContext): Promise<void>;
6
+ private getNextImageId;
6
7
  private createMarkup;
7
8
  private docProperties;
8
9
  private pictureMarkup;
@@ -3,6 +3,7 @@ export * from './link';
3
3
  export * from './loop';
4
4
  export * from './rawXml';
5
5
  export * from './text';
6
+ export * from "./chart";
6
7
  export * from './defaultPlugins';
7
8
  export * from './pluginContent';
8
9
  export * from './templatePlugin';
@@ -1,2 +1,3 @@
1
1
  export declare function stringValue(val: unknown): string;
2
2
  export declare function normalizeDoubleQuotes(text: string): string;
3
+ export declare function countOccurrences(text: string, substring: string): number;
@@ -1,3 +1,4 @@
1
- export * from './xml';
2
- export * from './xmlDepthTracker';
3
- export * from './xmlNode';
1
+ export * from "./xml";
2
+ export * from "./xmlDepthTracker";
3
+ export * from "./xmlNode";
4
+ export * from "./xmlTreeIterator";
@@ -2,6 +2,7 @@ import { XmlGeneralNode, XmlNode } from "./xmlNode";
2
2
  import { XmlCommentNode } from "./xmlNode";
3
3
  import { XmlTextNode } from "./xmlNode";
4
4
  import type { IMap } from "src/types";
5
+ export type XmlNodePredicate = (node: XmlNode) => boolean;
5
6
  export declare class XmlUtils {
6
7
  readonly parser: Parser;
7
8
  readonly create: Create;
@@ -30,6 +31,7 @@ declare class Create {
30
31
  }
31
32
  declare class Query {
32
33
  isTextNode(node: XmlNode): node is XmlTextNode;
34
+ isGeneralNode(node: XmlNode): node is XmlGeneralNode;
33
35
  isCommentNode(node: XmlNode): node is XmlCommentNode;
34
36
  lastTextChild(node: XmlNode, createIfMissing?: boolean): XmlTextNode;
35
37
  findParent(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode;
@@ -37,6 +39,7 @@ declare class Query {
37
39
  findChild(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode;
38
40
  findChildByName(node: XmlNode, childName: string): XmlNode;
39
41
  siblingsInRange(firstNode: XmlNode, lastNode: XmlNode): XmlNode[];
42
+ descendants(node: XmlNode, maxDepth: number, predicate: XmlNodePredicate): XmlNode[];
40
43
  }
41
44
  declare class Modify {
42
45
  insertBefore(newNode: XmlNode, referenceNode: XmlNode): void;
@@ -0,0 +1,10 @@
1
+ import { XmlNode } from "./xmlNode";
2
+ export declare class XmlTreeIterator<T extends XmlNode = XmlNode> {
3
+ get node(): T;
4
+ private _current;
5
+ private readonly depthTracker;
6
+ constructor(initial: XmlNode, maxDepth: number);
7
+ next(): XmlNode;
8
+ setCurrent(node: XmlNode): void;
9
+ private findNextNode;
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-template-x",
3
- "version": "6.2.0",
3
+ "version": "6.2.2",
4
4
  "description": "Generate docx documents from templates, in Node or in the browser.",
5
5
  "keywords": [
6
6
  "docx",