docxmlater 10.1.3 → 10.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +759 -754
  2. package/dist/constants/legacyCompatFlags.js +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.js.map +1 -1
  5. package/dist/core/Document.d.ts +50 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +483 -471
  8. package/dist/core/Document.js.map +1 -1
  9. package/dist/core/DocumentContent.d.ts +9 -9
  10. package/dist/core/DocumentContent.d.ts.map +1 -1
  11. package/dist/core/DocumentContent.js +1 -1
  12. package/dist/core/DocumentContent.js.map +1 -1
  13. package/dist/core/DocumentGenerator.d.ts +11 -11
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +251 -251
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.js.map +1 -1
  18. package/dist/core/DocumentParser.d.ts +15 -15
  19. package/dist/core/DocumentParser.d.ts.map +1 -1
  20. package/dist/core/DocumentParser.js +2123 -2155
  21. package/dist/core/DocumentParser.js.map +1 -1
  22. package/dist/core/DocumentValidator.d.ts.map +1 -1
  23. package/dist/core/DocumentValidator.js +2 -5
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.js.map +1 -1
  26. package/dist/core/RelationshipManager.d.ts.map +1 -1
  27. package/dist/core/RelationshipManager.js +3 -3
  28. package/dist/core/RelationshipManager.js.map +1 -1
  29. package/dist/elements/AlternateContent.js.map +1 -1
  30. package/dist/elements/Bookmark.d.ts.map +1 -1
  31. package/dist/elements/Bookmark.js +3 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.d.ts.map +1 -1
  36. package/dist/elements/Comment.js +9 -6
  37. package/dist/elements/Comment.js.map +1 -1
  38. package/dist/elements/CommentManager.d.ts.map +1 -1
  39. package/dist/elements/CommentManager.js +18 -17
  40. package/dist/elements/CommentManager.js.map +1 -1
  41. package/dist/elements/CommonTypes.d.ts +21 -21
  42. package/dist/elements/CommonTypes.d.ts.map +1 -1
  43. package/dist/elements/CommonTypes.js +56 -56
  44. package/dist/elements/CommonTypes.js.map +1 -1
  45. package/dist/elements/CustomXml.js.map +1 -1
  46. package/dist/elements/Endnote.d.ts.map +1 -1
  47. package/dist/elements/Endnote.js +6 -6
  48. package/dist/elements/Endnote.js.map +1 -1
  49. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  50. package/dist/elements/EndnoteManager.js +6 -7
  51. package/dist/elements/EndnoteManager.js.map +1 -1
  52. package/dist/elements/Field.d.ts.map +1 -1
  53. package/dist/elements/Field.js +82 -25
  54. package/dist/elements/Field.js.map +1 -1
  55. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  56. package/dist/elements/FieldHelpers.js.map +1 -1
  57. package/dist/elements/FontManager.d.ts.map +1 -1
  58. package/dist/elements/FontManager.js +1 -1
  59. package/dist/elements/FontManager.js.map +1 -1
  60. package/dist/elements/Footer.js +2 -2
  61. package/dist/elements/Footer.js.map +1 -1
  62. package/dist/elements/Footnote.d.ts.map +1 -1
  63. package/dist/elements/Footnote.js +6 -6
  64. package/dist/elements/Footnote.js.map +1 -1
  65. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  66. package/dist/elements/FootnoteManager.js +6 -7
  67. package/dist/elements/FootnoteManager.js.map +1 -1
  68. package/dist/elements/Header.js +2 -2
  69. package/dist/elements/Header.js.map +1 -1
  70. package/dist/elements/HeaderFooterManager.js.map +1 -1
  71. package/dist/elements/Hyperlink.d.ts +5 -3
  72. package/dist/elements/Hyperlink.d.ts.map +1 -1
  73. package/dist/elements/Hyperlink.js +134 -76
  74. package/dist/elements/Hyperlink.js.map +1 -1
  75. package/dist/elements/Image.d.ts.map +1 -1
  76. package/dist/elements/Image.js +238 -106
  77. package/dist/elements/Image.js.map +1 -1
  78. package/dist/elements/ImageManager.d.ts.map +1 -1
  79. package/dist/elements/ImageManager.js +1 -1
  80. package/dist/elements/ImageManager.js.map +1 -1
  81. package/dist/elements/ImageRun.js +1 -1
  82. package/dist/elements/ImageRun.js.map +1 -1
  83. package/dist/elements/MathElement.js.map +1 -1
  84. package/dist/elements/Paragraph.d.ts +24 -24
  85. package/dist/elements/Paragraph.d.ts.map +1 -1
  86. package/dist/elements/Paragraph.js +181 -188
  87. package/dist/elements/Paragraph.js.map +1 -1
  88. package/dist/elements/PreservedElement.js.map +1 -1
  89. package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
  90. package/dist/elements/PropertyChangeTypes.js +6 -6
  91. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  92. package/dist/elements/RangeMarker.d.ts.map +1 -1
  93. package/dist/elements/RangeMarker.js.map +1 -1
  94. package/dist/elements/Revision.d.ts.map +1 -1
  95. package/dist/elements/Revision.js +4 -5
  96. package/dist/elements/Revision.js.map +1 -1
  97. package/dist/elements/RevisionContent.js.map +1 -1
  98. package/dist/elements/RevisionManager.d.ts.map +1 -1
  99. package/dist/elements/RevisionManager.js +40 -48
  100. package/dist/elements/RevisionManager.js.map +1 -1
  101. package/dist/elements/Run.d.ts +16 -16
  102. package/dist/elements/Run.d.ts.map +1 -1
  103. package/dist/elements/Run.js +256 -238
  104. package/dist/elements/Run.js.map +1 -1
  105. package/dist/elements/Section.d.ts.map +1 -1
  106. package/dist/elements/Section.js +36 -11
  107. package/dist/elements/Section.js.map +1 -1
  108. package/dist/elements/Shape.d.ts.map +1 -1
  109. package/dist/elements/Shape.js.map +1 -1
  110. package/dist/elements/StructuredDocumentTag.d.ts +6 -6
  111. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  112. package/dist/elements/StructuredDocumentTag.js +99 -104
  113. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  114. package/dist/elements/Table.d.ts +11 -11
  115. package/dist/elements/Table.d.ts.map +1 -1
  116. package/dist/elements/Table.js +102 -107
  117. package/dist/elements/Table.js.map +1 -1
  118. package/dist/elements/TableCell.d.ts +10 -10
  119. package/dist/elements/TableCell.d.ts.map +1 -1
  120. package/dist/elements/TableCell.js +105 -106
  121. package/dist/elements/TableCell.js.map +1 -1
  122. package/dist/elements/TableGridChange.d.ts.map +1 -1
  123. package/dist/elements/TableGridChange.js.map +1 -1
  124. package/dist/elements/TableOfContents.d.ts.map +1 -1
  125. package/dist/elements/TableOfContents.js +4 -4
  126. package/dist/elements/TableOfContents.js.map +1 -1
  127. package/dist/elements/TableOfContentsElement.js.map +1 -1
  128. package/dist/elements/TableRow.d.ts.map +1 -1
  129. package/dist/elements/TableRow.js +13 -6
  130. package/dist/elements/TableRow.js.map +1 -1
  131. package/dist/elements/TextBox.d.ts.map +1 -1
  132. package/dist/elements/TextBox.js +3 -5
  133. package/dist/elements/TextBox.js.map +1 -1
  134. package/dist/formatting/AbstractNumbering.d.ts +4 -4
  135. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  136. package/dist/formatting/AbstractNumbering.js +54 -49
  137. package/dist/formatting/AbstractNumbering.js.map +1 -1
  138. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  139. package/dist/formatting/NumberingInstance.js +1 -3
  140. package/dist/formatting/NumberingInstance.js.map +1 -1
  141. package/dist/formatting/NumberingLevel.d.ts +5 -5
  142. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  143. package/dist/formatting/NumberingLevel.js +119 -125
  144. package/dist/formatting/NumberingLevel.js.map +1 -1
  145. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  146. package/dist/formatting/NumberingManager.js +9 -9
  147. package/dist/formatting/NumberingManager.js.map +1 -1
  148. package/dist/formatting/Style.d.ts +11 -11
  149. package/dist/formatting/Style.d.ts.map +1 -1
  150. package/dist/formatting/Style.js +219 -247
  151. package/dist/formatting/Style.js.map +1 -1
  152. package/dist/formatting/StylesManager.d.ts +2 -2
  153. package/dist/formatting/StylesManager.d.ts.map +1 -1
  154. package/dist/formatting/StylesManager.js +96 -102
  155. package/dist/formatting/StylesManager.js.map +1 -1
  156. package/dist/helpers/CleanupHelper.d.ts +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  158. package/dist/helpers/CleanupHelper.js +6 -6
  159. package/dist/helpers/CleanupHelper.js.map +1 -1
  160. package/dist/images/ImageOptimizer.js +7 -7
  161. package/dist/images/ImageOptimizer.js.map +1 -1
  162. package/dist/index.d.ts +9 -9
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/managers/DrawingManager.js.map +1 -1
  166. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.js +23 -7
  168. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  169. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  170. package/dist/tracking/TrackingContext.js.map +1 -1
  171. package/dist/types/compatibility-types.js.map +1 -1
  172. package/dist/types/formatting.js.map +1 -1
  173. package/dist/types/list-types.d.ts +6 -6
  174. package/dist/types/list-types.js.map +1 -1
  175. package/dist/types/settings-types.js.map +1 -1
  176. package/dist/types/styleConfig.d.ts +2 -2
  177. package/dist/types/styleConfig.js.map +1 -1
  178. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  179. package/dist/utils/ChangelogGenerator.js +97 -101
  180. package/dist/utils/ChangelogGenerator.js.map +1 -1
  181. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.js +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  184. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  186. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  187. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  188. package/dist/utils/MoveOperationHelper.js +1 -1
  189. package/dist/utils/MoveOperationHelper.js.map +1 -1
  190. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.js +2 -4
  192. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  193. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  194. package/dist/utils/RevisionWalker.js +4 -12
  195. package/dist/utils/RevisionWalker.js.map +1 -1
  196. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  198. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  199. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  200. package/dist/utils/ShadingResolver.js +1 -1
  201. package/dist/utils/ShadingResolver.js.map +1 -1
  202. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  203. package/dist/utils/acceptRevisions.js +23 -12
  204. package/dist/utils/acceptRevisions.js.map +1 -1
  205. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  207. package/dist/utils/cnfStyleDecoder.js +40 -40
  208. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  209. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  210. package/dist/utils/corruptionDetection.js.map +1 -1
  211. package/dist/utils/dateFormatting.js.map +1 -1
  212. package/dist/utils/deepClone.js +1 -1
  213. package/dist/utils/deepClone.js.map +1 -1
  214. package/dist/utils/diagnostics.d.ts.map +1 -1
  215. package/dist/utils/diagnostics.js +1 -1
  216. package/dist/utils/diagnostics.js.map +1 -1
  217. package/dist/utils/errorHandling.js.map +1 -1
  218. package/dist/utils/formatting.d.ts.map +1 -1
  219. package/dist/utils/formatting.js +10 -2
  220. package/dist/utils/formatting.js.map +1 -1
  221. package/dist/utils/list-detection.d.ts +2 -2
  222. package/dist/utils/list-detection.d.ts.map +1 -1
  223. package/dist/utils/list-detection.js +21 -23
  224. package/dist/utils/list-detection.js.map +1 -1
  225. package/dist/utils/logger.d.ts.map +1 -1
  226. package/dist/utils/logger.js +12 -7
  227. package/dist/utils/logger.js.map +1 -1
  228. package/dist/utils/parsingHelpers.js.map +1 -1
  229. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  230. package/dist/utils/stripTrackedChanges.js +3 -3
  231. package/dist/utils/stripTrackedChanges.js.map +1 -1
  232. package/dist/utils/textDiff.d.ts +1 -1
  233. package/dist/utils/textDiff.js +8 -8
  234. package/dist/utils/textDiff.js.map +1 -1
  235. package/dist/utils/units.js.map +1 -1
  236. package/dist/utils/validation.d.ts.map +1 -1
  237. package/dist/utils/validation.js +24 -7
  238. package/dist/utils/validation.js.map +1 -1
  239. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  240. package/dist/utils/xmlSanitization.js +3 -3
  241. package/dist/utils/xmlSanitization.js.map +1 -1
  242. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.js +5 -5
  244. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  245. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  246. package/dist/validation/RevisionValidator.js +7 -9
  247. package/dist/validation/RevisionValidator.js.map +1 -1
  248. package/dist/validation/ValidationRules.js +3 -3
  249. package/dist/validation/ValidationRules.js.map +1 -1
  250. package/dist/validation/index.js.map +1 -1
  251. package/dist/xml/XMLBuilder.d.ts +1 -1
  252. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  253. package/dist/xml/XMLBuilder.js +98 -100
  254. package/dist/xml/XMLBuilder.js.map +1 -1
  255. package/dist/xml/XMLParser.d.ts.map +1 -1
  256. package/dist/xml/XMLParser.js +61 -66
  257. package/dist/xml/XMLParser.js.map +1 -1
  258. package/dist/zip/ZipHandler.d.ts.map +1 -1
  259. package/dist/zip/ZipHandler.js.map +1 -1
  260. package/dist/zip/ZipReader.d.ts.map +1 -1
  261. package/dist/zip/ZipReader.js +1 -3
  262. package/dist/zip/ZipReader.js.map +1 -1
  263. package/dist/zip/ZipWriter.d.ts +1 -1
  264. package/dist/zip/ZipWriter.d.ts.map +1 -1
  265. package/dist/zip/ZipWriter.js +28 -36
  266. package/dist/zip/ZipWriter.js.map +1 -1
  267. package/dist/zip/types.js +1 -1
  268. package/dist/zip/types.js.map +1 -1
  269. package/package.json +92 -92
  270. package/src/__tests__/helper-methods.test.ts +512 -512
  271. package/src/constants/legacyCompatFlags.ts +138 -138
  272. package/src/constants/limits.ts +50 -50
  273. package/src/core/Document.ts +985 -1145
  274. package/src/core/DocumentContent.ts +461 -467
  275. package/src/core/DocumentGenerator.ts +1133 -1104
  276. package/src/core/DocumentIdManager.ts +158 -158
  277. package/src/core/DocumentParser.ts +2347 -2716
  278. package/src/core/DocumentValidator.ts +363 -372
  279. package/src/core/Relationship.ts +367 -367
  280. package/src/core/RelationshipManager.ts +429 -428
  281. package/src/elements/AlternateContent.ts +42 -42
  282. package/src/elements/Bookmark.ts +212 -210
  283. package/src/elements/BookmarkManager.ts +247 -250
  284. package/src/elements/Comment.ts +356 -359
  285. package/src/elements/CommentManager.ts +499 -502
  286. package/src/elements/CommonTypes.ts +524 -549
  287. package/src/elements/CustomXml.ts +36 -36
  288. package/src/elements/Endnote.ts +221 -217
  289. package/src/elements/EndnoteManager.ts +246 -249
  290. package/src/elements/Field.ts +1292 -1233
  291. package/src/elements/FieldHelpers.ts +329 -333
  292. package/src/elements/FontManager.ts +336 -339
  293. package/src/elements/Footer.ts +269 -269
  294. package/src/elements/Footnote.ts +221 -217
  295. package/src/elements/FootnoteManager.ts +246 -249
  296. package/src/elements/Header.ts +269 -269
  297. package/src/elements/HeaderFooterManager.ts +219 -219
  298. package/src/elements/Hyperlink.ts +1288 -1193
  299. package/src/elements/Image.ts +1982 -1756
  300. package/src/elements/ImageManager.ts +437 -432
  301. package/src/elements/ImageRun.ts +59 -59
  302. package/src/elements/MathElement.ts +65 -65
  303. package/src/elements/Paragraph.ts +4347 -4287
  304. package/src/elements/PreservedElement.ts +53 -53
  305. package/src/elements/PropertyChangeTypes.ts +458 -442
  306. package/src/elements/RangeMarker.ts +382 -400
  307. package/src/elements/Revision.ts +1198 -1217
  308. package/src/elements/RevisionContent.ts +73 -73
  309. package/src/elements/RevisionManager.ts +1070 -1070
  310. package/src/elements/Run.ts +3103 -3073
  311. package/src/elements/Section.ts +1521 -1421
  312. package/src/elements/Shape.ts +884 -873
  313. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  314. package/src/elements/Table.ts +2468 -2524
  315. package/src/elements/TableCell.ts +1617 -1621
  316. package/src/elements/TableGridChange.ts +149 -151
  317. package/src/elements/TableOfContents.ts +701 -691
  318. package/src/elements/TableOfContentsElement.ts +89 -89
  319. package/src/elements/TableRow.ts +960 -929
  320. package/src/elements/TextBox.ts +766 -768
  321. package/src/formatting/AbstractNumbering.ts +580 -579
  322. package/src/formatting/NumberingInstance.ts +295 -299
  323. package/src/formatting/NumberingLevel.ts +981 -1040
  324. package/src/formatting/NumberingManager.ts +833 -827
  325. package/src/formatting/Style.ts +1785 -1879
  326. package/src/formatting/StylesManager.ts +1090 -1130
  327. package/src/helpers/CleanupHelper.ts +524 -524
  328. package/src/images/ImageOptimizer.ts +274 -274
  329. package/src/index.ts +559 -554
  330. package/src/managers/DrawingManager.ts +319 -319
  331. package/src/tracking/DocumentTrackingContext.ts +687 -674
  332. package/src/tracking/TrackingContext.ts +175 -173
  333. package/src/types/compatibility-types.ts +49 -49
  334. package/src/types/formatting.ts +210 -210
  335. package/src/types/list-types.ts +14 -14
  336. package/src/types/settings-types.ts +59 -59
  337. package/src/types/styleConfig.ts +189 -189
  338. package/src/utils/ChangelogGenerator.ts +1583 -1581
  339. package/src/utils/CompatibilityUpgrader.ts +235 -237
  340. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  341. package/src/utils/MoveOperationHelper.ts +233 -238
  342. package/src/utils/RevisionAwareProcessor.ts +518 -526
  343. package/src/utils/RevisionWalker.ts +427 -457
  344. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  345. package/src/utils/ShadingResolver.ts +105 -107
  346. package/src/utils/acceptRevisions.ts +723 -714
  347. package/src/utils/cnfStyleDecoder.ts +212 -217
  348. package/src/utils/corruptionDetection.ts +346 -345
  349. package/src/utils/dateFormatting.ts +20 -20
  350. package/src/utils/deepClone.ts +77 -78
  351. package/src/utils/diagnostics.ts +125 -129
  352. package/src/utils/errorHandling.ts +80 -80
  353. package/src/utils/formatting.ts +220 -213
  354. package/src/utils/list-detection.ts +32 -42
  355. package/src/utils/logger.ts +412 -404
  356. package/src/utils/parsingHelpers.ts +190 -190
  357. package/src/utils/stripTrackedChanges.ts +356 -353
  358. package/src/utils/textDiff.ts +100 -100
  359. package/src/utils/units.ts +421 -421
  360. package/src/utils/validation.ts +553 -542
  361. package/src/utils/xmlSanitization.ts +179 -182
  362. package/src/validation/RevisionAutoFixer.ts +541 -542
  363. package/src/validation/RevisionValidator.ts +470 -460
  364. package/src/validation/ValidationRules.ts +338 -338
  365. package/src/validation/index.ts +30 -30
  366. package/src/xml/XMLBuilder.ts +857 -871
  367. package/src/xml/XMLParser.ts +877 -919
  368. package/src/zip/ZipHandler.ts +629 -637
  369. package/src/zip/ZipReader.ts +295 -299
  370. package/src/zip/ZipWriter.ts +374 -390
  371. package/src/zip/types.ts +116 -116
@@ -1,1233 +1,1292 @@
1
- /**
2
- * Field - Represents a dynamic field in a Word document
3
- *
4
- * Fields are used for dynamic content like page numbers, dates, document properties, etc.
5
- * They are represented using the <w:fldSimple> element with field codes.
6
- */
7
-
8
- import { XMLElement } from '../xml/XMLBuilder';
9
- import { RunFormatting, FormFieldData } from './Run';
10
- import { ParsedHyperlinkInstruction, parseHyperlinkInstruction, isHyperlinkInstruction } from './FieldHelpers';
11
- import type { Revision } from './Revision';
12
- import { pointsToHalfPoints } from '../utils/units';
13
-
14
- /**
15
- * Common field types
16
- */
17
- export type FieldType =
18
- | 'PAGE' // Current page number
19
- | 'NUMPAGES' // Total number of pages
20
- | 'DATE' // Current date
21
- | 'TIME' // Current time
22
- | 'AUTHOR' // Document author
23
- | 'TITLE' // Document title
24
- | 'FILENAME' // Document filename
25
- | 'FILENAMEWITHPATH' // Document filename with path
26
- | 'SUBJECT' // Document subject
27
- | 'KEYWORDS' // Document keywords
28
- | 'CREATEDATE' // Document creation date
29
- | 'SAVEDATE' // Last save date
30
- | 'PRINTDATE' // Last print date
31
- | 'SECTIONPAGES' // Pages in current section
32
- | 'SECTION' // Current section number
33
- | 'REF' // Cross-reference to bookmark
34
- | 'HYPERLINK' // Hyperlink field
35
- | 'SEQ' // Sequence numbering
36
- | 'TC' // Table of contents entry
37
- | 'XE' // Index entry
38
- | 'IF' // Conditional field
39
- | 'MERGEFIELD' // Mail merge field
40
- | 'INCLUDE' // Include text from external file
41
- | 'INCLUDETEXT' // Include text from external file (alias)
42
- | 'CUSTOM'; // Custom field type for unknown/specialized fields
43
-
44
- /**
45
- * Field properties
46
- */
47
- export interface FieldProperties {
48
- /** Field type */
49
- type: FieldType;
50
- /** Field instruction (e.g., 'PAGE \* MERGEFORMAT') */
51
- instruction?: string;
52
- /** Format switches (e.g., '\\* MERGEFORMAT') */
53
- format?: string;
54
- /** Date/time format (e.g., 'MMMM d, yyyy') */
55
- dateFormat?: string;
56
- /** Preserve formatting during updates */
57
- preserveFormatting?: boolean;
58
- /** Run formatting for field result */
59
- formatting?: RunFormatting;
60
- }
61
-
62
- /**
63
- * Represents a dynamic field
64
- */
65
- export class Field {
66
- private type: FieldType;
67
- private instruction: string;
68
- private formatting?: RunFormatting;
69
-
70
- /**
71
- * Creates a new field
72
- * @param properties Field properties
73
- */
74
- constructor(properties: FieldProperties) {
75
- this.type = properties.type;
76
- this.formatting = properties.formatting;
77
-
78
- // Build field instruction
79
- if (properties.instruction) {
80
- this.instruction = properties.instruction;
81
- } else {
82
- this.instruction = this.buildInstruction(properties);
83
- }
84
- }
85
-
86
- /**
87
- * Builds field instruction from properties
88
- */
89
- private buildInstruction(properties: FieldProperties): string {
90
- let instruction = properties.type;
91
-
92
- // Add date format for date/time fields
93
- if (properties.dateFormat && this.isDateField(properties.type)) {
94
- instruction += ` \\@ "${properties.dateFormat}"`;
95
- }
96
-
97
- // Add format switch
98
- if (properties.format) {
99
- instruction += ` ${properties.format}`;
100
- } else if (properties.preserveFormatting !== false) {
101
- // Add MERGEFORMAT by default to preserve formatting
102
- instruction += ' \\* MERGEFORMAT';
103
- }
104
-
105
- return instruction;
106
- }
107
-
108
- /**
109
- * Checks if field type is a date field
110
- */
111
- private isDateField(type: FieldType): boolean {
112
- return ['DATE', 'TIME', 'CREATEDATE', 'SAVEDATE', 'PRINTDATE'].includes(type);
113
- }
114
-
115
- /**
116
- * Gets the field type
117
- */
118
- getType(): FieldType {
119
- return this.type;
120
- }
121
-
122
- /**
123
- * Gets the field instruction
124
- */
125
- getInstruction(): string {
126
- return this.instruction;
127
- }
128
-
129
- /**
130
- * Sets run formatting for the field
131
- */
132
- setFormatting(formatting: RunFormatting): this {
133
- this.formatting = formatting;
134
- return this;
135
- }
136
-
137
- /**
138
- * Gets run formatting
139
- */
140
- getFormatting(): RunFormatting | undefined {
141
- return this.formatting;
142
- }
143
-
144
- /**
145
- * Checks if this field is a HYPERLINK field
146
- * @returns True if the field type is HYPERLINK or instruction starts with HYPERLINK
147
- */
148
- isHyperlinkField(): boolean {
149
- return this.type === 'HYPERLINK' ||
150
- this.instruction.trim().toUpperCase().startsWith('HYPERLINK');
151
- }
152
-
153
- /**
154
- * Sets text color for the field
155
- * @param color Color in hex format (e.g., '0000FF')
156
- * @returns This field for chaining
157
- */
158
- setColor(color: string): this {
159
- if (!this.formatting) {
160
- this.formatting = {};
161
- }
162
- this.formatting.color = color.replace('#', '');
163
- return this;
164
- }
165
-
166
- /**
167
- * Generates XML for the field.
168
- * Per ECMA-376, w:fldSimple is a paragraph-level element that CONTAINS w:r children.
169
- * Structure: <w:fldSimple w:instr="..."><w:r><w:rPr/><w:t>...</w:t></w:r></w:fldSimple>
170
- * The fldSimple element should be added directly to paragraph children (not wrapped in w:r).
171
- */
172
- toXML(): XMLElement {
173
- // Build the inner run with optional formatting
174
- const runChildren: XMLElement[] = [];
175
- if (this.formatting) {
176
- runChildren.push(this.createRunProperties());
177
- }
178
- runChildren.push({
179
- name: 'w:t',
180
- children: [this.getPlaceholderText()],
181
- });
182
-
183
- return {
184
- name: 'w:fldSimple',
185
- attributes: {
186
- 'w:instr': this.instruction,
187
- },
188
- children: [{
189
- name: 'w:r',
190
- children: runChildren,
191
- }],
192
- };
193
- }
194
-
195
- /**
196
- * Gets placeholder text for the field
197
- */
198
- private getPlaceholderText(): string {
199
- switch (this.type) {
200
- case 'PAGE':
201
- return '1';
202
- case 'NUMPAGES':
203
- return '1';
204
- case 'SECTIONPAGES':
205
- return '1';
206
- case 'SECTION':
207
- return '1';
208
- case 'DATE':
209
- return new Date().toLocaleDateString();
210
- case 'TIME':
211
- return new Date().toLocaleTimeString();
212
- case 'CREATEDATE':
213
- case 'SAVEDATE':
214
- case 'PRINTDATE':
215
- return new Date().toLocaleDateString();
216
- case 'FILENAME':
217
- return 'Document';
218
- case 'FILENAMEWITHPATH':
219
- return 'C:\\Document.docx';
220
- case 'AUTHOR':
221
- return 'Author';
222
- case 'TITLE':
223
- return 'Title';
224
- case 'SUBJECT':
225
- return 'Subject';
226
- case 'KEYWORDS':
227
- return 'Keywords';
228
- case 'REF':
229
- return '1'; // Reference typically shows page number or heading text
230
- case 'HYPERLINK':
231
- return 'Link'; // Hyperlink displays the link text
232
- case 'SEQ':
233
- return '1'; // Sequence shows the current number
234
- case 'TC':
235
- return ''; // TC fields are hidden
236
- case 'XE':
237
- return ''; // XE fields are hidden
238
- default:
239
- return '';
240
- }
241
- }
242
-
243
- /**
244
- * Creates run properties XML
245
- */
246
- private createRunProperties(): XMLElement {
247
- const children: XMLElement[] = [];
248
-
249
- if (!this.formatting) {
250
- return { name: 'w:rPr', children };
251
- }
252
-
253
- // Per ECMA-376 CT_RPr ordering:
254
- // rFonts b → i → strike → color → sz/szCs → highlight → u
255
-
256
- if (this.formatting.font) {
257
- children.push({
258
- name: 'w:rFonts',
259
- attributes: {
260
- 'w:ascii': this.formatting.font,
261
- 'w:hAnsi': this.formatting.font,
262
- 'w:cs': this.formatting.font,
263
- },
264
- selfClosing: true,
265
- });
266
- }
267
-
268
- if (this.formatting.bold) {
269
- children.push({ name: 'w:b', selfClosing: true });
270
- }
271
-
272
- if (this.formatting.italic) {
273
- children.push({ name: 'w:i', selfClosing: true });
274
- }
275
-
276
- if (this.formatting.strike) {
277
- children.push({ name: 'w:strike', selfClosing: true });
278
- }
279
-
280
- if (this.formatting.color) {
281
- const color = this.formatting.color.replace('#', '');
282
- children.push({
283
- name: 'w:color',
284
- attributes: { 'w:val': color },
285
- selfClosing: true,
286
- });
287
- }
288
-
289
- if (this.formatting.size) {
290
- const sizeValue = pointsToHalfPoints(this.formatting.size).toString();
291
- children.push({
292
- name: 'w:sz',
293
- attributes: { 'w:val': sizeValue },
294
- selfClosing: true,
295
- });
296
- children.push({
297
- name: 'w:szCs',
298
- attributes: { 'w:val': sizeValue },
299
- selfClosing: true,
300
- });
301
- }
302
-
303
- if (this.formatting.highlight) {
304
- children.push({
305
- name: 'w:highlight',
306
- attributes: { 'w:val': this.formatting.highlight },
307
- selfClosing: true,
308
- });
309
- }
310
-
311
- if (this.formatting.underline) {
312
- const val = typeof this.formatting.underline === 'string'
313
- ? this.formatting.underline
314
- : 'single';
315
- children.push({
316
- name: 'w:u',
317
- attributes: { 'w:val': val },
318
- selfClosing: true,
319
- });
320
- }
321
-
322
- return { name: 'w:rPr', children };
323
- }
324
-
325
- /**
326
- * Creates a page number field
327
- * @param formatting Optional run formatting
328
- */
329
- static createPageNumber(formatting?: RunFormatting): Field {
330
- return new Field({
331
- type: 'PAGE',
332
- formatting,
333
- });
334
- }
335
-
336
- /**
337
- * Creates a total pages field
338
- * @param formatting Optional run formatting
339
- */
340
- static createTotalPages(formatting?: RunFormatting): Field {
341
- return new Field({
342
- type: 'NUMPAGES',
343
- formatting,
344
- });
345
- }
346
-
347
- /**
348
- * Creates a date field
349
- * @param format Date format (e.g., 'MMMM d, yyyy')
350
- * @param formatting Optional run formatting
351
- */
352
- static createDate(format?: string, formatting?: RunFormatting): Field {
353
- return new Field({
354
- type: 'DATE',
355
- dateFormat: format,
356
- formatting,
357
- });
358
- }
359
-
360
- /**
361
- * Creates a time field
362
- * @param format Time format
363
- * @param formatting Optional run formatting
364
- */
365
- static createTime(format?: string, formatting?: RunFormatting): Field {
366
- return new Field({
367
- type: 'TIME',
368
- dateFormat: format,
369
- formatting,
370
- });
371
- }
372
-
373
- /**
374
- * Creates a filename field
375
- * @param includePath Whether to include full path
376
- * @param formatting Optional run formatting
377
- */
378
- static createFilename(includePath = false, formatting?: RunFormatting): Field {
379
- return new Field({
380
- type: includePath ? 'FILENAMEWITHPATH' : 'FILENAME',
381
- formatting,
382
- });
383
- }
384
-
385
- /**
386
- * Creates an author field
387
- * @param formatting Optional run formatting
388
- */
389
- static createAuthor(formatting?: RunFormatting): Field {
390
- return new Field({
391
- type: 'AUTHOR',
392
- formatting,
393
- });
394
- }
395
-
396
- /**
397
- * Creates a title field
398
- * @param formatting Optional run formatting
399
- */
400
- static createTitle(formatting?: RunFormatting): Field {
401
- return new Field({
402
- type: 'TITLE',
403
- formatting,
404
- });
405
- }
406
-
407
- /**
408
- * Creates a section pages field (pages in current section)
409
- * @param formatting Optional run formatting
410
- */
411
- static createSectionPages(formatting?: RunFormatting): Field {
412
- return new Field({
413
- type: 'SECTIONPAGES',
414
- formatting,
415
- });
416
- }
417
-
418
- /**
419
- * Creates a cross-reference field
420
- * @param bookmark Bookmark name to reference
421
- * @param format Reference format (\h for hyperlink, \p for page number, etc.)
422
- * @param formatting Optional run formatting
423
- */
424
- static createRef(bookmark: string, format?: string, formatting?: RunFormatting): Field {
425
- const formatSwitch = format || '\\h'; // Default to hyperlink format
426
- const instruction = `REF ${bookmark} ${formatSwitch} \\* MERGEFORMAT`;
427
-
428
- return new Field({
429
- type: 'REF',
430
- instruction,
431
- formatting,
432
- });
433
- }
434
-
435
- /**
436
- * Creates a hyperlink field
437
- * @param url The URL to link to
438
- * @param displayText The text to display
439
- * @param tooltip Optional tooltip text
440
- * @param formatting Optional run formatting
441
- */
442
- static createHyperlink(
443
- url: string,
444
- displayText: string = url,
445
- tooltip?: string,
446
- formatting?: RunFormatting
447
- ): Field {
448
- let instruction = `HYPERLINK "${url}"`;
449
-
450
- if (tooltip) {
451
- instruction += ` \\o "${tooltip}"`;
452
- }
453
-
454
- instruction += ' \\* MERGEFORMAT';
455
-
456
- return new Field({
457
- type: 'HYPERLINK',
458
- instruction,
459
- formatting,
460
- });
461
- }
462
-
463
- /**
464
- * Creates a sequence numbering field
465
- * @param identifier Sequence identifier (e.g., 'Figure', 'Table')
466
- * @param format Number format (\* ARABIC, \* ROMAN, etc.)
467
- * @param formatting Optional run formatting
468
- */
469
- static createSeq(
470
- identifier: string,
471
- format?: string,
472
- formatting?: RunFormatting
473
- ): Field {
474
- let instruction = `SEQ ${identifier}`;
475
-
476
- if (format) {
477
- instruction += ` ${format}`;
478
- } else {
479
- instruction += ' \\* ARABIC'; // Default to Arabic numerals
480
- }
481
-
482
- instruction += ' \\* MERGEFORMAT';
483
-
484
- return new Field({
485
- type: 'SEQ',
486
- instruction,
487
- formatting,
488
- });
489
- }
490
-
491
- /**
492
- * Creates a table of contents entry field (TC)
493
- * @param text Entry text
494
- * @param level TOC level (1-9)
495
- * @param formatting Optional run formatting
496
- */
497
- static createTCEntry(
498
- text: string,
499
- level = 1,
500
- formatting?: RunFormatting
501
- ): Field {
502
- if (level < 1 || level > 9) {
503
- throw new Error('TC level must be between 1 and 9');
504
- }
505
-
506
- const instruction = `TC "${text}" \\f C \\l ${level}`;
507
-
508
- return new Field({
509
- type: 'TC',
510
- instruction,
511
- formatting,
512
- });
513
- }
514
-
515
- /**
516
- * Creates an index entry field (XE)
517
- * @param text Entry text
518
- * @param subEntry Optional sub-entry text
519
- * @param formatting Optional run formatting
520
- */
521
- static createXEEntry(
522
- text: string,
523
- subEntry?: string,
524
- formatting?: RunFormatting
525
- ): Field {
526
- let instruction = `XE "${text}"`;
527
-
528
- if (subEntry) {
529
- instruction += `:${subEntry}`;
530
- }
531
-
532
- return new Field({
533
- type: 'XE',
534
- instruction,
535
- formatting,
536
- });
537
- }
538
-
539
- /**
540
- * Creates a custom field with instruction
541
- * @param instruction Field instruction code
542
- * @param formatting Optional run formatting
543
- */
544
- static createCustom(instruction: string, formatting?: RunFormatting): Field {
545
- return new Field({
546
- type: 'PAGE', // Placeholder type
547
- instruction,
548
- formatting,
549
- });
550
- }
551
-
552
- /**
553
- * Creates a field from properties
554
- * @param properties Field properties
555
- */
556
- static create(properties: FieldProperties): Field {
557
- return new Field(properties);
558
- }
559
- }
560
-
561
- /**
562
- * Field character type for complex fields
563
- */
564
- export type FieldCharType = 'begin' | 'separate' | 'end';
565
-
566
- /**
567
- * Complex field properties
568
- * Complex fields use begin/separate/end structure instead of fldSimple
569
- */
570
- export interface ComplexFieldProperties {
571
- /** Field instruction (e.g., " TOC \\o \"1-3\" \\h \\z \\u ") */
572
- instruction: string;
573
-
574
- /** Current field result text (optional) */
575
- result?: string;
576
-
577
- /** Run formatting for instruction */
578
- instructionFormatting?: RunFormatting;
579
-
580
- /** Run formatting for result */
581
- resultFormatting?: RunFormatting;
582
-
583
- /** Nested fields to include within this field */
584
- nestedFields?: ComplexField[];
585
-
586
- /** Custom XML content for result section */
587
- resultContent?: XMLElement[];
588
-
589
- /** Whether field spans multiple paragraphs */
590
- multiParagraph?: boolean;
591
-
592
- /** Parsed HYPERLINK instruction components (auto-populated if instruction is HYPERLINK) */
593
- parsedHyperlink?: ParsedHyperlinkInstruction;
594
-
595
- /**
596
- * Whether the field has a result section (w:fldSep was present during parsing)
597
- * Per ECMA-376, fields without results skip the separator element.
598
- * This flag distinguishes between:
599
- * - hasResult=true, result="" → Field had separator but result was empty
600
- * - hasResult=false, result="" → Field never had a result section (empty field)
601
- */
602
- hasResult?: boolean;
603
-
604
- /** Form field data (w:ffData) from begin field char per ECMA-376 §17.16.17 */
605
- formFieldData?: FormFieldData;
606
- }
607
-
608
- /**
609
- * Represents a complex field (begin/separate/end structure)
610
- * Used for TOC, cross-references, and other advanced fields
611
- *
612
- * Structure:
613
- * <w:r><w:fldChar w:fldCharType="begin"/></w:r>
614
- * <w:r><w:instrText>INSTRUCTION</w:instrText></w:r>
615
- * <w:r><w:fldChar w:fldCharType="separate"/></w:r>
616
- * <w:r><w:t>RESULT</w:t></w:r>
617
- * <w:r><w:fldChar w:fldCharType="end"/></w:r>
618
- */
619
- export class ComplexField {
620
- private instruction: string;
621
- private result?: string;
622
- private instructionFormatting?: RunFormatting;
623
- private resultFormatting?: RunFormatting;
624
- private nestedFields: ComplexField[];
625
- private resultContent: XMLElement[];
626
- private multiParagraph: boolean;
627
- private parsedHyperlink?: ParsedHyperlinkInstruction;
628
- /** Revisions that wrap the result section (for tracked changes in field content) */
629
- private resultRevisions: Revision[] = [];
630
- /**
631
- * Whether the field has a result section (w:fldSep was present during parsing)
632
- * Per ECMA-376, fields without results skip the separator element.
633
- */
634
- private _hasResultSection = false;
635
- private _formFieldData?: FormFieldData;
636
-
637
- /**
638
- * Creates a new complex field
639
- * @param properties Complex field properties
640
- */
641
- constructor(properties: ComplexFieldProperties) {
642
- this.instruction = properties.instruction;
643
- this.result = properties.result;
644
- this.instructionFormatting = properties.instructionFormatting;
645
- this.resultFormatting = properties.resultFormatting;
646
- this.nestedFields = properties.nestedFields || [];
647
- this.resultContent = properties.resultContent || [];
648
- this.multiParagraph = properties.multiParagraph || false;
649
- this._hasResultSection = properties.hasResult ?? false;
650
- this._formFieldData = properties.formFieldData;
651
-
652
- // Auto-parse HYPERLINK instruction if provided or detected
653
- if (properties.parsedHyperlink) {
654
- this.parsedHyperlink = properties.parsedHyperlink;
655
- } else if (isHyperlinkInstruction(this.instruction)) {
656
- this.parsedHyperlink = parseHyperlinkInstruction(this.instruction) || undefined;
657
- }
658
- }
659
-
660
- /**
661
- * Gets the field instruction
662
- */
663
- getInstruction(): string {
664
- return this.instruction;
665
- }
666
-
667
- /**
668
- * Sets the field instruction
669
- */
670
- setInstruction(instruction: string): this {
671
- this.instruction = instruction;
672
- return this;
673
- }
674
-
675
- /**
676
- * Gets form field data (w:ffData) if present
677
- */
678
- getFormFieldData(): FormFieldData | undefined {
679
- return this._formFieldData;
680
- }
681
-
682
- /**
683
- * Gets the field result text
684
- */
685
- getResult(): string | undefined {
686
- return this.result;
687
- }
688
-
689
- /**
690
- * Sets the field result text
691
- */
692
- setResult(result: string): this {
693
- this.result = result;
694
- return this;
695
- }
696
-
697
- /**
698
- * Checks if the field has a result section
699
- *
700
- * Per ECMA-376, complex fields may or may not have a result section.
701
- * Fields without results (like TOC markers or empty PAGE fields) skip
702
- * the w:fldSep (separator) element entirely.
703
- *
704
- * This method distinguishes between:
705
- * - `hasResultSection() === true && getResult() === ""` → Field had separator but result was empty
706
- * - `hasResultSection() === false && getResult() === undefined` → Field never had a result section
707
- *
708
- * @returns True if the field has a result section (w:fldSep was present)
709
- *
710
- * @example
711
- * ```typescript
712
- * const field = paragraph.getFields()[0];
713
- * if (field instanceof ComplexField) {
714
- * if (field.hasResultSection()) {
715
- * console.log('Field result:', field.getResult());
716
- * } else {
717
- * console.log('Field has no result section (empty field)');
718
- * }
719
- * }
720
- * ```
721
- */
722
- hasResultSection(): boolean {
723
- return this._hasResultSection;
724
- }
725
-
726
- /**
727
- * Sets instruction formatting
728
- */
729
- setInstructionFormatting(formatting: RunFormatting): this {
730
- this.instructionFormatting = formatting;
731
- return this;
732
- }
733
-
734
- /**
735
- * Sets result formatting
736
- */
737
- setResultFormatting(formatting: RunFormatting): this {
738
- this.resultFormatting = formatting;
739
- return this;
740
- }
741
-
742
- /**
743
- * Gets the parsed HYPERLINK instruction components
744
- * Returns undefined if this is not a HYPERLINK field
745
- */
746
- getParsedHyperlink(): ParsedHyperlinkInstruction | undefined {
747
- return this.parsedHyperlink;
748
- }
749
-
750
- /**
751
- * Checks if this field is a HYPERLINK field
752
- */
753
- isHyperlinkField(): boolean {
754
- return this.parsedHyperlink !== undefined;
755
- }
756
-
757
- /**
758
- * Gets the full URL for HYPERLINK fields (combining base URL and anchor)
759
- * Returns undefined if not a HYPERLINK field
760
- */
761
- getHyperlinkUrl(): string | undefined {
762
- return this.parsedHyperlink?.fullUrl;
763
- }
764
-
765
- /**
766
- * Adds a nested field within this field
767
- * Nested fields appear between instruction and separator
768
- */
769
- addNestedField(field: ComplexField): this {
770
- this.nestedFields.push(field);
771
- return this;
772
- }
773
-
774
- /**
775
- * Gets all nested fields
776
- */
777
- getNestedFields(): ComplexField[] {
778
- return [...this.nestedFields];
779
- }
780
-
781
- /**
782
- * Removes a nested field at the specified index
783
- * @param index - Index of the nested field to remove (0-based)
784
- * @returns True if removed, false if index out of bounds
785
- *
786
- * @example
787
- * ```typescript
788
- * const field = new ComplexField({ instruction: 'TOC' });
789
- * field.addNestedField(nested1);
790
- * field.addNestedField(nested2);
791
- * field.removeNestedField(0); // Removes nested1
792
- * ```
793
- */
794
- removeNestedField(index: number): boolean {
795
- if (index < 0 || index >= this.nestedFields.length) {
796
- return false;
797
- }
798
- this.nestedFields.splice(index, 1);
799
- return true;
800
- }
801
-
802
- /**
803
- * Gets the count of nested fields
804
- * @returns Number of nested fields
805
- */
806
- getNestedFieldCount(): number {
807
- return this.nestedFields.length;
808
- }
809
-
810
- /**
811
- * Clears all nested fields
812
- * @returns This field for chaining
813
- */
814
- clearNestedFields(): this {
815
- this.nestedFields = [];
816
- return this;
817
- }
818
-
819
- /**
820
- * Updates the field instruction
821
- * @param instruction - New field instruction (e.g., 'TOC \\o "1-3"')
822
- * @returns This field for chaining
823
- *
824
- * @example
825
- * ```typescript
826
- * const field = new ComplexField({ instruction: 'DATE' });
827
- * field.updateInstruction('DATE \\@ "yyyy-MM-dd"');
828
- * ```
829
- */
830
- updateInstruction(instruction: string): this {
831
- this.instruction = instruction;
832
- return this;
833
- }
834
-
835
- /**
836
- * Adds custom XML content to the result section
837
- */
838
- addResultContent(content: XMLElement): this {
839
- this.resultContent.push(content);
840
- return this;
841
- }
842
-
843
- /**
844
- * Gets result content XML elements
845
- */
846
- getResultContent(): XMLElement[] {
847
- return [...this.resultContent];
848
- }
849
-
850
- /**
851
- * Sets whether this field spans multiple paragraphs
852
- */
853
- setMultiParagraph(multiParagraph: boolean): this {
854
- this.multiParagraph = multiParagraph;
855
- return this;
856
- }
857
-
858
- /**
859
- * Gets whether this field spans multiple paragraphs
860
- */
861
- isMultiParagraph(): boolean {
862
- return this.multiParagraph;
863
- }
864
-
865
- /**
866
- * Sets revisions that wrap the result section
867
- * These are tracked changes (w:ins, w:del) that need to wrap the result AND end marker
868
- * @param revisions Array of Revision objects
869
- */
870
- setResultRevisions(revisions: Revision[]): this {
871
- this.resultRevisions = revisions;
872
- return this;
873
- }
874
-
875
- /**
876
- * Adds a revision to the result section
877
- * @param revision Revision to add
878
- */
879
- addResultRevision(revision: Revision): this {
880
- this.resultRevisions.push(revision);
881
- return this;
882
- }
883
-
884
- /**
885
- * Gets revisions that wrap the result section
886
- */
887
- getResultRevisions(): Revision[] {
888
- return [...this.resultRevisions];
889
- }
890
-
891
- /**
892
- * Checks if this field has revisions in the result section
893
- */
894
- hasResultRevisions(): boolean {
895
- return this.resultRevisions.length > 0;
896
- }
897
-
898
- /**
899
- * Generates XML for the complex field
900
- * Returns array of run elements (begin, instr, sep, result, end)
901
- */
902
- toXML(): XMLElement[] {
903
- const runs: XMLElement[] = [];
904
-
905
- // 1. Begin marker run
906
- const beginFldChar: XMLElement = this._formFieldData
907
- ? this.buildFldCharWithFfData()
908
- : {
909
- name: 'w:fldChar',
910
- attributes: { 'w:fldCharType': 'begin' },
911
- selfClosing: true,
912
- };
913
- runs.push({
914
- name: 'w:r',
915
- children: [beginFldChar],
916
- });
917
-
918
- // 2. Instruction run
919
- const instrChildren: XMLElement[] = [];
920
- if (this.instructionFormatting) {
921
- instrChildren.push(this.createRunProperties(this.instructionFormatting));
922
- }
923
- instrChildren.push({
924
- name: 'w:instrText',
925
- attributes: { 'xml:space': 'preserve' },
926
- children: [this.instruction],
927
- });
928
- runs.push({
929
- name: 'w:r',
930
- children: instrChildren,
931
- });
932
-
933
- // 2a. Nested fields (if any)
934
- for (const nestedField of this.nestedFields) {
935
- runs.push(...nestedField.toXML());
936
- }
937
-
938
- // 3. Separator run
939
- runs.push({
940
- name: 'w:r',
941
- children: [
942
- {
943
- name: 'w:fldChar',
944
- attributes: { 'w:fldCharType': 'separate' },
945
- selfClosing: true,
946
- },
947
- ],
948
- });
949
-
950
- // 4. Result content (prioritize custom XML content, then simple text)
951
- // Design note: For INCLUDEPICTURE fields, the parser stores the w:drawing
952
- // content in resultContent so it survives the parser→generator round-trip.
953
- // When flattenFieldCodes() is active, the full field structure is emitted
954
- // here, then _postProcessDocumentXml() strips the INCLUDEPICTURE scaffolding
955
- // (fldChar/instrText runs) from the final XML while preserving the drawing.
956
- // Non-INCLUDEPICTURE fields emit their complete structure unchanged.
957
- if (this.resultContent.length > 0) {
958
- // Use custom XML content
959
- runs.push(...this.resultContent);
960
- } else if (this.result) {
961
- // Use simple text result
962
- const resultChildren: XMLElement[] = [];
963
- if (this.resultFormatting) {
964
- resultChildren.push(this.createRunProperties(this.resultFormatting));
965
- }
966
- resultChildren.push({
967
- name: 'w:t',
968
- attributes: { 'xml:space': 'preserve' },
969
- children: [this.result],
970
- });
971
- runs.push({
972
- name: 'w:r',
973
- children: resultChildren,
974
- });
975
- }
976
-
977
- // 4a. Result revisions (tracked changes within the result section)
978
- // These MUST appear between the separator and end marker per ECMA-376
979
- // The revisions contain the actual field result content wrapped in w:ins or w:del
980
- for (const revision of this.resultRevisions) {
981
- const revisionXml = revision.toXML();
982
- if (revisionXml) {
983
- runs.push(revisionXml);
984
- }
985
- }
986
-
987
- // 5. End marker run
988
- runs.push({
989
- name: 'w:r',
990
- children: [
991
- {
992
- name: 'w:fldChar',
993
- attributes: { 'w:fldCharType': 'end' },
994
- selfClosing: true,
995
- },
996
- ],
997
- });
998
-
999
- return runs;
1000
- }
1001
-
1002
- /**
1003
- * Creates run properties XML from formatting
1004
- */
1005
- private createRunProperties(formatting: RunFormatting): XMLElement {
1006
- const children: XMLElement[] = [];
1007
-
1008
- if (formatting.bold) {
1009
- children.push({ name: 'w:b', selfClosing: true });
1010
- }
1011
-
1012
- if (formatting.italic) {
1013
- children.push({ name: 'w:i', selfClosing: true });
1014
- }
1015
-
1016
- if (formatting.underline) {
1017
- const val =
1018
- typeof formatting.underline === 'string'
1019
- ? formatting.underline
1020
- : 'single';
1021
- children.push({
1022
- name: 'w:u',
1023
- attributes: { 'w:val': val },
1024
- selfClosing: true,
1025
- });
1026
- }
1027
-
1028
- if (formatting.strike) {
1029
- children.push({ name: 'w:strike', selfClosing: true });
1030
- }
1031
-
1032
- if (formatting.font) {
1033
- children.push({
1034
- name: 'w:rFonts',
1035
- attributes: {
1036
- 'w:ascii': formatting.font,
1037
- 'w:hAnsi': formatting.font,
1038
- 'w:cs': formatting.font,
1039
- },
1040
- selfClosing: true,
1041
- });
1042
- }
1043
-
1044
- if (formatting.size) {
1045
- const sizeValue = pointsToHalfPoints(formatting.size).toString();
1046
- children.push({
1047
- name: 'w:sz',
1048
- attributes: { 'w:val': sizeValue },
1049
- selfClosing: true,
1050
- });
1051
- children.push({
1052
- name: 'w:szCs',
1053
- attributes: { 'w:val': sizeValue },
1054
- selfClosing: true,
1055
- });
1056
- }
1057
-
1058
- if (formatting.color) {
1059
- const color = formatting.color.replace('#', '');
1060
- children.push({
1061
- name: 'w:color',
1062
- attributes: { 'w:val': color },
1063
- selfClosing: true,
1064
- });
1065
- }
1066
-
1067
- if (formatting.highlight) {
1068
- children.push({
1069
- name: 'w:highlight',
1070
- attributes: { 'w:val': formatting.highlight },
1071
- selfClosing: true,
1072
- });
1073
- }
1074
-
1075
- return { name: 'w:rPr', children };
1076
- }
1077
-
1078
- /**
1079
- * Builds a w:fldChar begin element with w:ffData child
1080
- */
1081
- private buildFldCharWithFfData(): XMLElement {
1082
- const ffd = this._formFieldData!;
1083
- const ffDataChildren: (string | XMLElement)[] = [];
1084
-
1085
- if (ffd.name) {
1086
- ffDataChildren.push({ name: 'w:name', attributes: { 'w:val': ffd.name }, selfClosing: true });
1087
- }
1088
- if (ffd.enabled !== undefined) {
1089
- if (ffd.enabled) {
1090
- ffDataChildren.push({ name: 'w:enabled', selfClosing: true });
1091
- } else {
1092
- ffDataChildren.push({ name: 'w:enabled', attributes: { 'w:val': '0' }, selfClosing: true });
1093
- }
1094
- }
1095
- if (ffd.calcOnExit !== undefined) {
1096
- ffDataChildren.push({ name: 'w:calcOnExit', attributes: { 'w:val': ffd.calcOnExit ? '1' : '0' }, selfClosing: true });
1097
- }
1098
- if (ffd.helpText) {
1099
- ffDataChildren.push({ name: 'w:helpText', attributes: { 'w:type': 'text', 'w:val': ffd.helpText }, selfClosing: true });
1100
- }
1101
- if (ffd.statusText) {
1102
- ffDataChildren.push({ name: 'w:statusText', attributes: { 'w:type': 'text', 'w:val': ffd.statusText }, selfClosing: true });
1103
- }
1104
- if (ffd.entryMacro) {
1105
- ffDataChildren.push({ name: 'w:entryMacro', attributes: { 'w:val': ffd.entryMacro }, selfClosing: true });
1106
- }
1107
- if (ffd.exitMacro) {
1108
- ffDataChildren.push({ name: 'w:exitMacro', attributes: { 'w:val': ffd.exitMacro }, selfClosing: true });
1109
- }
1110
-
1111
- if (ffd.fieldType) {
1112
- switch (ffd.fieldType.type) {
1113
- case 'textInput': {
1114
- const tiChildren: (string | XMLElement)[] = [];
1115
- if (ffd.fieldType.inputType) tiChildren.push({ name: 'w:type', attributes: { 'w:val': ffd.fieldType.inputType }, selfClosing: true });
1116
- if (ffd.fieldType.defaultValue) tiChildren.push({ name: 'w:default', attributes: { 'w:val': ffd.fieldType.defaultValue }, selfClosing: true });
1117
- if (ffd.fieldType.maxLength !== undefined) tiChildren.push({ name: 'w:maxLength', attributes: { 'w:val': String(ffd.fieldType.maxLength) }, selfClosing: true });
1118
- if (ffd.fieldType.format) tiChildren.push({ name: 'w:format', attributes: { 'w:val': ffd.fieldType.format }, selfClosing: true });
1119
- ffDataChildren.push({ name: 'w:textInput', children: tiChildren });
1120
- break;
1121
- }
1122
- case 'checkBox': {
1123
- const cbChildren: (string | XMLElement)[] = [];
1124
- if (ffd.fieldType.size === 'auto') {
1125
- cbChildren.push({ name: 'w:sizeAuto', selfClosing: true });
1126
- } else if (ffd.fieldType.size !== undefined) {
1127
- cbChildren.push({ name: 'w:size', attributes: { 'w:val': String(ffd.fieldType.size) }, selfClosing: true });
1128
- }
1129
- if (ffd.fieldType.defaultChecked !== undefined) cbChildren.push({ name: 'w:default', attributes: { 'w:val': ffd.fieldType.defaultChecked ? '1' : '0' }, selfClosing: true });
1130
- if (ffd.fieldType.checked !== undefined) cbChildren.push({ name: 'w:checked', attributes: { 'w:val': ffd.fieldType.checked ? '1' : '0' }, selfClosing: true });
1131
- ffDataChildren.push({ name: 'w:checkBox', children: cbChildren });
1132
- break;
1133
- }
1134
- case 'dropDownList': {
1135
- const ddChildren: (string | XMLElement)[] = [];
1136
- if (ffd.fieldType.result !== undefined) ddChildren.push({ name: 'w:result', attributes: { 'w:val': String(ffd.fieldType.result) }, selfClosing: true });
1137
- if (ffd.fieldType.defaultResult !== undefined) ddChildren.push({ name: 'w:default', attributes: { 'w:val': String(ffd.fieldType.defaultResult) }, selfClosing: true });
1138
- if (ffd.fieldType.listEntries) {
1139
- for (const entry of ffd.fieldType.listEntries) {
1140
- ddChildren.push({ name: 'w:listEntry', attributes: { 'w:val': entry }, selfClosing: true });
1141
- }
1142
- }
1143
- ffDataChildren.push({ name: 'w:ddList', children: ddChildren });
1144
- break;
1145
- }
1146
- }
1147
- }
1148
-
1149
- return {
1150
- name: 'w:fldChar',
1151
- attributes: { 'w:fldCharType': 'begin' },
1152
- children: [{ name: 'w:ffData', children: ffDataChildren }],
1153
- };
1154
- }
1155
- }
1156
-
1157
- /**
1158
- * TOC field options
1159
- */
1160
- export interface TOCFieldOptions {
1161
- /** Heading levels to include (e.g., "1-3" for levels 1-3) */
1162
- levels?: string;
1163
-
1164
- /** Make entries hyperlinks (\h switch) */
1165
- hyperlinks?: boolean;
1166
-
1167
- /** Hide tab leaders and page numbers in Web Layout (\z switch) */
1168
- hideInWebLayout?: boolean;
1169
-
1170
- /** Use outline levels (\u switch) */
1171
- useOutlineLevels?: boolean;
1172
-
1173
- /** Omit page numbers (\n switch) */
1174
- omitPageNumbers?: boolean;
1175
-
1176
- /** Custom styles to use (\t switch) */
1177
- customStyles?: string;
1178
- }
1179
-
1180
- /**
1181
- * Creates a TOC (Table of Contents) complex field
1182
- * Generates proper field instruction with switches
1183
- *
1184
- * @param options TOC field options
1185
- * @returns ComplexField configured for TOC
1186
- *
1187
- * @example
1188
- * const toc = createTOCField({ levels: '1-3', hyperlinks: true });
1189
- * // Generates: TOC \o "1-3" \h \z \u
1190
- */
1191
- export function createTOCField(options: TOCFieldOptions = {}): ComplexField {
1192
- // Build instruction string
1193
- let instruction = ' TOC';
1194
-
1195
- // Add outline levels switch
1196
- if (options.levels !== undefined) {
1197
- instruction += ` \\o "${options.levels}"`;
1198
- } else {
1199
- instruction += ' \\o "1-3"'; // Default: levels 1-3
1200
- }
1201
-
1202
- // Add hyperlinks switch
1203
- if (options.hyperlinks !== false) {
1204
- instruction += ' \\h';
1205
- }
1206
-
1207
- // Add hide in web layout switch
1208
- if (options.hideInWebLayout !== false) {
1209
- instruction += ' \\z';
1210
- }
1211
-
1212
- // Add use outline levels switch
1213
- if (options.useOutlineLevels !== false) {
1214
- instruction += ' \\u';
1215
- }
1216
-
1217
- // Add omit page numbers switch
1218
- if (options.omitPageNumbers) {
1219
- instruction += ' \\n';
1220
- }
1221
-
1222
- // Add custom styles switch
1223
- if (options.customStyles) {
1224
- instruction += ` \\t "${options.customStyles}"`;
1225
- }
1226
-
1227
- instruction += ' '; // Trailing space per Microsoft convention
1228
-
1229
- return new ComplexField({
1230
- instruction,
1231
- result: 'Table of Contents', // Placeholder result
1232
- });
1233
- }
1
+ /**
2
+ * Field - Represents a dynamic field in a Word document
3
+ *
4
+ * Fields are used for dynamic content like page numbers, dates, document properties, etc.
5
+ * They are represented using the <w:fldSimple> element with field codes.
6
+ */
7
+
8
+ import { XMLElement } from '../xml/XMLBuilder';
9
+ import { RunFormatting, FormFieldData } from './Run';
10
+ import {
11
+ ParsedHyperlinkInstruction,
12
+ parseHyperlinkInstruction,
13
+ isHyperlinkInstruction,
14
+ } from './FieldHelpers';
15
+ import type { Revision } from './Revision';
16
+ import { pointsToHalfPoints } from '../utils/units';
17
+
18
+ /**
19
+ * Common field types
20
+ */
21
+ export type FieldType =
22
+ | 'PAGE' // Current page number
23
+ | 'NUMPAGES' // Total number of pages
24
+ | 'DATE' // Current date
25
+ | 'TIME' // Current time
26
+ | 'AUTHOR' // Document author
27
+ | 'TITLE' // Document title
28
+ | 'FILENAME' // Document filename
29
+ | 'FILENAMEWITHPATH' // Document filename with path
30
+ | 'SUBJECT' // Document subject
31
+ | 'KEYWORDS' // Document keywords
32
+ | 'CREATEDATE' // Document creation date
33
+ | 'SAVEDATE' // Last save date
34
+ | 'PRINTDATE' // Last print date
35
+ | 'SECTIONPAGES' // Pages in current section
36
+ | 'SECTION' // Current section number
37
+ | 'REF' // Cross-reference to bookmark
38
+ | 'HYPERLINK' // Hyperlink field
39
+ | 'SEQ' // Sequence numbering
40
+ | 'TC' // Table of contents entry
41
+ | 'XE' // Index entry
42
+ | 'IF' // Conditional field
43
+ | 'MERGEFIELD' // Mail merge field
44
+ | 'INCLUDE' // Include text from external file
45
+ | 'INCLUDETEXT' // Include text from external file (alias)
46
+ | 'CUSTOM'; // Custom field type for unknown/specialized fields
47
+
48
+ /**
49
+ * Field properties
50
+ */
51
+ export interface FieldProperties {
52
+ /** Field type */
53
+ type: FieldType;
54
+ /** Field instruction (e.g., 'PAGE \* MERGEFORMAT') */
55
+ instruction?: string;
56
+ /** Format switches (e.g., '\\* MERGEFORMAT') */
57
+ format?: string;
58
+ /** Date/time format (e.g., 'MMMM d, yyyy') */
59
+ dateFormat?: string;
60
+ /** Preserve formatting during updates */
61
+ preserveFormatting?: boolean;
62
+ /** Run formatting for field result */
63
+ formatting?: RunFormatting;
64
+ }
65
+
66
+ /**
67
+ * Represents a dynamic field
68
+ */
69
+ export class Field {
70
+ private type: FieldType;
71
+ private instruction: string;
72
+ private formatting?: RunFormatting;
73
+
74
+ /**
75
+ * Creates a new field
76
+ * @param properties Field properties
77
+ */
78
+ constructor(properties: FieldProperties) {
79
+ this.type = properties.type;
80
+ this.formatting = properties.formatting;
81
+
82
+ // Build field instruction
83
+ if (properties.instruction) {
84
+ this.instruction = properties.instruction;
85
+ } else {
86
+ this.instruction = this.buildInstruction(properties);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Builds field instruction from properties
92
+ */
93
+ private buildInstruction(properties: FieldProperties): string {
94
+ let instruction = properties.type;
95
+
96
+ // Add date format for date/time fields
97
+ if (properties.dateFormat && this.isDateField(properties.type)) {
98
+ instruction += ` \\@ "${properties.dateFormat}"`;
99
+ }
100
+
101
+ // Add format switch
102
+ if (properties.format) {
103
+ instruction += ` ${properties.format}`;
104
+ } else if (properties.preserveFormatting !== false) {
105
+ // Add MERGEFORMAT by default to preserve formatting
106
+ instruction += ' \\* MERGEFORMAT';
107
+ }
108
+
109
+ return instruction;
110
+ }
111
+
112
+ /**
113
+ * Checks if field type is a date field
114
+ */
115
+ private isDateField(type: FieldType): boolean {
116
+ return ['DATE', 'TIME', 'CREATEDATE', 'SAVEDATE', 'PRINTDATE'].includes(type);
117
+ }
118
+
119
+ /**
120
+ * Gets the field type
121
+ */
122
+ getType(): FieldType {
123
+ return this.type;
124
+ }
125
+
126
+ /**
127
+ * Gets the field instruction
128
+ */
129
+ getInstruction(): string {
130
+ return this.instruction;
131
+ }
132
+
133
+ /**
134
+ * Sets run formatting for the field
135
+ */
136
+ setFormatting(formatting: RunFormatting): this {
137
+ this.formatting = formatting;
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Gets run formatting
143
+ */
144
+ getFormatting(): RunFormatting | undefined {
145
+ return this.formatting;
146
+ }
147
+
148
+ /**
149
+ * Checks if this field is a HYPERLINK field
150
+ * @returns True if the field type is HYPERLINK or instruction starts with HYPERLINK
151
+ */
152
+ isHyperlinkField(): boolean {
153
+ return (
154
+ this.type === 'HYPERLINK' || this.instruction.trim().toUpperCase().startsWith('HYPERLINK')
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Sets text color for the field
160
+ * @param color Color in hex format (e.g., '0000FF')
161
+ * @returns This field for chaining
162
+ */
163
+ setColor(color: string): this {
164
+ if (!this.formatting) {
165
+ this.formatting = {};
166
+ }
167
+ this.formatting.color = color.replace('#', '');
168
+ return this;
169
+ }
170
+
171
+ /**
172
+ * Generates XML for the field.
173
+ * Per ECMA-376, w:fldSimple is a paragraph-level element that CONTAINS w:r children.
174
+ * Structure: <w:fldSimple w:instr="..."><w:r><w:rPr/><w:t>...</w:t></w:r></w:fldSimple>
175
+ * The fldSimple element should be added directly to paragraph children (not wrapped in w:r).
176
+ */
177
+ toXML(): XMLElement {
178
+ // Build the inner run with optional formatting
179
+ const runChildren: XMLElement[] = [];
180
+ if (this.formatting) {
181
+ runChildren.push(this.createRunProperties());
182
+ }
183
+ runChildren.push({
184
+ name: 'w:t',
185
+ children: [this.getPlaceholderText()],
186
+ });
187
+
188
+ return {
189
+ name: 'w:fldSimple',
190
+ attributes: {
191
+ 'w:instr': this.instruction,
192
+ },
193
+ children: [
194
+ {
195
+ name: 'w:r',
196
+ children: runChildren,
197
+ },
198
+ ],
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Gets placeholder text for the field
204
+ */
205
+ private getPlaceholderText(): string {
206
+ switch (this.type) {
207
+ case 'PAGE':
208
+ return '1';
209
+ case 'NUMPAGES':
210
+ return '1';
211
+ case 'SECTIONPAGES':
212
+ return '1';
213
+ case 'SECTION':
214
+ return '1';
215
+ case 'DATE':
216
+ return new Date().toLocaleDateString();
217
+ case 'TIME':
218
+ return new Date().toLocaleTimeString();
219
+ case 'CREATEDATE':
220
+ case 'SAVEDATE':
221
+ case 'PRINTDATE':
222
+ return new Date().toLocaleDateString();
223
+ case 'FILENAME':
224
+ return 'Document';
225
+ case 'FILENAMEWITHPATH':
226
+ return 'C:\\Document.docx';
227
+ case 'AUTHOR':
228
+ return 'Author';
229
+ case 'TITLE':
230
+ return 'Title';
231
+ case 'SUBJECT':
232
+ return 'Subject';
233
+ case 'KEYWORDS':
234
+ return 'Keywords';
235
+ case 'REF':
236
+ return '1'; // Reference typically shows page number or heading text
237
+ case 'HYPERLINK':
238
+ return 'Link'; // Hyperlink displays the link text
239
+ case 'SEQ':
240
+ return '1'; // Sequence shows the current number
241
+ case 'TC':
242
+ return ''; // TC fields are hidden
243
+ case 'XE':
244
+ return ''; // XE fields are hidden
245
+ default:
246
+ return '';
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Creates run properties XML
252
+ */
253
+ private createRunProperties(): XMLElement {
254
+ const children: XMLElement[] = [];
255
+
256
+ if (!this.formatting) {
257
+ return { name: 'w:rPr', children };
258
+ }
259
+
260
+ // Per ECMA-376 CT_RPr ordering:
261
+ // rFonts → b → i → strike → color → sz/szCs → highlight → u
262
+
263
+ if (this.formatting.font) {
264
+ children.push({
265
+ name: 'w:rFonts',
266
+ attributes: {
267
+ 'w:ascii': this.formatting.font,
268
+ 'w:hAnsi': this.formatting.font,
269
+ 'w:cs': this.formatting.font,
270
+ },
271
+ selfClosing: true,
272
+ });
273
+ }
274
+
275
+ if (this.formatting.bold) {
276
+ children.push({ name: 'w:b', selfClosing: true });
277
+ }
278
+
279
+ if (this.formatting.italic) {
280
+ children.push({ name: 'w:i', selfClosing: true });
281
+ }
282
+
283
+ if (this.formatting.strike) {
284
+ children.push({ name: 'w:strike', selfClosing: true });
285
+ }
286
+
287
+ if (this.formatting.color) {
288
+ const color = this.formatting.color.replace('#', '');
289
+ children.push({
290
+ name: 'w:color',
291
+ attributes: { 'w:val': color },
292
+ selfClosing: true,
293
+ });
294
+ }
295
+
296
+ if (this.formatting.size) {
297
+ const sizeValue = pointsToHalfPoints(this.formatting.size).toString();
298
+ children.push({
299
+ name: 'w:sz',
300
+ attributes: { 'w:val': sizeValue },
301
+ selfClosing: true,
302
+ });
303
+ children.push({
304
+ name: 'w:szCs',
305
+ attributes: { 'w:val': sizeValue },
306
+ selfClosing: true,
307
+ });
308
+ }
309
+
310
+ if (this.formatting.highlight) {
311
+ children.push({
312
+ name: 'w:highlight',
313
+ attributes: { 'w:val': this.formatting.highlight },
314
+ selfClosing: true,
315
+ });
316
+ }
317
+
318
+ if (this.formatting.underline) {
319
+ const val =
320
+ typeof this.formatting.underline === 'string' ? this.formatting.underline : 'single';
321
+ children.push({
322
+ name: 'w:u',
323
+ attributes: { 'w:val': val },
324
+ selfClosing: true,
325
+ });
326
+ }
327
+
328
+ return { name: 'w:rPr', children };
329
+ }
330
+
331
+ /**
332
+ * Creates a page number field
333
+ * @param formatting Optional run formatting
334
+ */
335
+ static createPageNumber(formatting?: RunFormatting): Field {
336
+ return new Field({
337
+ type: 'PAGE',
338
+ formatting,
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Creates a total pages field
344
+ * @param formatting Optional run formatting
345
+ */
346
+ static createTotalPages(formatting?: RunFormatting): Field {
347
+ return new Field({
348
+ type: 'NUMPAGES',
349
+ formatting,
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Creates a date field
355
+ * @param format Date format (e.g., 'MMMM d, yyyy')
356
+ * @param formatting Optional run formatting
357
+ */
358
+ static createDate(format?: string, formatting?: RunFormatting): Field {
359
+ return new Field({
360
+ type: 'DATE',
361
+ dateFormat: format,
362
+ formatting,
363
+ });
364
+ }
365
+
366
+ /**
367
+ * Creates a time field
368
+ * @param format Time format
369
+ * @param formatting Optional run formatting
370
+ */
371
+ static createTime(format?: string, formatting?: RunFormatting): Field {
372
+ return new Field({
373
+ type: 'TIME',
374
+ dateFormat: format,
375
+ formatting,
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Creates a filename field
381
+ * @param includePath Whether to include full path
382
+ * @param formatting Optional run formatting
383
+ */
384
+ static createFilename(includePath = false, formatting?: RunFormatting): Field {
385
+ return new Field({
386
+ type: includePath ? 'FILENAMEWITHPATH' : 'FILENAME',
387
+ formatting,
388
+ });
389
+ }
390
+
391
+ /**
392
+ * Creates an author field
393
+ * @param formatting Optional run formatting
394
+ */
395
+ static createAuthor(formatting?: RunFormatting): Field {
396
+ return new Field({
397
+ type: 'AUTHOR',
398
+ formatting,
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Creates a title field
404
+ * @param formatting Optional run formatting
405
+ */
406
+ static createTitle(formatting?: RunFormatting): Field {
407
+ return new Field({
408
+ type: 'TITLE',
409
+ formatting,
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Creates a section pages field (pages in current section)
415
+ * @param formatting Optional run formatting
416
+ */
417
+ static createSectionPages(formatting?: RunFormatting): Field {
418
+ return new Field({
419
+ type: 'SECTIONPAGES',
420
+ formatting,
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Creates a cross-reference field
426
+ * @param bookmark Bookmark name to reference
427
+ * @param format Reference format (\h for hyperlink, \p for page number, etc.)
428
+ * @param formatting Optional run formatting
429
+ */
430
+ static createRef(bookmark: string, format?: string, formatting?: RunFormatting): Field {
431
+ const formatSwitch = format || '\\h'; // Default to hyperlink format
432
+ const instruction = `REF ${bookmark} ${formatSwitch} \\* MERGEFORMAT`;
433
+
434
+ return new Field({
435
+ type: 'REF',
436
+ instruction,
437
+ formatting,
438
+ });
439
+ }
440
+
441
+ /**
442
+ * Creates a hyperlink field
443
+ * @param url The URL to link to
444
+ * @param displayText The text to display
445
+ * @param tooltip Optional tooltip text
446
+ * @param formatting Optional run formatting
447
+ */
448
+ static createHyperlink(
449
+ url: string,
450
+ displayText: string = url,
451
+ tooltip?: string,
452
+ formatting?: RunFormatting
453
+ ): Field {
454
+ let instruction = `HYPERLINK "${url}"`;
455
+
456
+ if (tooltip) {
457
+ instruction += ` \\o "${tooltip}"`;
458
+ }
459
+
460
+ instruction += ' \\* MERGEFORMAT';
461
+
462
+ return new Field({
463
+ type: 'HYPERLINK',
464
+ instruction,
465
+ formatting,
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Creates a sequence numbering field
471
+ * @param identifier Sequence identifier (e.g., 'Figure', 'Table')
472
+ * @param format Number format (\* ARABIC, \* ROMAN, etc.)
473
+ * @param formatting Optional run formatting
474
+ */
475
+ static createSeq(identifier: string, format?: string, formatting?: RunFormatting): Field {
476
+ let instruction = `SEQ ${identifier}`;
477
+
478
+ if (format) {
479
+ instruction += ` ${format}`;
480
+ } else {
481
+ instruction += ' \\* ARABIC'; // Default to Arabic numerals
482
+ }
483
+
484
+ instruction += ' \\* MERGEFORMAT';
485
+
486
+ return new Field({
487
+ type: 'SEQ',
488
+ instruction,
489
+ formatting,
490
+ });
491
+ }
492
+
493
+ /**
494
+ * Creates a table of contents entry field (TC)
495
+ * @param text Entry text
496
+ * @param level TOC level (1-9)
497
+ * @param formatting Optional run formatting
498
+ */
499
+ static createTCEntry(text: string, level = 1, formatting?: RunFormatting): Field {
500
+ if (level < 1 || level > 9) {
501
+ throw new Error('TC level must be between 1 and 9');
502
+ }
503
+
504
+ const instruction = `TC "${text}" \\f C \\l ${level}`;
505
+
506
+ return new Field({
507
+ type: 'TC',
508
+ instruction,
509
+ formatting,
510
+ });
511
+ }
512
+
513
+ /**
514
+ * Creates an index entry field (XE)
515
+ * @param text Entry text
516
+ * @param subEntry Optional sub-entry text
517
+ * @param formatting Optional run formatting
518
+ */
519
+ static createXEEntry(text: string, subEntry?: string, formatting?: RunFormatting): Field {
520
+ let instruction = `XE "${text}"`;
521
+
522
+ if (subEntry) {
523
+ instruction += `:${subEntry}`;
524
+ }
525
+
526
+ return new Field({
527
+ type: 'XE',
528
+ instruction,
529
+ formatting,
530
+ });
531
+ }
532
+
533
+ /**
534
+ * Creates a custom field with instruction
535
+ * @param instruction Field instruction code
536
+ * @param formatting Optional run formatting
537
+ */
538
+ static createCustom(instruction: string, formatting?: RunFormatting): Field {
539
+ return new Field({
540
+ type: 'PAGE', // Placeholder type
541
+ instruction,
542
+ formatting,
543
+ });
544
+ }
545
+
546
+ /**
547
+ * Creates a field from properties
548
+ * @param properties Field properties
549
+ */
550
+ static create(properties: FieldProperties): Field {
551
+ return new Field(properties);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Field character type for complex fields
557
+ */
558
+ export type FieldCharType = 'begin' | 'separate' | 'end';
559
+
560
+ /**
561
+ * Complex field properties
562
+ * Complex fields use begin/separate/end structure instead of fldSimple
563
+ */
564
+ export interface ComplexFieldProperties {
565
+ /** Field instruction (e.g., " TOC \\o \"1-3\" \\h \\z \\u ") */
566
+ instruction: string;
567
+
568
+ /** Current field result text (optional) */
569
+ result?: string;
570
+
571
+ /** Run formatting for instruction */
572
+ instructionFormatting?: RunFormatting;
573
+
574
+ /** Run formatting for result */
575
+ resultFormatting?: RunFormatting;
576
+
577
+ /** Nested fields to include within this field */
578
+ nestedFields?: ComplexField[];
579
+
580
+ /** Custom XML content for result section */
581
+ resultContent?: XMLElement[];
582
+
583
+ /** Whether field spans multiple paragraphs */
584
+ multiParagraph?: boolean;
585
+
586
+ /** Parsed HYPERLINK instruction components (auto-populated if instruction is HYPERLINK) */
587
+ parsedHyperlink?: ParsedHyperlinkInstruction;
588
+
589
+ /**
590
+ * Whether the field has a result section (w:fldSep was present during parsing)
591
+ * Per ECMA-376, fields without results skip the separator element.
592
+ * This flag distinguishes between:
593
+ * - hasResult=true, result="" → Field had separator but result was empty
594
+ * - hasResult=false, result="" → Field never had a result section (empty field)
595
+ */
596
+ hasResult?: boolean;
597
+
598
+ /** Form field data (w:ffData) from begin field char per ECMA-376 §17.16.17 */
599
+ formFieldData?: FormFieldData;
600
+ }
601
+
602
+ /**
603
+ * Represents a complex field (begin/separate/end structure)
604
+ * Used for TOC, cross-references, and other advanced fields
605
+ *
606
+ * Structure:
607
+ * <w:r><w:fldChar w:fldCharType="begin"/></w:r>
608
+ * <w:r><w:instrText>INSTRUCTION</w:instrText></w:r>
609
+ * <w:r><w:fldChar w:fldCharType="separate"/></w:r>
610
+ * <w:r><w:t>RESULT</w:t></w:r>
611
+ * <w:r><w:fldChar w:fldCharType="end"/></w:r>
612
+ */
613
+ export class ComplexField {
614
+ private instruction: string;
615
+ private result?: string;
616
+ private instructionFormatting?: RunFormatting;
617
+ private resultFormatting?: RunFormatting;
618
+ private nestedFields: ComplexField[];
619
+ private resultContent: XMLElement[];
620
+ private multiParagraph: boolean;
621
+ private parsedHyperlink?: ParsedHyperlinkInstruction;
622
+ /** Revisions that wrap the result section (for tracked changes in field content) */
623
+ private resultRevisions: Revision[] = [];
624
+ /**
625
+ * Whether the field has a result section (w:fldSep was present during parsing)
626
+ * Per ECMA-376, fields without results skip the separator element.
627
+ */
628
+ private _hasResultSection = false;
629
+ private _formFieldData?: FormFieldData;
630
+
631
+ /**
632
+ * Creates a new complex field
633
+ * @param properties Complex field properties
634
+ */
635
+ constructor(properties: ComplexFieldProperties) {
636
+ this.instruction = properties.instruction;
637
+ this.result = properties.result;
638
+ this.instructionFormatting = properties.instructionFormatting;
639
+ this.resultFormatting = properties.resultFormatting;
640
+ this.nestedFields = properties.nestedFields || [];
641
+ this.resultContent = properties.resultContent || [];
642
+ this.multiParagraph = properties.multiParagraph || false;
643
+ this._hasResultSection = properties.hasResult ?? false;
644
+ this._formFieldData = properties.formFieldData;
645
+
646
+ // Auto-parse HYPERLINK instruction if provided or detected
647
+ if (properties.parsedHyperlink) {
648
+ this.parsedHyperlink = properties.parsedHyperlink;
649
+ } else if (isHyperlinkInstruction(this.instruction)) {
650
+ this.parsedHyperlink = parseHyperlinkInstruction(this.instruction) || undefined;
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Gets the field instruction
656
+ */
657
+ getInstruction(): string {
658
+ return this.instruction;
659
+ }
660
+
661
+ /**
662
+ * Sets the field instruction
663
+ */
664
+ setInstruction(instruction: string): this {
665
+ this.instruction = instruction;
666
+ return this;
667
+ }
668
+
669
+ /**
670
+ * Gets form field data (w:ffData) if present
671
+ */
672
+ getFormFieldData(): FormFieldData | undefined {
673
+ return this._formFieldData;
674
+ }
675
+
676
+ /**
677
+ * Gets the field result text
678
+ */
679
+ getResult(): string | undefined {
680
+ return this.result;
681
+ }
682
+
683
+ /**
684
+ * Sets the field result text
685
+ */
686
+ setResult(result: string): this {
687
+ this.result = result;
688
+ return this;
689
+ }
690
+
691
+ /**
692
+ * Checks if the field has a result section
693
+ *
694
+ * Per ECMA-376, complex fields may or may not have a result section.
695
+ * Fields without results (like TOC markers or empty PAGE fields) skip
696
+ * the w:fldSep (separator) element entirely.
697
+ *
698
+ * This method distinguishes between:
699
+ * - `hasResultSection() === true && getResult() === ""` → Field had separator but result was empty
700
+ * - `hasResultSection() === false && getResult() === undefined` → Field never had a result section
701
+ *
702
+ * @returns True if the field has a result section (w:fldSep was present)
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * const field = paragraph.getFields()[0];
707
+ * if (field instanceof ComplexField) {
708
+ * if (field.hasResultSection()) {
709
+ * console.log('Field result:', field.getResult());
710
+ * } else {
711
+ * console.log('Field has no result section (empty field)');
712
+ * }
713
+ * }
714
+ * ```
715
+ */
716
+ hasResultSection(): boolean {
717
+ return this._hasResultSection;
718
+ }
719
+
720
+ /**
721
+ * Sets instruction formatting
722
+ */
723
+ setInstructionFormatting(formatting: RunFormatting): this {
724
+ this.instructionFormatting = formatting;
725
+ return this;
726
+ }
727
+
728
+ /**
729
+ * Sets result formatting
730
+ */
731
+ setResultFormatting(formatting: RunFormatting): this {
732
+ this.resultFormatting = formatting;
733
+ return this;
734
+ }
735
+
736
+ /**
737
+ * Gets the parsed HYPERLINK instruction components
738
+ * Returns undefined if this is not a HYPERLINK field
739
+ */
740
+ getParsedHyperlink(): ParsedHyperlinkInstruction | undefined {
741
+ return this.parsedHyperlink;
742
+ }
743
+
744
+ /**
745
+ * Checks if this field is a HYPERLINK field
746
+ */
747
+ isHyperlinkField(): boolean {
748
+ return this.parsedHyperlink !== undefined;
749
+ }
750
+
751
+ /**
752
+ * Gets the full URL for HYPERLINK fields (combining base URL and anchor)
753
+ * Returns undefined if not a HYPERLINK field
754
+ */
755
+ getHyperlinkUrl(): string | undefined {
756
+ return this.parsedHyperlink?.fullUrl;
757
+ }
758
+
759
+ /**
760
+ * Adds a nested field within this field
761
+ * Nested fields appear between instruction and separator
762
+ */
763
+ addNestedField(field: ComplexField): this {
764
+ this.nestedFields.push(field);
765
+ return this;
766
+ }
767
+
768
+ /**
769
+ * Gets all nested fields
770
+ */
771
+ getNestedFields(): ComplexField[] {
772
+ return [...this.nestedFields];
773
+ }
774
+
775
+ /**
776
+ * Removes a nested field at the specified index
777
+ * @param index - Index of the nested field to remove (0-based)
778
+ * @returns True if removed, false if index out of bounds
779
+ *
780
+ * @example
781
+ * ```typescript
782
+ * const field = new ComplexField({ instruction: 'TOC' });
783
+ * field.addNestedField(nested1);
784
+ * field.addNestedField(nested2);
785
+ * field.removeNestedField(0); // Removes nested1
786
+ * ```
787
+ */
788
+ removeNestedField(index: number): boolean {
789
+ if (index < 0 || index >= this.nestedFields.length) {
790
+ return false;
791
+ }
792
+ this.nestedFields.splice(index, 1);
793
+ return true;
794
+ }
795
+
796
+ /**
797
+ * Gets the count of nested fields
798
+ * @returns Number of nested fields
799
+ */
800
+ getNestedFieldCount(): number {
801
+ return this.nestedFields.length;
802
+ }
803
+
804
+ /**
805
+ * Clears all nested fields
806
+ * @returns This field for chaining
807
+ */
808
+ clearNestedFields(): this {
809
+ this.nestedFields = [];
810
+ return this;
811
+ }
812
+
813
+ /**
814
+ * Updates the field instruction
815
+ * @param instruction - New field instruction (e.g., 'TOC \\o "1-3"')
816
+ * @returns This field for chaining
817
+ *
818
+ * @example
819
+ * ```typescript
820
+ * const field = new ComplexField({ instruction: 'DATE' });
821
+ * field.updateInstruction('DATE \\@ "yyyy-MM-dd"');
822
+ * ```
823
+ */
824
+ updateInstruction(instruction: string): this {
825
+ this.instruction = instruction;
826
+ return this;
827
+ }
828
+
829
+ /**
830
+ * Adds custom XML content to the result section
831
+ */
832
+ addResultContent(content: XMLElement): this {
833
+ this.resultContent.push(content);
834
+ return this;
835
+ }
836
+
837
+ /**
838
+ * Gets result content XML elements
839
+ */
840
+ getResultContent(): XMLElement[] {
841
+ return [...this.resultContent];
842
+ }
843
+
844
+ /**
845
+ * Sets whether this field spans multiple paragraphs
846
+ */
847
+ setMultiParagraph(multiParagraph: boolean): this {
848
+ this.multiParagraph = multiParagraph;
849
+ return this;
850
+ }
851
+
852
+ /**
853
+ * Gets whether this field spans multiple paragraphs
854
+ */
855
+ isMultiParagraph(): boolean {
856
+ return this.multiParagraph;
857
+ }
858
+
859
+ /**
860
+ * Sets revisions that wrap the result section
861
+ * These are tracked changes (w:ins, w:del) that need to wrap the result AND end marker
862
+ * @param revisions Array of Revision objects
863
+ */
864
+ setResultRevisions(revisions: Revision[]): this {
865
+ this.resultRevisions = revisions;
866
+ return this;
867
+ }
868
+
869
+ /**
870
+ * Adds a revision to the result section
871
+ * @param revision Revision to add
872
+ */
873
+ addResultRevision(revision: Revision): this {
874
+ this.resultRevisions.push(revision);
875
+ return this;
876
+ }
877
+
878
+ /**
879
+ * Gets revisions that wrap the result section
880
+ */
881
+ getResultRevisions(): Revision[] {
882
+ return [...this.resultRevisions];
883
+ }
884
+
885
+ /**
886
+ * Checks if this field has revisions in the result section
887
+ */
888
+ hasResultRevisions(): boolean {
889
+ return this.resultRevisions.length > 0;
890
+ }
891
+
892
+ /**
893
+ * Generates XML for the complex field
894
+ * Returns array of run elements (begin, instr, sep, result, end)
895
+ */
896
+ toXML(): XMLElement[] {
897
+ const runs: XMLElement[] = [];
898
+
899
+ // 1. Begin marker run
900
+ const beginFldChar: XMLElement = this._formFieldData
901
+ ? this.buildFldCharWithFfData()
902
+ : {
903
+ name: 'w:fldChar',
904
+ attributes: { 'w:fldCharType': 'begin' },
905
+ selfClosing: true,
906
+ };
907
+ runs.push({
908
+ name: 'w:r',
909
+ children: [beginFldChar],
910
+ });
911
+
912
+ // 2. Instruction run
913
+ const instrChildren: XMLElement[] = [];
914
+ if (this.instructionFormatting) {
915
+ instrChildren.push(this.createRunProperties(this.instructionFormatting));
916
+ }
917
+ instrChildren.push({
918
+ name: 'w:instrText',
919
+ attributes: { 'xml:space': 'preserve' },
920
+ children: [this.instruction],
921
+ });
922
+ runs.push({
923
+ name: 'w:r',
924
+ children: instrChildren,
925
+ });
926
+
927
+ // 2a. Nested fields (if any)
928
+ for (const nestedField of this.nestedFields) {
929
+ runs.push(...nestedField.toXML());
930
+ }
931
+
932
+ // 3. Separator run
933
+ runs.push({
934
+ name: 'w:r',
935
+ children: [
936
+ {
937
+ name: 'w:fldChar',
938
+ attributes: { 'w:fldCharType': 'separate' },
939
+ selfClosing: true,
940
+ },
941
+ ],
942
+ });
943
+
944
+ // 4. Result content (prioritize custom XML content, then simple text)
945
+ // Design note: For INCLUDEPICTURE fields, the parser stores the w:drawing
946
+ // content in resultContent so it survives the parser→generator round-trip.
947
+ // When flattenFieldCodes() is active, the full field structure is emitted
948
+ // here, then _postProcessDocumentXml() strips the INCLUDEPICTURE scaffolding
949
+ // (fldChar/instrText runs) from the final XML while preserving the drawing.
950
+ // Non-INCLUDEPICTURE fields emit their complete structure unchanged.
951
+ if (this.resultContent.length > 0) {
952
+ // Use custom XML content
953
+ runs.push(...this.resultContent);
954
+ } else if (this.result) {
955
+ // Use simple text result
956
+ const resultChildren: XMLElement[] = [];
957
+ if (this.resultFormatting) {
958
+ resultChildren.push(this.createRunProperties(this.resultFormatting));
959
+ }
960
+ resultChildren.push({
961
+ name: 'w:t',
962
+ attributes: { 'xml:space': 'preserve' },
963
+ children: [this.result],
964
+ });
965
+ runs.push({
966
+ name: 'w:r',
967
+ children: resultChildren,
968
+ });
969
+ }
970
+
971
+ // 4a. Result revisions (tracked changes within the result section)
972
+ // These MUST appear between the separator and end marker per ECMA-376
973
+ // The revisions contain the actual field result content wrapped in w:ins or w:del
974
+ for (const revision of this.resultRevisions) {
975
+ const revisionXml = revision.toXML();
976
+ if (revisionXml) {
977
+ runs.push(revisionXml);
978
+ }
979
+ }
980
+
981
+ // 5. End marker run
982
+ runs.push({
983
+ name: 'w:r',
984
+ children: [
985
+ {
986
+ name: 'w:fldChar',
987
+ attributes: { 'w:fldCharType': 'end' },
988
+ selfClosing: true,
989
+ },
990
+ ],
991
+ });
992
+
993
+ return runs;
994
+ }
995
+
996
+ /**
997
+ * Creates run properties XML from formatting
998
+ */
999
+ private createRunProperties(formatting: RunFormatting): XMLElement {
1000
+ const children: XMLElement[] = [];
1001
+
1002
+ if (formatting.bold) {
1003
+ children.push({ name: 'w:b', selfClosing: true });
1004
+ }
1005
+
1006
+ if (formatting.italic) {
1007
+ children.push({ name: 'w:i', selfClosing: true });
1008
+ }
1009
+
1010
+ if (formatting.underline) {
1011
+ const val = typeof formatting.underline === 'string' ? formatting.underline : 'single';
1012
+ children.push({
1013
+ name: 'w:u',
1014
+ attributes: { 'w:val': val },
1015
+ selfClosing: true,
1016
+ });
1017
+ }
1018
+
1019
+ if (formatting.strike) {
1020
+ children.push({ name: 'w:strike', selfClosing: true });
1021
+ }
1022
+
1023
+ if (formatting.font) {
1024
+ children.push({
1025
+ name: 'w:rFonts',
1026
+ attributes: {
1027
+ 'w:ascii': formatting.font,
1028
+ 'w:hAnsi': formatting.font,
1029
+ 'w:cs': formatting.font,
1030
+ },
1031
+ selfClosing: true,
1032
+ });
1033
+ }
1034
+
1035
+ if (formatting.size) {
1036
+ const sizeValue = pointsToHalfPoints(formatting.size).toString();
1037
+ children.push({
1038
+ name: 'w:sz',
1039
+ attributes: { 'w:val': sizeValue },
1040
+ selfClosing: true,
1041
+ });
1042
+ children.push({
1043
+ name: 'w:szCs',
1044
+ attributes: { 'w:val': sizeValue },
1045
+ selfClosing: true,
1046
+ });
1047
+ }
1048
+
1049
+ if (formatting.color) {
1050
+ const color = formatting.color.replace('#', '');
1051
+ children.push({
1052
+ name: 'w:color',
1053
+ attributes: { 'w:val': color },
1054
+ selfClosing: true,
1055
+ });
1056
+ }
1057
+
1058
+ if (formatting.highlight) {
1059
+ children.push({
1060
+ name: 'w:highlight',
1061
+ attributes: { 'w:val': formatting.highlight },
1062
+ selfClosing: true,
1063
+ });
1064
+ }
1065
+
1066
+ return { name: 'w:rPr', children };
1067
+ }
1068
+
1069
+ /**
1070
+ * Builds a w:fldChar begin element with w:ffData child
1071
+ */
1072
+ private buildFldCharWithFfData(): XMLElement {
1073
+ const ffd = this._formFieldData!;
1074
+ const ffDataChildren: (string | XMLElement)[] = [];
1075
+
1076
+ if (ffd.name) {
1077
+ ffDataChildren.push({ name: 'w:name', attributes: { 'w:val': ffd.name }, selfClosing: true });
1078
+ }
1079
+ if (ffd.enabled !== undefined) {
1080
+ if (ffd.enabled) {
1081
+ ffDataChildren.push({ name: 'w:enabled', selfClosing: true });
1082
+ } else {
1083
+ ffDataChildren.push({ name: 'w:enabled', attributes: { 'w:val': '0' }, selfClosing: true });
1084
+ }
1085
+ }
1086
+ if (ffd.calcOnExit !== undefined) {
1087
+ ffDataChildren.push({
1088
+ name: 'w:calcOnExit',
1089
+ attributes: { 'w:val': ffd.calcOnExit ? '1' : '0' },
1090
+ selfClosing: true,
1091
+ });
1092
+ }
1093
+ if (ffd.helpText) {
1094
+ ffDataChildren.push({
1095
+ name: 'w:helpText',
1096
+ attributes: { 'w:type': 'text', 'w:val': ffd.helpText },
1097
+ selfClosing: true,
1098
+ });
1099
+ }
1100
+ if (ffd.statusText) {
1101
+ ffDataChildren.push({
1102
+ name: 'w:statusText',
1103
+ attributes: { 'w:type': 'text', 'w:val': ffd.statusText },
1104
+ selfClosing: true,
1105
+ });
1106
+ }
1107
+ if (ffd.entryMacro) {
1108
+ ffDataChildren.push({
1109
+ name: 'w:entryMacro',
1110
+ attributes: { 'w:val': ffd.entryMacro },
1111
+ selfClosing: true,
1112
+ });
1113
+ }
1114
+ if (ffd.exitMacro) {
1115
+ ffDataChildren.push({
1116
+ name: 'w:exitMacro',
1117
+ attributes: { 'w:val': ffd.exitMacro },
1118
+ selfClosing: true,
1119
+ });
1120
+ }
1121
+
1122
+ if (ffd.fieldType) {
1123
+ switch (ffd.fieldType.type) {
1124
+ case 'textInput': {
1125
+ const tiChildren: (string | XMLElement)[] = [];
1126
+ if (ffd.fieldType.inputType)
1127
+ tiChildren.push({
1128
+ name: 'w:type',
1129
+ attributes: { 'w:val': ffd.fieldType.inputType },
1130
+ selfClosing: true,
1131
+ });
1132
+ if (ffd.fieldType.defaultValue)
1133
+ tiChildren.push({
1134
+ name: 'w:default',
1135
+ attributes: { 'w:val': ffd.fieldType.defaultValue },
1136
+ selfClosing: true,
1137
+ });
1138
+ if (ffd.fieldType.maxLength !== undefined)
1139
+ tiChildren.push({
1140
+ name: 'w:maxLength',
1141
+ attributes: { 'w:val': String(ffd.fieldType.maxLength) },
1142
+ selfClosing: true,
1143
+ });
1144
+ if (ffd.fieldType.format)
1145
+ tiChildren.push({
1146
+ name: 'w:format',
1147
+ attributes: { 'w:val': ffd.fieldType.format },
1148
+ selfClosing: true,
1149
+ });
1150
+ ffDataChildren.push({ name: 'w:textInput', children: tiChildren });
1151
+ break;
1152
+ }
1153
+ case 'checkBox': {
1154
+ const cbChildren: (string | XMLElement)[] = [];
1155
+ if (ffd.fieldType.size === 'auto') {
1156
+ cbChildren.push({ name: 'w:sizeAuto', selfClosing: true });
1157
+ } else if (ffd.fieldType.size !== undefined) {
1158
+ cbChildren.push({
1159
+ name: 'w:size',
1160
+ attributes: { 'w:val': String(ffd.fieldType.size) },
1161
+ selfClosing: true,
1162
+ });
1163
+ }
1164
+ if (ffd.fieldType.defaultChecked !== undefined)
1165
+ cbChildren.push({
1166
+ name: 'w:default',
1167
+ attributes: { 'w:val': ffd.fieldType.defaultChecked ? '1' : '0' },
1168
+ selfClosing: true,
1169
+ });
1170
+ if (ffd.fieldType.checked !== undefined)
1171
+ cbChildren.push({
1172
+ name: 'w:checked',
1173
+ attributes: { 'w:val': ffd.fieldType.checked ? '1' : '0' },
1174
+ selfClosing: true,
1175
+ });
1176
+ ffDataChildren.push({ name: 'w:checkBox', children: cbChildren });
1177
+ break;
1178
+ }
1179
+ case 'dropDownList': {
1180
+ const ddChildren: (string | XMLElement)[] = [];
1181
+ if (ffd.fieldType.result !== undefined)
1182
+ ddChildren.push({
1183
+ name: 'w:result',
1184
+ attributes: { 'w:val': String(ffd.fieldType.result) },
1185
+ selfClosing: true,
1186
+ });
1187
+ if (ffd.fieldType.defaultResult !== undefined)
1188
+ ddChildren.push({
1189
+ name: 'w:default',
1190
+ attributes: { 'w:val': String(ffd.fieldType.defaultResult) },
1191
+ selfClosing: true,
1192
+ });
1193
+ if (ffd.fieldType.listEntries) {
1194
+ for (const entry of ffd.fieldType.listEntries) {
1195
+ ddChildren.push({
1196
+ name: 'w:listEntry',
1197
+ attributes: { 'w:val': entry },
1198
+ selfClosing: true,
1199
+ });
1200
+ }
1201
+ }
1202
+ ffDataChildren.push({ name: 'w:ddList', children: ddChildren });
1203
+ break;
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ return {
1209
+ name: 'w:fldChar',
1210
+ attributes: { 'w:fldCharType': 'begin' },
1211
+ children: [{ name: 'w:ffData', children: ffDataChildren }],
1212
+ };
1213
+ }
1214
+ }
1215
+
1216
+ /**
1217
+ * TOC field options
1218
+ */
1219
+ export interface TOCFieldOptions {
1220
+ /** Heading levels to include (e.g., "1-3" for levels 1-3) */
1221
+ levels?: string;
1222
+
1223
+ /** Make entries hyperlinks (\h switch) */
1224
+ hyperlinks?: boolean;
1225
+
1226
+ /** Hide tab leaders and page numbers in Web Layout (\z switch) */
1227
+ hideInWebLayout?: boolean;
1228
+
1229
+ /** Use outline levels (\u switch) */
1230
+ useOutlineLevels?: boolean;
1231
+
1232
+ /** Omit page numbers (\n switch) */
1233
+ omitPageNumbers?: boolean;
1234
+
1235
+ /** Custom styles to use (\t switch) */
1236
+ customStyles?: string;
1237
+ }
1238
+
1239
+ /**
1240
+ * Creates a TOC (Table of Contents) complex field
1241
+ * Generates proper field instruction with switches
1242
+ *
1243
+ * @param options TOC field options
1244
+ * @returns ComplexField configured for TOC
1245
+ *
1246
+ * @example
1247
+ * const toc = createTOCField({ levels: '1-3', hyperlinks: true });
1248
+ * // Generates: TOC \o "1-3" \h \z \u
1249
+ */
1250
+ export function createTOCField(options: TOCFieldOptions = {}): ComplexField {
1251
+ // Build instruction string
1252
+ let instruction = ' TOC';
1253
+
1254
+ // Add outline levels switch
1255
+ if (options.levels !== undefined) {
1256
+ instruction += ` \\o "${options.levels}"`;
1257
+ } else {
1258
+ instruction += ' \\o "1-3"'; // Default: levels 1-3
1259
+ }
1260
+
1261
+ // Add hyperlinks switch
1262
+ if (options.hyperlinks !== false) {
1263
+ instruction += ' \\h';
1264
+ }
1265
+
1266
+ // Add hide in web layout switch
1267
+ if (options.hideInWebLayout !== false) {
1268
+ instruction += ' \\z';
1269
+ }
1270
+
1271
+ // Add use outline levels switch
1272
+ if (options.useOutlineLevels !== false) {
1273
+ instruction += ' \\u';
1274
+ }
1275
+
1276
+ // Add omit page numbers switch
1277
+ if (options.omitPageNumbers) {
1278
+ instruction += ' \\n';
1279
+ }
1280
+
1281
+ // Add custom styles switch
1282
+ if (options.customStyles) {
1283
+ instruction += ` \\t "${options.customStyles}"`;
1284
+ }
1285
+
1286
+ instruction += ' '; // Trailing space per Microsoft convention
1287
+
1288
+ return new ComplexField({
1289
+ instruction,
1290
+ result: 'Table of Contents', // Placeholder result
1291
+ });
1292
+ }