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 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
- > ✓ PDF support
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
- #### Data Binding Extension
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
- // split nodes
2091
+ // Split nodes
2055
2092
  const wordTextNode = officeMarkup.query.containingTextNode(textNode);
2056
2093
  const newWordTextNode = xml.create.cloneNode(wordTextNode, true);
2057
2094
 
2058
- // set space preserve to prevent display differences after splitting
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
- // insert new node before existing one
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
- // insert new node after existing one
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
- // edit text
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
- // input validation
2128
+ // Input validation
2092
2129
  const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
2093
- if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
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
- // create run clone
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
- // copy props from original run node (preserve style)
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
- // move nodes from 'right' to 'left'
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
- // remove text node
2157
+ // Remove text node
2119
2158
  if (removeTextNode) {
2120
2159
  xml.modify.removeChild(rightRun, firstRunChildIndex);
2121
2160
  }
2122
2161
 
2123
- // create paragraph clone
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
- // copy props from original paragraph (preserve style)
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
- // move nodes from 'right' to 'left'
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
- // clean paragraphs - remove empty runs
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
- // find run nodes
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
- // find "word text nodes"
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
- // iterate runs
2206
+ // Iterate runs
2166
2207
  let curRunNode = firstRunNode;
2167
2208
  while (curRunNode) {
2168
- // iterate text nodes
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
- // move text to first node
2222
+ // Move text to first node
2182
2223
  const curXmlTextNode = xml.query.lastTextChild(curWordTextNode);
2183
2224
  totalText.push(curXmlTextNode.textContent);
2184
2225
 
2185
- // next text node
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
- // remove current text node
2234
+ // Remove current text node
2194
2235
  if (textToRemove !== firstWordTextNode) {
2195
2236
  xml.modify.remove(textToRemove);
2196
2237
  }
2197
2238
  }
2198
2239
 
2199
- // next run
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
- // remove current run
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
- // set the text content
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
- // gather some info
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
- // split first paragraph
3334
- let splitResult = officeMarkup.modify.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, true);
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
- // split last paragraph
3340
- splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, true);
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
- // disconnect splitted paragraph from their parents
3387
+ // Disconnect splitted paragraph from their parents
3346
3388
  xml.modify.remove(afterFirstParagraph);
3347
3389
  if (!areSame) xml.modify.remove(beforeLastParagraph);
3348
3390
 
3349
- // extract all paragraphs in between
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
- // merge first paragraphs
3408
+ // Merge first paragraphs
3367
3409
  officeMarkup.modify.joinParagraphs(mergeTo, curParagraphsGroup[0]);
3368
3410
 
3369
- // add middle and last paragraphs to the original document
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
- // merge last paragraph
3418
+ // Merge last paragraph
3377
3419
  officeMarkup.modify.joinParagraphs(mergeTo, lastParagraph);
3378
3420
 
3379
- // remove the old last paragraph (was merged into the new one)
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
- // vars
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
- // select the suitable strategy
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
- // prepare to loop
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
- // repeat (loop) the content
3681
+ // Repeat (loop) the content
3634
3682
  const repeatedNodes = this.repeat(nodesToRepeat, value.length);
3635
3683
 
3636
- // recursive compilation
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
- // merge back to the document
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
- // compile each node group with it's relevant data
3705
+ // Compile each node group with it's relevant data
3658
3706
  for (let i = 0; i < nodeGroups.length; i++) {
3659
- // create dummy root node
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
- // compile the new root
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
- // disconnect from dummy root
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
- // if it's a condition - don't go deeper in the path
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
- // else, it's an array - push the current index
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
- // reverse the "before" path operation
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.2" ;
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
- // split nodes
2089
+ // Split nodes
2053
2090
  const wordTextNode = officeMarkup.query.containingTextNode(textNode);
2054
2091
  const newWordTextNode = xml.create.cloneNode(wordTextNode, true);
2055
2092
 
2056
- // set space preserve to prevent display differences after splitting
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
- // insert new node before existing one
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
- // insert new node after existing one
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
- // edit text
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
- // input validation
2126
+ // Input validation
2090
2127
  const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
2091
- if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
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
- // create run clone
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
- // copy props from original run node (preserve style)
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
- // move nodes from 'right' to 'left'
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
- // remove text node
2155
+ // Remove text node
2117
2156
  if (removeTextNode) {
2118
2157
  xml.modify.removeChild(rightRun, firstRunChildIndex);
2119
2158
  }
2120
2159
 
2121
- // create paragraph clone
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
- // copy props from original paragraph (preserve style)
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
- // move nodes from 'right' to 'left'
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
- // clean paragraphs - remove empty runs
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
- // find run nodes
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
- // find "word text nodes"
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
- // iterate runs
2204
+ // Iterate runs
2164
2205
  let curRunNode = firstRunNode;
2165
2206
  while (curRunNode) {
2166
- // iterate text nodes
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
- // move text to first node
2220
+ // Move text to first node
2180
2221
  const curXmlTextNode = xml.query.lastTextChild(curWordTextNode);
2181
2222
  totalText.push(curXmlTextNode.textContent);
2182
2223
 
2183
- // next text node
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
- // remove current text node
2232
+ // Remove current text node
2192
2233
  if (textToRemove !== firstWordTextNode) {
2193
2234
  xml.modify.remove(textToRemove);
2194
2235
  }
2195
2236
  }
2196
2237
 
2197
- // next run
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
- // remove current run
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
- // set the text content
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
- // gather some info
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
- // split first paragraph
3332
- let splitResult = officeMarkup.modify.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, true);
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
- // split last paragraph
3338
- splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, true);
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
- // disconnect splitted paragraph from their parents
3385
+ // Disconnect splitted paragraph from their parents
3344
3386
  xml.modify.remove(afterFirstParagraph);
3345
3387
  if (!areSame) xml.modify.remove(beforeLastParagraph);
3346
3388
 
3347
- // extract all paragraphs in between
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
- // merge first paragraphs
3406
+ // Merge first paragraphs
3365
3407
  officeMarkup.modify.joinParagraphs(mergeTo, curParagraphsGroup[0]);
3366
3408
 
3367
- // add middle and last paragraphs to the original document
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
- // merge last paragraph
3416
+ // Merge last paragraph
3375
3417
  officeMarkup.modify.joinParagraphs(mergeTo, lastParagraph);
3376
3418
 
3377
- // remove the old last paragraph (was merged into the new one)
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
- // vars
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
- // select the suitable strategy
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
- // prepare to loop
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
- // repeat (loop) the content
3679
+ // Repeat (loop) the content
3632
3680
  const repeatedNodes = this.repeat(nodesToRepeat, value.length);
3633
3681
 
3634
- // recursive compilation
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
- // merge back to the document
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
- // compile each node group with it's relevant data
3703
+ // Compile each node group with it's relevant data
3656
3704
  for (let i = 0; i < nodeGroups.length; i++) {
3657
- // create dummy root node
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
- // compile the new root
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
- // disconnect from dummy root
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
- // if it's a condition - don't go deeper in the path
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
- // else, it's an array - push the current index
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
- // reverse the "before" path operation
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.2" ;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-template-x",
3
- "version": "7.0.2",
3
+ "version": "7.0.3",
4
4
  "description": "Generate docx documents from templates, in Node or in the browser.",
5
5
  "keywords": [
6
6
  "docx",
@@ -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
- // split nodes
230
+ // Split nodes
223
231
  const wordTextNode = officeMarkup.query.containingTextNode(textNode);
224
232
  const newWordTextNode = xml.create.cloneNode(wordTextNode, true);
225
233
 
226
- // set space preserve to prevent display differences after splitting
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
- // insert new node before existing one
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
- // insert new node after existing one
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
- // edit text
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
- // input validation
275
+ // Input validation
268
276
  const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
269
277
  if (containingParagraph != paragraph)
270
- throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
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
- // create run clone
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
- // copy props from original run node (preserve style)
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
- // move nodes from 'right' to 'left'
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
- // remove text node
306
+ // Remove text node
297
307
  if (removeTextNode) {
298
308
  xml.modify.removeChild(rightRun, firstRunChildIndex);
299
309
  }
300
310
 
301
- // create paragraph clone
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
- // copy props from original paragraph (preserve style)
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
- // move nodes from 'right' to 'left'
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
- // clean paragraphs - remove empty runs
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
- // find run nodes
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
- // find "word text nodes"
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
- // iterate runs
361
+ // Iterate runs
350
362
  let curRunNode: XmlNode = firstRunNode;
351
363
  while (curRunNode) {
352
364
 
353
- // iterate text nodes
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
- // move text to first node
379
+ // Move text to first node
368
380
  const curXmlTextNode = xml.query.lastTextChild(curWordTextNode);
369
381
  totalText.push(curXmlTextNode.textContent);
370
382
 
371
- // next text node
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
- // remove current text node
391
+ // Remove current text node
380
392
  if (textToRemove !== firstWordTextNode) {
381
393
  xml.modify.remove(textToRemove);
382
394
  }
383
395
  }
384
396
 
385
- // next run
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
- // remove current run
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
- // set the text content
411
+ // Set the text content
400
412
  const firstXmlTextNode = xml.query.lastTextChild(firstWordTextNode);
401
413
  firstXmlTextNode.textContent = totalText.join('');
402
414
  }
@@ -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
- // vars
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
- // select the suitable strategy
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
- // prepare to loop
65
+ // Prepare to loop
59
66
  const { firstNode, nodesToRepeat, lastNode } = loopStrategy.splitBefore(openTag, closeTag);
60
67
 
61
- // repeat (loop) the content
68
+ // Repeat (loop) the content
62
69
  const repeatedNodes = this.repeat(nodesToRepeat, value.length);
63
70
 
64
- // recursive compilation
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
- // merge back to the document
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
- // compile each node group with it's relevant data
98
+ // Compile each node group with it's relevant data
92
99
  for (let i = 0; i < nodeGroups.length; i++) {
93
100
 
94
- // create dummy root node
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
- // compile the new root
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
- // disconnect from dummy root
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
- // if it's a condition - don't go deeper in the path
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
- // else, it's an array - push the current index
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
- // reverse the "before" path operation
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
- // gather some info
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
- // split first paragraph
20
- let splitResult = officeMarkup.modify.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, true);
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
- // split last paragraph
27
- splitResult = officeMarkup.modify.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, true);
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
- // disconnect splitted paragraph from their parents
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
- // extract all paragraphs in between
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
- // merge first paragraphs
60
+ // Merge first paragraphs
60
61
  officeMarkup.modify.joinParagraphs(mergeTo, curParagraphsGroup[0]);
61
62
 
62
- // add middle and last paragraphs to the original document
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
- // merge last paragraph
70
+ // Merge last paragraph
70
71
  officeMarkup.modify.joinParagraphs(mergeTo, lastParagraph);
71
72
 
72
- // remove the old last paragraph (was merged into the new one)
73
+ // Remove the old last paragraph (was merged into the new one)
73
74
  xml.modify.remove(lastParagraph);
74
75
  }
75
76
  }