easy-template-x 7.0.2 → 7.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -9
- package/dist/cjs/easy-template-x.cjs +99 -51
- package/dist/es/easy-template-x.mjs +99 -51
- package/dist/types/office/officeMarkup.d.ts +1 -0
- package/dist/types/office/omlNode.d.ts +5 -0
- package/package.json +1 -1
- package/src/office/officeMarkup.ts +38 -26
- package/src/office/omlNode.ts +29 -0
- package/src/plugins/loop/loopPlugin.ts +20 -13
- package/src/plugins/loop/strategy/loopParagraphStrategy.ts +12 -11
package/README.md
CHANGED
|
@@ -12,10 +12,10 @@ Generate docx documents from templates, in Node or in the browser.
|
|
|
12
12
|
>
|
|
13
13
|
> Check out [templatedocs.io](https://templatedocs.io) - a cloud platform for document generation with:
|
|
14
14
|
>
|
|
15
|
-
> ✓
|
|
16
|
-
> ✓ REST API integration
|
|
17
|
-
> ✓ Built-in email delivery
|
|
15
|
+
> ✓ Built-in workflow automation and email delivery
|
|
18
16
|
> ✓ Zapier and Make integration
|
|
17
|
+
> ✓ REST API integration
|
|
18
|
+
> ✓ PDF output support
|
|
19
19
|
> ✓ Live preview functionality
|
|
20
20
|
> ✓ Easy-to-use UI
|
|
21
21
|
>
|
|
@@ -43,7 +43,6 @@ Generate docx documents from templates, in Node or in the browser.
|
|
|
43
43
|
- [Scope resolution](#scope-resolution)
|
|
44
44
|
- [Extensions](#extensions)
|
|
45
45
|
- [Community Extensions](#community-extensions)
|
|
46
|
-
- [Data Binding Extension](#data-binding-extension)
|
|
47
46
|
- [Template handler options](#template-handler-options)
|
|
48
47
|
- [Custom tag delimiters](#custom-tag-delimiters)
|
|
49
48
|
- [Advanced syntax and custom resolvers](#advanced-syntax-and-custom-resolvers)
|
|
@@ -749,11 +748,7 @@ const handler = new TemplateHandler({
|
|
|
749
748
|
The following extensions were developed by the community.
|
|
750
749
|
Want to see your extension here? Submit a pull request or [open an issue](https://github.com/alonrbar/easy-template-x/issues).
|
|
751
750
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
The [easy-template-x-data-binding](https://github.com/sebastianrogers/easy-template-x-data-binding) extension supports updating [custom XML files](https://docs.microsoft.com/en-gb/archive/blogs/modonovan/word-2007-content-controls-and-xml-part-1-the-basics) inside Word documents.
|
|
755
|
-
|
|
756
|
-
This allows using `easy-template-x` to automatically fill [Word forms](https://support.office.com/en-us/article/create-forms-that-users-complete-or-print-in-word-040c5cc1-e309-445b-94ac-542f732c8c8b) that uses content controls.
|
|
751
|
+
- **Data Binding Extension** - The [easy-template-x-data-binding](https://github.com/sebastianrogers/easy-template-x-data-binding) extension supports updating [custom XML parts](https://www.google.com/search?q=word+custom+xml+part) inside Word documents.
|
|
757
752
|
|
|
758
753
|
## Template handler options
|
|
759
754
|
|
|
@@ -1768,6 +1768,26 @@ class W {
|
|
|
1768
1768
|
TableCell = 'w:tc';
|
|
1769
1769
|
Drawing = 'w:drawing';
|
|
1770
1770
|
NumberProperties = 'w:numPr';
|
|
1771
|
+
/**
|
|
1772
|
+
* Structured document tag (content control).
|
|
1773
|
+
*
|
|
1774
|
+
* See: ECMA-376, Part 1, sections 17.5 and 17.5.2
|
|
1775
|
+
*/
|
|
1776
|
+
StructuredTag = 'w:sdt';
|
|
1777
|
+
/**
|
|
1778
|
+
* Structured document tag properties.
|
|
1779
|
+
*/
|
|
1780
|
+
StructuredTagProperties = 'w:sdtPr';
|
|
1781
|
+
/**
|
|
1782
|
+
* Structured document tag content.
|
|
1783
|
+
*/
|
|
1784
|
+
StructuredTagContent = 'w:sdtContent';
|
|
1785
|
+
/**
|
|
1786
|
+
* Complex field character (legacy form field).
|
|
1787
|
+
*
|
|
1788
|
+
* see: http://officeopenxml.com/WPfields.php
|
|
1789
|
+
*/
|
|
1790
|
+
FieldChar = 'w:fldChar';
|
|
1771
1791
|
}
|
|
1772
1792
|
|
|
1773
1793
|
/**
|
|
@@ -1853,6 +1873,9 @@ class Pic {
|
|
|
1853
1873
|
* used in Office Open XML documents. Including but not limited to
|
|
1854
1874
|
* Wordprocessing Markup Language, Drawing Markup Language and Spreadsheet
|
|
1855
1875
|
* Markup Language.
|
|
1876
|
+
*
|
|
1877
|
+
* - For an easy introduction, see: http://officeopenxml.com/WPcontentOverview.php
|
|
1878
|
+
* - For the complete specification, see: https://ecma-international.org/publications-and-standards/standards/ecma-376/
|
|
1856
1879
|
*/
|
|
1857
1880
|
class OmlNode {
|
|
1858
1881
|
/**
|
|
@@ -1877,6 +1900,12 @@ class OmlNode {
|
|
|
1877
1900
|
}
|
|
1878
1901
|
class OmlAttribute {
|
|
1879
1902
|
static SpacePreserve = 'xml:space';
|
|
1903
|
+
/**
|
|
1904
|
+
* Complex field character type.
|
|
1905
|
+
*
|
|
1906
|
+
* see: http://officeopenxml.com/WPfields.php
|
|
1907
|
+
*/
|
|
1908
|
+
static FieldCharType = 'w:fldCharType';
|
|
1880
1909
|
}
|
|
1881
1910
|
|
|
1882
1911
|
//
|
|
@@ -1897,7 +1926,8 @@ class OmlAttribute {
|
|
|
1897
1926
|
// </w:r>
|
|
1898
1927
|
// </w:p>
|
|
1899
1928
|
//
|
|
1900
|
-
// see: http://officeopenxml.com/WPcontentOverview.php
|
|
1929
|
+
// - For an easy introduction, see: http://officeopenxml.com/WPcontentOverview.php
|
|
1930
|
+
// - For the complete specification, see: https://ecma-international.org/publications-and-standards/standards/ecma-376/
|
|
1901
1931
|
//
|
|
1902
1932
|
|
|
1903
1933
|
/**
|
|
@@ -2012,6 +2042,13 @@ class Query {
|
|
|
2012
2042
|
return xml.query.findParentByName(node, OmlNode.W.Table);
|
|
2013
2043
|
}
|
|
2014
2044
|
|
|
2045
|
+
/**
|
|
2046
|
+
* Search **upwards** for the first `w:sdtContent` node.
|
|
2047
|
+
*/
|
|
2048
|
+
containingStructuredTagContentNode(node) {
|
|
2049
|
+
return xml.query.findParentByName(node, OmlNode.W.StructuredTagContent);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2015
2052
|
//
|
|
2016
2053
|
// Advanced queries
|
|
2017
2054
|
//
|
|
@@ -2051,29 +2088,29 @@ class Modify {
|
|
|
2051
2088
|
let firstXmlTextNode;
|
|
2052
2089
|
let secondXmlTextNode;
|
|
2053
2090
|
|
|
2054
|
-
//
|
|
2091
|
+
// Split nodes
|
|
2055
2092
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
2056
2093
|
const newWordTextNode = xml.create.cloneNode(wordTextNode, true);
|
|
2057
2094
|
|
|
2058
|
-
//
|
|
2095
|
+
// Set space preserve to prevent display differences after splitting
|
|
2059
2096
|
// (otherwise if there was a space in the middle of the text node and it
|
|
2060
2097
|
// is now at the beginning or end of the text node it will be ignored)
|
|
2061
2098
|
officeMarkup.modify.setSpacePreserveAttribute(wordTextNode);
|
|
2062
2099
|
officeMarkup.modify.setSpacePreserveAttribute(newWordTextNode);
|
|
2063
2100
|
if (addBefore) {
|
|
2064
|
-
//
|
|
2101
|
+
// Insert new node before existing one
|
|
2065
2102
|
xml.modify.insertBefore(newWordTextNode, wordTextNode);
|
|
2066
2103
|
firstXmlTextNode = xml.query.lastTextChild(newWordTextNode);
|
|
2067
2104
|
secondXmlTextNode = textNode;
|
|
2068
2105
|
} else {
|
|
2069
|
-
//
|
|
2106
|
+
// Insert new node after existing one
|
|
2070
2107
|
const curIndex = wordTextNode.parentNode.childNodes.indexOf(wordTextNode);
|
|
2071
2108
|
xml.modify.insertChild(wordTextNode.parentNode, newWordTextNode, curIndex + 1);
|
|
2072
2109
|
firstXmlTextNode = textNode;
|
|
2073
2110
|
secondXmlTextNode = xml.query.lastTextChild(newWordTextNode);
|
|
2074
2111
|
}
|
|
2075
2112
|
|
|
2076
|
-
//
|
|
2113
|
+
// Edit text
|
|
2077
2114
|
const firstText = firstXmlTextNode.textContent;
|
|
2078
2115
|
const secondText = secondXmlTextNode.textContent;
|
|
2079
2116
|
firstXmlTextNode.textContent = firstText.substring(0, splitIndex);
|
|
@@ -2088,25 +2125,27 @@ class Modify {
|
|
|
2088
2125
|
* `false` then the original text node is the first text node of `right`.
|
|
2089
2126
|
*/
|
|
2090
2127
|
splitParagraphByTextNode(paragraph, textNode, removeTextNode) {
|
|
2091
|
-
//
|
|
2128
|
+
// Input validation
|
|
2092
2129
|
const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
|
|
2093
|
-
if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not
|
|
2130
|
+
if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not contained in the specified paragraph.`);
|
|
2094
2131
|
const runNode = officeMarkup.query.containingRunNode(textNode);
|
|
2095
2132
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
2096
2133
|
|
|
2097
|
-
//
|
|
2134
|
+
// 1. Split the run
|
|
2135
|
+
|
|
2136
|
+
// Create run clone (left) and keep the original run (right).
|
|
2098
2137
|
const leftRun = xml.create.cloneNode(runNode, false);
|
|
2099
2138
|
const rightRun = runNode;
|
|
2100
2139
|
xml.modify.insertBefore(leftRun, rightRun);
|
|
2101
2140
|
|
|
2102
|
-
//
|
|
2141
|
+
// Copy props from original run node (preserve style)
|
|
2103
2142
|
const runProps = rightRun.childNodes.find(node => officeMarkup.query.isRunPropertiesNode(node));
|
|
2104
2143
|
if (runProps) {
|
|
2105
2144
|
const leftRunProps = xml.create.cloneNode(runProps, true);
|
|
2106
2145
|
xml.modify.appendChild(leftRun, leftRunProps);
|
|
2107
2146
|
}
|
|
2108
2147
|
|
|
2109
|
-
//
|
|
2148
|
+
// Move all text nodes up to the specified text node, to the new run.
|
|
2110
2149
|
const firstRunChildIndex = runProps ? 1 : 0;
|
|
2111
2150
|
let curChild = rightRun.childNodes[firstRunChildIndex];
|
|
2112
2151
|
while (curChild != wordTextNode) {
|
|
@@ -2115,24 +2154,26 @@ class Modify {
|
|
|
2115
2154
|
curChild = rightRun.childNodes[firstRunChildIndex];
|
|
2116
2155
|
}
|
|
2117
2156
|
|
|
2118
|
-
//
|
|
2157
|
+
// Remove text node
|
|
2119
2158
|
if (removeTextNode) {
|
|
2120
2159
|
xml.modify.removeChild(rightRun, firstRunChildIndex);
|
|
2121
2160
|
}
|
|
2122
2161
|
|
|
2123
|
-
//
|
|
2162
|
+
// 2. Split the paragraph
|
|
2163
|
+
|
|
2164
|
+
// Create paragraph clone (left) and keep the original paragraph (right).
|
|
2124
2165
|
const leftPara = xml.create.cloneNode(containingParagraph, false);
|
|
2125
2166
|
const rightPara = containingParagraph;
|
|
2126
2167
|
xml.modify.insertBefore(leftPara, rightPara);
|
|
2127
2168
|
|
|
2128
|
-
//
|
|
2169
|
+
// Copy props from original paragraph (preserve style)
|
|
2129
2170
|
const paragraphProps = rightPara.childNodes.find(node => officeMarkup.query.isParagraphPropertiesNode(node));
|
|
2130
2171
|
if (paragraphProps) {
|
|
2131
2172
|
const leftParagraphProps = xml.create.cloneNode(paragraphProps, true);
|
|
2132
2173
|
xml.modify.appendChild(leftPara, leftParagraphProps);
|
|
2133
2174
|
}
|
|
2134
2175
|
|
|
2135
|
-
//
|
|
2176
|
+
// Move all run nodes up to the original run (right), to the new paragraph (left).
|
|
2136
2177
|
const firstParaChildIndex = paragraphProps ? 1 : 0;
|
|
2137
2178
|
curChild = rightPara.childNodes[firstParaChildIndex];
|
|
2138
2179
|
while (curChild != rightRun) {
|
|
@@ -2141,7 +2182,7 @@ class Modify {
|
|
|
2141
2182
|
curChild = rightPara.childNodes[firstParaChildIndex];
|
|
2142
2183
|
}
|
|
2143
2184
|
|
|
2144
|
-
//
|
|
2185
|
+
// Clean paragraphs - remove empty runs
|
|
2145
2186
|
if (officeMarkup.query.isEmptyRun(leftRun)) xml.modify.remove(leftRun);
|
|
2146
2187
|
if (officeMarkup.query.isEmptyRun(rightRun)) xml.modify.remove(rightRun);
|
|
2147
2188
|
return [leftPara, rightPara];
|
|
@@ -2151,21 +2192,21 @@ class Modify {
|
|
|
2151
2192
|
* Move all text between the 'from' and 'to' nodes to the 'from' node.
|
|
2152
2193
|
*/
|
|
2153
2194
|
joinTextNodesRange(from, to) {
|
|
2154
|
-
//
|
|
2195
|
+
// Find run nodes
|
|
2155
2196
|
const firstRunNode = officeMarkup.query.containingRunNode(from);
|
|
2156
2197
|
const secondRunNode = officeMarkup.query.containingRunNode(to);
|
|
2157
2198
|
const paragraphNode = firstRunNode.parentNode;
|
|
2158
2199
|
if (secondRunNode.parentNode !== paragraphNode) throw new Error('Can not join text nodes from separate paragraphs.');
|
|
2159
2200
|
|
|
2160
|
-
//
|
|
2201
|
+
// Find "word text nodes"
|
|
2161
2202
|
const firstWordTextNode = officeMarkup.query.containingTextNode(from);
|
|
2162
2203
|
const secondWordTextNode = officeMarkup.query.containingTextNode(to);
|
|
2163
2204
|
const totalText = [];
|
|
2164
2205
|
|
|
2165
|
-
//
|
|
2206
|
+
// Iterate runs
|
|
2166
2207
|
let curRunNode = firstRunNode;
|
|
2167
2208
|
while (curRunNode) {
|
|
2168
|
-
//
|
|
2209
|
+
// Iterate text nodes
|
|
2169
2210
|
let curWordTextNode;
|
|
2170
2211
|
if (curRunNode === firstRunNode) {
|
|
2171
2212
|
curWordTextNode = firstWordTextNode;
|
|
@@ -2178,11 +2219,11 @@ class Modify {
|
|
|
2178
2219
|
continue;
|
|
2179
2220
|
}
|
|
2180
2221
|
|
|
2181
|
-
//
|
|
2222
|
+
// Move text to first node
|
|
2182
2223
|
const curXmlTextNode = xml.query.lastTextChild(curWordTextNode);
|
|
2183
2224
|
totalText.push(curXmlTextNode.textContent);
|
|
2184
2225
|
|
|
2185
|
-
//
|
|
2226
|
+
// Next text node
|
|
2186
2227
|
const textToRemove = curWordTextNode;
|
|
2187
2228
|
if (curWordTextNode === secondWordTextNode) {
|
|
2188
2229
|
curWordTextNode = null;
|
|
@@ -2190,13 +2231,13 @@ class Modify {
|
|
|
2190
2231
|
curWordTextNode = curWordTextNode.nextSibling;
|
|
2191
2232
|
}
|
|
2192
2233
|
|
|
2193
|
-
//
|
|
2234
|
+
// Remove current text node
|
|
2194
2235
|
if (textToRemove !== firstWordTextNode) {
|
|
2195
2236
|
xml.modify.remove(textToRemove);
|
|
2196
2237
|
}
|
|
2197
2238
|
}
|
|
2198
2239
|
|
|
2199
|
-
//
|
|
2240
|
+
// Next run
|
|
2200
2241
|
const runToRemove = curRunNode;
|
|
2201
2242
|
if (curRunNode === secondRunNode) {
|
|
2202
2243
|
curRunNode = null;
|
|
@@ -2204,13 +2245,13 @@ class Modify {
|
|
|
2204
2245
|
curRunNode = curRunNode.nextSibling;
|
|
2205
2246
|
}
|
|
2206
2247
|
|
|
2207
|
-
//
|
|
2248
|
+
// Remove current run
|
|
2208
2249
|
if (!runToRemove.childNodes || !runToRemove.childNodes.length) {
|
|
2209
2250
|
xml.modify.remove(runToRemove);
|
|
2210
2251
|
}
|
|
2211
2252
|
}
|
|
2212
2253
|
|
|
2213
|
-
//
|
|
2254
|
+
// Set the text content
|
|
2214
2255
|
const firstXmlTextNode = xml.query.lastTextChild(firstWordTextNode);
|
|
2215
2256
|
firstXmlTextNode.textContent = totalText.join('');
|
|
2216
2257
|
}
|
|
@@ -3325,28 +3366,29 @@ class LoopParagraphStrategy {
|
|
|
3325
3366
|
return true;
|
|
3326
3367
|
}
|
|
3327
3368
|
splitBefore(openTag, closeTag) {
|
|
3328
|
-
//
|
|
3369
|
+
// Gather some info
|
|
3329
3370
|
let firstParagraph = officeMarkup.query.containingParagraphNode(openTag.xmlTextNode);
|
|
3330
3371
|
let lastParagraph = officeMarkup.query.containingParagraphNode(closeTag.xmlTextNode);
|
|
3331
3372
|
const areSame = firstParagraph === lastParagraph;
|
|
3332
3373
|
|
|
3333
|
-
//
|
|
3334
|
-
|
|
3374
|
+
// Split first paragraph
|
|
3375
|
+
const removeTextNode = true;
|
|
3376
|
+
let splitResult = officeMarkup.modify.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, removeTextNode);
|
|
3335
3377
|
firstParagraph = splitResult[0];
|
|
3336
3378
|
let afterFirstParagraph = splitResult[1];
|
|
3337
3379
|
if (areSame) lastParagraph = afterFirstParagraph;
|
|
3338
3380
|
|
|
3339
|
-
//
|
|
3340
|
-
splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode,
|
|
3381
|
+
// Split last paragraph
|
|
3382
|
+
splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, removeTextNode);
|
|
3341
3383
|
const beforeLastParagraph = splitResult[0];
|
|
3342
3384
|
lastParagraph = splitResult[1];
|
|
3343
3385
|
if (areSame) afterFirstParagraph = beforeLastParagraph;
|
|
3344
3386
|
|
|
3345
|
-
//
|
|
3387
|
+
// Disconnect splitted paragraph from their parents
|
|
3346
3388
|
xml.modify.remove(afterFirstParagraph);
|
|
3347
3389
|
if (!areSame) xml.modify.remove(beforeLastParagraph);
|
|
3348
3390
|
|
|
3349
|
-
//
|
|
3391
|
+
// Extract all paragraphs in between
|
|
3350
3392
|
let middleParagraphs;
|
|
3351
3393
|
if (areSame) {
|
|
3352
3394
|
middleParagraphs = [afterFirstParagraph];
|
|
@@ -3363,20 +3405,20 @@ class LoopParagraphStrategy {
|
|
|
3363
3405
|
mergeBack(middleParagraphs, firstParagraph, lastParagraph) {
|
|
3364
3406
|
let mergeTo = firstParagraph;
|
|
3365
3407
|
for (const curParagraphsGroup of middleParagraphs) {
|
|
3366
|
-
//
|
|
3408
|
+
// Merge first paragraphs
|
|
3367
3409
|
officeMarkup.modify.joinParagraphs(mergeTo, curParagraphsGroup[0]);
|
|
3368
3410
|
|
|
3369
|
-
//
|
|
3411
|
+
// Add middle and last paragraphs to the original document
|
|
3370
3412
|
for (let i = 1; i < curParagraphsGroup.length; i++) {
|
|
3371
3413
|
xml.modify.insertBefore(curParagraphsGroup[i], lastParagraph);
|
|
3372
3414
|
mergeTo = curParagraphsGroup[i];
|
|
3373
3415
|
}
|
|
3374
3416
|
}
|
|
3375
3417
|
|
|
3376
|
-
//
|
|
3418
|
+
// Merge last paragraph
|
|
3377
3419
|
officeMarkup.modify.joinParagraphs(mergeTo, lastParagraph);
|
|
3378
3420
|
|
|
3379
|
-
//
|
|
3421
|
+
// Remove the old last paragraph (was merged into the new one)
|
|
3380
3422
|
xml.modify.remove(lastParagraph);
|
|
3381
3423
|
}
|
|
3382
3424
|
}
|
|
@@ -3609,7 +3651,7 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3609
3651
|
}
|
|
3610
3652
|
}
|
|
3611
3653
|
|
|
3612
|
-
//
|
|
3654
|
+
// Vars
|
|
3613
3655
|
const openTag = tags[0];
|
|
3614
3656
|
const closeTag = last(tags);
|
|
3615
3657
|
if (openTag.placement !== TagPlacement.TextNode) {
|
|
@@ -3618,28 +3660,34 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3618
3660
|
if (closeTag.placement !== TagPlacement.TextNode) {
|
|
3619
3661
|
throw new TemplateSyntaxError(`Loop closing tag "${closeTag.rawText}" must be placed in a text node but was placed in ${closeTag.placement}`);
|
|
3620
3662
|
}
|
|
3663
|
+
if (officeMarkup.query.containingStructuredTagContentNode(openTag.xmlTextNode)) {
|
|
3664
|
+
throw new TemplateSyntaxError(`Loop tag "${openTag.rawText}" cannot be placed inside a content control`);
|
|
3665
|
+
}
|
|
3666
|
+
if (officeMarkup.query.containingStructuredTagContentNode(closeTag.xmlTextNode)) {
|
|
3667
|
+
throw new TemplateSyntaxError(`Loop tag "${closeTag.rawText}" cannot be placed inside a content control`);
|
|
3668
|
+
}
|
|
3621
3669
|
|
|
3622
|
-
//
|
|
3670
|
+
// Select the suitable strategy
|
|
3623
3671
|
const loopStrategy = this.loopStrategies.find(strategy => strategy.isApplicable(openTag, closeTag, isCondition));
|
|
3624
3672
|
if (!loopStrategy) throw new Error(`No loop strategy found for tag '${openTag.rawText}'.`);
|
|
3625
3673
|
|
|
3626
|
-
//
|
|
3674
|
+
// Prepare to loop
|
|
3627
3675
|
const {
|
|
3628
3676
|
firstNode,
|
|
3629
3677
|
nodesToRepeat,
|
|
3630
3678
|
lastNode
|
|
3631
3679
|
} = loopStrategy.splitBefore(openTag, closeTag);
|
|
3632
3680
|
|
|
3633
|
-
//
|
|
3681
|
+
// Repeat (loop) the content
|
|
3634
3682
|
const repeatedNodes = this.repeat(nodesToRepeat, value.length);
|
|
3635
3683
|
|
|
3636
|
-
//
|
|
3684
|
+
// Recursive compilation
|
|
3637
3685
|
// (this step can be optimized in the future if we'll keep track of the
|
|
3638
3686
|
// path to each token and use that to create new tokens instead of
|
|
3639
3687
|
// search through the text again)
|
|
3640
3688
|
const compiledNodes = await this.compile(isCondition, repeatedNodes, data, context);
|
|
3641
3689
|
|
|
3642
|
-
//
|
|
3690
|
+
// Merge back to the document
|
|
3643
3691
|
loopStrategy.mergeBack(compiledNodes, firstNode, lastNode);
|
|
3644
3692
|
}
|
|
3645
3693
|
repeat(nodes, times) {
|
|
@@ -3654,19 +3702,19 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3654
3702
|
async compile(isCondition, nodeGroups, data, context) {
|
|
3655
3703
|
const compiledNodeGroups = [];
|
|
3656
3704
|
|
|
3657
|
-
//
|
|
3705
|
+
// Compile each node group with it's relevant data
|
|
3658
3706
|
for (let i = 0; i < nodeGroups.length; i++) {
|
|
3659
|
-
//
|
|
3707
|
+
// Create dummy root node
|
|
3660
3708
|
const curNodes = nodeGroups[i];
|
|
3661
3709
|
const dummyRootNode = xml.create.generalNode('dummyRootNode');
|
|
3662
3710
|
curNodes.forEach(node => xml.modify.appendChild(dummyRootNode, node));
|
|
3663
3711
|
|
|
3664
|
-
//
|
|
3712
|
+
// Compile the new root
|
|
3665
3713
|
const conditionTag = this.updatePathBefore(isCondition, data, i);
|
|
3666
3714
|
await this.utilities.compiler.compile(dummyRootNode, data, context);
|
|
3667
3715
|
this.updatePathAfter(isCondition, data, conditionTag);
|
|
3668
3716
|
|
|
3669
|
-
//
|
|
3717
|
+
// Disconnect from dummy root
|
|
3670
3718
|
const curResult = [];
|
|
3671
3719
|
while (dummyRootNode.childNodes && dummyRootNode.childNodes.length) {
|
|
3672
3720
|
const child = xml.modify.removeChild(dummyRootNode, 0);
|
|
@@ -3677,7 +3725,7 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3677
3725
|
return compiledNodeGroups;
|
|
3678
3726
|
}
|
|
3679
3727
|
updatePathBefore(isCondition, data, groupIndex) {
|
|
3680
|
-
//
|
|
3728
|
+
// If it's a condition - don't go deeper in the path
|
|
3681
3729
|
// (so we need to extract the already pushed condition tag)
|
|
3682
3730
|
if (isCondition) {
|
|
3683
3731
|
if (groupIndex > 0) {
|
|
@@ -3687,12 +3735,12 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3687
3735
|
return data.pathPop();
|
|
3688
3736
|
}
|
|
3689
3737
|
|
|
3690
|
-
//
|
|
3738
|
+
// Else, it's an array - push the current index
|
|
3691
3739
|
data.pathPush(groupIndex);
|
|
3692
3740
|
return null;
|
|
3693
3741
|
}
|
|
3694
3742
|
updatePathAfter(isCondition, data, conditionTag) {
|
|
3695
|
-
//
|
|
3743
|
+
// Reverse the "before" path operation
|
|
3696
3744
|
if (isCondition) {
|
|
3697
3745
|
data.pathPush(conditionTag);
|
|
3698
3746
|
} else {
|
|
@@ -5188,7 +5236,7 @@ class TemplateHandler {
|
|
|
5188
5236
|
/**
|
|
5189
5237
|
* Version number of the `easy-template-x` library.
|
|
5190
5238
|
*/
|
|
5191
|
-
version = "7.0.
|
|
5239
|
+
version = "7.0.3" ;
|
|
5192
5240
|
constructor(options) {
|
|
5193
5241
|
this.options = new TemplateHandlerOptions(options);
|
|
5194
5242
|
const delimiters = this.options.delimiters;
|
|
@@ -1766,6 +1766,26 @@ class W {
|
|
|
1766
1766
|
TableCell = 'w:tc';
|
|
1767
1767
|
Drawing = 'w:drawing';
|
|
1768
1768
|
NumberProperties = 'w:numPr';
|
|
1769
|
+
/**
|
|
1770
|
+
* Structured document tag (content control).
|
|
1771
|
+
*
|
|
1772
|
+
* See: ECMA-376, Part 1, sections 17.5 and 17.5.2
|
|
1773
|
+
*/
|
|
1774
|
+
StructuredTag = 'w:sdt';
|
|
1775
|
+
/**
|
|
1776
|
+
* Structured document tag properties.
|
|
1777
|
+
*/
|
|
1778
|
+
StructuredTagProperties = 'w:sdtPr';
|
|
1779
|
+
/**
|
|
1780
|
+
* Structured document tag content.
|
|
1781
|
+
*/
|
|
1782
|
+
StructuredTagContent = 'w:sdtContent';
|
|
1783
|
+
/**
|
|
1784
|
+
* Complex field character (legacy form field).
|
|
1785
|
+
*
|
|
1786
|
+
* see: http://officeopenxml.com/WPfields.php
|
|
1787
|
+
*/
|
|
1788
|
+
FieldChar = 'w:fldChar';
|
|
1769
1789
|
}
|
|
1770
1790
|
|
|
1771
1791
|
/**
|
|
@@ -1851,6 +1871,9 @@ class Pic {
|
|
|
1851
1871
|
* used in Office Open XML documents. Including but not limited to
|
|
1852
1872
|
* Wordprocessing Markup Language, Drawing Markup Language and Spreadsheet
|
|
1853
1873
|
* Markup Language.
|
|
1874
|
+
*
|
|
1875
|
+
* - For an easy introduction, see: http://officeopenxml.com/WPcontentOverview.php
|
|
1876
|
+
* - For the complete specification, see: https://ecma-international.org/publications-and-standards/standards/ecma-376/
|
|
1854
1877
|
*/
|
|
1855
1878
|
class OmlNode {
|
|
1856
1879
|
/**
|
|
@@ -1875,6 +1898,12 @@ class OmlNode {
|
|
|
1875
1898
|
}
|
|
1876
1899
|
class OmlAttribute {
|
|
1877
1900
|
static SpacePreserve = 'xml:space';
|
|
1901
|
+
/**
|
|
1902
|
+
* Complex field character type.
|
|
1903
|
+
*
|
|
1904
|
+
* see: http://officeopenxml.com/WPfields.php
|
|
1905
|
+
*/
|
|
1906
|
+
static FieldCharType = 'w:fldCharType';
|
|
1878
1907
|
}
|
|
1879
1908
|
|
|
1880
1909
|
//
|
|
@@ -1895,7 +1924,8 @@ class OmlAttribute {
|
|
|
1895
1924
|
// </w:r>
|
|
1896
1925
|
// </w:p>
|
|
1897
1926
|
//
|
|
1898
|
-
// see: http://officeopenxml.com/WPcontentOverview.php
|
|
1927
|
+
// - For an easy introduction, see: http://officeopenxml.com/WPcontentOverview.php
|
|
1928
|
+
// - For the complete specification, see: https://ecma-international.org/publications-and-standards/standards/ecma-376/
|
|
1899
1929
|
//
|
|
1900
1930
|
|
|
1901
1931
|
/**
|
|
@@ -2010,6 +2040,13 @@ class Query {
|
|
|
2010
2040
|
return xml.query.findParentByName(node, OmlNode.W.Table);
|
|
2011
2041
|
}
|
|
2012
2042
|
|
|
2043
|
+
/**
|
|
2044
|
+
* Search **upwards** for the first `w:sdtContent` node.
|
|
2045
|
+
*/
|
|
2046
|
+
containingStructuredTagContentNode(node) {
|
|
2047
|
+
return xml.query.findParentByName(node, OmlNode.W.StructuredTagContent);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2013
2050
|
//
|
|
2014
2051
|
// Advanced queries
|
|
2015
2052
|
//
|
|
@@ -2049,29 +2086,29 @@ class Modify {
|
|
|
2049
2086
|
let firstXmlTextNode;
|
|
2050
2087
|
let secondXmlTextNode;
|
|
2051
2088
|
|
|
2052
|
-
//
|
|
2089
|
+
// Split nodes
|
|
2053
2090
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
2054
2091
|
const newWordTextNode = xml.create.cloneNode(wordTextNode, true);
|
|
2055
2092
|
|
|
2056
|
-
//
|
|
2093
|
+
// Set space preserve to prevent display differences after splitting
|
|
2057
2094
|
// (otherwise if there was a space in the middle of the text node and it
|
|
2058
2095
|
// is now at the beginning or end of the text node it will be ignored)
|
|
2059
2096
|
officeMarkup.modify.setSpacePreserveAttribute(wordTextNode);
|
|
2060
2097
|
officeMarkup.modify.setSpacePreserveAttribute(newWordTextNode);
|
|
2061
2098
|
if (addBefore) {
|
|
2062
|
-
//
|
|
2099
|
+
// Insert new node before existing one
|
|
2063
2100
|
xml.modify.insertBefore(newWordTextNode, wordTextNode);
|
|
2064
2101
|
firstXmlTextNode = xml.query.lastTextChild(newWordTextNode);
|
|
2065
2102
|
secondXmlTextNode = textNode;
|
|
2066
2103
|
} else {
|
|
2067
|
-
//
|
|
2104
|
+
// Insert new node after existing one
|
|
2068
2105
|
const curIndex = wordTextNode.parentNode.childNodes.indexOf(wordTextNode);
|
|
2069
2106
|
xml.modify.insertChild(wordTextNode.parentNode, newWordTextNode, curIndex + 1);
|
|
2070
2107
|
firstXmlTextNode = textNode;
|
|
2071
2108
|
secondXmlTextNode = xml.query.lastTextChild(newWordTextNode);
|
|
2072
2109
|
}
|
|
2073
2110
|
|
|
2074
|
-
//
|
|
2111
|
+
// Edit text
|
|
2075
2112
|
const firstText = firstXmlTextNode.textContent;
|
|
2076
2113
|
const secondText = secondXmlTextNode.textContent;
|
|
2077
2114
|
firstXmlTextNode.textContent = firstText.substring(0, splitIndex);
|
|
@@ -2086,25 +2123,27 @@ class Modify {
|
|
|
2086
2123
|
* `false` then the original text node is the first text node of `right`.
|
|
2087
2124
|
*/
|
|
2088
2125
|
splitParagraphByTextNode(paragraph, textNode, removeTextNode) {
|
|
2089
|
-
//
|
|
2126
|
+
// Input validation
|
|
2090
2127
|
const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
|
|
2091
|
-
if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not
|
|
2128
|
+
if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not contained in the specified paragraph.`);
|
|
2092
2129
|
const runNode = officeMarkup.query.containingRunNode(textNode);
|
|
2093
2130
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
2094
2131
|
|
|
2095
|
-
//
|
|
2132
|
+
// 1. Split the run
|
|
2133
|
+
|
|
2134
|
+
// Create run clone (left) and keep the original run (right).
|
|
2096
2135
|
const leftRun = xml.create.cloneNode(runNode, false);
|
|
2097
2136
|
const rightRun = runNode;
|
|
2098
2137
|
xml.modify.insertBefore(leftRun, rightRun);
|
|
2099
2138
|
|
|
2100
|
-
//
|
|
2139
|
+
// Copy props from original run node (preserve style)
|
|
2101
2140
|
const runProps = rightRun.childNodes.find(node => officeMarkup.query.isRunPropertiesNode(node));
|
|
2102
2141
|
if (runProps) {
|
|
2103
2142
|
const leftRunProps = xml.create.cloneNode(runProps, true);
|
|
2104
2143
|
xml.modify.appendChild(leftRun, leftRunProps);
|
|
2105
2144
|
}
|
|
2106
2145
|
|
|
2107
|
-
//
|
|
2146
|
+
// Move all text nodes up to the specified text node, to the new run.
|
|
2108
2147
|
const firstRunChildIndex = runProps ? 1 : 0;
|
|
2109
2148
|
let curChild = rightRun.childNodes[firstRunChildIndex];
|
|
2110
2149
|
while (curChild != wordTextNode) {
|
|
@@ -2113,24 +2152,26 @@ class Modify {
|
|
|
2113
2152
|
curChild = rightRun.childNodes[firstRunChildIndex];
|
|
2114
2153
|
}
|
|
2115
2154
|
|
|
2116
|
-
//
|
|
2155
|
+
// Remove text node
|
|
2117
2156
|
if (removeTextNode) {
|
|
2118
2157
|
xml.modify.removeChild(rightRun, firstRunChildIndex);
|
|
2119
2158
|
}
|
|
2120
2159
|
|
|
2121
|
-
//
|
|
2160
|
+
// 2. Split the paragraph
|
|
2161
|
+
|
|
2162
|
+
// Create paragraph clone (left) and keep the original paragraph (right).
|
|
2122
2163
|
const leftPara = xml.create.cloneNode(containingParagraph, false);
|
|
2123
2164
|
const rightPara = containingParagraph;
|
|
2124
2165
|
xml.modify.insertBefore(leftPara, rightPara);
|
|
2125
2166
|
|
|
2126
|
-
//
|
|
2167
|
+
// Copy props from original paragraph (preserve style)
|
|
2127
2168
|
const paragraphProps = rightPara.childNodes.find(node => officeMarkup.query.isParagraphPropertiesNode(node));
|
|
2128
2169
|
if (paragraphProps) {
|
|
2129
2170
|
const leftParagraphProps = xml.create.cloneNode(paragraphProps, true);
|
|
2130
2171
|
xml.modify.appendChild(leftPara, leftParagraphProps);
|
|
2131
2172
|
}
|
|
2132
2173
|
|
|
2133
|
-
//
|
|
2174
|
+
// Move all run nodes up to the original run (right), to the new paragraph (left).
|
|
2134
2175
|
const firstParaChildIndex = paragraphProps ? 1 : 0;
|
|
2135
2176
|
curChild = rightPara.childNodes[firstParaChildIndex];
|
|
2136
2177
|
while (curChild != rightRun) {
|
|
@@ -2139,7 +2180,7 @@ class Modify {
|
|
|
2139
2180
|
curChild = rightPara.childNodes[firstParaChildIndex];
|
|
2140
2181
|
}
|
|
2141
2182
|
|
|
2142
|
-
//
|
|
2183
|
+
// Clean paragraphs - remove empty runs
|
|
2143
2184
|
if (officeMarkup.query.isEmptyRun(leftRun)) xml.modify.remove(leftRun);
|
|
2144
2185
|
if (officeMarkup.query.isEmptyRun(rightRun)) xml.modify.remove(rightRun);
|
|
2145
2186
|
return [leftPara, rightPara];
|
|
@@ -2149,21 +2190,21 @@ class Modify {
|
|
|
2149
2190
|
* Move all text between the 'from' and 'to' nodes to the 'from' node.
|
|
2150
2191
|
*/
|
|
2151
2192
|
joinTextNodesRange(from, to) {
|
|
2152
|
-
//
|
|
2193
|
+
// Find run nodes
|
|
2153
2194
|
const firstRunNode = officeMarkup.query.containingRunNode(from);
|
|
2154
2195
|
const secondRunNode = officeMarkup.query.containingRunNode(to);
|
|
2155
2196
|
const paragraphNode = firstRunNode.parentNode;
|
|
2156
2197
|
if (secondRunNode.parentNode !== paragraphNode) throw new Error('Can not join text nodes from separate paragraphs.');
|
|
2157
2198
|
|
|
2158
|
-
//
|
|
2199
|
+
// Find "word text nodes"
|
|
2159
2200
|
const firstWordTextNode = officeMarkup.query.containingTextNode(from);
|
|
2160
2201
|
const secondWordTextNode = officeMarkup.query.containingTextNode(to);
|
|
2161
2202
|
const totalText = [];
|
|
2162
2203
|
|
|
2163
|
-
//
|
|
2204
|
+
// Iterate runs
|
|
2164
2205
|
let curRunNode = firstRunNode;
|
|
2165
2206
|
while (curRunNode) {
|
|
2166
|
-
//
|
|
2207
|
+
// Iterate text nodes
|
|
2167
2208
|
let curWordTextNode;
|
|
2168
2209
|
if (curRunNode === firstRunNode) {
|
|
2169
2210
|
curWordTextNode = firstWordTextNode;
|
|
@@ -2176,11 +2217,11 @@ class Modify {
|
|
|
2176
2217
|
continue;
|
|
2177
2218
|
}
|
|
2178
2219
|
|
|
2179
|
-
//
|
|
2220
|
+
// Move text to first node
|
|
2180
2221
|
const curXmlTextNode = xml.query.lastTextChild(curWordTextNode);
|
|
2181
2222
|
totalText.push(curXmlTextNode.textContent);
|
|
2182
2223
|
|
|
2183
|
-
//
|
|
2224
|
+
// Next text node
|
|
2184
2225
|
const textToRemove = curWordTextNode;
|
|
2185
2226
|
if (curWordTextNode === secondWordTextNode) {
|
|
2186
2227
|
curWordTextNode = null;
|
|
@@ -2188,13 +2229,13 @@ class Modify {
|
|
|
2188
2229
|
curWordTextNode = curWordTextNode.nextSibling;
|
|
2189
2230
|
}
|
|
2190
2231
|
|
|
2191
|
-
//
|
|
2232
|
+
// Remove current text node
|
|
2192
2233
|
if (textToRemove !== firstWordTextNode) {
|
|
2193
2234
|
xml.modify.remove(textToRemove);
|
|
2194
2235
|
}
|
|
2195
2236
|
}
|
|
2196
2237
|
|
|
2197
|
-
//
|
|
2238
|
+
// Next run
|
|
2198
2239
|
const runToRemove = curRunNode;
|
|
2199
2240
|
if (curRunNode === secondRunNode) {
|
|
2200
2241
|
curRunNode = null;
|
|
@@ -2202,13 +2243,13 @@ class Modify {
|
|
|
2202
2243
|
curRunNode = curRunNode.nextSibling;
|
|
2203
2244
|
}
|
|
2204
2245
|
|
|
2205
|
-
//
|
|
2246
|
+
// Remove current run
|
|
2206
2247
|
if (!runToRemove.childNodes || !runToRemove.childNodes.length) {
|
|
2207
2248
|
xml.modify.remove(runToRemove);
|
|
2208
2249
|
}
|
|
2209
2250
|
}
|
|
2210
2251
|
|
|
2211
|
-
//
|
|
2252
|
+
// Set the text content
|
|
2212
2253
|
const firstXmlTextNode = xml.query.lastTextChild(firstWordTextNode);
|
|
2213
2254
|
firstXmlTextNode.textContent = totalText.join('');
|
|
2214
2255
|
}
|
|
@@ -3323,28 +3364,29 @@ class LoopParagraphStrategy {
|
|
|
3323
3364
|
return true;
|
|
3324
3365
|
}
|
|
3325
3366
|
splitBefore(openTag, closeTag) {
|
|
3326
|
-
//
|
|
3367
|
+
// Gather some info
|
|
3327
3368
|
let firstParagraph = officeMarkup.query.containingParagraphNode(openTag.xmlTextNode);
|
|
3328
3369
|
let lastParagraph = officeMarkup.query.containingParagraphNode(closeTag.xmlTextNode);
|
|
3329
3370
|
const areSame = firstParagraph === lastParagraph;
|
|
3330
3371
|
|
|
3331
|
-
//
|
|
3332
|
-
|
|
3372
|
+
// Split first paragraph
|
|
3373
|
+
const removeTextNode = true;
|
|
3374
|
+
let splitResult = officeMarkup.modify.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, removeTextNode);
|
|
3333
3375
|
firstParagraph = splitResult[0];
|
|
3334
3376
|
let afterFirstParagraph = splitResult[1];
|
|
3335
3377
|
if (areSame) lastParagraph = afterFirstParagraph;
|
|
3336
3378
|
|
|
3337
|
-
//
|
|
3338
|
-
splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode,
|
|
3379
|
+
// Split last paragraph
|
|
3380
|
+
splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, removeTextNode);
|
|
3339
3381
|
const beforeLastParagraph = splitResult[0];
|
|
3340
3382
|
lastParagraph = splitResult[1];
|
|
3341
3383
|
if (areSame) afterFirstParagraph = beforeLastParagraph;
|
|
3342
3384
|
|
|
3343
|
-
//
|
|
3385
|
+
// Disconnect splitted paragraph from their parents
|
|
3344
3386
|
xml.modify.remove(afterFirstParagraph);
|
|
3345
3387
|
if (!areSame) xml.modify.remove(beforeLastParagraph);
|
|
3346
3388
|
|
|
3347
|
-
//
|
|
3389
|
+
// Extract all paragraphs in between
|
|
3348
3390
|
let middleParagraphs;
|
|
3349
3391
|
if (areSame) {
|
|
3350
3392
|
middleParagraphs = [afterFirstParagraph];
|
|
@@ -3361,20 +3403,20 @@ class LoopParagraphStrategy {
|
|
|
3361
3403
|
mergeBack(middleParagraphs, firstParagraph, lastParagraph) {
|
|
3362
3404
|
let mergeTo = firstParagraph;
|
|
3363
3405
|
for (const curParagraphsGroup of middleParagraphs) {
|
|
3364
|
-
//
|
|
3406
|
+
// Merge first paragraphs
|
|
3365
3407
|
officeMarkup.modify.joinParagraphs(mergeTo, curParagraphsGroup[0]);
|
|
3366
3408
|
|
|
3367
|
-
//
|
|
3409
|
+
// Add middle and last paragraphs to the original document
|
|
3368
3410
|
for (let i = 1; i < curParagraphsGroup.length; i++) {
|
|
3369
3411
|
xml.modify.insertBefore(curParagraphsGroup[i], lastParagraph);
|
|
3370
3412
|
mergeTo = curParagraphsGroup[i];
|
|
3371
3413
|
}
|
|
3372
3414
|
}
|
|
3373
3415
|
|
|
3374
|
-
//
|
|
3416
|
+
// Merge last paragraph
|
|
3375
3417
|
officeMarkup.modify.joinParagraphs(mergeTo, lastParagraph);
|
|
3376
3418
|
|
|
3377
|
-
//
|
|
3419
|
+
// Remove the old last paragraph (was merged into the new one)
|
|
3378
3420
|
xml.modify.remove(lastParagraph);
|
|
3379
3421
|
}
|
|
3380
3422
|
}
|
|
@@ -3607,7 +3649,7 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3607
3649
|
}
|
|
3608
3650
|
}
|
|
3609
3651
|
|
|
3610
|
-
//
|
|
3652
|
+
// Vars
|
|
3611
3653
|
const openTag = tags[0];
|
|
3612
3654
|
const closeTag = last(tags);
|
|
3613
3655
|
if (openTag.placement !== TagPlacement.TextNode) {
|
|
@@ -3616,28 +3658,34 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3616
3658
|
if (closeTag.placement !== TagPlacement.TextNode) {
|
|
3617
3659
|
throw new TemplateSyntaxError(`Loop closing tag "${closeTag.rawText}" must be placed in a text node but was placed in ${closeTag.placement}`);
|
|
3618
3660
|
}
|
|
3661
|
+
if (officeMarkup.query.containingStructuredTagContentNode(openTag.xmlTextNode)) {
|
|
3662
|
+
throw new TemplateSyntaxError(`Loop tag "${openTag.rawText}" cannot be placed inside a content control`);
|
|
3663
|
+
}
|
|
3664
|
+
if (officeMarkup.query.containingStructuredTagContentNode(closeTag.xmlTextNode)) {
|
|
3665
|
+
throw new TemplateSyntaxError(`Loop tag "${closeTag.rawText}" cannot be placed inside a content control`);
|
|
3666
|
+
}
|
|
3619
3667
|
|
|
3620
|
-
//
|
|
3668
|
+
// Select the suitable strategy
|
|
3621
3669
|
const loopStrategy = this.loopStrategies.find(strategy => strategy.isApplicable(openTag, closeTag, isCondition));
|
|
3622
3670
|
if (!loopStrategy) throw new Error(`No loop strategy found for tag '${openTag.rawText}'.`);
|
|
3623
3671
|
|
|
3624
|
-
//
|
|
3672
|
+
// Prepare to loop
|
|
3625
3673
|
const {
|
|
3626
3674
|
firstNode,
|
|
3627
3675
|
nodesToRepeat,
|
|
3628
3676
|
lastNode
|
|
3629
3677
|
} = loopStrategy.splitBefore(openTag, closeTag);
|
|
3630
3678
|
|
|
3631
|
-
//
|
|
3679
|
+
// Repeat (loop) the content
|
|
3632
3680
|
const repeatedNodes = this.repeat(nodesToRepeat, value.length);
|
|
3633
3681
|
|
|
3634
|
-
//
|
|
3682
|
+
// Recursive compilation
|
|
3635
3683
|
// (this step can be optimized in the future if we'll keep track of the
|
|
3636
3684
|
// path to each token and use that to create new tokens instead of
|
|
3637
3685
|
// search through the text again)
|
|
3638
3686
|
const compiledNodes = await this.compile(isCondition, repeatedNodes, data, context);
|
|
3639
3687
|
|
|
3640
|
-
//
|
|
3688
|
+
// Merge back to the document
|
|
3641
3689
|
loopStrategy.mergeBack(compiledNodes, firstNode, lastNode);
|
|
3642
3690
|
}
|
|
3643
3691
|
repeat(nodes, times) {
|
|
@@ -3652,19 +3700,19 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3652
3700
|
async compile(isCondition, nodeGroups, data, context) {
|
|
3653
3701
|
const compiledNodeGroups = [];
|
|
3654
3702
|
|
|
3655
|
-
//
|
|
3703
|
+
// Compile each node group with it's relevant data
|
|
3656
3704
|
for (let i = 0; i < nodeGroups.length; i++) {
|
|
3657
|
-
//
|
|
3705
|
+
// Create dummy root node
|
|
3658
3706
|
const curNodes = nodeGroups[i];
|
|
3659
3707
|
const dummyRootNode = xml.create.generalNode('dummyRootNode');
|
|
3660
3708
|
curNodes.forEach(node => xml.modify.appendChild(dummyRootNode, node));
|
|
3661
3709
|
|
|
3662
|
-
//
|
|
3710
|
+
// Compile the new root
|
|
3663
3711
|
const conditionTag = this.updatePathBefore(isCondition, data, i);
|
|
3664
3712
|
await this.utilities.compiler.compile(dummyRootNode, data, context);
|
|
3665
3713
|
this.updatePathAfter(isCondition, data, conditionTag);
|
|
3666
3714
|
|
|
3667
|
-
//
|
|
3715
|
+
// Disconnect from dummy root
|
|
3668
3716
|
const curResult = [];
|
|
3669
3717
|
while (dummyRootNode.childNodes && dummyRootNode.childNodes.length) {
|
|
3670
3718
|
const child = xml.modify.removeChild(dummyRootNode, 0);
|
|
@@ -3675,7 +3723,7 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3675
3723
|
return compiledNodeGroups;
|
|
3676
3724
|
}
|
|
3677
3725
|
updatePathBefore(isCondition, data, groupIndex) {
|
|
3678
|
-
//
|
|
3726
|
+
// If it's a condition - don't go deeper in the path
|
|
3679
3727
|
// (so we need to extract the already pushed condition tag)
|
|
3680
3728
|
if (isCondition) {
|
|
3681
3729
|
if (groupIndex > 0) {
|
|
@@ -3685,12 +3733,12 @@ class LoopPlugin extends TemplatePlugin {
|
|
|
3685
3733
|
return data.pathPop();
|
|
3686
3734
|
}
|
|
3687
3735
|
|
|
3688
|
-
//
|
|
3736
|
+
// Else, it's an array - push the current index
|
|
3689
3737
|
data.pathPush(groupIndex);
|
|
3690
3738
|
return null;
|
|
3691
3739
|
}
|
|
3692
3740
|
updatePathAfter(isCondition, data, conditionTag) {
|
|
3693
|
-
//
|
|
3741
|
+
// Reverse the "before" path operation
|
|
3694
3742
|
if (isCondition) {
|
|
3695
3743
|
data.pathPush(conditionTag);
|
|
3696
3744
|
} else {
|
|
@@ -5186,7 +5234,7 @@ class TemplateHandler {
|
|
|
5186
5234
|
/**
|
|
5187
5235
|
* Version number of the `easy-template-x` library.
|
|
5188
5236
|
*/
|
|
5189
|
-
version = "7.0.
|
|
5237
|
+
version = "7.0.3" ;
|
|
5190
5238
|
constructor(options) {
|
|
5191
5239
|
this.options = new TemplateHandlerOptions(options);
|
|
5192
5240
|
const delimiters = this.options.delimiters;
|
|
@@ -21,6 +21,7 @@ declare class Query {
|
|
|
21
21
|
containingTableRowNode(node: XmlNode): XmlGeneralNode;
|
|
22
22
|
containingTableCellNode(node: XmlNode): XmlGeneralNode;
|
|
23
23
|
containingTableNode(node: XmlNode): XmlGeneralNode;
|
|
24
|
+
containingStructuredTagContentNode(node: XmlNode): XmlGeneralNode;
|
|
24
25
|
isEmptyTextNode(node: XmlNode): boolean;
|
|
25
26
|
isEmptyRun(node: XmlNode): boolean;
|
|
26
27
|
}
|
|
@@ -9,6 +9,10 @@ declare class W {
|
|
|
9
9
|
readonly TableCell = "w:tc";
|
|
10
10
|
readonly Drawing = "w:drawing";
|
|
11
11
|
readonly NumberProperties = "w:numPr";
|
|
12
|
+
readonly StructuredTag = "w:sdt";
|
|
13
|
+
readonly StructuredTagProperties = "w:sdtPr";
|
|
14
|
+
readonly StructuredTagContent = "w:sdtContent";
|
|
15
|
+
readonly FieldChar = "w:fldChar";
|
|
12
16
|
}
|
|
13
17
|
declare class A {
|
|
14
18
|
readonly Paragraph = "a:p";
|
|
@@ -44,5 +48,6 @@ export declare class OmlNode {
|
|
|
44
48
|
}
|
|
45
49
|
export declare class OmlAttribute {
|
|
46
50
|
static readonly SpacePreserve = "xml:space";
|
|
51
|
+
static readonly FieldCharType = "w:fldCharType";
|
|
47
52
|
}
|
|
48
53
|
export {};
|
package/package.json
CHANGED
|
@@ -20,7 +20,8 @@ import { OmlAttribute, OmlNode } from "./omlNode";
|
|
|
20
20
|
// </w:r>
|
|
21
21
|
// </w:p>
|
|
22
22
|
//
|
|
23
|
-
// see: http://officeopenxml.com/WPcontentOverview.php
|
|
23
|
+
// - For an easy introduction, see: http://officeopenxml.com/WPcontentOverview.php
|
|
24
|
+
// - For the complete specification, see: https://ecma-international.org/publications-and-standards/standards/ecma-376/
|
|
24
25
|
//
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -161,6 +162,13 @@ class Query {
|
|
|
161
162
|
return xml.query.findParentByName(node, OmlNode.W.Table);
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Search **upwards** for the first `w:sdtContent` node.
|
|
167
|
+
*/
|
|
168
|
+
public containingStructuredTagContentNode(node: XmlNode): XmlGeneralNode {
|
|
169
|
+
return xml.query.findParentByName(node, OmlNode.W.StructuredTagContent);
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
//
|
|
165
173
|
// Advanced queries
|
|
166
174
|
//
|
|
@@ -219,11 +227,11 @@ class Modify {
|
|
|
219
227
|
let firstXmlTextNode: XmlTextNode;
|
|
220
228
|
let secondXmlTextNode: XmlTextNode;
|
|
221
229
|
|
|
222
|
-
//
|
|
230
|
+
// Split nodes
|
|
223
231
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
224
232
|
const newWordTextNode = xml.create.cloneNode(wordTextNode, true);
|
|
225
233
|
|
|
226
|
-
//
|
|
234
|
+
// Set space preserve to prevent display differences after splitting
|
|
227
235
|
// (otherwise if there was a space in the middle of the text node and it
|
|
228
236
|
// is now at the beginning or end of the text node it will be ignored)
|
|
229
237
|
officeMarkup.modify.setSpacePreserveAttribute(wordTextNode);
|
|
@@ -231,7 +239,7 @@ class Modify {
|
|
|
231
239
|
|
|
232
240
|
if (addBefore) {
|
|
233
241
|
|
|
234
|
-
//
|
|
242
|
+
// Insert new node before existing one
|
|
235
243
|
xml.modify.insertBefore(newWordTextNode, wordTextNode);
|
|
236
244
|
|
|
237
245
|
firstXmlTextNode = xml.query.lastTextChild(newWordTextNode);
|
|
@@ -239,7 +247,7 @@ class Modify {
|
|
|
239
247
|
|
|
240
248
|
} else {
|
|
241
249
|
|
|
242
|
-
//
|
|
250
|
+
// Insert new node after existing one
|
|
243
251
|
const curIndex = wordTextNode.parentNode.childNodes.indexOf(wordTextNode);
|
|
244
252
|
xml.modify.insertChild(wordTextNode.parentNode, newWordTextNode, curIndex + 1);
|
|
245
253
|
|
|
@@ -247,7 +255,7 @@ class Modify {
|
|
|
247
255
|
secondXmlTextNode = xml.query.lastTextChild(newWordTextNode);
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
//
|
|
258
|
+
// Edit text
|
|
251
259
|
const firstText = firstXmlTextNode.textContent;
|
|
252
260
|
const secondText = secondXmlTextNode.textContent;
|
|
253
261
|
firstXmlTextNode.textContent = firstText.substring(0, splitIndex);
|
|
@@ -264,27 +272,29 @@ class Modify {
|
|
|
264
272
|
*/
|
|
265
273
|
public splitParagraphByTextNode(paragraph: XmlNode, textNode: XmlTextNode, removeTextNode: boolean): [XmlNode, XmlNode] {
|
|
266
274
|
|
|
267
|
-
//
|
|
275
|
+
// Input validation
|
|
268
276
|
const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
|
|
269
277
|
if (containingParagraph != paragraph)
|
|
270
|
-
throw new Error(`Node 'textNode' is not
|
|
278
|
+
throw new Error(`Node 'textNode' is not contained in the specified paragraph.`);
|
|
271
279
|
|
|
272
280
|
const runNode = officeMarkup.query.containingRunNode(textNode);
|
|
273
281
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
274
282
|
|
|
275
|
-
//
|
|
283
|
+
// 1. Split the run
|
|
284
|
+
|
|
285
|
+
// Create run clone (left) and keep the original run (right).
|
|
276
286
|
const leftRun = xml.create.cloneNode(runNode, false);
|
|
277
287
|
const rightRun = runNode;
|
|
278
288
|
xml.modify.insertBefore(leftRun, rightRun);
|
|
279
289
|
|
|
280
|
-
//
|
|
290
|
+
// Copy props from original run node (preserve style)
|
|
281
291
|
const runProps = rightRun.childNodes.find(node => officeMarkup.query.isRunPropertiesNode(node));
|
|
282
292
|
if (runProps) {
|
|
283
293
|
const leftRunProps = xml.create.cloneNode(runProps, true);
|
|
284
294
|
xml.modify.appendChild(leftRun, leftRunProps);
|
|
285
295
|
}
|
|
286
296
|
|
|
287
|
-
//
|
|
297
|
+
// Move all text nodes up to the specified text node, to the new run.
|
|
288
298
|
const firstRunChildIndex = (runProps ? 1 : 0);
|
|
289
299
|
let curChild = rightRun.childNodes[firstRunChildIndex];
|
|
290
300
|
while (curChild != wordTextNode) {
|
|
@@ -293,24 +303,26 @@ class Modify {
|
|
|
293
303
|
curChild = rightRun.childNodes[firstRunChildIndex];
|
|
294
304
|
}
|
|
295
305
|
|
|
296
|
-
//
|
|
306
|
+
// Remove text node
|
|
297
307
|
if (removeTextNode) {
|
|
298
308
|
xml.modify.removeChild(rightRun, firstRunChildIndex);
|
|
299
309
|
}
|
|
300
310
|
|
|
301
|
-
//
|
|
311
|
+
// 2. Split the paragraph
|
|
312
|
+
|
|
313
|
+
// Create paragraph clone (left) and keep the original paragraph (right).
|
|
302
314
|
const leftPara = xml.create.cloneNode(containingParagraph, false);
|
|
303
315
|
const rightPara = containingParagraph;
|
|
304
316
|
xml.modify.insertBefore(leftPara, rightPara);
|
|
305
317
|
|
|
306
|
-
//
|
|
318
|
+
// Copy props from original paragraph (preserve style)
|
|
307
319
|
const paragraphProps = rightPara.childNodes.find(node => officeMarkup.query.isParagraphPropertiesNode(node));
|
|
308
320
|
if (paragraphProps) {
|
|
309
321
|
const leftParagraphProps = xml.create.cloneNode(paragraphProps, true);
|
|
310
322
|
xml.modify.appendChild(leftPara, leftParagraphProps);
|
|
311
323
|
}
|
|
312
324
|
|
|
313
|
-
//
|
|
325
|
+
// Move all run nodes up to the original run (right), to the new paragraph (left).
|
|
314
326
|
const firstParaChildIndex = (paragraphProps ? 1 : 0);
|
|
315
327
|
curChild = rightPara.childNodes[firstParaChildIndex];
|
|
316
328
|
while (curChild != rightRun) {
|
|
@@ -319,7 +331,7 @@ class Modify {
|
|
|
319
331
|
curChild = rightPara.childNodes[firstParaChildIndex];
|
|
320
332
|
}
|
|
321
333
|
|
|
322
|
-
//
|
|
334
|
+
// Clean paragraphs - remove empty runs
|
|
323
335
|
if (officeMarkup.query.isEmptyRun(leftRun))
|
|
324
336
|
xml.modify.remove(leftRun);
|
|
325
337
|
if (officeMarkup.query.isEmptyRun(rightRun))
|
|
@@ -333,7 +345,7 @@ class Modify {
|
|
|
333
345
|
*/
|
|
334
346
|
public joinTextNodesRange(from: XmlTextNode, to: XmlTextNode): void {
|
|
335
347
|
|
|
336
|
-
//
|
|
348
|
+
// Find run nodes
|
|
337
349
|
const firstRunNode = officeMarkup.query.containingRunNode(from);
|
|
338
350
|
const secondRunNode = officeMarkup.query.containingRunNode(to);
|
|
339
351
|
|
|
@@ -341,16 +353,16 @@ class Modify {
|
|
|
341
353
|
if (secondRunNode.parentNode !== paragraphNode)
|
|
342
354
|
throw new Error('Can not join text nodes from separate paragraphs.');
|
|
343
355
|
|
|
344
|
-
//
|
|
356
|
+
// Find "word text nodes"
|
|
345
357
|
const firstWordTextNode = officeMarkup.query.containingTextNode(from);
|
|
346
358
|
const secondWordTextNode = officeMarkup.query.containingTextNode(to);
|
|
347
359
|
const totalText: string[] = [];
|
|
348
360
|
|
|
349
|
-
//
|
|
361
|
+
// Iterate runs
|
|
350
362
|
let curRunNode: XmlNode = firstRunNode;
|
|
351
363
|
while (curRunNode) {
|
|
352
364
|
|
|
353
|
-
//
|
|
365
|
+
// Iterate text nodes
|
|
354
366
|
let curWordTextNode: XmlNode;
|
|
355
367
|
if (curRunNode === firstRunNode) {
|
|
356
368
|
curWordTextNode = firstWordTextNode;
|
|
@@ -364,11 +376,11 @@ class Modify {
|
|
|
364
376
|
continue;
|
|
365
377
|
}
|
|
366
378
|
|
|
367
|
-
//
|
|
379
|
+
// Move text to first node
|
|
368
380
|
const curXmlTextNode = xml.query.lastTextChild(curWordTextNode);
|
|
369
381
|
totalText.push(curXmlTextNode.textContent);
|
|
370
382
|
|
|
371
|
-
//
|
|
383
|
+
// Next text node
|
|
372
384
|
const textToRemove = curWordTextNode;
|
|
373
385
|
if (curWordTextNode === secondWordTextNode) {
|
|
374
386
|
curWordTextNode = null;
|
|
@@ -376,13 +388,13 @@ class Modify {
|
|
|
376
388
|
curWordTextNode = curWordTextNode.nextSibling;
|
|
377
389
|
}
|
|
378
390
|
|
|
379
|
-
//
|
|
391
|
+
// Remove current text node
|
|
380
392
|
if (textToRemove !== firstWordTextNode) {
|
|
381
393
|
xml.modify.remove(textToRemove);
|
|
382
394
|
}
|
|
383
395
|
}
|
|
384
396
|
|
|
385
|
-
//
|
|
397
|
+
// Next run
|
|
386
398
|
const runToRemove = curRunNode;
|
|
387
399
|
if (curRunNode === secondRunNode) {
|
|
388
400
|
curRunNode = null;
|
|
@@ -390,13 +402,13 @@ class Modify {
|
|
|
390
402
|
curRunNode = curRunNode.nextSibling;
|
|
391
403
|
}
|
|
392
404
|
|
|
393
|
-
//
|
|
405
|
+
// Remove current run
|
|
394
406
|
if (!runToRemove.childNodes || !runToRemove.childNodes.length) {
|
|
395
407
|
xml.modify.remove(runToRemove);
|
|
396
408
|
}
|
|
397
409
|
}
|
|
398
410
|
|
|
399
|
-
//
|
|
411
|
+
// Set the text content
|
|
400
412
|
const firstXmlTextNode = xml.query.lastTextChild(firstWordTextNode);
|
|
401
413
|
firstXmlTextNode.textContent = totalText.join('');
|
|
402
414
|
}
|
package/src/office/omlNode.ts
CHANGED
|
@@ -13,6 +13,26 @@ class W {
|
|
|
13
13
|
public readonly TableCell = 'w:tc';
|
|
14
14
|
public readonly Drawing = 'w:drawing';
|
|
15
15
|
public readonly NumberProperties = 'w:numPr';
|
|
16
|
+
/**
|
|
17
|
+
* Structured document tag (content control).
|
|
18
|
+
*
|
|
19
|
+
* See: ECMA-376, Part 1, sections 17.5 and 17.5.2
|
|
20
|
+
*/
|
|
21
|
+
public readonly StructuredTag = 'w:sdt';
|
|
22
|
+
/**
|
|
23
|
+
* Structured document tag properties.
|
|
24
|
+
*/
|
|
25
|
+
public readonly StructuredTagProperties = 'w:sdtPr';
|
|
26
|
+
/**
|
|
27
|
+
* Structured document tag content.
|
|
28
|
+
*/
|
|
29
|
+
public readonly StructuredTagContent = 'w:sdtContent';
|
|
30
|
+
/**
|
|
31
|
+
* Complex field character (legacy form field).
|
|
32
|
+
*
|
|
33
|
+
* see: http://officeopenxml.com/WPfields.php
|
|
34
|
+
*/
|
|
35
|
+
public readonly FieldChar = 'w:fldChar';
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
/**
|
|
@@ -98,6 +118,9 @@ class Pic {
|
|
|
98
118
|
* used in Office Open XML documents. Including but not limited to
|
|
99
119
|
* Wordprocessing Markup Language, Drawing Markup Language and Spreadsheet
|
|
100
120
|
* Markup Language.
|
|
121
|
+
*
|
|
122
|
+
* - For an easy introduction, see: http://officeopenxml.com/WPcontentOverview.php
|
|
123
|
+
* - For the complete specification, see: https://ecma-international.org/publications-and-standards/standards/ecma-376/
|
|
101
124
|
*/
|
|
102
125
|
export class OmlNode {
|
|
103
126
|
|
|
@@ -124,4 +147,10 @@ export class OmlNode {
|
|
|
124
147
|
|
|
125
148
|
export class OmlAttribute {
|
|
126
149
|
public static readonly SpacePreserve = 'xml:space';
|
|
150
|
+
/**
|
|
151
|
+
* Complex field character type.
|
|
152
|
+
*
|
|
153
|
+
* see: http://officeopenxml.com/WPfields.php
|
|
154
|
+
*/
|
|
155
|
+
public static readonly FieldCharType = 'w:fldCharType';
|
|
127
156
|
}
|
|
@@ -2,6 +2,7 @@ import { PathPart, ScopeData } from "src/compilation/scopeData";
|
|
|
2
2
|
import { Tag, TagPlacement } from "src/compilation/tag";
|
|
3
3
|
import { TemplateContext } from "src/compilation/templateContext";
|
|
4
4
|
import { TemplateSyntaxError } from "src/errors";
|
|
5
|
+
import { officeMarkup } from "src/office/officeMarkup";
|
|
5
6
|
import { PluginUtilities, TemplatePlugin } from "src/plugins/templatePlugin";
|
|
6
7
|
import { TemplateData } from "src/templateData";
|
|
7
8
|
import { last } from "src/utils";
|
|
@@ -39,7 +40,7 @@ export class LoopPlugin extends TemplatePlugin {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
//
|
|
43
|
+
// Vars
|
|
43
44
|
const openTag = tags[0];
|
|
44
45
|
const closeTag = last(tags);
|
|
45
46
|
|
|
@@ -49,25 +50,31 @@ export class LoopPlugin extends TemplatePlugin {
|
|
|
49
50
|
if (closeTag.placement !== TagPlacement.TextNode) {
|
|
50
51
|
throw new TemplateSyntaxError(`Loop closing tag "${closeTag.rawText}" must be placed in a text node but was placed in ${closeTag.placement}`);
|
|
51
52
|
}
|
|
53
|
+
if (officeMarkup.query.containingStructuredTagContentNode(openTag.xmlTextNode)) {
|
|
54
|
+
throw new TemplateSyntaxError(`Loop tag "${openTag.rawText}" cannot be placed inside a content control`);
|
|
55
|
+
}
|
|
56
|
+
if (officeMarkup.query.containingStructuredTagContentNode(closeTag.xmlTextNode)) {
|
|
57
|
+
throw new TemplateSyntaxError(`Loop tag "${closeTag.rawText}" cannot be placed inside a content control`);
|
|
58
|
+
}
|
|
52
59
|
|
|
53
|
-
//
|
|
60
|
+
// Select the suitable strategy
|
|
54
61
|
const loopStrategy = this.loopStrategies.find(strategy => strategy.isApplicable(openTag, closeTag, isCondition));
|
|
55
62
|
if (!loopStrategy)
|
|
56
63
|
throw new Error(`No loop strategy found for tag '${openTag.rawText}'.`);
|
|
57
64
|
|
|
58
|
-
//
|
|
65
|
+
// Prepare to loop
|
|
59
66
|
const { firstNode, nodesToRepeat, lastNode } = loopStrategy.splitBefore(openTag, closeTag);
|
|
60
67
|
|
|
61
|
-
//
|
|
68
|
+
// Repeat (loop) the content
|
|
62
69
|
const repeatedNodes = this.repeat(nodesToRepeat, value.length);
|
|
63
70
|
|
|
64
|
-
//
|
|
71
|
+
// Recursive compilation
|
|
65
72
|
// (this step can be optimized in the future if we'll keep track of the
|
|
66
73
|
// path to each token and use that to create new tokens instead of
|
|
67
74
|
// search through the text again)
|
|
68
75
|
const compiledNodes = await this.compile(isCondition, repeatedNodes, data, context);
|
|
69
76
|
|
|
70
|
-
//
|
|
77
|
+
// Merge back to the document
|
|
71
78
|
loopStrategy.mergeBack(compiledNodes, firstNode, lastNode);
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -88,20 +95,20 @@ export class LoopPlugin extends TemplatePlugin {
|
|
|
88
95
|
private async compile(isCondition: boolean, nodeGroups: XmlNode[][], data: ScopeData, context: TemplateContext): Promise<XmlNode[][]> {
|
|
89
96
|
const compiledNodeGroups: XmlNode[][] = [];
|
|
90
97
|
|
|
91
|
-
//
|
|
98
|
+
// Compile each node group with it's relevant data
|
|
92
99
|
for (let i = 0; i < nodeGroups.length; i++) {
|
|
93
100
|
|
|
94
|
-
//
|
|
101
|
+
// Create dummy root node
|
|
95
102
|
const curNodes = nodeGroups[i];
|
|
96
103
|
const dummyRootNode = xml.create.generalNode('dummyRootNode');
|
|
97
104
|
curNodes.forEach(node => xml.modify.appendChild(dummyRootNode, node));
|
|
98
105
|
|
|
99
|
-
//
|
|
106
|
+
// Compile the new root
|
|
100
107
|
const conditionTag = this.updatePathBefore(isCondition, data, i);
|
|
101
108
|
await this.utilities.compiler.compile(dummyRootNode, data, context);
|
|
102
109
|
this.updatePathAfter(isCondition, data, conditionTag);
|
|
103
110
|
|
|
104
|
-
//
|
|
111
|
+
// Disconnect from dummy root
|
|
105
112
|
const curResult: XmlNode[] = [];
|
|
106
113
|
while (dummyRootNode.childNodes && dummyRootNode.childNodes.length) {
|
|
107
114
|
const child = xml.modify.removeChild(dummyRootNode, 0);
|
|
@@ -115,7 +122,7 @@ export class LoopPlugin extends TemplatePlugin {
|
|
|
115
122
|
|
|
116
123
|
private updatePathBefore(isCondition: boolean, data: ScopeData, groupIndex: number): PathPart {
|
|
117
124
|
|
|
118
|
-
//
|
|
125
|
+
// If it's a condition - don't go deeper in the path
|
|
119
126
|
// (so we need to extract the already pushed condition tag)
|
|
120
127
|
if (isCondition) {
|
|
121
128
|
if (groupIndex > 0) {
|
|
@@ -125,14 +132,14 @@ export class LoopPlugin extends TemplatePlugin {
|
|
|
125
132
|
return data.pathPop();
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
//
|
|
135
|
+
// Else, it's an array - push the current index
|
|
129
136
|
data.pathPush(groupIndex);
|
|
130
137
|
return null;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
private updatePathAfter(isCondition: boolean, data: ScopeData, conditionTag: PathPart): void {
|
|
134
141
|
|
|
135
|
-
//
|
|
142
|
+
// Reverse the "before" path operation
|
|
136
143
|
if (isCondition) {
|
|
137
144
|
data.pathPush(conditionTag);
|
|
138
145
|
} else {
|
|
@@ -11,31 +11,32 @@ export class LoopParagraphStrategy implements ILoopStrategy {
|
|
|
11
11
|
|
|
12
12
|
public splitBefore(openTag: TextNodeTag, closeTag: TextNodeTag): SplitBeforeResult {
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Gather some info
|
|
15
15
|
let firstParagraph: XmlNode = officeMarkup.query.containingParagraphNode(openTag.xmlTextNode);
|
|
16
16
|
let lastParagraph: XmlNode = officeMarkup.query.containingParagraphNode(closeTag.xmlTextNode);
|
|
17
17
|
const areSame = (firstParagraph === lastParagraph);
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
19
|
+
// Split first paragraph
|
|
20
|
+
const removeTextNode = true;
|
|
21
|
+
let splitResult = officeMarkup.modify.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, removeTextNode);
|
|
21
22
|
firstParagraph = splitResult[0];
|
|
22
23
|
let afterFirstParagraph = splitResult[1];
|
|
23
24
|
if (areSame)
|
|
24
25
|
lastParagraph = afterFirstParagraph;
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode,
|
|
27
|
+
// Split last paragraph
|
|
28
|
+
splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, removeTextNode);
|
|
28
29
|
const beforeLastParagraph = splitResult[0];
|
|
29
30
|
lastParagraph = splitResult[1];
|
|
30
31
|
if (areSame)
|
|
31
32
|
afterFirstParagraph = beforeLastParagraph;
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
+
// Disconnect splitted paragraph from their parents
|
|
34
35
|
xml.modify.remove(afterFirstParagraph);
|
|
35
36
|
if (!areSame)
|
|
36
37
|
xml.modify.remove(beforeLastParagraph);
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
+
// Extract all paragraphs in between
|
|
39
40
|
let middleParagraphs: XmlNode[];
|
|
40
41
|
if (areSame) {
|
|
41
42
|
middleParagraphs = [afterFirstParagraph];
|
|
@@ -56,20 +57,20 @@ export class LoopParagraphStrategy implements ILoopStrategy {
|
|
|
56
57
|
let mergeTo = firstParagraph;
|
|
57
58
|
for (const curParagraphsGroup of middleParagraphs) {
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
+
// Merge first paragraphs
|
|
60
61
|
officeMarkup.modify.joinParagraphs(mergeTo, curParagraphsGroup[0]);
|
|
61
62
|
|
|
62
|
-
//
|
|
63
|
+
// Add middle and last paragraphs to the original document
|
|
63
64
|
for (let i = 1; i < curParagraphsGroup.length; i++) {
|
|
64
65
|
xml.modify.insertBefore(curParagraphsGroup[i], lastParagraph);
|
|
65
66
|
mergeTo = curParagraphsGroup[i];
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
+
// Merge last paragraph
|
|
70
71
|
officeMarkup.modify.joinParagraphs(mergeTo, lastParagraph);
|
|
71
72
|
|
|
72
|
-
//
|
|
73
|
+
// Remove the old last paragraph (was merged into the new one)
|
|
73
74
|
xml.modify.remove(lastParagraph);
|
|
74
75
|
}
|
|
75
76
|
}
|