easy-template-x 3.2.0 → 4.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 (108) hide show
  1. package/README.md +38 -2
  2. package/dist/cjs/easy-template-x.js +184 -98
  3. package/dist/es/easy-template-x.js +177 -89
  4. package/dist/types/compilation/delimiterMark.d.ts +6 -6
  5. package/dist/types/compilation/delimiterSearcher.d.ts +16 -16
  6. package/dist/types/compilation/index.d.ts +7 -7
  7. package/dist/types/compilation/scopeData.d.ts +21 -21
  8. package/dist/types/compilation/tag.d.ts +14 -12
  9. package/dist/types/compilation/tagParser.d.ts +13 -13
  10. package/dist/types/compilation/templateCompiler.d.ts +25 -25
  11. package/dist/types/compilation/templateContext.d.ts +5 -5
  12. package/dist/types/delimiters.d.ts +10 -8
  13. package/dist/types/errors/index.d.ts +11 -10
  14. package/dist/types/errors/malformedFileError.d.ts +4 -4
  15. package/dist/types/errors/maxXmlDepthError.d.ts +4 -4
  16. package/dist/types/errors/missingArgumentError.d.ts +4 -4
  17. package/dist/types/errors/missingCloseDelimiterError.d.ts +4 -4
  18. package/dist/types/errors/missingStartDelimiterError.d.ts +4 -4
  19. package/dist/types/errors/tagOptionsParseError.d.ts +5 -0
  20. package/dist/types/errors/unclosedTagError.d.ts +4 -4
  21. package/dist/types/errors/unidentifiedFileTypeError.d.ts +3 -3
  22. package/dist/types/errors/unknownContentTypeError.d.ts +6 -6
  23. package/dist/types/errors/unopenedTagError.d.ts +4 -4
  24. package/dist/types/errors/unsupportedFileTypeError.d.ts +4 -4
  25. package/dist/types/extensions/extensionOptions.d.ts +5 -5
  26. package/dist/types/extensions/index.d.ts +2 -2
  27. package/dist/types/extensions/templateExtension.d.ts +14 -14
  28. package/dist/types/index.d.ts +13 -13
  29. package/dist/types/mimeType.d.ts +11 -11
  30. package/dist/types/office/contentPartType.d.ts +9 -9
  31. package/dist/types/office/contentTypesFile.d.ts +16 -16
  32. package/dist/types/office/docx.d.ts +28 -28
  33. package/dist/types/office/docxParser.d.ts +35 -35
  34. package/dist/types/office/index.d.ts +4 -4
  35. package/dist/types/office/mediaFiles.d.ts +14 -14
  36. package/dist/types/office/relationship.d.ts +11 -11
  37. package/dist/types/office/rels.d.ts +20 -20
  38. package/dist/types/office/xmlPart.d.ts +14 -14
  39. package/dist/types/plugins/defaultPlugins.d.ts +2 -2
  40. package/dist/types/plugins/image/imageContent.d.ts +12 -12
  41. package/dist/types/plugins/image/imagePlugin.d.ts +10 -10
  42. package/dist/types/plugins/image/index.d.ts +2 -2
  43. package/dist/types/plugins/index.d.ts +8 -8
  44. package/dist/types/plugins/link/index.d.ts +2 -2
  45. package/dist/types/plugins/link/linkContent.d.ts +7 -7
  46. package/dist/types/plugins/link/linkPlugin.d.ts +9 -9
  47. package/dist/types/plugins/loop/index.d.ts +1 -1
  48. package/dist/types/plugins/loop/loopPlugin.d.ts +13 -13
  49. package/dist/types/plugins/loop/loopTagOptions.d.ts +7 -0
  50. package/dist/types/plugins/loop/strategy/iLoopStrategy.d.ts +14 -14
  51. package/dist/types/plugins/loop/strategy/index.d.ts +4 -4
  52. package/dist/types/plugins/loop/strategy/loopListStrategy.d.ts +11 -11
  53. package/dist/types/plugins/loop/strategy/loopParagraphStrategy.d.ts +11 -11
  54. package/dist/types/plugins/loop/strategy/loopTableStrategy.d.ts +11 -11
  55. package/dist/types/plugins/pluginContent.d.ts +6 -6
  56. package/dist/types/plugins/rawXml/index.d.ts +2 -2
  57. package/dist/types/plugins/rawXml/rawXmlContent.d.ts +6 -6
  58. package/dist/types/plugins/rawXml/rawXmlPlugin.d.ts +6 -6
  59. package/dist/types/plugins/templatePlugin.d.ts +15 -15
  60. package/dist/types/plugins/text/index.d.ts +1 -1
  61. package/dist/types/plugins/text/textPlugin.d.ts +11 -11
  62. package/dist/types/templateData.d.ts +6 -6
  63. package/dist/types/templateHandler.d.ts +20 -20
  64. package/dist/types/templateHandlerOptions.d.ts +15 -15
  65. package/dist/types/types.d.ts +4 -6
  66. package/dist/types/utils/array.d.ts +6 -6
  67. package/dist/types/utils/base64.d.ts +3 -3
  68. package/dist/types/utils/binary.d.ts +11 -12
  69. package/dist/types/utils/index.d.ts +9 -9
  70. package/dist/types/utils/number.d.ts +1 -1
  71. package/dist/types/utils/path.d.ts +5 -5
  72. package/dist/types/utils/regex.d.ts +3 -3
  73. package/dist/types/utils/sha1.d.ts +1 -1
  74. package/dist/types/utils/txt.d.ts +2 -1
  75. package/dist/types/utils/types.d.ts +3 -3
  76. package/dist/types/xml/index.d.ts +3 -3
  77. package/dist/types/xml/xmlDepthTracker.d.ts +7 -7
  78. package/dist/types/xml/xmlNode.d.ts +58 -49
  79. package/dist/types/xml/xmlParser.d.ts +8 -8
  80. package/dist/types/zip/index.d.ts +2 -2
  81. package/dist/types/zip/jsZipHelper.d.ts +7 -7
  82. package/dist/types/zip/zip.d.ts +13 -13
  83. package/dist/types/zip/zipObject.d.ts +13 -13
  84. package/package.json +25 -19
  85. package/src/compilation/tag.ts +4 -2
  86. package/src/compilation/tagParser.ts +32 -13
  87. package/src/delimiters.ts +3 -1
  88. package/src/errors/index.ts +1 -0
  89. package/src/errors/malformedFileError.ts +1 -4
  90. package/src/errors/maxXmlDepthError.ts +1 -4
  91. package/src/errors/missingArgumentError.ts +1 -4
  92. package/src/errors/missingCloseDelimiterError.ts +1 -4
  93. package/src/errors/missingStartDelimiterError.ts +1 -4
  94. package/src/errors/tagOptionsParseError.ts +12 -0
  95. package/src/errors/unclosedTagError.ts +1 -4
  96. package/src/errors/unidentifiedFileTypeError.ts +1 -4
  97. package/src/errors/unknownContentTypeError.ts +1 -4
  98. package/src/errors/unopenedTagError.ts +1 -4
  99. package/src/errors/unsupportedFileTypeError.ts +1 -4
  100. package/src/office/docx.ts +4 -1
  101. package/src/office/docxParser.ts +3 -1
  102. package/src/plugins/loop/loopPlugin.ts +1 -1
  103. package/src/plugins/loop/loopTagOptions.ts +14 -0
  104. package/src/plugins/loop/strategy/loopTableStrategy.ts +22 -3
  105. package/src/plugins/templatePlugin.ts +0 -2
  106. package/src/types.ts +1 -3
  107. package/src/utils/txt.ts +29 -1
  108. package/src/xml/xmlNode.ts +50 -14
@@ -1,49 +1,58 @@
1
- import { IMap } from '../types';
2
- export declare enum XmlNodeType {
3
- Text = "Text",
4
- General = "General"
5
- }
6
- export declare type XmlNode = XmlTextNode | XmlGeneralNode;
7
- export interface XmlNodeBase {
8
- nodeType: XmlNodeType;
9
- nodeName: string;
10
- parentNode?: XmlNode;
11
- childNodes?: XmlNode[];
12
- nextSibling?: XmlNode;
13
- }
14
- export declare const TEXT_NODE_NAME = "#text";
15
- export interface XmlTextNode extends XmlNodeBase {
16
- nodeType: XmlNodeType.Text;
17
- nodeName: typeof TEXT_NODE_NAME;
18
- textContent: string;
19
- }
20
- export interface XmlGeneralNode extends XmlNodeBase {
21
- nodeType: XmlNodeType.General;
22
- attributes?: IMap<string>;
23
- }
24
- export declare const XmlNode: {
25
- createTextNode(text?: string): XmlTextNode;
26
- createGeneralNode(name: string): XmlGeneralNode;
27
- encodeValue(str: string): string;
28
- serialize(node: XmlNode): string;
29
- fromDomNode(domNode: Node): XmlNode;
30
- isTextNode(node: XmlNode): node is XmlTextNode;
31
- cloneNode<T extends XmlNode>(node: T, deep: boolean): T;
32
- insertBefore(newNode: XmlNode, referenceNode: XmlNode): void;
33
- insertAfter(newNode: XmlNode, referenceNode: XmlNode): void;
34
- insertChild(parent: XmlNode, child: XmlNode, childIndex: number): void;
35
- appendChild(parent: XmlNode, child: XmlNode): void;
36
- remove(node: XmlNode): void;
37
- removeChild: typeof removeChild;
38
- lastTextChild(node: XmlNode): XmlTextNode;
39
- removeSiblings(from: XmlNode, to: XmlNode): XmlNode[];
40
- splitByChild(parent: XmlNode, child: XmlNode, removeChild: boolean): [XmlNode, XmlNode];
41
- findParent(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode;
42
- findParentByName(node: XmlNode, nodeName: string): XmlNode;
43
- findChildByName(node: XmlNode, childName: string): XmlNode;
44
- siblingsInRange(firstNode: XmlNode, lastNode: XmlNode): XmlNode[];
45
- removeEmptyTextNodes(node: XmlNode): void;
46
- };
47
- declare function removeChild(parent: XmlNode, child: XmlNode): XmlNode;
48
- declare function removeChild(parent: XmlNode, childIndex: number): XmlNode;
49
- export {};
1
+ import { IMap } from '../types';
2
+ export declare enum XmlNodeType {
3
+ Text = "Text",
4
+ General = "General",
5
+ Comment = "Comment"
6
+ }
7
+ export type XmlNode = XmlTextNode | XmlGeneralNode | XmlCommentNode;
8
+ export interface XmlNodeBase {
9
+ nodeType: XmlNodeType;
10
+ nodeName: string;
11
+ parentNode?: XmlNode;
12
+ childNodes?: XmlNode[];
13
+ nextSibling?: XmlNode;
14
+ }
15
+ export declare const TEXT_NODE_NAME = "#text";
16
+ export declare const COMMENT_NODE_NAME = "#comment";
17
+ export interface XmlTextNode extends XmlNodeBase {
18
+ nodeType: XmlNodeType.Text;
19
+ nodeName: typeof TEXT_NODE_NAME;
20
+ textContent: string;
21
+ }
22
+ export interface XmlCommentNode extends XmlNodeBase {
23
+ nodeType: XmlNodeType.Comment;
24
+ nodeName: typeof COMMENT_NODE_NAME;
25
+ commentContent: string;
26
+ }
27
+ export interface XmlGeneralNode extends XmlNodeBase {
28
+ nodeType: XmlNodeType.General;
29
+ attributes?: IMap<string>;
30
+ }
31
+ export declare const XmlNode: {
32
+ createTextNode(text?: string): XmlTextNode;
33
+ createGeneralNode(name: string): XmlGeneralNode;
34
+ createCommentNode(text?: string): XmlCommentNode;
35
+ encodeValue(str: string): string;
36
+ serialize(node: XmlNode): string;
37
+ fromDomNode(domNode: Node): XmlNode;
38
+ isTextNode(node: XmlNode): node is XmlTextNode;
39
+ isCommentNode(node: XmlNode): node is XmlCommentNode;
40
+ cloneNode<T extends XmlNode>(node: T, deep: boolean): T;
41
+ insertBefore(newNode: XmlNode, referenceNode: XmlNode): void;
42
+ insertAfter(newNode: XmlNode, referenceNode: XmlNode): void;
43
+ insertChild(parent: XmlNode, child: XmlNode, childIndex: number): void;
44
+ appendChild(parent: XmlNode, child: XmlNode): void;
45
+ remove(node: XmlNode): void;
46
+ removeChild: typeof removeChild;
47
+ lastTextChild(node: XmlNode): XmlTextNode;
48
+ removeSiblings(from: XmlNode, to: XmlNode): XmlNode[];
49
+ splitByChild(parent: XmlNode, child: XmlNode, removeChild: boolean): [XmlNode, XmlNode];
50
+ findParent(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode;
51
+ findParentByName(node: XmlNode, nodeName: string): XmlNode;
52
+ findChildByName(node: XmlNode, childName: string): XmlNode;
53
+ siblingsInRange(firstNode: XmlNode, lastNode: XmlNode): XmlNode[];
54
+ removeEmptyTextNodes(node: XmlNode): void;
55
+ };
56
+ declare function removeChild(parent: XmlNode, child: XmlNode): XmlNode;
57
+ declare function removeChild(parent: XmlNode, childIndex: number): XmlNode;
58
+ export {};
@@ -1,8 +1,8 @@
1
- import { XmlNode } from './xmlNode';
2
- export declare class XmlParser {
3
- private static xmlHeader;
4
- private static readonly parser;
5
- parse(str: string): XmlNode;
6
- domParse(str: string): Document;
7
- serialize(xmlNode: XmlNode): string;
8
- }
1
+ import { XmlNode } from './xmlNode';
2
+ export declare class XmlParser {
3
+ private static xmlHeader;
4
+ private static readonly parser;
5
+ parse(str: string): XmlNode;
6
+ domParse(str: string): Document;
7
+ serialize(xmlNode: XmlNode): string;
8
+ }
@@ -1,2 +1,2 @@
1
- export * from './zip';
2
- export * from './zipObject';
1
+ export * from './zip';
2
+ export * from './zipObject';
@@ -1,7 +1,7 @@
1
- import * as JSZip from 'jszip';
2
- import { Constructor } from '../types';
3
- import { Binary } from '../utils';
4
- export declare class JsZipHelper {
5
- static toJsZipOutputType(binary: Binary): JSZip.OutputType;
6
- static toJsZipOutputType(binaryType: Constructor<Binary>): JSZip.OutputType;
7
- }
1
+ import * as JSZip from 'jszip';
2
+ import { Constructor } from '../types';
3
+ import { Binary } from '../utils';
4
+ export declare class JsZipHelper {
5
+ static toJsZipOutputType(binary: Binary): JSZip.OutputType;
6
+ static toJsZipOutputType(binaryType: Constructor<Binary>): JSZip.OutputType;
7
+ }
@@ -1,13 +1,13 @@
1
- import { Constructor } from '../types';
2
- import { Binary } from '../utils';
3
- import { ZipObject } from './zipObject';
4
- export declare class Zip {
5
- private readonly zip;
6
- static load(file: Binary): Promise<Zip>;
7
- private constructor();
8
- getFile(path: string): ZipObject;
9
- setFile(path: string, content: string | Binary): void;
10
- isFileExist(path: string): boolean;
11
- listFiles(): string[];
12
- export<T extends Binary>(outputType: Constructor<T>): Promise<T>;
13
- }
1
+ import { Constructor } from '../types';
2
+ import { Binary } from '../utils';
3
+ import { ZipObject } from './zipObject';
4
+ export declare class Zip {
5
+ private readonly zip;
6
+ static load(file: Binary): Promise<Zip>;
7
+ private constructor();
8
+ getFile(path: string): ZipObject;
9
+ setFile(path: string, content: string | Binary): void;
10
+ isFileExist(path: string): boolean;
11
+ listFiles(): string[];
12
+ export<T extends Binary>(outputType: Constructor<T>): Promise<T>;
13
+ }
@@ -1,13 +1,13 @@
1
- import * as JSZip from 'jszip';
2
- import { Constructor } from '../types';
3
- import { Binary } from '../utils';
4
- export declare class ZipObject {
5
- private readonly zipObject;
6
- get name(): string;
7
- set name(value: string);
8
- get isDirectory(): boolean;
9
- constructor(zipObject: JSZip.JSZipObject);
10
- getContentText(): Promise<string>;
11
- getContentBase64(): Promise<string>;
12
- getContentBinary<T extends Binary>(outputType: Constructor<T>): Promise<T>;
13
- }
1
+ import * as JSZip from 'jszip';
2
+ import { Constructor } from '../types';
3
+ import { Binary } from '../utils';
4
+ export declare class ZipObject {
5
+ private readonly zipObject;
6
+ get name(): string;
7
+ set name(value: string);
8
+ get isDirectory(): boolean;
9
+ constructor(zipObject: JSZip.JSZipObject);
10
+ getContentText(): Promise<string>;
11
+ getContentBase64(): Promise<string>;
12
+ getContentBinary<T extends Binary>(outputType: Constructor<T>): Promise<T>;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-template-x",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "Generate docx documents from templates, in Node or in the browser.",
5
5
  "keywords": [
6
6
  "docx",
@@ -20,6 +20,7 @@
20
20
  "bugs": {
21
21
  "url": "https://github.com/alonrbar/easy-template-x/issues"
22
22
  },
23
+ "type": "module",
23
24
  "main": "dist/cjs/easy-template-x.js",
24
25
  "module": "dist/es/easy-template-x.js",
25
26
  "typings": "dist/types/index.d.ts",
@@ -38,39 +39,44 @@
38
39
  "build": "yarn build-types && yarn build-src",
39
40
  "release": "yarn clean && yarn quality && yarn build"
40
41
  },
42
+ "packageManager": "yarn@4.3.1",
41
43
  "dependencies": {
42
- "@xmldom/xmldom": "0.8.7",
44
+ "@xmldom/xmldom": "0.8.10",
45
+ "json5": "2.2.3",
43
46
  "jszip": "3.10.1",
44
47
  "lodash.get": "4.4.2"
45
48
  },
46
49
  "devDependencies": {
47
- "@babel/core": "7.18.6",
50
+ "@babel/core": "7.24.7",
48
51
  "@babel/plugin-proposal-class-properties": "7.18.6",
49
52
  "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
50
53
  "@babel/plugin-proposal-object-rest-spread": "7.18.6",
51
54
  "@babel/plugin-proposal-optional-catch-binding": "7.18.6",
52
55
  "@babel/plugin-proposal-optional-chaining": "7.18.6",
53
- "@babel/plugin-transform-modules-commonjs": "7.18.6",
54
- "@babel/preset-typescript": "7.18.6",
55
- "@rollup/plugin-replace": "4.0.0",
56
- "@types/jest": "28.1.4",
56
+ "@babel/plugin-transform-modules-commonjs": "7.24.7",
57
+ "@babel/preset-typescript": "7.24.7",
58
+ "@eslint/js": "9.6.0",
59
+ "@rollup/plugin-replace": "5.0.7",
60
+ "@types/eslint__js": "8.42.3",
61
+ "@types/jest": "29.5.12",
57
62
  "@types/jszip": "3.4.1",
58
- "@types/node": "18.0.1",
59
- "@types/ts-nameof": "4.2.1",
60
- "@typescript-eslint/eslint-plugin": "5.30.4",
61
- "@typescript-eslint/parser": "5.30.4",
62
- "babel-jest": "28.1.2",
63
- "babel-loader": "8.2.5",
63
+ "@types/node": "20.14.9",
64
+ "@types/ts-nameof": "4.2.5",
65
+ "babel-jest": "29.7.0",
66
+ "babel-loader": "9.1.3",
64
67
  "babel-plugin-ts-nameof": "4.2.1",
65
- "eslint": "8.19.0",
66
- "jest": "28.1.2",
67
- "jest-junit": "14.0.0",
68
+ "eslint": "9.6.0",
69
+ "globals": "15.8.0",
70
+ "jest": "29.7.0",
71
+ "jest-html-reporters": "3.1.7",
72
+ "jest-junit": "16.0.0",
68
73
  "lorem-ipsum": "2.0.8",
69
- "rimraf": "3.0.2",
70
- "rollup": "2.75.7",
74
+ "rimraf": "5.0.7",
75
+ "rollup": "4.18.0",
71
76
  "rollup-plugin-auto-external": "2.0.0",
72
77
  "rollup-plugin-babel": "4.4.0",
73
78
  "rollup-plugin-node-resolve": "5.2.0",
74
- "typescript": "4.7.4"
79
+ "typescript": "5.5.3",
80
+ "typescript-eslint": "7.15.0"
75
81
  }
76
82
  }
@@ -1,3 +1,4 @@
1
+ import { IMap } from '../types';
1
2
  import { XmlTextNode } from '../xml';
2
3
 
3
4
  export enum TagDisposition {
@@ -6,12 +7,13 @@ export enum TagDisposition {
6
7
  SelfClosed = "SelfClosed"
7
8
  }
8
9
 
9
- export interface Tag {
10
+ export interface Tag {
10
11
  name: string;
12
+ options?: IMap<any>;
11
13
  /**
12
14
  * The full tag text, for instance: "{#my-tag}".
13
15
  */
14
16
  rawText: string;
15
17
  disposition: TagDisposition;
16
18
  xmlTextNode: XmlTextNode;
17
- }
19
+ }
@@ -1,7 +1,8 @@
1
+ import * as JSON5 from 'json5';
1
2
  import { Delimiters } from '../delimiters';
2
- import { MissingArgumentError, MissingCloseDelimiterError, MissingStartDelimiterError } from '../errors';
3
+ import { MissingArgumentError, MissingCloseDelimiterError, MissingStartDelimiterError, TagOptionsParseError } from '../errors';
3
4
  import { DocxParser } from '../office';
4
- import { Regex } from '../utils';
5
+ import { normalizeDoubleQuotes, Regex } from '../utils';
5
6
  import { DelimiterMark } from './delimiterMark';
6
7
  import { Tag, TagDisposition } from './tag';
7
8
 
@@ -18,7 +19,8 @@ export class TagParser {
18
19
  if (!delimiters)
19
20
  throw new MissingArgumentError(nameof(delimiters));
20
21
 
21
- this.tagRegex = new RegExp(`^${Regex.escape(delimiters.tagStart)}(.*?)${Regex.escape(delimiters.tagEnd)}`, 'm');
22
+ const tagOptionsRegex = `${Regex.escape(delimiters.tagOptionsStart)}(?<tagOptions>.*?)${Regex.escape(delimiters.tagOptionsEnd)}`;
23
+ this.tagRegex = new RegExp(`^${Regex.escape(delimiters.tagStart)}(?<tagName>.*?)(${tagOptionsRegex})?${Regex.escape(delimiters.tagEnd)}`, 'm');
22
24
  }
23
25
 
24
26
  public parse(delimiters: DelimiterMark[]): Tag[] {
@@ -136,23 +138,40 @@ export class TagParser {
136
138
  tag.rawText = tag.xmlTextNode.textContent;
137
139
 
138
140
  const tagParts = this.tagRegex.exec(tag.rawText);
139
- const tagContent = (tagParts[1] || '').trim();
140
- if (!tagContent || !tagContent.length) {
141
+ const tagName = (tagParts.groups?.["tagName"] || '').trim();
142
+
143
+ // Ignoring empty tags.
144
+ if (!tagName?.length) {
141
145
  tag.disposition = TagDisposition.SelfClosed;
142
146
  return;
143
147
  }
144
148
 
145
- if (tagContent.startsWith(this.delimiters.containerTagOpen)) {
149
+ // Tag options.
150
+ const tagOptionsText = (tagParts.groups?.["tagOptions"] || '').trim();
151
+ if (tagOptionsText) {
152
+ try {
153
+ tag.options = JSON5.parse("{" + normalizeDoubleQuotes(tagOptionsText) + "}");
154
+ } catch (e) {
155
+ throw new TagOptionsParseError(tag.rawText, e);
156
+ }
157
+ }
158
+
159
+ // Container open tag.
160
+ if (tagName.startsWith(this.delimiters.containerTagOpen)) {
146
161
  tag.disposition = TagDisposition.Open;
147
- tag.name = tagContent.slice(this.delimiters.containerTagOpen.length).trim();
162
+ tag.name = tagName.slice(this.delimiters.containerTagOpen.length).trim();
163
+ return;
164
+ }
148
165
 
149
- } else if (tagContent.startsWith(this.delimiters.containerTagClose)) {
166
+ // Container close tag.
167
+ if (tagName.startsWith(this.delimiters.containerTagClose)) {
150
168
  tag.disposition = TagDisposition.Close;
151
- tag.name = tagContent.slice(this.delimiters.containerTagClose.length).trim();
152
-
153
- } else {
154
- tag.disposition = TagDisposition.SelfClosed;
155
- tag.name = tagContent;
169
+ tag.name = tagName.slice(this.delimiters.containerTagClose.length).trim();
170
+ return;
156
171
  }
172
+
173
+ // Self-closed tag.
174
+ tag.disposition = TagDisposition.SelfClosed;
175
+ tag.name = tagName;
157
176
  }
158
177
  }
package/src/delimiters.ts CHANGED
@@ -5,6 +5,8 @@ export class Delimiters {
5
5
  public tagEnd = "}";
6
6
  public containerTagOpen = "#";
7
7
  public containerTagClose = "/";
8
+ public tagOptionsStart = "[";
9
+ public tagOptionsEnd = "]";
8
10
 
9
11
  constructor(initial?: Partial<Delimiters>) {
10
12
  Object.assign(this, initial);
@@ -27,4 +29,4 @@ export class Delimiters {
27
29
  throw new Error(`${key} can not contain leading or trailing whitespace.`);
28
30
  }
29
31
  }
30
- }
32
+ }
@@ -3,6 +3,7 @@ export * from './maxXmlDepthError';
3
3
  export * from './missingArgumentError';
4
4
  export * from './missingCloseDelimiterError';
5
5
  export * from './missingStartDelimiterError';
6
+ export * from './tagOptionsParseError';
6
7
  export * from './unclosedTagError';
7
8
  export * from './unidentifiedFileTypeError';
8
9
  export * from './unknownContentTypeError';
@@ -6,8 +6,5 @@ export class MalformedFileError extends Error {
6
6
  super(`Malformed file detected. Make sure the file is a valid ${expectedFileType} file.`);
7
7
 
8
8
  this.expectedFileType = expectedFileType;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, MalformedFileError.prototype);
12
9
  }
13
- }
10
+ }
@@ -6,8 +6,5 @@ export class MaxXmlDepthError extends Error {
6
6
  super(`XML maximum depth reached (max depth: ${maxDepth}).`);
7
7
 
8
8
  this.maxDepth = maxDepth;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, MaxXmlDepthError.prototype);
12
9
  }
13
- }
10
+ }
@@ -6,8 +6,5 @@ export class MissingArgumentError extends Error {
6
6
  super(`Argument '${argName}' is missing.`);
7
7
 
8
8
  this.argName = argName;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, MissingArgumentError.prototype);
12
9
  }
13
- }
10
+ }
@@ -6,8 +6,5 @@ export class MissingCloseDelimiterError extends Error {
6
6
  super(`Close delimiter is missing from '${openDelimiterText}'.`);
7
7
 
8
8
  this.openDelimiterText = openDelimiterText;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, MissingCloseDelimiterError.prototype);
12
9
  }
13
- }
10
+ }
@@ -6,8 +6,5 @@ export class MissingStartDelimiterError extends Error {
6
6
  super(`Open delimiter is missing from '${closeDelimiterText}'.`);
7
7
 
8
8
  this.closeDelimiterText = closeDelimiterText;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, MissingStartDelimiterError.prototype);
12
9
  }
13
- }
10
+ }
@@ -0,0 +1,12 @@
1
+ export class TagOptionsParseError extends Error {
2
+
3
+ public readonly tagRawText: string;
4
+ public readonly parseError: Error;
5
+
6
+ constructor(tagRawText: string, parseError: Error) {
7
+ super(`Failed to parse tag options of '${tagRawText}': ${parseError.message}.`);
8
+
9
+ this.tagRawText = tagRawText;
10
+ this.parseError = parseError;
11
+ }
12
+ }
@@ -6,8 +6,5 @@ export class UnclosedTagError extends Error {
6
6
  super(`Tag '${tagName}' is never closed.`);
7
7
 
8
8
  this.tagName = tagName;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, UnclosedTagError.prototype);
12
9
  }
13
- }
10
+ }
@@ -1,8 +1,5 @@
1
1
  export class UnidentifiedFileTypeError extends Error {
2
2
  constructor() {
3
3
  super(`The filetype for this file could not be identified, is this file corrupted?`);
4
-
5
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
6
- Object.setPrototypeOf(this, UnidentifiedFileTypeError.prototype);
7
4
  }
8
- }
5
+ }
@@ -10,8 +10,5 @@ export class UnknownContentTypeError extends Error {
10
10
  this.contentType = contentType;
11
11
  this.tagRawText = tagRawText;
12
12
  this.path = path;
13
-
14
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
15
- Object.setPrototypeOf(this, UnknownContentTypeError.prototype);
16
13
  }
17
- }
14
+ }
@@ -6,8 +6,5 @@ export class UnopenedTagError extends Error {
6
6
  super(`Tag '${tagName}' is closed but was never opened.`);
7
7
 
8
8
  this.tagName = tagName;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, UnopenedTagError.prototype);
12
9
  }
13
- }
10
+ }
@@ -6,8 +6,5 @@ export class UnsupportedFileTypeError extends Error {
6
6
  super(`Filetype "${fileType}" is not supported.`);
7
7
 
8
8
  this.fileType = fileType;
9
-
10
- // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
11
- Object.setPrototypeOf(this, UnsupportedFileTypeError.prototype);
12
9
  }
13
- }
10
+ }
@@ -114,7 +114,10 @@ export class Docx {
114
114
  // find the last section properties
115
115
  // see: http://officeopenxml.com/WPsection.php
116
116
  const docRoot = await this.mainDocument.xmlRoot();
117
- const body = docRoot.childNodes[0];
117
+ const body = docRoot.childNodes.find(node => node.nodeName == 'w:body');
118
+ if (body == null)
119
+ return null;
120
+
118
121
  const sectionProps = last(body.childNodes.filter(node => node.nodeType === XmlNodeType.General));
119
122
  if (sectionProps.nodeName != 'w:sectPr')
120
123
  return null;
@@ -208,8 +208,10 @@ export class DocxParser {
208
208
  }
209
209
  while (curWordTextNode) {
210
210
 
211
- if (curWordTextNode.nodeName !== DocxParser.TEXT_NODE)
211
+ if (curWordTextNode.nodeName !== DocxParser.TEXT_NODE) {
212
+ curWordTextNode = curWordTextNode.nextSibling;
212
213
  continue;
214
+ }
213
215
 
214
216
  // move text to first node
215
217
  const curXmlTextNode = XmlNode.lastTextChild(curWordTextNode);
@@ -29,7 +29,7 @@ export class LoopPlugin extends TemplatePlugin {
29
29
  // Non array value - treat as a boolean condition.
30
30
  const isCondition = !Array.isArray(value);
31
31
  if (isCondition) {
32
- if (!!value) {
32
+ if (value) {
33
33
  value = [{}];
34
34
  } else {
35
35
  value = [];
@@ -0,0 +1,14 @@
1
+ export enum LoopOver {
2
+ /**
3
+ * Loop over the entire row.
4
+ */
5
+ Row = 'row',
6
+ /**
7
+ * Loop over the content enclosed between the opening and closing tag.
8
+ */
9
+ Content = 'content'
10
+ }
11
+
12
+ export class LoopTagOptions {
13
+ public loopOver = LoopOver.Content;
14
+ }
@@ -1,6 +1,7 @@
1
1
  import { Tag } from '../../../compilation';
2
2
  import { XmlNode } from '../../../xml';
3
3
  import { PluginUtilities } from '../../templatePlugin';
4
+ import { LoopOver, LoopTagOptions } from '../loopTagOptions';
4
5
  import { ILoopStrategy, SplitBeforeResult } from './iLoopStrategy';
5
6
 
6
7
  export class LoopTableStrategy implements ILoopStrategy {
@@ -12,10 +13,28 @@ export class LoopTableStrategy implements ILoopStrategy {
12
13
  }
13
14
 
14
15
  public isApplicable(openTag: Tag, closeTag: Tag): boolean {
15
- const containingParagraph = this.utilities.docxParser.containingParagraphNode(openTag.xmlTextNode);
16
- if (!containingParagraph.parentNode)
16
+ const openParagraph = this.utilities.docxParser.containingParagraphNode(openTag.xmlTextNode);
17
+ if (!openParagraph.parentNode)
17
18
  return false;
18
- return this.utilities.docxParser.isTableCellNode(containingParagraph.parentNode);
19
+
20
+ if (!this.utilities.docxParser.isTableCellNode(openParagraph.parentNode))
21
+ return false;
22
+
23
+ const closeParagraph = this.utilities.docxParser.containingParagraphNode(closeTag.xmlTextNode);
24
+ if (!closeParagraph.parentNode)
25
+ return false;
26
+
27
+ if (!this.utilities.docxParser.isTableCellNode(closeParagraph.parentNode))
28
+ return false;
29
+
30
+ const options = openTag.options as LoopTagOptions;
31
+ const forceRowLoop = options?.loopOver === LoopOver.Row;
32
+
33
+ // If both tags are in the same cell, assume it's a paragraph loop (iterate content, not rows).
34
+ if (!forceRowLoop && openParagraph.parentNode === closeParagraph.parentNode)
35
+ return false;
36
+
37
+ return true;
19
38
  }
20
39
 
21
40
  public splitBefore(openTag: Tag, closeTag: Tag): SplitBeforeResult {
@@ -8,8 +8,6 @@ export interface PluginUtilities {
8
8
  xmlParser: XmlParser;
9
9
  }
10
10
 
11
- /* eslint-disable @typescript-eslint/member-ordering */
12
-
13
11
  export abstract class TemplatePlugin {
14
12
 
15
13
  /**
package/src/types.ts CHANGED
@@ -1,7 +1,5 @@
1
1
 
2
- export interface IMap<T> {
3
- [key: string]: T;
4
- }
2
+ export type IMap<T> = Record<string, T>;
5
3
 
6
4
  export interface Constructor<T> {
7
5
  new(...args: any[]): T;