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
@@ -0,0 +1,845 @@
1
+ import { AttributeDelimiterMark, TextNodeDelimiterMark } from "src/compilation/delimiters/delimiterMark";
2
+ import { AttributeTag, TagDisposition, TagPlacement } from "src/compilation/tag";
3
+ import { TagParser } from "src/compilation/tagParser";
4
+ import { Delimiters } from "src/delimiters";
5
+ import { MissingCloseDelimiterError, MissingStartDelimiterError, TagOptionsParseError } from "src/errors";
6
+ import { xml, XmlNodeType, XmlTextNode } from "src/xml";
7
+ import { parseXml } from "test/testUtils";
8
+ import { describe, expect, test } from "vitest";
9
+
10
+ describe(TagParser, () => {
11
+
12
+ describe('text node delimiters', () => {
13
+
14
+ test('trim tag names', () => {
15
+
16
+ const paragraph = parseXml(`
17
+ <w:p><w:r><w:t>{# my loop }{ my tag }{/ my loop }</w:t></w:r></w:p>
18
+ `, false);
19
+
20
+ const textNode = xml.query.findByPath(paragraph, XmlNodeType.Text, 0, 0, 0);
21
+ expect(textNode.textContent).toEqual('{# my loop }{ my tag }{/ my loop }');
22
+
23
+ const delimiters: TextNodeDelimiterMark[] = [
24
+ {
25
+ placement: TagPlacement.TextNode,
26
+ isOpen: true,
27
+ index: 0,
28
+ xmlTextNode: textNode
29
+ },
30
+ {
31
+ placement: TagPlacement.TextNode,
32
+ isOpen: false,
33
+ index: 12,
34
+ xmlTextNode: textNode
35
+ },
36
+ {
37
+ placement: TagPlacement.TextNode,
38
+ isOpen: true,
39
+ index: 13,
40
+ xmlTextNode: textNode
41
+ },
42
+ {
43
+ placement: TagPlacement.TextNode,
44
+ isOpen: false,
45
+ index: 23,
46
+ xmlTextNode: textNode
47
+ },
48
+ {
49
+ placement: TagPlacement.TextNode,
50
+ isOpen: true,
51
+ index: 24,
52
+ xmlTextNode: textNode
53
+ },
54
+ {
55
+ placement: TagPlacement.TextNode,
56
+ isOpen: false,
57
+ index: 36,
58
+ xmlTextNode: textNode
59
+ }
60
+ ];
61
+
62
+ const parser = createTagParser();
63
+ const tags = parser.parse(delimiters);
64
+
65
+ expect(tags.length).toEqual(3);
66
+
67
+ // open tag
68
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
69
+ expect(tags[0].name).toEqual('my loop');
70
+ expect(tags[0].rawText).toEqual('{# my loop }');
71
+
72
+ // middle tag
73
+ expect(tags[1].disposition).toEqual(TagDisposition.SelfClosed);
74
+ expect(tags[1].name).toEqual('my tag');
75
+ expect(tags[1].rawText).toEqual('{ my tag }');
76
+
77
+ // close tag
78
+ expect(tags[2].disposition).toEqual(TagDisposition.Close);
79
+ expect(tags[2].name).toEqual('my loop');
80
+ expect(tags[2].rawText).toEqual('{/ my loop }');
81
+ });
82
+
83
+ test('multiple tags on the same text node', () => {
84
+
85
+ const paragraph = parseXml(`
86
+ <w:p>
87
+ <w:r>
88
+ <w:t>{#loop}{/loop}</w:t>
89
+ </w:r>
90
+ </w:p>
91
+ `);
92
+
93
+ const textNode = xml.query.findByPath(paragraph, XmlNodeType.Text, 0, 0, 0);
94
+ const delimiters: TextNodeDelimiterMark[] = [
95
+ {
96
+ placement: TagPlacement.TextNode,
97
+ isOpen: true,
98
+ index: 0,
99
+ xmlTextNode: textNode
100
+ },
101
+ {
102
+ placement: TagPlacement.TextNode,
103
+ isOpen: false,
104
+ index: 6,
105
+ xmlTextNode: textNode
106
+ },
107
+ {
108
+ placement: TagPlacement.TextNode,
109
+ isOpen: true,
110
+ index: 7,
111
+ xmlTextNode: textNode
112
+ },
113
+ {
114
+ placement: TagPlacement.TextNode,
115
+ isOpen: false,
116
+ index: 13,
117
+ xmlTextNode: textNode
118
+ }
119
+ ];
120
+
121
+ const parser = createTagParser();
122
+ const tags = parser.parse(delimiters);
123
+
124
+ expect(tags.length).toEqual(2);
125
+
126
+ // open tag
127
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
128
+ expect(tags[0].name).toEqual('loop');
129
+ expect(tags[0].rawText).toEqual('{#loop}');
130
+
131
+ // close tag
132
+ expect(tags[1].disposition).toEqual(TagDisposition.Close);
133
+ expect(tags[1].name).toEqual('loop');
134
+ expect(tags[1].rawText).toEqual('{/loop}');
135
+ });
136
+
137
+ test('multiple tags on the same text node, with leading text', () => {
138
+
139
+ const paragraph = parseXml(`
140
+ <w:p>
141
+ <w:r>
142
+ <w:t>text1{#loop}text2{/loop}</w:t>
143
+ </w:r>
144
+ </w:p>
145
+ `);
146
+
147
+ const textNode = xml.query.findByPath(paragraph, XmlNodeType.Text, 0, 0, 0);
148
+ const delimiters: TextNodeDelimiterMark[] = [
149
+ {
150
+ placement: TagPlacement.TextNode,
151
+ isOpen: true,
152
+ index: 5,
153
+ xmlTextNode: textNode
154
+ },
155
+ {
156
+ placement: TagPlacement.TextNode,
157
+ isOpen: false,
158
+ index: 11,
159
+ xmlTextNode: textNode
160
+ },
161
+ {
162
+ placement: TagPlacement.TextNode,
163
+ isOpen: true,
164
+ index: 17,
165
+ xmlTextNode: textNode
166
+ },
167
+ {
168
+ placement: TagPlacement.TextNode,
169
+ isOpen: false,
170
+ index: 23,
171
+ xmlTextNode: textNode
172
+ }
173
+ ];
174
+
175
+ const parser = createTagParser();
176
+ const tags = parser.parse(delimiters);
177
+
178
+ expect(tags.length).toEqual(2);
179
+
180
+ // open tag
181
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
182
+ expect(tags[0].name).toEqual('loop');
183
+ expect(tags[0].rawText).toEqual('{#loop}');
184
+
185
+ // close tag
186
+ expect(tags[1].disposition).toEqual(TagDisposition.Close);
187
+ expect(tags[1].name).toEqual('loop');
188
+ expect(tags[1].rawText).toEqual('{/loop}');
189
+ });
190
+
191
+ test('parse a butterfly }{', () => {
192
+
193
+ const paragraphNode = parseXml(`
194
+ <w:p>
195
+ <w:r>
196
+ <w:t>{#loop</w:t>
197
+ <w:t>}{</w:t>
198
+ <w:t>/loop}</w:t>
199
+ </w:r>
200
+ </w:p>
201
+ `);
202
+ const runNode = paragraphNode.childNodes[0];
203
+
204
+ const firstTextNode = xml.query.findByPath(runNode, XmlNodeType.Text, 0, 0);
205
+ const secondTextNode = xml.query.findByPath(runNode, XmlNodeType.Text, 1, 0);
206
+ const thirdTextNode = xml.query.findByPath(runNode, XmlNodeType.Text, 2, 0);
207
+ const delimiters: TextNodeDelimiterMark[] = [
208
+ {
209
+ placement: TagPlacement.TextNode,
210
+ isOpen: true,
211
+ index: 0,
212
+ xmlTextNode: firstTextNode
213
+ },
214
+ {
215
+ placement: TagPlacement.TextNode,
216
+ isOpen: false,
217
+ index: 0,
218
+ xmlTextNode: secondTextNode
219
+ },
220
+ {
221
+ placement: TagPlacement.TextNode,
222
+ isOpen: true,
223
+ index: 1,
224
+ xmlTextNode: secondTextNode
225
+ },
226
+ {
227
+ placement: TagPlacement.TextNode,
228
+ isOpen: false,
229
+ index: 5,
230
+ xmlTextNode: thirdTextNode
231
+ }
232
+ ];
233
+
234
+ const parser = createTagParser();
235
+ const tags = parser.parse(delimiters);
236
+
237
+ expect(tags.length).toEqual(2);
238
+
239
+ // open tag
240
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
241
+ expect(tags[0].name).toEqual('loop');
242
+ expect(tags[0].rawText).toEqual('{#loop}');
243
+
244
+ // close tag
245
+ expect(tags[1].disposition).toEqual(TagDisposition.Close);
246
+ expect(tags[1].name).toEqual('loop');
247
+ expect(tags[1].rawText).toEqual('{/loop}');
248
+ });
249
+
250
+ test('splitted simple tag', () => {
251
+
252
+ const paragraphNode = parseXml(`
253
+ <w:p>
254
+ <w:r>
255
+ <w:t>{#loo</w:t>
256
+ <w:t>p}</w:t>
257
+ </w:r>
258
+ </w:p>
259
+ `);
260
+ const runNode = xml.query.findByPath(paragraphNode, XmlNodeType.General, 0);
261
+
262
+ const firstTextNode = xml.query.findByPath(runNode, XmlNodeType.Text, 0, 0);
263
+ const secondTextNode = xml.query.findByPath(runNode, XmlNodeType.Text, 1, 0);
264
+ const delimiters: TextNodeDelimiterMark[] = [
265
+ {
266
+ placement: TagPlacement.TextNode,
267
+ isOpen: true,
268
+ index: 0,
269
+ xmlTextNode: firstTextNode
270
+ },
271
+ {
272
+ placement: TagPlacement.TextNode,
273
+ isOpen: false,
274
+ index: 1,
275
+ xmlTextNode: secondTextNode
276
+ }
277
+ ];
278
+
279
+ const parser = createTagParser();
280
+ const tags = parser.parse(delimiters);
281
+
282
+ expect(tags.length).toEqual(1);
283
+
284
+ // tag
285
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
286
+ expect(tags[0].name).toEqual('loop');
287
+ expect(tags[0].rawText).toEqual('{#loop}');
288
+ });
289
+
290
+ test('splitted closing tag', () => {
291
+
292
+ const paragraphNode = parseXml(`
293
+ <w:p>
294
+ <w:r>
295
+ <w:t>{#loop}{text}{/</w:t>
296
+ </w:r>
297
+ <w:r>
298
+ <w:t>loop</w:t>
299
+ </w:r>
300
+ <w:r>
301
+ <w:t>}</w:t>
302
+ </w:r>
303
+ </w:p>
304
+ `);
305
+
306
+ const firstTextNode = xml.query.findByPath(paragraphNode, XmlNodeType.Text, 0, 0, 0);
307
+ expect(firstTextNode.textContent).toEqual('{#loop}{text}{/');
308
+
309
+ const thirdTextNode = xml.query.findByPath(paragraphNode, XmlNodeType.Text, 2, 0, 0);
310
+ expect(thirdTextNode.textContent).toEqual('}');
311
+
312
+ const delimiters: TextNodeDelimiterMark[] = [
313
+ {
314
+ placement: TagPlacement.TextNode,
315
+ isOpen: true,
316
+ index: 0,
317
+ xmlTextNode: firstTextNode
318
+ },
319
+ {
320
+ placement: TagPlacement.TextNode,
321
+ isOpen: false,
322
+ index: 6,
323
+ xmlTextNode: firstTextNode
324
+ },
325
+ {
326
+ placement: TagPlacement.TextNode,
327
+ isOpen: true,
328
+ index: 7,
329
+ xmlTextNode: firstTextNode
330
+ },
331
+ {
332
+ placement: TagPlacement.TextNode,
333
+ isOpen: false,
334
+ index: 12,
335
+ xmlTextNode: firstTextNode
336
+ },
337
+ {
338
+ placement: TagPlacement.TextNode,
339
+ isOpen: true,
340
+ index: 13,
341
+ xmlTextNode: firstTextNode
342
+ },
343
+ {
344
+ placement: TagPlacement.TextNode,
345
+ isOpen: false,
346
+ index: 0,
347
+ xmlTextNode: thirdTextNode
348
+ }
349
+ ];
350
+
351
+ const parser = createTagParser();
352
+ const tags = parser.parse(delimiters);
353
+
354
+ expect(tags).toHaveLength(3);
355
+
356
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
357
+ expect(tags[0].name).toEqual('loop');
358
+ expect(tags[0].rawText).toEqual('{#loop}');
359
+
360
+ expect(tags[1].disposition).toEqual(TagDisposition.SelfClosed);
361
+ expect(tags[1].name).toEqual('text');
362
+ expect(tags[1].rawText).toEqual('{text}');
363
+
364
+ expect(tags[2].disposition).toEqual(TagDisposition.Close);
365
+ expect(tags[2].name).toEqual('loop');
366
+ expect(tags[2].rawText).toEqual('{/loop}');
367
+ });
368
+
369
+ test('close delimiter in different paragraph throws MissingCloseDelimiterError', () => {
370
+
371
+ const body = parseXml(`
372
+ <w:body>
373
+ <w:p><w:r><w:t>{my_simple_</w:t></w:r></w:p>
374
+ <w:p><w:r><w:t>tag}</w:t></w:r></w:p>
375
+ </w:body>
376
+ `, false);
377
+
378
+ const firstParagraph = body.childNodes.find(node => node.nodeName === 'w:p');
379
+ const secondParagraph = body.childNodes.findLast(node => node.nodeName === 'w:p');
380
+
381
+ const firstRun = firstParagraph.childNodes[0];
382
+ const secondRun = secondParagraph.childNodes[0];
383
+
384
+ const firstTextNode = firstRun.childNodes[0] as XmlTextNode;
385
+ const secondTextNode = secondRun.childNodes[0] as XmlTextNode;
386
+
387
+ const delimiters: TextNodeDelimiterMark[] = [
388
+ {
389
+ placement: TagPlacement.TextNode,
390
+ isOpen: true,
391
+ index: 0,
392
+ xmlTextNode: firstTextNode
393
+ },
394
+ {
395
+ placement: TagPlacement.TextNode,
396
+ isOpen: false,
397
+ index: 4,
398
+ xmlTextNode: secondTextNode
399
+ }
400
+ ];
401
+
402
+ const parser = createTagParser();
403
+ expect(() => parser.parse(delimiters)).toThrow(MissingCloseDelimiterError);
404
+ });
405
+ });
406
+
407
+ describe('attribute delimiters', () => {
408
+
409
+ test('simple attribute tag', () => {
410
+ const paragraph = parseXml(`
411
+ <w:p>
412
+ <w:r>
413
+ <w:drawing>
414
+ <wp:inline>
415
+ <wp:docPr descr="{my_tag}" />
416
+ </wp:inline>
417
+ </w:drawing>
418
+ </w:r>
419
+ </w:p>
420
+ `, false);
421
+
422
+ const docPrNode = xml.query.findByPath(paragraph, XmlNodeType.General, 0, 0, 0, 0);
423
+ expect(docPrNode.attributes.descr).toEqual('{my_tag}');
424
+
425
+ const delimiters: AttributeDelimiterMark[] = [
426
+ {
427
+ placement: TagPlacement.Attribute,
428
+ isOpen: true,
429
+ index: 0,
430
+ xmlNode: docPrNode,
431
+ attributeName: 'descr'
432
+ },
433
+ {
434
+ placement: TagPlacement.Attribute,
435
+ isOpen: false,
436
+ index: 7,
437
+ xmlNode: docPrNode,
438
+ attributeName: 'descr'
439
+ }
440
+ ];
441
+
442
+ const parser = createTagParser();
443
+ const tags = parser.parse(delimiters);
444
+
445
+ expect(tags.length).toEqual(1);
446
+
447
+ const tag = tags[0] as AttributeTag;
448
+ expect(tag.disposition).toEqual(TagDisposition.SelfClosed);
449
+ expect(tag.name).toEqual('my_tag');
450
+ expect(tag.rawText).toEqual('{my_tag}');
451
+ expect(tag.placement).toEqual(TagPlacement.Attribute);
452
+ expect(tag.xmlNode).toEqual(docPrNode);
453
+ expect(tag.attributeName).toEqual('descr');
454
+ });
455
+
456
+ test('open and close tags in attribute', () => {
457
+ const paragraph = parseXml(`
458
+ <w:p>
459
+ <w:drawing>
460
+ <wp:docPr descr="{#loop}content{/loop}"/>
461
+ </w:drawing>
462
+ </w:p>
463
+ `, false);
464
+
465
+ const docPrNode = xml.query.findByPath(paragraph, XmlNodeType.General, 0, 0);
466
+ expect(docPrNode.attributes.descr).toEqual('{#loop}content{/loop}');
467
+
468
+ const delimiters: AttributeDelimiterMark[] = [
469
+ {
470
+ placement: TagPlacement.Attribute,
471
+ isOpen: true,
472
+ index: 0,
473
+ xmlNode: docPrNode,
474
+ attributeName: 'descr'
475
+ },
476
+ {
477
+ placement: TagPlacement.Attribute,
478
+ isOpen: false,
479
+ index: 6,
480
+ xmlNode: docPrNode,
481
+ attributeName: 'descr'
482
+ },
483
+ {
484
+ placement: TagPlacement.Attribute,
485
+ isOpen: true,
486
+ index: 14,
487
+ xmlNode: docPrNode,
488
+ attributeName: 'descr'
489
+ },
490
+ {
491
+ placement: TagPlacement.Attribute,
492
+ isOpen: false,
493
+ index: 20,
494
+ xmlNode: docPrNode,
495
+ attributeName: 'descr'
496
+ }
497
+ ];
498
+
499
+ const parser = createTagParser();
500
+ const tags = parser.parse(delimiters);
501
+
502
+ expect(tags.length).toEqual(2);
503
+
504
+ // open tag
505
+ const openTag = tags[0] as AttributeTag;
506
+ expect(openTag.disposition).toEqual(TagDisposition.Open);
507
+ expect(openTag.name).toEqual('loop');
508
+ expect(openTag.rawText).toEqual('{#loop}');
509
+ expect(openTag.placement).toEqual(TagPlacement.Attribute);
510
+
511
+ // close tag
512
+ const closeTag = tags[1] as AttributeTag;
513
+ expect(closeTag.disposition).toEqual(TagDisposition.Close);
514
+ expect(closeTag.name).toEqual('loop');
515
+ expect(closeTag.rawText).toEqual('{/loop}');
516
+ expect(closeTag.placement).toEqual(TagPlacement.Attribute);
517
+ });
518
+
519
+ test('multiple self-closed tags in attribute', () => {
520
+ const paragraph = parseXml(`
521
+ <w:p>
522
+ <w:drawing>
523
+ <wp:docPr descr="{tag1}{tag2}{tag3}"/>
524
+ </w:drawing>
525
+ </w:p>
526
+ `, false);
527
+
528
+ const docPrNode = xml.query.findByPath(paragraph, XmlNodeType.General, 0, 0);
529
+ expect(docPrNode.attributes.descr).toEqual('{tag1}{tag2}{tag3}');
530
+
531
+ const delimiters: AttributeDelimiterMark[] = [
532
+ {
533
+ placement: TagPlacement.Attribute,
534
+ isOpen: true,
535
+ index: 0,
536
+ xmlNode: docPrNode,
537
+ attributeName: 'descr'
538
+ },
539
+ {
540
+ placement: TagPlacement.Attribute,
541
+ isOpen: false,
542
+ index: 5,
543
+ xmlNode: docPrNode,
544
+ attributeName: 'descr'
545
+ },
546
+ {
547
+ placement: TagPlacement.Attribute,
548
+ isOpen: true,
549
+ index: 6,
550
+ xmlNode: docPrNode,
551
+ attributeName: 'descr'
552
+ },
553
+ {
554
+ placement: TagPlacement.Attribute,
555
+ isOpen: false,
556
+ index: 11,
557
+ xmlNode: docPrNode,
558
+ attributeName: 'descr'
559
+ },
560
+ {
561
+ placement: TagPlacement.Attribute,
562
+ isOpen: true,
563
+ index: 12,
564
+ xmlNode: docPrNode,
565
+ attributeName: 'descr'
566
+ },
567
+ {
568
+ placement: TagPlacement.Attribute,
569
+ isOpen: false,
570
+ index: 17,
571
+ xmlNode: docPrNode,
572
+ attributeName: 'descr'
573
+ }
574
+ ];
575
+
576
+ const parser = createTagParser();
577
+ const tags = parser.parse(delimiters);
578
+
579
+ expect(tags.length).toEqual(3);
580
+
581
+ expect(tags[0].disposition).toEqual(TagDisposition.SelfClosed);
582
+ expect(tags[0].name).toEqual('tag1');
583
+ expect(tags[0].rawText).toEqual('{tag1}');
584
+
585
+ expect(tags[1].disposition).toEqual(TagDisposition.SelfClosed);
586
+ expect(tags[1].name).toEqual('tag2');
587
+ expect(tags[1].rawText).toEqual('{tag2}');
588
+
589
+ expect(tags[2].disposition).toEqual(TagDisposition.SelfClosed);
590
+ expect(tags[2].name).toEqual('tag3');
591
+ expect(tags[2].rawText).toEqual('{tag3}');
592
+ });
593
+
594
+ test('tag with options in attribute', () => {
595
+ const paragraph = parseXml(`
596
+ <w:p>
597
+ <w:drawing>
598
+ <wp:docPr descr="{my_tag [opt: 'value', count: 5]}"/>
599
+ </w:drawing>
600
+ </w:p>
601
+ `, false);
602
+
603
+ const docPrNode = xml.query.findByPath(paragraph, XmlNodeType.General, 0, 0);
604
+ expect(docPrNode.attributes.descr).toEqual("{my_tag [opt: 'value', count: 5]}");
605
+
606
+ const delimiters: AttributeDelimiterMark[] = [
607
+ {
608
+ placement: TagPlacement.Attribute,
609
+ isOpen: true,
610
+ index: 0,
611
+ xmlNode: docPrNode,
612
+ attributeName: 'descr'
613
+ },
614
+ {
615
+ placement: TagPlacement.Attribute,
616
+ isOpen: false,
617
+ index: 32,
618
+ xmlNode: docPrNode,
619
+ attributeName: 'descr'
620
+ }
621
+ ];
622
+
623
+ const parser = createTagParser();
624
+ const tags = parser.parse(delimiters);
625
+
626
+ expect(tags.length).toEqual(1);
627
+ expect(tags[0].disposition).toEqual(TagDisposition.SelfClosed);
628
+ expect(tags[0].name).toEqual('my_tag');
629
+ expect(tags[0].options).toEqual({ opt: 'value', count: 5 });
630
+ expect(tags[0].rawText).toEqual("{my_tag [opt: 'value', count: 5]}");
631
+ });
632
+
633
+ test('missing open delimiter in attribute throws MissingStartDelimiterError', () => {
634
+ const paragraph = parseXml(`
635
+ <w:p>
636
+ <w:drawing>
637
+ <wp:docPr descr="my_tag}"/>
638
+ </w:drawing>
639
+ </w:p>
640
+ `, false);
641
+
642
+ const docPrNode = xml.query.findByPath(paragraph, XmlNodeType.General, 0, 0);
643
+ expect(docPrNode.attributes.descr).toEqual('my_tag}');
644
+
645
+ const delimiters: AttributeDelimiterMark[] = [
646
+ {
647
+ placement: TagPlacement.Attribute,
648
+ isOpen: false,
649
+ index: 6,
650
+ xmlNode: docPrNode,
651
+ attributeName: 'descr'
652
+ }
653
+ ];
654
+
655
+ const parser = createTagParser();
656
+ expect(() => parser.parse(delimiters)).toThrow(MissingStartDelimiterError);
657
+ });
658
+
659
+ test('open and close tags in different attributes throws MissingCloseDelimiterError', () => {
660
+ const paragraph = parseXml(`
661
+ <w:p>
662
+ <w:r>
663
+ <w:drawing>
664
+ <wp:inline>
665
+ <wp:docPr descr="{my_tag" />
666
+ </wp:inline>
667
+ </w:drawing>
668
+ </w:r>
669
+ <w:r>
670
+ <w:drawing>
671
+ <wp:inline>
672
+ <wp:docPr descr="}" />
673
+ </wp:inline>
674
+ </w:drawing>
675
+ </w:r>
676
+ </w:p>
677
+ `, false);
678
+
679
+ const docPrNode1 = xml.query.findByPath(paragraph, XmlNodeType.General, 0, 0, 0, 0);
680
+ expect(docPrNode1.attributes.descr).toEqual('{my_tag');
681
+
682
+ const docPrNode2 = xml.query.findByPath(paragraph, XmlNodeType.General, 1, 0, 0, 0);
683
+ expect(docPrNode2.attributes.descr).toEqual('}');
684
+
685
+ const delimiters: AttributeDelimiterMark[] = [
686
+ {
687
+ placement: TagPlacement.Attribute,
688
+ isOpen: true,
689
+ index: 0,
690
+ xmlNode: docPrNode1,
691
+ attributeName: 'descr'
692
+ },
693
+ {
694
+ placement: TagPlacement.Attribute,
695
+ isOpen: false,
696
+ index: 0,
697
+ xmlNode: docPrNode2,
698
+ attributeName: 'descr'
699
+ }
700
+ ];
701
+
702
+ const parser = createTagParser();
703
+ expect(() => parser.parse(delimiters)).toThrow(MissingCloseDelimiterError);
704
+ });
705
+ });
706
+
707
+ describe('tag options', () => {
708
+
709
+ test('simple', () => {
710
+
711
+ const text = '{#loop [opt: "yes"]}{/loop}';
712
+ const paragraph = parseXml(`
713
+ <w:p><w:r><w:t>${text}</w:t></w:r></w:p>
714
+ `, false);
715
+
716
+ const textNode = xml.query.findByPath(paragraph, XmlNodeType.Text, 0, 0, 0);
717
+ const delimiters: TextNodeDelimiterMark[] = [
718
+ {
719
+ placement: TagPlacement.TextNode,
720
+ isOpen: true,
721
+ index: 0,
722
+ xmlTextNode: textNode
723
+ },
724
+ {
725
+ placement: TagPlacement.TextNode,
726
+ isOpen: false,
727
+ index: 19,
728
+ xmlTextNode: textNode
729
+ },
730
+ {
731
+ placement: TagPlacement.TextNode,
732
+ isOpen: true,
733
+ index: 20,
734
+ xmlTextNode: textNode
735
+ },
736
+ {
737
+ placement: TagPlacement.TextNode,
738
+ isOpen: false,
739
+ index: 26,
740
+ xmlTextNode: textNode
741
+ }
742
+ ];
743
+
744
+ const parser = createTagParser();
745
+ const tags = parser.parse(delimiters);
746
+
747
+ expect(tags.length).toEqual(2);
748
+
749
+ // open tag
750
+ expect(tags[0].disposition).toEqual(TagDisposition.Open);
751
+ expect(tags[0].name).toEqual('loop');
752
+ expect(tags[0].options).toEqual({ opt: 'yes' });
753
+ expect(tags[0].rawText).toEqual('{#loop [opt: "yes"]}');
754
+
755
+ // close tag
756
+ expect(tags[1].disposition).toEqual(TagDisposition.Close);
757
+ expect(tags[1].name).toEqual('loop');
758
+ expect(tags[1].options).toBeFalsy();
759
+ expect(tags[1].rawText).toEqual('{/loop}');
760
+ });
761
+
762
+ test('angular parser style with brackets', () => {
763
+
764
+ const text = '{something[0] [[myOpt: 5]]}';
765
+ const paragraph = parseXml(`
766
+ <w:p><w:r><w:t>${text}</w:t></w:r></w:p>
767
+ `, false);
768
+
769
+ const textNode = xml.query.findByPath(paragraph, XmlNodeType.Text, 0, 0, 0);
770
+ const delimiters: TextNodeDelimiterMark[] = [
771
+ {
772
+ placement: TagPlacement.TextNode,
773
+ isOpen: true,
774
+ index: 0,
775
+ xmlTextNode: textNode
776
+ },
777
+ {
778
+ placement: TagPlacement.TextNode,
779
+ isOpen: false,
780
+ index: 26,
781
+ xmlTextNode: textNode
782
+ }
783
+ ];
784
+
785
+ const parser = createTagParser({
786
+ tagOptionsStart: '[[',
787
+ tagOptionsEnd: ']]'
788
+ });
789
+ const tags = parser.parse(delimiters);
790
+
791
+ expect(tags.length).toEqual(1);
792
+
793
+ expect(tags[0].disposition).toEqual(TagDisposition.SelfClosed);
794
+ expect(tags[0].name).toEqual('something[0]');
795
+ expect(tags[0].options).toEqual({ myOpt: 5 });
796
+ expect(tags[0].rawText).toEqual('{something[0] [[myOpt: 5]]}');
797
+ });
798
+
799
+ test('invalid options', () => {
800
+
801
+ const text = '{something [myOpt 5]}';
802
+ const paragraph = parseXml(`
803
+ <w:p><w:r><w:t>${text}</w:t></w:r></w:p>
804
+ `, false);
805
+
806
+ const textNode = xml.query.findByPath(paragraph, XmlNodeType.Text, 0, 0, 0);
807
+ const delimiters: TextNodeDelimiterMark[] = [
808
+ {
809
+ placement: TagPlacement.TextNode,
810
+ isOpen: true,
811
+ index: 0,
812
+ xmlTextNode: textNode
813
+ },
814
+ {
815
+ placement: TagPlacement.TextNode,
816
+ isOpen: false,
817
+ index: 20,
818
+ xmlTextNode: textNode
819
+ }
820
+ ];
821
+
822
+ const parser = createTagParser();
823
+ let err: Error;
824
+ try {
825
+ parser.parse(delimiters);
826
+ } catch (e) {
827
+ err = e;
828
+ }
829
+
830
+ expect(err).toBeTruthy();
831
+ expect(err).toBeInstanceOf(TagOptionsParseError);
832
+
833
+ const parseErr = err as TagOptionsParseError;
834
+ expect(parseErr.tagRawText).toEqual(text);
835
+ expect(parseErr.parseError).toBeTruthy();
836
+ });
837
+
838
+ });
839
+ });
840
+
841
+ function createTagParser(delim?: Partial<Delimiters>): TagParser {
842
+
843
+ const delimiters = new Delimiters(delim);
844
+ return new TagParser(delimiters);
845
+ }