docxmlater 10.0.1 → 10.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/README.md +3 -2
  2. package/dist/constants/legacyCompatFlags.d.ts.map +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.d.ts +0 -27
  5. package/dist/constants/limits.d.ts.map +1 -1
  6. package/dist/constants/limits.js +13 -13
  7. package/dist/constants/limits.js.map +1 -1
  8. package/dist/core/Document.d.ts +24 -19
  9. package/dist/core/Document.d.ts.map +1 -1
  10. package/dist/core/Document.js +272 -71
  11. package/dist/core/Document.js.map +1 -1
  12. package/dist/core/DocumentContent.d.ts.map +1 -1
  13. package/dist/core/DocumentContent.js.map +1 -1
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +59 -24
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.d.ts.map +1 -1
  18. package/dist/core/DocumentIdManager.js.map +1 -1
  19. package/dist/core/DocumentParser.d.ts +6 -6
  20. package/dist/core/DocumentParser.d.ts.map +1 -1
  21. package/dist/core/DocumentParser.js +60 -54
  22. package/dist/core/DocumentParser.js.map +1 -1
  23. package/dist/core/DocumentValidator.d.ts.map +1 -1
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.d.ts.map +1 -1
  26. package/dist/core/Relationship.js +1 -1
  27. package/dist/core/Relationship.js.map +1 -1
  28. package/dist/core/RelationshipManager.js +3 -3
  29. package/dist/core/RelationshipManager.js.map +1 -1
  30. package/dist/elements/AlternateContent.js.map +1 -1
  31. package/dist/elements/Bookmark.d.ts.map +1 -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.js +1 -1
  36. package/dist/elements/Comment.js.map +1 -1
  37. package/dist/elements/CommentManager.d.ts.map +1 -1
  38. package/dist/elements/CommentManager.js +8 -2
  39. package/dist/elements/CommentManager.js.map +1 -1
  40. package/dist/elements/CommonTypes.d.ts.map +1 -1
  41. package/dist/elements/CommonTypes.js +1 -2
  42. package/dist/elements/CommonTypes.js.map +1 -1
  43. package/dist/elements/CustomXml.js.map +1 -1
  44. package/dist/elements/Endnote.d.ts.map +1 -1
  45. package/dist/elements/Endnote.js.map +1 -1
  46. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  47. package/dist/elements/EndnoteManager.js.map +1 -1
  48. package/dist/elements/Field.d.ts.map +1 -1
  49. package/dist/elements/Field.js +31 -28
  50. package/dist/elements/Field.js.map +1 -1
  51. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  52. package/dist/elements/FieldHelpers.js +6 -6
  53. package/dist/elements/FieldHelpers.js.map +1 -1
  54. package/dist/elements/FontManager.d.ts.map +1 -1
  55. package/dist/elements/FontManager.js.map +1 -1
  56. package/dist/elements/Footer.js.map +1 -1
  57. package/dist/elements/Footnote.d.ts.map +1 -1
  58. package/dist/elements/Footnote.js.map +1 -1
  59. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  60. package/dist/elements/FootnoteManager.js.map +1 -1
  61. package/dist/elements/Header.js.map +1 -1
  62. package/dist/elements/HeaderFooterManager.js.map +1 -1
  63. package/dist/elements/Hyperlink.d.ts.map +1 -1
  64. package/dist/elements/Hyperlink.js +5 -5
  65. package/dist/elements/Hyperlink.js.map +1 -1
  66. package/dist/elements/Image.d.ts +2 -2
  67. package/dist/elements/Image.d.ts.map +1 -1
  68. package/dist/elements/Image.js +21 -5
  69. package/dist/elements/Image.js.map +1 -1
  70. package/dist/elements/ImageManager.d.ts.map +1 -1
  71. package/dist/elements/ImageManager.js +2 -2
  72. package/dist/elements/ImageManager.js.map +1 -1
  73. package/dist/elements/ImageRun.js.map +1 -1
  74. package/dist/elements/MathElement.js.map +1 -1
  75. package/dist/elements/Paragraph.d.ts.map +1 -1
  76. package/dist/elements/Paragraph.js +128 -117
  77. package/dist/elements/Paragraph.js.map +1 -1
  78. package/dist/elements/PreservedElement.js.map +1 -1
  79. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  80. package/dist/elements/RangeMarker.js.map +1 -1
  81. package/dist/elements/Revision.d.ts +1 -0
  82. package/dist/elements/Revision.d.ts.map +1 -1
  83. package/dist/elements/Revision.js +44 -5
  84. package/dist/elements/Revision.js.map +1 -1
  85. package/dist/elements/RevisionContent.js.map +1 -1
  86. package/dist/elements/RevisionManager.d.ts.map +1 -1
  87. package/dist/elements/RevisionManager.js.map +1 -1
  88. package/dist/elements/Run.d.ts.map +1 -1
  89. package/dist/elements/Run.js +1 -3
  90. package/dist/elements/Run.js.map +1 -1
  91. package/dist/elements/Section.d.ts.map +1 -1
  92. package/dist/elements/Section.js +127 -118
  93. package/dist/elements/Section.js.map +1 -1
  94. package/dist/elements/Shape.d.ts.map +1 -1
  95. package/dist/elements/Shape.js +21 -0
  96. package/dist/elements/Shape.js.map +1 -1
  97. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  98. package/dist/elements/StructuredDocumentTag.js +20 -8
  99. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  100. package/dist/elements/Table.d.ts +2 -2
  101. package/dist/elements/Table.d.ts.map +1 -1
  102. package/dist/elements/Table.js +29 -35
  103. package/dist/elements/Table.js.map +1 -1
  104. package/dist/elements/TableCell.d.ts +2 -2
  105. package/dist/elements/TableCell.d.ts.map +1 -1
  106. package/dist/elements/TableCell.js +63 -67
  107. package/dist/elements/TableCell.js.map +1 -1
  108. package/dist/elements/TableGridChange.js.map +1 -1
  109. package/dist/elements/TableOfContents.d.ts +6 -6
  110. package/dist/elements/TableOfContents.d.ts.map +1 -1
  111. package/dist/elements/TableOfContents.js.map +1 -1
  112. package/dist/elements/TableOfContentsElement.js.map +1 -1
  113. package/dist/elements/TableRow.d.ts.map +1 -1
  114. package/dist/elements/TableRow.js +65 -47
  115. package/dist/elements/TableRow.js.map +1 -1
  116. package/dist/elements/TextBox.d.ts.map +1 -1
  117. package/dist/elements/TextBox.js +1 -1
  118. package/dist/elements/TextBox.js.map +1 -1
  119. package/dist/formatting/AbstractNumbering.d.ts +1 -1
  120. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  121. package/dist/formatting/AbstractNumbering.js +11 -11
  122. package/dist/formatting/AbstractNumbering.js.map +1 -1
  123. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  124. package/dist/formatting/NumberingInstance.js +4 -4
  125. package/dist/formatting/NumberingInstance.js.map +1 -1
  126. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  127. package/dist/formatting/NumberingLevel.js +26 -26
  128. package/dist/formatting/NumberingLevel.js.map +1 -1
  129. package/dist/formatting/NumberingManager.d.ts +1 -1
  130. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  131. package/dist/formatting/NumberingManager.js.map +1 -1
  132. package/dist/formatting/Style.d.ts.map +1 -1
  133. package/dist/formatting/Style.js +87 -95
  134. package/dist/formatting/Style.js.map +1 -1
  135. package/dist/formatting/StylesManager.d.ts +3 -3
  136. package/dist/formatting/StylesManager.d.ts.map +1 -1
  137. package/dist/formatting/StylesManager.js.map +1 -1
  138. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  139. package/dist/helpers/CleanupHelper.js +1 -7
  140. package/dist/helpers/CleanupHelper.js.map +1 -1
  141. package/dist/images/ImageOptimizer.js.map +1 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/managers/DrawingManager.d.ts.map +1 -1
  144. package/dist/managers/DrawingManager.js.map +1 -1
  145. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  146. package/dist/tracking/TrackingContext.js.map +1 -1
  147. package/dist/types/compatibility-types.js.map +1 -1
  148. package/dist/types/formatting.js.map +1 -1
  149. package/dist/types/list-types.d.ts +4 -4
  150. package/dist/types/list-types.d.ts.map +1 -1
  151. package/dist/types/list-types.js.map +1 -1
  152. package/dist/types/settings-types.js.map +1 -1
  153. package/dist/types/styleConfig.js.map +1 -1
  154. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  155. package/dist/utils/ChangelogGenerator.js.map +1 -1
  156. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  157. package/dist/utils/CompatibilityUpgrader.js +7 -7
  158. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  159. package/dist/utils/InMemoryRevisionAcceptor.js +1 -1
  160. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  161. package/dist/utils/MoveOperationHelper.js.map +1 -1
  162. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  163. package/dist/utils/RevisionWalker.js.map +1 -1
  164. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  165. package/dist/utils/ShadingResolver.js +1 -1
  166. package/dist/utils/ShadingResolver.js.map +1 -1
  167. package/dist/utils/acceptRevisions.d.ts +0 -28
  168. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  169. package/dist/utils/acceptRevisions.js +5 -7
  170. package/dist/utils/acceptRevisions.js.map +1 -1
  171. package/dist/utils/cnfStyleDecoder.js +1 -1
  172. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  173. package/dist/utils/corruptionDetection.js.map +1 -1
  174. package/dist/utils/dateFormatting.js.map +1 -1
  175. package/dist/utils/deepClone.d.ts +0 -1
  176. package/dist/utils/deepClone.d.ts.map +1 -1
  177. package/dist/utils/deepClone.js +0 -7
  178. package/dist/utils/deepClone.js.map +1 -1
  179. package/dist/utils/diagnostics.d.ts +2 -2
  180. package/dist/utils/diagnostics.d.ts.map +1 -1
  181. package/dist/utils/diagnostics.js.map +1 -1
  182. package/dist/utils/errorHandling.js.map +1 -1
  183. package/dist/utils/formatting.js.map +1 -1
  184. package/dist/utils/list-detection.d.ts +2 -2
  185. package/dist/utils/list-detection.d.ts.map +1 -1
  186. package/dist/utils/list-detection.js +3 -3
  187. package/dist/utils/list-detection.js.map +1 -1
  188. package/dist/utils/logger.d.ts +2 -4
  189. package/dist/utils/logger.d.ts.map +1 -1
  190. package/dist/utils/logger.js +0 -2
  191. package/dist/utils/logger.js.map +1 -1
  192. package/dist/utils/parsingHelpers.js.map +1 -1
  193. package/dist/utils/stripTrackedChanges.d.ts +0 -19
  194. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  195. package/dist/utils/stripTrackedChanges.js +0 -2
  196. package/dist/utils/stripTrackedChanges.js.map +1 -1
  197. package/dist/utils/textDiff.js.map +1 -1
  198. package/dist/utils/units.js.map +1 -1
  199. package/dist/utils/validation.d.ts.map +1 -1
  200. package/dist/utils/validation.js.map +1 -1
  201. package/dist/utils/xmlSanitization.js.map +1 -1
  202. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  203. package/dist/validation/RevisionValidator.js.map +1 -1
  204. package/dist/validation/ValidationRules.js.map +1 -1
  205. package/dist/validation/index.js.map +1 -1
  206. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  207. package/dist/xml/XMLBuilder.js +10 -0
  208. package/dist/xml/XMLBuilder.js.map +1 -1
  209. package/dist/xml/XMLParser.d.ts.map +1 -1
  210. package/dist/xml/XMLParser.js +4 -5
  211. package/dist/xml/XMLParser.js.map +1 -1
  212. package/dist/zip/ZipHandler.js.map +1 -1
  213. package/dist/zip/ZipReader.js.map +1 -1
  214. package/dist/zip/ZipWriter.js.map +1 -1
  215. package/dist/zip/errors.js.map +1 -1
  216. package/dist/zip/types.js.map +1 -1
  217. package/package.json +34 -4
  218. package/src/__tests__/helper-methods.test.ts +512 -0
  219. package/src/constants/legacyCompatFlags.ts +138 -0
  220. package/src/constants/limits.ts +50 -0
  221. package/src/core/CLAUDE.md +109 -0
  222. package/src/core/Document.ts +15569 -0
  223. package/src/core/DocumentContent.ts +467 -0
  224. package/src/core/DocumentGenerator.ts +1104 -0
  225. package/src/core/DocumentIdManager.ts +158 -0
  226. package/src/core/DocumentParser.ts +10107 -0
  227. package/src/core/DocumentValidator.ts +372 -0
  228. package/src/core/Relationship.ts +367 -0
  229. package/src/core/RelationshipManager.ts +428 -0
  230. package/src/elements/AlternateContent.ts +42 -0
  231. package/src/elements/Bookmark.ts +210 -0
  232. package/src/elements/BookmarkManager.ts +250 -0
  233. package/src/elements/CLAUDE.md +126 -0
  234. package/src/elements/Comment.ts +359 -0
  235. package/src/elements/CommentManager.ts +502 -0
  236. package/src/elements/CommonTypes.ts +549 -0
  237. package/src/elements/CustomXml.ts +36 -0
  238. package/src/elements/Endnote.ts +217 -0
  239. package/src/elements/EndnoteManager.ts +249 -0
  240. package/src/elements/Field.ts +1233 -0
  241. package/src/elements/FieldHelpers.ts +333 -0
  242. package/src/elements/FontManager.ts +339 -0
  243. package/src/elements/Footer.ts +269 -0
  244. package/src/elements/Footnote.ts +217 -0
  245. package/src/elements/FootnoteManager.ts +249 -0
  246. package/src/elements/Header.ts +269 -0
  247. package/src/elements/HeaderFooterManager.ts +219 -0
  248. package/src/elements/Hyperlink.ts +1146 -0
  249. package/src/elements/Image.ts +1756 -0
  250. package/src/elements/ImageManager.ts +432 -0
  251. package/src/elements/ImageRun.ts +59 -0
  252. package/src/elements/MathElement.ts +65 -0
  253. package/src/elements/Paragraph.ts +4227 -0
  254. package/src/elements/PreservedElement.ts +53 -0
  255. package/src/elements/PropertyChangeTypes.ts +442 -0
  256. package/src/elements/RangeMarker.ts +400 -0
  257. package/src/elements/Revision.ts +1217 -0
  258. package/src/elements/RevisionContent.ts +73 -0
  259. package/src/elements/RevisionManager.ts +1070 -0
  260. package/src/elements/Run.ts +3068 -0
  261. package/src/elements/Section.ts +1421 -0
  262. package/src/elements/Shape.ts +873 -0
  263. package/src/elements/StructuredDocumentTag.ts +978 -0
  264. package/src/elements/Table.ts +2524 -0
  265. package/src/elements/TableCell.ts +1586 -0
  266. package/src/elements/TableGridChange.ts +151 -0
  267. package/src/elements/TableOfContents.ts +691 -0
  268. package/src/elements/TableOfContentsElement.ts +89 -0
  269. package/src/elements/TableRow.ts +906 -0
  270. package/src/elements/TextBox.ts +768 -0
  271. package/src/formatting/AbstractNumbering.ts +548 -0
  272. package/src/formatting/CLAUDE.md +74 -0
  273. package/src/formatting/NumberingInstance.ts +212 -0
  274. package/src/formatting/NumberingLevel.ts +1006 -0
  275. package/src/formatting/NumberingManager.ts +827 -0
  276. package/src/formatting/Style.ts +1833 -0
  277. package/src/formatting/StylesManager.ts +1005 -0
  278. package/src/helpers/CleanupHelper.ts +524 -0
  279. package/src/images/ImageOptimizer.ts +274 -0
  280. package/src/index.ts +554 -0
  281. package/src/managers/CLAUDE.md +47 -0
  282. package/src/managers/DrawingManager.ts +319 -0
  283. package/src/tracking/DocumentTrackingContext.ts +643 -0
  284. package/src/tracking/TrackingContext.ts +173 -0
  285. package/src/types/compatibility-types.ts +49 -0
  286. package/src/types/formatting.ts +210 -0
  287. package/src/types/list-types.ts +152 -0
  288. package/src/types/settings-types.ts +59 -0
  289. package/src/types/styleConfig.ts +189 -0
  290. package/src/utils/CLAUDE.md +153 -0
  291. package/src/utils/ChangelogGenerator.ts +1581 -0
  292. package/src/utils/CompatibilityUpgrader.ts +237 -0
  293. package/src/utils/InMemoryRevisionAcceptor.ts +668 -0
  294. package/src/utils/MoveOperationHelper.ts +238 -0
  295. package/src/utils/RevisionAwareProcessor.ts +526 -0
  296. package/src/utils/RevisionWalker.ts +457 -0
  297. package/src/utils/SelectiveRevisionAcceptor.ts +613 -0
  298. package/src/utils/ShadingResolver.ts +107 -0
  299. package/src/utils/acceptRevisions.ts +714 -0
  300. package/src/utils/cnfStyleDecoder.ts +217 -0
  301. package/src/utils/corruptionDetection.ts +345 -0
  302. package/src/utils/dateFormatting.ts +20 -0
  303. package/src/utils/deepClone.ts +78 -0
  304. package/src/utils/diagnostics.ts +129 -0
  305. package/src/utils/errorHandling.ts +80 -0
  306. package/src/utils/formatting.ts +213 -0
  307. package/src/utils/list-detection.ts +274 -0
  308. package/src/utils/logger.ts +404 -0
  309. package/src/utils/parsingHelpers.ts +190 -0
  310. package/src/utils/stripTrackedChanges.ts +353 -0
  311. package/src/utils/textDiff.ts +100 -0
  312. package/src/utils/units.ts +421 -0
  313. package/src/utils/validation.ts +542 -0
  314. package/src/utils/xmlSanitization.ts +182 -0
  315. package/src/validation/RevisionAutoFixer.ts +542 -0
  316. package/src/validation/RevisionValidator.ts +460 -0
  317. package/src/validation/ValidationRules.ts +338 -0
  318. package/src/validation/index.ts +30 -0
  319. package/src/xml/CLAUDE.md +65 -0
  320. package/src/xml/XMLBuilder.ts +871 -0
  321. package/src/xml/XMLParser.ts +919 -0
  322. package/src/zip/CLAUDE.md +55 -0
  323. package/src/zip/ZipHandler.ts +637 -0
  324. package/src/zip/ZipReader.ts +299 -0
  325. package/src/zip/ZipWriter.ts +390 -0
  326. package/src/zip/errors.ts +69 -0
  327. package/src/zip/types.ts +116 -0
  328. package/dist/core/ListNormalizer.d.ts +0 -23
  329. package/dist/core/ListNormalizer.d.ts.map +0 -1
  330. package/dist/core/ListNormalizer.js +0 -624
  331. package/dist/core/ListNormalizer.js.map +0 -1
  332. package/dist/images/index.d.ts +0 -2
  333. package/dist/images/index.d.ts.map +0 -1
  334. package/dist/images/index.js +0 -8
  335. package/dist/images/index.js.map +0 -1
  336. package/dist/ms-doc/cfb/CFBReader.d.ts +0 -35
  337. package/dist/ms-doc/cfb/CFBReader.d.ts.map +0 -1
  338. package/dist/ms-doc/cfb/CFBReader.js +0 -360
  339. package/dist/ms-doc/cfb/CFBReader.js.map +0 -1
  340. package/dist/ms-doc/converter/DocToDocxConverter.d.ts +0 -55
  341. package/dist/ms-doc/converter/DocToDocxConverter.d.ts.map +0 -1
  342. package/dist/ms-doc/converter/DocToDocxConverter.js +0 -324
  343. package/dist/ms-doc/converter/DocToDocxConverter.js.map +0 -1
  344. package/dist/ms-doc/fib/FIB.d.ts +0 -18
  345. package/dist/ms-doc/fib/FIB.d.ts.map +0 -1
  346. package/dist/ms-doc/fib/FIB.js +0 -342
  347. package/dist/ms-doc/fib/FIB.js.map +0 -1
  348. package/dist/ms-doc/fields/FieldParser.d.ts +0 -31
  349. package/dist/ms-doc/fields/FieldParser.d.ts.map +0 -1
  350. package/dist/ms-doc/fields/FieldParser.js +0 -266
  351. package/dist/ms-doc/fields/FieldParser.js.map +0 -1
  352. package/dist/ms-doc/images/PictureExtractor.d.ts +0 -22
  353. package/dist/ms-doc/images/PictureExtractor.d.ts.map +0 -1
  354. package/dist/ms-doc/images/PictureExtractor.js +0 -233
  355. package/dist/ms-doc/images/PictureExtractor.js.map +0 -1
  356. package/dist/ms-doc/index.d.ts +0 -20
  357. package/dist/ms-doc/index.d.ts.map +0 -1
  358. package/dist/ms-doc/index.js +0 -59
  359. package/dist/ms-doc/index.js.map +0 -1
  360. package/dist/ms-doc/properties/SPRM.d.ts +0 -210
  361. package/dist/ms-doc/properties/SPRM.d.ts.map +0 -1
  362. package/dist/ms-doc/properties/SPRM.js +0 -633
  363. package/dist/ms-doc/properties/SPRM.js.map +0 -1
  364. package/dist/ms-doc/sections/SectionParser.d.ts +0 -25
  365. package/dist/ms-doc/sections/SectionParser.d.ts.map +0 -1
  366. package/dist/ms-doc/sections/SectionParser.js +0 -214
  367. package/dist/ms-doc/sections/SectionParser.js.map +0 -1
  368. package/dist/ms-doc/styles/StyleSheet.d.ts +0 -23
  369. package/dist/ms-doc/styles/StyleSheet.d.ts.map +0 -1
  370. package/dist/ms-doc/styles/StyleSheet.js +0 -268
  371. package/dist/ms-doc/styles/StyleSheet.js.map +0 -1
  372. package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts +0 -61
  373. package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts.map +0 -1
  374. package/dist/ms-doc/subdocuments/SubdocumentParser.js +0 -208
  375. package/dist/ms-doc/subdocuments/SubdocumentParser.js.map +0 -1
  376. package/dist/ms-doc/tables/TableParser.d.ts +0 -29
  377. package/dist/ms-doc/tables/TableParser.d.ts.map +0 -1
  378. package/dist/ms-doc/tables/TableParser.js +0 -176
  379. package/dist/ms-doc/tables/TableParser.js.map +0 -1
  380. package/dist/ms-doc/text/PieceTable.d.ts +0 -21
  381. package/dist/ms-doc/text/PieceTable.d.ts.map +0 -1
  382. package/dist/ms-doc/text/PieceTable.js +0 -171
  383. package/dist/ms-doc/text/PieceTable.js.map +0 -1
  384. package/dist/ms-doc/types/Constants.d.ts +0 -99
  385. package/dist/ms-doc/types/Constants.d.ts.map +0 -1
  386. package/dist/ms-doc/types/Constants.js +0 -102
  387. package/dist/ms-doc/types/Constants.js.map +0 -1
  388. package/dist/ms-doc/types/DocTypes.d.ts +0 -368
  389. package/dist/ms-doc/types/DocTypes.d.ts.map +0 -1
  390. package/dist/ms-doc/types/DocTypes.js +0 -3
  391. package/dist/ms-doc/types/DocTypes.js.map +0 -1
  392. package/dist/tracking/index.d.ts +0 -3
  393. package/dist/tracking/index.d.ts.map +0 -1
  394. package/dist/tracking/index.js +0 -6
  395. package/dist/tracking/index.js.map +0 -1
@@ -0,0 +1,4227 @@
1
+ /**
2
+ * Paragraph - Represents a paragraph in a Word document
3
+ * Contains one or more runs of formatted text
4
+ */
5
+
6
+ import { deepClone } from "../utils/deepClone";
7
+ import { formatDateForXml } from "../utils/dateFormatting";
8
+ import { logParagraphContent, logTextDirection } from "../utils/diagnostics";
9
+ import { defaultLogger } from "../utils/logger";
10
+ import { XMLBuilder, XMLElement } from "../xml/XMLBuilder";
11
+ import { Bookmark } from "./Bookmark";
12
+ import type { Comment } from "./Comment";
13
+ import {
14
+ // Import common types
15
+ ParagraphAlignment as CommonParagraphAlignment,
16
+ BorderStyle as CommonBorderStyle,
17
+ ShadingPattern as CommonShadingPattern,
18
+ BasicShadingPattern,
19
+ TabAlignment as CommonTabAlignment,
20
+ TabLeader as CommonTabLeader,
21
+ TextDirection as CommonTextDirection,
22
+ TextVerticalAlignment,
23
+ BorderDefinition as CommonBorderDefinition,
24
+ TabStop as CommonTabStop,
25
+ ShadingConfig,
26
+ buildShadingAttributes,
27
+ } from "./CommonTypes";
28
+ import { ComplexField, Field } from "./Field";
29
+ import { Hyperlink } from "./Hyperlink";
30
+ import { RangeMarker } from "./RangeMarker";
31
+ import { Revision } from "./Revision";
32
+ import { Run, RunFormatting } from "./Run";
33
+ import { Shape } from "./Shape";
34
+ import { TextBox } from "./TextBox";
35
+ import { PreservedElement } from "./PreservedElement";
36
+
37
+ // ============================================================================
38
+ // RE-EXPORTED TYPES (for backward compatibility)
39
+ // These types are now defined in CommonTypes.ts but re-exported here
40
+ // to maintain backward compatibility with existing imports.
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Paragraph alignment options
45
+ * @see CommonTypes.ParagraphAlignment
46
+ */
47
+ export type ParagraphAlignment = CommonParagraphAlignment;
48
+
49
+ /**
50
+ * Type to indicate ComplexField support in paragraph content
51
+ */
52
+ export type FieldLike = Field | ComplexField;
53
+
54
+ /**
55
+ * Border style types for paragraph borders
56
+ * @see CommonTypes.BorderStyle
57
+ */
58
+ export type BorderStyle = CommonBorderStyle;
59
+
60
+ /**
61
+ * Shading pattern types (basic patterns without percentage fills)
62
+ * @see CommonTypes.BasicShadingPattern for basic patterns
63
+ * @see CommonTypes.ShadingPattern for full pattern set including percentages
64
+ */
65
+ export type ShadingPattern = BasicShadingPattern;
66
+
67
+ /**
68
+ * Tab stop alignment types
69
+ * @see CommonTypes.TabAlignment
70
+ */
71
+ export type TabAlignment = CommonTabAlignment;
72
+
73
+ /**
74
+ * Tab stop leader types
75
+ * @see CommonTypes.TabLeader
76
+ */
77
+ export type TabLeader = CommonTabLeader;
78
+
79
+ /**
80
+ * Text direction types for paragraphs
81
+ * @see CommonTypes.TextDirection
82
+ */
83
+ export type TextDirection = CommonTextDirection;
84
+
85
+ /**
86
+ * Text vertical alignment types
87
+ * @see CommonTypes.TextVerticalAlignment
88
+ */
89
+ export type TextAlignment = TextVerticalAlignment;
90
+
91
+ /**
92
+ * Textbox tight wrap modes
93
+ */
94
+ export type TextboxTightWrap =
95
+ | "none"
96
+ | "allLines"
97
+ | "firstAndLastLine"
98
+ | "firstLineOnly"
99
+ | "lastLineOnly";
100
+
101
+ /**
102
+ * Paragraph property change tracking (for revision history)
103
+ */
104
+ export interface ParagraphPropertiesChange {
105
+ /** Author of the change */
106
+ author?: string;
107
+ /** Date of the change */
108
+ date?: string;
109
+ /** Unique ID for this revision */
110
+ id?: string;
111
+ /** Previous paragraph properties before the change (stored as object) */
112
+ previousProperties?: Partial<ParagraphFormatting>;
113
+ }
114
+
115
+ /**
116
+ * Frame/text box properties
117
+ */
118
+ export interface FrameProperties {
119
+ /** Width in twips */
120
+ w?: number;
121
+ /** Height in twips */
122
+ h?: number;
123
+ /** Height rule */
124
+ hRule?: "auto" | "atLeast" | "exact";
125
+ /** Absolute horizontal position in twips */
126
+ x?: number;
127
+ /** Absolute vertical position in twips */
128
+ y?: number;
129
+ /** Relative horizontal alignment */
130
+ xAlign?: "left" | "center" | "right" | "inside" | "outside";
131
+ /** Relative vertical alignment */
132
+ yAlign?: "top" | "center" | "bottom" | "inside" | "outside";
133
+ /** Horizontal anchor/positioning base */
134
+ hAnchor?: "page" | "margin" | "text";
135
+ /** Vertical anchor/positioning base */
136
+ vAnchor?: "page" | "margin" | "text";
137
+ /** Horizontal padding in twips */
138
+ hSpace?: number;
139
+ /** Vertical padding in twips */
140
+ vSpace?: number;
141
+ /** Text wrapping around frame */
142
+ wrap?: "around" | "notBeside" | "none" | "tight";
143
+ /** Drop cap style */
144
+ dropCap?: "none" | "drop" | "margin";
145
+ /** Drop cap height in lines */
146
+ lines?: number;
147
+ /** Lock frame anchor to paragraph */
148
+ anchorLock?: boolean;
149
+ }
150
+
151
+ /**
152
+ * Single border definition
153
+ */
154
+ export interface BorderDefinition {
155
+ /** Border style */
156
+ style?: BorderStyle;
157
+ /** Border width in eighths of a point (1-96) */
158
+ size?: number;
159
+ /** Border color (hex without #) */
160
+ color?: string;
161
+ /** Space between border and text in points (0-31) */
162
+ space?: number;
163
+ }
164
+
165
+ /**
166
+ * Tab stop definition
167
+ */
168
+ export interface TabStop {
169
+ /** Position in twips */
170
+ position: number;
171
+ /** Alignment type */
172
+ val?: TabAlignment;
173
+ /** Leader character */
174
+ leader?: TabLeader;
175
+ }
176
+
177
+ /**
178
+ * Paragraph formatting options
179
+ */
180
+ export interface ParagraphFormatting {
181
+ /** Text alignment */
182
+ alignment?: ParagraphAlignment;
183
+ /** Indentation in twips (1/20th of a point) */
184
+ indentation?: {
185
+ left?: number;
186
+ right?: number;
187
+ firstLine?: number;
188
+ hanging?: number;
189
+ };
190
+ /** Spacing in twips */
191
+ spacing?: {
192
+ before?: number;
193
+ after?: number;
194
+ line?: number;
195
+ lineRule?: "auto" | "exact" | "atLeast";
196
+ };
197
+ /** Keep with next paragraph */
198
+ keepNext?: boolean;
199
+ /** Keep lines together */
200
+ keepLines?: boolean;
201
+ /** Page break before */
202
+ pageBreakBefore?: boolean;
203
+ /** Paragraph style ID */
204
+ style?: string;
205
+ /** Numbering properties */
206
+ numbering?: {
207
+ numId: number;
208
+ level: number;
209
+ };
210
+ /** Contextual spacing - removes spacing between paragraphs of same style */
211
+ contextualSpacing?: boolean;
212
+ /** Paragraph ID (Word 2010+) - required by modern Word for change tracking */
213
+ paraId?: string;
214
+ /** Paragraph borders (top, bottom, left, right, between, bar) */
215
+ borders?: {
216
+ top?: BorderDefinition;
217
+ bottom?: BorderDefinition;
218
+ left?: BorderDefinition;
219
+ right?: BorderDefinition;
220
+ between?: BorderDefinition;
221
+ bar?: BorderDefinition;
222
+ };
223
+ /** Paragraph shading (background color and pattern) */
224
+ shading?: ShadingConfig;
225
+ /** Tab stops */
226
+ tabs?: TabStop[];
227
+ /** Widow/orphan control - prevents single lines at top/bottom of pages */
228
+ widowControl?: boolean;
229
+ /** Outline level (0-9) for table of contents hierarchy */
230
+ outlineLevel?: number;
231
+ /** Suppress line numbers for this paragraph */
232
+ suppressLineNumbers?: boolean;
233
+ /** Right-to-left paragraph layout (for Arabic, Hebrew, etc.) */
234
+ bidi?: boolean;
235
+ /** Text flow direction */
236
+ textDirection?: TextDirection;
237
+ /** Vertical text alignment */
238
+ textAlignment?: TextAlignment;
239
+ /** Use inside/outside indents instead of left/right (for double-sided printing) */
240
+ mirrorIndents?: boolean;
241
+ /** Auto-adjust right indent when document grid is defined */
242
+ adjustRightInd?: boolean;
243
+ /** Text frame/box properties (positioning, wrapping, drop cap) */
244
+ framePr?: FrameProperties;
245
+ /** Suppress automatic hyphenation for this paragraph */
246
+ suppressAutoHyphens?: boolean;
247
+ /** Kinsoku rules - CJK line-breaking rules per ECMA-376 Part 1 §17.3.1.16 */
248
+ kinsoku?: boolean;
249
+ /** Word wrap - allow CJK text to wrap mid-word per ECMA-376 Part 1 §17.3.1.45 */
250
+ wordWrap?: boolean;
251
+ /** Overflow punctuation - allow CJK punctuation to overhang margins per ECMA-376 Part 1 §17.3.1.24 */
252
+ overflowPunct?: boolean;
253
+ /** Top line punctuation - compress CJK punctuation at start of line per ECMA-376 Part 1 §17.3.1.43 */
254
+ topLinePunct?: boolean;
255
+ /** Auto space between East Asian and numeric text per ECMA-376 Part 1 §17.3.1.2 */
256
+ autoSpaceDE?: boolean;
257
+ /** Auto space between East Asian and Western text per ECMA-376 Part 1 §17.3.1.3 */
258
+ autoSpaceDN?: boolean;
259
+ /** Prevent text frames from overlapping */
260
+ suppressOverlap?: boolean;
261
+ /** Tight wrapping mode for text boxes */
262
+ textboxTightWrap?: TextboxTightWrap;
263
+ /** Associated HTML div ID (for HTML round-trip) */
264
+ divId?: number;
265
+ /** Conditional table style formatting (bitmask string, e.g., "101000000100") */
266
+ cnfStyle?: string;
267
+ /** Section properties at paragraph level (for section breaks) */
268
+ sectPr?: string | Record<string, unknown>;
269
+ /** Paragraph property change tracking (revision history) */
270
+ pPrChange?: ParagraphPropertiesChange;
271
+ /** Run properties for the paragraph mark (¶ symbol formatting) */
272
+ paragraphMarkRunProperties?: RunFormatting;
273
+ /** Paragraph mark deletion tracking (for deleted ¶ symbols) */
274
+ paragraphMarkDeletion?: {
275
+ /** Unique revision ID */
276
+ id: number;
277
+ /** Author who deleted the paragraph mark */
278
+ author: string;
279
+ /** Date when the paragraph mark was deleted */
280
+ date: Date;
281
+ };
282
+ /** True when the original XML had numId=0 (explicitly suppressed numbering) */
283
+ numberingSuppressed?: boolean;
284
+ }
285
+
286
+ /**
287
+ * Paragraph content (runs, fields, hyperlinks, revisions, range markers, shapes, text boxes)
288
+ */
289
+ export type ParagraphContent =
290
+ | Run
291
+ | FieldLike
292
+ | Hyperlink
293
+ | Revision
294
+ | RangeMarker
295
+ | Shape
296
+ | TextBox
297
+ | PreservedElement;
298
+
299
+ // ============================================================================
300
+ // TYPE GUARDS FOR ParagraphContent
301
+ // These functions help with type narrowing when working with paragraph content
302
+ // ============================================================================
303
+
304
+ /**
305
+ * Type guard: Check if content is a Run
306
+ * @param content - Paragraph content item to check
307
+ * @returns True if the content is a Run instance
308
+ */
309
+ export function isRun(content: ParagraphContent): content is Run {
310
+ return content instanceof Run;
311
+ }
312
+
313
+ /**
314
+ * Type guard: Check if content is a Field (simple or complex)
315
+ * @param content - Paragraph content item to check
316
+ * @returns True if the content is a Field or ComplexField instance
317
+ */
318
+ export function isField(content: ParagraphContent): content is FieldLike {
319
+ return content instanceof Field || content instanceof ComplexField;
320
+ }
321
+
322
+ /**
323
+ * Type guard: Check if content is a simple Field
324
+ * @param content - Paragraph content item to check
325
+ * @returns True if the content is a Field instance (not ComplexField)
326
+ */
327
+ export function isSimpleField(content: ParagraphContent): content is Field {
328
+ return content instanceof Field && !(content instanceof ComplexField);
329
+ }
330
+
331
+ /**
332
+ * Type guard: Check if content is a ComplexField
333
+ * @param content - Paragraph content item to check
334
+ * @returns True if the content is a ComplexField instance
335
+ */
336
+ export function isComplexField(content: ParagraphContent): content is ComplexField {
337
+ return content instanceof ComplexField;
338
+ }
339
+
340
+ /**
341
+ * Type guard: Check if content is a Hyperlink
342
+ * @param content - Paragraph content item to check
343
+ * @returns True if the content is a Hyperlink instance
344
+ */
345
+ export function isHyperlink(content: ParagraphContent): content is Hyperlink {
346
+ return content instanceof Hyperlink;
347
+ }
348
+
349
+ /**
350
+ * Type guard: Check if content is a Revision
351
+ * @param content - Paragraph content item to check
352
+ * @returns True if the content is a Revision instance
353
+ */
354
+ export function isRevision(content: ParagraphContent): content is Revision {
355
+ return content instanceof Revision;
356
+ }
357
+
358
+ /**
359
+ * Type guard: Check if content is a RangeMarker (bookmark start/end, comment start/end)
360
+ * @param content - Paragraph content item to check
361
+ * @returns True if the content is a RangeMarker instance
362
+ */
363
+ export function isRangeMarker(content: ParagraphContent): content is RangeMarker {
364
+ return content instanceof RangeMarker;
365
+ }
366
+
367
+ /**
368
+ * Type guard: Check if content is a Shape
369
+ * @param content - Paragraph content item to check
370
+ * @returns True if the content is a Shape instance
371
+ */
372
+ export function isShape(content: ParagraphContent): content is Shape {
373
+ return content instanceof Shape;
374
+ }
375
+
376
+ /**
377
+ * Type guard: Check if content is a TextBox
378
+ * @param content - Paragraph content item to check
379
+ * @returns True if the content is a TextBox instance
380
+ */
381
+ export function isTextBox(content: ParagraphContent): content is TextBox {
382
+ return content instanceof TextBox;
383
+ }
384
+
385
+ /**
386
+ * Represents a paragraph in a document
387
+ */
388
+ export class Paragraph {
389
+ private content: ParagraphContent[] = [];
390
+ public formatting: ParagraphFormatting;
391
+ private bookmarksStart: Bookmark[] = [];
392
+ private bookmarksEnd: Bookmark[] = [];
393
+ private commentsStart: Comment[] = [];
394
+ private commentsEnd: Comment[] = [];
395
+ /** Internal flag to mark paragraph as preserved from removal operations */
396
+ private _isPreserved = false;
397
+ /** Tracking context for automatic change tracking */
398
+ private trackingContext?: import('../tracking/TrackingContext').TrackingContext;
399
+ /** Parent table cell reference (if paragraph is inside a table cell) */
400
+ private _parentCell?: import('./TableCell').TableCell;
401
+ /** StylesManager reference for conditional formatting resolution */
402
+ private _stylesManager?: import("../formatting/StylesManager").StylesManager;
403
+ /**
404
+ * Internal flag to mark paragraph as part of a multi-paragraph field (e.g., TOC)
405
+ * When true, assembleComplexFields() will skip processing this paragraph
406
+ * to preserve the original field structure across paragraphs.
407
+ * @internal
408
+ */
409
+ _isPartOfMultiParagraphField?: boolean;
410
+
411
+ /**
412
+ * Creates a new Paragraph
413
+ * @param formatting - Paragraph formatting options
414
+ */
415
+ constructor(formatting: ParagraphFormatting = {}) {
416
+ this.formatting = formatting;
417
+ }
418
+
419
+ /**
420
+ * Sets the tracking context for automatic change tracking.
421
+ * Called by Document when track changes is enabled.
422
+ * @internal
423
+ */
424
+ _setTrackingContext(context: import('../tracking/TrackingContext').TrackingContext): void {
425
+ this.trackingContext = context;
426
+ }
427
+
428
+ /**
429
+ * Sets the parent cell reference for this paragraph.
430
+ * Called by TableCell when adding paragraphs.
431
+ * @internal
432
+ */
433
+ _setParentCell(cell: import('./TableCell').TableCell | undefined): void {
434
+ this._parentCell = cell;
435
+ }
436
+
437
+ /**
438
+ * Gets the parent cell reference for this paragraph.
439
+ * @internal
440
+ */
441
+ _getParentCell(): import('./TableCell').TableCell | undefined {
442
+ return this._parentCell;
443
+ }
444
+
445
+ /**
446
+ * Checks if this paragraph is inside a table cell.
447
+ * @returns True if paragraph has a parent cell
448
+ */
449
+ isInTableCell(): boolean {
450
+ return this._parentCell !== undefined;
451
+ }
452
+
453
+ /**
454
+ * Gets the table's cnfStyle (conditional formatting flags) for this paragraph.
455
+ * Returns the cell's cnfStyle if in a table, or the paragraph's own cnfStyle.
456
+ * @returns The cnfStyle string or undefined
457
+ */
458
+ getTableConditionalStyle(): string | undefined {
459
+ // Cell cnfStyle takes precedence
460
+ if (this._parentCell) {
461
+ const cellFormatting = this._parentCell.getFormatting();
462
+ if (cellFormatting.cnfStyle) {
463
+ return cellFormatting.cnfStyle;
464
+ }
465
+ }
466
+ // Fall back to paragraph's own cnfStyle
467
+ return this.formatting.cnfStyle;
468
+ }
469
+
470
+ /**
471
+ * Sets the StylesManager reference for conditional formatting resolution.
472
+ * Called by Document/Table when adding paragraphs.
473
+ * @internal
474
+ */
475
+ _setStylesManager(
476
+ manager: import("../formatting/StylesManager").StylesManager
477
+ ): void {
478
+ this._stylesManager = manager;
479
+ }
480
+
481
+ /**
482
+ * Gets the StylesManager reference for conditional formatting resolution.
483
+ * @internal
484
+ */
485
+ _getStylesManager():
486
+ | import("../formatting/StylesManager").StylesManager
487
+ | undefined {
488
+ return this._stylesManager;
489
+ }
490
+
491
+ /**
492
+ * Creates an empty detached paragraph
493
+ * @returns New Paragraph instance
494
+ * @example
495
+ * const para = Paragraph.create();
496
+ * para.addText('Added later');
497
+ */
498
+ static create(): Paragraph;
499
+
500
+ /**
501
+ * Creates a detached paragraph with formatting
502
+ * @param formatting - Paragraph formatting options
503
+ * @returns New Paragraph instance
504
+ * @example
505
+ * const para = Paragraph.create({ alignment: 'center' });
506
+ */
507
+ static create(formatting: ParagraphFormatting): Paragraph;
508
+
509
+ /**
510
+ * Creates a detached paragraph with text and optional formatting
511
+ * @param text - Text content
512
+ * @param formatting - Optional paragraph formatting
513
+ * @returns New Paragraph instance
514
+ * @example
515
+ * const para = Paragraph.create('Hello World', { alignment: 'center' });
516
+ */
517
+ static create(text: string, formatting?: ParagraphFormatting): Paragraph;
518
+
519
+ /**
520
+ * Creates a detached paragraph (not yet added to a document)
521
+ * @param textOrFormatting - Optional text content or paragraph formatting
522
+ * @param formatting - Optional paragraph formatting (only used if first param is text)
523
+ * @returns New Paragraph instance
524
+ * @example
525
+ * // Create with text and formatting
526
+ * const para1 = Paragraph.create('Hello World', { alignment: 'center' });
527
+ *
528
+ * // Create with just formatting
529
+ * const para2 = Paragraph.create({ alignment: 'right' });
530
+ *
531
+ * // Create empty
532
+ * const para3 = Paragraph.create();
533
+ *
534
+ * // Add to document later
535
+ * doc.addParagraph(para1);
536
+ */
537
+ static create(
538
+ textOrFormatting?: string | ParagraphFormatting,
539
+ formatting?: ParagraphFormatting
540
+ ): Paragraph {
541
+ // Handle overloaded parameters
542
+ if (typeof textOrFormatting === "string") {
543
+ // First param is text
544
+ const paragraph = new Paragraph(formatting);
545
+ paragraph.addText(textOrFormatting);
546
+ return paragraph;
547
+ } else {
548
+ // First param is formatting (or undefined)
549
+ return new Paragraph(textOrFormatting);
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Creates a detached paragraph with a specific style
555
+ * @param text - Text content
556
+ * @param styleId - Style ID (e.g., 'Heading1', 'Title')
557
+ * @returns New Paragraph instance
558
+ * @example
559
+ * const heading = Paragraph.createWithStyle('Chapter 1', 'Heading1');
560
+ * doc.addParagraph(heading);
561
+ */
562
+ static createWithStyle(text: string, styleId: string): Paragraph {
563
+ const paragraph = new Paragraph({ style: styleId });
564
+ paragraph.addText(text);
565
+ return paragraph;
566
+ }
567
+
568
+ /**
569
+ * Creates a detached empty paragraph
570
+ * Useful for adding blank lines or spacing
571
+ * @returns New empty Paragraph instance
572
+ * @example
573
+ * const blank = Paragraph.createEmpty();
574
+ * doc.addParagraph(blank);
575
+ */
576
+ static createEmpty(): Paragraph {
577
+ return new Paragraph();
578
+ }
579
+
580
+ /**
581
+ * Creates a detached paragraph with formatted text
582
+ * @param text - Text content
583
+ * @param runFormatting - Run formatting (bold, italic, etc.)
584
+ * @param paragraphFormatting - Paragraph formatting (alignment, spacing, etc.)
585
+ * @returns New Paragraph instance
586
+ * @example
587
+ * const para = Paragraph.createFormatted(
588
+ * 'Important Text',
589
+ * { bold: true, color: 'FF0000' },
590
+ * { alignment: 'center' }
591
+ * );
592
+ */
593
+ static createFormatted(
594
+ text: string,
595
+ runFormatting?: RunFormatting,
596
+ paragraphFormatting?: ParagraphFormatting
597
+ ): Paragraph {
598
+ const paragraph = new Paragraph(paragraphFormatting);
599
+ paragraph.addText(text, runFormatting);
600
+ return paragraph;
601
+ }
602
+
603
+ /**
604
+ * Adds a Run to the paragraph
605
+ *
606
+ * Appends a Run instance to the paragraph's content. Runs are sequences
607
+ * of text with uniform formatting.
608
+ *
609
+ * @param run - The Run instance to add
610
+ * @returns This paragraph instance for method chaining
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * const para = new Paragraph();
615
+ * const run = new Run('Bold text', { bold: true });
616
+ * para.addRun(run);
617
+ * ```
618
+ */
619
+ addRun(run: Run): this {
620
+ // Set parent reference for setText tracking
621
+ run._setParentParagraph(this);
622
+
623
+ if (this.trackingContext?.isEnabled()) {
624
+ // Wrap the run in an insert revision when tracking is enabled
625
+ const revision = Revision.createInsertion(
626
+ this.trackingContext.getAuthor(),
627
+ run,
628
+ new Date()
629
+ );
630
+ this.trackingContext.getRevisionManager().register(revision);
631
+ this.content.push(revision);
632
+ } else {
633
+ this.content.push(run);
634
+ }
635
+ return this;
636
+ }
637
+
638
+ /**
639
+ * Adds a field to the paragraph (supports both Field and ComplexField)
640
+ * @param field - Field or ComplexField to add
641
+ * @returns This paragraph for chaining
642
+ */
643
+ addField(field: FieldLike): this {
644
+ if (this.trackingContext?.isEnabled()) {
645
+ // Fields need special handling - wrap in revision if the field has runs
646
+ // For now, just add directly as complex fields have their own structure
647
+ this.content.push(field);
648
+ } else {
649
+ this.content.push(field);
650
+ }
651
+ return this;
652
+ }
653
+
654
+ /**
655
+ * Adds a complex field to the paragraph
656
+ * @param field - ComplexField to add
657
+ * @returns This paragraph for chaining
658
+ */
659
+ addComplexField(field: ComplexField): this {
660
+ this.content.push(field);
661
+ return this;
662
+ }
663
+
664
+ /**
665
+ * Adds a hyperlink to the paragraph
666
+ * @param urlOrHyperlink - URL string for new hyperlink, or existing Hyperlink object
667
+ * @returns Hyperlink object for fluent chaining (when creating new), or this paragraph (when adding existing)
668
+ *
669
+ * @example
670
+ * // Fluent API (new signature)
671
+ * const link = para.addHyperlink('https://example.com');
672
+ * link.setText('Visit Example');
673
+ *
674
+ * // Or use without URL
675
+ * const link2 = para.addHyperlink();
676
+ * link2.setUrl('https://example.com').setText('Link');
677
+ *
678
+ * // Legacy API (still supported)
679
+ * const hyperlink = new Hyperlink({ url: 'https://example.com', text: 'Link' });
680
+ * para.addHyperlink(hyperlink);
681
+ */
682
+ /**
683
+ * Adds a hyperlink to the paragraph
684
+ * @param url - Optional URL for the hyperlink
685
+ * @returns Hyperlink object for fluent chaining
686
+ */
687
+ addHyperlink(url?: string): Hyperlink;
688
+ /**
689
+ * Adds an existing hyperlink to the paragraph
690
+ * @param hyperlink - Existing Hyperlink object
691
+ * @returns This paragraph for chaining
692
+ */
693
+ addHyperlink(hyperlink: Hyperlink): this;
694
+ addHyperlink(urlOrHyperlink?: string | Hyperlink): Hyperlink | this {
695
+ if (typeof urlOrHyperlink === 'string') {
696
+ // New fluent API: create hyperlink from URL
697
+ const hyperlink = new Hyperlink({ url: urlOrHyperlink, text: urlOrHyperlink });
698
+ hyperlink._setParentParagraph(this);
699
+ if (this.trackingContext?.isEnabled()) {
700
+ const revision = Revision.createInsertion(
701
+ this.trackingContext.getAuthor(),
702
+ hyperlink,
703
+ new Date()
704
+ );
705
+ this.trackingContext.getRevisionManager().register(revision);
706
+ this.content.push(revision);
707
+ } else {
708
+ this.content.push(hyperlink);
709
+ }
710
+ return hyperlink;
711
+ } else if (urlOrHyperlink instanceof Hyperlink) {
712
+ // Legacy API: add existing hyperlink
713
+ urlOrHyperlink._setParentParagraph(this);
714
+ if (this.trackingContext?.isEnabled()) {
715
+ const revision = Revision.createInsertion(
716
+ this.trackingContext.getAuthor(),
717
+ urlOrHyperlink,
718
+ new Date()
719
+ );
720
+ this.trackingContext.getRevisionManager().register(revision);
721
+ this.content.push(revision);
722
+ } else {
723
+ this.content.push(urlOrHyperlink);
724
+ }
725
+ return this;
726
+ } else {
727
+ // No argument: create empty hyperlink for fluent building
728
+ const hyperlink = new Hyperlink({ text: 'Link' });
729
+ hyperlink._setParentParagraph(this);
730
+ if (this.trackingContext?.isEnabled()) {
731
+ const revision = Revision.createInsertion(
732
+ this.trackingContext.getAuthor(),
733
+ hyperlink,
734
+ new Date()
735
+ );
736
+ this.trackingContext.getRevisionManager().register(revision);
737
+ this.content.push(revision);
738
+ } else {
739
+ this.content.push(hyperlink);
740
+ }
741
+ return hyperlink;
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Adds a revision (tracked change) to the paragraph
747
+ * @param revision - Revision to add
748
+ * @returns This paragraph for chaining
749
+ */
750
+ addRevision(revision: Revision): this {
751
+ this.content.push(revision);
752
+ return this;
753
+ }
754
+
755
+ /**
756
+ * Adds a range marker to the paragraph
757
+ * Range markers mark the boundaries of moved, inserted, or deleted content
758
+ * @param rangeMarker - Range marker to add
759
+ * @returns This paragraph for chaining
760
+ */
761
+ addRangeMarker(rangeMarker: RangeMarker): this {
762
+ this.content.push(rangeMarker);
763
+ return this;
764
+ }
765
+
766
+ /**
767
+ * Adds a shape to the paragraph
768
+ * @param shape - Shape to add
769
+ * @returns This paragraph for chaining
770
+ * @example
771
+ * const rect = Shape.createRectangle(inchesToEmus(2), inchesToEmus(1));
772
+ * paragraph.addShape(rect);
773
+ */
774
+ addShape(shape: Shape): this {
775
+ this.content.push(shape);
776
+ return this;
777
+ }
778
+
779
+ /**
780
+ * Adds a text box to the paragraph
781
+ * @param textbox - TextBox to add
782
+ * @returns This paragraph for chaining
783
+ * @example
784
+ * const textbox = TextBox.create(inchesToEmus(3), inchesToEmus(2));
785
+ * paragraph.addTextBox(textbox);
786
+ */
787
+ addTextBox(textbox: TextBox): this {
788
+ this.content.push(textbox);
789
+ return this;
790
+ }
791
+
792
+ /**
793
+ * Adds any ParagraphContent item to the paragraph
794
+ * Used for preserved elements (proofErr, permStart/End, etc.)
795
+ * @param item - Content item to add
796
+ * @returns This paragraph for chaining
797
+ */
798
+ addContent(item: ParagraphContent): this {
799
+ this.content.push(item);
800
+ return this;
801
+ }
802
+
803
+ /**
804
+ * Adds a bookmark start marker at the beginning of this paragraph
805
+ * @param bookmark - Bookmark to add
806
+ * @returns This paragraph for chaining
807
+ */
808
+ addBookmarkStart(bookmark: Bookmark): this {
809
+ this.bookmarksStart.push(bookmark);
810
+ return this;
811
+ }
812
+
813
+ /**
814
+ * Adds a bookmark end marker at the end of this paragraph
815
+ * @param bookmark - Bookmark to add (must have matching start marker)
816
+ * @returns This paragraph for chaining
817
+ */
818
+ addBookmarkEnd(bookmark: Bookmark): this {
819
+ this.bookmarksEnd.push(bookmark);
820
+ return this;
821
+ }
822
+
823
+ /**
824
+ * Adds both start and end bookmark markers (wraps entire paragraph)
825
+ * @param bookmark - Bookmark to add
826
+ * @returns This paragraph for chaining
827
+ */
828
+ addBookmark(bookmark: Bookmark): this {
829
+ this.addBookmarkStart(bookmark);
830
+ this.addBookmarkEnd(bookmark);
831
+ return this;
832
+ }
833
+
834
+ /**
835
+ * Gets all bookmarks that start in this paragraph
836
+ * @returns Array of bookmarks
837
+ */
838
+ getBookmarksStart(): Bookmark[] {
839
+ return [...this.bookmarksStart];
840
+ }
841
+
842
+ /**
843
+ * Gets all bookmarks that end in this paragraph
844
+ * @returns Array of bookmarks
845
+ */
846
+ getBookmarksEnd(): Bookmark[] {
847
+ return [...this.bookmarksEnd];
848
+ }
849
+
850
+ /**
851
+ * Removes a bookmark end marker by its ID
852
+ * @param id - The bookmark ID to remove
853
+ * @returns true if a bookmark was removed, false if not found
854
+ */
855
+ removeBookmarkEnd(id: number): boolean {
856
+ const index = this.bookmarksEnd.findIndex(bm => bm.getId() === id);
857
+ if (index !== -1) {
858
+ this.bookmarksEnd.splice(index, 1);
859
+ return true;
860
+ }
861
+ return false;
862
+ }
863
+
864
+ /**
865
+ * Adds a comment range start marker at the beginning of this paragraph
866
+ * @param comment - Comment to start
867
+ * @returns This paragraph for chaining
868
+ */
869
+ addCommentStart(comment: Comment): this {
870
+ this.commentsStart.push(comment);
871
+ return this;
872
+ }
873
+
874
+ /**
875
+ * Adds a comment range end marker at the end of this paragraph
876
+ * @param comment - Comment to end (must have matching start marker)
877
+ * @returns This paragraph for chaining
878
+ */
879
+ addCommentEnd(comment: Comment): this {
880
+ this.commentsEnd.push(comment);
881
+ return this;
882
+ }
883
+
884
+ /**
885
+ * Adds both start and end comment range markers (comments entire paragraph)
886
+ * @param comment - Comment to add
887
+ * @returns This paragraph for chaining
888
+ */
889
+ addComment(comment: Comment): this {
890
+ this.addCommentStart(comment);
891
+ this.addCommentEnd(comment);
892
+ return this;
893
+ }
894
+
895
+ /**
896
+ * Gets all comments that start in this paragraph
897
+ * @returns Array of comments
898
+ */
899
+ getCommentsStart(): Comment[] {
900
+ return [...this.commentsStart];
901
+ }
902
+
903
+ /**
904
+ * Gets all comments that end in this paragraph
905
+ * @returns Array of comments
906
+ */
907
+ getCommentsEnd(): Comment[] {
908
+ return [...this.commentsEnd];
909
+ }
910
+
911
+ /**
912
+ * Adds a page number field
913
+ * @param formatting - Optional run formatting for the page number
914
+ * @returns This paragraph for chaining
915
+ * @example
916
+ * paragraph.addPageNumber();
917
+ * paragraph.addPageNumber({ bold: true, size: 12 });
918
+ */
919
+ addPageNumber(formatting?: RunFormatting): this {
920
+ return this.addField(Field.createPageNumber(formatting));
921
+ }
922
+
923
+ /**
924
+ * Adds a total pages field (NUMPAGES)
925
+ * @param formatting - Optional run formatting
926
+ * @returns This paragraph for chaining
927
+ * @example
928
+ * paragraph.addTotalPages();
929
+ */
930
+ addTotalPages(formatting?: RunFormatting): this {
931
+ return this.addField(Field.createTotalPages(formatting));
932
+ }
933
+
934
+ /**
935
+ * Adds a date field
936
+ * @param format - Date format (e.g., 'MMMM d, yyyy', 'M/d/yyyy')
937
+ * @param formatting - Optional run formatting
938
+ * @returns This paragraph for chaining
939
+ * @example
940
+ * paragraph.addDate();
941
+ * paragraph.addDate('MMMM d, yyyy');
942
+ * paragraph.addDate('M/d/yyyy', { italic: true });
943
+ */
944
+ addDate(format?: string, formatting?: RunFormatting): this {
945
+ return this.addField(Field.createDate(format, formatting));
946
+ }
947
+
948
+ /**
949
+ * Adds a time field
950
+ * @param format - Time format
951
+ * @param formatting - Optional run formatting
952
+ * @returns This paragraph for chaining
953
+ * @example
954
+ * paragraph.addTime();
955
+ * paragraph.addTime('h:mm:ss tt');
956
+ */
957
+ addTime(format?: string, formatting?: RunFormatting): this {
958
+ return this.addField(Field.createTime(format, formatting));
959
+ }
960
+
961
+ /**
962
+ * Adds a filename field
963
+ * @param includePath - Whether to include full path
964
+ * @param formatting - Optional run formatting
965
+ * @returns This paragraph for chaining
966
+ * @example
967
+ * paragraph.addFilename();
968
+ * paragraph.addFilename(true); // Includes full path
969
+ */
970
+ addFilename(includePath = false, formatting?: RunFormatting): this {
971
+ return this.addField(Field.createFilename(includePath, formatting));
972
+ }
973
+
974
+ /**
975
+ * Adds an author field
976
+ * @param formatting - Optional run formatting
977
+ * @returns This paragraph for chaining
978
+ * @example
979
+ * paragraph.addAuthor();
980
+ */
981
+ addAuthor(formatting?: RunFormatting): this {
982
+ return this.addField(Field.createAuthor(formatting));
983
+ }
984
+
985
+ /**
986
+ * Adds a title field
987
+ * @param formatting - Optional run formatting
988
+ * @returns This paragraph for chaining
989
+ * @example
990
+ * paragraph.addTitle();
991
+ */
992
+ addTitle(formatting?: RunFormatting): this {
993
+ return this.addField(Field.createTitle(formatting));
994
+ }
995
+
996
+ /**
997
+ * Adds text to the paragraph with optional formatting
998
+ *
999
+ * Creates a new Run with the specified text and formatting, then adds it
1000
+ * to the paragraph. This is a convenience method combining Run creation
1001
+ * and addition in one call.
1002
+ *
1003
+ * @param text - The text content to add
1004
+ * @param formatting - Optional formatting to apply to the text
1005
+ * @returns This paragraph instance for method chaining
1006
+ *
1007
+ * @example
1008
+ * ```typescript
1009
+ * const para = new Paragraph();
1010
+ * para.addText('Normal text');
1011
+ * para.addText('Bold text', { bold: true });
1012
+ * para.addText('Red italic', { italic: true, color: 'FF0000' });
1013
+ * ```
1014
+ */
1015
+ addText(text: string, formatting?: RunFormatting): this {
1016
+ const run = new Run(text, formatting);
1017
+ // Set parent reference for setText tracking
1018
+ run._setParentParagraph(this);
1019
+
1020
+ if (this.trackingContext?.isEnabled()) {
1021
+ // Wrap the run in an insert revision when tracking is enabled
1022
+ const revision = Revision.createInsertion(
1023
+ this.trackingContext.getAuthor(),
1024
+ run,
1025
+ new Date()
1026
+ );
1027
+ this.trackingContext.getRevisionManager().register(revision);
1028
+ this.content.push(revision);
1029
+ } else {
1030
+ this.content.push(run);
1031
+ }
1032
+ return this;
1033
+ }
1034
+
1035
+ /**
1036
+ * Sets the paragraph text content (replaces all existing content)
1037
+ *
1038
+ * Clears all existing runs, fields, hyperlinks, and other content,
1039
+ * then adds a single Run with the specified text and formatting.
1040
+ *
1041
+ * @param text - The new text content
1042
+ * @param formatting - Optional formatting to apply to the text
1043
+ * @returns This paragraph instance for method chaining
1044
+ *
1045
+ * @example
1046
+ * ```typescript
1047
+ * const para = new Paragraph();
1048
+ * para.addText('First text');
1049
+ * para.addText('More text');
1050
+ * para.setText('Replace all', { bold: true }); // Replaces everything
1051
+ * ```
1052
+ */
1053
+ setText(text: string, formatting?: RunFormatting): this {
1054
+ this.content = [new Run(text, formatting)];
1055
+ return this;
1056
+ }
1057
+
1058
+ /**
1059
+ * Gets all Run instances in the paragraph
1060
+ *
1061
+ * Returns only Run objects, excluding other content types like fields,
1062
+ * hyperlinks, revisions, and range markers.
1063
+ *
1064
+ * @returns Array of Run instances
1065
+ *
1066
+ * @example
1067
+ * ```typescript
1068
+ * const runs = para.getRuns();
1069
+ * console.log(`Paragraph has ${runs.length} text runs`);
1070
+ *
1071
+ * // Apply formatting to all runs
1072
+ * for (const run of runs) {
1073
+ * run.setBold(true);
1074
+ * }
1075
+ * ```
1076
+ */
1077
+ getRuns(): Run[] {
1078
+ const runs: Run[] = [];
1079
+ for (const item of this.content) {
1080
+ if (item instanceof Run) {
1081
+ runs.push(item);
1082
+ } else if (item instanceof Revision) {
1083
+ // Extract runs from inside revisions (track changes)
1084
+ runs.push(...item.getRuns());
1085
+ } else if (item instanceof Hyperlink) {
1086
+ // Extract run from inside hyperlink
1087
+ runs.push(item.getRun());
1088
+ }
1089
+ }
1090
+ return runs;
1091
+ }
1092
+
1093
+ /**
1094
+ * Gets all Revision instances in the paragraph
1095
+ *
1096
+ * Returns only Revision objects (tracked changes), excluding other content types.
1097
+ *
1098
+ * @returns Array of Revision instances
1099
+ *
1100
+ * @example
1101
+ * ```typescript
1102
+ * const revisions = para.getRevisions();
1103
+ * console.log(`Paragraph has ${revisions.length} tracked changes`);
1104
+ *
1105
+ * // Check each revision
1106
+ * for (const rev of revisions) {
1107
+ * console.log(`${rev.getType()} by ${rev.getAuthor()}`);
1108
+ * }
1109
+ * ```
1110
+ */
1111
+ getRevisions(): Revision[] {
1112
+ return this.content.filter((item): item is Revision => item instanceof Revision);
1113
+ }
1114
+
1115
+ /**
1116
+ * Gets all content in the paragraph
1117
+ *
1118
+ * Returns all content items including runs, fields, hyperlinks,
1119
+ * revisions, range markers, shapes, and text boxes.
1120
+ *
1121
+ * @returns Array of all content items in order
1122
+ *
1123
+ * @example
1124
+ * ```typescript
1125
+ * const content = para.getContent();
1126
+ * for (const item of content) {
1127
+ * if (item instanceof Run) {
1128
+ * console.log('Text:', item.getText());
1129
+ * } else if (item instanceof Hyperlink) {
1130
+ * console.log('Link:', item.getUrl());
1131
+ * }
1132
+ * }
1133
+ * ```
1134
+ */
1135
+ getContent(): ParagraphContent[] {
1136
+ return [...this.content];
1137
+ }
1138
+
1139
+ /**
1140
+ * Clears all content from the paragraph
1141
+ *
1142
+ * Removes all runs, hyperlinks, fields, revisions, and other content items.
1143
+ * The paragraph formatting is preserved.
1144
+ *
1145
+ * @returns This paragraph instance for method chaining
1146
+ *
1147
+ * @example
1148
+ * ```typescript
1149
+ * para.clearContent();
1150
+ * para.addText('Fresh start');
1151
+ * ```
1152
+ */
1153
+ clearContent(): this {
1154
+ if (this.trackingContext?.isEnabled() && this.content.length > 0) {
1155
+ // Wrap all existing content in delete revisions instead of removing
1156
+ const deletedContent: ParagraphContent[] = [];
1157
+ for (const item of this.content) {
1158
+ // Skip items that are already revisions (don't double-wrap)
1159
+ if (item instanceof Revision) {
1160
+ deletedContent.push(item);
1161
+ } else if (item instanceof Run || item instanceof Hyperlink) {
1162
+ const revision = Revision.createDeletion(
1163
+ this.trackingContext.getAuthor(),
1164
+ item,
1165
+ new Date()
1166
+ );
1167
+ this.trackingContext.getRevisionManager().register(revision);
1168
+ deletedContent.push(revision);
1169
+ } else {
1170
+ // For other content types (fields, etc.), just keep them
1171
+ // as they may have complex structures
1172
+ deletedContent.push(item);
1173
+ }
1174
+ }
1175
+ this.content = deletedContent;
1176
+ } else {
1177
+ this.content = [];
1178
+ }
1179
+ return this;
1180
+ }
1181
+
1182
+ /**
1183
+ * Replaces a content item with one or more new items
1184
+ *
1185
+ * This is useful for tracked changes where a hyperlink needs to be replaced
1186
+ * with a deletion revision (containing old hyperlink) and insertion revision
1187
+ * (containing new hyperlink).
1188
+ *
1189
+ * @param oldItem - The item to replace (must be the exact same object reference)
1190
+ * @param newItems - The new items to insert in place of the old item
1191
+ * @returns true if the item was found and replaced, false if not found
1192
+ *
1193
+ * @example
1194
+ * ```typescript
1195
+ * // Replace a hyperlink with tracked changes
1196
+ * const deletion = Revision.createDeletion(author, [oldHyperlink]);
1197
+ * const insertion = Revision.createInsertion(author, [newHyperlink]);
1198
+ * para.replaceContent(hyperlink, [deletion, insertion]);
1199
+ * ```
1200
+ */
1201
+ replaceContent(oldItem: ParagraphContent, newItems: ParagraphContent[]): boolean {
1202
+ const index = this.content.indexOf(oldItem);
1203
+ if (index === -1) {
1204
+ return false;
1205
+ }
1206
+
1207
+ // Replace the item with the new items
1208
+ this.content.splice(index, 1, ...newItems);
1209
+ return true;
1210
+ }
1211
+
1212
+ /**
1213
+ * Sets all content of the paragraph, replacing existing content
1214
+ *
1215
+ * Used for bulk content operations like regrouping field runs
1216
+ * into ComplexField objects during parsing.
1217
+ *
1218
+ * @param content - Array of content items to set
1219
+ *
1220
+ * @example
1221
+ * ```typescript
1222
+ * // Replace all content with grouped items
1223
+ * para.setContent([run1, complexField, run2]);
1224
+ * ```
1225
+ */
1226
+ setContent(content: ParagraphContent[]): void {
1227
+ // If tracking is enabled and we have existing content, create delete revisions
1228
+ if (this.trackingContext?.isEnabled() && this.content.length > 0) {
1229
+ const deletedContent: ParagraphContent[] = [];
1230
+ for (const item of this.content) {
1231
+ if (item instanceof Revision) {
1232
+ deletedContent.push(item);
1233
+ } else if (item instanceof Run || item instanceof Hyperlink) {
1234
+ const revision = Revision.createDeletion(
1235
+ this.trackingContext.getAuthor(),
1236
+ item,
1237
+ new Date()
1238
+ );
1239
+ this.trackingContext.getRevisionManager().register(revision);
1240
+ deletedContent.push(revision);
1241
+ }
1242
+ }
1243
+ // New content array starts with deleted items followed by new items
1244
+ // But actually, for setContent we probably want to just replace
1245
+ // Keep the deletion revisions and add new content
1246
+ this.content = [...deletedContent, ...content];
1247
+ } else {
1248
+ this.content = [...content];
1249
+ }
1250
+ // Set parent reference for runs and hyperlinks in the content
1251
+ for (const item of this.content) {
1252
+ if (item instanceof Hyperlink) {
1253
+ item._setParentParagraph(this);
1254
+ } else if (item instanceof Run) {
1255
+ item._setParentParagraph(this);
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ /**
1261
+ * Removes a content item at the specified index
1262
+ *
1263
+ * When track changes is enabled, wraps the removed item in a delete revision
1264
+ * instead of actually removing it.
1265
+ *
1266
+ * @param index - Index of the item to remove
1267
+ * @returns The removed item, or undefined if index is out of bounds
1268
+ *
1269
+ * @example
1270
+ * ```typescript
1271
+ * const para = new Paragraph();
1272
+ * para.addText('First');
1273
+ * para.addText('Second');
1274
+ * para.removeContentAt(0); // Removes 'First'
1275
+ * ```
1276
+ */
1277
+ removeContentAt(index: number): ParagraphContent | undefined {
1278
+ if (index < 0 || index >= this.content.length) {
1279
+ return undefined;
1280
+ }
1281
+
1282
+ const item = this.content[index];
1283
+
1284
+ if (this.trackingContext?.isEnabled()) {
1285
+ // Wrap the item in a delete revision instead of removing
1286
+ if (item instanceof Run || item instanceof Hyperlink) {
1287
+ const revision = Revision.createDeletion(
1288
+ this.trackingContext.getAuthor(),
1289
+ item,
1290
+ new Date()
1291
+ );
1292
+ this.trackingContext.getRevisionManager().register(revision);
1293
+ this.content[index] = revision;
1294
+ }
1295
+ // For revisions and other types, leave them as-is
1296
+ } else {
1297
+ this.content.splice(index, 1);
1298
+ }
1299
+
1300
+ return item;
1301
+ }
1302
+
1303
+ /**
1304
+ * Gets the combined text content of the paragraph
1305
+ *
1306
+ * Concatenates text from all Runs and Hyperlinks in the paragraph,
1307
+ * excluding other content types like fields or revisions.
1308
+ *
1309
+ * @returns Combined text string from all text-bearing elements
1310
+ *
1311
+ * @example
1312
+ * ```typescript
1313
+ * const para = new Paragraph();
1314
+ * para.addText('Hello ');
1315
+ * para.addText('World');
1316
+ * console.log(para.getText()); // "Hello World"
1317
+ * ```
1318
+ */
1319
+ getText(): string {
1320
+ return this.content
1321
+ .filter(
1322
+ (item): item is Run | Hyperlink =>
1323
+ item instanceof Run || item instanceof Hyperlink
1324
+ )
1325
+ .map((item) => item.getText())
1326
+ .join("");
1327
+ }
1328
+
1329
+ /**
1330
+ * Gets all fields in the paragraph (both Field and ComplexField)
1331
+ *
1332
+ * Returns all field instances including simple fields (`<w:fldSimple>`)
1333
+ * and complex fields (begin/separate/end structure).
1334
+ *
1335
+ * @returns Array of fields
1336
+ *
1337
+ * @example
1338
+ * ```typescript
1339
+ * const fields = para.getFields();
1340
+ * console.log(`P aragraph has ${fields.length} fields`);
1341
+ * for (const field of fields) {
1342
+ * console.log(`Instruction: ${field.getInstruction()}`);
1343
+ * }
1344
+ * ```
1345
+ */
1346
+ getFields(): FieldLike[] {
1347
+ return this.content.filter(
1348
+ (item): item is FieldLike =>
1349
+ item instanceof Field || item instanceof ComplexField
1350
+ );
1351
+ }
1352
+
1353
+ /**
1354
+ * Finds fields matching an instruction pattern
1355
+ *
1356
+ * Searches all fields and returns those whose instruction matches
1357
+ * the specified pattern (string or regex).
1358
+ *
1359
+ * @param pattern - Regex pattern or string to match against instruction
1360
+ * @returns Array of matching fields
1361
+ *
1362
+ * @example
1363
+ * ```typescript
1364
+ * // Find all PAGE fields
1365
+ * const pageFields = para.findFieldsByInstruction('PAGE');
1366
+ *
1367
+ * // Find all TOC fields
1368
+ * const tocFields = para.findFieldsByInstruction(/^TOC/i);
1369
+ *
1370
+ * // Find fields with specific switches
1371
+ * const hyperlinkedFields = para.findFieldsByInstruction(/\\h/);
1372
+ * ```
1373
+ */
1374
+ findFieldsByInstruction(pattern: string | RegExp): FieldLike[] {
1375
+ const regex =
1376
+ typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
1377
+
1378
+ return this.getFields().filter((field) => {
1379
+ const instruction = field.getInstruction();
1380
+ return regex.test(instruction);
1381
+ });
1382
+ }
1383
+
1384
+ /**
1385
+ * Removes all fields from the paragraph
1386
+ *
1387
+ * Filters out all Field and ComplexField instances, converting them
1388
+ * to plain text if they have result text.
1389
+ *
1390
+ * @returns Count of fields removed
1391
+ *
1392
+ * @example
1393
+ * ```typescript
1394
+ * const count = para.removeAllFields();
1395
+ * console.log(`Removed ${count} fields`);
1396
+ * ```
1397
+ */
1398
+ removeAllFields(): number {
1399
+ const originalLength = this.content.length;
1400
+ this.content = this.content.filter(
1401
+ (item) => !(item instanceof Field || item instanceof ComplexField)
1402
+ );
1403
+ return originalLength - this.content.length;
1404
+ }
1405
+
1406
+ /**
1407
+ * Replaces a field with another field or text
1408
+ *
1409
+ * Swaps out an existing field with a replacement. If replacement is a string,
1410
+ * converts it to a Run.
1411
+ *
1412
+ * @param oldField - Field to replace
1413
+ * @param replacement - New field or text to insert
1414
+ * @returns True if replacement successful, false if field not found
1415
+ *
1416
+ * @example
1417
+ * ```typescript
1418
+ * const pageField = para.getFields()[0];
1419
+ * if (pageField) {
1420
+ * // Replace with text
1421
+ * para.replaceField(pageField, 'Page 1');
1422
+ *
1423
+ * // Or replace with another field
1424
+ * para.replaceField(pageField, Field.createDate());
1425
+ * }
1426
+ * ```
1427
+ */
1428
+ replaceField(oldField: FieldLike, replacement: FieldLike | string): boolean {
1429
+ const index = this.content.indexOf(oldField);
1430
+ if (index === -1) return false;
1431
+
1432
+ if (typeof replacement === "string") {
1433
+ this.content[index] = new Run(replacement);
1434
+ } else {
1435
+ this.content[index] = replacement;
1436
+ }
1437
+ return true;
1438
+ }
1439
+
1440
+ /**
1441
+ * Gets a copy of the paragraph formatting
1442
+ *
1443
+ * Returns a copy of all formatting properties including alignment,
1444
+ * indentation, spacing, style, numbering, borders, shading, etc.
1445
+ *
1446
+ * @returns Copy of the paragraph formatting object
1447
+ *
1448
+ * @example
1449
+ * ```typescript
1450
+ * const formatting = para.getFormatting();
1451
+ * console.log(`Style: ${formatting.style}`);
1452
+ * console.log(`Alignment: ${formatting.alignment}`);
1453
+ * if (formatting.spacing) {
1454
+ * console.log(`Spacing before: ${formatting.spacing.before} twips`);
1455
+ * }
1456
+ * ```
1457
+ */
1458
+ getFormatting(): ParagraphFormatting {
1459
+ return { ...this.formatting };
1460
+ }
1461
+
1462
+ // ============================================================================
1463
+ // Individual Formatting Getters
1464
+ // ============================================================================
1465
+
1466
+ /**
1467
+ * Gets the left indentation in twips
1468
+ * @returns Left indent in twips or undefined if not set
1469
+ */
1470
+ getLeftIndent(): number | undefined {
1471
+ return this.formatting.indentation?.left;
1472
+ }
1473
+
1474
+ /**
1475
+ * Gets the right indentation in twips
1476
+ * @returns Right indent in twips or undefined if not set
1477
+ */
1478
+ getRightIndent(): number | undefined {
1479
+ return this.formatting.indentation?.right;
1480
+ }
1481
+
1482
+ /**
1483
+ * Gets the first line indentation in twips
1484
+ * @returns First line indent in twips or undefined if not set
1485
+ */
1486
+ getFirstLineIndent(): number | undefined {
1487
+ return this.formatting.indentation?.firstLine;
1488
+ }
1489
+
1490
+ /**
1491
+ * Gets the hanging indentation in twips
1492
+ * @returns Hanging indent in twips or undefined if not set
1493
+ */
1494
+ getHangingIndent(): number | undefined {
1495
+ return this.formatting.indentation?.hanging;
1496
+ }
1497
+
1498
+ /**
1499
+ * Gets the spacing before the paragraph in twips
1500
+ * @returns Space before in twips or undefined if not set
1501
+ */
1502
+ getSpaceBefore(): number | undefined {
1503
+ return this.formatting.spacing?.before;
1504
+ }
1505
+
1506
+ /**
1507
+ * Gets the spacing after the paragraph in twips
1508
+ * @returns Space after in twips or undefined if not set
1509
+ */
1510
+ getSpaceAfter(): number | undefined {
1511
+ return this.formatting.spacing?.after;
1512
+ }
1513
+
1514
+ /**
1515
+ * Gets the line spacing value
1516
+ * @returns Line spacing value or undefined if not set
1517
+ */
1518
+ getLineSpacing(): number | undefined {
1519
+ return this.formatting.spacing?.line;
1520
+ }
1521
+
1522
+ /**
1523
+ * Gets the paragraph alignment
1524
+ * @returns Alignment ('left', 'center', 'right', 'justify') or undefined
1525
+ */
1526
+ getAlignment(): string | undefined {
1527
+ return this.formatting.alignment;
1528
+ }
1529
+
1530
+ /**
1531
+ * Gets the keepNext property (keep with next paragraph)
1532
+ * @returns True if keepNext is set
1533
+ */
1534
+ getKeepNext(): boolean {
1535
+ return this.formatting.keepNext ?? false;
1536
+ }
1537
+
1538
+ /**
1539
+ * Gets the keepLines property (keep lines together)
1540
+ * @returns True if keepLines is set
1541
+ */
1542
+ getKeepLines(): boolean {
1543
+ return this.formatting.keepLines ?? false;
1544
+ }
1545
+
1546
+ /**
1547
+ * Gets the pageBreakBefore property
1548
+ * @returns True if page break before is set
1549
+ */
1550
+ getPageBreakBefore(): boolean {
1551
+ return this.formatting.pageBreakBefore ?? false;
1552
+ }
1553
+
1554
+ /**
1555
+ * Gets the outline level for TOC headings
1556
+ * @returns Outline level (0-8) or undefined if not set
1557
+ */
1558
+ getOutlineLevel(): number | undefined {
1559
+ return this.formatting.outlineLevel;
1560
+ }
1561
+
1562
+ /**
1563
+ * Gets the text direction
1564
+ * @returns Text direction or undefined if not set
1565
+ */
1566
+ getTextDirection(): string | undefined {
1567
+ return this.formatting.textDirection;
1568
+ }
1569
+
1570
+ /**
1571
+ * Gets the widow/orphan control setting
1572
+ * @returns True if widow control is enabled
1573
+ */
1574
+ getWidowControl(): boolean {
1575
+ return this.formatting.widowControl ?? true; // Word defaults to true
1576
+ }
1577
+
1578
+ /**
1579
+ * Gets the contextual spacing setting
1580
+ * @returns True if contextual spacing is enabled
1581
+ */
1582
+ getContextualSpacing(): boolean {
1583
+ return this.formatting.contextualSpacing ?? false;
1584
+ }
1585
+
1586
+ // ============================================================================
1587
+ // Checker Methods (has*, is*, isEmpty)
1588
+ // ============================================================================
1589
+
1590
+ /**
1591
+ * Checks if the paragraph has list numbering applied
1592
+ * @returns True if paragraph is part of a list
1593
+ */
1594
+ hasNumbering(): boolean {
1595
+ return this.formatting.numbering?.numId !== undefined &&
1596
+ this.formatting.numbering.numId !== 0;
1597
+ }
1598
+
1599
+ /**
1600
+ * Checks if numbering was explicitly suppressed in the original XML (numId=0).
1601
+ * This is different from having no numbering — it means the document author
1602
+ * intentionally overrode style-inherited numbering.
1603
+ * @returns True if numbering is explicitly suppressed
1604
+ */
1605
+ isNumberingSuppressed(): boolean {
1606
+ return this.formatting.numberingSuppressed === true;
1607
+ }
1608
+
1609
+ /**
1610
+ * Checks if the paragraph contains any fields
1611
+ * @returns True if paragraph has fields
1612
+ */
1613
+ hasFields(): boolean {
1614
+ return this.getFields().length > 0;
1615
+ }
1616
+
1617
+ /**
1618
+ * Checks if the paragraph has any bookmark start markers
1619
+ * @returns True if paragraph has bookmarks
1620
+ */
1621
+ hasBookmarks(): boolean {
1622
+ return this.getBookmarksStart().length > 0;
1623
+ }
1624
+
1625
+ /**
1626
+ * Checks if the paragraph has any comment start markers
1627
+ * @returns True if paragraph has comments
1628
+ */
1629
+ hasComments(): boolean {
1630
+ return this.getCommentsStart().length > 0;
1631
+ }
1632
+
1633
+ /**
1634
+ * Checks if the paragraph contains any revisions
1635
+ * @returns True if paragraph has revisions
1636
+ */
1637
+ hasRevisions(): boolean {
1638
+ return this.getRevisions().length > 0;
1639
+ }
1640
+
1641
+ /**
1642
+ * Consolidates adjacent revisions of the same type, author, and within a time window.
1643
+ *
1644
+ * This addresses the "random insertions and deletions" problem where Word displays
1645
+ * many small revisions instead of consolidated ones. The method merges:
1646
+ * - Adjacent insertions from the same author within 1 second
1647
+ * - Adjacent deletions from the same author within 1 second
1648
+ *
1649
+ * **How it works:**
1650
+ * - Iterates through paragraph content
1651
+ * - When finding consecutive Revisions of same type/author within time window:
1652
+ * - Merges their content (Runs/Hyperlinks) into a single Revision
1653
+ * - Uses the earliest timestamp for the merged revision
1654
+ * - Non-revision content acts as a boundary (stops merging)
1655
+ *
1656
+ * **Why this matters:**
1657
+ * Microsoft Word typically consolidates edits made in quick succession by the same
1658
+ * author. Without consolidation, programmatic edits create many tiny revisions that
1659
+ * clutter the document and confuse users when reviewing changes.
1660
+ *
1661
+ * @param timeWindowMs - Time window in milliseconds for consolidation (default: 1000ms)
1662
+ * @returns Number of revisions that were consolidated (merged)
1663
+ *
1664
+ * @example
1665
+ * ```typescript
1666
+ * // Before: Multiple separate insertions
1667
+ * para.addText('Hello '); // Creates w:ins #1
1668
+ * para.addText('World'); // Creates w:ins #2
1669
+ *
1670
+ * // Consolidate into single revision
1671
+ * const merged = para.consolidateRevisions();
1672
+ * console.log(`Consolidated ${merged} revisions`);
1673
+ * // Result: Single w:ins containing "Hello World"
1674
+ * ```
1675
+ */
1676
+ consolidateRevisions(timeWindowMs = 1000): number {
1677
+ if (!this.hasRevisions()) {
1678
+ return 0;
1679
+ }
1680
+
1681
+ const consolidatedContent: ParagraphContent[] = [];
1682
+ let mergeCount = 0;
1683
+ let currentMergeGroup: Revision | null = null;
1684
+
1685
+ for (const item of this.content) {
1686
+ if (item instanceof Revision) {
1687
+ // Check if we can merge with current group
1688
+ if (currentMergeGroup && this.canMergeRevisions(currentMergeGroup, item, timeWindowMs)) {
1689
+ // Merge this revision into the current group
1690
+ for (const content of item.getContent()) {
1691
+ currentMergeGroup.addContent(content);
1692
+ }
1693
+ mergeCount++;
1694
+ } else {
1695
+ // Start a new merge group
1696
+ if (currentMergeGroup) {
1697
+ consolidatedContent.push(currentMergeGroup);
1698
+ }
1699
+ // Clone the revision to avoid modifying the original
1700
+ currentMergeGroup = this.cloneRevision(item);
1701
+ }
1702
+ } else {
1703
+ // Non-revision content - acts as boundary
1704
+ if (currentMergeGroup) {
1705
+ consolidatedContent.push(currentMergeGroup);
1706
+ currentMergeGroup = null;
1707
+ }
1708
+ consolidatedContent.push(item);
1709
+ }
1710
+ }
1711
+
1712
+ // Don't forget the last merge group
1713
+ if (currentMergeGroup) {
1714
+ consolidatedContent.push(currentMergeGroup);
1715
+ }
1716
+
1717
+ // Only update content if we actually merged something
1718
+ if (mergeCount > 0) {
1719
+ this.content = consolidatedContent;
1720
+ }
1721
+
1722
+ return mergeCount;
1723
+ }
1724
+
1725
+ /**
1726
+ * Checks if two revisions can be merged based on type, author, and time window.
1727
+ * @private
1728
+ */
1729
+ private canMergeRevisions(rev1: Revision, rev2: Revision, timeWindowMs: number): boolean {
1730
+ // Must be same type (both insertions or both deletions)
1731
+ if (rev1.getType() !== rev2.getType()) {
1732
+ return false;
1733
+ }
1734
+
1735
+ // Only merge insert/delete content revisions, not property changes
1736
+ const mergeableTypes: string[] = ['insert', 'delete', 'moveFrom', 'moveTo'];
1737
+ if (!mergeableTypes.includes(rev1.getType())) {
1738
+ return false;
1739
+ }
1740
+
1741
+ // Must be same author
1742
+ if (rev1.getAuthor() !== rev2.getAuthor()) {
1743
+ return false;
1744
+ }
1745
+
1746
+ // Must be within time window
1747
+ const timeDiff = Math.abs(rev1.getDate().getTime() - rev2.getDate().getTime());
1748
+ if (timeDiff > timeWindowMs) {
1749
+ return false;
1750
+ }
1751
+
1752
+ return true;
1753
+ }
1754
+
1755
+ /**
1756
+ * Creates a shallow clone of a revision with cloned content array.
1757
+ * @private
1758
+ */
1759
+ private cloneRevision(revision: Revision): Revision {
1760
+ const cloned = new Revision({
1761
+ id: revision.getId(),
1762
+ author: revision.getAuthor(),
1763
+ date: revision.getDate(),
1764
+ type: revision.getType(),
1765
+ content: [...revision.getContent()],
1766
+ previousProperties: revision.getPreviousProperties(),
1767
+ newProperties: revision.getNewProperties(),
1768
+ moveId: revision.getMoveId(),
1769
+ moveLocation: revision.getMoveLocation(),
1770
+ location: revision.getLocation(),
1771
+ fieldContext: revision.getFieldContext(),
1772
+ });
1773
+ return cloned;
1774
+ }
1775
+
1776
+ /**
1777
+ * Checks if the paragraph is empty (no text content)
1778
+ * @returns True if paragraph has no text
1779
+ */
1780
+ isEmpty(): boolean {
1781
+ return this.getText().trim().length === 0;
1782
+ }
1783
+
1784
+ /**
1785
+ * Gets the paragraph style ID
1786
+ *
1787
+ * Returns the style identifier if a style is applied to this paragraph.
1788
+ *
1789
+ * @returns Style ID (e.g., 'Heading1', 'Normal') or undefined if no style is set
1790
+ *
1791
+ * @example
1792
+ * ```typescript
1793
+ * const style = para.getStyle();
1794
+ * if (style === 'Heading1') {
1795
+ * console.log('This is a heading paragraph');
1796
+ * }
1797
+ * ```
1798
+ */
1799
+ getStyle(): string | undefined {
1800
+ return this.formatting.style;
1801
+ }
1802
+
1803
+ /**
1804
+ * Detects if this paragraph is a heading and returns its level (1-9)
1805
+ *
1806
+ * Detection is performed using multiple methods in order of reliability:
1807
+ * 1. **Style ID matching** - Checks for 'Heading1', 'Heading2', etc. (most reliable)
1808
+ * 2. **Outline level** - Uses ECMA-376 outlineLevel property (0-based, where 0 = H1)
1809
+ * 3. **Formatting heuristics** - Analyzes text size, boldness, and patterns
1810
+ *
1811
+ * @returns Heading level (1-9) if detected, null if not a heading
1812
+ *
1813
+ * @example
1814
+ * ```typescript
1815
+ * const para = doc.createParagraph('Chapter 1');
1816
+ * para.setStyle('Heading1');
1817
+ *
1818
+ * const level = para.detectHeadingLevel();
1819
+ * console.log(level); // 1
1820
+ *
1821
+ * // Use for TOC generation
1822
+ * for (const para of doc.getParagraphs()) {
1823
+ * const level = para.detectHeadingLevel();
1824
+ * if (level) {
1825
+ * console.log(`H${level}: ${para.getText()}`);
1826
+ * }
1827
+ * }
1828
+ * ```
1829
+ */
1830
+ detectHeadingLevel(): number | null {
1831
+ // Method 1: Check style ID (most reliable)
1832
+ const style = this.formatting.style;
1833
+ if (style) {
1834
+ const match = /^Heading(\d)$/i.exec(style);
1835
+ if (match?.[1]) {
1836
+ const level = parseInt(match[1], 10);
1837
+ // Word supports Heading1-Heading9
1838
+ return level >= 1 && level <= 9 ? level : null;
1839
+ }
1840
+ }
1841
+
1842
+ // Method 2: Check outline level (ECMA-376 property)
1843
+ // Per ECMA-376 Part 1 §17.3.1.20, outlineLevel is 0-based (0 = Level 1)
1844
+ if (this.formatting.outlineLevel !== undefined) {
1845
+ const level = this.formatting.outlineLevel + 1;
1846
+ return level >= 1 && level <= 9 ? level : null;
1847
+ }
1848
+
1849
+ // Method 3: Formatting heuristics (least reliable, use as fallback)
1850
+ // Only attempt if paragraph has text
1851
+ const text = this.getText().trim();
1852
+ if (!text) return null;
1853
+
1854
+ // Get formatting from first run (headings typically have uniform formatting)
1855
+ const runs = this.getRuns();
1856
+ if (runs.length === 0) return null;
1857
+
1858
+ const firstRun = runs[0];
1859
+ if (!firstRun) return null;
1860
+
1861
+ const fmt = firstRun.getFormatting();
1862
+ if (!fmt) return null;
1863
+
1864
+ // Heuristic: Large bold text suggests a heading
1865
+ if (fmt.bold && fmt.size) {
1866
+ // H1: Very large (>= 24pt)
1867
+ if (fmt.size >= 24) return 1;
1868
+ // H2: Large (>= 20pt)
1869
+ if (fmt.size >= 20) return 2;
1870
+ // H3: Medium-large (>= 16pt)
1871
+ if (fmt.size >= 16) return 3;
1872
+ // H4-H6: Moderate size (14pt+)
1873
+ if (fmt.size >= 14) return 4;
1874
+ }
1875
+
1876
+ // Additional heuristic: All caps bold text (common for headings)
1877
+ if (fmt.bold && fmt.allCaps && text.length < 100) {
1878
+ return 2; // Often used for section headings
1879
+ }
1880
+
1881
+ // Not detected as a heading
1882
+ return null;
1883
+ }
1884
+
1885
+ /**
1886
+ * Sets paragraph text alignment
1887
+ *
1888
+ * Controls how text is aligned within the paragraph boundaries.
1889
+ *
1890
+ * @param alignment - Alignment value ('left' | 'center' | 'right' | 'justify' | 'both')
1891
+ * @returns This paragraph instance for method chaining
1892
+ *
1893
+ * @example
1894
+ * ```typescript
1895
+ * para.setAlignment('center'); // Center-aligned
1896
+ * para.setAlignment('justify'); // Justified text
1897
+ * ```
1898
+ */
1899
+ setAlignment(alignment: ParagraphAlignment): this {
1900
+ const previousValue = this.formatting.alignment;
1901
+ this.formatting.alignment = alignment;
1902
+ if (this.trackingContext?.isEnabled() && previousValue !== alignment) {
1903
+ this.trackingContext.trackParagraphPropertyChange(this, 'alignment', previousValue, alignment);
1904
+ }
1905
+ return this;
1906
+ }
1907
+
1908
+ /**
1909
+ * Sets left indentation
1910
+ *
1911
+ * WARNING: If this paragraph has numbering (is part of a list), setting
1912
+ * left indentation will be ignored as numbering controls indentation.
1913
+ * Use setNumbering() with different levels to change list indentation.
1914
+ *
1915
+ * @param twips - Indentation in twips (1/20th of a point)
1916
+ * @returns This paragraph for chaining
1917
+ *
1918
+ * @example
1919
+ * ```typescript
1920
+ * // For regular paragraphs
1921
+ * paragraph.setLeftIndent(720); // 0.5 inch indent
1922
+ *
1923
+ * // For list items, use numbering levels instead
1924
+ * paragraph.setNumbering(listId, 1); // Increases indent to level 1
1925
+ * ```
1926
+ */
1927
+ setLeftIndent(twips: number): this {
1928
+ if (this.formatting.numbering) {
1929
+ // Note: This will be cleared when setNumbering() was called or will be on next call
1930
+ // Still allow setting for edge cases, but it will have no effect
1931
+ defaultLogger.warn(
1932
+ "Setting left indentation on a numbered paragraph has no effect. " +
1933
+ "Numbering controls indentation. Use different numbering levels to change indent."
1934
+ );
1935
+ }
1936
+ const previousValue = this.formatting.indentation?.left;
1937
+ if (!this.formatting.indentation) {
1938
+ this.formatting.indentation = {};
1939
+ }
1940
+ this.formatting.indentation.left = twips;
1941
+ if (this.trackingContext?.isEnabled() && previousValue !== twips) {
1942
+ this.trackingContext.trackParagraphPropertyChange(this, 'indentation.left', previousValue, twips);
1943
+ }
1944
+ return this;
1945
+ }
1946
+
1947
+ /**
1948
+ * Sets right indentation
1949
+ * @param twips - Indentation in twips
1950
+ * @returns This paragraph for chaining
1951
+ */
1952
+ setRightIndent(twips: number): this {
1953
+ const previousValue = this.formatting.indentation?.right;
1954
+ if (!this.formatting.indentation) {
1955
+ this.formatting.indentation = {};
1956
+ }
1957
+ this.formatting.indentation.right = twips;
1958
+ if (this.trackingContext?.isEnabled() && previousValue !== twips) {
1959
+ this.trackingContext.trackParagraphPropertyChange(this, 'indentation.right', previousValue, twips);
1960
+ }
1961
+ return this;
1962
+ }
1963
+
1964
+ /**
1965
+ * Sets first line indentation
1966
+ *
1967
+ * WARNING: If this paragraph has numbering (is part of a list), setting
1968
+ * first line indentation will be ignored as numbering controls indentation.
1969
+ * Numbered lists use hanging indentation for proper alignment.
1970
+ *
1971
+ * @param twips - Indentation in twips
1972
+ * @returns This paragraph for chaining
1973
+ */
1974
+ setFirstLineIndent(twips: number): this {
1975
+ if (this.formatting.numbering) {
1976
+ defaultLogger.warn(
1977
+ "Setting first line indentation on a numbered paragraph has no effect. " +
1978
+ "Numbering controls indentation using hanging indent."
1979
+ );
1980
+ }
1981
+ const previousValue = this.formatting.indentation?.firstLine;
1982
+ if (!this.formatting.indentation) {
1983
+ this.formatting.indentation = {};
1984
+ }
1985
+ this.formatting.indentation.firstLine = twips;
1986
+ if (this.trackingContext?.isEnabled() && previousValue !== twips) {
1987
+ this.trackingContext.trackParagraphPropertyChange(this, 'indentation.firstLine', previousValue, twips);
1988
+ }
1989
+ return this;
1990
+ }
1991
+
1992
+ /**
1993
+ * Sets hanging indentation
1994
+ *
1995
+ * Creates a hanging indent where the first line starts at the left margin
1996
+ * and subsequent lines are indented. Common for bulleted/numbered lists
1997
+ * and bibliographies.
1998
+ *
1999
+ * WARNING: If this paragraph has numbering (is part of a list), setting
2000
+ * hanging indentation manually will be ignored as numbering controls indentation.
2001
+ *
2002
+ * @param twips - Indentation in twips (must be non-negative)
2003
+ * @returns This paragraph for chaining
2004
+ *
2005
+ * @example
2006
+ * ```typescript
2007
+ * // Create hanging indent of 0.5 inch (720 twips)
2008
+ * paragraph.setHangingIndent(720);
2009
+ * ```
2010
+ */
2011
+ setHangingIndent(twips: number): this {
2012
+ if (twips < 0) {
2013
+ throw new Error('Hanging indent must be non-negative');
2014
+ }
2015
+ if (this.formatting.numbering) {
2016
+ defaultLogger.warn(
2017
+ "Setting hanging indentation on a numbered paragraph has no effect. " +
2018
+ "Numbering controls indentation."
2019
+ );
2020
+ }
2021
+ const previousValue = this.formatting.indentation?.hanging;
2022
+ if (!this.formatting.indentation) {
2023
+ this.formatting.indentation = {};
2024
+ }
2025
+ this.formatting.indentation.hanging = twips;
2026
+ if (this.trackingContext?.isEnabled() && previousValue !== twips) {
2027
+ this.trackingContext.trackParagraphPropertyChange(this, 'indentation.hanging', previousValue, twips);
2028
+ }
2029
+ return this;
2030
+ }
2031
+
2032
+ /**
2033
+ * Sets spacing before the paragraph
2034
+ *
2035
+ * Controls the vertical space above the paragraph.
2036
+ * 1 point = 20 twips, so 120 twips = 6pt spacing.
2037
+ *
2038
+ * @param twips - Spacing value in twips (1/20th of a point)
2039
+ * @returns This paragraph instance for method chaining
2040
+ *
2041
+ * @example
2042
+ * ```typescript
2043
+ * para.setSpaceBefore(240); // 12pt (240 twips) before paragraph
2044
+ * para.setSpaceBefore(0); // No space before
2045
+ * ```
2046
+ */
2047
+ setSpaceBefore(twips: number): this {
2048
+ const previousValue = this.formatting.spacing?.before;
2049
+ if (!this.formatting.spacing) {
2050
+ this.formatting.spacing = {};
2051
+ }
2052
+ this.formatting.spacing.before = twips;
2053
+ if (this.trackingContext?.isEnabled() && previousValue !== twips) {
2054
+ this.trackingContext.trackParagraphPropertyChange(this, 'spacing.before', previousValue, twips);
2055
+ }
2056
+ return this;
2057
+ }
2058
+
2059
+ /**
2060
+ * Sets spacing after the paragraph
2061
+ *
2062
+ * Controls the vertical space below the paragraph.
2063
+ * 1 point = 20 twips, so 120 twips = 6pt spacing.
2064
+ *
2065
+ * @param twips - Spacing value in twips (1/20th of a point)
2066
+ * @returns This paragraph instance for method chaining
2067
+ *
2068
+ * @example
2069
+ * ```typescript
2070
+ * para.setSpaceAfter(120); // 6pt (120 twips) after paragraph
2071
+ * para.setSpaceAfter(0); // No space after
2072
+ * ```
2073
+ */
2074
+ setSpaceAfter(twips: number): this {
2075
+ const previousValue = this.formatting.spacing?.after;
2076
+ if (!this.formatting.spacing) {
2077
+ this.formatting.spacing = {};
2078
+ }
2079
+ this.formatting.spacing.after = twips;
2080
+ if (this.trackingContext?.isEnabled() && previousValue !== twips) {
2081
+ this.trackingContext.trackParagraphPropertyChange(this, 'spacing.after', previousValue, twips);
2082
+ }
2083
+ return this;
2084
+ }
2085
+
2086
+ /**
2087
+ * Sets line spacing within the paragraph
2088
+ *
2089
+ * Controls the vertical space between lines of text within the paragraph.
2090
+ * Per ECMA-376 Part 1 §17.3.1.33 (w:spacing/@w:line)
2091
+ *
2092
+ * @param twips - Line spacing value in twips (1/20th of a point)
2093
+ * @param rule - Line spacing rule (default: 'auto')
2094
+ * - 'auto': Line spacing is based on font size (240 twips = single spacing)
2095
+ * - 'exact': Exact line height regardless of font size
2096
+ * - 'atLeast': Minimum line height, expands if content is larger
2097
+ * @returns This paragraph instance for method chaining
2098
+ *
2099
+ * @example
2100
+ * ```typescript
2101
+ * para.setLineSpacing(240, 'auto'); // Single spacing
2102
+ * para.setLineSpacing(360, 'auto'); // 1.5 spacing
2103
+ * para.setLineSpacing(480, 'auto'); // Double spacing
2104
+ * para.setLineSpacing(300, 'exact'); // Exactly 15pt line height
2105
+ * ```
2106
+ */
2107
+ setLineSpacing(
2108
+ twips: number,
2109
+ rule: "auto" | "exact" | "atLeast" = "auto"
2110
+ ): this {
2111
+ const previousLine = this.formatting.spacing?.line;
2112
+ const previousRule = this.formatting.spacing?.lineRule;
2113
+ if (!this.formatting.spacing) {
2114
+ this.formatting.spacing = {};
2115
+ }
2116
+ this.formatting.spacing.line = twips;
2117
+ this.formatting.spacing.lineRule = rule;
2118
+ if (this.trackingContext?.isEnabled()) {
2119
+ if (previousLine !== twips) {
2120
+ this.trackingContext.trackParagraphPropertyChange(this, 'spacing.line', previousLine, twips);
2121
+ }
2122
+ if (previousRule !== rule) {
2123
+ this.trackingContext.trackParagraphPropertyChange(this, 'spacing.lineRule', previousRule, rule);
2124
+ }
2125
+ }
2126
+ return this;
2127
+ }
2128
+
2129
+ /**
2130
+ * Clears all direct spacing properties from the paragraph.
2131
+ *
2132
+ * Removes the spacing object (before, after, line, lineRule) so the paragraph
2133
+ * inherits spacing from its style definition. This is different from setting
2134
+ * spacing to 0 — clearing means "use style value", while 0 means "no spacing".
2135
+ *
2136
+ * @returns This paragraph instance for method chaining
2137
+ *
2138
+ * @example
2139
+ * ```typescript
2140
+ * // Clear direct spacing so Normal style's 3pt/3pt takes effect
2141
+ * para.clearSpacing();
2142
+ * ```
2143
+ */
2144
+ clearSpacing(): this {
2145
+ delete this.formatting.spacing;
2146
+ return this;
2147
+ }
2148
+
2149
+ /**
2150
+ * Sets the paragraph style
2151
+ *
2152
+ * Applies a style definition to the paragraph. The style must exist
2153
+ * in the document's StylesManager.
2154
+ *
2155
+ * @param styleId - The style identifier (e.g., 'Heading1', 'Normal', 'ListParagraph')
2156
+ * @returns This paragraph instance for method chaining
2157
+ *
2158
+ * @example
2159
+ * ```typescript
2160
+ * para.setStyle('Heading1'); // Apply Heading1 style
2161
+ * para.setStyle('Normal'); // Apply Normal style
2162
+ * ```
2163
+ */
2164
+ setStyle(styleId: string): this {
2165
+ const previousValue = this.formatting.style;
2166
+ this.formatting.style = styleId;
2167
+ if (this.trackingContext?.isEnabled() && previousValue !== styleId) {
2168
+ this.trackingContext.trackParagraphPropertyChange(this, 'style', previousValue, styleId);
2169
+ }
2170
+ return this;
2171
+ }
2172
+
2173
+ /**
2174
+ * Sets keep with next
2175
+ *
2176
+ * **Automatic Conflict Resolution:**
2177
+ * When setting keepNext to true, pageBreakBefore is automatically cleared to prevent
2178
+ * layout conflicts. The pageBreakBefore property creates contradictory constraints
2179
+ * (breaking the page vs. keeping content together) that cause massive whitespace
2180
+ * in Word as the layout engine tries to satisfy both. Keep properties take priority
2181
+ * as they represent explicit user intent to keep content together.
2182
+ *
2183
+ * @param keepNext - Whether to keep with next paragraph
2184
+ * @returns This paragraph for chaining
2185
+ */
2186
+ setKeepNext(keepNext = true): this {
2187
+ const previousValue = this.formatting.keepNext;
2188
+ this.formatting.keepNext = keepNext;
2189
+
2190
+ // Resolve property conflicts: keepNext contradicts pageBreakBefore
2191
+ if (keepNext) {
2192
+ this.formatting.pageBreakBefore = false;
2193
+ }
2194
+
2195
+ if (this.trackingContext?.isEnabled() && previousValue !== keepNext) {
2196
+ this.trackingContext.trackParagraphPropertyChange(this, 'keepNext', previousValue, keepNext);
2197
+ }
2198
+ return this;
2199
+ }
2200
+
2201
+ /**
2202
+ * Sets keep lines together
2203
+ *
2204
+ * **Automatic Conflict Resolution:**
2205
+ * When setting keepLines to true, pageBreakBefore is automatically cleared to prevent
2206
+ * layout conflicts. The pageBreakBefore property creates contradictory constraints
2207
+ * (breaking the page vs. keeping lines together) that cause massive whitespace
2208
+ * in Word as the layout engine tries to satisfy both. Keep properties take priority
2209
+ * as they represent explicit user intent to keep content together.
2210
+ *
2211
+ * @param keepLines - Whether to keep lines together
2212
+ * @returns This paragraph for chaining
2213
+ */
2214
+ setKeepLines(keepLines = true): this {
2215
+ const previousValue = this.formatting.keepLines;
2216
+ this.formatting.keepLines = keepLines;
2217
+
2218
+ // Resolve property conflicts: keepLines contradicts pageBreakBefore
2219
+ if (keepLines) {
2220
+ this.formatting.pageBreakBefore = false;
2221
+ }
2222
+
2223
+ if (this.trackingContext?.isEnabled() && previousValue !== keepLines) {
2224
+ this.trackingContext.trackParagraphPropertyChange(this, 'keepLines', previousValue, keepLines);
2225
+ }
2226
+ return this;
2227
+ }
2228
+
2229
+ /**
2230
+ * Sets page break before
2231
+ * @param pageBreakBefore - Whether to insert page break before
2232
+ * @returns This paragraph for chaining
2233
+ */
2234
+ setPageBreakBefore(pageBreakBefore = true): this {
2235
+ const previousValue = this.formatting.pageBreakBefore;
2236
+ this.formatting.pageBreakBefore = pageBreakBefore;
2237
+ if (this.trackingContext?.isEnabled() && previousValue !== pageBreakBefore) {
2238
+ this.trackingContext.trackParagraphPropertyChange(this, 'pageBreakBefore', previousValue, pageBreakBefore);
2239
+ }
2240
+ return this;
2241
+ }
2242
+
2243
+ /**
2244
+ * Marks this paragraph as preserved to prevent automatic removal by document processing operations
2245
+ * (e.g., removing extra blank paragraphs). Useful for spacing paragraphs that should remain
2246
+ * even if they appear to be "extra" blank lines.
2247
+ * @param preserved - Whether to preserve this paragraph
2248
+ * @returns This paragraph for chaining
2249
+ */
2250
+ setPreserved(preserved = true): this {
2251
+ this._isPreserved = preserved;
2252
+ return this;
2253
+ }
2254
+
2255
+ /**
2256
+ * Checks if this paragraph is marked as preserved from automatic removal
2257
+ * @returns True if paragraph should be preserved from removal operations
2258
+ */
2259
+ isPreserved(): boolean {
2260
+ return this._isPreserved;
2261
+ }
2262
+
2263
+ /**
2264
+ * Sets numbering for this paragraph (adds to a list)
2265
+ *
2266
+ * When numbering is applied, any conflicting paragraph indentation
2267
+ * (left, firstLine, hanging) is automatically cleared to prevent
2268
+ * override issues. Right indentation is preserved as it doesn't
2269
+ * conflict with list formatting.
2270
+ *
2271
+ * This matches Microsoft Word behavior where numbering controls
2272
+ * the indentation, not paragraph-level formatting.
2273
+ *
2274
+ * @param numId - The numbering instance ID
2275
+ * @param level - The level (0-8, where 0 is the outermost level)
2276
+ * @returns This paragraph for chaining
2277
+ *
2278
+ * @example
2279
+ * ```typescript
2280
+ * const listId = doc.createBulletList();
2281
+ * paragraph.setNumbering(listId, 0); // Level 0, indent controlled by numbering
2282
+ * paragraph.setNumbering(listId, 1); // Level 1, deeper indent
2283
+ * ```
2284
+ */
2285
+ setNumbering(numId: number, level = 0): this {
2286
+ if (numId < 0) {
2287
+ throw new Error("Numbering ID must be non-negative");
2288
+ }
2289
+ if (level < 0 || level > 8) {
2290
+ throw new Error("Level must be between 0 and 8");
2291
+ }
2292
+
2293
+ const previousValue = this.formatting.numbering;
2294
+ this.formatting.numbering = { numId, level };
2295
+ delete this.formatting.numberingSuppressed;
2296
+
2297
+ // Clear conflicting indentation properties
2298
+ // Per ECMA-376 §17.3.1.12, paragraph indentation overrides numbering indentation
2299
+ // To prevent unexpected behavior, we clear left/firstLine/hanging when numbering is applied
2300
+ // This matches Microsoft Word behavior where numbering controls indentation
2301
+ if (this.formatting.indentation) {
2302
+ const { right } = this.formatting.indentation;
2303
+ // Preserve right indent only (doesn't conflict with numbering)
2304
+ this.formatting.indentation = right !== undefined ? { right } : undefined;
2305
+ }
2306
+
2307
+ if (this.trackingContext?.isEnabled()) {
2308
+ const newValue = { numId, level };
2309
+ if (previousValue?.numId !== newValue.numId || previousValue?.level !== newValue.level) {
2310
+ this.trackingContext.trackParagraphPropertyChange(this, 'numbering', previousValue, newValue);
2311
+ }
2312
+ }
2313
+ return this;
2314
+ }
2315
+
2316
+ /**
2317
+ * Sets contextual spacing for this paragraph
2318
+ * When enabled, removes spacing between consecutive paragraphs of the same style
2319
+ * Per ECMA-376 Part 1 §17.3.1.8
2320
+ * @param enable - Whether to enable contextual spacing
2321
+ * @returns This paragraph for chaining
2322
+ */
2323
+ setContextualSpacing(enable = true): this {
2324
+ const previousValue = this.formatting.contextualSpacing;
2325
+ this.formatting.contextualSpacing = enable;
2326
+ if (this.trackingContext?.isEnabled() && previousValue !== enable) {
2327
+ this.trackingContext.trackParagraphPropertyChange(this, 'contextualSpacing', previousValue, enable);
2328
+ }
2329
+ return this;
2330
+ }
2331
+
2332
+ /**
2333
+ * Removes numbering from this paragraph
2334
+ * @returns This paragraph for chaining
2335
+ */
2336
+ removeNumbering(): this {
2337
+ delete this.formatting.numbering;
2338
+ delete this.formatting.numberingSuppressed;
2339
+ return this;
2340
+ }
2341
+
2342
+ /**
2343
+ * Gets the numbering properties
2344
+ * @returns Numbering properties or undefined
2345
+ */
2346
+ getNumbering(): { numId: number; level: number } | undefined {
2347
+ return this.formatting.numbering
2348
+ ? { ...this.formatting.numbering }
2349
+ : undefined;
2350
+ }
2351
+
2352
+ /**
2353
+ * Sets widow/orphan control for this paragraph
2354
+ * Controls whether to prevent single lines at the top or bottom of a page.
2355
+ * Word's default is true - set to false to allow widows/orphans.
2356
+ * Per ECMA-376 Part 1 §17.3.1.40
2357
+ * @param enable - Whether to enable widow/orphan control
2358
+ * @returns This paragraph for chaining
2359
+ */
2360
+ setWidowControl(enable = true): this {
2361
+ const previousValue = this.formatting.widowControl;
2362
+ this.formatting.widowControl = enable;
2363
+ if (this.trackingContext?.isEnabled() && previousValue !== enable) {
2364
+ this.trackingContext.trackParagraphPropertyChange(this, 'widowControl', previousValue, enable);
2365
+ }
2366
+ return this;
2367
+ }
2368
+
2369
+ /**
2370
+ * Sets outline level for this paragraph (for table of contents)
2371
+ * Level 0-9 indicates hierarchy in document structure.
2372
+ * Level 0 = highest level (like Heading 1)
2373
+ * Level 9 = lowest level
2374
+ * Per ECMA-376 Part 1 §17.3.1.19
2375
+ * @param level - Outline level (0-9)
2376
+ * @returns This paragraph for chaining
2377
+ */
2378
+ setOutlineLevel(level: number): this {
2379
+ if (level < 0 || level > 9) {
2380
+ throw new Error("Outline level must be between 0 and 9");
2381
+ }
2382
+ const previousValue = this.formatting.outlineLevel;
2383
+ this.formatting.outlineLevel = level;
2384
+ if (this.trackingContext?.isEnabled() && previousValue !== level) {
2385
+ this.trackingContext.trackParagraphPropertyChange(this, 'outlineLevel', previousValue, level);
2386
+ }
2387
+ return this;
2388
+ }
2389
+
2390
+ /**
2391
+ * Sets whether to suppress line numbers for this paragraph
2392
+ * Per ECMA-376 Part 1 §17.3.1.34
2393
+ * @param suppress - Whether to suppress line numbers
2394
+ * @returns This paragraph for chaining
2395
+ */
2396
+ setSuppressLineNumbers(suppress = true): this {
2397
+ const previousValue = this.formatting.suppressLineNumbers;
2398
+ this.formatting.suppressLineNumbers = suppress;
2399
+ if (this.trackingContext?.isEnabled() && previousValue !== suppress) {
2400
+ this.trackingContext.trackParagraphPropertyChange(this, 'suppressLineNumbers', previousValue, suppress);
2401
+ }
2402
+ return this;
2403
+ }
2404
+
2405
+ /**
2406
+ * Sets bidirectional text layout (right-to-left)
2407
+ * Enables right-to-left paragraph layout for languages like Arabic and Hebrew.
2408
+ * Per ECMA-376 Part 1 §17.3.1.6
2409
+ * @param enable - Whether to enable bidirectional (RTL) layout
2410
+ * @returns This paragraph for chaining
2411
+ */
2412
+ setBidi(enable = true): this {
2413
+ const previousValue = this.formatting.bidi;
2414
+ this.formatting.bidi = enable;
2415
+ if (this.trackingContext?.isEnabled() && previousValue !== enable) {
2416
+ this.trackingContext.trackParagraphPropertyChange(this, 'bidi', previousValue, enable);
2417
+ }
2418
+ return this;
2419
+ }
2420
+
2421
+ /**
2422
+ * Sets text flow direction for this paragraph
2423
+ * Per ECMA-376 Part 1 §17.3.1.36
2424
+ * @param direction - Text flow direction
2425
+ * - 'lrTb': Left-to-right, top-to-bottom (default for English)
2426
+ * - 'tbRl': Top-to-bottom, right-to-left (traditional Chinese/Japanese)
2427
+ * - 'btLr': Bottom-to-top, left-to-right (Mongolian)
2428
+ * - 'lrTbV': Left-to-right, top-to-bottom vertical
2429
+ * - 'tbRlV': Top-to-bottom, right-to-left vertical
2430
+ * - 'tbLrV': Top-to-bottom, left-to-right vertical
2431
+ * @returns This paragraph for chaining
2432
+ */
2433
+ setTextDirection(direction: TextDirection): this {
2434
+ const previousValue = this.formatting.textDirection;
2435
+ this.formatting.textDirection = direction;
2436
+ if (this.trackingContext?.isEnabled() && previousValue !== direction) {
2437
+ this.trackingContext.trackParagraphPropertyChange(this, 'textDirection', previousValue, direction);
2438
+ }
2439
+ return this;
2440
+ }
2441
+
2442
+ /**
2443
+ * Sets vertical text alignment for this paragraph
2444
+ * Per ECMA-376 Part 1 §17.3.1.35
2445
+ * @param alignment - Vertical alignment
2446
+ * - 'top': Align to top of line
2447
+ * - 'center': Align to center of line
2448
+ * - 'baseline': Align to baseline
2449
+ * - 'bottom': Align to bottom of line
2450
+ * - 'auto': Automatic alignment
2451
+ * @returns This paragraph for chaining
2452
+ */
2453
+ setTextAlignment(alignment: TextAlignment): this {
2454
+ const previousValue = this.formatting.textAlignment;
2455
+ this.formatting.textAlignment = alignment;
2456
+ if (this.trackingContext?.isEnabled() && previousValue !== alignment) {
2457
+ this.trackingContext.trackParagraphPropertyChange(this, 'textAlignment', previousValue, alignment);
2458
+ }
2459
+ return this;
2460
+ }
2461
+
2462
+ /**
2463
+ * Sets mirror indents for this paragraph
2464
+ * When enabled, uses inside/outside indents instead of left/right for double-sided printing.
2465
+ * Per ECMA-376 Part 1 §17.3.1.18
2466
+ * @param enable - Whether to enable mirror indents
2467
+ * @returns This paragraph for chaining
2468
+ */
2469
+ setMirrorIndents(enable = true): this {
2470
+ const previousValue = this.formatting.mirrorIndents;
2471
+ this.formatting.mirrorIndents = enable;
2472
+ if (this.trackingContext?.isEnabled() && previousValue !== enable) {
2473
+ this.trackingContext.trackParagraphPropertyChange(this, 'mirrorIndents', previousValue, enable);
2474
+ }
2475
+ return this;
2476
+ }
2477
+
2478
+ /**
2479
+ * Sets auto-adjust right indent for this paragraph
2480
+ * When enabled, automatically adjusts right indent when a document grid is defined.
2481
+ * Per ECMA-376 Part 1 §17.3.1.1
2482
+ * @param enable - Whether to enable auto-adjust right indent
2483
+ * @returns This paragraph for chaining
2484
+ */
2485
+ setAdjustRightInd(enable = true): this {
2486
+ const previousValue = this.formatting.adjustRightInd;
2487
+ this.formatting.adjustRightInd = enable;
2488
+ if (this.trackingContext?.isEnabled() && previousValue !== enable) {
2489
+ this.trackingContext.trackParagraphPropertyChange(this, 'adjustRightInd', previousValue, enable);
2490
+ }
2491
+ return this;
2492
+ }
2493
+
2494
+ /**
2495
+ * Sets text frame/box properties for this paragraph
2496
+ * Text frames allow positioning paragraphs in specific locations with text wrapping.
2497
+ * Per ECMA-376 Part 1 §17.3.1.11
2498
+ * @param props - Frame properties
2499
+ * - w, h: Width and height in twips
2500
+ * - hRule: Height rule ('auto', 'atLeast', 'exact')
2501
+ * - x, y: Absolute positioning in twips
2502
+ * - xAlign, yAlign: Relative alignment
2503
+ * - hAnchor, vAnchor: Positioning base ('page', 'margin', 'text')
2504
+ * - hSpace, vSpace: Padding around frame in twips
2505
+ * - wrap: Text wrapping ('around', 'notBeside', 'none', 'tight')
2506
+ * - dropCap: Drop cap style ('none', 'drop', 'margin')
2507
+ * - lines: Drop cap height in lines
2508
+ * - anchorLock: Lock frame anchor to paragraph
2509
+ * @returns This paragraph for chaining
2510
+ */
2511
+ setFrameProperties(props: FrameProperties): this {
2512
+ const previousValue = this.formatting.framePr;
2513
+ this.formatting.framePr = props;
2514
+ if (this.trackingContext?.isEnabled() && previousValue !== props) {
2515
+ this.trackingContext.trackParagraphPropertyChange(this, 'framePr', previousValue, props);
2516
+ }
2517
+ return this;
2518
+ }
2519
+
2520
+ /**
2521
+ * Suppress automatic hyphenation for this paragraph
2522
+ * Per ECMA-376 Part 1 §17.3.1.33
2523
+ * @param suppress - Whether to suppress hyphenation (default: true)
2524
+ * @returns This paragraph for chaining
2525
+ */
2526
+ setSuppressAutoHyphens(suppress = true): this {
2527
+ const previousValue = this.formatting.suppressAutoHyphens;
2528
+ this.formatting.suppressAutoHyphens = suppress;
2529
+ if (this.trackingContext?.isEnabled() && previousValue !== suppress) {
2530
+ this.trackingContext.trackParagraphPropertyChange(this, 'suppressAutoHyphens', previousValue, suppress);
2531
+ }
2532
+ return this;
2533
+ }
2534
+
2535
+ /**
2536
+ * Sets CJK kinsoku line-breaking rules
2537
+ * Per ECMA-376 Part 1 §17.3.1.16
2538
+ * @param enable - Whether to enable kinsoku rules (default: true)
2539
+ * @returns This paragraph for chaining
2540
+ */
2541
+ setKinsoku(enable = true): this {
2542
+ this.formatting.kinsoku = enable;
2543
+ return this;
2544
+ }
2545
+
2546
+ /**
2547
+ * Sets CJK word wrap behavior
2548
+ * Per ECMA-376 Part 1 §17.3.1.45
2549
+ * @param enable - Whether to allow wrapping mid-word (default: true)
2550
+ * @returns This paragraph for chaining
2551
+ */
2552
+ setWordWrap(enable = true): this {
2553
+ this.formatting.wordWrap = enable;
2554
+ return this;
2555
+ }
2556
+
2557
+ /**
2558
+ * Sets CJK overflow punctuation
2559
+ * Per ECMA-376 Part 1 §17.3.1.24
2560
+ * @param enable - Whether to allow punctuation overhang (default: true)
2561
+ * @returns This paragraph for chaining
2562
+ */
2563
+ setOverflowPunct(enable = true): this {
2564
+ this.formatting.overflowPunct = enable;
2565
+ return this;
2566
+ }
2567
+
2568
+ /**
2569
+ * Sets CJK top line punctuation compression
2570
+ * Per ECMA-376 Part 1 §17.3.1.43
2571
+ * @param enable - Whether to compress punctuation at line start (default: true)
2572
+ * @returns This paragraph for chaining
2573
+ */
2574
+ setTopLinePunct(enable = true): this {
2575
+ this.formatting.topLinePunct = enable;
2576
+ return this;
2577
+ }
2578
+
2579
+ /**
2580
+ * Sets auto space between East Asian and numeric text
2581
+ * Per ECMA-376 Part 1 §17.3.1.2
2582
+ * @param enable - Whether to auto-space (default: true)
2583
+ * @returns This paragraph for chaining
2584
+ */
2585
+ setAutoSpaceDE(enable = true): this {
2586
+ this.formatting.autoSpaceDE = enable;
2587
+ return this;
2588
+ }
2589
+
2590
+ /**
2591
+ * Sets auto space between East Asian and Western text
2592
+ * Per ECMA-376 Part 1 §17.3.1.3
2593
+ * @param enable - Whether to auto-space (default: true)
2594
+ * @returns This paragraph for chaining
2595
+ */
2596
+ setAutoSpaceDN(enable = true): this {
2597
+ this.formatting.autoSpaceDN = enable;
2598
+ return this;
2599
+ }
2600
+
2601
+ /**
2602
+ * Prevent text frames from overlapping with this paragraph
2603
+ * Per ECMA-376 Part 1 §17.3.1.34
2604
+ * @param suppress - Whether to prevent overlap (default: true)
2605
+ * @returns This paragraph for chaining
2606
+ */
2607
+ setSuppressOverlap(suppress = true): this {
2608
+ const previousValue = this.formatting.suppressOverlap;
2609
+ this.formatting.suppressOverlap = suppress;
2610
+ if (this.trackingContext?.isEnabled() && previousValue !== suppress) {
2611
+ this.trackingContext.trackParagraphPropertyChange(this, 'suppressOverlap', previousValue, suppress);
2612
+ }
2613
+ return this;
2614
+ }
2615
+
2616
+ /**
2617
+ * Sets tight wrapping mode for text boxes
2618
+ * Controls how tightly surrounding text wraps around text box content.
2619
+ * Per ECMA-376 Part 1 §17.3.1.37
2620
+ * @param wrap - Tight wrap mode
2621
+ * - 'none': No tight wrapping
2622
+ * - 'allLines': Tight wrap all lines
2623
+ * - 'firstAndLastLine': Tight wrap first and last lines only
2624
+ * - 'firstLineOnly': Tight wrap first line only
2625
+ * - 'lastLineOnly': Tight wrap last line only
2626
+ * @returns This paragraph for chaining
2627
+ */
2628
+ setTextboxTightWrap(wrap: TextboxTightWrap): this {
2629
+ const previousValue = this.formatting.textboxTightWrap;
2630
+ this.formatting.textboxTightWrap = wrap;
2631
+ if (this.trackingContext?.isEnabled() && previousValue !== wrap) {
2632
+ this.trackingContext.trackParagraphPropertyChange(this, 'textboxTightWrap', previousValue, wrap);
2633
+ }
2634
+ return this;
2635
+ }
2636
+
2637
+ /**
2638
+ * Sets the HTML div ID associated with this paragraph
2639
+ * Used for HTML round-trip conversion to preserve div structure.
2640
+ * Per ECMA-376 Part 1 §17.3.1.9
2641
+ * @param id - Decimal ID referencing a div in the web settings part
2642
+ * @returns This paragraph for chaining
2643
+ */
2644
+ setDivId(id: number): this {
2645
+ const previousValue = this.formatting.divId;
2646
+ this.formatting.divId = id;
2647
+ if (this.trackingContext?.isEnabled() && previousValue !== id) {
2648
+ this.trackingContext.trackParagraphPropertyChange(this, 'divId', previousValue, id);
2649
+ }
2650
+ return this;
2651
+ }
2652
+
2653
+ /**
2654
+ * Sets conditional table style formatting for this paragraph
2655
+ * Used to apply conditional formatting based on table position (first row, last column, etc.).
2656
+ * Per ECMA-376 Part 1 §17.3.1.8
2657
+ * @param bitmask - Bitmask string (e.g., "101000000100")
2658
+ * Each bit represents a conditional formatting property:
2659
+ * - Bit 0: First row
2660
+ * - Bit 1: Last row
2661
+ * - Bit 2: First column
2662
+ * - Bit 3: Last column
2663
+ * - Bit 4: Band 1 vertical
2664
+ * - Bit 5: Band 2 vertical
2665
+ * - Bit 6: Band 1 horizontal
2666
+ * - Bit 7: Band 2 horizontal
2667
+ * - Bit 8-11: Corner cells (NE, NW, SE, SW)
2668
+ * @returns This paragraph for chaining
2669
+ */
2670
+ setConditionalFormatting(bitmask: string): this {
2671
+ const previousValue = this.formatting.cnfStyle;
2672
+ this.formatting.cnfStyle = bitmask;
2673
+ if (this.trackingContext?.isEnabled() && previousValue !== bitmask) {
2674
+ this.trackingContext.trackParagraphPropertyChange(this, 'cnfStyle', previousValue, bitmask);
2675
+ }
2676
+ return this;
2677
+ }
2678
+
2679
+ /**
2680
+ * Sets section properties for this paragraph
2681
+ * Used to define section breaks and section-specific formatting.
2682
+ * Per ECMA-376 Part 1 §17.3.1.30
2683
+ * @param properties - Section properties object
2684
+ * @returns This paragraph for chaining
2685
+ */
2686
+ setSectionProperties(properties: string | Record<string, unknown>): this {
2687
+ const previousValue = this.formatting.sectPr;
2688
+ this.formatting.sectPr = properties;
2689
+ if (this.trackingContext?.isEnabled() && previousValue !== properties) {
2690
+ this.trackingContext.trackParagraphPropertyChange(this, 'sectPr', previousValue, properties);
2691
+ }
2692
+ return this;
2693
+ }
2694
+
2695
+ /**
2696
+ * Sets paragraph property change tracking information
2697
+ * Used for revision history and change tracking.
2698
+ * Per ECMA-376 Part 1 §17.3.1.27
2699
+ * @param change - Change tracking information
2700
+ * @returns This paragraph for chaining
2701
+ */
2702
+ setParagraphPropertiesChange(change: ParagraphPropertiesChange): this {
2703
+ const previousValue = this.formatting.pPrChange;
2704
+ this.formatting.pPrChange = change;
2705
+ if (this.trackingContext?.isEnabled() && previousValue !== change) {
2706
+ this.trackingContext.trackParagraphPropertyChange(this, 'pPrChange', previousValue, change);
2707
+ }
2708
+ return this;
2709
+ }
2710
+
2711
+ /**
2712
+ * Clears paragraph property change tracking information.
2713
+ * Used when accepting revisions to remove the w:pPrChange element.
2714
+ * @returns This paragraph for chaining
2715
+ */
2716
+ clearParagraphPropertiesChange(): this {
2717
+ delete this.formatting.pPrChange;
2718
+ return this;
2719
+ }
2720
+
2721
+ /**
2722
+ * Sets run properties for the paragraph mark (¶ symbol)
2723
+ *
2724
+ * The paragraph mark is the invisible character at the end of every paragraph.
2725
+ * It can have its own formatting independent of the text runs in the paragraph.
2726
+ * This is useful for controlling formatting behavior when text is inserted after
2727
+ * the paragraph mark.
2728
+ *
2729
+ * Per ECMA-376 Part 1 §17.3.1.29 (Run Properties for the Paragraph Mark)
2730
+ *
2731
+ * Common use cases:
2732
+ * - Set default font for new text added to paragraph
2733
+ * - Control paragraph mark visibility in "Show/Hide ¶" mode
2734
+ * - Apply highlighting to paragraph mark for visual consistency
2735
+ *
2736
+ * @param properties - Run formatting properties for the paragraph mark
2737
+ * @returns This paragraph for chaining
2738
+ *
2739
+ * @example
2740
+ * ```typescript
2741
+ * // Set paragraph mark to be red and bold
2742
+ * paragraph.setParagraphMarkFormatting({ bold: true, color: 'FF0000' });
2743
+ *
2744
+ * // Hide paragraph mark
2745
+ * paragraph.setParagraphMarkFormatting({ vanish: true });
2746
+ *
2747
+ * // Set default font for new text in this paragraph
2748
+ * paragraph.setParagraphMarkFormatting({ font: 'Arial', size: 12 });
2749
+ * ```
2750
+ */
2751
+ setParagraphMarkFormatting(properties: RunFormatting): this {
2752
+ const previousValue = this.formatting.paragraphMarkRunProperties;
2753
+ this.formatting.paragraphMarkRunProperties = properties;
2754
+ if (this.trackingContext?.isEnabled() && previousValue !== properties) {
2755
+ this.trackingContext.trackParagraphPropertyChange(this, 'paragraphMarkRunProperties', previousValue, properties);
2756
+ }
2757
+ return this;
2758
+ }
2759
+
2760
+ /**
2761
+ * Marks the paragraph mark as deleted (tracked change)
2762
+ *
2763
+ * When a paragraph mark is deleted, it indicates that the paragraph
2764
+ * was joined with the next paragraph. Word shows this as a deletion
2765
+ * of the ¶ symbol.
2766
+ *
2767
+ * @param id - Unique revision ID
2768
+ * @param author - Author who deleted the paragraph mark
2769
+ * @param date - Date when the deletion occurred (defaults to now)
2770
+ * @returns This paragraph for chaining
2771
+ *
2772
+ * @example
2773
+ * ```typescript
2774
+ * paragraph.markParagraphMarkAsDeleted(1, 'Alice', new Date());
2775
+ * ```
2776
+ */
2777
+ markParagraphMarkAsDeleted(id: number, author: string, date?: Date): this {
2778
+ this.formatting.paragraphMarkDeletion = {
2779
+ id,
2780
+ author,
2781
+ date: date || new Date(),
2782
+ };
2783
+ return this;
2784
+ }
2785
+
2786
+ /**
2787
+ * Clears the paragraph mark deletion marker
2788
+ * @returns This paragraph for chaining
2789
+ */
2790
+ clearParagraphMarkDeletion(): this {
2791
+ delete this.formatting.paragraphMarkDeletion;
2792
+ return this;
2793
+ }
2794
+
2795
+ /**
2796
+ * Checks if the paragraph mark is marked as deleted
2797
+ * @returns True if the paragraph mark is deleted
2798
+ */
2799
+ isParagraphMarkDeleted(): boolean {
2800
+ return !!this.formatting.paragraphMarkDeletion;
2801
+ }
2802
+
2803
+ /**
2804
+ * Converts the paragraph to WordprocessingML XML element
2805
+ *
2806
+ * **ECMA-376 Compliance:** Properties are generated in the order specified by
2807
+ * ECMA-376 Part 1 §17.3.1.26 to ensure strict OpenXML conformance.
2808
+ *
2809
+ * Per spec, the order includes (partial list):
2810
+ * 1. pStyle (style reference)
2811
+ * 2. keepNext (keep with next paragraph)
2812
+ * 3. keepLines (keep lines together)
2813
+ * 4. pageBreakBefore (page break before)
2814
+ * 5. widowControl (widow/orphan control)
2815
+ * 6. numPr (numbering properties)
2816
+ * 7. suppressLineNumbers (suppress line numbers)
2817
+ * 8-10. borders, shading, tabs
2818
+ * 11-19. East Asian typography properties
2819
+ * 20. bidi (bidirectional layout)
2820
+ * 21. adjustRightInd (auto-adjust right indent)
2821
+ * 22. spacing, indentation, contextualSpacing
2822
+ * 23. mirrorIndents (mirror indents)
2823
+ * 24. jc (justification/alignment)
2824
+ * 25. textAlignment (vertical text alignment)
2825
+ * 26. textDirection (text flow direction)
2826
+ * 27. outlineLvl (outline level)
2827
+ *
2828
+ * @returns XMLElement representing the paragraph
2829
+ */
2830
+ toXML(): XMLElement {
2831
+ // Diagnostic logging before serialization
2832
+ const runData = this.getRuns().map((run) => ({
2833
+ text: run.getText(),
2834
+ rtl: run.isRTL(),
2835
+ }));
2836
+ logParagraphContent("serialization", -1, runData, this.formatting.bidi);
2837
+
2838
+ if (this.formatting.bidi) {
2839
+ logTextDirection(`Serializing paragraph with BiDi enabled`);
2840
+ }
2841
+
2842
+ const pPrChildren: XMLElement[] = [];
2843
+
2844
+ // 1. Paragraph style (must be first per ECMA-376 §17.3.1.26)
2845
+ if (this.formatting.style) {
2846
+ pPrChildren.push(
2847
+ XMLBuilder.wSelf("pStyle", { "w:val": this.formatting.style })
2848
+ );
2849
+ }
2850
+
2851
+ // 2. Keep with next paragraph
2852
+ if (this.formatting.keepNext) {
2853
+ pPrChildren.push(XMLBuilder.wSelf("keepNext"));
2854
+ }
2855
+
2856
+ // 3. Keep lines together
2857
+ if (this.formatting.keepLines) {
2858
+ pPrChildren.push(XMLBuilder.wSelf("keepLines"));
2859
+ }
2860
+
2861
+ // CT_PPrBase element order per ECMA-376:
2862
+ // pStyle → keepNext → keepLines → pageBreakBefore → framePr → widowControl →
2863
+ // numPr → suppressLineNumbers → pBdr → shd → tabs → suppressAutoHyphens →
2864
+ // kinsoku → wordWrap → overflowPunct → topLinePunct → autoSpaceDE → autoSpaceDN →
2865
+ // bidi → adjustRightInd → spacing → ind → contextualSpacing → mirrorIndents →
2866
+ // suppressOverlap → jc → textDirection → textAlignment → textboxTightWrap →
2867
+ // outlineLvl → divId → cnfStyle
2868
+
2869
+ // 4. Page break before
2870
+ if (this.formatting.pageBreakBefore) {
2871
+ pPrChildren.push(XMLBuilder.wSelf("pageBreakBefore"));
2872
+ }
2873
+
2874
+ // 5. Text frame properties (framePr)
2875
+ if (this.formatting.framePr) {
2876
+ const attrs: Record<string, string> = {};
2877
+ const f = this.formatting.framePr;
2878
+ if (f.w !== undefined) attrs["w:w"] = f.w.toString();
2879
+ if (f.h !== undefined) attrs["w:h"] = f.h.toString();
2880
+ if (f.hRule) attrs["w:hRule"] = f.hRule;
2881
+ if (f.x !== undefined) attrs["w:x"] = f.x.toString();
2882
+ if (f.y !== undefined) attrs["w:y"] = f.y.toString();
2883
+ if (f.xAlign) attrs["w:xAlign"] = f.xAlign;
2884
+ if (f.yAlign) attrs["w:yAlign"] = f.yAlign;
2885
+ if (f.hAnchor) attrs["w:hAnchor"] = f.hAnchor;
2886
+ if (f.vAnchor) attrs["w:vAnchor"] = f.vAnchor;
2887
+ if (f.hSpace !== undefined) attrs["w:hSpace"] = f.hSpace.toString();
2888
+ if (f.vSpace !== undefined) attrs["w:vSpace"] = f.vSpace.toString();
2889
+ if (f.wrap) attrs["w:wrap"] = f.wrap;
2890
+ if (f.dropCap) attrs["w:dropCap"] = f.dropCap;
2891
+ if (f.lines !== undefined) attrs["w:lines"] = f.lines.toString();
2892
+ if (f.anchorLock !== undefined)
2893
+ attrs["w:anchorLock"] = f.anchorLock ? "1" : "0";
2894
+ if (Object.keys(attrs).length > 0) {
2895
+ pPrChildren.push(XMLBuilder.wSelf("framePr", attrs));
2896
+ }
2897
+ }
2898
+
2899
+ // 6. Widow/orphan control
2900
+ if (this.formatting.widowControl !== undefined) {
2901
+ pPrChildren.push(
2902
+ XMLBuilder.wSelf("widowControl", {
2903
+ "w:val": this.formatting.widowControl ? "1" : "0",
2904
+ })
2905
+ );
2906
+ }
2907
+
2908
+ // 7. Numbering properties
2909
+ if (this.formatting.numbering) {
2910
+ const numPr = XMLBuilder.w("numPr", undefined, [
2911
+ XMLBuilder.wSelf("ilvl", {
2912
+ "w:val": this.formatting.numbering.level.toString(),
2913
+ }),
2914
+ XMLBuilder.wSelf("numId", {
2915
+ "w:val": this.formatting.numbering.numId.toString(),
2916
+ }),
2917
+ ]);
2918
+ pPrChildren.push(numPr);
2919
+ }
2920
+
2921
+ // 8. Suppress line numbers
2922
+ if (this.formatting.suppressLineNumbers) {
2923
+ pPrChildren.push(XMLBuilder.wSelf("suppressLineNumbers"));
2924
+ }
2925
+
2926
+ // 9. Paragraph borders
2927
+ if (this.formatting.borders) {
2928
+ const borderChildren: XMLElement[] = [];
2929
+ const borders = this.formatting.borders;
2930
+
2931
+ const createBorder = (
2932
+ borderType: string,
2933
+ border: BorderDefinition | undefined
2934
+ ): XMLElement | null => {
2935
+ if (!border) return null;
2936
+ const attributes: Record<string, string | number> = {};
2937
+ if (border.style) attributes["w:val"] = border.style;
2938
+ if (border.size !== undefined) attributes["w:sz"] = border.size;
2939
+ if (border.color) attributes["w:color"] = border.color;
2940
+ if (border.space !== undefined) attributes["w:space"] = border.space;
2941
+ if (Object.keys(attributes).length > 0) {
2942
+ return XMLBuilder.wSelf(borderType, attributes);
2943
+ }
2944
+ return null;
2945
+ };
2946
+
2947
+ // Add borders in order: top, left, bottom, right, between, bar
2948
+ const topBorder = createBorder("top", borders.top);
2949
+ if (topBorder) borderChildren.push(topBorder);
2950
+
2951
+ const leftBorder = createBorder("left", borders.left);
2952
+ if (leftBorder) borderChildren.push(leftBorder);
2953
+
2954
+ const bottomBorder = createBorder("bottom", borders.bottom);
2955
+ if (bottomBorder) borderChildren.push(bottomBorder);
2956
+
2957
+ const rightBorder = createBorder("right", borders.right);
2958
+ if (rightBorder) borderChildren.push(rightBorder);
2959
+
2960
+ const betweenBorder = createBorder("between", borders.between);
2961
+ if (betweenBorder) borderChildren.push(betweenBorder);
2962
+
2963
+ const barBorder = createBorder("bar", borders.bar);
2964
+ if (barBorder) borderChildren.push(barBorder);
2965
+
2966
+ if (borderChildren.length > 0) {
2967
+ pPrChildren.push(XMLBuilder.w("pBdr", undefined, borderChildren));
2968
+ }
2969
+ }
2970
+
2971
+ // 10. Paragraph shading
2972
+ if (this.formatting.shading) {
2973
+ const shdAttrs = buildShadingAttributes(this.formatting.shading);
2974
+ if (Object.keys(shdAttrs).length > 0) {
2975
+ pPrChildren.push(XMLBuilder.wSelf("shd", shdAttrs));
2976
+ }
2977
+ }
2978
+
2979
+ // 11. Tab stops
2980
+ if (this.formatting.tabs && this.formatting.tabs.length > 0) {
2981
+ const tabChildren: XMLElement[] = [];
2982
+ for (const tab of this.formatting.tabs) {
2983
+ const attributes: Record<string, string | number> = {};
2984
+ attributes["w:pos"] = tab.position;
2985
+ if (tab.val) attributes["w:val"] = tab.val;
2986
+ if (tab.leader) attributes["w:leader"] = tab.leader;
2987
+ tabChildren.push(XMLBuilder.wSelf("tab", attributes));
2988
+ }
2989
+ if (tabChildren.length > 0) {
2990
+ pPrChildren.push(XMLBuilder.w("tabs", undefined, tabChildren));
2991
+ }
2992
+ }
2993
+
2994
+ // 12. Suppress automatic hyphenation
2995
+ if (this.formatting.suppressAutoHyphens) {
2996
+ pPrChildren.push(XMLBuilder.wSelf("suppressAutoHyphens"));
2997
+ }
2998
+
2999
+ // 13. CJK paragraph properties
3000
+ if (this.formatting.kinsoku !== undefined) {
3001
+ pPrChildren.push(XMLBuilder.wSelf("kinsoku", { "w:val": this.formatting.kinsoku ? "1" : "0" }));
3002
+ }
3003
+ if (this.formatting.wordWrap !== undefined) {
3004
+ pPrChildren.push(XMLBuilder.wSelf("wordWrap", { "w:val": this.formatting.wordWrap ? "1" : "0" }));
3005
+ }
3006
+ if (this.formatting.overflowPunct !== undefined) {
3007
+ pPrChildren.push(XMLBuilder.wSelf("overflowPunct", { "w:val": this.formatting.overflowPunct ? "1" : "0" }));
3008
+ }
3009
+ if (this.formatting.topLinePunct !== undefined) {
3010
+ pPrChildren.push(XMLBuilder.wSelf("topLinePunct", { "w:val": this.formatting.topLinePunct ? "1" : "0" }));
3011
+ }
3012
+ if (this.formatting.autoSpaceDE !== undefined) {
3013
+ pPrChildren.push(XMLBuilder.wSelf("autoSpaceDE", { "w:val": this.formatting.autoSpaceDE ? "1" : "0" }));
3014
+ }
3015
+ if (this.formatting.autoSpaceDN !== undefined) {
3016
+ pPrChildren.push(XMLBuilder.wSelf("autoSpaceDN", { "w:val": this.formatting.autoSpaceDN ? "1" : "0" }));
3017
+ }
3018
+
3019
+ // 14. Bidirectional layout
3020
+ if (this.formatting.bidi !== undefined) {
3021
+ pPrChildren.push(
3022
+ XMLBuilder.wSelf("bidi", { "w:val": this.formatting.bidi ? "1" : "0" })
3023
+ );
3024
+ }
3025
+
3026
+ // 15. Auto-adjust right indent
3027
+ if (this.formatting.adjustRightInd !== undefined) {
3028
+ pPrChildren.push(
3029
+ XMLBuilder.wSelf("adjustRightInd", {
3030
+ "w:val": this.formatting.adjustRightInd ? "1" : "0",
3031
+ })
3032
+ );
3033
+ }
3034
+
3035
+ // 16. Spacing (before/after/line)
3036
+ if (this.formatting.spacing) {
3037
+ const spc = this.formatting.spacing;
3038
+ const attributes: Record<string, number | string> = {};
3039
+ if (spc.before !== undefined) attributes["w:before"] = spc.before;
3040
+ if (spc.after !== undefined) attributes["w:after"] = spc.after;
3041
+ if (spc.line !== undefined) attributes["w:line"] = spc.line;
3042
+ if (spc.lineRule) attributes["w:lineRule"] = spc.lineRule;
3043
+ if (Object.keys(attributes).length > 0) {
3044
+ pPrChildren.push(XMLBuilder.wSelf("spacing", attributes));
3045
+ }
3046
+ }
3047
+
3048
+ // 17. Indentation (left/right/firstLine/hanging)
3049
+ if (this.formatting.indentation) {
3050
+ const ind = this.formatting.indentation;
3051
+ const attributes: Record<string, number> = {};
3052
+ if (ind.left !== undefined) attributes["w:left"] = ind.left;
3053
+ if (ind.right !== undefined) attributes["w:right"] = ind.right;
3054
+ if (ind.firstLine !== undefined)
3055
+ attributes["w:firstLine"] = ind.firstLine;
3056
+ if (ind.hanging !== undefined) attributes["w:hanging"] = ind.hanging;
3057
+ if (Object.keys(attributes).length > 0) {
3058
+ pPrChildren.push(XMLBuilder.wSelf("ind", attributes));
3059
+ }
3060
+ }
3061
+
3062
+ // 18. Contextual spacing
3063
+ if (this.formatting.contextualSpacing) {
3064
+ pPrChildren.push(XMLBuilder.wSelf("contextualSpacing", { "w:val": "1" }));
3065
+ }
3066
+
3067
+ // 19. Mirror indents
3068
+ if (this.formatting.mirrorIndents) {
3069
+ pPrChildren.push(XMLBuilder.wSelf("mirrorIndents"));
3070
+ }
3071
+
3072
+ // 20. Suppress text frame overlap
3073
+ if (this.formatting.suppressOverlap) {
3074
+ pPrChildren.push(XMLBuilder.wSelf("suppressOverlap"));
3075
+ }
3076
+
3077
+ // 21. Justification/Alignment
3078
+ if (this.formatting.alignment) {
3079
+ const alignmentValue =
3080
+ this.formatting.alignment === "justify"
3081
+ ? "both"
3082
+ : this.formatting.alignment;
3083
+ pPrChildren.push(XMLBuilder.wSelf("jc", { "w:val": alignmentValue }));
3084
+ }
3085
+
3086
+ // 22. Text direction
3087
+ if (this.formatting.textDirection) {
3088
+ pPrChildren.push(
3089
+ XMLBuilder.wSelf("textDirection", {
3090
+ "w:val": this.formatting.textDirection,
3091
+ })
3092
+ );
3093
+ }
3094
+
3095
+ // 23. Text vertical alignment
3096
+ if (this.formatting.textAlignment) {
3097
+ pPrChildren.push(
3098
+ XMLBuilder.wSelf("textAlignment", {
3099
+ "w:val": this.formatting.textAlignment,
3100
+ })
3101
+ );
3102
+ }
3103
+
3104
+ // 24. Textbox tight wrap
3105
+ if (this.formatting.textboxTightWrap) {
3106
+ pPrChildren.push(
3107
+ XMLBuilder.wSelf("textboxTightWrap", {
3108
+ "w:val": this.formatting.textboxTightWrap,
3109
+ })
3110
+ );
3111
+ }
3112
+
3113
+ // 25. Outline level
3114
+ if (this.formatting.outlineLevel !== undefined) {
3115
+ pPrChildren.push(
3116
+ XMLBuilder.wSelf("outlineLvl", {
3117
+ "w:val": this.formatting.outlineLevel.toString(),
3118
+ })
3119
+ );
3120
+ }
3121
+
3122
+ // 17. HTML div ID per ECMA-376 Part 1 §17.3.1.9
3123
+ if (this.formatting.divId !== undefined) {
3124
+ pPrChildren.push(
3125
+ XMLBuilder.wSelf("divId", { "w:val": this.formatting.divId.toString() })
3126
+ );
3127
+ }
3128
+
3129
+ // 18. Conditional table style formatting per ECMA-376 Part 1 §17.3.1.8
3130
+ if (this.formatting.cnfStyle) {
3131
+ pPrChildren.push(
3132
+ XMLBuilder.wSelf("cnfStyle", { "w:val": this.formatting.cnfStyle })
3133
+ );
3134
+ }
3135
+
3136
+ // 19. Paragraph mark run properties per ECMA-376 Part 1 §17.3.1.29
3137
+ // Per CT_PPr, w:rPr comes after all CT_PPrBase elements and before w:sectPr/w:pPrChange
3138
+ if (
3139
+ this.formatting.paragraphMarkRunProperties ||
3140
+ this.formatting.paragraphMarkDeletion
3141
+ ) {
3142
+ const rPrChildren: XMLElement[] = [];
3143
+
3144
+ // Add run properties for the paragraph mark if they exist
3145
+ if (this.formatting.paragraphMarkRunProperties) {
3146
+ const rPr = Run.generateRunPropertiesXML(
3147
+ this.formatting.paragraphMarkRunProperties
3148
+ );
3149
+ if (rPr?.children) {
3150
+ // Filter to only XMLElement types (children can be XMLElement or string)
3151
+ for (const child of rPr.children) {
3152
+ if (typeof child !== "string") {
3153
+ rPrChildren.push(child);
3154
+ }
3155
+ }
3156
+ }
3157
+ }
3158
+
3159
+ // Add deletion marker if the paragraph mark is deleted (w:del)
3160
+ // Per ECMA-376 Part 1 §17.13.5.14 - tracks deletion of paragraph mark
3161
+ if (this.formatting.paragraphMarkDeletion) {
3162
+ const del = this.formatting.paragraphMarkDeletion;
3163
+ rPrChildren.push(
3164
+ XMLBuilder.wSelf("del", {
3165
+ "w:id": del.id.toString(),
3166
+ "w:author": del.author,
3167
+ "w:date": formatDateForXml(del.date),
3168
+ })
3169
+ );
3170
+ }
3171
+
3172
+ // Add w:rPr element if there are any run properties
3173
+ if (rPrChildren.length > 0) {
3174
+ pPrChildren.push(XMLBuilder.w("rPr", undefined, rPrChildren));
3175
+ }
3176
+ }
3177
+
3178
+ // 20. Paragraph property change tracking per ECMA-376 Part 1 §17.3.1.27
3179
+ /**
3180
+ * Per OOXML spec, w:pPrChange contains:
3181
+ * - Attributes: w:id (required), w:author (required), w:date (optional)
3182
+ * - Child element: w:pPr containing the PREVIOUS paragraph properties before the change
3183
+ *
3184
+ * Structure:
3185
+ * <w:pPrChange w:id="1" w:author="Author" w:date="2024-01-01T12:00:00Z">
3186
+ * <w:pPr>
3187
+ * <!-- Previous paragraph properties -->
3188
+ * </w:pPr>
3189
+ * </w:pPrChange>
3190
+ */
3191
+ // Per CT_PPr: sectPr comes BEFORE pPrChange
3192
+ if (this.formatting.sectPr) {
3193
+ if (typeof this.formatting.sectPr === 'string') {
3194
+ // Raw XML passthrough for inline sectPr (preserves exact structure)
3195
+ pPrChildren.push({
3196
+ name: "__rawXml",
3197
+ rawXml: this.formatting.sectPr,
3198
+ } as XMLElement);
3199
+ }
3200
+ // Non-string (parsed object) is skipped to prevent corruption from
3201
+ // XMLBuilder.wSelf treating complex objects as flat attributes
3202
+ }
3203
+
3204
+ if (this.formatting.pPrChange) {
3205
+ const change = this.formatting.pPrChange;
3206
+ const attrs: Record<string, string> = {};
3207
+ // ECMA-376 attribute order: w:id (required), w:author (required), w:date (optional)
3208
+ if (change.id) attrs["w:id"] = change.id;
3209
+ if (change.author) attrs["w:author"] = change.author;
3210
+ if (change.date) attrs["w:date"] = change.date;
3211
+
3212
+ // Build child w:pPr element with previous properties
3213
+ // Per CT_PPrBase schema order: pStyle, keepNext, keepLines, pageBreakBefore,
3214
+ // widowControl, numPr, suppressLineNumbers, pBdr, shd, tabs,
3215
+ // suppressAutoHyphens, bidi, adjustRightInd, spacing, ind,
3216
+ // contextualSpacing, mirrorIndents, jc, textDirection, textAlignment, outlineLvl
3217
+ const prevPPrChildren: XMLElement[] = [];
3218
+ if (change.previousProperties) {
3219
+ const prev = change.previousProperties;
3220
+
3221
+ // 1. pStyle
3222
+ if (prev.style) {
3223
+ prevPPrChildren.push(
3224
+ XMLBuilder.wSelf("pStyle", { "w:val": prev.style })
3225
+ );
3226
+ }
3227
+
3228
+ // 2. keepNext
3229
+ if (prev.keepNext !== undefined) {
3230
+ prevPPrChildren.push(
3231
+ XMLBuilder.wSelf(
3232
+ "keepNext",
3233
+ prev.keepNext ? { "w:val": "1" } : { "w:val": "0" }
3234
+ )
3235
+ );
3236
+ }
3237
+
3238
+ // 3. keepLines
3239
+ if (prev.keepLines !== undefined) {
3240
+ prevPPrChildren.push(
3241
+ XMLBuilder.wSelf(
3242
+ "keepLines",
3243
+ prev.keepLines ? { "w:val": "1" } : { "w:val": "0" }
3244
+ )
3245
+ );
3246
+ }
3247
+
3248
+ // 4. pageBreakBefore
3249
+ if (prev.pageBreakBefore !== undefined) {
3250
+ prevPPrChildren.push(
3251
+ XMLBuilder.wSelf(
3252
+ "pageBreakBefore",
3253
+ prev.pageBreakBefore ? { "w:val": "1" } : { "w:val": "0" }
3254
+ )
3255
+ );
3256
+ }
3257
+
3258
+ // 5. widowControl
3259
+ if (prev.widowControl !== undefined) {
3260
+ prevPPrChildren.push(
3261
+ XMLBuilder.wSelf("widowControl", {
3262
+ "w:val": prev.widowControl ? "1" : "0",
3263
+ })
3264
+ );
3265
+ }
3266
+
3267
+ // 6. numPr
3268
+ if (prev.numbering) {
3269
+ const numPrChildren: XMLElement[] = [];
3270
+ if (prev.numbering.level !== undefined) {
3271
+ numPrChildren.push(
3272
+ XMLBuilder.wSelf("ilvl", { "w:val": prev.numbering.level.toString() })
3273
+ );
3274
+ }
3275
+ if (prev.numbering.numId !== undefined) {
3276
+ numPrChildren.push(
3277
+ XMLBuilder.wSelf("numId", { "w:val": prev.numbering.numId.toString() })
3278
+ );
3279
+ }
3280
+ if (numPrChildren.length > 0) {
3281
+ prevPPrChildren.push(XMLBuilder.w("numPr", undefined, numPrChildren));
3282
+ }
3283
+ }
3284
+
3285
+ // 7. suppressLineNumbers
3286
+ if (prev.suppressLineNumbers !== undefined) {
3287
+ if (prev.suppressLineNumbers) {
3288
+ prevPPrChildren.push(XMLBuilder.wSelf("suppressLineNumbers"));
3289
+ }
3290
+ }
3291
+
3292
+ // 8. pBdr (paragraph borders)
3293
+ if (prev.borders) {
3294
+ const borderChildren: XMLElement[] = [];
3295
+ const borderSides = ["top", "left", "bottom", "right", "between", "bar"] as const;
3296
+ for (const side of borderSides) {
3297
+ const border = prev.borders[side];
3298
+ if (border) {
3299
+ const attrs: Record<string, string> = {};
3300
+ if (border.style) attrs["w:val"] = border.style;
3301
+ if (border.size !== undefined) attrs["w:sz"] = border.size.toString();
3302
+ if (border.color) attrs["w:color"] = border.color;
3303
+ if (border.space !== undefined) attrs["w:space"] = border.space.toString();
3304
+ if (Object.keys(attrs).length > 0) {
3305
+ borderChildren.push(XMLBuilder.wSelf(side, attrs));
3306
+ }
3307
+ }
3308
+ }
3309
+ if (borderChildren.length > 0) {
3310
+ prevPPrChildren.push(XMLBuilder.w("pBdr", undefined, borderChildren));
3311
+ }
3312
+ }
3313
+
3314
+ // 9. shd (paragraph shading)
3315
+ if (prev.shading) {
3316
+ const shdAttrs = buildShadingAttributes(prev.shading);
3317
+ if (Object.keys(shdAttrs).length > 0) {
3318
+ prevPPrChildren.push(XMLBuilder.wSelf("shd", shdAttrs));
3319
+ }
3320
+ }
3321
+
3322
+ // 10. tabs
3323
+ if (prev.tabs && prev.tabs.length > 0) {
3324
+ const tabChildren: XMLElement[] = prev.tabs.map((tab) => {
3325
+ const tabAttrs: Record<string, string> = {
3326
+ "w:pos": tab.position.toString(),
3327
+ };
3328
+ if (tab.val) tabAttrs["w:val"] = tab.val;
3329
+ if (tab.leader) tabAttrs["w:leader"] = tab.leader;
3330
+ return XMLBuilder.wSelf("tab", tabAttrs);
3331
+ });
3332
+ prevPPrChildren.push(XMLBuilder.w("tabs", undefined, tabChildren));
3333
+ }
3334
+
3335
+ // 11. suppressAutoHyphens
3336
+ if (prev.suppressAutoHyphens !== undefined) {
3337
+ if (prev.suppressAutoHyphens) {
3338
+ prevPPrChildren.push(
3339
+ XMLBuilder.wSelf("suppressAutoHyphens", { "w:val": "1" })
3340
+ );
3341
+ }
3342
+ }
3343
+
3344
+ // 12. bidi
3345
+ if (prev.bidi !== undefined) {
3346
+ prevPPrChildren.push(
3347
+ XMLBuilder.wSelf("bidi", { "w:val": prev.bidi ? "1" : "0" })
3348
+ );
3349
+ }
3350
+
3351
+ // 13. adjustRightInd
3352
+ if (prev.adjustRightInd !== undefined) {
3353
+ prevPPrChildren.push(
3354
+ XMLBuilder.wSelf("adjustRightInd", {
3355
+ "w:val": prev.adjustRightInd ? "1" : "0",
3356
+ })
3357
+ );
3358
+ }
3359
+
3360
+ // 14. spacing
3361
+ if (prev.spacing) {
3362
+ const spacingAttrs: Record<string, string> = {};
3363
+ if (prev.spacing.before !== undefined)
3364
+ spacingAttrs["w:before"] = prev.spacing.before.toString();
3365
+ if (prev.spacing.after !== undefined)
3366
+ spacingAttrs["w:after"] = prev.spacing.after.toString();
3367
+ if (prev.spacing.line !== undefined)
3368
+ spacingAttrs["w:line"] = prev.spacing.line.toString();
3369
+ if (prev.spacing.lineRule)
3370
+ spacingAttrs["w:lineRule"] = prev.spacing.lineRule;
3371
+ if (Object.keys(spacingAttrs).length > 0) {
3372
+ prevPPrChildren.push(XMLBuilder.wSelf("spacing", spacingAttrs));
3373
+ }
3374
+ }
3375
+
3376
+ // 15. ind (indentation)
3377
+ if (prev.indentation) {
3378
+ const indAttrs: Record<string, string> = {};
3379
+ if (prev.indentation.left !== undefined)
3380
+ indAttrs["w:left"] = prev.indentation.left.toString();
3381
+ if (prev.indentation.right !== undefined)
3382
+ indAttrs["w:right"] = prev.indentation.right.toString();
3383
+ if (prev.indentation.firstLine !== undefined)
3384
+ indAttrs["w:firstLine"] = prev.indentation.firstLine.toString();
3385
+ if (prev.indentation.hanging !== undefined)
3386
+ indAttrs["w:hanging"] = prev.indentation.hanging.toString();
3387
+ prevPPrChildren.push(XMLBuilder.wSelf("ind", indAttrs));
3388
+ }
3389
+
3390
+ // 16. contextualSpacing
3391
+ if (prev.contextualSpacing !== undefined) {
3392
+ prevPPrChildren.push(
3393
+ XMLBuilder.wSelf("contextualSpacing", {
3394
+ "w:val": prev.contextualSpacing ? "1" : "0",
3395
+ })
3396
+ );
3397
+ }
3398
+
3399
+ // 17. mirrorIndents
3400
+ if (prev.mirrorIndents !== undefined) {
3401
+ prevPPrChildren.push(
3402
+ XMLBuilder.wSelf("mirrorIndents", {
3403
+ "w:val": prev.mirrorIndents ? "1" : "0",
3404
+ })
3405
+ );
3406
+ }
3407
+
3408
+ // 18. jc (alignment)
3409
+ if (prev.alignment) {
3410
+ // Map 'justify' to 'both' per ECMA-376 ST_Jc enumeration
3411
+ const alignmentValue =
3412
+ prev.alignment === "justify" ? "both" : prev.alignment;
3413
+ prevPPrChildren.push(
3414
+ XMLBuilder.wSelf("jc", { "w:val": alignmentValue })
3415
+ );
3416
+ }
3417
+
3418
+ // 19. textDirection
3419
+ if (prev.textDirection) {
3420
+ prevPPrChildren.push(
3421
+ XMLBuilder.wSelf("textDirection", { "w:val": prev.textDirection })
3422
+ );
3423
+ }
3424
+
3425
+ // 20. textAlignment
3426
+ if (prev.textAlignment) {
3427
+ prevPPrChildren.push(
3428
+ XMLBuilder.wSelf("textAlignment", { "w:val": prev.textAlignment })
3429
+ );
3430
+ }
3431
+
3432
+ // 21. outlineLvl
3433
+ if (prev.outlineLevel !== undefined) {
3434
+ prevPPrChildren.push(
3435
+ XMLBuilder.wSelf("outlineLvl", { "w:val": prev.outlineLevel.toString() })
3436
+ );
3437
+ }
3438
+ }
3439
+
3440
+ // Create w:pPrChange element with child w:pPr
3441
+ // Per ECMA-376 Part 1 §17.13.5.29, w:pPrChange MUST contain a w:pPr child element.
3442
+ // Only output pPrChange if we have properties to include in w:pPr.
3443
+ // Empty pPrChange elements cause Word to report "unreadable content" corruption.
3444
+ if (prevPPrChildren.length > 0) {
3445
+ const pPrChangeChildren: XMLElement[] = [{
3446
+ name: "w:pPr",
3447
+ attributes: {},
3448
+ children: prevPPrChildren,
3449
+ }];
3450
+
3451
+ pPrChildren.push({
3452
+ name: "w:pPrChange",
3453
+ attributes: attrs,
3454
+ children: pPrChangeChildren,
3455
+ });
3456
+ }
3457
+ // If no previous properties to record, skip w:pPrChange entirely to avoid corruption
3458
+ }
3459
+
3460
+ // Build paragraph element
3461
+ const paragraphChildren: XMLElement[] = [];
3462
+
3463
+ // Add paragraph properties if there are any
3464
+ if (pPrChildren.length > 0) {
3465
+ paragraphChildren.push(XMLBuilder.w("pPr", undefined, pPrChildren));
3466
+ }
3467
+
3468
+ // Add bookmark start markers
3469
+ for (const bookmark of this.bookmarksStart) {
3470
+ paragraphChildren.push(bookmark.toStartXML());
3471
+ }
3472
+
3473
+ // Add comment range start markers
3474
+ for (const comment of this.commentsStart) {
3475
+ paragraphChildren.push(comment.toRangeStartXML());
3476
+ }
3477
+
3478
+ // Add content (runs, fields, hyperlinks, revisions, range markers, shapes, text boxes)
3479
+ for (let i = 0; i < this.content.length; i++) {
3480
+ const item = this.content[i];
3481
+ if (item instanceof Field) {
3482
+ // Simple Field — fldSimple is a paragraph-level element containing w:r children
3483
+ paragraphChildren.push(item.toXML());
3484
+ } else if (item instanceof ComplexField) {
3485
+ // ComplexField returns array of runs - spread them directly into paragraphChildren
3486
+ const fieldXml = item.toXML();
3487
+ if (Array.isArray(fieldXml)) {
3488
+ paragraphChildren.push(...fieldXml);
3489
+ } else {
3490
+ // Fallback if toXML() doesn't return array
3491
+ paragraphChildren.push(XMLBuilder.w("r", undefined, [fieldXml]));
3492
+ }
3493
+ } else if (item instanceof Hyperlink) {
3494
+ // Hyperlinks are their own element
3495
+ paragraphChildren.push(item.toXML());
3496
+ } else if (item instanceof Revision) {
3497
+ // Revisions (track changes) are their own element
3498
+ // Property change types (rPrChange, pPrChange, tblPrChange, etc.) are only valid
3499
+ // inside their respective property elements (w:rPr, w:pPr, etc.), not as direct
3500
+ // children of w:p. Skip them here to avoid producing invalid XML.
3501
+ const revType = item.getType();
3502
+ if (
3503
+ revType === 'runPropertiesChange' ||
3504
+ revType === 'paragraphPropertiesChange' ||
3505
+ revType === 'tablePropertiesChange' ||
3506
+ revType === 'tableExceptionPropertiesChange' ||
3507
+ revType === 'tableRowPropertiesChange' ||
3508
+ revType === 'tableCellPropertiesChange' ||
3509
+ revType === 'sectionPropertiesChange' ||
3510
+ revType === 'numberingChange'
3511
+ ) {
3512
+ continue;
3513
+ }
3514
+ // Note: toXML() returns null for internal-only revision types (e.g., hyperlinkChange)
3515
+ const revisionXml = item.toXML();
3516
+ if (revisionXml) {
3517
+ paragraphChildren.push(revisionXml);
3518
+ }
3519
+ } else if (item instanceof RangeMarker) {
3520
+ // Range markers are their own element (mark boundaries of moves, etc.)
3521
+ paragraphChildren.push(item.toXML());
3522
+ } else if (item instanceof Shape) {
3523
+ // Shapes are wrapped in a run
3524
+ paragraphChildren.push(XMLBuilder.w("r", undefined, [item.toXML()]));
3525
+ } else if (item instanceof TextBox) {
3526
+ // Text boxes are wrapped in a run
3527
+ paragraphChildren.push(XMLBuilder.w("r", undefined, [item.toXML()]));
3528
+ } else if (item) {
3529
+ paragraphChildren.push(item.toXML());
3530
+ }
3531
+ }
3532
+
3533
+ // If no content, add empty run to prevent invalid XML
3534
+ if (this.content.length === 0) {
3535
+ paragraphChildren.push(new Run("").toXML());
3536
+ }
3537
+
3538
+ // Add comment range end markers
3539
+ for (const comment of this.commentsEnd) {
3540
+ paragraphChildren.push(comment.toRangeEndXML());
3541
+ }
3542
+
3543
+ // Add comment references (must come after range end)
3544
+ for (const comment of this.commentsEnd) {
3545
+ paragraphChildren.push(comment.toReferenceXML());
3546
+ }
3547
+
3548
+ // Add bookmark end markers
3549
+ for (const bookmark of this.bookmarksEnd) {
3550
+ paragraphChildren.push(bookmark.toEndXML());
3551
+ }
3552
+
3553
+ // Add paragraph-level attributes (Word 2010+ requires w14:paraId)
3554
+ const paragraphAttributes: Record<string, string> = {};
3555
+ if (this.formatting.paraId) {
3556
+ paragraphAttributes["w14:paraId"] = this.formatting.paraId;
3557
+ }
3558
+
3559
+ return XMLBuilder.w(
3560
+ "p",
3561
+ Object.keys(paragraphAttributes).length > 0
3562
+ ? paragraphAttributes
3563
+ : undefined,
3564
+ paragraphChildren
3565
+ );
3566
+ }
3567
+
3568
+ /**
3569
+ * Gets the word count for this paragraph
3570
+ *
3571
+ * Counts words by splitting text on whitespace and filtering empty strings.
3572
+ *
3573
+ * @returns Number of words in the paragraph
3574
+ *
3575
+ * @example
3576
+ * ```typescript
3577
+ * const count = para.getWordCount();
3578
+ * console.log(`Paragraph has ${count} words`);
3579
+ * ```
3580
+ */
3581
+ getWordCount(): number {
3582
+ const text = this.getText().trim();
3583
+ if (!text) return 0;
3584
+
3585
+ // Split by whitespace and filter out empty strings
3586
+ const words = text.split(/\s+/).filter((word) => word.length > 0);
3587
+ return words.length;
3588
+ }
3589
+
3590
+ /**
3591
+ * Gets the character count for this paragraph
3592
+ *
3593
+ * Counts all characters including or excluding whitespace.
3594
+ *
3595
+ * @param includeSpaces - If true, includes spaces; if false, excludes them (default: true)
3596
+ * @returns Number of characters in the paragraph
3597
+ *
3598
+ * @example
3599
+ * ```typescript
3600
+ * const withSpaces = para.getLength(); // Includes spaces
3601
+ * const noSpaces = para.getLength(false); // Excludes spaces
3602
+ * console.log(`${withSpaces} chars (${noSpaces} without spaces)`);
3603
+ * ```
3604
+ */
3605
+ getLength(includeSpaces = true): number {
3606
+ const text = this.getText();
3607
+ if (includeSpaces) {
3608
+ return text.length;
3609
+ } else {
3610
+ return text.replace(/\s/g, "").length;
3611
+ }
3612
+ }
3613
+
3614
+ /**
3615
+ * Creates a deep clone of this paragraph
3616
+ *
3617
+ * Creates a new Paragraph with copies of all content, formatting,
3618
+ * bookmarks, and comments. The clone is independent of the original.
3619
+ *
3620
+ * @returns A new Paragraph instance with the same content and formatting
3621
+ *
3622
+ * @example
3623
+ * ```typescript
3624
+ * const original = new Paragraph();
3625
+ * original.addText('Template text', { bold: true });
3626
+ * original.setStyle('Heading1');
3627
+ *
3628
+ * const copy = original.clone();
3629
+ * copy.addText(' - modified'); // Original unchanged
3630
+ * ```
3631
+ */
3632
+ clone(): Paragraph {
3633
+ // Clone the formatting
3634
+ const clonedFormatting: ParagraphFormatting = deepClone(this.formatting);
3635
+
3636
+ // Create new paragraph with cloned formatting
3637
+ const clonedParagraph = new Paragraph(clonedFormatting);
3638
+
3639
+ // Clone all content (runs, fields, hyperlinks, revisions)
3640
+ for (const item of this.content) {
3641
+ if (item instanceof Run) {
3642
+ // Clone the run with its text and formatting
3643
+ const runFormatting = item.getFormatting();
3644
+ const clonedRun = new Run(item.getText(), deepClone(runFormatting));
3645
+ clonedParagraph.addRun(clonedRun);
3646
+ } else {
3647
+ // For other content types, add them as-is (shallow copy for now)
3648
+ // In a more complete implementation, we'd clone these too
3649
+ clonedParagraph.content.push(item);
3650
+ }
3651
+ }
3652
+
3653
+ // Clone bookmark and comment markers
3654
+ clonedParagraph.bookmarksStart = [...this.bookmarksStart];
3655
+ clonedParagraph.bookmarksEnd = [...this.bookmarksEnd];
3656
+ clonedParagraph.commentsStart = [...this.commentsStart];
3657
+ clonedParagraph.commentsEnd = [...this.commentsEnd];
3658
+
3659
+ return clonedParagraph;
3660
+ }
3661
+
3662
+ /**
3663
+ * Sets paragraph borders
3664
+ * @param borders - Border definitions for each side
3665
+ * @returns This paragraph for chaining
3666
+ * @example
3667
+ * ```typescript
3668
+ * para.setBorder({
3669
+ * top: { style: 'single', size: 4, color: '000000', space: 1 },
3670
+ * bottom: { style: 'single', size: 4, color: '000000', space: 1 }
3671
+ * });
3672
+ * ```
3673
+ */
3674
+ setBorder(borders: {
3675
+ top?: BorderDefinition;
3676
+ bottom?: BorderDefinition;
3677
+ left?: BorderDefinition;
3678
+ right?: BorderDefinition;
3679
+ between?: BorderDefinition;
3680
+ bar?: BorderDefinition;
3681
+ }): this {
3682
+ if (!this.formatting) {
3683
+ this.formatting = {};
3684
+ }
3685
+
3686
+ this.formatting.borders = borders;
3687
+
3688
+ return this;
3689
+ }
3690
+
3691
+ /**
3692
+ * Sets paragraph shading (background color and pattern)
3693
+ * @param shading - Shading options
3694
+ * @returns This paragraph for chaining
3695
+ * @example
3696
+ * ```typescript
3697
+ * // Solid background
3698
+ * para.setShading({ fill: 'FFFF00', pattern: 'solid' });
3699
+ *
3700
+ * // Pattern with colors
3701
+ * para.setShading({
3702
+ * fill: 'FFFF00',
3703
+ * color: '000000',
3704
+ * pattern: 'diagStripe'
3705
+ * });
3706
+ * ```
3707
+ */
3708
+ setShading(shading: ShadingConfig): this {
3709
+ if (!this.formatting) {
3710
+ this.formatting = {};
3711
+ }
3712
+
3713
+ const previousValue = this.formatting.shading;
3714
+ this.formatting.shading = shading;
3715
+
3716
+ if (this.trackingContext?.isEnabled() && previousValue !== shading) {
3717
+ this.trackingContext.trackParagraphPropertyChange(this, 'shading', previousValue, shading);
3718
+ }
3719
+
3720
+ return this;
3721
+ }
3722
+
3723
+ /**
3724
+ * Sets tab stops for the paragraph
3725
+ * @param tabs - Array of tab stop definitions
3726
+ * @returns This paragraph for chaining
3727
+ * @example
3728
+ * ```typescript
3729
+ * para.setTabs([
3730
+ * { position: 720, val: 'left' },
3731
+ * { position: 1440, val: 'center', leader: 'dot' },
3732
+ * { position: 2160, val: 'right' }
3733
+ * ]);
3734
+ * ```
3735
+ */
3736
+ setTabs(tabs: TabStop[]): this {
3737
+ if (!this.formatting) {
3738
+ this.formatting = {};
3739
+ }
3740
+
3741
+ this.formatting.tabs = tabs;
3742
+
3743
+ return this;
3744
+ }
3745
+
3746
+ /**
3747
+ * Inserts a run at a specific position
3748
+ * @param index - Position to insert at (0-based)
3749
+ * @param run - Run to insert
3750
+ * @returns This paragraph for chaining
3751
+ * @example
3752
+ * ```typescript
3753
+ * const para = new Paragraph();
3754
+ * const run = new Run('Inserted', { bold: true });
3755
+ * para.insertRunAt(0, run);
3756
+ * ```
3757
+ */
3758
+ insertRunAt(index: number, run: Run): this {
3759
+ if (index < 0) index = 0;
3760
+ if (index > this.content.length) index = this.content.length;
3761
+
3762
+ this.content.splice(index, 0, run);
3763
+ return this;
3764
+ }
3765
+
3766
+ /**
3767
+ * Removes a run at a specific position
3768
+ * @param index - Position to remove (0-based)
3769
+ * @returns True if removed, false if index invalid
3770
+ * @example
3771
+ * ```typescript
3772
+ * para.removeRunAt(2); // Remove third run
3773
+ * ```
3774
+ */
3775
+ removeRunAt(index: number): boolean {
3776
+ if (index >= 0 && index < this.content.length) {
3777
+ const item = this.content[index];
3778
+ if (item instanceof Run) {
3779
+ this.content.splice(index, 1);
3780
+ return true;
3781
+ }
3782
+ }
3783
+ return false;
3784
+ }
3785
+
3786
+ /**
3787
+ * Replaces a run at a specific position
3788
+ * @param index - Position to replace (0-based)
3789
+ * @param run - New run
3790
+ * @returns True if replaced, false if index invalid or not a run
3791
+ * @example
3792
+ * ```typescript
3793
+ * const newRun = new Run('Replacement', { italic: true });
3794
+ * para.replaceRunAt(1, newRun);
3795
+ * ```
3796
+ */
3797
+ replaceRunAt(index: number, run: Run): boolean {
3798
+ if (index >= 0 && index < this.content.length) {
3799
+ const item = this.content[index];
3800
+ if (item instanceof Run) {
3801
+ this.content[index] = run;
3802
+ return true;
3803
+ }
3804
+ }
3805
+ return false;
3806
+ }
3807
+
3808
+ /**
3809
+ * Finds text within the paragraph and returns run indices
3810
+ *
3811
+ * Searches through all runs in the paragraph and returns the indices
3812
+ * of runs that contain the search text.
3813
+ *
3814
+ * @param text - The text to search for
3815
+ * @param options - Optional search configuration
3816
+ * @param options.caseSensitive - If true, match case exactly (default: false)
3817
+ * @param options.wholeWord - If true, match whole words only (default: false)
3818
+ * @returns Array of run indices (0-based) that contain the search text
3819
+ *
3820
+ * @example
3821
+ * ```typescript
3822
+ * const indices = para.findText('important');
3823
+ * console.log(`Found in runs: ${indices.join(', ')}`);
3824
+ *
3825
+ * // Highlight all matching runs
3826
+ * for (const idx of indices) {
3827
+ * const run = para.getRuns()[idx];
3828
+ * run?.setHighlight('yellow');
3829
+ * }
3830
+ * ```
3831
+ */
3832
+ findText(
3833
+ text: string,
3834
+ options?: { caseSensitive?: boolean; wholeWord?: boolean }
3835
+ ): number[] {
3836
+ const indices: number[] = [];
3837
+ const caseSensitive = options?.caseSensitive || false;
3838
+ const wholeWord = options?.wholeWord || false;
3839
+
3840
+ const searchText = caseSensitive ? text : text.toLowerCase();
3841
+
3842
+ for (let i = 0; i < this.content.length; i++) {
3843
+ const item = this.content[i];
3844
+ if (item instanceof Run) {
3845
+ const runText = caseSensitive
3846
+ ? item.getText()
3847
+ : item.getText().toLowerCase();
3848
+
3849
+ if (wholeWord) {
3850
+ // Use word boundary regex
3851
+ const wordPattern = new RegExp(
3852
+ `\\b${searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`
3853
+ );
3854
+ if (wordPattern.test(runText)) {
3855
+ indices.push(i);
3856
+ }
3857
+ } else {
3858
+ // Simple substring search
3859
+ if (runText.includes(searchText)) {
3860
+ indices.push(i);
3861
+ }
3862
+ }
3863
+ }
3864
+ }
3865
+
3866
+ return indices;
3867
+ }
3868
+
3869
+ /**
3870
+ * Replaces text within paragraph runs
3871
+ *
3872
+ * Searches through all runs and replaces matching text while preserving
3873
+ * the original formatting of each run.
3874
+ *
3875
+ * @param find - The text to search for
3876
+ * @param replace - The replacement text
3877
+ * @param options - Optional search configuration
3878
+ * @param options.caseSensitive - If true, match case exactly (default: false)
3879
+ * @param options.wholeWord - If true, match whole words only (default: false)
3880
+ * @returns Number of replacements made
3881
+ *
3882
+ * @example
3883
+ * ```typescript
3884
+ * const count = para.replaceText('color', 'colour');
3885
+ * console.log(`Replaced ${count} occurrences`);
3886
+ * ```
3887
+ *
3888
+ * @example
3889
+ * ```typescript
3890
+ * // Case-sensitive whole word replacement
3891
+ * const count = para.replaceText('Error', 'Warning', {
3892
+ * caseSensitive: true,
3893
+ * wholeWord: true
3894
+ * });
3895
+ * ```
3896
+ */
3897
+ replaceText(
3898
+ find: string,
3899
+ replace: string,
3900
+ options?: { caseSensitive?: boolean; wholeWord?: boolean }
3901
+ ): number {
3902
+ let replacementCount = 0;
3903
+ const caseSensitive = options?.caseSensitive || false;
3904
+ const wholeWord = options?.wholeWord || false;
3905
+
3906
+ for (const item of this.content) {
3907
+ if (item instanceof Run) {
3908
+ const originalText = item.getText();
3909
+ let newText = originalText;
3910
+
3911
+ if (wholeWord) {
3912
+ // Use word boundary regex
3913
+ const wordPattern = new RegExp(
3914
+ `\\b${find.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
3915
+ caseSensitive ? "g" : "gi"
3916
+ );
3917
+ const matches = originalText.match(wordPattern);
3918
+ if (matches) {
3919
+ replacementCount += matches.length;
3920
+ newText = originalText.replace(wordPattern, replace);
3921
+ }
3922
+ } else {
3923
+ // Simple substring replacement
3924
+ const searchPattern = new RegExp(
3925
+ find.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
3926
+ caseSensitive ? "g" : "gi"
3927
+ );
3928
+ const matches = originalText.match(searchPattern);
3929
+ if (matches) {
3930
+ replacementCount += matches.length;
3931
+ newText = originalText.replace(searchPattern, replace);
3932
+ }
3933
+ }
3934
+
3935
+ if (newText !== originalText) {
3936
+ item.setText(newText);
3937
+ }
3938
+ }
3939
+ }
3940
+
3941
+ return replacementCount;
3942
+ }
3943
+
3944
+ /**
3945
+ * Merges another paragraph's content into this one
3946
+ *
3947
+ * Appends all content (runs, fields, hyperlinks), bookmarks, and comments
3948
+ * from another paragraph to this one. The other paragraph is not modified.
3949
+ *
3950
+ * @param otherPara - The paragraph whose content to merge
3951
+ * @returns This paragraph instance for method chaining
3952
+ *
3953
+ * @example
3954
+ * ```typescript
3955
+ * const para1 = new Paragraph().addText('Hello ');
3956
+ * const para2 = new Paragraph().addText('World');
3957
+ * para1.mergeWith(para2); // para1 now contains "Hello World"
3958
+ * ```
3959
+ */
3960
+ mergeWith(otherPara: Paragraph): this {
3961
+ // Add all content from other paragraph
3962
+ for (const item of otherPara.content) {
3963
+ if (item instanceof Run) {
3964
+ this.content.push(item.clone());
3965
+ } else {
3966
+ this.content.push(item);
3967
+ }
3968
+ }
3969
+
3970
+ // Merge bookmarks
3971
+ this.bookmarksStart.push(...otherPara.bookmarksStart);
3972
+ this.bookmarksEnd.push(...otherPara.bookmarksEnd);
3973
+
3974
+ // Merge comments
3975
+ this.commentsStart.push(...otherPara.commentsStart);
3976
+ this.commentsEnd.push(...otherPara.commentsEnd);
3977
+
3978
+ return this;
3979
+ }
3980
+
3981
+ /**
3982
+ * Clears direct run formatting from all runs in this paragraph
3983
+ *
3984
+ * This is useful when applying styles, as direct run formatting takes precedence
3985
+ * over style formatting in Word. By clearing direct formatting, the style's
3986
+ * formatting can take effect.
3987
+ *
3988
+ * @param properties - Optional array of specific properties to clear.
3989
+ * If not specified, clears ALL direct formatting.
3990
+ * Valid properties: 'bold', 'italic', 'underline', 'strike',
3991
+ * 'font', 'size', 'color', 'highlight', 'subscript', 'superscript',
3992
+ * 'smallCaps', 'allCaps', 'dstrike'
3993
+ * @returns This paragraph for chaining
3994
+ * @example
3995
+ * ```typescript
3996
+ * // Clear all direct formatting
3997
+ * para.clearDirectRunFormatting();
3998
+ *
3999
+ * // Clear only font and color
4000
+ * para.clearDirectRunFormatting(['font', 'color']);
4001
+ *
4002
+ * // Apply style and clear all formatting
4003
+ * para.setStyle('Heading1').clearDirectRunFormatting();
4004
+ * ```
4005
+ */
4006
+ clearDirectRunFormatting(properties?: string[]): this {
4007
+ const runs = this.getRuns();
4008
+
4009
+ for (const run of runs) {
4010
+ const formatting = run.getFormatting();
4011
+
4012
+ if (properties && properties.length > 0) {
4013
+ // Clear only specified properties
4014
+ const newFormatting: RunFormatting = { ...formatting };
4015
+ for (const prop of properties) {
4016
+ if (prop in newFormatting) {
4017
+ delete (newFormatting as any)[prop];
4018
+ }
4019
+ }
4020
+
4021
+ // Create new run with cleared formatting
4022
+ const text = run.getText();
4023
+ const newRun = new Run(text, newFormatting);
4024
+
4025
+ // Replace in content array
4026
+ const index = this.content.indexOf(run);
4027
+ if (index !== -1) {
4028
+ this.content[index] = newRun;
4029
+ }
4030
+ } else {
4031
+ // Clear ALL direct formatting - replace with plain text run
4032
+ const text = run.getText();
4033
+ const newRun = new Run(text, {});
4034
+
4035
+ // Replace in content array
4036
+ const index = this.content.indexOf(run);
4037
+ if (index !== -1) {
4038
+ this.content[index] = newRun;
4039
+ }
4040
+ }
4041
+ }
4042
+
4043
+ return this;
4044
+ }
4045
+
4046
+ /**
4047
+ * Applies a style to this paragraph and optionally clears direct run formatting
4048
+ *
4049
+ * In Word, direct run formatting takes precedence over style formatting. This method
4050
+ * provides a way to apply a style while ensuring it takes effect by clearing
4051
+ * conflicting direct formatting.
4052
+ *
4053
+ * @param styleId - Style ID to apply (e.g., 'Normal', 'Heading1')
4054
+ * @param clearProperties - Optional array of run properties to clear.
4055
+ * If not specified, does NOT clear any formatting (style overlay behavior).
4056
+ * Pass empty array [] to clear ALL formatting.
4057
+ * @returns This paragraph for chaining
4058
+ * @example
4059
+ * ```typescript
4060
+ * // Apply Heading1 and clear all direct formatting
4061
+ * para.applyStyleAndClearFormatting('Heading1', []);
4062
+ *
4063
+ * // Apply Normal and clear only font and color
4064
+ * para.applyStyleAndClearFormatting('Normal', ['font', 'color']);
4065
+ *
4066
+ * // Apply Title but keep existing run formatting (overlay style)
4067
+ * para.applyStyleAndClearFormatting('Title');
4068
+ * ```
4069
+ */
4070
+ applyStyleAndClearFormatting(
4071
+ styleId: string,
4072
+ clearProperties?: string[]
4073
+ ): this {
4074
+ // Apply the style
4075
+ this.setStyle(styleId);
4076
+
4077
+ // Clear direct formatting if requested
4078
+ if (clearProperties !== undefined) {
4079
+ this.clearDirectRunFormatting(
4080
+ clearProperties.length === 0 ? undefined : clearProperties
4081
+ );
4082
+ }
4083
+
4084
+ return this;
4085
+ }
4086
+
4087
+ /**
4088
+ * Clears paragraph and run formatting that conflicts with a style definition.
4089
+ * Uses smart clearing per ECMA-376 §17.7.2 formatting hierarchy.
4090
+ *
4091
+ * This is critical because direct formatting in document.xml ALWAYS overrides
4092
+ * style definitions in styles.xml. To make style modifications take effect,
4093
+ * we must remove conflicting direct formatting.
4094
+ *
4095
+ * Strategy:
4096
+ * - Compare paragraph properties with style's paragraph properties
4097
+ * - Clear only properties that DIFFER from the style
4098
+ * - For each run, call run.clearFormattingConflicts() with style's run formatting
4099
+ * - Preserve style reference (pStyle element)
4100
+ *
4101
+ * @param styleDefinition - Style object containing both paragraph and run formatting
4102
+ * @returns This paragraph for method chaining
4103
+ * @example
4104
+ * ```typescript
4105
+ * // Style defines: left alignment, 14pt black Verdana, 6pt spacing
4106
+ * // Paragraph has: center alignment (conflict!), 120 twips spacing (conflict!)
4107
+ * // Runs have: red color (conflict!), 12pt size (conflict!), bold (not in style - keep!)
4108
+ * const style = stylesManager.getStyle('Heading2');
4109
+ * paragraph.clearDirectFormattingConflicts(style);
4110
+ * // Result: Alignment cleared, spacing cleared, runs' color/size cleared, bold preserved
4111
+ * ```
4112
+ */
4113
+ clearDirectFormattingConflicts(styleDefinition: {
4114
+ getProperties(): {
4115
+ paragraphFormatting?: ParagraphFormatting;
4116
+ runFormatting?: RunFormatting;
4117
+ };
4118
+ }): this {
4119
+ const styleProperties = styleDefinition.getProperties();
4120
+ const styleParagraphFormatting = styleProperties.paragraphFormatting || {};
4121
+ const styleRunFormatting = styleProperties.runFormatting || {};
4122
+
4123
+ // Clear conflicting paragraph-level properties
4124
+ // Keep only pStyle (style reference) - clear everything else that conflicts
4125
+ const conflictingParaProps: (keyof ParagraphFormatting)[] = [];
4126
+
4127
+ for (const key in this.formatting) {
4128
+ const propKey = key as keyof ParagraphFormatting;
4129
+
4130
+ // Always preserve style reference
4131
+ if (propKey === "style") {
4132
+ continue;
4133
+ }
4134
+
4135
+ // Skip if style doesn't define this property
4136
+ if (styleParagraphFormatting[propKey] === undefined) {
4137
+ continue;
4138
+ }
4139
+
4140
+ // Special handling for indentation - strip direct formatting so style/numbering values take effect
4141
+ if (propKey === "indentation") {
4142
+ const paraIndent = this.formatting.indentation;
4143
+ const styleIndent = styleParagraphFormatting.indentation;
4144
+
4145
+ // Preserve intentional zero-indent overrides
4146
+ // When style has left indent > 0 and paragraph explicitly sets left to 0,
4147
+ // this is an intentional override to remove the indent - preserve it
4148
+ if (paraIndent?.left === 0 && styleIndent?.left && styleIndent.left > 0) {
4149
+ continue;
4150
+ }
4151
+
4152
+ // Clear direct indentation so style value takes effect (consistent with other properties)
4153
+ conflictingParaProps.push(propKey);
4154
+ continue;
4155
+ }
4156
+
4157
+ // Handle complex objects (spacing, borders, etc.)
4158
+ if (
4159
+ propKey === "spacing" ||
4160
+ propKey === "borders" ||
4161
+ propKey === "shading" ||
4162
+ propKey === "numbering"
4163
+ ) {
4164
+ // Deep comparison for objects
4165
+ const paraValue = this.formatting[propKey];
4166
+ const styleValue = styleParagraphFormatting[propKey];
4167
+
4168
+ if (JSON.stringify(paraValue) !== JSON.stringify(styleValue)) {
4169
+ conflictingParaProps.push(propKey);
4170
+ }
4171
+ } else {
4172
+ // Simple value comparison
4173
+ if (this.formatting[propKey] !== styleParagraphFormatting[propKey]) {
4174
+ conflictingParaProps.push(propKey);
4175
+ }
4176
+ }
4177
+ }
4178
+
4179
+ // Clear conflicting paragraph properties
4180
+ for (const prop of conflictingParaProps) {
4181
+ delete this.formatting[prop];
4182
+ }
4183
+
4184
+ // Clear conflicting run-level properties from all runs
4185
+ for (const item of this.content) {
4186
+ if (item instanceof Run) {
4187
+ item.clearFormattingConflicts(styleRunFormatting);
4188
+ }
4189
+ }
4190
+
4191
+ return this;
4192
+ }
4193
+
4194
+ /**
4195
+ * Clears all direct formatting from this paragraph and its runs
4196
+ *
4197
+ * Removes all direct formatting properties from the paragraph and all its runs,
4198
+ * leaving only the style reference and text content. This is useful for ensuring
4199
+ * paragraphs match their defined style without formatting overrides.
4200
+ *
4201
+ * @returns This paragraph for chaining
4202
+ * @example
4203
+ * ```typescript
4204
+ * paragraph.clearDirectFormatting();
4205
+ * ```
4206
+ */
4207
+ clearDirectFormatting(): this {
4208
+ // Clear paragraph-level formatting (keep only style, numbering, and preserved flag)
4209
+ const style = this.formatting.style;
4210
+ const numbering = this.formatting.numbering;
4211
+
4212
+ this.formatting = {};
4213
+
4214
+ // Restore essential properties
4215
+ if (style) this.formatting.style = style;
4216
+ if (numbering) this.formatting.numbering = numbering;
4217
+
4218
+ // Clear run-level formatting
4219
+ for (const item of this.content) {
4220
+ if (item instanceof Run) {
4221
+ item.clearFormatting();
4222
+ }
4223
+ }
4224
+
4225
+ return this;
4226
+ }
4227
+ }