easy-template-x 6.2.2 → 7.0.0

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.
Files changed (178) hide show
  1. package/README.md +36 -7
  2. package/dist/cjs/easy-template-x.cjs +920 -430
  3. package/dist/es/easy-template-x.mjs +920 -431
  4. package/dist/types/src/compilation/delimiters/attributesDelimiterSearcher.d.ts +15 -0
  5. package/dist/types/src/compilation/delimiters/delimiterMark.d.ts +19 -0
  6. package/dist/types/src/compilation/delimiters/delimiterSearcher.d.ts +9 -0
  7. package/dist/types/src/compilation/delimiters/delimiterSearcher.tests.d.ts +1 -0
  8. package/dist/types/src/compilation/delimiters/index.d.ts +2 -0
  9. package/dist/types/src/compilation/delimiters/textNodesDelimiterSearcher.d.ts +18 -0
  10. package/dist/types/{compilation → src/compilation}/index.d.ts +1 -2
  11. package/dist/types/src/compilation/scopeData.tests.d.ts +1 -0
  12. package/dist/types/src/compilation/tag.d.ts +29 -0
  13. package/dist/types/src/compilation/tagParser.d.ts +16 -0
  14. package/dist/types/src/compilation/tagParser.tests.d.ts +1 -0
  15. package/dist/types/src/compilation/tagUtils.d.ts +2 -0
  16. package/dist/types/{compilation → src/compilation}/templateCompiler.d.ts +1 -1
  17. package/dist/types/src/compilation/templateCompiler.tests.d.ts +1 -0
  18. package/dist/types/src/errors/malformedFileError.d.ts +3 -0
  19. package/dist/types/src/office/contentTypesFile.tests.d.ts +1 -0
  20. package/dist/types/src/office/mediaFiles.tests.d.ts +1 -0
  21. package/dist/types/{office → src/office}/officeMarkup.d.ts +8 -6
  22. package/dist/types/src/office/officeMarkup.tests.d.ts +1 -0
  23. package/dist/types/{office → src/office}/omlNode.d.ts +22 -0
  24. package/dist/types/src/office/rel.tests.d.ts +1 -0
  25. package/dist/types/{office → src/office}/relationship.d.ts +2 -2
  26. package/dist/types/src/plugins/chart/chartDataValidation.tests.d.ts +1 -0
  27. package/dist/types/{plugins → src/plugins}/chart/chartPlugin.d.ts +3 -1
  28. package/dist/types/src/plugins/image/createImage.d.ts +3 -0
  29. package/dist/types/{plugins → src/plugins}/image/imageContent.d.ts +2 -2
  30. package/dist/types/{plugins → src/plugins}/image/imagePlugin.d.ts +3 -6
  31. package/dist/types/src/plugins/image/imageUtils.d.ts +3 -0
  32. package/dist/types/src/plugins/image/updateImage.d.ts +3 -0
  33. package/dist/types/{plugins → src/plugins}/link/linkPlugin.d.ts +3 -1
  34. package/dist/types/{plugins → src/plugins}/loop/loopPlugin.d.ts +3 -1
  35. package/dist/types/{plugins → src/plugins}/loop/strategy/iLoopStrategy.d.ts +3 -3
  36. package/dist/types/{plugins → src/plugins}/loop/strategy/loopListStrategy.d.ts +3 -3
  37. package/dist/types/{plugins → src/plugins}/loop/strategy/loopParagraphStrategy.d.ts +3 -3
  38. package/dist/types/src/plugins/loop/strategy/loopParagraphStrategy.tests.d.ts +1 -0
  39. package/dist/types/{plugins → src/plugins}/loop/strategy/loopTableColumnsStrategy.d.ts +3 -3
  40. package/dist/types/{plugins → src/plugins}/loop/strategy/loopTableRowsStrategy.d.ts +3 -3
  41. package/dist/types/{plugins → src/plugins}/rawXml/rawXmlPlugin.d.ts +2 -1
  42. package/dist/types/{plugins → src/plugins}/text/textPlugin.d.ts +4 -1
  43. package/dist/types/src/templateHandler.tests.d.ts +1 -0
  44. package/dist/types/{xml → src/xml}/xml.d.ts +5 -4
  45. package/dist/types/src/xml/xml.tests.d.ts +1 -0
  46. package/dist/types/{xml → src/xml}/xmlNode.d.ts +1 -1
  47. package/dist/types/test/fixtures/fixtureUtils.d.ts +1 -0
  48. package/dist/types/test/testUtils.d.ts +8 -0
  49. package/package.json +17 -19
  50. package/src/compilation/delimiters/attributesDelimiterSearcher.ts +109 -0
  51. package/src/compilation/delimiters/delimiterMark.ts +32 -0
  52. package/src/compilation/delimiters/delimiterSearcher.tests.ts +571 -0
  53. package/src/compilation/delimiters/delimiterSearcher.ts +41 -0
  54. package/src/compilation/delimiters/index.ts +2 -0
  55. package/src/compilation/delimiters/textNodesDelimiterSearcher.ts +185 -0
  56. package/src/compilation/index.ts +1 -2
  57. package/src/compilation/scopeData.tests.ts +40 -0
  58. package/src/compilation/tag.ts +22 -3
  59. package/src/compilation/tagParser.tests.ts +845 -0
  60. package/src/compilation/tagParser.ts +170 -66
  61. package/src/compilation/tagUtils.ts +9 -0
  62. package/src/compilation/templateCompiler.tests.ts +91 -0
  63. package/src/compilation/templateCompiler.ts +1 -1
  64. package/src/delimiters.ts +1 -1
  65. package/src/errors/malformedFileError.ts +3 -7
  66. package/src/office/contentTypesFile.tests.ts +88 -0
  67. package/src/office/docx.ts +2 -2
  68. package/src/office/mediaFiles.tests.ts +56 -0
  69. package/src/office/officeMarkup.tests.ts +114 -0
  70. package/src/office/officeMarkup.ts +48 -19
  71. package/src/office/omlNode.ts +75 -2
  72. package/src/office/openXmlPart.ts +1 -1
  73. package/src/office/rel.tests.ts +23 -0
  74. package/src/office/relationship.ts +22 -6
  75. package/src/office/relsFile.ts +21 -22
  76. package/src/office/xlsx.ts +2 -2
  77. package/src/plugins/chart/chartDataValidation.tests.ts +109 -0
  78. package/src/plugins/chart/chartPlugin.ts +11 -5
  79. package/src/plugins/chart/updateChart.ts +4 -4
  80. package/src/plugins/image/createImage.ts +115 -0
  81. package/src/plugins/image/imageContent.ts +16 -4
  82. package/src/plugins/image/imagePlugin.ts +28 -138
  83. package/src/plugins/image/imageUtils.ts +22 -0
  84. package/src/plugins/image/updateImage.ts +113 -0
  85. package/src/plugins/link/linkPlugin.ts +10 -3
  86. package/src/plugins/loop/loopPlugin.ts +11 -1
  87. package/src/plugins/loop/strategy/iLoopStrategy.ts +3 -3
  88. package/src/plugins/loop/strategy/loopListStrategy.ts +3 -3
  89. package/src/plugins/loop/strategy/loopParagraphStrategy.tests.ts +64 -0
  90. package/src/plugins/loop/strategy/loopParagraphStrategy.ts +5 -5
  91. package/src/plugins/loop/strategy/loopTableColumnsStrategy.ts +3 -3
  92. package/src/plugins/loop/strategy/loopTableRowsStrategy.ts +3 -3
  93. package/src/plugins/rawXml/rawXmlPlugin.ts +8 -2
  94. package/src/plugins/text/textPlugin.ts +37 -5
  95. package/src/templateHandler.tests.ts +84 -0
  96. package/src/templateHandler.ts +12 -15
  97. package/src/utils/path.ts +8 -1
  98. package/src/xml/xml.tests.ts +260 -0
  99. package/src/xml/xml.ts +55 -24
  100. package/src/xml/xmlNode.ts +1 -1
  101. package/src/zip/jsZipHelper.ts +1 -1
  102. package/src/zip/zip.ts +3 -0
  103. package/dist/types/compilation/delimiterMark.d.ts +0 -8
  104. package/dist/types/compilation/delimiterSearcher.d.ts +0 -12
  105. package/dist/types/compilation/tag.d.ts +0 -15
  106. package/dist/types/compilation/tagParser.d.ts +0 -11
  107. package/dist/types/errors/malformedFileError.d.ts +0 -4
  108. package/src/compilation/delimiterMark.ts +0 -16
  109. package/src/compilation/delimiterSearcher.ts +0 -183
  110. /package/dist/types/{compilation → src/compilation}/scopeData.d.ts +0 -0
  111. /package/dist/types/{compilation → src/compilation}/templateContext.d.ts +0 -0
  112. /package/dist/types/{delimiters.d.ts → src/delimiters.d.ts} +0 -0
  113. /package/dist/types/{errors → src/errors}/index.d.ts +0 -0
  114. /package/dist/types/{errors → src/errors}/internalArgumentMissingError.d.ts +0 -0
  115. /package/dist/types/{errors → src/errors}/internalError.d.ts +0 -0
  116. /package/dist/types/{errors → src/errors}/maxXmlDepthError.d.ts +0 -0
  117. /package/dist/types/{errors → src/errors}/missingCloseDelimiterError.d.ts +0 -0
  118. /package/dist/types/{errors → src/errors}/missingStartDelimiterError.d.ts +0 -0
  119. /package/dist/types/{errors → src/errors}/tagOptionsParseError.d.ts +0 -0
  120. /package/dist/types/{errors → src/errors}/templateDataError.d.ts +0 -0
  121. /package/dist/types/{errors → src/errors}/templateSyntaxError.d.ts +0 -0
  122. /package/dist/types/{errors → src/errors}/unclosedTagError.d.ts +0 -0
  123. /package/dist/types/{errors → src/errors}/unidentifiedFileTypeError.d.ts +0 -0
  124. /package/dist/types/{errors → src/errors}/unknownContentTypeError.d.ts +0 -0
  125. /package/dist/types/{errors → src/errors}/unopenedTagError.d.ts +0 -0
  126. /package/dist/types/{errors → src/errors}/unsupportedFileTypeError.d.ts +0 -0
  127. /package/dist/types/{extensions → src/extensions}/extensionOptions.d.ts +0 -0
  128. /package/dist/types/{extensions → src/extensions}/index.d.ts +0 -0
  129. /package/dist/types/{extensions → src/extensions}/templateExtension.d.ts +0 -0
  130. /package/dist/types/{index.d.ts → src/index.d.ts} +0 -0
  131. /package/dist/types/{mimeType.d.ts → src/mimeType.d.ts} +0 -0
  132. /package/dist/types/{office → src/office}/contentTypesFile.d.ts +0 -0
  133. /package/dist/types/{office → src/office}/docx.d.ts +0 -0
  134. /package/dist/types/{office → src/office}/index.d.ts +0 -0
  135. /package/dist/types/{office → src/office}/mediaFiles.d.ts +0 -0
  136. /package/dist/types/{office → src/office}/openXmlPart.d.ts +0 -0
  137. /package/dist/types/{office → src/office}/relsFile.d.ts +0 -0
  138. /package/dist/types/{office → src/office}/xlsx.d.ts +0 -0
  139. /package/dist/types/{plugins → src/plugins}/chart/chartColors.d.ts +0 -0
  140. /package/dist/types/{plugins → src/plugins}/chart/chartContent.d.ts +0 -0
  141. /package/dist/types/{plugins → src/plugins}/chart/chartData.d.ts +0 -0
  142. /package/dist/types/{plugins → src/plugins}/chart/chartDataValidation.d.ts +0 -0
  143. /package/dist/types/{plugins → src/plugins}/chart/index.d.ts +0 -0
  144. /package/dist/types/{plugins → src/plugins}/chart/updateChart.d.ts +0 -0
  145. /package/dist/types/{plugins → src/plugins}/defaultPlugins.d.ts +0 -0
  146. /package/dist/types/{plugins → src/plugins}/image/index.d.ts +0 -0
  147. /package/dist/types/{plugins → src/plugins}/index.d.ts +0 -0
  148. /package/dist/types/{plugins → src/plugins}/link/index.d.ts +0 -0
  149. /package/dist/types/{plugins → src/plugins}/link/linkContent.d.ts +0 -0
  150. /package/dist/types/{plugins → src/plugins}/loop/index.d.ts +0 -0
  151. /package/dist/types/{plugins → src/plugins}/loop/loopTagOptions.d.ts +0 -0
  152. /package/dist/types/{plugins → src/plugins}/loop/strategy/index.d.ts +0 -0
  153. /package/dist/types/{plugins → src/plugins}/pluginContent.d.ts +0 -0
  154. /package/dist/types/{plugins → src/plugins}/rawXml/index.d.ts +0 -0
  155. /package/dist/types/{plugins → src/plugins}/rawXml/rawXmlContent.d.ts +0 -0
  156. /package/dist/types/{plugins → src/plugins}/templatePlugin.d.ts +0 -0
  157. /package/dist/types/{plugins → src/plugins}/text/index.d.ts +0 -0
  158. /package/dist/types/{templateData.d.ts → src/templateData.d.ts} +0 -0
  159. /package/dist/types/{templateHandler.d.ts → src/templateHandler.d.ts} +0 -0
  160. /package/dist/types/{templateHandlerOptions.d.ts → src/templateHandlerOptions.d.ts} +0 -0
  161. /package/dist/types/{types.d.ts → src/types.d.ts} +0 -0
  162. /package/dist/types/{utils → src/utils}/array.d.ts +0 -0
  163. /package/dist/types/{utils → src/utils}/base64.d.ts +0 -0
  164. /package/dist/types/{utils → src/utils}/binary.d.ts +0 -0
  165. /package/dist/types/{utils → src/utils}/index.d.ts +0 -0
  166. /package/dist/types/{utils → src/utils}/number.d.ts +0 -0
  167. /package/dist/types/{utils → src/utils}/path.d.ts +0 -0
  168. /package/dist/types/{utils → src/utils}/regex.d.ts +0 -0
  169. /package/dist/types/{utils → src/utils}/sha1.d.ts +0 -0
  170. /package/dist/types/{utils → src/utils}/txt.d.ts +0 -0
  171. /package/dist/types/{utils → src/utils}/types.d.ts +0 -0
  172. /package/dist/types/{xml → src/xml}/index.d.ts +0 -0
  173. /package/dist/types/{xml → src/xml}/xmlDepthTracker.d.ts +0 -0
  174. /package/dist/types/{xml → src/xml}/xmlTreeIterator.d.ts +0 -0
  175. /package/dist/types/{zip → src/zip}/index.d.ts +0 -0
  176. /package/dist/types/{zip → src/zip}/jsZipHelper.d.ts +0 -0
  177. /package/dist/types/{zip → src/zip}/zip.d.ts +0 -0
  178. /package/dist/types/{zip → src/zip}/zipObject.d.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var JSZip = require('jszip');
4
3
  var xmldom = require('@xmldom/xmldom');
4
+ var JSZip = require('jszip');
5
5
  var getProp = require('lodash.get');
6
6
  var JSON5 = require('json5');
7
7
 
@@ -19,9 +19,8 @@ class InternalArgumentMissingError extends InternalError {
19
19
  }
20
20
 
21
21
  class MalformedFileError extends Error {
22
- constructor(expectedFileType) {
23
- super(`Malformed file detected. Make sure the file is a valid ${expectedFileType} file.`);
24
- this.expectedFileType = expectedFileType;
22
+ constructor(message) {
23
+ super(message);
25
24
  }
26
25
  }
27
26
 
@@ -202,8 +201,16 @@ function isNumber(value) {
202
201
  class Path {
203
202
  static getFilename(path) {
204
203
  const lastSlashIndex = path.lastIndexOf('/');
205
- return path.substr(lastSlashIndex + 1);
204
+ return path.substring(lastSlashIndex + 1);
206
205
  }
206
+
207
+ /**
208
+ * Get the directory of a path.
209
+ * Exclude the last slash.
210
+ *
211
+ * Example:
212
+ * /folder/subfolder/file.txt -> /folder/subfolder
213
+ */
207
214
  static getDirectory(path) {
208
215
  const lastSlashIndex = path.lastIndexOf('/');
209
216
  return path.substring(0, lastSlashIndex);
@@ -409,84 +416,6 @@ function countOccurrences(text, substring) {
409
416
  return (text.match(new RegExp(substring, 'g')) || []).length;
410
417
  }
411
418
 
412
- class JsZipHelper {
413
- static toJsZipOutputType(binaryOrType) {
414
- if (!binaryOrType) throw new InternalArgumentMissingError("binaryOrType");
415
- let binaryType;
416
- if (typeof binaryOrType === 'function') {
417
- binaryType = binaryOrType;
418
- } else {
419
- binaryType = binaryOrType.constructor;
420
- }
421
- if (Binary.isBlobConstructor(binaryType)) return 'blob';
422
- if (Binary.isArrayBufferConstructor(binaryType)) return 'arraybuffer';
423
- if (Binary.isBufferConstructor(binaryType)) return 'nodebuffer';
424
- throw new Error(`Binary type '${binaryType.name}' is not supported.`);
425
- }
426
- }
427
-
428
- class ZipObject {
429
- get name() {
430
- return this.zipObject.name;
431
- }
432
- set name(value) {
433
- this.zipObject.name = value;
434
- }
435
- get isDirectory() {
436
- return this.zipObject.dir;
437
- }
438
- constructor(zipObject, binaryFormat) {
439
- this.zipObject = zipObject;
440
- this.binaryFormat = binaryFormat;
441
- }
442
- getContentText() {
443
- return this.zipObject.async('text');
444
- }
445
- getContentBase64() {
446
- return this.zipObject.async('binarystring');
447
- }
448
- getContentBinary(outputType) {
449
- const zipOutputType = JsZipHelper.toJsZipOutputType(outputType ?? this.binaryFormat);
450
- return this.zipObject.async(zipOutputType);
451
- }
452
- }
453
-
454
- class Zip {
455
- static async load(file) {
456
- const zip = await JSZip.loadAsync(file);
457
- return new Zip(zip, file.constructor);
458
- }
459
- constructor(zip, binaryFormat) {
460
- this.zip = zip;
461
- this.binaryFormat = binaryFormat;
462
- }
463
- getFile(path) {
464
- const internalZipObject = this.zip.files[path];
465
- if (!internalZipObject) return null;
466
- return new ZipObject(internalZipObject, this.binaryFormat);
467
- }
468
- setFile(path, content) {
469
- this.zip.file(path, content);
470
- }
471
- isFileExist(path) {
472
- return !!this.zip.files[path];
473
- }
474
- listFiles() {
475
- return Object.keys(this.zip.files);
476
- }
477
- async export(outputType) {
478
- const zipOutputType = JsZipHelper.toJsZipOutputType(outputType ?? this.binaryFormat);
479
- const output = await this.zip.generateAsync({
480
- type: zipOutputType,
481
- compression: "DEFLATE",
482
- compressionOptions: {
483
- level: 6 // between 1 (best speed) and 9 (best compression)
484
- }
485
- });
486
- return output;
487
- }
488
- }
489
-
490
419
  const XmlNodeType = Object.freeze({
491
420
  Text: "Text",
492
421
  General: "General",
@@ -830,8 +759,28 @@ let Query$1 = class Query {
830
759
  if (!node) return null;
831
760
  return (node.childNodes || []).find(child => predicate(child));
832
761
  }
833
- findChildByName(node, childName) {
834
- return xml.query.findChild(node, n => n.nodeName === childName);
762
+ findByPath(root, nodeType, ...path) {
763
+ if (!root) {
764
+ return null;
765
+ }
766
+ let curNode = root;
767
+ for (let i = 0; i < path.length; i++) {
768
+ const curIndex = path[i];
769
+ if (typeof curIndex === 'string') {
770
+ curNode = xml.query.findChild(curNode, n => n.nodeName === curIndex);
771
+ }
772
+ if (typeof curIndex === 'number') {
773
+ const curNodeType = i == path.length - 1 ? nodeType : XmlNodeType.General;
774
+ curNode = curNode.childNodes.filter(c => c.nodeType === curNodeType)[curIndex];
775
+ }
776
+ if (!curNode) {
777
+ return null;
778
+ }
779
+ }
780
+ if (curNode.nodeType !== nodeType) {
781
+ return null;
782
+ }
783
+ return curNode;
835
784
  }
836
785
 
837
786
  /**
@@ -872,7 +821,7 @@ let Modify$1 = class Modify {
872
821
  insertBefore(newNode, referenceNode) {
873
822
  if (!newNode) throw new InternalArgumentMissingError("newNode");
874
823
  if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
875
- if (!referenceNode.parentNode) throw new Error(`'${"referenceNode"}' has no parent`);
824
+ if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
876
825
  const childNodes = referenceNode.parentNode.childNodes;
877
826
  const beforeNodeIndex = childNodes.indexOf(referenceNode);
878
827
  xml.modify.insertChild(referenceNode.parentNode, newNode, beforeNodeIndex);
@@ -887,7 +836,7 @@ let Modify$1 = class Modify {
887
836
  insertAfter(newNode, referenceNode) {
888
837
  if (!newNode) throw new InternalArgumentMissingError("newNode");
889
838
  if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
890
- if (!referenceNode.parentNode) throw new Error(`'${"referenceNode"}' has no parent`);
839
+ if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
891
840
  const childNodes = referenceNode.parentNode.childNodes;
892
841
  const referenceNodeIndex = childNodes.indexOf(referenceNode);
893
842
  xml.modify.insertChild(referenceNode.parentNode, newNode, referenceNodeIndex + 1);
@@ -1024,7 +973,7 @@ let Modify$1 = class Modify {
1024
973
  * `false` then the original child node is the first child of `right`.
1025
974
  */
1026
975
  splitByChild(parent, child, removeChild) {
1027
- if (child.parentNode != parent) throw new Error(`Node '${"child"}' is not a direct child of '${"parent"}'.`);
976
+ if (child.parentNode != parent) throw new Error(`Node 'child' is not a direct child of 'parent'.`);
1028
977
 
1029
978
  // create childless clone 'left'
1030
979
  const left = xml.create.cloneNode(parent, false);
@@ -1113,6 +1062,104 @@ function recursiveRemoveEmptyTextNodes(node) {
1113
1062
  }
1114
1063
  const xml = new XmlUtils();
1115
1064
 
1065
+ const TagDisposition = Object.freeze({
1066
+ Open: "Open",
1067
+ Close: "Close",
1068
+ SelfClosed: "SelfClosed"
1069
+ });
1070
+ const TagPlacement = Object.freeze({
1071
+ TextNode: "TextNode",
1072
+ Attribute: "Attribute"
1073
+ });
1074
+
1075
+ function tagRegex(delimiters, global = false) {
1076
+ const tagOptionsPattern = `${Regex.escape(delimiters.tagOptionsStart)}(?<tagOptions>.*?)${Regex.escape(delimiters.tagOptionsEnd)}`;
1077
+ const tagPattern = `${Regex.escape(delimiters.tagStart)}(?<tagName>.*?)(${tagOptionsPattern})?${Regex.escape(delimiters.tagEnd)}`;
1078
+ const flags = global ? 'gm' : 'm';
1079
+ return new RegExp(tagPattern, flags);
1080
+ }
1081
+
1082
+ class JsZipHelper {
1083
+ static toJsZipOutputType(binaryOrType) {
1084
+ if (!binaryOrType) throw new InternalArgumentMissingError("binaryOrType");
1085
+ let binaryType;
1086
+ if (typeof binaryOrType === 'function') {
1087
+ binaryType = binaryOrType;
1088
+ } else {
1089
+ binaryType = binaryOrType.constructor;
1090
+ }
1091
+ if (Binary.isBlobConstructor(binaryType)) return 'blob';
1092
+ if (Binary.isArrayBufferConstructor(binaryType)) return 'arraybuffer';
1093
+ if (Binary.isBufferConstructor(binaryType)) return 'nodebuffer';
1094
+ throw new Error(`Binary type '${binaryType.name}' is not supported.`);
1095
+ }
1096
+ }
1097
+
1098
+ class ZipObject {
1099
+ get name() {
1100
+ return this.zipObject.name;
1101
+ }
1102
+ set name(value) {
1103
+ this.zipObject.name = value;
1104
+ }
1105
+ get isDirectory() {
1106
+ return this.zipObject.dir;
1107
+ }
1108
+ constructor(zipObject, binaryFormat) {
1109
+ this.zipObject = zipObject;
1110
+ this.binaryFormat = binaryFormat;
1111
+ }
1112
+ getContentText() {
1113
+ return this.zipObject.async('text');
1114
+ }
1115
+ getContentBase64() {
1116
+ return this.zipObject.async('binarystring');
1117
+ }
1118
+ getContentBinary(outputType) {
1119
+ const zipOutputType = JsZipHelper.toJsZipOutputType(outputType ?? this.binaryFormat);
1120
+ return this.zipObject.async(zipOutputType);
1121
+ }
1122
+ }
1123
+
1124
+ class Zip {
1125
+ static async load(file) {
1126
+ const zip = await JSZip.loadAsync(file);
1127
+ return new Zip(zip, file.constructor);
1128
+ }
1129
+ constructor(zip, binaryFormat) {
1130
+ this.zip = zip;
1131
+ this.binaryFormat = binaryFormat;
1132
+ }
1133
+ getFile(path) {
1134
+ if (path && path.startsWith('/')) {
1135
+ path = path.substring(1);
1136
+ }
1137
+ const internalZipObject = this.zip.files[path];
1138
+ if (!internalZipObject) return null;
1139
+ return new ZipObject(internalZipObject, this.binaryFormat);
1140
+ }
1141
+ setFile(path, content) {
1142
+ this.zip.file(path, content);
1143
+ }
1144
+ isFileExist(path) {
1145
+ return !!this.zip.files[path];
1146
+ }
1147
+ listFiles() {
1148
+ return Object.keys(this.zip.files);
1149
+ }
1150
+ async export(outputType) {
1151
+ const zipOutputType = JsZipHelper.toJsZipOutputType(outputType ?? this.binaryFormat);
1152
+ const output = await this.zip.generateAsync({
1153
+ type: zipOutputType,
1154
+ compression: "DEFLATE",
1155
+ compressionOptions: {
1156
+ level: 6 // between 1 (best speed) and 9 (best compression)
1157
+ }
1158
+ });
1159
+ return output;
1160
+ }
1161
+ }
1162
+
1116
1163
  /**
1117
1164
  * The types of relationships that can be created in a docx file.
1118
1165
  * A non-comprehensive list.
@@ -1132,20 +1179,35 @@ const RelType = Object.freeze({
1132
1179
  Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table'
1133
1180
  });
1134
1181
  class Relationship {
1135
- static fromXml(xml) {
1182
+ static fromXml(partDir, xml) {
1136
1183
  return new Relationship({
1137
1184
  id: xml.attributes?.['Id'],
1138
1185
  type: xml.attributes?.['Type'],
1139
- target: Relationship.normalizeRelTarget(xml.attributes?.['Target']),
1186
+ target: Relationship.normalizeRelTarget(partDir, xml.attributes?.['Target']),
1140
1187
  targetMode: xml.attributes?.['TargetMode']
1141
1188
  });
1142
1189
  }
1143
- static normalizeRelTarget(target) {
1190
+ static normalizeRelTarget(partDir, target) {
1144
1191
  if (!target) {
1145
1192
  return target;
1146
1193
  }
1194
+
1195
+ // Remove leading slashes from input
1196
+ if (partDir.startsWith('/')) {
1197
+ partDir = partDir.substring(1);
1198
+ }
1199
+ if (target.startsWith('/')) {
1200
+ target = target.substring(1);
1201
+ }
1202
+
1203
+ // Convert target to relative path
1204
+ if (target.startsWith(partDir)) {
1205
+ target = target.substring(partDir.length);
1206
+ }
1207
+
1208
+ // Remove leading slashes from output
1147
1209
  if (target.startsWith('/')) {
1148
- return target.substring(1);
1210
+ target = target.substring(1);
1149
1211
  }
1150
1212
  return target;
1151
1213
  }
@@ -1156,11 +1218,11 @@ class Relationship {
1156
1218
  const node = xml.create.generalNode('Relationship');
1157
1219
  node.attributes = {};
1158
1220
 
1159
- // set only non-empty attributes
1221
+ // Set only non-empty attributes
1160
1222
  for (const propKey of Object.keys(this)) {
1161
1223
  const value = this[propKey];
1162
1224
  if (value && typeof value === 'string') {
1163
- const attrName = propKey[0].toUpperCase() + propKey.substr(1);
1225
+ const attrName = propKey[0].toUpperCase() + propKey.substring(1);
1164
1226
  node.attributes[attrName] = value;
1165
1227
  }
1166
1228
  }
@@ -1365,20 +1427,20 @@ class RelsFile {
1365
1427
  * Returns the rel ID.
1366
1428
  */
1367
1429
  async add(relTarget, relType, relTargetMode) {
1368
- // if relTarget is an internal file it should be relative to the part dir
1430
+ // If relTarget is an internal file it should be relative to the part dir
1369
1431
  if (this.partDir && relTarget.startsWith(this.partDir)) {
1370
- relTarget = relTarget.substr(this.partDir.length + 1);
1432
+ relTarget = relTarget.substring(this.partDir.length + 1);
1371
1433
  }
1372
1434
 
1373
- // parse rels file
1435
+ // Parse rels file
1374
1436
  await this.parseRelsFile();
1375
1437
 
1376
- // already exists?
1438
+ // Already exists?
1377
1439
  const relTargetKey = this.getRelTargetKey(relType, relTarget);
1378
1440
  let relId = this.relTargets[relTargetKey];
1379
1441
  if (relId) return relId;
1380
1442
 
1381
- // create rel node
1443
+ // Create rel node
1382
1444
  relId = this.getNextRelId();
1383
1445
  const rel = new Relationship({
1384
1446
  id: relId,
@@ -1387,11 +1449,11 @@ class RelsFile {
1387
1449
  targetMode: relTargetMode
1388
1450
  });
1389
1451
 
1390
- // update lookups
1452
+ // Update lookups
1391
1453
  this.rels[relId] = rel;
1392
1454
  this.relTargets[relTargetKey] = relId;
1393
1455
 
1394
- // return
1456
+ // Return
1395
1457
  return relId;
1396
1458
  }
1397
1459
  async list() {
@@ -1410,20 +1472,20 @@ class RelsFile {
1410
1472
  * Called automatically by the holding `Docx` before exporting.
1411
1473
  */
1412
1474
  async save() {
1413
- // not change - no need to save
1475
+ // Not change - no need to save
1414
1476
  if (!this.rels) return;
1415
1477
 
1416
- // create rels xml
1478
+ // Create rels xml
1417
1479
  const root = this.createRootNode();
1418
1480
  root.childNodes = Object.values(this.rels).map(rel => rel.toXml());
1419
1481
 
1420
- // serialize and save
1482
+ // Serialize and save
1421
1483
  const xmlContent = xml.parser.serializeFile(root);
1422
1484
  this.zip.setFile(this.relsFilePath, xmlContent);
1423
1485
  }
1424
1486
 
1425
1487
  //
1426
- // private methods
1488
+ // Private methods
1427
1489
  //
1428
1490
 
1429
1491
  getNextRelId() {
@@ -1435,10 +1497,10 @@ class RelsFile {
1435
1497
  return relId;
1436
1498
  }
1437
1499
  async parseRelsFile() {
1438
- // already parsed
1500
+ // Already parsed
1439
1501
  if (this.rels) return;
1440
1502
 
1441
- // parse xml
1503
+ // Parse xml
1442
1504
  let root;
1443
1505
  const relsFile = this.zip.getFile(this.relsFilePath);
1444
1506
  if (relsFile) {
@@ -1448,24 +1510,23 @@ class RelsFile {
1448
1510
  root = this.createRootNode();
1449
1511
  }
1450
1512
 
1451
- // parse relationship nodes
1513
+ // Parse relationship nodes
1452
1514
  this.rels = {};
1453
1515
  this.relTargets = {};
1454
1516
  for (const relNode of root.childNodes) {
1455
- const attributes = relNode.attributes;
1517
+ const genRelNode = relNode;
1518
+ const attributes = genRelNode.attributes;
1456
1519
  if (!attributes) continue;
1457
1520
  const idAttr = attributes['Id'];
1458
1521
  if (!idAttr) continue;
1459
1522
 
1460
- // store rel
1461
- const rel = Relationship.fromXml(relNode);
1523
+ // Store rel
1524
+ const rel = Relationship.fromXml(this.partDir, genRelNode);
1462
1525
  this.rels[idAttr] = rel;
1463
1526
 
1464
- // create rel target lookup
1465
- const typeAttr = attributes['Type'];
1466
- const targetAttr = Relationship.normalizeRelTarget(attributes['Target']);
1467
- if (typeAttr && targetAttr) {
1468
- const relTargetKey = this.getRelTargetKey(typeAttr, targetAttr);
1527
+ // Create rel target lookup
1528
+ if (rel.type && rel.target) {
1529
+ const relTargetKey = this.getRelTargetKey(rel.type, rel.target);
1469
1530
  this.relTargets[relTargetKey] = idAttr;
1470
1531
  }
1471
1532
  }
@@ -1521,7 +1582,7 @@ class OpenXmlPart {
1521
1582
  async getText() {
1522
1583
  const xmlDocument = await this.xmlRoot();
1523
1584
 
1524
- // ugly but good enough...
1585
+ // Ugly but good enough...
1525
1586
  const xmlString = xml.parser.serializeFile(xmlDocument);
1526
1587
  const domDocument = xml.parser.domParse(xmlString);
1527
1588
  return domDocument.documentElement.textContent;
@@ -1625,7 +1686,7 @@ class Docx {
1625
1686
  try {
1626
1687
  zip = await Zip.load(file);
1627
1688
  } catch {
1628
- throw new MalformedFileError('docx');
1689
+ throw new MalformedFileError("Failed to load zip file.");
1629
1690
  }
1630
1691
 
1631
1692
  // Load the docx file
@@ -1638,7 +1699,7 @@ class Docx {
1638
1699
  */
1639
1700
  static async open(zip) {
1640
1701
  const mainDocumentPath = await Docx.getMainDocumentPath(zip);
1641
- if (!mainDocumentPath) throw new MalformedFileError('docx');
1702
+ if (!mainDocumentPath) throw new MalformedFileError("Cannot find main document path.");
1642
1703
  return new Docx(mainDocumentPath, zip);
1643
1704
  }
1644
1705
  static async getMainDocumentPath(zip) {
@@ -1705,11 +1766,12 @@ class W {
1705
1766
  Table = 'w:tbl';
1706
1767
  TableRow = 'w:tr';
1707
1768
  TableCell = 'w:tc';
1769
+ Drawing = 'w:drawing';
1708
1770
  NumberProperties = 'w:numPr';
1709
1771
  }
1710
1772
 
1711
1773
  /**
1712
- * Drawing Markup Language node names.
1774
+ * Drawing Markup Language main namespace node names.
1713
1775
  *
1714
1776
  * These elements are part of the main drawingML namespace:
1715
1777
  * xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main".
@@ -1720,6 +1782,68 @@ class A {
1720
1782
  Run = 'a:r';
1721
1783
  RunProperties = 'a:rPr';
1722
1784
  Text = 'a:t';
1785
+ Graphic = 'a:graphic';
1786
+ GraphicData = 'a:graphicData';
1787
+ /**
1788
+ * Binary large image (or) picture.
1789
+ */
1790
+ Blip = 'a:blip';
1791
+ AlphaModFix = 'a:alphaModFix';
1792
+ }
1793
+
1794
+ /**
1795
+ * Drawing Markup Language "wordprocessing drawing" namespace node names.
1796
+ *
1797
+ * These elements are part of the wordprocessingDrawing namespace:
1798
+ * xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing".
1799
+ */
1800
+ class Wp {
1801
+ /**
1802
+ * docPr stands for "Drawing Object Non-Visual Properties", which isn't
1803
+ * exactly a good acronym but that's how it's called nevertheless.
1804
+ */
1805
+ DocPr = 'wp:docPr';
1806
+ /**
1807
+ * Inline DrawingML Object.
1808
+ *
1809
+ * see: http://officeopenxml.com/drwPicInline.php
1810
+ */
1811
+ Inline = 'wp:inline';
1812
+ /**
1813
+ * Anchor for Floating DrawingML Object.
1814
+ *
1815
+ * see: http://officeopenxml.com/drwPicFloating.php
1816
+ */
1817
+ FloatingAnchor = 'wp:anchor';
1818
+ /**
1819
+ * Drawing extent.
1820
+ */
1821
+ Extent = 'wp:extent';
1822
+ }
1823
+
1824
+ /**
1825
+ * Drawing Markup Language "picture" namespace node names.
1826
+ *
1827
+ * These elements are part of the picture namespace:
1828
+ * xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture".
1829
+ */
1830
+ class Pic {
1831
+ Pic = 'pic:pic';
1832
+ /**
1833
+ * Non-visual picture properties.
1834
+ */
1835
+ NvPicPr = 'pic:nvPicPr';
1836
+ CnVPr = 'pic:cNvPr';
1837
+ /**
1838
+ * Binary large image (or) picture fill.
1839
+ */
1840
+ BlipFill = 'pic:blipFill';
1841
+ /**
1842
+ * Shape properties.
1843
+ */
1844
+ SpPr = 'pic:spPr';
1845
+ Xfrm = 'a:xfrm';
1846
+ Ext = 'a:ext';
1723
1847
  }
1724
1848
 
1725
1849
  /**
@@ -1737,9 +1861,19 @@ class OmlNode {
1737
1861
  static W = new W();
1738
1862
 
1739
1863
  /**
1740
- * Drawing Markup Language node names.
1864
+ * Drawing Markup Language main namespace node names.
1741
1865
  */
1742
1866
  static A = new A();
1867
+
1868
+ /**
1869
+ * Drawing Markup Language "wordprocessing drawing" namespace node names.
1870
+ */
1871
+ static Wp = new Wp();
1872
+
1873
+ /**
1874
+ * Drawing Markup Language "picture" namespace node names.
1875
+ */
1876
+ static Pic = new Pic();
1743
1877
  }
1744
1878
  class OmlAttribute {
1745
1879
  static SpacePreserve = 'xml:space';
@@ -1810,9 +1944,12 @@ class Query {
1810
1944
  }
1811
1945
  isListParagraph(paragraphNode) {
1812
1946
  const paragraphProperties = officeMarkup.query.findParagraphPropertiesNode(paragraphNode);
1813
- const listNumberProperties = xml.query.findChildByName(paragraphProperties, OmlNode.W.NumberProperties);
1947
+ const listNumberProperties = xml.query.findByPath(paragraphProperties, XmlNodeType.General, OmlNode.W.NumberProperties);
1814
1948
  return !!listNumberProperties;
1815
1949
  }
1950
+ isInlineDrawingNode(node) {
1951
+ return node.nodeName === OmlNode.Wp.Inline && node.parentNode?.nodeName === OmlNode.W.Drawing;
1952
+ }
1816
1953
  findParagraphPropertiesNode(paragraphNode) {
1817
1954
  if (!officeMarkup.query.isParagraphNode(paragraphNode)) throw new Error(`Expected paragraph node but received a '${paragraphNode.nodeName}' node.`);
1818
1955
  return xml.query.findChild(paragraphNode, officeMarkup.query.isParagraphPropertiesNode);
@@ -1836,7 +1973,7 @@ class Query {
1836
1973
  */
1837
1974
  containingTextNode(node) {
1838
1975
  if (!node) return null;
1839
- if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument ${"node"}. Expected a XmlTextNode.`);
1976
+ if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument node. Expected a XmlTextNode.`);
1840
1977
  return xml.query.findParent(node, officeMarkup.query.isTextNode);
1841
1978
  }
1842
1979
 
@@ -1953,7 +2090,7 @@ class Modify {
1953
2090
  splitParagraphByTextNode(paragraph, textNode, removeTextNode) {
1954
2091
  // input validation
1955
2092
  const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
1956
- if (containingParagraph != paragraph) throw new Error(`Node '${"textNode"}' is not a descendant of '${"paragraph"}'.`);
2093
+ if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
1957
2094
  const runNode = officeMarkup.query.containingRunNode(textNode);
1958
2095
  const wordTextNode = officeMarkup.query.containingTextNode(textNode);
1959
2096
 
@@ -2102,17 +2239,36 @@ class Modify {
2102
2239
  node.attributes[OmlAttribute.SpacePreserve] = 'preserve';
2103
2240
  }
2104
2241
  }
2105
- removeTag(textNode) {
2106
- const wordTextNode = officeMarkup.query.containingTextNode(textNode);
2107
- const runNode = officeMarkup.query.containingRunNode(textNode);
2242
+ removeTag(tag) {
2243
+ if (tag.placement === TagPlacement.TextNode) {
2244
+ const wordTextNode = officeMarkup.query.containingTextNode(tag.xmlTextNode);
2245
+ const runNode = officeMarkup.query.containingRunNode(tag.xmlTextNode);
2246
+
2247
+ // Remove the word text node
2248
+ xml.modify.remove(wordTextNode);
2108
2249
 
2109
- // Remove the word text node
2110
- xml.modify.remove(wordTextNode);
2250
+ // Remove the run node if it's empty
2251
+ if (officeMarkup.query.isEmptyRun(runNode)) {
2252
+ xml.modify.remove(runNode);
2253
+ }
2254
+ return;
2255
+ }
2256
+ if (tag.placement === TagPlacement.Attribute) {
2257
+ if (!tag.xmlNode.attributes || !(tag.attributeName in tag.xmlNode.attributes)) {
2258
+ return;
2259
+ }
2260
+
2261
+ // Remove the tag from the attribute value
2262
+ tag.xmlNode.attributes[tag.attributeName] = tag.xmlNode.attributes[tag.attributeName].replace(tag.rawText, "");
2111
2263
 
2112
- // Remove the run node if it's empty
2113
- if (officeMarkup.query.isEmptyRun(runNode)) {
2114
- xml.modify.remove(runNode);
2264
+ // Remove the attribute if it's empty
2265
+ if (tag.xmlNode.attributes[tag.attributeName] === "") {
2266
+ delete tag.xmlNode.attributes[tag.attributeName];
2267
+ }
2268
+ return;
2115
2269
  }
2270
+ const anyTag = tag;
2271
+ throw new Error(`Unexpected tag placement "${anyTag.placement}" for tag "${anyTag.rawText}".`);
2116
2272
  }
2117
2273
  }
2118
2274
 
@@ -2134,7 +2290,7 @@ class Xlsx {
2134
2290
  try {
2135
2291
  zip = await Zip.load(file);
2136
2292
  } catch {
2137
- throw new MalformedFileError('xlsx');
2293
+ throw new MalformedFileError("Failed to load zip file.");
2138
2294
  }
2139
2295
 
2140
2296
  // Load the xlsx file
@@ -2147,7 +2303,7 @@ class Xlsx {
2147
2303
  */
2148
2304
  static async open(zip) {
2149
2305
  const mainDocumentPath = await Xlsx.getMainDocumentPath(zip);
2150
- if (!mainDocumentPath) throw new MalformedFileError('xlsx');
2306
+ if (!mainDocumentPath) throw new MalformedFileError("Cannot find main document path.");
2151
2307
  return new Xlsx(mainDocumentPath, zip);
2152
2308
  }
2153
2309
  static async getMainDocumentPath(zip) {
@@ -2200,7 +2356,86 @@ class Xlsx {
2200
2356
  }
2201
2357
  }
2202
2358
 
2203
- class MatchState {
2359
+ const drawingDescriptionAttributeName = "descr";
2360
+ class AttributesDelimiterSearcher {
2361
+ visitedNodes = new Set();
2362
+ constructor(delimiters) {
2363
+ if (!delimiters) throw new InternalArgumentMissingError("delimiters");
2364
+ this.delimiters = delimiters;
2365
+ this.tagRegex = tagRegex(delimiters, true);
2366
+ }
2367
+ processNode(it, delimiters) {
2368
+ // Ignore irrelevant nodes
2369
+ if (!this.shouldSearchNode(it)) {
2370
+ return;
2371
+ }
2372
+
2373
+ // Search delimiters in attributes
2374
+ this.findDelimiters(it, delimiters);
2375
+ }
2376
+ shouldSearchNode(it) {
2377
+ if (this.visitedNodes.has(it.node)) {
2378
+ return false;
2379
+ }
2380
+ this.visitedNodes.add(it.node);
2381
+ if (!xml.query.isGeneralNode(it.node)) return false;
2382
+ if (Object.keys(it.node.attributes || {}).length === 0) return false;
2383
+
2384
+ // Currently we only support description attributes of drawing objects
2385
+ if (!this.isDrawingPropertiesNode(it.node)) {
2386
+ return false;
2387
+ }
2388
+ if (!it.node.attributes[drawingDescriptionAttributeName]) {
2389
+ return false;
2390
+ }
2391
+ return true;
2392
+ }
2393
+ isDrawingPropertiesNode(node) {
2394
+ // Node is drawing properties
2395
+ if (node.nodeName !== OmlNode.Wp.DocPr) {
2396
+ return false;
2397
+ }
2398
+
2399
+ // Parent is drawing
2400
+ if (!node.parentNode) {
2401
+ return false;
2402
+ }
2403
+ const parent = xml.query.findParentByName(node, OmlNode.W.Drawing);
2404
+ return !!parent;
2405
+ }
2406
+ findDelimiters(it, delimiters) {
2407
+ // Currently we only support description attributes of drawing objects
2408
+ this.findDelimitersInAttribute(it.node, drawingDescriptionAttributeName, delimiters);
2409
+ }
2410
+ findDelimitersInAttribute(node, attributeName, delimiters) {
2411
+ const attrValue = node.attributes?.[attributeName];
2412
+ if (!attrValue) {
2413
+ return;
2414
+ }
2415
+ const matches = attrValue.matchAll(this.tagRegex);
2416
+ for (const match of matches) {
2417
+ const tag = match[0];
2418
+ const openDelimiterIndex = match.index;
2419
+ const closeDelimiterIndex = openDelimiterIndex + tag.length - this.delimiters.tagEnd.length;
2420
+ const openDelimiter = this.createCurrentDelimiterMark(openDelimiterIndex, true, node, attributeName);
2421
+ const closeDelimiter = this.createCurrentDelimiterMark(closeDelimiterIndex, false, node, attributeName);
2422
+ delimiters.push(openDelimiter);
2423
+ delimiters.push(closeDelimiter);
2424
+ }
2425
+ }
2426
+ createCurrentDelimiterMark(index, isOpen, xmlNode, attributeName) {
2427
+ return {
2428
+ placement: TagPlacement.Attribute,
2429
+ isOpen: isOpen,
2430
+ index: index,
2431
+ attributeName: attributeName,
2432
+ xmlNode: xmlNode
2433
+ };
2434
+ }
2435
+ }
2436
+
2437
+ class TextNodesDelimiterSearcher {
2438
+ lookForOpenDelimiter = true;
2204
2439
  /**
2205
2440
  * The index of the current delimiter character being matched.
2206
2441
  *
@@ -2208,30 +2443,55 @@ class MatchState {
2208
2443
  * are now looking for the character `{`. If it is 1, then we are looking
2209
2444
  * for `!`.
2210
2445
  */
2211
- delimiterIndex = 0;
2446
+ lookForDelimiterIndex = 0;
2212
2447
  /**
2213
- * The list of text nodes containing the delimiter characters.
2448
+ * The list of text nodes containing the delimiter characters of the current match.
2214
2449
  */
2215
- openNodes = [];
2450
+ matchOpenNodes = [];
2216
2451
  /**
2217
- * The index of the first character of the delimiter, in the text node it
2452
+ * The index of the first character of the current delimiter match, in the text node it
2218
2453
  * was found at.
2219
2454
  *
2220
2455
  * Example: If the delimiter is `{!`, and the text node content is `abc{!xyz`,
2221
2456
  * then the firstMatchIndex is 3.
2222
2457
  */
2223
2458
  firstMatchIndex = -1;
2224
- reset() {
2225
- this.delimiterIndex = 0;
2226
- this.openNodes = [];
2459
+ constructor(startDelimiter, endDelimiter) {
2460
+ this.startDelimiter = startDelimiter;
2461
+ this.endDelimiter = endDelimiter;
2462
+ }
2463
+ processNode(it, delimiters) {
2464
+ // Reset match state on paragraph transition
2465
+ if (officeMarkup.query.isParagraphNode(it.node)) {
2466
+ this.resetMatch();
2467
+ }
2468
+
2469
+ // Reset match state on inline drawing
2470
+ if (officeMarkup.query.isInlineDrawingNode(it.node)) {
2471
+ this.resetMatch();
2472
+ }
2473
+
2474
+ // Ignore non-text nodes
2475
+ if (!this.shouldSearchNode(it)) {
2476
+ return;
2477
+ }
2478
+
2479
+ // Search delimiters in text nodes
2480
+ this.findDelimiters(it, delimiters);
2481
+ }
2482
+ resetMatch() {
2483
+ this.lookForDelimiterIndex = 0;
2484
+ this.matchOpenNodes = [];
2227
2485
  this.firstMatchIndex = -1;
2228
2486
  }
2229
- }
2230
- class DelimiterSearcher {
2231
- maxXmlDepth = 20;
2232
- startDelimiter = "{";
2233
- endDelimiter = "}";
2234
- findDelimiters(node) {
2487
+ shouldSearchNode(it) {
2488
+ if (!xml.query.isTextNode(it.node)) return false;
2489
+ if (!it.node.textContent) return false;
2490
+ if (!it.node.parentNode) return false;
2491
+ if (!officeMarkup.query.isTextNode(it.node.parentNode)) return false;
2492
+ return true;
2493
+ }
2494
+ findDelimiters(it, delimiters) {
2235
2495
  //
2236
2496
  // Performance note:
2237
2497
  //
@@ -2243,57 +2503,38 @@ class DelimiterSearcher {
2243
2503
  // complexity and effort.
2244
2504
  //
2245
2505
 
2246
- const delimiters = [];
2247
- const match = new MatchState();
2248
- const it = new XmlTreeIterator(node, this.maxXmlDepth);
2249
- let lookForOpenDelimiter = true;
2250
- while (it.node) {
2251
- // Reset state on paragraph transition
2252
- if (officeMarkup.query.isParagraphNode(it.node)) {
2253
- match.reset();
2254
- }
2506
+ // Search delimiters in text nodes
2507
+ this.matchOpenNodes.push(it.node);
2508
+ let textIndex = 0;
2509
+ while (textIndex < it.node.textContent.length) {
2510
+ const delimiterPattern = this.lookForOpenDelimiter ? this.startDelimiter : this.endDelimiter;
2511
+ const char = it.node.textContent[textIndex];
2255
2512
 
2256
- // Skip irrelevant nodes
2257
- if (!this.shouldSearchNode(it)) {
2258
- it.next();
2513
+ // No match
2514
+ if (char !== delimiterPattern[this.lookForDelimiterIndex]) {
2515
+ textIndex = this.noMatch(it, textIndex);
2516
+ textIndex++;
2259
2517
  continue;
2260
2518
  }
2261
2519
 
2262
- // Search delimiters in text nodes
2263
- match.openNodes.push(it.node);
2264
- let textIndex = 0;
2265
- while (textIndex < it.node.textContent.length) {
2266
- const delimiterPattern = lookForOpenDelimiter ? this.startDelimiter : this.endDelimiter;
2267
- const char = it.node.textContent[textIndex];
2268
-
2269
- // No match
2270
- if (char !== delimiterPattern[match.delimiterIndex]) {
2271
- textIndex = this.noMatch(it, textIndex, match);
2272
- textIndex++;
2273
- continue;
2274
- }
2275
-
2276
- // First match
2277
- if (match.firstMatchIndex === -1) {
2278
- match.firstMatchIndex = textIndex;
2279
- }
2280
-
2281
- // Partial match
2282
- if (match.delimiterIndex !== delimiterPattern.length - 1) {
2283
- match.delimiterIndex++;
2284
- textIndex++;
2285
- continue;
2286
- }
2520
+ // First match
2521
+ if (this.firstMatchIndex === -1) {
2522
+ this.firstMatchIndex = textIndex;
2523
+ }
2287
2524
 
2288
- // Full delimiter match
2289
- [textIndex, lookForOpenDelimiter] = this.fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters);
2525
+ // Partial match
2526
+ if (this.lookForDelimiterIndex !== delimiterPattern.length - 1) {
2527
+ this.lookForDelimiterIndex++;
2290
2528
  textIndex++;
2529
+ continue;
2291
2530
  }
2292
- it.next();
2531
+
2532
+ // Full delimiter match
2533
+ textIndex = this.fullMatch(it, textIndex, delimiters);
2534
+ textIndex++;
2293
2535
  }
2294
- return delimiters;
2295
2536
  }
2296
- noMatch(it, textIndex, match) {
2537
+ noMatch(it, textIndex) {
2297
2538
  //
2298
2539
  // Go back to first open node
2299
2540
  //
@@ -2302,57 +2543,76 @@ class DelimiterSearcher {
2302
2543
  // For instance:
2303
2544
  // Delimiter is '{!' and template text contains the string '{{!'
2304
2545
  //
2305
- if (match.firstMatchIndex !== -1) {
2306
- const node = first(match.openNodes);
2546
+ if (this.firstMatchIndex !== -1) {
2547
+ const node = first(this.matchOpenNodes);
2307
2548
  it.setCurrent(node);
2308
- textIndex = match.firstMatchIndex;
2549
+ textIndex = this.firstMatchIndex;
2309
2550
  }
2310
2551
 
2311
2552
  // Update state
2312
- match.reset();
2553
+ this.resetMatch();
2313
2554
  if (textIndex < it.node.textContent.length - 1) {
2314
- match.openNodes.push(it.node);
2555
+ this.matchOpenNodes.push(it.node);
2315
2556
  }
2316
2557
  return textIndex;
2317
2558
  }
2318
- fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters) {
2559
+ fullMatch(it, textIndex, delimiters) {
2319
2560
  // Move all delimiters characters to the same text node
2320
- if (match.openNodes.length > 1) {
2321
- const firstNode = first(match.openNodes);
2322
- const lastNode = last(match.openNodes);
2561
+ if (this.matchOpenNodes.length > 1) {
2562
+ const firstNode = first(this.matchOpenNodes);
2563
+ const lastNode = last(this.matchOpenNodes);
2323
2564
  officeMarkup.modify.joinTextNodesRange(firstNode, lastNode);
2324
2565
  textIndex += firstNode.textContent.length - it.node.textContent.length;
2325
2566
  it.setCurrent(firstNode);
2326
2567
  }
2327
2568
 
2328
2569
  // Store delimiter
2329
- const delimiterMark = this.createDelimiterMark(match, lookForOpenDelimiter);
2570
+ const delimiterMark = this.createCurrentDelimiterMark();
2330
2571
  delimiters.push(delimiterMark);
2331
2572
 
2332
2573
  // Update state
2333
- lookForOpenDelimiter = !lookForOpenDelimiter;
2334
- match.reset();
2574
+ this.lookForOpenDelimiter = !this.lookForOpenDelimiter;
2575
+ this.resetMatch();
2335
2576
  if (textIndex < it.node.textContent.length - 1) {
2336
- match.openNodes.push(it.node);
2577
+ this.matchOpenNodes.push(it.node);
2337
2578
  }
2338
- return [textIndex, lookForOpenDelimiter];
2339
- }
2340
- shouldSearchNode(it) {
2341
- if (!xml.query.isTextNode(it.node)) return false;
2342
- if (!it.node.textContent) return false;
2343
- if (!it.node.parentNode) return false;
2344
- if (!officeMarkup.query.isTextNode(it.node.parentNode)) return false;
2345
- return true;
2579
+ return textIndex;
2346
2580
  }
2347
- createDelimiterMark(match, isOpenDelimiter) {
2581
+ createCurrentDelimiterMark() {
2348
2582
  return {
2349
- index: match.firstMatchIndex,
2350
- isOpen: isOpenDelimiter,
2351
- xmlTextNode: match.openNodes[0]
2583
+ placement: TagPlacement.TextNode,
2584
+ isOpen: this.lookForOpenDelimiter,
2585
+ index: this.firstMatchIndex,
2586
+ xmlTextNode: this.matchOpenNodes[0]
2352
2587
  };
2353
2588
  }
2354
2589
  }
2355
2590
 
2591
+ class DelimiterSearcher {
2592
+ constructor(delimiters, maxXmlDepth) {
2593
+ if (!delimiters) {
2594
+ throw new InternalArgumentMissingError("delimiters");
2595
+ }
2596
+ if (!maxXmlDepth) {
2597
+ throw new InternalArgumentMissingError("maxXmlDepth");
2598
+ }
2599
+ this.delimiters = delimiters;
2600
+ this.maxXmlDepth = maxXmlDepth;
2601
+ }
2602
+ findDelimiters(node) {
2603
+ const delimiters = [];
2604
+ const it = new XmlTreeIterator(node, this.maxXmlDepth);
2605
+ const attributeSearcher = new AttributesDelimiterSearcher(this.delimiters);
2606
+ const textSearcher = new TextNodesDelimiterSearcher(this.delimiters.tagStart, this.delimiters.tagEnd);
2607
+ while (it.node) {
2608
+ attributeSearcher.processNode(it, delimiters);
2609
+ textSearcher.processNode(it, delimiters);
2610
+ it.next();
2611
+ }
2612
+ return delimiters;
2613
+ }
2614
+ }
2615
+
2356
2616
  class ScopeData {
2357
2617
  static defaultResolver(args) {
2358
2618
  let result;
@@ -2394,59 +2654,141 @@ class ScopeData {
2394
2654
  }
2395
2655
  }
2396
2656
 
2397
- const TagDisposition = Object.freeze({
2398
- Open: "Open",
2399
- Close: "Close",
2400
- SelfClosed: "SelfClosed"
2401
- });
2402
-
2403
2657
  class TagParser {
2404
2658
  constructor(delimiters) {
2405
2659
  if (!delimiters) throw new InternalArgumentMissingError("delimiters");
2406
2660
  this.delimiters = delimiters;
2407
- const tagOptionsRegex = `${Regex.escape(delimiters.tagOptionsStart)}(?<tagOptions>.*?)${Regex.escape(delimiters.tagOptionsEnd)}`;
2408
- this.tagRegex = new RegExp(`^${Regex.escape(delimiters.tagStart)}(?<tagName>.*?)(${tagOptionsRegex})?${Regex.escape(delimiters.tagEnd)}`, 'm');
2661
+ this.tagRegex = tagRegex(delimiters);
2409
2662
  }
2410
2663
  parse(delimiters) {
2411
2664
  const tags = [];
2412
- let openedTag;
2413
- let openedDelimiter;
2665
+ let openedTextDelimiter;
2666
+ let openedAttributeDelimiter;
2414
2667
  for (let i = 0; i < delimiters.length; i++) {
2415
- const delimiter = delimiters[i];
2416
-
2417
- // close before open
2418
- if (!openedTag && !delimiter.isOpen) {
2419
- const closeTagText = delimiter.xmlTextNode.textContent;
2420
- throw new MissingStartDelimiterError(closeTagText);
2668
+ if (delimiters[i].placement === TagPlacement.TextNode) {
2669
+ openedTextDelimiter = this.processDelimiter(delimiters, i, openedTextDelimiter, tags);
2670
+ continue;
2421
2671
  }
2422
-
2423
- // open before close
2424
- if (openedTag && delimiter.isOpen) {
2425
- const openTagText = openedDelimiter.xmlTextNode.textContent;
2426
- throw new MissingCloseDelimiterError(openTagText);
2672
+ if (delimiters[i].placement === TagPlacement.Attribute) {
2673
+ openedAttributeDelimiter = this.processDelimiter(delimiters, i, openedAttributeDelimiter, tags);
2674
+ continue;
2427
2675
  }
2676
+ throw new Error(`Unexpected delimiter placement value "${delimiters[i].placement}"`);
2677
+ }
2678
+ return tags;
2679
+ }
2680
+ processDelimiter(delimiters, i, openedDelimiter, tags) {
2681
+ const delimiter = delimiters[i];
2682
+
2683
+ // Close before open
2684
+ if (!openedDelimiter && !delimiter.isOpen) {
2685
+ const closeTagText = this.getPartialTagText(delimiter);
2686
+ throw new MissingStartDelimiterError(closeTagText);
2687
+ }
2688
+
2689
+ // Open before close
2690
+ if (openedDelimiter && delimiter.isOpen) {
2691
+ const openTagText = this.getPartialTagText(openedDelimiter);
2692
+ throw new MissingCloseDelimiterError(openTagText);
2693
+ }
2694
+
2695
+ // Valid open
2696
+ if (!openedDelimiter && delimiter.isOpen) {
2697
+ openedDelimiter = delimiter;
2698
+ }
2428
2699
 
2429
- // valid open
2430
- if (!openedTag && delimiter.isOpen) {
2431
- openedTag = {};
2432
- openedDelimiter = delimiter;
2700
+ // Valid close
2701
+ if (openedDelimiter && !delimiter.isOpen) {
2702
+ // Create the tag
2703
+ const partialTag = this.processDelimiterPair(openedDelimiter, delimiter, i, delimiters);
2704
+ const tag = this.populateTagFields(partialTag);
2705
+ tags.push(tag);
2706
+ openedDelimiter = null;
2707
+ }
2708
+ return openedDelimiter;
2709
+ }
2710
+ getPartialTagText(delimiter) {
2711
+ if (delimiter.placement === TagPlacement.TextNode) {
2712
+ return delimiter.xmlTextNode.textContent;
2713
+ }
2714
+ if (delimiter.placement === TagPlacement.Attribute) {
2715
+ return delimiter.xmlNode.attributes[delimiter.attributeName];
2716
+ }
2717
+ throw new Error(`Unexpected delimiter placement value "${delimiter.placement}"`);
2718
+ }
2719
+ processDelimiterPair(openDelimiter, closeDelimiter, closeDelimiterIndex, allDelimiters) {
2720
+ if (openDelimiter.placement === TagPlacement.TextNode && closeDelimiter.placement === TagPlacement.TextNode) {
2721
+ return this.processTextNodeDelimiterPair(openDelimiter, closeDelimiter, closeDelimiterIndex, allDelimiters);
2722
+ }
2723
+ if (openDelimiter.placement === TagPlacement.Attribute && closeDelimiter.placement === TagPlacement.Attribute) {
2724
+ return this.processAttributeDelimiterPair(openDelimiter, closeDelimiter);
2725
+ }
2726
+ throw new Error(`Unexpected delimiter placement values. Open delimiter: "${openDelimiter.placement}", Close delimiter: "${closeDelimiter.placement}"`);
2727
+ }
2728
+ processTextNodeDelimiterPair(openDelimiter, closeDelimiter, closeDelimiterIndex, allDelimiters) {
2729
+ // Verify tag delimiters are in the same paragraph
2730
+ const openTextNode = openDelimiter.xmlTextNode;
2731
+ const closeTextNode = closeDelimiter.xmlTextNode;
2732
+ const sameNode = openTextNode === closeTextNode;
2733
+ if (!sameNode) {
2734
+ const startParagraph = officeMarkup.query.containingParagraphNode(openTextNode);
2735
+ const endParagraph = officeMarkup.query.containingParagraphNode(closeTextNode);
2736
+ if (startParagraph !== endParagraph) {
2737
+ throw new MissingCloseDelimiterError(openTextNode.textContent);
2433
2738
  }
2739
+ }
2434
2740
 
2435
- // valid close
2436
- if (openedTag && !delimiter.isOpen) {
2437
- // normalize the underlying xml structure
2438
- // (make sure the tag's node only includes the tag's text)
2439
- this.normalizeTagNodes(openedDelimiter, delimiter, i, delimiters);
2440
- openedTag.xmlTextNode = openedDelimiter.xmlTextNode;
2441
-
2442
- // extract tag info from tag's text
2443
- this.processTag(openedTag);
2444
- tags.push(openedTag);
2445
- openedTag = null;
2446
- openedDelimiter = null;
2741
+ // Verify no inline drawing in the middle
2742
+ const startRun = officeMarkup.query.containingRunNode(openTextNode);
2743
+ const endRun = officeMarkup.query.containingRunNode(closeTextNode);
2744
+ let currentRun = startRun;
2745
+ while (currentRun && currentRun !== endRun) {
2746
+ const drawing = currentRun.childNodes?.find(child => child.nodeName === OmlNode.W.Drawing);
2747
+ if (!drawing) {
2748
+ currentRun = currentRun.nextSibling;
2749
+ continue;
2447
2750
  }
2751
+ const inline = drawing.childNodes?.find(child => child.nodeName === OmlNode.Wp.Inline);
2752
+ if (!inline) {
2753
+ currentRun = currentRun.nextSibling;
2754
+ continue;
2755
+ }
2756
+ throw new MissingCloseDelimiterError(openTextNode.textContent);
2448
2757
  }
2449
- return tags;
2758
+
2759
+ // Normalize the underlying xml structure
2760
+ // (make sure the tag's node only includes the tag's text)
2761
+ this.normalizeTextTagNodes(openDelimiter, closeDelimiter, closeDelimiterIndex, allDelimiters);
2762
+
2763
+ // Create the tag
2764
+ const tag = {
2765
+ placement: TagPlacement.TextNode,
2766
+ xmlTextNode: openDelimiter.xmlTextNode,
2767
+ rawText: openDelimiter.xmlTextNode.textContent
2768
+ };
2769
+ return tag;
2770
+ }
2771
+ processAttributeDelimiterPair(openDelimiter, closeDelimiter) {
2772
+ // Verify tag delimiters are in the same attribute
2773
+ const openNode = openDelimiter.xmlNode;
2774
+ const closeNode = closeDelimiter.xmlNode;
2775
+ if (openNode !== closeNode) {
2776
+ throw new MissingCloseDelimiterError(openNode.attributes[openDelimiter.attributeName]);
2777
+ }
2778
+ if (openDelimiter.attributeName !== closeDelimiter.attributeName) {
2779
+ throw new MissingCloseDelimiterError(openNode.attributes[openDelimiter.attributeName]);
2780
+ }
2781
+
2782
+ // Create the tag
2783
+ const attrValue = openNode.attributes[openDelimiter.attributeName];
2784
+ const tagText = attrValue.substring(openDelimiter.index, closeDelimiter.index + this.delimiters.tagEnd.length);
2785
+ const tag = {
2786
+ placement: TagPlacement.Attribute,
2787
+ xmlNode: openNode,
2788
+ attributeName: openDelimiter.attributeName,
2789
+ rawText: tagText
2790
+ };
2791
+ return tag;
2450
2792
  }
2451
2793
 
2452
2794
  /**
@@ -2457,19 +2799,12 @@ class TagParser {
2457
2799
  * Text node before: "some text {some tag} some more text"
2458
2800
  * Text nodes after: [ "some text ", "{some tag}", " some more text" ]
2459
2801
  */
2460
- normalizeTagNodes(openDelimiter, closeDelimiter, closeDelimiterIndex, allDelimiters) {
2802
+ normalizeTextTagNodes(openDelimiter, closeDelimiter, closeDelimiterIndex, allDelimiters) {
2461
2803
  let startTextNode = openDelimiter.xmlTextNode;
2462
2804
  let endTextNode = closeDelimiter.xmlTextNode;
2463
2805
  const sameNode = startTextNode === endTextNode;
2464
- if (!sameNode) {
2465
- const startParagraph = officeMarkup.query.containingParagraphNode(startTextNode);
2466
- const endParagraph = officeMarkup.query.containingParagraphNode(endTextNode);
2467
- if (startParagraph !== endParagraph) {
2468
- throw new MissingCloseDelimiterError(startTextNode.textContent);
2469
- }
2470
- }
2471
2806
 
2472
- // trim start
2807
+ // Trim start
2473
2808
  if (openDelimiter.index > 0) {
2474
2809
  officeMarkup.modify.splitTextNode(startTextNode, openDelimiter.index, true);
2475
2810
  if (sameNode) {
@@ -2477,7 +2812,7 @@ class TagParser {
2477
2812
  }
2478
2813
  }
2479
2814
 
2480
- // trim end
2815
+ // Trim end
2481
2816
  if (closeDelimiter.index < endTextNode.textContent.length - 1) {
2482
2817
  endTextNode = officeMarkup.modify.splitTextNode(endTextNode, closeDelimiter.index + this.delimiters.tagEnd.length, true);
2483
2818
  if (sameNode) {
@@ -2485,43 +2820,46 @@ class TagParser {
2485
2820
  }
2486
2821
  }
2487
2822
 
2488
- // join nodes
2823
+ // Join nodes
2489
2824
  if (!sameNode) {
2490
2825
  officeMarkup.modify.joinTextNodesRange(startTextNode, endTextNode);
2491
2826
  endTextNode = startTextNode;
2492
2827
  }
2493
2828
 
2494
- // update offsets of next delimiters
2829
+ // Update offsets of next delimiters
2495
2830
  for (let i = closeDelimiterIndex + 1; i < allDelimiters.length; i++) {
2496
2831
  let updated = false;
2497
2832
  const curDelimiter = allDelimiters[i];
2498
- if (curDelimiter.xmlTextNode === openDelimiter.xmlTextNode) {
2833
+ if (curDelimiter.placement === TagPlacement.TextNode && curDelimiter.xmlTextNode === openDelimiter.xmlTextNode) {
2499
2834
  curDelimiter.index -= openDelimiter.index;
2500
2835
  updated = true;
2501
2836
  }
2502
- if (curDelimiter.xmlTextNode === closeDelimiter.xmlTextNode) {
2837
+ if (curDelimiter.placement === TagPlacement.TextNode && curDelimiter.xmlTextNode === closeDelimiter.xmlTextNode) {
2503
2838
  curDelimiter.index -= closeDelimiter.index + this.delimiters.tagEnd.length;
2504
2839
  updated = true;
2505
2840
  }
2506
2841
  if (!updated) break;
2507
2842
  }
2508
2843
 
2509
- // update references
2844
+ // Update references
2510
2845
  openDelimiter.xmlTextNode = startTextNode;
2511
2846
  closeDelimiter.xmlTextNode = endTextNode;
2512
2847
  }
2513
- processTag(tag) {
2514
- tag.rawText = tag.xmlTextNode.textContent;
2515
- const tagParts = this.tagRegex.exec(tag.rawText);
2848
+ populateTagFields(partialTag) {
2849
+ if (!partialTag.rawText) {
2850
+ throw new InternalError("tag.rawText is required");
2851
+ }
2852
+ const tag = partialTag;
2853
+ const tagParts = tag.rawText.match(this.tagRegex);
2516
2854
  const tagName = (tagParts.groups?.["tagName"] || '').trim();
2517
2855
 
2518
- // Ignoring empty tags.
2856
+ // Ignoring empty tags
2519
2857
  if (!tagName?.length) {
2520
2858
  tag.disposition = TagDisposition.SelfClosed;
2521
- return;
2859
+ return tag;
2522
2860
  }
2523
2861
 
2524
- // Tag options.
2862
+ // Tag options
2525
2863
  const tagOptionsText = (tagParts.groups?.["tagOptions"] || '').trim();
2526
2864
  if (tagOptionsText) {
2527
2865
  try {
@@ -2531,23 +2869,24 @@ class TagParser {
2531
2869
  }
2532
2870
  }
2533
2871
 
2534
- // Container open tag.
2872
+ // Container open tag
2535
2873
  if (tagName.startsWith(this.delimiters.containerTagOpen)) {
2536
2874
  tag.disposition = TagDisposition.Open;
2537
2875
  tag.name = tagName.slice(this.delimiters.containerTagOpen.length).trim();
2538
- return;
2876
+ return tag;
2539
2877
  }
2540
2878
 
2541
- // Container close tag.
2879
+ // Container close tag
2542
2880
  if (tagName.startsWith(this.delimiters.containerTagClose)) {
2543
2881
  tag.disposition = TagDisposition.Close;
2544
2882
  tag.name = tagName.slice(this.delimiters.containerTagClose.length).trim();
2545
- return;
2883
+ return tag;
2546
2884
  }
2547
2885
 
2548
- // Self-closed tag.
2886
+ // Self-closed tag
2549
2887
  tag.disposition = TagDisposition.SelfClosed;
2550
2888
  tag.name = tagName;
2889
+ return tag;
2551
2890
  }
2552
2891
  }
2553
2892
 
@@ -2584,12 +2923,228 @@ class TemplatePlugin {
2584
2923
  }
2585
2924
  }
2586
2925
 
2926
+ function nameFromId(imageId) {
2927
+ return `Picture ${imageId}`;
2928
+ }
2929
+ function pixelsToEmu(pixels) {
2930
+ // https://stackoverflow.com/questions/20194403/openxml-distance-size-units
2931
+ // https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement
2932
+ // https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML
2933
+ // http://www.java2s.com/Code/CSharp/2D-Graphics/ConvertpixelstoEMUEMUtopixels.htm
2934
+
2935
+ return Math.round(pixels * 9525);
2936
+ }
2937
+ function transparencyPercentToAlpha(transparencyPercent) {
2938
+ if (transparencyPercent < 0 || transparencyPercent > 100) {
2939
+ throw new TemplateDataError(`Transparency percent must be between 0 and 100, but was ${transparencyPercent}.`);
2940
+ }
2941
+ return Math.round((100 - transparencyPercent) * 1000);
2942
+ }
2943
+
2944
+ function createImage(imageId, relId, content) {
2945
+ // http://officeopenxml.com/drwPicInline.php
2946
+
2947
+ //
2948
+ // Performance note:
2949
+ //
2950
+ // I've tried to improve the markup generation performance by parsing
2951
+ // the string once and caching the result (and of course customizing it
2952
+ // per image) but it made no change whatsoever (in both cases 1000 items
2953
+ // loop takes around 8 seconds on my machine) so I'm sticking with this
2954
+ // approach which I find to be more readable.
2955
+ //
2956
+
2957
+ const name = nameFromId(imageId);
2958
+ const markupText = `
2959
+ <w:drawing>
2960
+ <wp:inline distT="0" distB="0" distL="0" distR="0">
2961
+ <wp:extent cx="${pixelsToEmu(content.width)}" cy="${pixelsToEmu(content.height)}"/>
2962
+ <wp:effectExtent l="0" t="0" r="0" b="0"/>
2963
+ ${docProperties(imageId, name, content)}
2964
+ <wp:cNvGraphicFramePr>
2965
+ <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
2966
+ </wp:cNvGraphicFramePr>
2967
+ <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
2968
+ <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
2969
+ ${pictureMarkup(imageId, relId, name, content)}
2970
+ </a:graphicData>
2971
+ </a:graphic>
2972
+ </wp:inline>
2973
+ </w:drawing>
2974
+ `;
2975
+ const markupXml = xml.parser.parse(markupText);
2976
+ xml.modify.removeEmptyTextNodes(markupXml); // remove whitespace
2977
+
2978
+ return markupXml;
2979
+ }
2980
+ function docProperties(imageId, name, content) {
2981
+ if (content.altText) {
2982
+ return `<wp:docPr id="${imageId}" name="${name}" descr="${content.altText}"/>`;
2983
+ }
2984
+ return `
2985
+ <wp:docPr id="${imageId}" name="${name}">
2986
+ <a:extLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
2987
+ <a:ext uri="{C183D7F6-B498-43B3-948B-1728B52AA6E4}">
2988
+ <adec:decorative xmlns:adec="http://schemas.microsoft.com/office/drawing/2017/decorative" val="1"/>
2989
+ </a:ext>
2990
+ </a:extLst>
2991
+ </wp:docPr>
2992
+ `;
2993
+ }
2994
+ function pictureMarkup(imageId, relId, name, content) {
2995
+ // http://officeopenxml.com/drwPic.php
2996
+
2997
+ // Legend:
2998
+ // nvPicPr - non-visual picture properties - id, name, etc.
2999
+ // blipFill - binary large image (or) picture fill - image size, image fill, etc.
3000
+ // spPr - shape properties - frame size, frame fill, etc.
3001
+
3002
+ return `
3003
+ <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
3004
+ <pic:nvPicPr>
3005
+ <pic:cNvPr id="${imageId}" name="${name}"/>
3006
+ <pic:cNvPicPr>
3007
+ <a:picLocks noChangeAspect="1" noChangeArrowheads="1"/>
3008
+ </pic:cNvPicPr>
3009
+ </pic:nvPicPr>
3010
+ <pic:blipFill>
3011
+ <a:blip r:embed="${relId}">
3012
+ ${transparencyMarkup(content.transparencyPercent)}
3013
+ <a:extLst>
3014
+ <a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
3015
+ <a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
3016
+ </a:ext>
3017
+ </a:extLst>
3018
+ </a:blip>
3019
+ <a:srcRect/>
3020
+ <a:stretch>
3021
+ <a:fillRect/>
3022
+ </a:stretch>
3023
+ </pic:blipFill>
3024
+ <pic:spPr bwMode="auto">
3025
+ <a:xfrm>
3026
+ <a:off x="0" y="0"/>
3027
+ <a:ext cx="${pixelsToEmu(content.width)}" cy="${pixelsToEmu(content.height)}"/>
3028
+ </a:xfrm>
3029
+ <a:prstGeom prst="rect">
3030
+ <a:avLst/>
3031
+ </a:prstGeom>
3032
+ <a:noFill/>
3033
+ <a:ln>
3034
+ <a:noFill/>
3035
+ </a:ln>
3036
+ </pic:spPr>
3037
+ </pic:pic>
3038
+ `;
3039
+ }
3040
+ function transparencyMarkup(transparencyPercent) {
3041
+ if (transparencyPercent === null || transparencyPercent === undefined) {
3042
+ return '';
3043
+ }
3044
+ const alpha = transparencyPercentToAlpha(transparencyPercent);
3045
+ return `<a:alphaModFix amt="${alpha}" />`;
3046
+ }
3047
+
3048
+ function updateImage(drawingContainerNode, imageId, relId, content) {
3049
+ const inlineNode = xml.query.findByPath(drawingContainerNode, XmlNodeType.General, OmlNode.Wp.Inline);
3050
+ const floatingNode = xml.query.findByPath(drawingContainerNode, XmlNodeType.General, OmlNode.Wp.FloatingAnchor);
3051
+ const drawingNode = inlineNode || floatingNode;
3052
+ if (!inlineNode && !floatingNode) {
3053
+ throw new MalformedFileError("Invalid drawing container node. Expected inline or floating anchor node.");
3054
+ }
3055
+ const pictureNode = xml.query.findByPath(drawingNode, XmlNodeType.General, OmlNode.A.Graphic, OmlNode.A.GraphicData, OmlNode.Pic.Pic);
3056
+ if (!pictureNode) {
3057
+ throw new MalformedFileError("Invalid drawing container node. Expected picture node.");
3058
+ }
3059
+
3060
+ // Set rel ID
3061
+ setRelId(pictureNode, relId);
3062
+
3063
+ // Update non-visual properties
3064
+ updateNonVisualProps(drawingNode, pictureNode, imageId, content);
3065
+
3066
+ // Update size
3067
+ updateSize(drawingNode, pictureNode, content);
3068
+
3069
+ // Update transparency
3070
+ updateTransparency(pictureNode, content);
3071
+ }
3072
+ function setRelId(pictureNode, relId) {
3073
+ const blipNode = xml.query.findByPath(pictureNode, XmlNodeType.General, OmlNode.Pic.BlipFill, OmlNode.A.Blip);
3074
+ pictureNode.attributes["r:embed"] = relId;
3075
+ blipNode.attributes["r:embed"] = relId;
3076
+ }
3077
+ function updateNonVisualProps(drawingNode, pictureNode, imageId, content) {
3078
+ const docPrNode = xml.query.findByPath(drawingNode, XmlNodeType.General, OmlNode.Wp.DocPr);
3079
+ if (!docPrNode) {
3080
+ throw new MalformedFileError("Cannot find doc properties node.");
3081
+ }
3082
+ const nvPicPrNode = xml.query.findByPath(pictureNode, XmlNodeType.General, OmlNode.Pic.NvPicPr, OmlNode.Pic.CnVPr);
3083
+ if (!nvPicPrNode) {
3084
+ throw new MalformedFileError("Cannot find non-visual picture properties node.");
3085
+ }
3086
+ docPrNode.attributes["id"] = imageId.toString();
3087
+ nvPicPrNode.attributes["id"] = imageId.toString();
3088
+ const imageName = nameFromId(imageId);
3089
+ docPrNode.attributes["name"] = imageName;
3090
+ nvPicPrNode.attributes["name"] = imageName;
3091
+ if (content.altText) {
3092
+ docPrNode.attributes["descr"] = content.altText;
3093
+ nvPicPrNode.attributes["descr"] = content.altText;
3094
+ }
3095
+ }
3096
+ function updateSize(drawingNode, pictureNode, content) {
3097
+ if (typeof content.width !== 'number' && typeof content.height !== 'number') {
3098
+ return;
3099
+ }
3100
+ const drawingExtentNode = xml.query.findByPath(drawingNode, XmlNodeType.General, OmlNode.Wp.Extent);
3101
+ if (!drawingExtentNode) {
3102
+ throw new MalformedFileError("Cannot find drawing extent node.");
3103
+ }
3104
+ const pictureExtentNode = xml.query.findByPath(pictureNode, XmlNodeType.General, OmlNode.Pic.SpPr, OmlNode.Pic.Xfrm, OmlNode.Pic.Ext);
3105
+ if (!pictureExtentNode) {
3106
+ throw new MalformedFileError("Cannot find picture extent node.");
3107
+ }
3108
+ if (typeof content.width === 'number') {
3109
+ const widthEmu = pixelsToEmu(content.width);
3110
+ drawingExtentNode.attributes["cx"] = widthEmu.toString();
3111
+ pictureExtentNode.attributes["cx"] = widthEmu.toString();
3112
+ }
3113
+ if (typeof content.height === 'number') {
3114
+ const heightEmu = pixelsToEmu(content.height);
3115
+ drawingExtentNode.attributes["cy"] = heightEmu.toString();
3116
+ pictureExtentNode.attributes["cy"] = heightEmu.toString();
3117
+ }
3118
+ }
3119
+ function updateTransparency(pictureNode, content) {
3120
+ if (content.transparencyPercent === null || content.transparencyPercent === undefined) {
3121
+ return;
3122
+ }
3123
+ const blipNode = xml.query.findByPath(pictureNode, XmlNodeType.General, OmlNode.Pic.BlipFill, OmlNode.A.Blip);
3124
+ if (!blipNode) {
3125
+ throw new MalformedFileError("Cannot find blip node.");
3126
+ }
3127
+ let alphaNode = xml.query.findByPath(blipNode, XmlNodeType.General, OmlNode.A.AlphaModFix);
3128
+
3129
+ // If the alpha node is not present, create it
3130
+ if (!alphaNode) {
3131
+ alphaNode = xml.create.generalNode(OmlNode.A.AlphaModFix, {
3132
+ attributes: {}
3133
+ });
3134
+ xml.modify.insertChild(blipNode, alphaNode, 0);
3135
+ }
3136
+
3137
+ // Set the alpha value
3138
+ const alpha = transparencyPercentToAlpha(content.transparencyPercent);
3139
+ alphaNode.attributes["amt"] = alpha.toString();
3140
+ }
3141
+
2587
3142
  class ImagePlugin extends TemplatePlugin {
2588
3143
  contentType = 'image';
2589
3144
  async simpleTagReplacements(tag, data, context) {
2590
3145
  const content = data.getScopeData();
2591
3146
  if (!content || !content.source) {
2592
- officeMarkup.modify.removeTag(tag.xmlTextNode);
3147
+ officeMarkup.modify.removeTag(tag);
2593
3148
  return;
2594
3149
  }
2595
3150
 
@@ -2599,12 +3154,25 @@ class ImagePlugin extends TemplatePlugin {
2599
3154
  const relId = await context.currentPart.rels.add(mediaFilePath, relType);
2600
3155
  await context.docx.contentTypes.ensureContentType(content.format);
2601
3156
 
2602
- // Create the xml markup
3157
+ // Generate a unique image ID
2603
3158
  const imageId = await this.getNextImageId(context);
2604
- const imageXml = this.createMarkup(imageId, relId, content);
2605
- const wordTextNode = officeMarkup.query.containingTextNode(tag.xmlTextNode);
2606
- xml.modify.insertAfter(imageXml, wordTextNode);
2607
- officeMarkup.modify.removeTag(tag.xmlTextNode);
3159
+
3160
+ // For text tags, create xml markup from scratch
3161
+ if (tag.placement === TagPlacement.TextNode) {
3162
+ const imageXml = createImage(imageId, relId, content);
3163
+ const wordTextNode = officeMarkup.query.containingTextNode(tag.xmlTextNode);
3164
+ xml.modify.insertAfter(imageXml, wordTextNode);
3165
+ }
3166
+
3167
+ // For attribute tags, modify the existing markup
3168
+ if (tag.placement === TagPlacement.Attribute) {
3169
+ const drawingNode = xml.query.findParentByName(tag.xmlNode, OmlNode.W.Drawing);
3170
+ if (!drawingNode) {
3171
+ throw new TemplateSyntaxError(`Cannot find placeholder image for tag "${tag.rawText}".`);
3172
+ }
3173
+ updateImage(drawingNode, imageId, relId, content);
3174
+ }
3175
+ officeMarkup.modify.removeTag(tag);
2608
3176
  }
2609
3177
  async getNextImageId(context) {
2610
3178
  // Init plugin context.
@@ -2629,10 +3197,8 @@ class ImagePlugin extends TemplatePlugin {
2629
3197
  const maxDepth = context.options.maxXmlDepth;
2630
3198
 
2631
3199
  // Get all existing doc props IDs
2632
- // (docPr stands for "Drawing Object Non-Visual Properties", which isn't
2633
- // exactly a good acronym but that's how it's called nevertheless)
2634
3200
  const docProps = xml.query.descendants(partRoot, maxDepth, node => {
2635
- return xml.query.isGeneralNode(node) && node.nodeName === 'wp:docPr';
3201
+ return xml.query.isGeneralNode(node) && node.nodeName === OmlNode.Wp.DocPr;
2636
3202
  });
2637
3203
 
2638
3204
  // Start counting from the current max
@@ -2641,128 +3207,17 @@ class ImagePlugin extends TemplatePlugin {
2641
3207
  lastIdMap[lastIdKey] = maxId + 1;
2642
3208
  return lastIdMap[lastIdKey];
2643
3209
  }
2644
- createMarkup(imageId, relId, content) {
2645
- // http://officeopenxml.com/drwPicInline.php
2646
-
2647
- //
2648
- // Performance note:
2649
- //
2650
- // I've tried to improve the markup generation performance by parsing
2651
- // the string once and caching the result (and of course customizing it
2652
- // per image) but it made no change whatsoever (in both cases 1000 items
2653
- // loop takes around 8 seconds on my machine) so I'm sticking with this
2654
- // approach which I find to be more readable.
2655
- //
2656
-
2657
- const name = `Picture ${imageId}`;
2658
- const markupText = `
2659
- <w:drawing>
2660
- <wp:inline distT="0" distB="0" distL="0" distR="0">
2661
- <wp:extent cx="${this.pixelsToEmu(content.width)}" cy="${this.pixelsToEmu(content.height)}"/>
2662
- <wp:effectExtent l="0" t="0" r="0" b="0"/>
2663
- ${this.docProperties(imageId, name, content)}
2664
- <wp:cNvGraphicFramePr>
2665
- <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
2666
- </wp:cNvGraphicFramePr>
2667
- <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
2668
- <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
2669
- ${this.pictureMarkup(imageId, relId, name, content)}
2670
- </a:graphicData>
2671
- </a:graphic>
2672
- </wp:inline>
2673
- </w:drawing>
2674
- `;
2675
- const markupXml = xml.parser.parse(markupText);
2676
- xml.modify.removeEmptyTextNodes(markupXml); // remove whitespace
2677
-
2678
- return markupXml;
2679
- }
2680
- docProperties(imageId, name, content) {
2681
- if (content.altText) {
2682
- return `<wp:docPr id="${imageId}" name="${name}" descr="${content.altText}"/>`;
2683
- }
2684
- return `
2685
- <wp:docPr id="${imageId}" name="${name}">
2686
- <a:extLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
2687
- <a:ext uri="{C183D7F6-B498-43B3-948B-1728B52AA6E4}">
2688
- <adec:decorative xmlns:adec="http://schemas.microsoft.com/office/drawing/2017/decorative" val="1"/>
2689
- </a:ext>
2690
- </a:extLst>
2691
- </wp:docPr>
2692
- `;
2693
- }
2694
- pictureMarkup(imageId, relId, name, content) {
2695
- // http://officeopenxml.com/drwPic.php
2696
-
2697
- // Legend:
2698
- // nvPicPr - non-visual picture properties - id, name, etc.
2699
- // blipFill - binary large image (or) picture fill - image size, image fill, etc.
2700
- // spPr - shape properties - frame size, frame fill, etc.
2701
-
2702
- return `
2703
- <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
2704
- <pic:nvPicPr>
2705
- <pic:cNvPr id="${imageId}" name="${name}"/>
2706
- <pic:cNvPicPr>
2707
- <a:picLocks noChangeAspect="1" noChangeArrowheads="1"/>
2708
- </pic:cNvPicPr>
2709
- </pic:nvPicPr>
2710
- <pic:blipFill>
2711
- <a:blip r:embed="${relId}">
2712
- ${this.transparencyMarkup(content.transparencyPercent)}
2713
- <a:extLst>
2714
- <a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
2715
- <a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
2716
- </a:ext>
2717
- </a:extLst>
2718
- </a:blip>
2719
- <a:srcRect/>
2720
- <a:stretch>
2721
- <a:fillRect/>
2722
- </a:stretch>
2723
- </pic:blipFill>
2724
- <pic:spPr bwMode="auto">
2725
- <a:xfrm>
2726
- <a:off x="0" y="0"/>
2727
- <a:ext cx="${this.pixelsToEmu(content.width)}" cy="${this.pixelsToEmu(content.height)}"/>
2728
- </a:xfrm>
2729
- <a:prstGeom prst="rect">
2730
- <a:avLst/>
2731
- </a:prstGeom>
2732
- <a:noFill/>
2733
- <a:ln>
2734
- <a:noFill/>
2735
- </a:ln>
2736
- </pic:spPr>
2737
- </pic:pic>
2738
- `;
2739
- }
2740
- transparencyMarkup(transparencyPercent) {
2741
- if (transparencyPercent === null || transparencyPercent === undefined) {
2742
- return '';
2743
- }
2744
- if (transparencyPercent < 0 || transparencyPercent > 100) {
2745
- throw new TemplateDataError(`Transparency percent must be between 0 and 100, but was ${transparencyPercent}.`);
2746
- }
2747
- const alpha = Math.round((100 - transparencyPercent) * 1000);
2748
- return `<a:alphaModFix amt="${alpha}" />`;
2749
- }
2750
- pixelsToEmu(pixels) {
2751
- // https://stackoverflow.com/questions/20194403/openxml-distance-size-units
2752
- // https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement
2753
- // https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML
2754
- // http://www.java2s.com/Code/CSharp/2D-Graphics/ConvertpixelstoEMUEMUtopixels.htm
2755
-
2756
- return Math.round(pixels * 9525);
2757
- }
2758
3210
  }
2759
3211
 
2760
3212
  class LinkPlugin extends TemplatePlugin {
2761
3213
  contentType = 'link';
2762
3214
  async simpleTagReplacements(tag, data, context) {
3215
+ if (tag.placement !== TagPlacement.TextNode) {
3216
+ throw new TemplateSyntaxError(`Link tag "${tag.rawText}" must be placed in a text node but was placed in ${tag.placement}`);
3217
+ }
2763
3218
  const content = data.getScopeData();
2764
3219
  if (!content || !content.target) {
2765
- officeMarkup.modify.removeTag(tag.xmlTextNode);
3220
+ officeMarkup.modify.removeTag(tag);
2766
3221
  return;
2767
3222
  }
2768
3223
 
@@ -3157,6 +3612,12 @@ class LoopPlugin extends TemplatePlugin {
3157
3612
  // vars
3158
3613
  const openTag = tags[0];
3159
3614
  const closeTag = last(tags);
3615
+ if (openTag.placement !== TagPlacement.TextNode) {
3616
+ throw new TemplateSyntaxError(`Loop opening tag "${openTag.rawText}" must be placed in a text node but was placed in ${openTag.placement}`);
3617
+ }
3618
+ if (closeTag.placement !== TagPlacement.TextNode) {
3619
+ throw new TemplateSyntaxError(`Loop closing tag "${closeTag.rawText}" must be placed in a text node but was placed in ${closeTag.placement}`);
3620
+ }
3160
3621
 
3161
3622
  // select the suitable strategy
3162
3623
  const loopStrategy = this.loopStrategies.find(strategy => strategy.isApplicable(openTag, closeTag, isCondition));
@@ -3243,6 +3704,9 @@ class LoopPlugin extends TemplatePlugin {
3243
3704
  class RawXmlPlugin extends TemplatePlugin {
3244
3705
  contentType = 'rawXml';
3245
3706
  simpleTagReplacements(tag, data) {
3707
+ if (tag.placement !== TagPlacement.TextNode) {
3708
+ throw new TemplateSyntaxError(`RawXml tag "${tag.rawText}" must be placed in a text node but was placed in ${tag.placement}`);
3709
+ }
3246
3710
  const value = data.getScopeData();
3247
3711
  const replaceNode = value?.replaceParagraph ? officeMarkup.query.containingParagraphNode(tag.xmlTextNode) : officeMarkup.query.containingTextNode(tag.xmlTextNode);
3248
3712
  if (typeof value?.xml === 'string') {
@@ -3252,7 +3716,7 @@ class RawXmlPlugin extends TemplatePlugin {
3252
3716
  if (value?.replaceParagraph) {
3253
3717
  xml.modify.remove(replaceNode);
3254
3718
  } else {
3255
- officeMarkup.modify.removeTag(tag.xmlTextNode);
3719
+ officeMarkup.modify.removeTag(tag);
3256
3720
  }
3257
3721
  }
3258
3722
  }
@@ -3266,20 +3730,44 @@ class TextPlugin extends TemplatePlugin {
3266
3730
  */
3267
3731
  simpleTagReplacements(tag, data) {
3268
3732
  const value = data.getScopeData();
3269
- const lines = stringValue(value).split('\n');
3733
+ const strValue = stringValue(value);
3734
+ if (tag.placement === TagPlacement.TextNode) {
3735
+ this.replaceInTextNode(tag, strValue);
3736
+ return;
3737
+ }
3738
+ if (tag.placement === TagPlacement.Attribute) {
3739
+ this.replaceInAttribute(tag, strValue);
3740
+ return;
3741
+ }
3742
+ const anyTag = tag;
3743
+ throw new TemplateSyntaxError(`Unexpected tag placement "${anyTag.placement}" for tag "${anyTag.rawText}".`);
3744
+ }
3745
+ replaceInTextNode(tag, text) {
3746
+ const lines = text.split('\n');
3270
3747
  if (lines.length < 2) {
3271
- this.replaceSingleLine(tag.xmlTextNode, lines.length ? lines[0] : '');
3748
+ this.replaceSingleLine(tag, lines.length ? lines[0] : '');
3272
3749
  } else {
3273
3750
  this.replaceMultiLine(tag.xmlTextNode, lines);
3274
3751
  }
3275
3752
  }
3276
- replaceSingleLine(textNode, text) {
3753
+ replaceInAttribute(tag, text) {
3277
3754
  // Set text
3755
+ tag.xmlNode.attributes[tag.attributeName] = tag.xmlNode.attributes[tag.attributeName].replace(tag.rawText, text);
3756
+
3757
+ // Remove the attribute if it's empty
3758
+ if (!text) {
3759
+ officeMarkup.modify.removeTag(tag);
3760
+ return;
3761
+ }
3762
+ }
3763
+ replaceSingleLine(tag, text) {
3764
+ // Set text
3765
+ const textNode = tag.xmlTextNode;
3278
3766
  textNode.textContent = text;
3279
3767
 
3280
3768
  // Clean up if the text node is now empty
3281
3769
  if (!text) {
3282
- officeMarkup.modify.removeTag(textNode);
3770
+ officeMarkup.modify.removeTag(tag);
3283
3771
  return;
3284
3772
  }
3285
3773
 
@@ -3672,15 +4160,15 @@ async function updateChart(chartPart, chartData) {
3672
4160
  // Get the chart node
3673
4161
  const root = await chartPart.xmlRoot();
3674
4162
  if (root.nodeName !== "c:chartSpace") {
3675
- throw new Error(`Unexpected chart root node "${root.nodeName}"`);
4163
+ throw new MalformedFileError(`Unexpected chart root node "${root.nodeName}"`);
3676
4164
  }
3677
4165
  const chartWrapperNode = root.childNodes?.find(child => child.nodeName === "c:chart");
3678
4166
  if (!chartWrapperNode) {
3679
- throw new Error("Chart node not found");
4167
+ throw new MalformedFileError("Chart node not found");
3680
4168
  }
3681
4169
  const plotAreaNode = chartWrapperNode.childNodes?.find(child => child.nodeName === "c:plotArea");
3682
4170
  if (!plotAreaNode) {
3683
- throw new Error("Plot area node not found");
4171
+ throw new MalformedFileError("Plot area node not found");
3684
4172
  }
3685
4173
  const chartNode = plotAreaNode.childNodes?.find(child => Object.values(chartTypes).includes(child.nodeName));
3686
4174
  if (!chartNode) {
@@ -4450,13 +4938,16 @@ function parseXmlNode(xmlString) {
4450
4938
  class ChartPlugin extends TemplatePlugin {
4451
4939
  contentType = 'chart';
4452
4940
  async simpleTagReplacements(tag, data, context) {
4941
+ if (tag.placement !== TagPlacement.TextNode) {
4942
+ throw new TemplateSyntaxError(`Chart tag "${tag.rawText}" must be placed in a text node but was placed in ${tag.placement}`);
4943
+ }
4453
4944
  const chartNode = xml.query.findParentByName(tag.xmlTextNode, "c:chart");
4454
4945
  if (!chartNode) {
4455
- throw new TemplateSyntaxError("Chart tag not placed in chart title");
4946
+ throw new TemplateSyntaxError(`Chart tag "${tag.rawText}" must be placed in chart title`);
4456
4947
  }
4457
4948
  const content = data.getScopeData();
4458
4949
  if (!content) {
4459
- officeMarkup.modify.removeTag(tag.xmlTextNode);
4950
+ officeMarkup.modify.removeTag(tag);
4460
4951
  return;
4461
4952
  }
4462
4953
 
@@ -4464,7 +4955,7 @@ class ChartPlugin extends TemplatePlugin {
4464
4955
  if (content.title) {
4465
4956
  updateTitle(tag, content.title);
4466
4957
  } else {
4467
- officeMarkup.modify.removeTag(tag.xmlTextNode);
4958
+ officeMarkup.modify.removeTag(tag);
4468
4959
  }
4469
4960
  if (!chartHasData(content)) {
4470
4961
  return;
@@ -4654,7 +5145,7 @@ class Delimiters {
4654
5145
  constructor(initial) {
4655
5146
  Object.assign(this, initial);
4656
5147
  this.encodeAndValidate();
4657
- if (this.containerTagOpen === this.containerTagClose) throw new Error(`${"containerTagOpen"} can not be equal to ${"containerTagClose"}`);
5148
+ if (this.containerTagOpen === this.containerTagClose) throw new Error(`containerTagOpen can not be equal to containerTagClose`);
4658
5149
  }
4659
5150
  encodeAndValidate() {
4660
5151
  const keys = ['tagStart', 'tagEnd', 'containerTagOpen', 'containerTagClose'];
@@ -4697,19 +5188,17 @@ class TemplateHandler {
4697
5188
  /**
4698
5189
  * Version number of the `easy-template-x` library.
4699
5190
  */
4700
- version = "6.2.2" ;
5191
+ version = "7.0.0" ;
4701
5192
  constructor(options) {
4702
5193
  this.options = new TemplateHandlerOptions(options);
5194
+ const delimiters = this.options.delimiters;
4703
5195
 
4704
5196
  //
4705
- // this is the library's composition root
5197
+ // This is the library's composition root
4706
5198
  //
4707
5199
 
4708
- const delimiterSearcher = new DelimiterSearcher();
4709
- delimiterSearcher.startDelimiter = this.options.delimiters.tagStart;
4710
- delimiterSearcher.endDelimiter = this.options.delimiters.tagEnd;
4711
- delimiterSearcher.maxXmlDepth = this.options.maxXmlDepth;
4712
- const tagParser = new TagParser(this.options.delimiters);
5200
+ const delimiterSearcher = new DelimiterSearcher(delimiters, this.options.maxXmlDepth);
5201
+ const tagParser = new TagParser(delimiters);
4713
5202
  this.compiler = new TemplateCompiler(delimiterSearcher, tagParser, this.options.plugins, {
4714
5203
  skipEmptyTags: this.options.skipEmptyTags,
4715
5204
  defaultContentType: this.options.defaultContentType,
@@ -4733,14 +5222,14 @@ class TemplateHandler {
4733
5222
  }
4734
5223
 
4735
5224
  //
4736
- // public methods
5225
+ // Public methods
4737
5226
  //
4738
5227
 
4739
5228
  async process(templateFile, data) {
4740
- // load the docx file
5229
+ // Load the docx file
4741
5230
  const docx = await Docx.load(templateFile);
4742
5231
 
4743
- // prepare context
5232
+ // Prepare context
4744
5233
  const scopeData = new ScopeData(data);
4745
5234
  scopeData.scopeDataResolver = this.options.scopeDataResolver;
4746
5235
  const context = {
@@ -4755,18 +5244,18 @@ class TemplateHandler {
4755
5244
  for (const part of contentParts) {
4756
5245
  context.currentPart = part;
4757
5246
 
4758
- // extensions - before compilation
5247
+ // Extensions - before compilation
4759
5248
  await this.callExtensions(this.options.extensions?.beforeCompilation, scopeData, context);
4760
5249
 
4761
- // compilation (do replacements)
5250
+ // Compilation (do replacements)
4762
5251
  const xmlRoot = await part.xmlRoot();
4763
5252
  await this.compiler.compile(xmlRoot, scopeData, context);
4764
5253
 
4765
- // extensions - after compilation
5254
+ // Extensions - after compilation
4766
5255
  await this.callExtensions(this.options.extensions?.afterCompilation, scopeData, context);
4767
5256
  }
4768
5257
 
4769
- // export the result
5258
+ // Export the result
4770
5259
  return docx.export();
4771
5260
  }
4772
5261
  async parseTags(templateFile) {
@@ -4828,7 +5317,7 @@ class TemplateHandler {
4828
5317
  }
4829
5318
 
4830
5319
  //
4831
- // private methods
5320
+ // Private methods
4832
5321
  //
4833
5322
 
4834
5323
  async callExtensions(extensions, scopeData, context) {
@@ -4874,6 +5363,7 @@ exports.TEXT_NODE_NAME = TEXT_NODE_NAME;
4874
5363
  exports.TagDisposition = TagDisposition;
4875
5364
  exports.TagOptionsParseError = TagOptionsParseError;
4876
5365
  exports.TagParser = TagParser;
5366
+ exports.TagPlacement = TagPlacement;
4877
5367
  exports.TemplateCompiler = TemplateCompiler;
4878
5368
  exports.TemplateDataError = TemplateDataError;
4879
5369
  exports.TemplateExtension = TemplateExtension;