docxmlater 10.1.3 → 10.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +759 -754
  2. package/dist/constants/legacyCompatFlags.js +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.js.map +1 -1
  5. package/dist/core/Document.d.ts +50 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +483 -471
  8. package/dist/core/Document.js.map +1 -1
  9. package/dist/core/DocumentContent.d.ts +9 -9
  10. package/dist/core/DocumentContent.d.ts.map +1 -1
  11. package/dist/core/DocumentContent.js +1 -1
  12. package/dist/core/DocumentContent.js.map +1 -1
  13. package/dist/core/DocumentGenerator.d.ts +11 -11
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +251 -251
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.js.map +1 -1
  18. package/dist/core/DocumentParser.d.ts +15 -15
  19. package/dist/core/DocumentParser.d.ts.map +1 -1
  20. package/dist/core/DocumentParser.js +2123 -2155
  21. package/dist/core/DocumentParser.js.map +1 -1
  22. package/dist/core/DocumentValidator.d.ts.map +1 -1
  23. package/dist/core/DocumentValidator.js +2 -5
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.js.map +1 -1
  26. package/dist/core/RelationshipManager.d.ts.map +1 -1
  27. package/dist/core/RelationshipManager.js +3 -3
  28. package/dist/core/RelationshipManager.js.map +1 -1
  29. package/dist/elements/AlternateContent.js.map +1 -1
  30. package/dist/elements/Bookmark.d.ts.map +1 -1
  31. package/dist/elements/Bookmark.js +3 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.d.ts.map +1 -1
  36. package/dist/elements/Comment.js +9 -6
  37. package/dist/elements/Comment.js.map +1 -1
  38. package/dist/elements/CommentManager.d.ts.map +1 -1
  39. package/dist/elements/CommentManager.js +18 -17
  40. package/dist/elements/CommentManager.js.map +1 -1
  41. package/dist/elements/CommonTypes.d.ts +21 -21
  42. package/dist/elements/CommonTypes.d.ts.map +1 -1
  43. package/dist/elements/CommonTypes.js +56 -56
  44. package/dist/elements/CommonTypes.js.map +1 -1
  45. package/dist/elements/CustomXml.js.map +1 -1
  46. package/dist/elements/Endnote.d.ts.map +1 -1
  47. package/dist/elements/Endnote.js +6 -6
  48. package/dist/elements/Endnote.js.map +1 -1
  49. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  50. package/dist/elements/EndnoteManager.js +6 -7
  51. package/dist/elements/EndnoteManager.js.map +1 -1
  52. package/dist/elements/Field.d.ts.map +1 -1
  53. package/dist/elements/Field.js +82 -25
  54. package/dist/elements/Field.js.map +1 -1
  55. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  56. package/dist/elements/FieldHelpers.js.map +1 -1
  57. package/dist/elements/FontManager.d.ts.map +1 -1
  58. package/dist/elements/FontManager.js +1 -1
  59. package/dist/elements/FontManager.js.map +1 -1
  60. package/dist/elements/Footer.js +2 -2
  61. package/dist/elements/Footer.js.map +1 -1
  62. package/dist/elements/Footnote.d.ts.map +1 -1
  63. package/dist/elements/Footnote.js +6 -6
  64. package/dist/elements/Footnote.js.map +1 -1
  65. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  66. package/dist/elements/FootnoteManager.js +6 -7
  67. package/dist/elements/FootnoteManager.js.map +1 -1
  68. package/dist/elements/Header.js +2 -2
  69. package/dist/elements/Header.js.map +1 -1
  70. package/dist/elements/HeaderFooterManager.js.map +1 -1
  71. package/dist/elements/Hyperlink.d.ts +5 -3
  72. package/dist/elements/Hyperlink.d.ts.map +1 -1
  73. package/dist/elements/Hyperlink.js +134 -76
  74. package/dist/elements/Hyperlink.js.map +1 -1
  75. package/dist/elements/Image.d.ts.map +1 -1
  76. package/dist/elements/Image.js +238 -106
  77. package/dist/elements/Image.js.map +1 -1
  78. package/dist/elements/ImageManager.d.ts.map +1 -1
  79. package/dist/elements/ImageManager.js +1 -1
  80. package/dist/elements/ImageManager.js.map +1 -1
  81. package/dist/elements/ImageRun.js +1 -1
  82. package/dist/elements/ImageRun.js.map +1 -1
  83. package/dist/elements/MathElement.js.map +1 -1
  84. package/dist/elements/Paragraph.d.ts +24 -24
  85. package/dist/elements/Paragraph.d.ts.map +1 -1
  86. package/dist/elements/Paragraph.js +181 -188
  87. package/dist/elements/Paragraph.js.map +1 -1
  88. package/dist/elements/PreservedElement.js.map +1 -1
  89. package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
  90. package/dist/elements/PropertyChangeTypes.js +6 -6
  91. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  92. package/dist/elements/RangeMarker.d.ts.map +1 -1
  93. package/dist/elements/RangeMarker.js.map +1 -1
  94. package/dist/elements/Revision.d.ts.map +1 -1
  95. package/dist/elements/Revision.js +4 -5
  96. package/dist/elements/Revision.js.map +1 -1
  97. package/dist/elements/RevisionContent.js.map +1 -1
  98. package/dist/elements/RevisionManager.d.ts.map +1 -1
  99. package/dist/elements/RevisionManager.js +40 -48
  100. package/dist/elements/RevisionManager.js.map +1 -1
  101. package/dist/elements/Run.d.ts +16 -16
  102. package/dist/elements/Run.d.ts.map +1 -1
  103. package/dist/elements/Run.js +256 -238
  104. package/dist/elements/Run.js.map +1 -1
  105. package/dist/elements/Section.d.ts.map +1 -1
  106. package/dist/elements/Section.js +36 -11
  107. package/dist/elements/Section.js.map +1 -1
  108. package/dist/elements/Shape.d.ts.map +1 -1
  109. package/dist/elements/Shape.js.map +1 -1
  110. package/dist/elements/StructuredDocumentTag.d.ts +6 -6
  111. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  112. package/dist/elements/StructuredDocumentTag.js +99 -104
  113. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  114. package/dist/elements/Table.d.ts +11 -11
  115. package/dist/elements/Table.d.ts.map +1 -1
  116. package/dist/elements/Table.js +102 -107
  117. package/dist/elements/Table.js.map +1 -1
  118. package/dist/elements/TableCell.d.ts +10 -10
  119. package/dist/elements/TableCell.d.ts.map +1 -1
  120. package/dist/elements/TableCell.js +105 -106
  121. package/dist/elements/TableCell.js.map +1 -1
  122. package/dist/elements/TableGridChange.d.ts.map +1 -1
  123. package/dist/elements/TableGridChange.js.map +1 -1
  124. package/dist/elements/TableOfContents.d.ts.map +1 -1
  125. package/dist/elements/TableOfContents.js +4 -4
  126. package/dist/elements/TableOfContents.js.map +1 -1
  127. package/dist/elements/TableOfContentsElement.js.map +1 -1
  128. package/dist/elements/TableRow.d.ts.map +1 -1
  129. package/dist/elements/TableRow.js +13 -6
  130. package/dist/elements/TableRow.js.map +1 -1
  131. package/dist/elements/TextBox.d.ts.map +1 -1
  132. package/dist/elements/TextBox.js +3 -5
  133. package/dist/elements/TextBox.js.map +1 -1
  134. package/dist/formatting/AbstractNumbering.d.ts +4 -4
  135. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  136. package/dist/formatting/AbstractNumbering.js +54 -49
  137. package/dist/formatting/AbstractNumbering.js.map +1 -1
  138. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  139. package/dist/formatting/NumberingInstance.js +1 -3
  140. package/dist/formatting/NumberingInstance.js.map +1 -1
  141. package/dist/formatting/NumberingLevel.d.ts +5 -5
  142. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  143. package/dist/formatting/NumberingLevel.js +119 -125
  144. package/dist/formatting/NumberingLevel.js.map +1 -1
  145. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  146. package/dist/formatting/NumberingManager.js +9 -9
  147. package/dist/formatting/NumberingManager.js.map +1 -1
  148. package/dist/formatting/Style.d.ts +11 -11
  149. package/dist/formatting/Style.d.ts.map +1 -1
  150. package/dist/formatting/Style.js +219 -247
  151. package/dist/formatting/Style.js.map +1 -1
  152. package/dist/formatting/StylesManager.d.ts +2 -2
  153. package/dist/formatting/StylesManager.d.ts.map +1 -1
  154. package/dist/formatting/StylesManager.js +96 -102
  155. package/dist/formatting/StylesManager.js.map +1 -1
  156. package/dist/helpers/CleanupHelper.d.ts +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  158. package/dist/helpers/CleanupHelper.js +6 -6
  159. package/dist/helpers/CleanupHelper.js.map +1 -1
  160. package/dist/images/ImageOptimizer.js +7 -7
  161. package/dist/images/ImageOptimizer.js.map +1 -1
  162. package/dist/index.d.ts +9 -9
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/managers/DrawingManager.js.map +1 -1
  166. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.js +23 -7
  168. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  169. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  170. package/dist/tracking/TrackingContext.js.map +1 -1
  171. package/dist/types/compatibility-types.js.map +1 -1
  172. package/dist/types/formatting.js.map +1 -1
  173. package/dist/types/list-types.d.ts +6 -6
  174. package/dist/types/list-types.js.map +1 -1
  175. package/dist/types/settings-types.js.map +1 -1
  176. package/dist/types/styleConfig.d.ts +2 -2
  177. package/dist/types/styleConfig.js.map +1 -1
  178. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  179. package/dist/utils/ChangelogGenerator.js +97 -101
  180. package/dist/utils/ChangelogGenerator.js.map +1 -1
  181. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.js +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  184. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  186. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  187. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  188. package/dist/utils/MoveOperationHelper.js +1 -1
  189. package/dist/utils/MoveOperationHelper.js.map +1 -1
  190. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.js +2 -4
  192. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  193. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  194. package/dist/utils/RevisionWalker.js +4 -12
  195. package/dist/utils/RevisionWalker.js.map +1 -1
  196. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  198. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  199. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  200. package/dist/utils/ShadingResolver.js +1 -1
  201. package/dist/utils/ShadingResolver.js.map +1 -1
  202. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  203. package/dist/utils/acceptRevisions.js +23 -12
  204. package/dist/utils/acceptRevisions.js.map +1 -1
  205. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  207. package/dist/utils/cnfStyleDecoder.js +40 -40
  208. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  209. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  210. package/dist/utils/corruptionDetection.js.map +1 -1
  211. package/dist/utils/dateFormatting.js.map +1 -1
  212. package/dist/utils/deepClone.js +1 -1
  213. package/dist/utils/deepClone.js.map +1 -1
  214. package/dist/utils/diagnostics.d.ts.map +1 -1
  215. package/dist/utils/diagnostics.js +1 -1
  216. package/dist/utils/diagnostics.js.map +1 -1
  217. package/dist/utils/errorHandling.js.map +1 -1
  218. package/dist/utils/formatting.d.ts.map +1 -1
  219. package/dist/utils/formatting.js +10 -2
  220. package/dist/utils/formatting.js.map +1 -1
  221. package/dist/utils/list-detection.d.ts +2 -2
  222. package/dist/utils/list-detection.d.ts.map +1 -1
  223. package/dist/utils/list-detection.js +21 -23
  224. package/dist/utils/list-detection.js.map +1 -1
  225. package/dist/utils/logger.d.ts.map +1 -1
  226. package/dist/utils/logger.js +12 -7
  227. package/dist/utils/logger.js.map +1 -1
  228. package/dist/utils/parsingHelpers.js.map +1 -1
  229. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  230. package/dist/utils/stripTrackedChanges.js +3 -3
  231. package/dist/utils/stripTrackedChanges.js.map +1 -1
  232. package/dist/utils/textDiff.d.ts +1 -1
  233. package/dist/utils/textDiff.js +8 -8
  234. package/dist/utils/textDiff.js.map +1 -1
  235. package/dist/utils/units.js.map +1 -1
  236. package/dist/utils/validation.d.ts.map +1 -1
  237. package/dist/utils/validation.js +24 -7
  238. package/dist/utils/validation.js.map +1 -1
  239. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  240. package/dist/utils/xmlSanitization.js +3 -3
  241. package/dist/utils/xmlSanitization.js.map +1 -1
  242. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.js +5 -5
  244. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  245. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  246. package/dist/validation/RevisionValidator.js +7 -9
  247. package/dist/validation/RevisionValidator.js.map +1 -1
  248. package/dist/validation/ValidationRules.js +3 -3
  249. package/dist/validation/ValidationRules.js.map +1 -1
  250. package/dist/validation/index.js.map +1 -1
  251. package/dist/xml/XMLBuilder.d.ts +1 -1
  252. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  253. package/dist/xml/XMLBuilder.js +98 -100
  254. package/dist/xml/XMLBuilder.js.map +1 -1
  255. package/dist/xml/XMLParser.d.ts.map +1 -1
  256. package/dist/xml/XMLParser.js +61 -66
  257. package/dist/xml/XMLParser.js.map +1 -1
  258. package/dist/zip/ZipHandler.d.ts.map +1 -1
  259. package/dist/zip/ZipHandler.js.map +1 -1
  260. package/dist/zip/ZipReader.d.ts.map +1 -1
  261. package/dist/zip/ZipReader.js +1 -3
  262. package/dist/zip/ZipReader.js.map +1 -1
  263. package/dist/zip/ZipWriter.d.ts +1 -1
  264. package/dist/zip/ZipWriter.d.ts.map +1 -1
  265. package/dist/zip/ZipWriter.js +28 -36
  266. package/dist/zip/ZipWriter.js.map +1 -1
  267. package/dist/zip/types.js +1 -1
  268. package/dist/zip/types.js.map +1 -1
  269. package/package.json +92 -92
  270. package/src/__tests__/helper-methods.test.ts +512 -512
  271. package/src/constants/legacyCompatFlags.ts +138 -138
  272. package/src/constants/limits.ts +50 -50
  273. package/src/core/Document.ts +985 -1145
  274. package/src/core/DocumentContent.ts +461 -467
  275. package/src/core/DocumentGenerator.ts +1133 -1104
  276. package/src/core/DocumentIdManager.ts +158 -158
  277. package/src/core/DocumentParser.ts +2347 -2716
  278. package/src/core/DocumentValidator.ts +363 -372
  279. package/src/core/Relationship.ts +367 -367
  280. package/src/core/RelationshipManager.ts +429 -428
  281. package/src/elements/AlternateContent.ts +42 -42
  282. package/src/elements/Bookmark.ts +212 -210
  283. package/src/elements/BookmarkManager.ts +247 -250
  284. package/src/elements/Comment.ts +356 -359
  285. package/src/elements/CommentManager.ts +499 -502
  286. package/src/elements/CommonTypes.ts +524 -549
  287. package/src/elements/CustomXml.ts +36 -36
  288. package/src/elements/Endnote.ts +221 -217
  289. package/src/elements/EndnoteManager.ts +246 -249
  290. package/src/elements/Field.ts +1292 -1233
  291. package/src/elements/FieldHelpers.ts +329 -333
  292. package/src/elements/FontManager.ts +336 -339
  293. package/src/elements/Footer.ts +269 -269
  294. package/src/elements/Footnote.ts +221 -217
  295. package/src/elements/FootnoteManager.ts +246 -249
  296. package/src/elements/Header.ts +269 -269
  297. package/src/elements/HeaderFooterManager.ts +219 -219
  298. package/src/elements/Hyperlink.ts +1288 -1193
  299. package/src/elements/Image.ts +1982 -1756
  300. package/src/elements/ImageManager.ts +437 -432
  301. package/src/elements/ImageRun.ts +59 -59
  302. package/src/elements/MathElement.ts +65 -65
  303. package/src/elements/Paragraph.ts +4347 -4287
  304. package/src/elements/PreservedElement.ts +53 -53
  305. package/src/elements/PropertyChangeTypes.ts +458 -442
  306. package/src/elements/RangeMarker.ts +382 -400
  307. package/src/elements/Revision.ts +1198 -1217
  308. package/src/elements/RevisionContent.ts +73 -73
  309. package/src/elements/RevisionManager.ts +1070 -1070
  310. package/src/elements/Run.ts +3103 -3073
  311. package/src/elements/Section.ts +1521 -1421
  312. package/src/elements/Shape.ts +884 -873
  313. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  314. package/src/elements/Table.ts +2468 -2524
  315. package/src/elements/TableCell.ts +1617 -1621
  316. package/src/elements/TableGridChange.ts +149 -151
  317. package/src/elements/TableOfContents.ts +701 -691
  318. package/src/elements/TableOfContentsElement.ts +89 -89
  319. package/src/elements/TableRow.ts +960 -929
  320. package/src/elements/TextBox.ts +766 -768
  321. package/src/formatting/AbstractNumbering.ts +580 -579
  322. package/src/formatting/NumberingInstance.ts +295 -299
  323. package/src/formatting/NumberingLevel.ts +981 -1040
  324. package/src/formatting/NumberingManager.ts +833 -827
  325. package/src/formatting/Style.ts +1785 -1879
  326. package/src/formatting/StylesManager.ts +1090 -1130
  327. package/src/helpers/CleanupHelper.ts +524 -524
  328. package/src/images/ImageOptimizer.ts +274 -274
  329. package/src/index.ts +559 -554
  330. package/src/managers/DrawingManager.ts +319 -319
  331. package/src/tracking/DocumentTrackingContext.ts +687 -674
  332. package/src/tracking/TrackingContext.ts +175 -173
  333. package/src/types/compatibility-types.ts +49 -49
  334. package/src/types/formatting.ts +210 -210
  335. package/src/types/list-types.ts +14 -14
  336. package/src/types/settings-types.ts +59 -59
  337. package/src/types/styleConfig.ts +189 -189
  338. package/src/utils/ChangelogGenerator.ts +1583 -1581
  339. package/src/utils/CompatibilityUpgrader.ts +235 -237
  340. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  341. package/src/utils/MoveOperationHelper.ts +233 -238
  342. package/src/utils/RevisionAwareProcessor.ts +518 -526
  343. package/src/utils/RevisionWalker.ts +427 -457
  344. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  345. package/src/utils/ShadingResolver.ts +105 -107
  346. package/src/utils/acceptRevisions.ts +723 -714
  347. package/src/utils/cnfStyleDecoder.ts +212 -217
  348. package/src/utils/corruptionDetection.ts +346 -345
  349. package/src/utils/dateFormatting.ts +20 -20
  350. package/src/utils/deepClone.ts +77 -78
  351. package/src/utils/diagnostics.ts +125 -129
  352. package/src/utils/errorHandling.ts +80 -80
  353. package/src/utils/formatting.ts +220 -213
  354. package/src/utils/list-detection.ts +32 -42
  355. package/src/utils/logger.ts +412 -404
  356. package/src/utils/parsingHelpers.ts +190 -190
  357. package/src/utils/stripTrackedChanges.ts +356 -353
  358. package/src/utils/textDiff.ts +100 -100
  359. package/src/utils/units.ts +421 -421
  360. package/src/utils/validation.ts +553 -542
  361. package/src/utils/xmlSanitization.ts +179 -182
  362. package/src/validation/RevisionAutoFixer.ts +541 -542
  363. package/src/validation/RevisionValidator.ts +470 -460
  364. package/src/validation/ValidationRules.ts +338 -338
  365. package/src/validation/index.ts +30 -30
  366. package/src/xml/XMLBuilder.ts +857 -871
  367. package/src/xml/XMLParser.ts +877 -919
  368. package/src/zip/ZipHandler.ts +629 -637
  369. package/src/zip/ZipReader.ts +295 -299
  370. package/src/zip/ZipWriter.ts +374 -390
  371. package/src/zip/types.ts +116 -116
@@ -1,1621 +1,1617 @@
1
- /**
2
- * TableCell - Represents a cell in a table
3
- */
4
-
5
- import { formatDateForXml } from "../utils/dateFormatting";
6
- import { XMLBuilder, XMLElement } from "../xml/XMLBuilder";
7
- import { Paragraph, TextDirection } from "./Paragraph";
8
- import { Revision } from "./Revision";
9
- import {
10
- BorderStyle as CommonBorderStyle,
11
- BorderDefinition,
12
- FourSidedBorders,
13
- CellVerticalAlignment as CommonCellVerticalAlignment,
14
- ShadingConfig,
15
- ShadingPattern,
16
- buildShadingAttributes,
17
- WidthType,
18
- } from "./CommonTypes";
19
-
20
- // ============================================================================
21
- // RE-EXPORTED TYPES (for backward compatibility)
22
- // ============================================================================
23
-
24
- /**
25
- * Cell border style
26
- * @see CommonTypes.BorderStyle
27
- */
28
- export type BorderStyle = CommonBorderStyle;
29
-
30
- /**
31
- * Cell border definition
32
- * @see CommonTypes.BorderDefinition
33
- */
34
- export interface CellBorder {
35
- style?: BorderStyle;
36
- size?: number; // Size in eighths of a point
37
- color?: string; // Hex color without #
38
- }
39
-
40
- /**
41
- * Cell borders
42
- * @see CommonTypes.FourSidedBorders
43
- */
44
- export interface CellBorders {
45
- top?: CellBorder;
46
- bottom?: CellBorder;
47
- left?: CellBorder;
48
- right?: CellBorder;
49
- /** Diagonal border from top-left to bottom-right per ECMA-376 Part 1 §17.4.84 */
50
- tl2br?: CellBorder;
51
- /** Diagonal border from top-right to bottom-left per ECMA-376 Part 1 §17.4.85 */
52
- tr2bl?: CellBorder;
53
- }
54
-
55
- /**
56
- * Cell shading/background
57
- * @see ShadingConfig in CommonTypes.ts for the canonical definition
58
- */
59
- export type CellShading = ShadingConfig;
60
-
61
- /**
62
- * Vertical alignment in cell
63
- * @see CommonTypes.CellVerticalAlignment
64
- */
65
- export type CellVerticalAlignment = CommonCellVerticalAlignment;
66
-
67
- /**
68
- * Cell margins (spacing inside cell borders)
69
- * Per ECMA-376 Part 1 §17.4.43
70
- */
71
- export interface CellMargins {
72
- top?: number; // Margin in twips
73
- bottom?: number; // Margin in twips
74
- left?: number; // Margin in twips
75
- right?: number; // Margin in twips
76
- }
77
-
78
- /**
79
- * Cell width type
80
- * Per ECMA-376 Part 1 §17.18.87
81
- */
82
- export type CellWidthType = "auto" | "dxa" | "pct";
83
-
84
- /**
85
- * Vertical merge type for cells
86
- * Per ECMA-376 Part 1 §17.4.85
87
- */
88
- export type VerticalMerge = "restart" | "continue";
89
-
90
- /**
91
- * Cell formatting options
92
- */
93
- export interface CellFormatting {
94
- width?: number; // Width in twips
95
- widthType?: CellWidthType; // Width type (auto, dxa, pct)
96
- borders?: CellBorders;
97
- shading?: CellShading;
98
- verticalAlignment?: CellVerticalAlignment;
99
- columnSpan?: number; // Number of columns to span (gridSpan)
100
- rowSpan?: number; // Number of rows to span (gridSpan)
101
- margins?: CellMargins; // Cell margins (spacing inside borders)
102
- textDirection?: TextDirection; // Text flow direction
103
- fitText?: boolean; // Fit text to cell width
104
- noWrap?: boolean; // Prevent text wrapping
105
- hideMark?: boolean; // Hide end-of-cell mark
106
- cnfStyle?: string; // Conditional formatting style (14-char binary string)
107
- vMerge?: VerticalMerge; // Vertical cell merge
108
- hMerge?: 'restart' | 'continue'; // Legacy horizontal merge (w:hMerge) per ECMA-376 Part 1 §17.4.22
109
- /** Cell headers attribute for accessibility per ECMA-376 Part 1 §17.4.26 */
110
- headers?: string;
111
- }
112
-
113
- /**
114
- * Raw nested content stored as XML to preserve nested tables/SDTs
115
- * Position indicates after which paragraph index the content appears
116
- */
117
- export interface RawNestedContent {
118
- position: number; // Content appears after this paragraph index
119
- xml: string; // Raw XML content
120
- type: "table" | "sdt"; // Type of nested content
121
- }
122
-
123
- /**
124
- * Table cell property change tracking (w:tcPrChange)
125
- * Per ECMA-376 Part 1 §17.13.5.37
126
- */
127
- export interface TcPrChange {
128
- author: string;
129
- date: string;
130
- id: string;
131
- previousProperties: Record<string, any>;
132
- }
133
-
134
- /**
135
- * Represents a table cell
136
- */
137
- export class TableCell {
138
- private paragraphs: Paragraph[] = [];
139
- private formatting: CellFormatting;
140
- /** Raw nested content (tables, SDTs) stored as XML for passthrough */
141
- private rawNestedContent: RawNestedContent[] = [];
142
- /** Parent row reference (if cell is inside a table row) */
143
- private _parentRow?: import('./TableRow').TableRow;
144
- /** Table cell revision (w:cellIns, w:cellDel, w:cellMerge) per ECMA-376 Part 1 §17.13.5.4-5.6 */
145
- private cellRevision?: Revision;
146
- /** Tracking context for automatic change tracking */
147
- private trackingContext?: import('../tracking/TrackingContext').TrackingContext;
148
- /** Table cell property change tracking (w:tcPrChange) */
149
- private tcPrChange?: TcPrChange;
150
-
151
- /**
152
- * Creates a new TableCell
153
- * @param formatting - Cell formatting options
154
- */
155
- constructor(formatting: CellFormatting = {}) {
156
- this.formatting = formatting;
157
- }
158
-
159
- /**
160
- * Sets the tracking context for automatic change tracking.
161
- * Called by Document when track changes is enabled.
162
- * @internal
163
- */
164
- _setTrackingContext(context: import('../tracking/TrackingContext').TrackingContext): void {
165
- this.trackingContext = context;
166
- }
167
-
168
- /**
169
- * Gets the table cell property change tracking info
170
- * @returns tcPrChange or undefined
171
- */
172
- getTcPrChange(): TcPrChange | undefined {
173
- return this.tcPrChange;
174
- }
175
-
176
- /**
177
- * Sets the table cell property change tracking info
178
- * @param change - Property change info or undefined to clear
179
- */
180
- setTcPrChange(change: TcPrChange | undefined): void {
181
- this.tcPrChange = change;
182
- }
183
-
184
- /**
185
- * Clears the table cell property change tracking
186
- */
187
- clearTcPrChange(): void {
188
- this.tcPrChange = undefined;
189
- }
190
-
191
- /**
192
- * Adds a paragraph to the cell
193
- * @param paragraph - Paragraph to add
194
- * @returns This cell for chaining
195
- */
196
- addParagraph(paragraph: Paragraph): this {
197
- this.paragraphs.push(paragraph);
198
- paragraph._setParentCell(this);
199
- // Propagate StylesManager from table if available
200
- const stylesManager = this._parentRow?._getParentTable()?._getStylesManager();
201
- if (stylesManager) {
202
- paragraph._setStylesManager(stylesManager);
203
- }
204
- return this;
205
- }
206
-
207
- /**
208
- * Creates and adds a new paragraph with text
209
- * @param text - Text content
210
- * @returns The created paragraph
211
- */
212
- createParagraph(text?: string): Paragraph {
213
- const para = new Paragraph();
214
- if (text) {
215
- para.addText(text);
216
- }
217
- this.paragraphs.push(para);
218
- para._setParentCell(this);
219
- // Propagate StylesManager from table if available
220
- const stylesManager = this._parentRow?._getParentTable()?._getStylesManager();
221
- if (stylesManager) {
222
- para._setStylesManager(stylesManager);
223
- }
224
- return para;
225
- }
226
-
227
- /**
228
- * Gets all paragraphs in the cell
229
- * @returns Array of paragraphs
230
- */
231
- getParagraphs(): Paragraph[] {
232
- return [...this.paragraphs];
233
- }
234
-
235
- /**
236
- * Removes a paragraph at the specified index
237
- * @param index - Index of paragraph to remove
238
- * @returns True if removed, false if index out of bounds
239
- */
240
- removeParagraph(index: number): boolean {
241
- if (index < 0 || index >= this.paragraphs.length) {
242
- return false;
243
- }
244
-
245
- // When tracking enabled, wrap content in w:del instead of removing
246
- if (this.trackingContext?.isEnabled()) {
247
- const paragraph = this.paragraphs[index]!;
248
- const runs = paragraph.getRuns();
249
- if (runs.length > 0) {
250
- const author = this.trackingContext.getAuthor();
251
- const deletion = Revision.createDeletion(author, runs);
252
- this.trackingContext.getRevisionManager().register(deletion);
253
- paragraph.addRevision(deletion);
254
- }
255
- return true;
256
- }
257
-
258
- const removed = this.paragraphs.splice(index, 1);
259
- const removedPara = removed[0];
260
- if (removedPara) {
261
- removedPara._setParentCell(undefined);
262
- }
263
-
264
- // Update raw nested content positions
265
- // Any nested content positioned AFTER the removed paragraph needs its position decremented
266
- // This maintains correct relative positioning for nested tables, SDTs, etc.
267
- for (const item of this.rawNestedContent) {
268
- if (item.position > index) {
269
- item.position--;
270
- }
271
- }
272
-
273
- return true;
274
- }
275
-
276
- /**
277
- * Adds a paragraph at the specified index
278
- * @param index - Index to insert at
279
- * @param paragraph - Paragraph to add
280
- * @returns This cell for chaining
281
- */
282
- addParagraphAt(index: number, paragraph: Paragraph): this {
283
- if (index < 0) {
284
- index = 0;
285
- }
286
-
287
- // Determine actual insertion index
288
- const actualIndex = index >= this.paragraphs.length ? this.paragraphs.length : index;
289
-
290
- if (index >= this.paragraphs.length) {
291
- this.paragraphs.push(paragraph);
292
- } else {
293
- this.paragraphs.splice(index, 0, paragraph);
294
- }
295
-
296
- // Update raw nested content positions
297
- // Any nested content positioned AT OR AFTER the insertion point needs its position incremented
298
- // This maintains correct relative positioning for nested tables, SDTs, etc.
299
- for (const item of this.rawNestedContent) {
300
- if (item.position >= actualIndex) {
301
- item.position++;
302
- }
303
- }
304
-
305
- paragraph._setParentCell(this);
306
- // Propagate StylesManager from table if available
307
- const stylesManager = this._parentRow?._getParentTable()?._getStylesManager();
308
- if (stylesManager) {
309
- paragraph._setStylesManager(stylesManager);
310
- }
311
-
312
- // When tracking enabled, wrap paragraph content in w:ins revision
313
- if (this.trackingContext?.isEnabled()) {
314
- const runs = paragraph.getRuns();
315
- if (runs.length > 0) {
316
- const author = this.trackingContext.getAuthor();
317
- const insertion = Revision.createInsertion(author, runs);
318
- this.trackingContext.getRevisionManager().register(insertion);
319
- paragraph.addRevision(insertion);
320
- }
321
- }
322
-
323
- return this;
324
- }
325
-
326
- /**
327
- * Gets the text content of all paragraphs
328
- * @returns Combined text
329
- */
330
- getText(): string {
331
- return this.paragraphs.map((p) => p.getText()).join("\n");
332
- }
333
-
334
- /**
335
- * Gets all fields from paragraphs in this cell
336
- *
337
- * Collects all Field and ComplexField instances from every paragraph
338
- * in the table cell.
339
- *
340
- * @returns Array of fields (Field and ComplexField instances)
341
- *
342
- * @example
343
- * ```typescript
344
- * const cell = table.getCell(0, 0);
345
- * const fields = cell?.getFields() || [];
346
- * console.log(`Cell has ${fields.length} fields`);
347
- * ```
348
- */
349
- getFields(): (import("./Field").Field | import("./Field").ComplexField)[] {
350
- const fields: (import("./Field").Field | import("./Field").ComplexField)[] = [];
351
- for (const para of this.paragraphs) {
352
- fields.push(...para.getFields());
353
- }
354
- return fields;
355
- }
356
-
357
- /**
358
- * Finds fields matching a predicate function
359
- *
360
- * Searches through all fields in the cell and returns those matching
361
- * the specified criteria.
362
- *
363
- * @param predicate - Function to test each field
364
- * @returns Array of matching fields
365
- *
366
- * @example
367
- * ```typescript
368
- * // Find all PAGE fields
369
- * const pageFields = cell.findFields(f =>
370
- * f.getInstruction().startsWith('PAGE')
371
- * );
372
- *
373
- * // Find fields with specific switches
374
- * const mergeFields = cell.findFields(f =>
375
- * f.getInstruction().includes('MERGEFIELD')
376
- * );
377
- * ```
378
- */
379
- findFields(
380
- predicate: (
381
- field: import("./Field").Field | import("./Field").ComplexField
382
- ) => boolean
383
- ): (import("./Field").Field | import("./Field").ComplexField)[] {
384
- return this.getFields().filter(predicate);
385
- }
386
-
387
- /**
388
- * Removes all fields from paragraphs in this cell
389
- *
390
- * Iterates through each paragraph and removes all fields,
391
- * preserving text content.
392
- *
393
- * @returns Count of fields removed
394
- *
395
- * @example
396
- * ```typescript
397
- * const count = cell.removeAllFields();
398
- * console.log(`Removed ${count} fields from cell`);
399
- * ```
400
- */
401
- removeAllFields(): number {
402
- let count = 0;
403
- for (const para of this.paragraphs) {
404
- count += para.removeAllFields();
405
- }
406
- return count;
407
- }
408
-
409
- /**
410
- * Sets cell width
411
- * @param twips - Width in twips
412
- * @returns This cell for chaining
413
- */
414
- setWidth(twips: number): this {
415
- const prev = this.formatting.width;
416
- this.formatting.width = twips;
417
- if (this.trackingContext?.isEnabled() && prev !== twips) {
418
- this.trackingContext.trackTableChange(this, 'width', prev, twips);
419
- }
420
- return this;
421
- }
422
-
423
- /**
424
- * Sets cell borders
425
- * @param borders - Border definitions
426
- * @returns This cell for chaining
427
- */
428
- setBorders(borders: CellBorders): this {
429
- const prev = this.formatting.borders;
430
- this.formatting.borders = borders;
431
- if (this.trackingContext?.isEnabled() && prev !== borders) {
432
- this.trackingContext.trackTableChange(this, 'borders', prev, borders);
433
- }
434
- return this;
435
- }
436
-
437
- /**
438
- * Sets cell shading/background
439
- * @param shading - Shading definition
440
- * @returns This cell for chaining
441
- */
442
- setShading(shading: CellShading): this {
443
- const prev = this.formatting.shading;
444
- this.formatting.shading = shading;
445
- if (this.trackingContext?.isEnabled() && prev !== shading) {
446
- this.trackingContext.trackTableChange(this, 'shading', prev, shading);
447
- }
448
- return this;
449
- }
450
-
451
- /**
452
- * Sets vertical alignment
453
- * @param alignment - Vertical alignment
454
- * @returns This cell for chaining
455
- */
456
- setVerticalAlignment(alignment: CellVerticalAlignment): this {
457
- const prev = this.formatting.verticalAlignment;
458
- this.formatting.verticalAlignment = alignment;
459
- if (this.trackingContext?.isEnabled() && prev !== alignment) {
460
- this.trackingContext.trackTableChange(this, 'verticalAlignment', prev, alignment);
461
- }
462
- return this;
463
- }
464
-
465
- /**
466
- * Sets column span (merge cells horizontally)
467
- * @param span - Number of columns to span
468
- * @returns This cell for chaining
469
- */
470
- setColumnSpan(span: number): this {
471
- const prev = this.formatting.columnSpan;
472
- this.formatting.columnSpan = span;
473
- if (this.trackingContext?.isEnabled() && prev !== span) {
474
- this.trackingContext.trackTableChange(this, 'columnSpan', prev, span);
475
- }
476
- return this;
477
- }
478
-
479
- /**
480
- * Gets the number of columns this cell spans (gridSpan)
481
- * @returns Column span, defaults to 1 if not set
482
- */
483
- getColumnSpan(): number {
484
- return this.formatting.columnSpan || 1;
485
- }
486
-
487
- /**
488
- * Gets whether to fit text to cell width
489
- * @returns true if fit text is enabled, false otherwise
490
- */
491
- getFitText(): boolean {
492
- return this.formatting.fitText ?? false;
493
- }
494
-
495
- /**
496
- * Gets whether text wrapping is prevented in cell
497
- * @returns true if no-wrap is enabled, false otherwise
498
- */
499
- getNoWrap(): boolean {
500
- return this.formatting.noWrap ?? false;
501
- }
502
-
503
- /**
504
- * Gets whether the end-of-cell mark is hidden
505
- * @returns true if hidden, false otherwise
506
- */
507
- getHideMark(): boolean {
508
- return this.formatting.hideMark ?? false;
509
- }
510
-
511
- /**
512
- * Gets the conditional formatting style (cnfStyle) for this cell
513
- * @returns 14-character binary string or undefined if not set
514
- */
515
- getCnfStyle(): string | undefined {
516
- return this.formatting.cnfStyle;
517
- }
518
-
519
- /**
520
- * Validates a margin value
521
- * @param value - Margin value in twips
522
- * @param side - Name of the margin side (for error messages)
523
- * @throws {Error} If margin is negative or exceeds maximum
524
- * @private
525
- */
526
- private validateMargin(value: number | undefined, side: string): void {
527
- if (value === undefined) return;
528
-
529
- // Margins must be non-negative
530
- if (value < 0) {
531
- throw new Error(
532
- `Invalid ${side} margin: ${value} twips. Cell margins cannot be negative.`
533
- );
534
- }
535
-
536
- // Maximum reasonable margin (1 inch = 1440 twips)
537
- // Word typically allows up to several inches, but we set a reasonable limit
538
- const MAX_MARGIN_TWIPS = 14400; // 10 inches
539
- if (value > MAX_MARGIN_TWIPS) {
540
- throw new Error(
541
- `Invalid ${side} margin: ${value} twips exceeds maximum of ${MAX_MARGIN_TWIPS} twips (10 inches).`
542
- );
543
- }
544
- }
545
-
546
- /**
547
- * Sets cell margins (spacing inside cell borders)
548
- * Per ECMA-376 Part 1 §17.4.43
549
- * @param margins - Margin definitions for each side
550
- * @returns This cell for chaining
551
- * @throws {Error} If any margin value is negative or exceeds maximum
552
- */
553
- setMargins(margins: CellMargins): this {
554
- // Validate each margin
555
- this.validateMargin(margins.top, "top");
556
- this.validateMargin(margins.bottom, "bottom");
557
- this.validateMargin(margins.left, "left");
558
- this.validateMargin(margins.right, "right");
559
-
560
- const prev = this.formatting.margins;
561
- this.formatting.margins = margins;
562
- if (this.trackingContext?.isEnabled() && prev !== margins) {
563
- this.trackingContext.trackTableChange(this, 'margins', prev, margins);
564
- }
565
- return this;
566
- }
567
-
568
- /**
569
- * Sets all cell margins to the same value
570
- * @param margin - Margin in twips to apply to all sides
571
- * @returns This cell for chaining
572
- * @throws {Error} If margin value is negative or exceeds maximum
573
- */
574
- setAllMargins(margin: number): this {
575
- this.validateMargin(margin, "all");
576
- return this.setMargins({ top: margin, bottom: margin, left: margin, right: margin });
577
- }
578
-
579
- /**
580
- * Sets text direction for cell content
581
- * Per ECMA-376 Part 1 §17.4.72
582
- * @param direction - Text flow direction
583
- * - 'lrTb': Left-to-right, top-to-bottom (default)
584
- * - 'tbRl': Top-to-bottom, right-to-left (vertical text, East Asian)
585
- * - 'btLr': Bottom-to-top, left-to-right (vertical text)
586
- * - 'lrTbV': Left-to-right, top-to-bottom, vertical
587
- * - 'tbRlV': Top-to-bottom, right-to-left, vertical
588
- * - 'tbLrV': Top-to-bottom, left-to-right, vertical
589
- * @returns This cell for chaining
590
- */
591
- setTextDirection(direction: TextDirection): this {
592
- const prev = this.formatting.textDirection;
593
- this.formatting.textDirection = direction;
594
- if (this.trackingContext?.isEnabled() && prev !== direction) {
595
- this.trackingContext.trackTableChange(this, 'textDirection', prev, direction);
596
- }
597
- return this;
598
- }
599
-
600
- /**
601
- * Sets whether to fit text to cell width
602
- * Per ECMA-376 Part 1 §17.4.68
603
- * @param fit - Whether to expand/compress text to fit cell width
604
- * @returns This cell for chaining
605
- */
606
- setFitText(fit = true): this {
607
- const prev = this.formatting.fitText;
608
- this.formatting.fitText = fit;
609
- if (this.trackingContext?.isEnabled() && prev !== fit) {
610
- this.trackingContext.trackTableChange(this, 'fitText', prev, fit);
611
- }
612
- return this;
613
- }
614
-
615
- /**
616
- * Sets whether to prevent text wrapping in cell
617
- * Per ECMA-376 Part 1 §17.4.34
618
- * @param noWrap - Whether to prevent wrapping (default: true)
619
- * @returns This cell for chaining
620
- */
621
- setNoWrap(noWrap = true): this {
622
- const prev = this.formatting.noWrap;
623
- this.formatting.noWrap = noWrap;
624
- if (this.trackingContext?.isEnabled() && prev !== noWrap) {
625
- this.trackingContext.trackTableChange(this, 'noWrap', prev, noWrap);
626
- }
627
- return this;
628
- }
629
-
630
- /**
631
- * Sets whether to hide the end-of-cell mark
632
- * Per ECMA-376 Part 1 §17.4.24
633
- * @param hide - Whether to ignore cell end mark in height calculations (default: true)
634
- * @returns This cell for chaining
635
- */
636
- setHideMark(hide = true): this {
637
- const prev = this.formatting.hideMark;
638
- this.formatting.hideMark = hide;
639
- if (this.trackingContext?.isEnabled() && prev !== hide) {
640
- this.trackingContext.trackTableChange(this, 'hideMark', prev, hide);
641
- }
642
- return this;
643
- }
644
-
645
- /**
646
- * Sets conditional formatting style for this cell
647
- * Per ECMA-376 Part 1 §17.4.7
648
- * @param cnfStyle - 14-character binary string representing which conditional formats to apply
649
- * Each bit position controls a different conditional format (e.g., "100000000000" for first row)
650
- * @returns This cell for chaining
651
- */
652
- setConditionalStyle(cnfStyle: string): this {
653
- const prev = this.formatting.cnfStyle;
654
- this.formatting.cnfStyle = cnfStyle;
655
- if (this.trackingContext?.isEnabled() && prev !== cnfStyle) {
656
- this.trackingContext.trackTableChange(this, 'cnfStyle', prev, cnfStyle);
657
- }
658
- return this;
659
- }
660
-
661
- /**
662
- * Sets cell width with type specification
663
- * Per ECMA-376 Part 1 §17.4.81
664
- * @param width - Width value
665
- * @param type - Width type: 'auto' (automatic), 'dxa' (twips), or 'pct' (percentage * 50)
666
- * @returns This cell for chaining
667
- */
668
- setWidthType(width: number, type: CellWidthType = "dxa"): this {
669
- const prevWidth = this.formatting.width;
670
- const prevType = this.formatting.widthType;
671
- this.formatting.width = width;
672
- this.formatting.widthType = type;
673
- if (this.trackingContext?.isEnabled()) {
674
- if (prevWidth !== width) {
675
- this.trackingContext.trackTableChange(this, 'width', prevWidth, width);
676
- }
677
- if (prevType !== type) {
678
- this.trackingContext.trackTableChange(this, 'widthType', prevType, type);
679
- }
680
- }
681
- return this;
682
- }
683
-
684
- /**
685
- * Sets vertical merge for this cell
686
- * Per ECMA-376 Part 1 §17.4.85
687
- * @param merge - Vertical merge type:
688
- * - 'restart': Start a new vertically merged region (top cell)
689
- * - 'continue': Continue the current vertically merged region (cells below)
690
- * @returns This cell for chaining
691
- */
692
- setVerticalMerge(merge: VerticalMerge | undefined): this {
693
- const prev = this.formatting.vMerge;
694
- this.formatting.vMerge = merge;
695
- if (this.trackingContext?.isEnabled() && prev !== merge) {
696
- this.trackingContext.trackTableChange(this, 'vMerge', prev, merge);
697
- }
698
- return this;
699
- }
700
-
701
- /**
702
- * Sets the legacy horizontal merge state per ECMA-376 Part 1 §17.4.22
703
- * @param merge - 'restart' to start a new merge region, 'continue' to continue, or undefined to clear
704
- * @returns This cell for chaining
705
- */
706
- setHorizontalMerge(merge: 'restart' | 'continue' | undefined): this {
707
- const prev = this.formatting.hMerge;
708
- this.formatting.hMerge = merge;
709
- if (this.trackingContext?.isEnabled() && prev !== merge) {
710
- this.trackingContext.trackTableChange(this, 'hMerge', prev, merge);
711
- }
712
- return this;
713
- }
714
-
715
- // ============================================================================
716
- // CELL REVISIONS (w:cellIns, w:cellDel, w:cellMerge)
717
- // ============================================================================
718
-
719
- /**
720
- * Sets the revision marker for this cell
721
- * Per ECMA-376 Part 1 §17.13.5.4-5.6
722
- *
723
- * Table cell revisions track structural changes to table cells:
724
- * - tableCellInsert (w:cellIns): Cell was inserted
725
- * - tableCellDelete (w:cellDel): Cell was deleted
726
- * - tableCellMerge (w:cellMerge): Cell merge/split operation
727
- *
728
- * @param revision - Revision marker for this cell
729
- * @returns This cell for chaining
730
- *
731
- * @example
732
- * ```typescript
733
- * const revision = new Revision({
734
- * id: 1,
735
- * author: 'Alice',
736
- * date: new Date(),
737
- * type: 'tableCellInsert',
738
- * content: [],
739
- * });
740
- * cell.setCellRevision(revision);
741
- * ```
742
- */
743
- setCellRevision(revision: Revision): this {
744
- this.cellRevision = revision;
745
- return this;
746
- }
747
-
748
- /**
749
- * Gets the revision marker for this cell
750
- *
751
- * @returns The cell revision if present, undefined otherwise
752
- *
753
- * @example
754
- * ```typescript
755
- * const revision = cell.getCellRevision();
756
- * if (revision) {
757
- * console.log(`Cell ${revision.getType()} by ${revision.getAuthor()}`);
758
- * }
759
- * ```
760
- */
761
- getCellRevision(): Revision | undefined {
762
- return this.cellRevision;
763
- }
764
-
765
- /**
766
- * Checks if this cell has a revision marker
767
- *
768
- * @returns True if cell has a revision (insert, delete, or merge)
769
- */
770
- hasCellRevision(): boolean {
771
- return this.cellRevision !== undefined;
772
- }
773
-
774
- /**
775
- * Clears the revision marker for this cell
776
- *
777
- * @returns This cell for chaining
778
- */
779
- clearCellRevision(): this {
780
- this.cellRevision = undefined;
781
- return this;
782
- }
783
-
784
- // ============================================================================
785
- // CONVENIENCE METHODS (for easier paragraph manipulation)
786
- // ============================================================================
787
-
788
- /**
789
- * Sets text alignment for all paragraphs in the cell
790
- *
791
- * Applies the specified horizontal alignment to every paragraph
792
- * in this cell.
793
- *
794
- * @param alignment - Paragraph alignment (left, center, right, both)
795
- * @returns This cell for chaining
796
- *
797
- * @example
798
- * ```typescript
799
- * cell.setTextAlignment('center');
800
- * ```
801
- */
802
- setTextAlignment(alignment: import('./Paragraph').ParagraphAlignment): this {
803
- for (const para of this.paragraphs) {
804
- para.setAlignment(alignment);
805
- }
806
- return this;
807
- }
808
-
809
- /**
810
- * Sets the style for all paragraphs in the cell
811
- *
812
- * Applies the specified style ID to every paragraph in this cell.
813
- *
814
- * @param styleId - Style ID to apply
815
- * @returns This cell for chaining
816
- *
817
- * @example
818
- * ```typescript
819
- * cell.setAllParagraphsStyle('TableContent');
820
- * ```
821
- */
822
- setAllParagraphsStyle(styleId: string): this {
823
- for (const para of this.paragraphs) {
824
- para.setStyle(styleId);
825
- }
826
- return this;
827
- }
828
-
829
- /**
830
- * Sets font for all runs in the cell
831
- *
832
- * Applies the specified font to every text run in every paragraph
833
- * in this cell.
834
- *
835
- * @param fontName - Font name to apply
836
- * @returns Number of runs modified
837
- *
838
- * @example
839
- * ```typescript
840
- * const count = cell.setAllRunsFont('Arial');
841
- * ```
842
- */
843
- setAllRunsFont(fontName: string): number {
844
- let count = 0;
845
- for (const para of this.paragraphs) {
846
- for (const run of para.getRuns()) {
847
- run.setFont(fontName);
848
- count++;
849
- }
850
- }
851
- return count;
852
- }
853
-
854
- /**
855
- * Sets font size for all runs in the cell
856
- *
857
- * Applies the specified font size to every text run in every paragraph
858
- * in this cell.
859
- *
860
- * @param size - Font size in half-points (e.g., 24 = 12pt)
861
- * @returns Number of runs modified
862
- *
863
- * @example
864
- * ```typescript
865
- * const count = cell.setAllRunsSize(22); // 11pt
866
- * ```
867
- */
868
- setAllRunsSize(size: number): number {
869
- let count = 0;
870
- for (const para of this.paragraphs) {
871
- for (const run of para.getRuns()) {
872
- run.setSize(size);
873
- count++;
874
- }
875
- }
876
- return count;
877
- }
878
-
879
- /**
880
- * Sets color for all runs in the cell
881
- *
882
- * Applies the specified color to every text run in every paragraph
883
- * in this cell.
884
- *
885
- * @param color - Hex color code (e.g., 'FF0000', '#0000FF')
886
- * @returns Number of runs modified
887
- *
888
- * @example
889
- * ```typescript
890
- * const count = cell.setAllRunsColor('000000'); // Black
891
- * ```
892
- */
893
- setAllRunsColor(color: string): number {
894
- let count = 0;
895
- for (const para of this.paragraphs) {
896
- for (const run of para.getRuns()) {
897
- run.setColor(color);
898
- count++;
899
- }
900
- }
901
- return count;
902
- }
903
-
904
- /**
905
- * Gets the cell formatting
906
- * @returns Cell formatting
907
- */
908
- getFormatting(): CellFormatting {
909
- return { ...this.formatting };
910
- }
911
-
912
- // ============================================================================
913
- // Individual Formatting Getters
914
- // ============================================================================
915
-
916
- /**
917
- * Gets the cell width in twips
918
- * @returns Width in twips or undefined if not set
919
- */
920
- getWidth(): number | undefined {
921
- return this.formatting.width;
922
- }
923
-
924
- /**
925
- * Gets the cell width type
926
- * @returns Width type ('auto', 'dxa', 'pct', 'nil') or undefined
927
- */
928
- getWidthType(): string | undefined {
929
- return this.formatting.widthType;
930
- }
931
-
932
- /**
933
- * Gets the vertical alignment of cell content
934
- * @returns Vertical alignment ('top', 'center', 'bottom') or undefined
935
- */
936
- getVerticalAlignment(): string | undefined {
937
- return this.formatting.verticalAlignment;
938
- }
939
-
940
- /**
941
- * Gets the vertical merge state
942
- * @returns Vertical merge state ('restart', 'continue') or undefined
943
- */
944
- getVerticalMerge(): VerticalMerge | undefined {
945
- return this.formatting.vMerge;
946
- }
947
-
948
- /**
949
- * Gets the legacy horizontal merge state
950
- * @returns Horizontal merge state ('restart', 'continue') or undefined
951
- */
952
- getHorizontalMerge(): 'restart' | 'continue' | undefined {
953
- return this.formatting.hMerge;
954
- }
955
-
956
- /**
957
- * Sets the cell headers attribute for accessibility
958
- * Links data cells to header cells per ECMA-376 Part 1 §17.4.26
959
- * @param headers - Space-separated list of header cell IDs
960
- * @returns This cell for chaining
961
- */
962
- setHeaders(headers: string): this {
963
- this.formatting.headers = headers;
964
- return this;
965
- }
966
-
967
- /**
968
- * Gets the cell headers attribute
969
- * @returns Headers string or undefined
970
- */
971
- getHeaders(): string | undefined {
972
- return this.formatting.headers;
973
- }
974
-
975
- /**
976
- * Gets the cell margins
977
- * @returns Margins object with top, right, bottom, left or undefined
978
- */
979
- getMargins(): CellMargins | undefined {
980
- return this.formatting.margins;
981
- }
982
-
983
- /**
984
- * Gets the cell borders
985
- * @returns Borders object or undefined
986
- */
987
- getBorders(): CellBorders | undefined {
988
- return this.formatting.borders;
989
- }
990
-
991
- /**
992
- * Gets the cell shading/background
993
- * @returns Shading object or undefined
994
- */
995
- getShading(): CellShading | undefined {
996
- return this.formatting.shading;
997
- }
998
-
999
- /**
1000
- * Gets the text direction for the cell
1001
- * @returns Text direction or undefined
1002
- */
1003
- getTextDirection(): string | undefined {
1004
- return this.formatting.textDirection;
1005
- }
1006
-
1007
- // ============================================================================
1008
- // RAW NESTED CONTENT (Tables, SDTs preserved as XML)
1009
- // ============================================================================
1010
-
1011
- /**
1012
- * Adds raw nested content (table or SDT) to the cell
1013
- * Used during parsing to preserve nested tables that cannot be fully modeled
1014
- * @param position - Paragraph index after which this content appears (0 = before first paragraph)
1015
- * @param xml - Raw XML content
1016
- * @param type - Type of content ('table' or 'sdt')
1017
- * @returns This cell for chaining
1018
- */
1019
- addRawNestedContent(
1020
- position: number,
1021
- xml: string,
1022
- type: "table" | "sdt" = "table"
1023
- ): this {
1024
- this.rawNestedContent.push({ position, xml, type });
1025
- return this;
1026
- }
1027
-
1028
- /**
1029
- * Gets all raw nested content in this cell
1030
- * @returns Array of raw nested content items
1031
- */
1032
- getRawNestedContent(): RawNestedContent[] {
1033
- return [...this.rawNestedContent];
1034
- }
1035
-
1036
- /**
1037
- * Checks if this cell has any nested tables
1038
- * @returns True if cell contains nested tables stored as raw XML
1039
- */
1040
- hasNestedTables(): boolean {
1041
- return this.rawNestedContent.some((c) => c.type === "table");
1042
- }
1043
-
1044
- /**
1045
- * Checks if this cell has any raw nested content (tables or SDTs)
1046
- * @returns True if cell contains any raw nested content
1047
- */
1048
- hasRawNestedContent(): boolean {
1049
- return this.rawNestedContent.length > 0;
1050
- }
1051
-
1052
- /**
1053
- * Clears all raw nested content from this cell
1054
- * @returns This cell for chaining
1055
- */
1056
- clearRawNestedContent(): this {
1057
- this.rawNestedContent = [];
1058
- return this;
1059
- }
1060
-
1061
- /**
1062
- * Updates raw nested content at a specific index
1063
- * Used for revision acceptance in nested tables
1064
- * @param index - Index in the rawNestedContent array
1065
- * @param xml - New XML content
1066
- * @returns True if updated, false if index out of bounds
1067
- */
1068
- updateRawNestedContent(index: number, xml: string): boolean {
1069
- if (index < 0 || index >= this.rawNestedContent.length) {
1070
- return false;
1071
- }
1072
- const item = this.rawNestedContent[index];
1073
- if (item) {
1074
- item.xml = xml;
1075
- return true;
1076
- }
1077
- return false;
1078
- }
1079
-
1080
- // ============================================================================
1081
- // TRAILING BLANK PARAGRAPH REMOVAL
1082
- // ============================================================================
1083
-
1084
- /**
1085
- * Removes trailing blank paragraphs from this cell.
1086
- * A trailing blank is a blank paragraph at the end of the cell, after all content.
1087
- * This respects ECMA-376 requirement of at least one paragraph per cell.
1088
- *
1089
- * @param options.ignorePreserveFlag - If true, removes trailing blanks even if marked preserved (default: false)
1090
- * @returns Number of paragraphs removed
1091
- *
1092
- * @example
1093
- * ```typescript
1094
- * // Remove trailing blanks, respecting preserve flags
1095
- * const removed = cell.removeTrailingBlankParagraphs();
1096
- *
1097
- * // Remove all trailing blanks, ignoring preserve flags
1098
- * const removed = cell.removeTrailingBlankParagraphs({ ignorePreserveFlag: true });
1099
- * ```
1100
- */
1101
- removeTrailingBlankParagraphs(options?: { ignorePreserveFlag?: boolean }): number {
1102
- let removed = 0;
1103
- const ignorePreserve = options?.ignorePreserveFlag ?? false;
1104
-
1105
- // Work backwards from end of paragraphs array
1106
- while (this.paragraphs.length > 1) {
1107
- const lastIndex = this.paragraphs.length - 1;
1108
- const lastPara = this.paragraphs[lastIndex];
1109
-
1110
- if (!lastPara) break;
1111
-
1112
- // Check if this is a blank paragraph
1113
- const isBlank = this.isParaBlank(lastPara);
1114
-
1115
- // Stop if not blank
1116
- if (!isBlank) break;
1117
-
1118
- // Stop if preserved and we're respecting preserve flags
1119
- if (!ignorePreserve && lastPara.isPreserved()) break;
1120
-
1121
- // Check if there's raw nested content positioned after this paragraph
1122
- // If so, we should NOT remove this trailing blank as it maintains structure
1123
- const hasContentAfter = this.rawNestedContent.some(
1124
- (item) => item.position >= lastIndex
1125
- );
1126
- if (hasContentAfter) break;
1127
-
1128
- this.removeParagraph(lastIndex);
1129
- removed++;
1130
- }
1131
-
1132
- return removed;
1133
- }
1134
-
1135
- /**
1136
- * Checks if a paragraph is blank (no meaningful content).
1137
- * A paragraph is considered blank if it has no text, images, shapes, hyperlinks, fields,
1138
- * cnfStyle (conditional formatting), or other structural elements.
1139
- *
1140
- * IMPORTANT: cnfStyle preservation is critical! When a paragraph with cnfStyle is removed,
1141
- * Word may apply default table style conditional formatting to the cell, causing unexpected
1142
- * shading changes. A paragraph with cnfStyle="000000000000" (no conditionals) keeps the cell
1143
- * from matching table style conditionals like firstRow or band1Horz.
1144
- *
1145
- * @private
1146
- */
1147
- private isParaBlank(para: Paragraph): boolean {
1148
- // Check for text content
1149
- const text = para.getText().trim();
1150
- if (text !== "") return false;
1151
-
1152
- // Check for cnfStyle (conditional formatting) - critical for shading preservation
1153
- // Even a "blank" cnfStyle like "000000000000" is meaningful as it prevents
1154
- // the cell from inheriting table style conditional formatting
1155
- const cnfStyle = para.getTableConditionalStyle();
1156
- if (cnfStyle && cnfStyle !== "") {
1157
- return false;
1158
- }
1159
-
1160
- // Check all content items for non-text elements
1161
- const content = para.getContent();
1162
- for (const item of content) {
1163
- // Cast to unknown first for safe duck-typing checks
1164
- const itemAny = item as unknown as Record<string, unknown>;
1165
-
1166
- // ImageRun check - ImageRun has getImageElement method
1167
- if (item && typeof itemAny.getImageElement === "function") {
1168
- return false;
1169
- }
1170
-
1171
- // Shape check - Shape has getShapeType method
1172
- if (item && typeof itemAny.getShapeType === "function") {
1173
- return false;
1174
- }
1175
-
1176
- // TextBox check - TextBox has getTextContent method
1177
- if (item && typeof itemAny.getTextContent === "function") {
1178
- return false;
1179
- }
1180
-
1181
- // Hyperlink check - Hyperlink has getUrl method
1182
- if (item && typeof itemAny.getUrl === "function") {
1183
- return false;
1184
- }
1185
-
1186
- // Field check - Field has getInstruction method
1187
- if (item && typeof itemAny.getInstruction === "function") {
1188
- return false;
1189
- }
1190
-
1191
- // Revision check - check if revision contains meaningful content
1192
- if (item && typeof itemAny.getText === "function") {
1193
- const itemText = (itemAny.getText as () => string)().trim();
1194
- if (itemText !== "") return false;
1195
-
1196
- // Also check if revision contains non-text elements (hyperlinks, images, shapes, textboxes)
1197
- if (typeof itemAny.getContent === "function") {
1198
- const revContent = (itemAny.getContent as () => unknown[])();
1199
- for (const revItem of revContent) {
1200
- const revItemAny = revItem as Record<string, unknown>;
1201
- // Check if revision content is a Hyperlink using duck typing (getUrl method)
1202
- if (revItem && typeof revItemAny.getUrl === "function") {
1203
- return false; // Revision contains hyperlink - not blank
1204
- }
1205
- // Check if revision content is an ImageRun (getImageElement method)
1206
- if (revItem && typeof revItemAny.getImageElement === "function") {
1207
- return false; // Revision contains image - not blank
1208
- }
1209
- // Check if revision content is a Shape (getShapeType method)
1210
- if (revItem && typeof revItemAny.getShapeType === "function") {
1211
- return false; // Revision contains shape - not blank
1212
- }
1213
- // Check if revision content is a TextBox (getTextContent method)
1214
- if (revItem && typeof revItemAny.getTextContent === "function") {
1215
- return false; // Revision contains textbox - not blank
1216
- }
1217
- }
1218
- }
1219
- }
1220
- }
1221
-
1222
- // Check for bookmarks (they count as content)
1223
- if (
1224
- para.getBookmarksStart().length > 0 ||
1225
- para.getBookmarksEnd().length > 0
1226
- ) {
1227
- return false;
1228
- }
1229
-
1230
- // Check for comments (start/end markers)
1231
- if (typeof para.getCommentsStart === "function") {
1232
- const commentsStart = para.getCommentsStart();
1233
- if (commentsStart && commentsStart.length > 0) {
1234
- return false;
1235
- }
1236
- }
1237
- if (typeof para.getCommentsEnd === "function") {
1238
- const commentsEnd = para.getCommentsEnd();
1239
- if (commentsEnd && commentsEnd.length > 0) {
1240
- return false;
1241
- }
1242
- }
1243
-
1244
- return true;
1245
- }
1246
-
1247
- /**
1248
- * Sets the parent row reference for this cell.
1249
- * Called by TableRow when adding cells.
1250
- * @internal
1251
- */
1252
- _setParentRow(row: import('./TableRow').TableRow | undefined): void {
1253
- this._parentRow = row;
1254
- }
1255
-
1256
- /**
1257
- * Gets the parent row reference for this cell.
1258
- * @internal
1259
- */
1260
- _getParentRow(): import('./TableRow').TableRow | undefined {
1261
- return this._parentRow;
1262
- }
1263
-
1264
- /**
1265
- * Gets the table style ID by traversing up the parent chain.
1266
- * @returns Table style ID or undefined if not in a table or no style set
1267
- */
1268
- getTableStyleId(): string | undefined {
1269
- const row = this._parentRow;
1270
- if (!row) return undefined;
1271
-
1272
- const table = row._getParentTable();
1273
- if (!table) return undefined;
1274
-
1275
- return table.getFormatting().style;
1276
- }
1277
-
1278
- /**
1279
- * Converts the cell to WordprocessingML XML element
1280
- * @returns XMLElement representing the cell
1281
- */
1282
- toXML(): XMLElement {
1283
- const tcPrChildren: XMLElement[] = [];
1284
-
1285
- // tcPr children ordered per ECMA-376 CT_TcPr:
1286
- // cnfStyle tcW → gridSpan → hMerge → vMerge → tcBorders → shd → noWrap → tcMar →
1287
- // textDirection → tcFitText → vAlign → hideMark →
1288
- // cellIns/cellDel/cellMerge → tcPrChange
1289
-
1290
- // cnfStyle - conditional formatting style (MUST be first child per CT_TcPr)
1291
- if (this.formatting.cnfStyle) {
1292
- tcPrChildren.push(
1293
- XMLBuilder.wSelf("cnfStyle", { "w:val": this.formatting.cnfStyle })
1294
- );
1295
- }
1296
-
1297
- // tcW - cell width
1298
- if (this.formatting.width !== undefined) {
1299
- const widthAttrs: Record<string, string | number> = {
1300
- "w:w": this.formatting.width,
1301
- "w:type": this.formatting.widthType || "dxa",
1302
- };
1303
- tcPrChildren.push(XMLBuilder.wSelf("tcW", widthAttrs));
1304
- }
1305
-
1306
- // gridSpan - column span
1307
- if (this.formatting.columnSpan && this.formatting.columnSpan > 1) {
1308
- tcPrChildren.push(
1309
- XMLBuilder.wSelf("gridSpan", { "w:val": this.formatting.columnSpan })
1310
- );
1311
- }
1312
-
1313
- // hMerge - legacy horizontal merge
1314
- if (this.formatting.hMerge) {
1315
- tcPrChildren.push(XMLBuilder.wSelf("hMerge", { "w:val": this.formatting.hMerge }));
1316
- }
1317
-
1318
- // vMerge - vertical merge
1319
- // Per OOXML, <w:vMerge/> without val means "continue" (default).
1320
- // Only "restart" needs an explicit w:val attribute.
1321
- if (this.formatting.vMerge) {
1322
- if (this.formatting.vMerge === "restart") {
1323
- tcPrChildren.push(XMLBuilder.wSelf("vMerge", { "w:val": "restart" }));
1324
- } else {
1325
- tcPrChildren.push(XMLBuilder.wSelf("vMerge"));
1326
- }
1327
- }
1328
-
1329
- // tcBorders - cell borders
1330
- if (this.formatting.borders) {
1331
- const borderElements: XMLElement[] = [];
1332
- const borders = this.formatting.borders;
1333
-
1334
- // Ordered per ECMA-376 CT_TcBorders: top, left, bottom, right, insideH, insideV, tl2br, tr2bl
1335
- if (borders.top) {
1336
- borderElements.push(XMLBuilder.createBorder("top", borders.top));
1337
- }
1338
- if (borders.left) {
1339
- borderElements.push(XMLBuilder.createBorder("left", borders.left));
1340
- }
1341
- if (borders.bottom) {
1342
- borderElements.push(XMLBuilder.createBorder("bottom", borders.bottom));
1343
- }
1344
- if (borders.right) {
1345
- borderElements.push(XMLBuilder.createBorder("right", borders.right));
1346
- }
1347
- if (borders.tl2br) {
1348
- borderElements.push(XMLBuilder.createBorder("tl2br", borders.tl2br));
1349
- }
1350
- if (borders.tr2bl) {
1351
- borderElements.push(XMLBuilder.createBorder("tr2bl", borders.tr2bl));
1352
- }
1353
-
1354
- if (borderElements.length > 0) {
1355
- tcPrChildren.push(XMLBuilder.w("tcBorders", undefined, borderElements));
1356
- }
1357
- }
1358
-
1359
- // shd - shading
1360
- if (this.formatting.shading) {
1361
- const shadingAttrs = buildShadingAttributes(this.formatting.shading);
1362
- if (Object.keys(shadingAttrs).length > 0) {
1363
- tcPrChildren.push(XMLBuilder.wSelf("shd", shadingAttrs));
1364
- }
1365
- }
1366
-
1367
- // noWrap
1368
- if (this.formatting.noWrap) {
1369
- tcPrChildren.push(XMLBuilder.wSelf("noWrap"));
1370
- }
1371
-
1372
- // tcMar - cell margins (ordered per CT_TcMar: top, left, bottom, right)
1373
- if (this.formatting.margins) {
1374
- const margins = this.formatting.margins;
1375
- const marginChildren: XMLElement[] = [];
1376
-
1377
- if (margins.top !== undefined) {
1378
- marginChildren.push(
1379
- XMLBuilder.wSelf("top", {
1380
- "w:w": margins.top.toString(),
1381
- "w:type": "dxa",
1382
- })
1383
- );
1384
- }
1385
- if (margins.left !== undefined) {
1386
- marginChildren.push(
1387
- XMLBuilder.wSelf("left", {
1388
- "w:w": margins.left.toString(),
1389
- "w:type": "dxa",
1390
- })
1391
- );
1392
- }
1393
- if (margins.bottom !== undefined) {
1394
- marginChildren.push(
1395
- XMLBuilder.wSelf("bottom", {
1396
- "w:w": margins.bottom.toString(),
1397
- "w:type": "dxa",
1398
- })
1399
- );
1400
- }
1401
- if (margins.right !== undefined) {
1402
- marginChildren.push(
1403
- XMLBuilder.wSelf("right", {
1404
- "w:w": margins.right.toString(),
1405
- "w:type": "dxa",
1406
- })
1407
- );
1408
- }
1409
-
1410
- if (marginChildren.length > 0) {
1411
- tcPrChildren.push(XMLBuilder.w("tcMar", undefined, marginChildren));
1412
- }
1413
- }
1414
-
1415
- // textDirection
1416
- if (this.formatting.textDirection) {
1417
- tcPrChildren.push(
1418
- XMLBuilder.wSelf("textDirection", {
1419
- "w:val": this.formatting.textDirection,
1420
- })
1421
- );
1422
- }
1423
-
1424
- // tcFitText
1425
- if (this.formatting.fitText) {
1426
- tcPrChildren.push(XMLBuilder.wSelf("tcFitText"));
1427
- }
1428
-
1429
- // vAlign - vertical alignment
1430
- if (this.formatting.verticalAlignment) {
1431
- tcPrChildren.push(
1432
- XMLBuilder.wSelf("vAlign", {
1433
- "w:val": this.formatting.verticalAlignment,
1434
- })
1435
- );
1436
- }
1437
-
1438
- // hideMark
1439
- if (this.formatting.hideMark) {
1440
- tcPrChildren.push(XMLBuilder.wSelf("hideMark"));
1441
- }
1442
-
1443
- // Note: w:headers (cell headers for accessibility) is defined in ECMA-376 Part 1 §17.4.26
1444
- // but is NOT included in the Transitional schema and fails OOXML validation.
1445
- // The property is preserved in memory for reading but not generated in XML.
1446
-
1447
- // Add cell revision markers (w:cellIns, w:cellDel, w:cellMerge) per ECMA-376 Part 1 §17.13.5.4-5.6
1448
- if (this.cellRevision) {
1449
- const revType = this.cellRevision.getType();
1450
- const attrs: Record<string, string | number> = {
1451
- "w:id": this.cellRevision.getId(),
1452
- "w:author": this.cellRevision.getAuthor(),
1453
- "w:date": formatDateForXml(this.cellRevision.getDate()),
1454
- };
1455
-
1456
- if (revType === "tableCellInsert") {
1457
- tcPrChildren.push(XMLBuilder.wSelf("cellIns", attrs));
1458
- } else if (revType === "tableCellDelete") {
1459
- tcPrChildren.push(XMLBuilder.wSelf("cellDel", attrs));
1460
- } else if (revType === "tableCellMerge") {
1461
- // Add vMerge and vMergeOrig attributes if present
1462
- // ST_AnnotationVMerge: "cont" | "rest" (not "continue" / "restart")
1463
- const mergeMap: Record<string, string> = { continue: "cont", restart: "rest" };
1464
- const prevProps = this.cellRevision.getPreviousProperties();
1465
- if (prevProps?.vMerge) {
1466
- attrs["w:vMerge"] = mergeMap[prevProps.vMerge] || prevProps.vMerge;
1467
- }
1468
- if (prevProps?.vMergeOrig) {
1469
- attrs["w:vMergeOrig"] = mergeMap[prevProps.vMergeOrig] || prevProps.vMergeOrig;
1470
- }
1471
- tcPrChildren.push(XMLBuilder.wSelf("cellMerge", attrs));
1472
- }
1473
- }
1474
-
1475
- // Add table cell property change (w:tcPrChange) per ECMA-376 Part 1 §17.13.5.37
1476
- // Must be last child of w:tcPr
1477
- if (this.tcPrChange) {
1478
- const changeAttrs: Record<string, string | number> = {
1479
- "w:id": this.tcPrChange.id,
1480
- "w:author": this.tcPrChange.author,
1481
- "w:date": this.tcPrChange.date,
1482
- };
1483
- const prevTcPrChildren: XMLElement[] = [];
1484
- const prev = this.tcPrChange.previousProperties;
1485
- if (prev) {
1486
- // Ordered per CT_TcPr: tcW → tcBorders → shd → noWrap → tcMar →
1487
- // textDirection → tcFitText → vAlign → hideMark → cnfStyle
1488
- if (prev.width !== undefined) {
1489
- prevTcPrChildren.push(XMLBuilder.wSelf("tcW", {
1490
- "w:w": prev.width,
1491
- "w:type": prev.widthType || "dxa",
1492
- }));
1493
- }
1494
- if (prev.borders) {
1495
- const borderElements: XMLElement[] = [];
1496
- if (prev.borders.top) borderElements.push(XMLBuilder.createBorder("top", prev.borders.top));
1497
- if (prev.borders.left) borderElements.push(XMLBuilder.createBorder("left", prev.borders.left));
1498
- if (prev.borders.bottom) borderElements.push(XMLBuilder.createBorder("bottom", prev.borders.bottom));
1499
- if (prev.borders.right) borderElements.push(XMLBuilder.createBorder("right", prev.borders.right));
1500
- if (borderElements.length > 0) {
1501
- prevTcPrChildren.push(XMLBuilder.w("tcBorders", undefined, borderElements));
1502
- }
1503
- }
1504
- if (prev.shading) {
1505
- const shadingAttrs = buildShadingAttributes(prev.shading);
1506
- if (Object.keys(shadingAttrs).length > 0) {
1507
- prevTcPrChildren.push(XMLBuilder.wSelf("shd", shadingAttrs));
1508
- }
1509
- }
1510
- if (prev.noWrap) {
1511
- prevTcPrChildren.push(XMLBuilder.wSelf("noWrap"));
1512
- }
1513
- if (prev.margins) {
1514
- const marginChildren: XMLElement[] = [];
1515
- if (prev.margins.top !== undefined) {
1516
- marginChildren.push(XMLBuilder.wSelf("top", { "w:w": prev.margins.top.toString(), "w:type": "dxa" }));
1517
- }
1518
- if (prev.margins.left !== undefined) {
1519
- marginChildren.push(XMLBuilder.wSelf("left", { "w:w": prev.margins.left.toString(), "w:type": "dxa" }));
1520
- }
1521
- if (prev.margins.bottom !== undefined) {
1522
- marginChildren.push(XMLBuilder.wSelf("bottom", { "w:w": prev.margins.bottom.toString(), "w:type": "dxa" }));
1523
- }
1524
- if (prev.margins.right !== undefined) {
1525
- marginChildren.push(XMLBuilder.wSelf("right", { "w:w": prev.margins.right.toString(), "w:type": "dxa" }));
1526
- }
1527
- if (marginChildren.length > 0) {
1528
- prevTcPrChildren.push(XMLBuilder.w("tcMar", undefined, marginChildren));
1529
- }
1530
- }
1531
- if (prev.textDirection) {
1532
- prevTcPrChildren.push(XMLBuilder.wSelf("textDirection", { "w:val": prev.textDirection }));
1533
- }
1534
- if (prev.fitText) {
1535
- prevTcPrChildren.push(XMLBuilder.wSelf("tcFitText"));
1536
- }
1537
- if (prev.verticalAlignment) {
1538
- prevTcPrChildren.push(XMLBuilder.wSelf("vAlign", { "w:val": prev.verticalAlignment }));
1539
- }
1540
- if (prev.hideMark) {
1541
- prevTcPrChildren.push(XMLBuilder.wSelf("hideMark"));
1542
- }
1543
- if (prev.cnfStyle) {
1544
- prevTcPrChildren.push(XMLBuilder.wSelf("cnfStyle", { "w:val": prev.cnfStyle }));
1545
- }
1546
- }
1547
- const prevTcPr = prevTcPrChildren.length > 0
1548
- ? XMLBuilder.w("tcPr", undefined, prevTcPrChildren)
1549
- : XMLBuilder.w("tcPr", undefined, []);
1550
- tcPrChildren.push(XMLBuilder.w("tcPrChange", changeAttrs, [prevTcPr]));
1551
- }
1552
-
1553
- // Build cell element
1554
- const cellChildren: XMLElement[] = [];
1555
-
1556
- // Add cell properties if there are any
1557
- if (tcPrChildren.length > 0) {
1558
- cellChildren.push(XMLBuilder.w("tcPr", undefined, tcPrChildren));
1559
- }
1560
-
1561
- // Add paragraphs and raw nested content in correct order
1562
- // Raw nested content (tables, SDTs) are interspersed with paragraphs
1563
- if (this.paragraphs.length > 0 || this.rawNestedContent.length > 0) {
1564
- // Sort raw content by position
1565
- const sortedRaw = [...this.rawNestedContent].sort(
1566
- (a, b) => a.position - b.position
1567
- );
1568
- let rawIndex = 0;
1569
-
1570
- for (let i = 0; i < this.paragraphs.length; i++) {
1571
- // Insert any raw content that comes before this paragraph (position <= i)
1572
- let rawItem = sortedRaw[rawIndex];
1573
- while (rawIndex < sortedRaw.length && rawItem && rawItem.position <= i) {
1574
- // Use __rawXml element for passthrough (supported by XMLBuilder)
1575
- cellChildren.push({
1576
- name: "__rawXml",
1577
- rawXml: rawItem.xml,
1578
- });
1579
- rawIndex++;
1580
- rawItem = sortedRaw[rawIndex];
1581
- }
1582
- const para = this.paragraphs[i];
1583
- if (para) {
1584
- cellChildren.push(para.toXML());
1585
- }
1586
- }
1587
-
1588
- // Insert any remaining raw content after all paragraphs
1589
- while (rawIndex < sortedRaw.length) {
1590
- const rawItem = sortedRaw[rawIndex];
1591
- if (rawItem) {
1592
- cellChildren.push({
1593
- name: "__rawXml",
1594
- rawXml: rawItem.xml,
1595
- });
1596
- }
1597
- rawIndex++;
1598
- }
1599
-
1600
- // If we only have raw content and no paragraphs, we need at least one empty paragraph
1601
- // per ECMA-376 (table cell must contain at least one block-level element)
1602
- if (this.paragraphs.length === 0) {
1603
- cellChildren.push(new Paragraph().toXML());
1604
- }
1605
- } else {
1606
- // Empty cell needs at least one empty paragraph
1607
- cellChildren.push(new Paragraph().toXML());
1608
- }
1609
-
1610
- return XMLBuilder.w("tc", undefined, cellChildren);
1611
- }
1612
-
1613
- /**
1614
- * Creates a new TableCell
1615
- * @param formatting - Cell formatting
1616
- * @returns New TableCell instance
1617
- */
1618
- static create(formatting?: CellFormatting): TableCell {
1619
- return new TableCell(formatting);
1620
- }
1621
- }
1
+ /**
2
+ * TableCell - Represents a cell in a table
3
+ */
4
+
5
+ import { formatDateForXml } from '../utils/dateFormatting';
6
+ import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
7
+ import { Paragraph, TextDirection } from './Paragraph';
8
+ import { Revision } from './Revision';
9
+ import {
10
+ BorderStyle as CommonBorderStyle,
11
+ BorderDefinition,
12
+ FourSidedBorders,
13
+ CellVerticalAlignment as CommonCellVerticalAlignment,
14
+ ShadingConfig,
15
+ ShadingPattern,
16
+ buildShadingAttributes,
17
+ WidthType,
18
+ } from './CommonTypes';
19
+
20
+ // ============================================================================
21
+ // RE-EXPORTED TYPES (for backward compatibility)
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Cell border style
26
+ * @see CommonTypes.BorderStyle
27
+ */
28
+ export type BorderStyle = CommonBorderStyle;
29
+
30
+ /**
31
+ * Cell border definition
32
+ * @see CommonTypes.BorderDefinition
33
+ */
34
+ export interface CellBorder {
35
+ style?: BorderStyle;
36
+ size?: number; // Size in eighths of a point
37
+ color?: string; // Hex color without #
38
+ }
39
+
40
+ /**
41
+ * Cell borders
42
+ * @see CommonTypes.FourSidedBorders
43
+ */
44
+ export interface CellBorders {
45
+ top?: CellBorder;
46
+ bottom?: CellBorder;
47
+ left?: CellBorder;
48
+ right?: CellBorder;
49
+ /** Diagonal border from top-left to bottom-right per ECMA-376 Part 1 §17.4.84 */
50
+ tl2br?: CellBorder;
51
+ /** Diagonal border from top-right to bottom-left per ECMA-376 Part 1 §17.4.85 */
52
+ tr2bl?: CellBorder;
53
+ }
54
+
55
+ /**
56
+ * Cell shading/background
57
+ * @see ShadingConfig in CommonTypes.ts for the canonical definition
58
+ */
59
+ export type CellShading = ShadingConfig;
60
+
61
+ /**
62
+ * Vertical alignment in cell
63
+ * @see CommonTypes.CellVerticalAlignment
64
+ */
65
+ export type CellVerticalAlignment = CommonCellVerticalAlignment;
66
+
67
+ /**
68
+ * Cell margins (spacing inside cell borders)
69
+ * Per ECMA-376 Part 1 §17.4.43
70
+ */
71
+ export interface CellMargins {
72
+ top?: number; // Margin in twips
73
+ bottom?: number; // Margin in twips
74
+ left?: number; // Margin in twips
75
+ right?: number; // Margin in twips
76
+ }
77
+
78
+ /**
79
+ * Cell width type
80
+ * Per ECMA-376 Part 1 §17.18.87
81
+ */
82
+ export type CellWidthType = 'auto' | 'dxa' | 'pct';
83
+
84
+ /**
85
+ * Vertical merge type for cells
86
+ * Per ECMA-376 Part 1 §17.4.85
87
+ */
88
+ export type VerticalMerge = 'restart' | 'continue';
89
+
90
+ /**
91
+ * Cell formatting options
92
+ */
93
+ export interface CellFormatting {
94
+ width?: number; // Width in twips
95
+ widthType?: CellWidthType; // Width type (auto, dxa, pct)
96
+ borders?: CellBorders;
97
+ shading?: CellShading;
98
+ verticalAlignment?: CellVerticalAlignment;
99
+ columnSpan?: number; // Number of columns to span (gridSpan)
100
+ rowSpan?: number; // Number of rows to span (gridSpan)
101
+ margins?: CellMargins; // Cell margins (spacing inside borders)
102
+ textDirection?: TextDirection; // Text flow direction
103
+ fitText?: boolean; // Fit text to cell width
104
+ noWrap?: boolean; // Prevent text wrapping
105
+ hideMark?: boolean; // Hide end-of-cell mark
106
+ cnfStyle?: string; // Conditional formatting style (14-char binary string)
107
+ vMerge?: VerticalMerge; // Vertical cell merge
108
+ hMerge?: 'restart' | 'continue'; // Legacy horizontal merge (w:hMerge) per ECMA-376 Part 1 §17.4.22
109
+ /** Cell headers attribute for accessibility per ECMA-376 Part 1 §17.4.26 */
110
+ headers?: string;
111
+ }
112
+
113
+ /**
114
+ * Raw nested content stored as XML to preserve nested tables/SDTs
115
+ * Position indicates after which paragraph index the content appears
116
+ */
117
+ export interface RawNestedContent {
118
+ position: number; // Content appears after this paragraph index
119
+ xml: string; // Raw XML content
120
+ type: 'table' | 'sdt'; // Type of nested content
121
+ }
122
+
123
+ /**
124
+ * Table cell property change tracking (w:tcPrChange)
125
+ * Per ECMA-376 Part 1 §17.13.5.37
126
+ */
127
+ export interface TcPrChange {
128
+ author: string;
129
+ date: string;
130
+ id: string;
131
+ previousProperties: Record<string, any>;
132
+ }
133
+
134
+ /**
135
+ * Represents a table cell
136
+ */
137
+ export class TableCell {
138
+ private paragraphs: Paragraph[] = [];
139
+ private formatting: CellFormatting;
140
+ /** Raw nested content (tables, SDTs) stored as XML for passthrough */
141
+ private rawNestedContent: RawNestedContent[] = [];
142
+ /** Parent row reference (if cell is inside a table row) */
143
+ private _parentRow?: import('./TableRow').TableRow;
144
+ /** Table cell revision (w:cellIns, w:cellDel, w:cellMerge) per ECMA-376 Part 1 §17.13.5.4-5.6 */
145
+ private cellRevision?: Revision;
146
+ /** Tracking context for automatic change tracking */
147
+ private trackingContext?: import('../tracking/TrackingContext').TrackingContext;
148
+ /** Table cell property change tracking (w:tcPrChange) */
149
+ private tcPrChange?: TcPrChange;
150
+
151
+ /**
152
+ * Creates a new TableCell
153
+ * @param formatting - Cell formatting options
154
+ */
155
+ constructor(formatting: CellFormatting = {}) {
156
+ this.formatting = formatting;
157
+ }
158
+
159
+ /**
160
+ * Sets the tracking context for automatic change tracking.
161
+ * Called by Document when track changes is enabled.
162
+ * @internal
163
+ */
164
+ _setTrackingContext(context: import('../tracking/TrackingContext').TrackingContext): void {
165
+ this.trackingContext = context;
166
+ }
167
+
168
+ /**
169
+ * Gets the table cell property change tracking info
170
+ * @returns tcPrChange or undefined
171
+ */
172
+ getTcPrChange(): TcPrChange | undefined {
173
+ return this.tcPrChange;
174
+ }
175
+
176
+ /**
177
+ * Sets the table cell property change tracking info
178
+ * @param change - Property change info or undefined to clear
179
+ */
180
+ setTcPrChange(change: TcPrChange | undefined): void {
181
+ this.tcPrChange = change;
182
+ }
183
+
184
+ /**
185
+ * Clears the table cell property change tracking
186
+ */
187
+ clearTcPrChange(): void {
188
+ this.tcPrChange = undefined;
189
+ }
190
+
191
+ /**
192
+ * Adds a paragraph to the cell
193
+ * @param paragraph - Paragraph to add
194
+ * @returns This cell for chaining
195
+ */
196
+ addParagraph(paragraph: Paragraph): this {
197
+ this.paragraphs.push(paragraph);
198
+ paragraph._setParentCell(this);
199
+ // Propagate StylesManager from table if available
200
+ const stylesManager = this._parentRow?._getParentTable()?._getStylesManager();
201
+ if (stylesManager) {
202
+ paragraph._setStylesManager(stylesManager);
203
+ }
204
+ return this;
205
+ }
206
+
207
+ /**
208
+ * Creates and adds a new paragraph with text
209
+ * @param text - Text content
210
+ * @returns The created paragraph
211
+ */
212
+ createParagraph(text?: string): Paragraph {
213
+ const para = new Paragraph();
214
+ if (text) {
215
+ para.addText(text);
216
+ }
217
+ this.paragraphs.push(para);
218
+ para._setParentCell(this);
219
+ // Propagate StylesManager from table if available
220
+ const stylesManager = this._parentRow?._getParentTable()?._getStylesManager();
221
+ if (stylesManager) {
222
+ para._setStylesManager(stylesManager);
223
+ }
224
+ return para;
225
+ }
226
+
227
+ /**
228
+ * Gets all paragraphs in the cell
229
+ * @returns Array of paragraphs
230
+ */
231
+ getParagraphs(): Paragraph[] {
232
+ return [...this.paragraphs];
233
+ }
234
+
235
+ /**
236
+ * Removes a paragraph at the specified index
237
+ * @param index - Index of paragraph to remove
238
+ * @returns True if removed, false if index out of bounds
239
+ */
240
+ removeParagraph(index: number): boolean {
241
+ if (index < 0 || index >= this.paragraphs.length) {
242
+ return false;
243
+ }
244
+
245
+ // When tracking enabled, wrap content in w:del instead of removing
246
+ if (this.trackingContext?.isEnabled()) {
247
+ const paragraph = this.paragraphs[index]!;
248
+ const runs = paragraph.getRuns();
249
+ if (runs.length > 0) {
250
+ const author = this.trackingContext.getAuthor();
251
+ const deletion = Revision.createDeletion(author, runs);
252
+ this.trackingContext.getRevisionManager().register(deletion);
253
+ paragraph.addRevision(deletion);
254
+ }
255
+ return true;
256
+ }
257
+
258
+ const removed = this.paragraphs.splice(index, 1);
259
+ const removedPara = removed[0];
260
+ if (removedPara) {
261
+ removedPara._setParentCell(undefined);
262
+ }
263
+
264
+ // Update raw nested content positions
265
+ // Any nested content positioned AFTER the removed paragraph needs its position decremented
266
+ // This maintains correct relative positioning for nested tables, SDTs, etc.
267
+ for (const item of this.rawNestedContent) {
268
+ if (item.position > index) {
269
+ item.position--;
270
+ }
271
+ }
272
+
273
+ return true;
274
+ }
275
+
276
+ /**
277
+ * Adds a paragraph at the specified index
278
+ * @param index - Index to insert at
279
+ * @param paragraph - Paragraph to add
280
+ * @returns This cell for chaining
281
+ */
282
+ addParagraphAt(index: number, paragraph: Paragraph): this {
283
+ if (index < 0) {
284
+ index = 0;
285
+ }
286
+
287
+ // Determine actual insertion index
288
+ const actualIndex = index >= this.paragraphs.length ? this.paragraphs.length : index;
289
+
290
+ if (index >= this.paragraphs.length) {
291
+ this.paragraphs.push(paragraph);
292
+ } else {
293
+ this.paragraphs.splice(index, 0, paragraph);
294
+ }
295
+
296
+ // Update raw nested content positions
297
+ // Any nested content positioned AT OR AFTER the insertion point needs its position incremented
298
+ // This maintains correct relative positioning for nested tables, SDTs, etc.
299
+ for (const item of this.rawNestedContent) {
300
+ if (item.position >= actualIndex) {
301
+ item.position++;
302
+ }
303
+ }
304
+
305
+ paragraph._setParentCell(this);
306
+ // Propagate StylesManager from table if available
307
+ const stylesManager = this._parentRow?._getParentTable()?._getStylesManager();
308
+ if (stylesManager) {
309
+ paragraph._setStylesManager(stylesManager);
310
+ }
311
+
312
+ // When tracking enabled, wrap paragraph content in w:ins revision
313
+ if (this.trackingContext?.isEnabled()) {
314
+ const runs = paragraph.getRuns();
315
+ if (runs.length > 0) {
316
+ const author = this.trackingContext.getAuthor();
317
+ const insertion = Revision.createInsertion(author, runs);
318
+ this.trackingContext.getRevisionManager().register(insertion);
319
+ paragraph.addRevision(insertion);
320
+ }
321
+ }
322
+
323
+ return this;
324
+ }
325
+
326
+ /**
327
+ * Gets the text content of all paragraphs
328
+ * @returns Combined text
329
+ */
330
+ getText(): string {
331
+ return this.paragraphs.map((p) => p.getText()).join('\n');
332
+ }
333
+
334
+ /**
335
+ * Gets all fields from paragraphs in this cell
336
+ *
337
+ * Collects all Field and ComplexField instances from every paragraph
338
+ * in the table cell.
339
+ *
340
+ * @returns Array of fields (Field and ComplexField instances)
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * const cell = table.getCell(0, 0);
345
+ * const fields = cell?.getFields() || [];
346
+ * console.log(`Cell has ${fields.length} fields`);
347
+ * ```
348
+ */
349
+ getFields(): (import('./Field').Field | import('./Field').ComplexField)[] {
350
+ const fields: (import('./Field').Field | import('./Field').ComplexField)[] = [];
351
+ for (const para of this.paragraphs) {
352
+ fields.push(...para.getFields());
353
+ }
354
+ return fields;
355
+ }
356
+
357
+ /**
358
+ * Finds fields matching a predicate function
359
+ *
360
+ * Searches through all fields in the cell and returns those matching
361
+ * the specified criteria.
362
+ *
363
+ * @param predicate - Function to test each field
364
+ * @returns Array of matching fields
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * // Find all PAGE fields
369
+ * const pageFields = cell.findFields(f =>
370
+ * f.getInstruction().startsWith('PAGE')
371
+ * );
372
+ *
373
+ * // Find fields with specific switches
374
+ * const mergeFields = cell.findFields(f =>
375
+ * f.getInstruction().includes('MERGEFIELD')
376
+ * );
377
+ * ```
378
+ */
379
+ findFields(
380
+ predicate: (field: import('./Field').Field | import('./Field').ComplexField) => boolean
381
+ ): (import('./Field').Field | import('./Field').ComplexField)[] {
382
+ return this.getFields().filter(predicate);
383
+ }
384
+
385
+ /**
386
+ * Removes all fields from paragraphs in this cell
387
+ *
388
+ * Iterates through each paragraph and removes all fields,
389
+ * preserving text content.
390
+ *
391
+ * @returns Count of fields removed
392
+ *
393
+ * @example
394
+ * ```typescript
395
+ * const count = cell.removeAllFields();
396
+ * console.log(`Removed ${count} fields from cell`);
397
+ * ```
398
+ */
399
+ removeAllFields(): number {
400
+ let count = 0;
401
+ for (const para of this.paragraphs) {
402
+ count += para.removeAllFields();
403
+ }
404
+ return count;
405
+ }
406
+
407
+ /**
408
+ * Sets cell width
409
+ * @param twips - Width in twips
410
+ * @returns This cell for chaining
411
+ */
412
+ setWidth(twips: number): this {
413
+ const prev = this.formatting.width;
414
+ this.formatting.width = twips;
415
+ if (this.trackingContext?.isEnabled() && prev !== twips) {
416
+ this.trackingContext.trackTableChange(this, 'width', prev, twips);
417
+ }
418
+ return this;
419
+ }
420
+
421
+ /**
422
+ * Sets cell borders
423
+ * @param borders - Border definitions
424
+ * @returns This cell for chaining
425
+ */
426
+ setBorders(borders: CellBorders): this {
427
+ const prev = this.formatting.borders;
428
+ this.formatting.borders = borders;
429
+ if (this.trackingContext?.isEnabled() && prev !== borders) {
430
+ this.trackingContext.trackTableChange(this, 'borders', prev, borders);
431
+ }
432
+ return this;
433
+ }
434
+
435
+ /**
436
+ * Sets cell shading/background
437
+ * @param shading - Shading definition
438
+ * @returns This cell for chaining
439
+ */
440
+ setShading(shading: CellShading): this {
441
+ const prev = this.formatting.shading;
442
+ this.formatting.shading = shading;
443
+ if (this.trackingContext?.isEnabled() && prev !== shading) {
444
+ this.trackingContext.trackTableChange(this, 'shading', prev, shading);
445
+ }
446
+ return this;
447
+ }
448
+
449
+ /**
450
+ * Sets vertical alignment
451
+ * @param alignment - Vertical alignment
452
+ * @returns This cell for chaining
453
+ */
454
+ setVerticalAlignment(alignment: CellVerticalAlignment): this {
455
+ const prev = this.formatting.verticalAlignment;
456
+ this.formatting.verticalAlignment = alignment;
457
+ if (this.trackingContext?.isEnabled() && prev !== alignment) {
458
+ this.trackingContext.trackTableChange(this, 'verticalAlignment', prev, alignment);
459
+ }
460
+ return this;
461
+ }
462
+
463
+ /**
464
+ * Sets column span (merge cells horizontally)
465
+ * @param span - Number of columns to span
466
+ * @returns This cell for chaining
467
+ */
468
+ setColumnSpan(span: number): this {
469
+ const prev = this.formatting.columnSpan;
470
+ this.formatting.columnSpan = span;
471
+ if (this.trackingContext?.isEnabled() && prev !== span) {
472
+ this.trackingContext.trackTableChange(this, 'columnSpan', prev, span);
473
+ }
474
+ return this;
475
+ }
476
+
477
+ /**
478
+ * Gets the number of columns this cell spans (gridSpan)
479
+ * @returns Column span, defaults to 1 if not set
480
+ */
481
+ getColumnSpan(): number {
482
+ return this.formatting.columnSpan || 1;
483
+ }
484
+
485
+ /**
486
+ * Gets whether to fit text to cell width
487
+ * @returns true if fit text is enabled, false otherwise
488
+ */
489
+ getFitText(): boolean {
490
+ return this.formatting.fitText ?? false;
491
+ }
492
+
493
+ /**
494
+ * Gets whether text wrapping is prevented in cell
495
+ * @returns true if no-wrap is enabled, false otherwise
496
+ */
497
+ getNoWrap(): boolean {
498
+ return this.formatting.noWrap ?? false;
499
+ }
500
+
501
+ /**
502
+ * Gets whether the end-of-cell mark is hidden
503
+ * @returns true if hidden, false otherwise
504
+ */
505
+ getHideMark(): boolean {
506
+ return this.formatting.hideMark ?? false;
507
+ }
508
+
509
+ /**
510
+ * Gets the conditional formatting style (cnfStyle) for this cell
511
+ * @returns 14-character binary string or undefined if not set
512
+ */
513
+ getCnfStyle(): string | undefined {
514
+ return this.formatting.cnfStyle;
515
+ }
516
+
517
+ /**
518
+ * Validates a margin value
519
+ * @param value - Margin value in twips
520
+ * @param side - Name of the margin side (for error messages)
521
+ * @throws {Error} If margin is negative or exceeds maximum
522
+ * @private
523
+ */
524
+ private validateMargin(value: number | undefined, side: string): void {
525
+ if (value === undefined) return;
526
+
527
+ // Margins must be non-negative
528
+ if (value < 0) {
529
+ throw new Error(`Invalid ${side} margin: ${value} twips. Cell margins cannot be negative.`);
530
+ }
531
+
532
+ // Maximum reasonable margin (1 inch = 1440 twips)
533
+ // Word typically allows up to several inches, but we set a reasonable limit
534
+ const MAX_MARGIN_TWIPS = 14400; // 10 inches
535
+ if (value > MAX_MARGIN_TWIPS) {
536
+ throw new Error(
537
+ `Invalid ${side} margin: ${value} twips exceeds maximum of ${MAX_MARGIN_TWIPS} twips (10 inches).`
538
+ );
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Sets cell margins (spacing inside cell borders)
544
+ * Per ECMA-376 Part 1 §17.4.43
545
+ * @param margins - Margin definitions for each side
546
+ * @returns This cell for chaining
547
+ * @throws {Error} If any margin value is negative or exceeds maximum
548
+ */
549
+ setMargins(margins: CellMargins): this {
550
+ // Validate each margin
551
+ this.validateMargin(margins.top, 'top');
552
+ this.validateMargin(margins.bottom, 'bottom');
553
+ this.validateMargin(margins.left, 'left');
554
+ this.validateMargin(margins.right, 'right');
555
+
556
+ const prev = this.formatting.margins;
557
+ this.formatting.margins = margins;
558
+ if (this.trackingContext?.isEnabled() && prev !== margins) {
559
+ this.trackingContext.trackTableChange(this, 'margins', prev, margins);
560
+ }
561
+ return this;
562
+ }
563
+
564
+ /**
565
+ * Sets all cell margins to the same value
566
+ * @param margin - Margin in twips to apply to all sides
567
+ * @returns This cell for chaining
568
+ * @throws {Error} If margin value is negative or exceeds maximum
569
+ */
570
+ setAllMargins(margin: number): this {
571
+ this.validateMargin(margin, 'all');
572
+ return this.setMargins({ top: margin, bottom: margin, left: margin, right: margin });
573
+ }
574
+
575
+ /**
576
+ * Sets text direction for cell content
577
+ * Per ECMA-376 Part 1 §17.4.72
578
+ * @param direction - Text flow direction
579
+ * - 'lrTb': Left-to-right, top-to-bottom (default)
580
+ * - 'tbRl': Top-to-bottom, right-to-left (vertical text, East Asian)
581
+ * - 'btLr': Bottom-to-top, left-to-right (vertical text)
582
+ * - 'lrTbV': Left-to-right, top-to-bottom, vertical
583
+ * - 'tbRlV': Top-to-bottom, right-to-left, vertical
584
+ * - 'tbLrV': Top-to-bottom, left-to-right, vertical
585
+ * @returns This cell for chaining
586
+ */
587
+ setTextDirection(direction: TextDirection): this {
588
+ const prev = this.formatting.textDirection;
589
+ this.formatting.textDirection = direction;
590
+ if (this.trackingContext?.isEnabled() && prev !== direction) {
591
+ this.trackingContext.trackTableChange(this, 'textDirection', prev, direction);
592
+ }
593
+ return this;
594
+ }
595
+
596
+ /**
597
+ * Sets whether to fit text to cell width
598
+ * Per ECMA-376 Part 1 §17.4.68
599
+ * @param fit - Whether to expand/compress text to fit cell width
600
+ * @returns This cell for chaining
601
+ */
602
+ setFitText(fit = true): this {
603
+ const prev = this.formatting.fitText;
604
+ this.formatting.fitText = fit;
605
+ if (this.trackingContext?.isEnabled() && prev !== fit) {
606
+ this.trackingContext.trackTableChange(this, 'fitText', prev, fit);
607
+ }
608
+ return this;
609
+ }
610
+
611
+ /**
612
+ * Sets whether to prevent text wrapping in cell
613
+ * Per ECMA-376 Part 1 §17.4.34
614
+ * @param noWrap - Whether to prevent wrapping (default: true)
615
+ * @returns This cell for chaining
616
+ */
617
+ setNoWrap(noWrap = true): this {
618
+ const prev = this.formatting.noWrap;
619
+ this.formatting.noWrap = noWrap;
620
+ if (this.trackingContext?.isEnabled() && prev !== noWrap) {
621
+ this.trackingContext.trackTableChange(this, 'noWrap', prev, noWrap);
622
+ }
623
+ return this;
624
+ }
625
+
626
+ /**
627
+ * Sets whether to hide the end-of-cell mark
628
+ * Per ECMA-376 Part 1 §17.4.24
629
+ * @param hide - Whether to ignore cell end mark in height calculations (default: true)
630
+ * @returns This cell for chaining
631
+ */
632
+ setHideMark(hide = true): this {
633
+ const prev = this.formatting.hideMark;
634
+ this.formatting.hideMark = hide;
635
+ if (this.trackingContext?.isEnabled() && prev !== hide) {
636
+ this.trackingContext.trackTableChange(this, 'hideMark', prev, hide);
637
+ }
638
+ return this;
639
+ }
640
+
641
+ /**
642
+ * Sets conditional formatting style for this cell
643
+ * Per ECMA-376 Part 1 §17.4.7
644
+ * @param cnfStyle - 14-character binary string representing which conditional formats to apply
645
+ * Each bit position controls a different conditional format (e.g., "100000000000" for first row)
646
+ * @returns This cell for chaining
647
+ */
648
+ setConditionalStyle(cnfStyle: string): this {
649
+ const prev = this.formatting.cnfStyle;
650
+ this.formatting.cnfStyle = cnfStyle;
651
+ if (this.trackingContext?.isEnabled() && prev !== cnfStyle) {
652
+ this.trackingContext.trackTableChange(this, 'cnfStyle', prev, cnfStyle);
653
+ }
654
+ return this;
655
+ }
656
+
657
+ /**
658
+ * Sets cell width with type specification
659
+ * Per ECMA-376 Part 1 §17.4.81
660
+ * @param width - Width value
661
+ * @param type - Width type: 'auto' (automatic), 'dxa' (twips), or 'pct' (percentage * 50)
662
+ * @returns This cell for chaining
663
+ */
664
+ setWidthType(width: number, type: CellWidthType = 'dxa'): this {
665
+ const prevWidth = this.formatting.width;
666
+ const prevType = this.formatting.widthType;
667
+ this.formatting.width = width;
668
+ this.formatting.widthType = type;
669
+ if (this.trackingContext?.isEnabled()) {
670
+ if (prevWidth !== width) {
671
+ this.trackingContext.trackTableChange(this, 'width', prevWidth, width);
672
+ }
673
+ if (prevType !== type) {
674
+ this.trackingContext.trackTableChange(this, 'widthType', prevType, type);
675
+ }
676
+ }
677
+ return this;
678
+ }
679
+
680
+ /**
681
+ * Sets vertical merge for this cell
682
+ * Per ECMA-376 Part 1 §17.4.85
683
+ * @param merge - Vertical merge type:
684
+ * - 'restart': Start a new vertically merged region (top cell)
685
+ * - 'continue': Continue the current vertically merged region (cells below)
686
+ * @returns This cell for chaining
687
+ */
688
+ setVerticalMerge(merge: VerticalMerge | undefined): this {
689
+ const prev = this.formatting.vMerge;
690
+ this.formatting.vMerge = merge;
691
+ if (this.trackingContext?.isEnabled() && prev !== merge) {
692
+ this.trackingContext.trackTableChange(this, 'vMerge', prev, merge);
693
+ }
694
+ return this;
695
+ }
696
+
697
+ /**
698
+ * Sets the legacy horizontal merge state per ECMA-376 Part 1 §17.4.22
699
+ * @param merge - 'restart' to start a new merge region, 'continue' to continue, or undefined to clear
700
+ * @returns This cell for chaining
701
+ */
702
+ setHorizontalMerge(merge: 'restart' | 'continue' | undefined): this {
703
+ const prev = this.formatting.hMerge;
704
+ this.formatting.hMerge = merge;
705
+ if (this.trackingContext?.isEnabled() && prev !== merge) {
706
+ this.trackingContext.trackTableChange(this, 'hMerge', prev, merge);
707
+ }
708
+ return this;
709
+ }
710
+
711
+ // ============================================================================
712
+ // CELL REVISIONS (w:cellIns, w:cellDel, w:cellMerge)
713
+ // ============================================================================
714
+
715
+ /**
716
+ * Sets the revision marker for this cell
717
+ * Per ECMA-376 Part 1 §17.13.5.4-5.6
718
+ *
719
+ * Table cell revisions track structural changes to table cells:
720
+ * - tableCellInsert (w:cellIns): Cell was inserted
721
+ * - tableCellDelete (w:cellDel): Cell was deleted
722
+ * - tableCellMerge (w:cellMerge): Cell merge/split operation
723
+ *
724
+ * @param revision - Revision marker for this cell
725
+ * @returns This cell for chaining
726
+ *
727
+ * @example
728
+ * ```typescript
729
+ * const revision = new Revision({
730
+ * id: 1,
731
+ * author: 'Alice',
732
+ * date: new Date(),
733
+ * type: 'tableCellInsert',
734
+ * content: [],
735
+ * });
736
+ * cell.setCellRevision(revision);
737
+ * ```
738
+ */
739
+ setCellRevision(revision: Revision): this {
740
+ this.cellRevision = revision;
741
+ return this;
742
+ }
743
+
744
+ /**
745
+ * Gets the revision marker for this cell
746
+ *
747
+ * @returns The cell revision if present, undefined otherwise
748
+ *
749
+ * @example
750
+ * ```typescript
751
+ * const revision = cell.getCellRevision();
752
+ * if (revision) {
753
+ * console.log(`Cell ${revision.getType()} by ${revision.getAuthor()}`);
754
+ * }
755
+ * ```
756
+ */
757
+ getCellRevision(): Revision | undefined {
758
+ return this.cellRevision;
759
+ }
760
+
761
+ /**
762
+ * Checks if this cell has a revision marker
763
+ *
764
+ * @returns True if cell has a revision (insert, delete, or merge)
765
+ */
766
+ hasCellRevision(): boolean {
767
+ return this.cellRevision !== undefined;
768
+ }
769
+
770
+ /**
771
+ * Clears the revision marker for this cell
772
+ *
773
+ * @returns This cell for chaining
774
+ */
775
+ clearCellRevision(): this {
776
+ this.cellRevision = undefined;
777
+ return this;
778
+ }
779
+
780
+ // ============================================================================
781
+ // CONVENIENCE METHODS (for easier paragraph manipulation)
782
+ // ============================================================================
783
+
784
+ /**
785
+ * Sets text alignment for all paragraphs in the cell
786
+ *
787
+ * Applies the specified horizontal alignment to every paragraph
788
+ * in this cell.
789
+ *
790
+ * @param alignment - Paragraph alignment (left, center, right, both)
791
+ * @returns This cell for chaining
792
+ *
793
+ * @example
794
+ * ```typescript
795
+ * cell.setTextAlignment('center');
796
+ * ```
797
+ */
798
+ setTextAlignment(alignment: import('./Paragraph').ParagraphAlignment): this {
799
+ for (const para of this.paragraphs) {
800
+ para.setAlignment(alignment);
801
+ }
802
+ return this;
803
+ }
804
+
805
+ /**
806
+ * Sets the style for all paragraphs in the cell
807
+ *
808
+ * Applies the specified style ID to every paragraph in this cell.
809
+ *
810
+ * @param styleId - Style ID to apply
811
+ * @returns This cell for chaining
812
+ *
813
+ * @example
814
+ * ```typescript
815
+ * cell.setAllParagraphsStyle('TableContent');
816
+ * ```
817
+ */
818
+ setAllParagraphsStyle(styleId: string): this {
819
+ for (const para of this.paragraphs) {
820
+ para.setStyle(styleId);
821
+ }
822
+ return this;
823
+ }
824
+
825
+ /**
826
+ * Sets font for all runs in the cell
827
+ *
828
+ * Applies the specified font to every text run in every paragraph
829
+ * in this cell.
830
+ *
831
+ * @param fontName - Font name to apply
832
+ * @returns Number of runs modified
833
+ *
834
+ * @example
835
+ * ```typescript
836
+ * const count = cell.setAllRunsFont('Arial');
837
+ * ```
838
+ */
839
+ setAllRunsFont(fontName: string): number {
840
+ let count = 0;
841
+ for (const para of this.paragraphs) {
842
+ for (const run of para.getRuns()) {
843
+ run.setFont(fontName);
844
+ count++;
845
+ }
846
+ }
847
+ return count;
848
+ }
849
+
850
+ /**
851
+ * Sets font size for all runs in the cell
852
+ *
853
+ * Applies the specified font size to every text run in every paragraph
854
+ * in this cell.
855
+ *
856
+ * @param size - Font size in half-points (e.g., 24 = 12pt)
857
+ * @returns Number of runs modified
858
+ *
859
+ * @example
860
+ * ```typescript
861
+ * const count = cell.setAllRunsSize(22); // 11pt
862
+ * ```
863
+ */
864
+ setAllRunsSize(size: number): number {
865
+ let count = 0;
866
+ for (const para of this.paragraphs) {
867
+ for (const run of para.getRuns()) {
868
+ run.setSize(size);
869
+ count++;
870
+ }
871
+ }
872
+ return count;
873
+ }
874
+
875
+ /**
876
+ * Sets color for all runs in the cell
877
+ *
878
+ * Applies the specified color to every text run in every paragraph
879
+ * in this cell.
880
+ *
881
+ * @param color - Hex color code (e.g., 'FF0000', '#0000FF')
882
+ * @returns Number of runs modified
883
+ *
884
+ * @example
885
+ * ```typescript
886
+ * const count = cell.setAllRunsColor('000000'); // Black
887
+ * ```
888
+ */
889
+ setAllRunsColor(color: string): number {
890
+ let count = 0;
891
+ for (const para of this.paragraphs) {
892
+ for (const run of para.getRuns()) {
893
+ run.setColor(color);
894
+ count++;
895
+ }
896
+ }
897
+ return count;
898
+ }
899
+
900
+ /**
901
+ * Gets the cell formatting
902
+ * @returns Cell formatting
903
+ */
904
+ getFormatting(): CellFormatting {
905
+ return { ...this.formatting };
906
+ }
907
+
908
+ // ============================================================================
909
+ // Individual Formatting Getters
910
+ // ============================================================================
911
+
912
+ /**
913
+ * Gets the cell width in twips
914
+ * @returns Width in twips or undefined if not set
915
+ */
916
+ getWidth(): number | undefined {
917
+ return this.formatting.width;
918
+ }
919
+
920
+ /**
921
+ * Gets the cell width type
922
+ * @returns Width type ('auto', 'dxa', 'pct', 'nil') or undefined
923
+ */
924
+ getWidthType(): string | undefined {
925
+ return this.formatting.widthType;
926
+ }
927
+
928
+ /**
929
+ * Gets the vertical alignment of cell content
930
+ * @returns Vertical alignment ('top', 'center', 'bottom') or undefined
931
+ */
932
+ getVerticalAlignment(): string | undefined {
933
+ return this.formatting.verticalAlignment;
934
+ }
935
+
936
+ /**
937
+ * Gets the vertical merge state
938
+ * @returns Vertical merge state ('restart', 'continue') or undefined
939
+ */
940
+ getVerticalMerge(): VerticalMerge | undefined {
941
+ return this.formatting.vMerge;
942
+ }
943
+
944
+ /**
945
+ * Gets the legacy horizontal merge state
946
+ * @returns Horizontal merge state ('restart', 'continue') or undefined
947
+ */
948
+ getHorizontalMerge(): 'restart' | 'continue' | undefined {
949
+ return this.formatting.hMerge;
950
+ }
951
+
952
+ /**
953
+ * Sets the cell headers attribute for accessibility
954
+ * Links data cells to header cells per ECMA-376 Part 1 §17.4.26
955
+ * @param headers - Space-separated list of header cell IDs
956
+ * @returns This cell for chaining
957
+ */
958
+ setHeaders(headers: string): this {
959
+ this.formatting.headers = headers;
960
+ return this;
961
+ }
962
+
963
+ /**
964
+ * Gets the cell headers attribute
965
+ * @returns Headers string or undefined
966
+ */
967
+ getHeaders(): string | undefined {
968
+ return this.formatting.headers;
969
+ }
970
+
971
+ /**
972
+ * Gets the cell margins
973
+ * @returns Margins object with top, right, bottom, left or undefined
974
+ */
975
+ getMargins(): CellMargins | undefined {
976
+ return this.formatting.margins;
977
+ }
978
+
979
+ /**
980
+ * Gets the cell borders
981
+ * @returns Borders object or undefined
982
+ */
983
+ getBorders(): CellBorders | undefined {
984
+ return this.formatting.borders;
985
+ }
986
+
987
+ /**
988
+ * Gets the cell shading/background
989
+ * @returns Shading object or undefined
990
+ */
991
+ getShading(): CellShading | undefined {
992
+ return this.formatting.shading;
993
+ }
994
+
995
+ /**
996
+ * Gets the text direction for the cell
997
+ * @returns Text direction or undefined
998
+ */
999
+ getTextDirection(): string | undefined {
1000
+ return this.formatting.textDirection;
1001
+ }
1002
+
1003
+ // ============================================================================
1004
+ // RAW NESTED CONTENT (Tables, SDTs preserved as XML)
1005
+ // ============================================================================
1006
+
1007
+ /**
1008
+ * Adds raw nested content (table or SDT) to the cell
1009
+ * Used during parsing to preserve nested tables that cannot be fully modeled
1010
+ * @param position - Paragraph index after which this content appears (0 = before first paragraph)
1011
+ * @param xml - Raw XML content
1012
+ * @param type - Type of content ('table' or 'sdt')
1013
+ * @returns This cell for chaining
1014
+ */
1015
+ addRawNestedContent(position: number, xml: string, type: 'table' | 'sdt' = 'table'): this {
1016
+ this.rawNestedContent.push({ position, xml, type });
1017
+ return this;
1018
+ }
1019
+
1020
+ /**
1021
+ * Gets all raw nested content in this cell
1022
+ * @returns Array of raw nested content items
1023
+ */
1024
+ getRawNestedContent(): RawNestedContent[] {
1025
+ return [...this.rawNestedContent];
1026
+ }
1027
+
1028
+ /**
1029
+ * Checks if this cell has any nested tables
1030
+ * @returns True if cell contains nested tables stored as raw XML
1031
+ */
1032
+ hasNestedTables(): boolean {
1033
+ return this.rawNestedContent.some((c) => c.type === 'table');
1034
+ }
1035
+
1036
+ /**
1037
+ * Checks if this cell has any raw nested content (tables or SDTs)
1038
+ * @returns True if cell contains any raw nested content
1039
+ */
1040
+ hasRawNestedContent(): boolean {
1041
+ return this.rawNestedContent.length > 0;
1042
+ }
1043
+
1044
+ /**
1045
+ * Clears all raw nested content from this cell
1046
+ * @returns This cell for chaining
1047
+ */
1048
+ clearRawNestedContent(): this {
1049
+ this.rawNestedContent = [];
1050
+ return this;
1051
+ }
1052
+
1053
+ /**
1054
+ * Updates raw nested content at a specific index
1055
+ * Used for revision acceptance in nested tables
1056
+ * @param index - Index in the rawNestedContent array
1057
+ * @param xml - New XML content
1058
+ * @returns True if updated, false if index out of bounds
1059
+ */
1060
+ updateRawNestedContent(index: number, xml: string): boolean {
1061
+ if (index < 0 || index >= this.rawNestedContent.length) {
1062
+ return false;
1063
+ }
1064
+ const item = this.rawNestedContent[index];
1065
+ if (item) {
1066
+ item.xml = xml;
1067
+ return true;
1068
+ }
1069
+ return false;
1070
+ }
1071
+
1072
+ // ============================================================================
1073
+ // TRAILING BLANK PARAGRAPH REMOVAL
1074
+ // ============================================================================
1075
+
1076
+ /**
1077
+ * Removes trailing blank paragraphs from this cell.
1078
+ * A trailing blank is a blank paragraph at the end of the cell, after all content.
1079
+ * This respects ECMA-376 requirement of at least one paragraph per cell.
1080
+ *
1081
+ * @param options.ignorePreserveFlag - If true, removes trailing blanks even if marked preserved (default: false)
1082
+ * @returns Number of paragraphs removed
1083
+ *
1084
+ * @example
1085
+ * ```typescript
1086
+ * // Remove trailing blanks, respecting preserve flags
1087
+ * const removed = cell.removeTrailingBlankParagraphs();
1088
+ *
1089
+ * // Remove all trailing blanks, ignoring preserve flags
1090
+ * const removed = cell.removeTrailingBlankParagraphs({ ignorePreserveFlag: true });
1091
+ * ```
1092
+ */
1093
+ removeTrailingBlankParagraphs(options?: { ignorePreserveFlag?: boolean }): number {
1094
+ let removed = 0;
1095
+ const ignorePreserve = options?.ignorePreserveFlag ?? false;
1096
+
1097
+ // Work backwards from end of paragraphs array
1098
+ while (this.paragraphs.length > 1) {
1099
+ const lastIndex = this.paragraphs.length - 1;
1100
+ const lastPara = this.paragraphs[lastIndex];
1101
+
1102
+ if (!lastPara) break;
1103
+
1104
+ // Check if this is a blank paragraph
1105
+ const isBlank = this.isParaBlank(lastPara);
1106
+
1107
+ // Stop if not blank
1108
+ if (!isBlank) break;
1109
+
1110
+ // Stop if preserved and we're respecting preserve flags
1111
+ if (!ignorePreserve && lastPara.isPreserved()) break;
1112
+
1113
+ // Check if there's raw nested content positioned after this paragraph
1114
+ // If so, we should NOT remove this trailing blank as it maintains structure
1115
+ const hasContentAfter = this.rawNestedContent.some((item) => item.position >= lastIndex);
1116
+ if (hasContentAfter) break;
1117
+
1118
+ this.removeParagraph(lastIndex);
1119
+ removed++;
1120
+ }
1121
+
1122
+ return removed;
1123
+ }
1124
+
1125
+ /**
1126
+ * Checks if a paragraph is blank (no meaningful content).
1127
+ * A paragraph is considered blank if it has no text, images, shapes, hyperlinks, fields,
1128
+ * cnfStyle (conditional formatting), or other structural elements.
1129
+ *
1130
+ * IMPORTANT: cnfStyle preservation is critical! When a paragraph with cnfStyle is removed,
1131
+ * Word may apply default table style conditional formatting to the cell, causing unexpected
1132
+ * shading changes. A paragraph with cnfStyle="000000000000" (no conditionals) keeps the cell
1133
+ * from matching table style conditionals like firstRow or band1Horz.
1134
+ *
1135
+ * @private
1136
+ */
1137
+ private isParaBlank(para: Paragraph): boolean {
1138
+ // Check for text content
1139
+ const text = para.getText().trim();
1140
+ if (text !== '') return false;
1141
+
1142
+ // Check for cnfStyle (conditional formatting) - critical for shading preservation
1143
+ // Even a "blank" cnfStyle like "000000000000" is meaningful as it prevents
1144
+ // the cell from inheriting table style conditional formatting
1145
+ const cnfStyle = para.getTableConditionalStyle();
1146
+ if (cnfStyle && cnfStyle !== '') {
1147
+ return false;
1148
+ }
1149
+
1150
+ // Check all content items for non-text elements
1151
+ const content = para.getContent();
1152
+ for (const item of content) {
1153
+ // Cast to unknown first for safe duck-typing checks
1154
+ const itemAny = item as unknown as Record<string, unknown>;
1155
+
1156
+ // ImageRun check - ImageRun has getImageElement method
1157
+ if (item && typeof itemAny.getImageElement === 'function') {
1158
+ return false;
1159
+ }
1160
+
1161
+ // Shape check - Shape has getShapeType method
1162
+ if (item && typeof itemAny.getShapeType === 'function') {
1163
+ return false;
1164
+ }
1165
+
1166
+ // TextBox check - TextBox has getTextContent method
1167
+ if (item && typeof itemAny.getTextContent === 'function') {
1168
+ return false;
1169
+ }
1170
+
1171
+ // Hyperlink check - Hyperlink has getUrl method
1172
+ if (item && typeof itemAny.getUrl === 'function') {
1173
+ return false;
1174
+ }
1175
+
1176
+ // Field check - Field has getInstruction method
1177
+ if (item && typeof itemAny.getInstruction === 'function') {
1178
+ return false;
1179
+ }
1180
+
1181
+ // Revision check - check if revision contains meaningful content
1182
+ if (item && typeof itemAny.getText === 'function') {
1183
+ const itemText = (itemAny.getText as () => string)().trim();
1184
+ if (itemText !== '') return false;
1185
+
1186
+ // Also check if revision contains non-text elements (hyperlinks, images, shapes, textboxes)
1187
+ if (typeof itemAny.getContent === 'function') {
1188
+ const revContent = (itemAny.getContent as () => unknown[])();
1189
+ for (const revItem of revContent) {
1190
+ const revItemAny = revItem as Record<string, unknown>;
1191
+ // Check if revision content is a Hyperlink using duck typing (getUrl method)
1192
+ if (revItem && typeof revItemAny.getUrl === 'function') {
1193
+ return false; // Revision contains hyperlink - not blank
1194
+ }
1195
+ // Check if revision content is an ImageRun (getImageElement method)
1196
+ if (revItem && typeof revItemAny.getImageElement === 'function') {
1197
+ return false; // Revision contains image - not blank
1198
+ }
1199
+ // Check if revision content is a Shape (getShapeType method)
1200
+ if (revItem && typeof revItemAny.getShapeType === 'function') {
1201
+ return false; // Revision contains shape - not blank
1202
+ }
1203
+ // Check if revision content is a TextBox (getTextContent method)
1204
+ if (revItem && typeof revItemAny.getTextContent === 'function') {
1205
+ return false; // Revision contains textbox - not blank
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ // Check for bookmarks (they count as content)
1213
+ if (para.getBookmarksStart().length > 0 || para.getBookmarksEnd().length > 0) {
1214
+ return false;
1215
+ }
1216
+
1217
+ // Check for comments (start/end markers)
1218
+ if (typeof para.getCommentsStart === 'function') {
1219
+ const commentsStart = para.getCommentsStart();
1220
+ if (commentsStart && commentsStart.length > 0) {
1221
+ return false;
1222
+ }
1223
+ }
1224
+ if (typeof para.getCommentsEnd === 'function') {
1225
+ const commentsEnd = para.getCommentsEnd();
1226
+ if (commentsEnd && commentsEnd.length > 0) {
1227
+ return false;
1228
+ }
1229
+ }
1230
+
1231
+ return true;
1232
+ }
1233
+
1234
+ /**
1235
+ * Sets the parent row reference for this cell.
1236
+ * Called by TableRow when adding cells.
1237
+ * @internal
1238
+ */
1239
+ _setParentRow(row: import('./TableRow').TableRow | undefined): void {
1240
+ this._parentRow = row;
1241
+ }
1242
+
1243
+ /**
1244
+ * Gets the parent row reference for this cell.
1245
+ * @internal
1246
+ */
1247
+ _getParentRow(): import('./TableRow').TableRow | undefined {
1248
+ return this._parentRow;
1249
+ }
1250
+
1251
+ /**
1252
+ * Gets the table style ID by traversing up the parent chain.
1253
+ * @returns Table style ID or undefined if not in a table or no style set
1254
+ */
1255
+ getTableStyleId(): string | undefined {
1256
+ const row = this._parentRow;
1257
+ if (!row) return undefined;
1258
+
1259
+ const table = row._getParentTable();
1260
+ if (!table) return undefined;
1261
+
1262
+ return table.getFormatting().style;
1263
+ }
1264
+
1265
+ /**
1266
+ * Converts the cell to WordprocessingML XML element
1267
+ * @returns XMLElement representing the cell
1268
+ */
1269
+ toXML(): XMLElement {
1270
+ const tcPrChildren: XMLElement[] = [];
1271
+
1272
+ // tcPr children ordered per ECMA-376 CT_TcPr:
1273
+ // cnfStyle tcW → gridSpan → hMerge → vMerge → tcBorders → shd → noWrap → tcMar →
1274
+ // textDirection → tcFitText → vAlign → hideMark →
1275
+ // cellIns/cellDel/cellMerge → tcPrChange
1276
+
1277
+ // cnfStyle - conditional formatting style (MUST be first child per CT_TcPr)
1278
+ if (this.formatting.cnfStyle) {
1279
+ tcPrChildren.push(XMLBuilder.wSelf('cnfStyle', { 'w:val': this.formatting.cnfStyle }));
1280
+ }
1281
+
1282
+ // tcW - cell width
1283
+ if (this.formatting.width !== undefined) {
1284
+ const widthAttrs: Record<string, string | number> = {
1285
+ 'w:w': this.formatting.width,
1286
+ 'w:type': this.formatting.widthType || 'dxa',
1287
+ };
1288
+ tcPrChildren.push(XMLBuilder.wSelf('tcW', widthAttrs));
1289
+ }
1290
+
1291
+ // gridSpan - column span
1292
+ if (this.formatting.columnSpan && this.formatting.columnSpan > 1) {
1293
+ tcPrChildren.push(XMLBuilder.wSelf('gridSpan', { 'w:val': this.formatting.columnSpan }));
1294
+ }
1295
+
1296
+ // hMerge - legacy horizontal merge
1297
+ if (this.formatting.hMerge) {
1298
+ tcPrChildren.push(XMLBuilder.wSelf('hMerge', { 'w:val': this.formatting.hMerge }));
1299
+ }
1300
+
1301
+ // vMerge - vertical merge
1302
+ // Per OOXML, <w:vMerge/> without val means "continue" (default).
1303
+ // Only "restart" needs an explicit w:val attribute.
1304
+ if (this.formatting.vMerge) {
1305
+ if (this.formatting.vMerge === 'restart') {
1306
+ tcPrChildren.push(XMLBuilder.wSelf('vMerge', { 'w:val': 'restart' }));
1307
+ } else {
1308
+ tcPrChildren.push(XMLBuilder.wSelf('vMerge'));
1309
+ }
1310
+ }
1311
+
1312
+ // tcBorders - cell borders
1313
+ if (this.formatting.borders) {
1314
+ const borderElements: XMLElement[] = [];
1315
+ const borders = this.formatting.borders;
1316
+
1317
+ // Ordered per ECMA-376 CT_TcBorders: top, left, bottom, right, insideH, insideV, tl2br, tr2bl
1318
+ if (borders.top) {
1319
+ borderElements.push(XMLBuilder.createBorder('top', borders.top));
1320
+ }
1321
+ if (borders.left) {
1322
+ borderElements.push(XMLBuilder.createBorder('left', borders.left));
1323
+ }
1324
+ if (borders.bottom) {
1325
+ borderElements.push(XMLBuilder.createBorder('bottom', borders.bottom));
1326
+ }
1327
+ if (borders.right) {
1328
+ borderElements.push(XMLBuilder.createBorder('right', borders.right));
1329
+ }
1330
+ if (borders.tl2br) {
1331
+ borderElements.push(XMLBuilder.createBorder('tl2br', borders.tl2br));
1332
+ }
1333
+ if (borders.tr2bl) {
1334
+ borderElements.push(XMLBuilder.createBorder('tr2bl', borders.tr2bl));
1335
+ }
1336
+
1337
+ if (borderElements.length > 0) {
1338
+ tcPrChildren.push(XMLBuilder.w('tcBorders', undefined, borderElements));
1339
+ }
1340
+ }
1341
+
1342
+ // shd - shading
1343
+ if (this.formatting.shading) {
1344
+ const shadingAttrs = buildShadingAttributes(this.formatting.shading);
1345
+ if (Object.keys(shadingAttrs).length > 0) {
1346
+ tcPrChildren.push(XMLBuilder.wSelf('shd', shadingAttrs));
1347
+ }
1348
+ }
1349
+
1350
+ // noWrap
1351
+ if (this.formatting.noWrap) {
1352
+ tcPrChildren.push(XMLBuilder.wSelf('noWrap'));
1353
+ }
1354
+
1355
+ // tcMar - cell margins (ordered per CT_TcMar: top, left, bottom, right)
1356
+ if (this.formatting.margins) {
1357
+ const margins = this.formatting.margins;
1358
+ const marginChildren: XMLElement[] = [];
1359
+
1360
+ if (margins.top !== undefined) {
1361
+ marginChildren.push(
1362
+ XMLBuilder.wSelf('top', {
1363
+ 'w:w': margins.top.toString(),
1364
+ 'w:type': 'dxa',
1365
+ })
1366
+ );
1367
+ }
1368
+ if (margins.left !== undefined) {
1369
+ marginChildren.push(
1370
+ XMLBuilder.wSelf('left', {
1371
+ 'w:w': margins.left.toString(),
1372
+ 'w:type': 'dxa',
1373
+ })
1374
+ );
1375
+ }
1376
+ if (margins.bottom !== undefined) {
1377
+ marginChildren.push(
1378
+ XMLBuilder.wSelf('bottom', {
1379
+ 'w:w': margins.bottom.toString(),
1380
+ 'w:type': 'dxa',
1381
+ })
1382
+ );
1383
+ }
1384
+ if (margins.right !== undefined) {
1385
+ marginChildren.push(
1386
+ XMLBuilder.wSelf('right', {
1387
+ 'w:w': margins.right.toString(),
1388
+ 'w:type': 'dxa',
1389
+ })
1390
+ );
1391
+ }
1392
+
1393
+ if (marginChildren.length > 0) {
1394
+ tcPrChildren.push(XMLBuilder.w('tcMar', undefined, marginChildren));
1395
+ }
1396
+ }
1397
+
1398
+ // textDirection
1399
+ if (this.formatting.textDirection) {
1400
+ tcPrChildren.push(
1401
+ XMLBuilder.wSelf('textDirection', {
1402
+ 'w:val': this.formatting.textDirection,
1403
+ })
1404
+ );
1405
+ }
1406
+
1407
+ // tcFitText
1408
+ if (this.formatting.fitText) {
1409
+ tcPrChildren.push(XMLBuilder.wSelf('tcFitText'));
1410
+ }
1411
+
1412
+ // vAlign - vertical alignment
1413
+ if (this.formatting.verticalAlignment) {
1414
+ tcPrChildren.push(
1415
+ XMLBuilder.wSelf('vAlign', {
1416
+ 'w:val': this.formatting.verticalAlignment,
1417
+ })
1418
+ );
1419
+ }
1420
+
1421
+ // hideMark
1422
+ if (this.formatting.hideMark) {
1423
+ tcPrChildren.push(XMLBuilder.wSelf('hideMark'));
1424
+ }
1425
+
1426
+ // Note: w:headers (cell headers for accessibility) is defined in ECMA-376 Part 1 §17.4.26
1427
+ // but is NOT included in the Transitional schema and fails OOXML validation.
1428
+ // The property is preserved in memory for reading but not generated in XML.
1429
+
1430
+ // Add cell revision markers (w:cellIns, w:cellDel, w:cellMerge) per ECMA-376 Part 1 §17.13.5.4-5.6
1431
+ if (this.cellRevision) {
1432
+ const revType = this.cellRevision.getType();
1433
+ const attrs: Record<string, string | number> = {
1434
+ 'w:id': this.cellRevision.getId(),
1435
+ 'w:author': this.cellRevision.getAuthor(),
1436
+ 'w:date': formatDateForXml(this.cellRevision.getDate()),
1437
+ };
1438
+
1439
+ if (revType === 'tableCellInsert') {
1440
+ tcPrChildren.push(XMLBuilder.wSelf('cellIns', attrs));
1441
+ } else if (revType === 'tableCellDelete') {
1442
+ tcPrChildren.push(XMLBuilder.wSelf('cellDel', attrs));
1443
+ } else if (revType === 'tableCellMerge') {
1444
+ // Add vMerge and vMergeOrig attributes if present
1445
+ // ST_AnnotationVMerge: "cont" | "rest" (not "continue" / "restart")
1446
+ const mergeMap: Record<string, string> = { continue: 'cont', restart: 'rest' };
1447
+ const prevProps = this.cellRevision.getPreviousProperties();
1448
+ if (prevProps?.vMerge) {
1449
+ attrs['w:vMerge'] = mergeMap[prevProps.vMerge] || prevProps.vMerge;
1450
+ }
1451
+ if (prevProps?.vMergeOrig) {
1452
+ attrs['w:vMergeOrig'] = mergeMap[prevProps.vMergeOrig] || prevProps.vMergeOrig;
1453
+ }
1454
+ tcPrChildren.push(XMLBuilder.wSelf('cellMerge', attrs));
1455
+ }
1456
+ }
1457
+
1458
+ // Add table cell property change (w:tcPrChange) per ECMA-376 Part 1 §17.13.5.37
1459
+ // Must be last child of w:tcPr
1460
+ if (this.tcPrChange) {
1461
+ const changeAttrs: Record<string, string | number> = {
1462
+ 'w:id': this.tcPrChange.id,
1463
+ 'w:author': this.tcPrChange.author,
1464
+ 'w:date': this.tcPrChange.date,
1465
+ };
1466
+ const prevTcPrChildren: XMLElement[] = [];
1467
+ const prev = this.tcPrChange.previousProperties;
1468
+ if (prev) {
1469
+ // Ordered per CT_TcPr: tcW tcBorders → shd → noWrap → tcMar →
1470
+ // textDirection → tcFitText → vAlign → hideMark → cnfStyle
1471
+ if (prev.width !== undefined) {
1472
+ prevTcPrChildren.push(
1473
+ XMLBuilder.wSelf('tcW', {
1474
+ 'w:w': prev.width,
1475
+ 'w:type': prev.widthType || 'dxa',
1476
+ })
1477
+ );
1478
+ }
1479
+ if (prev.borders) {
1480
+ const borderElements: XMLElement[] = [];
1481
+ if (prev.borders.top)
1482
+ borderElements.push(XMLBuilder.createBorder('top', prev.borders.top));
1483
+ if (prev.borders.left)
1484
+ borderElements.push(XMLBuilder.createBorder('left', prev.borders.left));
1485
+ if (prev.borders.bottom)
1486
+ borderElements.push(XMLBuilder.createBorder('bottom', prev.borders.bottom));
1487
+ if (prev.borders.right)
1488
+ borderElements.push(XMLBuilder.createBorder('right', prev.borders.right));
1489
+ if (borderElements.length > 0) {
1490
+ prevTcPrChildren.push(XMLBuilder.w('tcBorders', undefined, borderElements));
1491
+ }
1492
+ }
1493
+ if (prev.shading) {
1494
+ const shadingAttrs = buildShadingAttributes(prev.shading);
1495
+ if (Object.keys(shadingAttrs).length > 0) {
1496
+ prevTcPrChildren.push(XMLBuilder.wSelf('shd', shadingAttrs));
1497
+ }
1498
+ }
1499
+ if (prev.noWrap) {
1500
+ prevTcPrChildren.push(XMLBuilder.wSelf('noWrap'));
1501
+ }
1502
+ if (prev.margins) {
1503
+ const marginChildren: XMLElement[] = [];
1504
+ if (prev.margins.top !== undefined) {
1505
+ marginChildren.push(
1506
+ XMLBuilder.wSelf('top', { 'w:w': prev.margins.top.toString(), 'w:type': 'dxa' })
1507
+ );
1508
+ }
1509
+ if (prev.margins.left !== undefined) {
1510
+ marginChildren.push(
1511
+ XMLBuilder.wSelf('left', { 'w:w': prev.margins.left.toString(), 'w:type': 'dxa' })
1512
+ );
1513
+ }
1514
+ if (prev.margins.bottom !== undefined) {
1515
+ marginChildren.push(
1516
+ XMLBuilder.wSelf('bottom', { 'w:w': prev.margins.bottom.toString(), 'w:type': 'dxa' })
1517
+ );
1518
+ }
1519
+ if (prev.margins.right !== undefined) {
1520
+ marginChildren.push(
1521
+ XMLBuilder.wSelf('right', { 'w:w': prev.margins.right.toString(), 'w:type': 'dxa' })
1522
+ );
1523
+ }
1524
+ if (marginChildren.length > 0) {
1525
+ prevTcPrChildren.push(XMLBuilder.w('tcMar', undefined, marginChildren));
1526
+ }
1527
+ }
1528
+ if (prev.textDirection) {
1529
+ prevTcPrChildren.push(XMLBuilder.wSelf('textDirection', { 'w:val': prev.textDirection }));
1530
+ }
1531
+ if (prev.fitText) {
1532
+ prevTcPrChildren.push(XMLBuilder.wSelf('tcFitText'));
1533
+ }
1534
+ if (prev.verticalAlignment) {
1535
+ prevTcPrChildren.push(XMLBuilder.wSelf('vAlign', { 'w:val': prev.verticalAlignment }));
1536
+ }
1537
+ if (prev.hideMark) {
1538
+ prevTcPrChildren.push(XMLBuilder.wSelf('hideMark'));
1539
+ }
1540
+ if (prev.cnfStyle) {
1541
+ prevTcPrChildren.push(XMLBuilder.wSelf('cnfStyle', { 'w:val': prev.cnfStyle }));
1542
+ }
1543
+ }
1544
+ const prevTcPr =
1545
+ prevTcPrChildren.length > 0
1546
+ ? XMLBuilder.w('tcPr', undefined, prevTcPrChildren)
1547
+ : XMLBuilder.w('tcPr', undefined, []);
1548
+ tcPrChildren.push(XMLBuilder.w('tcPrChange', changeAttrs, [prevTcPr]));
1549
+ }
1550
+
1551
+ // Build cell element
1552
+ const cellChildren: XMLElement[] = [];
1553
+
1554
+ // Add cell properties if there are any
1555
+ if (tcPrChildren.length > 0) {
1556
+ cellChildren.push(XMLBuilder.w('tcPr', undefined, tcPrChildren));
1557
+ }
1558
+
1559
+ // Add paragraphs and raw nested content in correct order
1560
+ // Raw nested content (tables, SDTs) are interspersed with paragraphs
1561
+ if (this.paragraphs.length > 0 || this.rawNestedContent.length > 0) {
1562
+ // Sort raw content by position
1563
+ const sortedRaw = [...this.rawNestedContent].sort((a, b) => a.position - b.position);
1564
+ let rawIndex = 0;
1565
+
1566
+ for (let i = 0; i < this.paragraphs.length; i++) {
1567
+ // Insert any raw content that comes before this paragraph (position <= i)
1568
+ let rawItem = sortedRaw[rawIndex];
1569
+ while (rawIndex < sortedRaw.length && rawItem && rawItem.position <= i) {
1570
+ // Use __rawXml element for passthrough (supported by XMLBuilder)
1571
+ cellChildren.push({
1572
+ name: '__rawXml',
1573
+ rawXml: rawItem.xml,
1574
+ });
1575
+ rawIndex++;
1576
+ rawItem = sortedRaw[rawIndex];
1577
+ }
1578
+ const para = this.paragraphs[i];
1579
+ if (para) {
1580
+ cellChildren.push(para.toXML());
1581
+ }
1582
+ }
1583
+
1584
+ // Insert any remaining raw content after all paragraphs
1585
+ while (rawIndex < sortedRaw.length) {
1586
+ const rawItem = sortedRaw[rawIndex];
1587
+ if (rawItem) {
1588
+ cellChildren.push({
1589
+ name: '__rawXml',
1590
+ rawXml: rawItem.xml,
1591
+ });
1592
+ }
1593
+ rawIndex++;
1594
+ }
1595
+
1596
+ // If we only have raw content and no paragraphs, we need at least one empty paragraph
1597
+ // per ECMA-376 (table cell must contain at least one block-level element)
1598
+ if (this.paragraphs.length === 0) {
1599
+ cellChildren.push(new Paragraph().toXML());
1600
+ }
1601
+ } else {
1602
+ // Empty cell needs at least one empty paragraph
1603
+ cellChildren.push(new Paragraph().toXML());
1604
+ }
1605
+
1606
+ return XMLBuilder.w('tc', undefined, cellChildren);
1607
+ }
1608
+
1609
+ /**
1610
+ * Creates a new TableCell
1611
+ * @param formatting - Cell formatting
1612
+ * @returns New TableCell instance
1613
+ */
1614
+ static create(formatting?: CellFormatting): TableCell {
1615
+ return new TableCell(formatting);
1616
+ }
1617
+ }