easy-template-x 6.2.1 → 6.2.3

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.
@@ -202,8 +202,16 @@ function isNumber(value) {
202
202
  class Path {
203
203
  static getFilename(path) {
204
204
  const lastSlashIndex = path.lastIndexOf('/');
205
- return path.substr(lastSlashIndex + 1);
205
+ return path.substring(lastSlashIndex + 1);
206
206
  }
207
+
208
+ /**
209
+ * Get the directory of a path.
210
+ * Exclude the last slash.
211
+ *
212
+ * Example:
213
+ * /folder/subfolder/file.txt -> /folder/subfolder
214
+ */
207
215
  static getDirectory(path) {
208
216
  const lastSlashIndex = path.lastIndexOf('/');
209
217
  return path.substring(0, lastSlashIndex);
@@ -404,6 +412,10 @@ function stringValue(val) {
404
412
  function normalizeDoubleQuotes(text) {
405
413
  return text.replace(nonStandardDoubleQuotesRegex, standardDoubleQuotes);
406
414
  }
415
+ function countOccurrences(text, substring) {
416
+ // https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
417
+ return (text.match(new RegExp(substring, 'g')) || []).length;
418
+ }
407
419
 
408
420
  class JsZipHelper {
409
421
  static toJsZipOutputType(binaryOrType) {
@@ -457,6 +469,9 @@ class Zip {
457
469
  this.binaryFormat = binaryFormat;
458
470
  }
459
471
  getFile(path) {
472
+ if (path && path.startsWith('/')) {
473
+ path = path.substring(1);
474
+ }
460
475
  const internalZipObject = this.zip.files[path];
461
476
  if (!internalZipObject) return null;
462
477
  return new ZipObject(internalZipObject, this.binaryFormat);
@@ -868,7 +883,7 @@ let Modify$1 = class Modify {
868
883
  insertBefore(newNode, referenceNode) {
869
884
  if (!newNode) throw new InternalArgumentMissingError("newNode");
870
885
  if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
871
- if (!referenceNode.parentNode) throw new Error(`'${"referenceNode"}' has no parent`);
886
+ if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
872
887
  const childNodes = referenceNode.parentNode.childNodes;
873
888
  const beforeNodeIndex = childNodes.indexOf(referenceNode);
874
889
  xml.modify.insertChild(referenceNode.parentNode, newNode, beforeNodeIndex);
@@ -883,7 +898,7 @@ let Modify$1 = class Modify {
883
898
  insertAfter(newNode, referenceNode) {
884
899
  if (!newNode) throw new InternalArgumentMissingError("newNode");
885
900
  if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
886
- if (!referenceNode.parentNode) throw new Error(`'${"referenceNode"}' has no parent`);
901
+ if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
887
902
  const childNodes = referenceNode.parentNode.childNodes;
888
903
  const referenceNodeIndex = childNodes.indexOf(referenceNode);
889
904
  xml.modify.insertChild(referenceNode.parentNode, newNode, referenceNodeIndex + 1);
@@ -1020,7 +1035,7 @@ let Modify$1 = class Modify {
1020
1035
  * `false` then the original child node is the first child of `right`.
1021
1036
  */
1022
1037
  splitByChild(parent, child, removeChild) {
1023
- if (child.parentNode != parent) throw new Error(`Node '${"child"}' is not a direct child of '${"parent"}'.`);
1038
+ if (child.parentNode != parent) throw new Error(`Node 'child' is not a direct child of 'parent'.`);
1024
1039
 
1025
1040
  // create childless clone 'left'
1026
1041
  const left = xml.create.cloneNode(parent, false);
@@ -1128,20 +1143,35 @@ const RelType = Object.freeze({
1128
1143
  Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table'
1129
1144
  });
1130
1145
  class Relationship {
1131
- static fromXml(xml) {
1146
+ static fromXml(partDir, xml) {
1132
1147
  return new Relationship({
1133
1148
  id: xml.attributes?.['Id'],
1134
1149
  type: xml.attributes?.['Type'],
1135
- target: Relationship.normalizeRelTarget(xml.attributes?.['Target']),
1150
+ target: Relationship.normalizeRelTarget(partDir, xml.attributes?.['Target']),
1136
1151
  targetMode: xml.attributes?.['TargetMode']
1137
1152
  });
1138
1153
  }
1139
- static normalizeRelTarget(target) {
1154
+ static normalizeRelTarget(partDir, target) {
1140
1155
  if (!target) {
1141
1156
  return target;
1142
1157
  }
1158
+
1159
+ // Remove leading slashes from input
1160
+ if (partDir.startsWith('/')) {
1161
+ partDir = partDir.substring(1);
1162
+ }
1163
+ if (target.startsWith('/')) {
1164
+ target = target.substring(1);
1165
+ }
1166
+
1167
+ // Convert target to relative path
1168
+ if (target.startsWith(partDir)) {
1169
+ target = target.substring(partDir.length);
1170
+ }
1171
+
1172
+ // Remove leading slashes from output
1143
1173
  if (target.startsWith('/')) {
1144
- return target.substring(1);
1174
+ target = target.substring(1);
1145
1175
  }
1146
1176
  return target;
1147
1177
  }
@@ -1152,11 +1182,11 @@ class Relationship {
1152
1182
  const node = xml.create.generalNode('Relationship');
1153
1183
  node.attributes = {};
1154
1184
 
1155
- // set only non-empty attributes
1185
+ // Set only non-empty attributes
1156
1186
  for (const propKey of Object.keys(this)) {
1157
1187
  const value = this[propKey];
1158
1188
  if (value && typeof value === 'string') {
1159
- const attrName = propKey[0].toUpperCase() + propKey.substr(1);
1189
+ const attrName = propKey[0].toUpperCase() + propKey.substring(1);
1160
1190
  node.attributes[attrName] = value;
1161
1191
  }
1162
1192
  }
@@ -1215,13 +1245,12 @@ class ContentTypesFile {
1215
1245
  // Parse the content types file
1216
1246
  await this.parseContentTypesFile();
1217
1247
 
1218
- // Mime type already exists
1219
- if (this.contentTypes[mime]) return;
1220
-
1221
1248
  // Extension already exists
1222
- // Unfortunately, this can happen in real life so we need to handle it.
1249
+ //
1250
+ // Multiple extensions may map to the same mime type, but a single
1251
+ // extension must only map to one mime type.
1223
1252
  const extension = MimeTypeHelper.getDefaultExtension(mime);
1224
- if (Object.values(this.contentTypes).includes(extension)) return;
1253
+ if (this.contentTypes[extension]) return;
1225
1254
 
1226
1255
  // Add new node
1227
1256
  const typeNode = xml.create.generalNode('Default');
@@ -1233,11 +1262,11 @@ class ContentTypesFile {
1233
1262
 
1234
1263
  // Update state
1235
1264
  this.addedNew = true;
1236
- this.contentTypes[mime] = extension;
1265
+ this.contentTypes[extension] = mime;
1237
1266
  }
1238
- async count() {
1267
+ async xmlString() {
1239
1268
  await this.parseContentTypesFile();
1240
- return this.root.childNodes.filter(node => !xml.query.isTextNode(node)).length;
1269
+ return xml.parser.serializeFile(this.root);
1241
1270
  }
1242
1271
 
1243
1272
  /**
@@ -1266,7 +1295,7 @@ class ContentTypesFile {
1266
1295
  if (!contentTypeAttribute) continue;
1267
1296
  const extensionAttribute = genNode.attributes['Extension'];
1268
1297
  if (!extensionAttribute) continue;
1269
- this.contentTypes[contentTypeAttribute] = extensionAttribute;
1298
+ this.contentTypes[extensionAttribute] = contentTypeAttribute;
1270
1299
  }
1271
1300
  }
1272
1301
  }
@@ -1286,39 +1315,38 @@ class MediaFiles {
1286
1315
  * Returns the media file path.
1287
1316
  */
1288
1317
  async add(mediaFile, mime) {
1289
- // check if already added
1318
+ // Check if already added
1290
1319
  if (this.files.has(mediaFile)) return this.files.get(mediaFile);
1291
1320
 
1292
- // hash existing media files
1321
+ // Hash existing media files
1293
1322
  await this.hashMediaFiles();
1294
1323
 
1295
- // hash the new file
1324
+ // Hash the new file
1296
1325
  // Note: Even though hashing the base64 string may seem inefficient
1297
1326
  // (requires extra step in some cases) in practice it is significantly
1298
1327
  // faster than hashing a 'binarystring'.
1299
1328
  const base64 = await Binary.toBase64(mediaFile);
1300
1329
  const hash = sha1(base64);
1301
1330
 
1302
- // check if file already exists
1303
- // note: this can be optimized by keeping both mapping by filename as well as by hash
1331
+ // Check if file already exists
1332
+ // Note: this can be optimized by keeping both mapping by filename as well as by hash
1304
1333
  let path = Object.keys(this.hashes).find(p => this.hashes[p] === hash);
1305
1334
  if (path) return path;
1306
1335
 
1307
- // generate unique media file name
1336
+ // Generate unique media file name
1337
+ const baseFilename = this.baseFilename(mime);
1308
1338
  const extension = MimeTypeHelper.getDefaultExtension(mime);
1309
1339
  do {
1310
1340
  this.nextFileId++;
1311
- path = `${MediaFiles.mediaDir}/media${this.nextFileId}.${extension}`;
1341
+ path = `${MediaFiles.mediaDir}/${baseFilename}${this.nextFileId}.${extension}`;
1312
1342
  } while (this.hashes[path]);
1313
1343
 
1314
- // add media to zip
1315
- await this.zip.setFile(path, mediaFile);
1344
+ // Add media to zip
1345
+ this.zip.setFile(path, mediaFile);
1316
1346
 
1317
- // add media to our lookups
1347
+ // Add media to our lookups
1318
1348
  this.hashes[path] = hash;
1319
1349
  this.files.set(mediaFile, path);
1320
-
1321
- // return
1322
1350
  return path;
1323
1351
  }
1324
1352
  async count() {
@@ -1334,9 +1362,15 @@ class MediaFiles {
1334
1362
  if (!filename) continue;
1335
1363
  const fileData = await this.zip.getFile(path).getContentBase64();
1336
1364
  const fileHash = sha1(fileData);
1337
- this.hashes[filename] = fileHash;
1365
+ this.hashes[path] = fileHash;
1338
1366
  }
1339
1367
  }
1368
+ baseFilename(mime) {
1369
+ // Naive heuristic.
1370
+ // May need to be modified if we're going to support more mime types.
1371
+ const parts = mime.split('/');
1372
+ return parts[0];
1373
+ }
1340
1374
  }
1341
1375
 
1342
1376
  /**
@@ -1357,20 +1391,20 @@ class RelsFile {
1357
1391
  * Returns the rel ID.
1358
1392
  */
1359
1393
  async add(relTarget, relType, relTargetMode) {
1360
- // if relTarget is an internal file it should be relative to the part dir
1394
+ // If relTarget is an internal file it should be relative to the part dir
1361
1395
  if (this.partDir && relTarget.startsWith(this.partDir)) {
1362
- relTarget = relTarget.substr(this.partDir.length + 1);
1396
+ relTarget = relTarget.substring(this.partDir.length + 1);
1363
1397
  }
1364
1398
 
1365
- // parse rels file
1399
+ // Parse rels file
1366
1400
  await this.parseRelsFile();
1367
1401
 
1368
- // already exists?
1402
+ // Already exists?
1369
1403
  const relTargetKey = this.getRelTargetKey(relType, relTarget);
1370
1404
  let relId = this.relTargets[relTargetKey];
1371
1405
  if (relId) return relId;
1372
1406
 
1373
- // create rel node
1407
+ // Create rel node
1374
1408
  relId = this.getNextRelId();
1375
1409
  const rel = new Relationship({
1376
1410
  id: relId,
@@ -1379,11 +1413,11 @@ class RelsFile {
1379
1413
  targetMode: relTargetMode
1380
1414
  });
1381
1415
 
1382
- // update lookups
1416
+ // Update lookups
1383
1417
  this.rels[relId] = rel;
1384
1418
  this.relTargets[relTargetKey] = relId;
1385
1419
 
1386
- // return
1420
+ // Return
1387
1421
  return relId;
1388
1422
  }
1389
1423
  async list() {
@@ -1402,20 +1436,20 @@ class RelsFile {
1402
1436
  * Called automatically by the holding `Docx` before exporting.
1403
1437
  */
1404
1438
  async save() {
1405
- // not change - no need to save
1439
+ // Not change - no need to save
1406
1440
  if (!this.rels) return;
1407
1441
 
1408
- // create rels xml
1442
+ // Create rels xml
1409
1443
  const root = this.createRootNode();
1410
1444
  root.childNodes = Object.values(this.rels).map(rel => rel.toXml());
1411
1445
 
1412
- // serialize and save
1446
+ // Serialize and save
1413
1447
  const xmlContent = xml.parser.serializeFile(root);
1414
1448
  this.zip.setFile(this.relsFilePath, xmlContent);
1415
1449
  }
1416
1450
 
1417
1451
  //
1418
- // private methods
1452
+ // Private methods
1419
1453
  //
1420
1454
 
1421
1455
  getNextRelId() {
@@ -1427,10 +1461,10 @@ class RelsFile {
1427
1461
  return relId;
1428
1462
  }
1429
1463
  async parseRelsFile() {
1430
- // already parsed
1464
+ // Already parsed
1431
1465
  if (this.rels) return;
1432
1466
 
1433
- // parse xml
1467
+ // Parse xml
1434
1468
  let root;
1435
1469
  const relsFile = this.zip.getFile(this.relsFilePath);
1436
1470
  if (relsFile) {
@@ -1440,24 +1474,23 @@ class RelsFile {
1440
1474
  root = this.createRootNode();
1441
1475
  }
1442
1476
 
1443
- // parse relationship nodes
1477
+ // Parse relationship nodes
1444
1478
  this.rels = {};
1445
1479
  this.relTargets = {};
1446
1480
  for (const relNode of root.childNodes) {
1447
- const attributes = relNode.attributes;
1481
+ const genRelNode = relNode;
1482
+ const attributes = genRelNode.attributes;
1448
1483
  if (!attributes) continue;
1449
1484
  const idAttr = attributes['Id'];
1450
1485
  if (!idAttr) continue;
1451
1486
 
1452
- // store rel
1453
- const rel = Relationship.fromXml(relNode);
1487
+ // Store rel
1488
+ const rel = Relationship.fromXml(this.partDir, genRelNode);
1454
1489
  this.rels[idAttr] = rel;
1455
1490
 
1456
- // create rel target lookup
1457
- const typeAttr = attributes['Type'];
1458
- const targetAttr = Relationship.normalizeRelTarget(attributes['Target']);
1459
- if (typeAttr && targetAttr) {
1460
- const relTargetKey = this.getRelTargetKey(typeAttr, targetAttr);
1491
+ // Create rel target lookup
1492
+ if (rel.type && rel.target) {
1493
+ const relTargetKey = this.getRelTargetKey(rel.type, rel.target);
1461
1494
  this.relTargets[relTargetKey] = idAttr;
1462
1495
  }
1463
1496
  }
@@ -1513,7 +1546,7 @@ class OpenXmlPart {
1513
1546
  async getText() {
1514
1547
  const xmlDocument = await this.xmlRoot();
1515
1548
 
1516
- // ugly but good enough...
1549
+ // Ugly but good enough...
1517
1550
  const xmlString = xml.parser.serializeFile(xmlDocument);
1518
1551
  const domDocument = xml.parser.domParse(xmlString);
1519
1552
  return domDocument.documentElement.textContent;
@@ -1828,7 +1861,7 @@ class Query {
1828
1861
  */
1829
1862
  containingTextNode(node) {
1830
1863
  if (!node) return null;
1831
- if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument ${"node"}. Expected a XmlTextNode.`);
1864
+ if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument node. Expected a XmlTextNode.`);
1832
1865
  return xml.query.findParent(node, officeMarkup.query.isTextNode);
1833
1866
  }
1834
1867
 
@@ -1945,7 +1978,7 @@ class Modify {
1945
1978
  splitParagraphByTextNode(paragraph, textNode, removeTextNode) {
1946
1979
  // input validation
1947
1980
  const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
1948
- if (containingParagraph != paragraph) throw new Error(`Node '${"textNode"}' is not a descendant of '${"paragraph"}'.`);
1981
+ if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
1949
1982
  const runNode = officeMarkup.query.containingRunNode(textNode);
1950
1983
  const wordTextNode = officeMarkup.query.containingTextNode(textNode);
1951
1984
 
@@ -2603,19 +2636,17 @@ class ImagePlugin extends TemplatePlugin {
2603
2636
  if (!context.pluginContext[this.contentType]) {
2604
2637
  context.pluginContext[this.contentType] = {};
2605
2638
  }
2606
- if (!context.pluginContext[this.contentType]) {
2607
- context.pluginContext[this.contentType] = {};
2608
- }
2609
2639
  const pluginContext = context.pluginContext[this.contentType];
2610
2640
  if (!pluginContext.lastDrawingObjectId) {
2611
2641
  pluginContext.lastDrawingObjectId = {};
2612
2642
  }
2613
2643
  const lastIdMap = pluginContext.lastDrawingObjectId;
2644
+ const lastIdKey = context.currentPart.path;
2614
2645
 
2615
2646
  // Get next image ID if already initialized.
2616
- if (lastIdMap[context.currentPart.path]) {
2617
- lastIdMap[context.currentPart.path]++;
2618
- return lastIdMap[context.currentPart.path];
2647
+ if (lastIdMap[lastIdKey]) {
2648
+ lastIdMap[lastIdKey]++;
2649
+ return lastIdMap[lastIdKey];
2619
2650
  }
2620
2651
 
2621
2652
  // Init next image ID.
@@ -2632,8 +2663,8 @@ class ImagePlugin extends TemplatePlugin {
2632
2663
  // Start counting from the current max
2633
2664
  const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
2634
2665
  const maxId = Math.max(...ids, 0);
2635
- lastIdMap[context.currentPart.path] = maxId + 1;
2636
- return lastIdMap[context.currentPart.path];
2666
+ lastIdMap[lastIdKey] = maxId + 1;
2667
+ return lastIdMap[lastIdKey];
2637
2668
  }
2638
2669
  createMarkup(imageId, relId, content) {
2639
2670
  // http://officeopenxml.com/drwPicInline.php
@@ -4648,7 +4679,7 @@ class Delimiters {
4648
4679
  constructor(initial) {
4649
4680
  Object.assign(this, initial);
4650
4681
  this.encodeAndValidate();
4651
- if (this.containerTagOpen === this.containerTagClose) throw new Error(`${"containerTagOpen"} can not be equal to ${"containerTagClose"}`);
4682
+ if (this.containerTagOpen === this.containerTagClose) throw new Error(`containerTagOpen can not be equal to containerTagClose`);
4652
4683
  }
4653
4684
  encodeAndValidate() {
4654
4685
  const keys = ['tagStart', 'tagEnd', 'containerTagOpen', 'containerTagClose'];
@@ -4691,12 +4722,12 @@ class TemplateHandler {
4691
4722
  /**
4692
4723
  * Version number of the `easy-template-x` library.
4693
4724
  */
4694
- version = "6.2.1" ;
4725
+ version = "6.2.3" ;
4695
4726
  constructor(options) {
4696
4727
  this.options = new TemplateHandlerOptions(options);
4697
4728
 
4698
4729
  //
4699
- // this is the library's composition root
4730
+ // This is the library's composition root
4700
4731
  //
4701
4732
 
4702
4733
  const delimiterSearcher = new DelimiterSearcher();
@@ -4727,14 +4758,14 @@ class TemplateHandler {
4727
4758
  }
4728
4759
 
4729
4760
  //
4730
- // public methods
4761
+ // Public methods
4731
4762
  //
4732
4763
 
4733
4764
  async process(templateFile, data) {
4734
- // load the docx file
4765
+ // Load the docx file
4735
4766
  const docx = await Docx.load(templateFile);
4736
4767
 
4737
- // prepare context
4768
+ // Prepare context
4738
4769
  const scopeData = new ScopeData(data);
4739
4770
  scopeData.scopeDataResolver = this.options.scopeDataResolver;
4740
4771
  const context = {
@@ -4749,18 +4780,18 @@ class TemplateHandler {
4749
4780
  for (const part of contentParts) {
4750
4781
  context.currentPart = part;
4751
4782
 
4752
- // extensions - before compilation
4783
+ // Extensions - before compilation
4753
4784
  await this.callExtensions(this.options.extensions?.beforeCompilation, scopeData, context);
4754
4785
 
4755
- // compilation (do replacements)
4786
+ // Compilation (do replacements)
4756
4787
  const xmlRoot = await part.xmlRoot();
4757
4788
  await this.compiler.compile(xmlRoot, scopeData, context);
4758
4789
 
4759
- // extensions - after compilation
4790
+ // Extensions - after compilation
4760
4791
  await this.callExtensions(this.options.extensions?.afterCompilation, scopeData, context);
4761
4792
  }
4762
4793
 
4763
- // export the result
4794
+ // Export the result
4764
4795
  return docx.export();
4765
4796
  }
4766
4797
  async parseTags(templateFile) {
@@ -4822,7 +4853,7 @@ class TemplateHandler {
4822
4853
  }
4823
4854
 
4824
4855
  //
4825
- // private methods
4856
+ // Private methods
4826
4857
  //
4827
4858
 
4828
4859
  async callExtensions(extensions, scopeData, context) {
@@ -4836,6 +4867,7 @@ class TemplateHandler {
4836
4867
  exports.Base64 = Base64;
4837
4868
  exports.Binary = Binary;
4838
4869
  exports.COMMENT_NODE_NAME = COMMENT_NODE_NAME;
4870
+ exports.ChartPlugin = ChartPlugin;
4839
4871
  exports.DelimiterSearcher = DelimiterSearcher;
4840
4872
  exports.Delimiters = Delimiters;
4841
4873
  exports.Docx = Docx;
@@ -4887,6 +4919,7 @@ exports.XmlTreeIterator = XmlTreeIterator;
4887
4919
  exports.XmlUtils = XmlUtils;
4888
4920
  exports.Zip = Zip;
4889
4921
  exports.ZipObject = ZipObject;
4922
+ exports.countOccurrences = countOccurrences;
4890
4923
  exports.createDefaultPlugins = createDefaultPlugins;
4891
4924
  exports.first = first;
4892
4925
  exports.inheritsFrom = inheritsFrom;