docxmlater 11.0.10 → 12.0.1

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 (354) hide show
  1. package/README.md +51 -13
  2. package/dist/core/Document.d.ts +28 -2
  3. package/dist/core/Document.d.ts.map +1 -1
  4. package/dist/core/Document.js +725 -170
  5. package/dist/core/Document.js.map +1 -1
  6. package/dist/core/DocumentGenerator.d.ts +1 -0
  7. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  8. package/dist/core/DocumentGenerator.js +77 -25
  9. package/dist/core/DocumentGenerator.js.map +1 -1
  10. package/dist/core/DocumentParser.d.ts +7 -3
  11. package/dist/core/DocumentParser.d.ts.map +1 -1
  12. package/dist/core/DocumentParser.js +427 -81
  13. package/dist/core/DocumentParser.js.map +1 -1
  14. package/dist/core/RelationshipManager.d.ts.map +1 -1
  15. package/dist/core/RelationshipManager.js +2 -2
  16. package/dist/core/RelationshipManager.js.map +1 -1
  17. package/dist/elements/Comment.d.ts +15 -1
  18. package/dist/elements/Comment.d.ts.map +1 -1
  19. package/dist/elements/Comment.js +67 -12
  20. package/dist/elements/Comment.js.map +1 -1
  21. package/dist/elements/CommentManager.d.ts +4 -0
  22. package/dist/elements/CommentManager.d.ts.map +1 -1
  23. package/dist/elements/CommentManager.js +74 -16
  24. package/dist/elements/CommentManager.js.map +1 -1
  25. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  26. package/dist/elements/EndnoteManager.js.map +1 -1
  27. package/dist/elements/Field.d.ts +10 -1
  28. package/dist/elements/Field.d.ts.map +1 -1
  29. package/dist/elements/Field.js +76 -34
  30. package/dist/elements/Field.js.map +1 -1
  31. package/dist/elements/FieldHelpers.d.ts +1 -0
  32. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  33. package/dist/elements/FieldHelpers.js +8 -4
  34. package/dist/elements/FieldHelpers.js.map +1 -1
  35. package/dist/elements/Footer.d.ts +3 -1
  36. package/dist/elements/Footer.d.ts.map +1 -1
  37. package/dist/elements/Footer.js +39 -53
  38. package/dist/elements/Footer.js.map +1 -1
  39. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  40. package/dist/elements/FootnoteManager.js.map +1 -1
  41. package/dist/elements/Header.d.ts +3 -1
  42. package/dist/elements/Header.d.ts.map +1 -1
  43. package/dist/elements/Header.js +39 -53
  44. package/dist/elements/Header.js.map +1 -1
  45. package/dist/elements/HeaderFooterManager.d.ts +4 -2
  46. package/dist/elements/HeaderFooterManager.d.ts.map +1 -1
  47. package/dist/elements/HeaderFooterManager.js +36 -6
  48. package/dist/elements/HeaderFooterManager.js.map +1 -1
  49. package/dist/elements/Hyperlink.d.ts +6 -1
  50. package/dist/elements/Hyperlink.d.ts.map +1 -1
  51. package/dist/elements/Hyperlink.js +69 -44
  52. package/dist/elements/Hyperlink.js.map +1 -1
  53. package/dist/elements/Image.d.ts.map +1 -1
  54. package/dist/elements/Image.js +33 -6
  55. package/dist/elements/Image.js.map +1 -1
  56. package/dist/elements/ImageManager.d.ts +1 -0
  57. package/dist/elements/ImageManager.d.ts.map +1 -1
  58. package/dist/elements/ImageManager.js +11 -0
  59. package/dist/elements/ImageManager.js.map +1 -1
  60. package/dist/elements/Paragraph.d.ts +3 -0
  61. package/dist/elements/Paragraph.d.ts.map +1 -1
  62. package/dist/elements/Paragraph.js +91 -26
  63. package/dist/elements/Paragraph.js.map +1 -1
  64. package/dist/elements/Revision.d.ts +4 -0
  65. package/dist/elements/Revision.d.ts.map +1 -1
  66. package/dist/elements/Revision.js +369 -29
  67. package/dist/elements/Revision.js.map +1 -1
  68. package/dist/elements/Run.d.ts +1 -0
  69. package/dist/elements/Run.d.ts.map +1 -1
  70. package/dist/elements/Run.js +24 -4
  71. package/dist/elements/Run.js.map +1 -1
  72. package/dist/elements/Section.d.ts.map +1 -1
  73. package/dist/elements/Section.js +17 -7
  74. package/dist/elements/Section.js.map +1 -1
  75. package/dist/elements/Shape.d.ts.map +1 -1
  76. package/dist/elements/Shape.js +50 -42
  77. package/dist/elements/Shape.js.map +1 -1
  78. package/dist/elements/StructuredDocumentTag.d.ts +9 -1
  79. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  80. package/dist/elements/StructuredDocumentTag.js +46 -6
  81. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  82. package/dist/elements/Table.d.ts +2 -0
  83. package/dist/elements/Table.d.ts.map +1 -1
  84. package/dist/elements/Table.js +90 -42
  85. package/dist/elements/Table.js.map +1 -1
  86. package/dist/elements/TableCell.d.ts +1 -0
  87. package/dist/elements/TableCell.d.ts.map +1 -1
  88. package/dist/elements/TableCell.js +32 -13
  89. package/dist/elements/TableCell.js.map +1 -1
  90. package/dist/elements/TableOfContents.d.ts.map +1 -1
  91. package/dist/elements/TableOfContents.js +5 -7
  92. package/dist/elements/TableOfContents.js.map +1 -1
  93. package/dist/elements/TableRow.d.ts +1 -0
  94. package/dist/elements/TableRow.d.ts.map +1 -1
  95. package/dist/elements/TableRow.js +16 -1
  96. package/dist/elements/TableRow.js.map +1 -1
  97. package/dist/elements/TextBox.d.ts.map +1 -1
  98. package/dist/elements/TextBox.js +55 -42
  99. package/dist/elements/TextBox.js.map +1 -1
  100. package/dist/esm/core/Document.js +726 -171
  101. package/dist/esm/core/Document.js.map +1 -1
  102. package/dist/esm/core/DocumentGenerator.js +77 -25
  103. package/dist/esm/core/DocumentGenerator.js.map +1 -1
  104. package/dist/esm/core/DocumentParser.js +427 -81
  105. package/dist/esm/core/DocumentParser.js.map +1 -1
  106. package/dist/esm/core/RelationshipManager.js +2 -2
  107. package/dist/esm/core/RelationshipManager.js.map +1 -1
  108. package/dist/esm/elements/Comment.js +67 -12
  109. package/dist/esm/elements/Comment.js.map +1 -1
  110. package/dist/esm/elements/CommentManager.js +74 -16
  111. package/dist/esm/elements/CommentManager.js.map +1 -1
  112. package/dist/esm/elements/EndnoteManager.js.map +1 -1
  113. package/dist/esm/elements/Field.js +77 -35
  114. package/dist/esm/elements/Field.js.map +1 -1
  115. package/dist/esm/elements/FieldHelpers.js +7 -4
  116. package/dist/esm/elements/FieldHelpers.js.map +1 -1
  117. package/dist/esm/elements/Footer.js +39 -53
  118. package/dist/esm/elements/Footer.js.map +1 -1
  119. package/dist/esm/elements/FootnoteManager.js.map +1 -1
  120. package/dist/esm/elements/Header.js +39 -53
  121. package/dist/esm/elements/Header.js.map +1 -1
  122. package/dist/esm/elements/HeaderFooterManager.js +36 -6
  123. package/dist/esm/elements/HeaderFooterManager.js.map +1 -1
  124. package/dist/esm/elements/Hyperlink.js +69 -44
  125. package/dist/esm/elements/Hyperlink.js.map +1 -1
  126. package/dist/esm/elements/Image.js +33 -6
  127. package/dist/esm/elements/Image.js.map +1 -1
  128. package/dist/esm/elements/ImageManager.js +11 -0
  129. package/dist/esm/elements/ImageManager.js.map +1 -1
  130. package/dist/esm/elements/Paragraph.js +91 -26
  131. package/dist/esm/elements/Paragraph.js.map +1 -1
  132. package/dist/esm/elements/Revision.js +369 -29
  133. package/dist/esm/elements/Revision.js.map +1 -1
  134. package/dist/esm/elements/Run.js +24 -4
  135. package/dist/esm/elements/Run.js.map +1 -1
  136. package/dist/esm/elements/Section.js +17 -7
  137. package/dist/esm/elements/Section.js.map +1 -1
  138. package/dist/esm/elements/Shape.js +50 -42
  139. package/dist/esm/elements/Shape.js.map +1 -1
  140. package/dist/esm/elements/StructuredDocumentTag.js +46 -6
  141. package/dist/esm/elements/StructuredDocumentTag.js.map +1 -1
  142. package/dist/esm/elements/Table.js +90 -42
  143. package/dist/esm/elements/Table.js.map +1 -1
  144. package/dist/esm/elements/TableCell.js +32 -13
  145. package/dist/esm/elements/TableCell.js.map +1 -1
  146. package/dist/esm/elements/TableOfContents.js +5 -7
  147. package/dist/esm/elements/TableOfContents.js.map +1 -1
  148. package/dist/esm/elements/TableRow.js +16 -1
  149. package/dist/esm/elements/TableRow.js.map +1 -1
  150. package/dist/esm/elements/TextBox.js +55 -42
  151. package/dist/esm/elements/TextBox.js.map +1 -1
  152. package/dist/esm/formatting/AbstractNumbering.js +1 -1
  153. package/dist/esm/formatting/AbstractNumbering.js.map +1 -1
  154. package/dist/esm/formatting/NumberingLevel.js +8 -6
  155. package/dist/esm/formatting/NumberingLevel.js.map +1 -1
  156. package/dist/esm/formatting/NumberingManager.js +16 -1
  157. package/dist/esm/formatting/NumberingManager.js.map +1 -1
  158. package/dist/esm/formatting/Style.js +14 -0
  159. package/dist/esm/formatting/Style.js.map +1 -1
  160. package/dist/esm/formatting/StylesManager.js +14 -2
  161. package/dist/esm/formatting/StylesManager.js.map +1 -1
  162. package/dist/esm/helpers/CleanupHelper.js +103 -26
  163. package/dist/esm/helpers/CleanupHelper.js.map +1 -1
  164. package/dist/esm/images/ImageOptimizer.js +52 -4
  165. package/dist/esm/images/ImageOptimizer.js.map +1 -1
  166. package/dist/esm/index.js +1 -0
  167. package/dist/esm/index.js.map +1 -1
  168. package/dist/esm/internal.js +1 -1
  169. package/dist/esm/internal.js.map +1 -1
  170. package/dist/esm/processors/CompatibilityUpgrader.js +7 -1
  171. package/dist/esm/processors/CompatibilityUpgrader.js.map +1 -1
  172. package/dist/esm/processors/InMemoryRevisionAcceptor.js +134 -13
  173. package/dist/esm/processors/InMemoryRevisionAcceptor.js.map +1 -1
  174. package/dist/esm/processors/RevisionWalker.js +58 -8
  175. package/dist/esm/processors/RevisionWalker.js.map +1 -1
  176. package/dist/esm/processors/SelectiveRevisionAcceptor.js +116 -17
  177. package/dist/esm/processors/SelectiveRevisionAcceptor.js.map +1 -1
  178. package/dist/esm/processors/cnfStyleDecoder.js +7 -7
  179. package/dist/esm/processors/cnfStyleDecoder.js.map +1 -1
  180. package/dist/esm/processors/stripTrackedChanges.js +16 -5
  181. package/dist/esm/processors/stripTrackedChanges.js.map +1 -1
  182. package/dist/esm/utils/deepClone.js +24 -11
  183. package/dist/esm/utils/deepClone.js.map +1 -1
  184. package/dist/esm/utils/units.js +1 -1
  185. package/dist/esm/utils/units.js.map +1 -1
  186. package/dist/esm/utils/validation.js +11 -23
  187. package/dist/esm/utils/validation.js.map +1 -1
  188. package/dist/esm/utils/xmlSanitization.js +131 -7
  189. package/dist/esm/utils/xmlSanitization.js.map +1 -1
  190. package/dist/esm/validation/RevisionAutoFixer.js +69 -55
  191. package/dist/esm/validation/RevisionAutoFixer.js.map +1 -1
  192. package/dist/esm/validation/RevisionValidator.js +24 -20
  193. package/dist/esm/validation/RevisionValidator.js.map +1 -1
  194. package/dist/esm/xml/XMLBuilder.js +52 -20
  195. package/dist/esm/xml/XMLBuilder.js.map +1 -1
  196. package/dist/esm/xml/XMLParser.js +55 -13
  197. package/dist/esm/xml/XMLParser.js.map +1 -1
  198. package/dist/esm/zip/ZipHandler.js +37 -13
  199. package/dist/esm/zip/ZipHandler.js.map +1 -1
  200. package/dist/esm/zip/ZipReader.js +53 -6
  201. package/dist/esm/zip/ZipReader.js.map +1 -1
  202. package/dist/esm/zip/errors.js +7 -0
  203. package/dist/esm/zip/errors.js.map +1 -1
  204. package/dist/esm/zip/types.js +4 -0
  205. package/dist/esm/zip/types.js.map +1 -1
  206. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  207. package/dist/formatting/AbstractNumbering.js +1 -1
  208. package/dist/formatting/AbstractNumbering.js.map +1 -1
  209. package/dist/formatting/NumberingLevel.d.ts +1 -1
  210. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  211. package/dist/formatting/NumberingLevel.js +8 -6
  212. package/dist/formatting/NumberingLevel.js.map +1 -1
  213. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  214. package/dist/formatting/NumberingManager.js +16 -1
  215. package/dist/formatting/NumberingManager.js.map +1 -1
  216. package/dist/formatting/Style.d.ts.map +1 -1
  217. package/dist/formatting/Style.js +14 -0
  218. package/dist/formatting/Style.js.map +1 -1
  219. package/dist/formatting/StylesManager.d.ts +2 -0
  220. package/dist/formatting/StylesManager.d.ts.map +1 -1
  221. package/dist/formatting/StylesManager.js +14 -2
  222. package/dist/formatting/StylesManager.js.map +1 -1
  223. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  224. package/dist/helpers/CleanupHelper.js +103 -26
  225. package/dist/helpers/CleanupHelper.js.map +1 -1
  226. package/dist/images/ImageOptimizer.d.ts.map +1 -1
  227. package/dist/images/ImageOptimizer.js +52 -4
  228. package/dist/images/ImageOptimizer.js.map +1 -1
  229. package/dist/index.d.ts +3 -1
  230. package/dist/index.d.ts.map +1 -1
  231. package/dist/index.js +4 -2
  232. package/dist/index.js.map +1 -1
  233. package/dist/internal.d.ts +1 -1
  234. package/dist/internal.d.ts.map +1 -1
  235. package/dist/internal.js +2 -1
  236. package/dist/internal.js.map +1 -1
  237. package/dist/processors/CompatibilityUpgrader.d.ts.map +1 -1
  238. package/dist/processors/CompatibilityUpgrader.js +7 -1
  239. package/dist/processors/CompatibilityUpgrader.js.map +1 -1
  240. package/dist/processors/InMemoryRevisionAcceptor.d.ts +1 -0
  241. package/dist/processors/InMemoryRevisionAcceptor.d.ts.map +1 -1
  242. package/dist/processors/InMemoryRevisionAcceptor.js +135 -13
  243. package/dist/processors/InMemoryRevisionAcceptor.js.map +1 -1
  244. package/dist/processors/RevisionWalker.d.ts +2 -0
  245. package/dist/processors/RevisionWalker.d.ts.map +1 -1
  246. package/dist/processors/RevisionWalker.js +58 -8
  247. package/dist/processors/RevisionWalker.js.map +1 -1
  248. package/dist/processors/SelectiveRevisionAcceptor.d.ts.map +1 -1
  249. package/dist/processors/SelectiveRevisionAcceptor.js +116 -17
  250. package/dist/processors/SelectiveRevisionAcceptor.js.map +1 -1
  251. package/dist/processors/cnfStyleDecoder.d.ts.map +1 -1
  252. package/dist/processors/cnfStyleDecoder.js +7 -7
  253. package/dist/processors/cnfStyleDecoder.js.map +1 -1
  254. package/dist/processors/stripTrackedChanges.d.ts.map +1 -1
  255. package/dist/processors/stripTrackedChanges.js +16 -5
  256. package/dist/processors/stripTrackedChanges.js.map +1 -1
  257. package/dist/utils/deepClone.d.ts.map +1 -1
  258. package/dist/utils/deepClone.js +24 -11
  259. package/dist/utils/deepClone.js.map +1 -1
  260. package/dist/utils/units.d.ts.map +1 -1
  261. package/dist/utils/units.js +1 -1
  262. package/dist/utils/units.js.map +1 -1
  263. package/dist/utils/validation.d.ts.map +1 -1
  264. package/dist/utils/validation.js +11 -23
  265. package/dist/utils/validation.js.map +1 -1
  266. package/dist/utils/xmlSanitization.d.ts +2 -0
  267. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  268. package/dist/utils/xmlSanitization.js +133 -7
  269. package/dist/utils/xmlSanitization.js.map +1 -1
  270. package/dist/validation/RevisionAutoFixer.d.ts +6 -2
  271. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  272. package/dist/validation/RevisionAutoFixer.js +68 -54
  273. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  274. package/dist/validation/RevisionValidator.d.ts +2 -0
  275. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  276. package/dist/validation/RevisionValidator.js +26 -21
  277. package/dist/validation/RevisionValidator.js.map +1 -1
  278. package/dist/xml/XMLBuilder.d.ts +1 -0
  279. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  280. package/dist/xml/XMLBuilder.js +52 -20
  281. package/dist/xml/XMLBuilder.js.map +1 -1
  282. package/dist/xml/XMLParser.d.ts +1 -0
  283. package/dist/xml/XMLParser.d.ts.map +1 -1
  284. package/dist/xml/XMLParser.js +55 -13
  285. package/dist/xml/XMLParser.js.map +1 -1
  286. package/dist/zip/ZipHandler.d.ts.map +1 -1
  287. package/dist/zip/ZipHandler.js +37 -13
  288. package/dist/zip/ZipHandler.js.map +1 -1
  289. package/dist/zip/ZipReader.d.ts.map +1 -1
  290. package/dist/zip/ZipReader.js +52 -5
  291. package/dist/zip/ZipReader.js.map +1 -1
  292. package/dist/zip/errors.d.ts +3 -0
  293. package/dist/zip/errors.d.ts.map +1 -1
  294. package/dist/zip/errors.js +9 -1
  295. package/dist/zip/errors.js.map +1 -1
  296. package/dist/zip/types.d.ts +4 -0
  297. package/dist/zip/types.d.ts.map +1 -1
  298. package/dist/zip/types.js +4 -0
  299. package/dist/zip/types.js.map +1 -1
  300. package/package.json +2 -2
  301. package/src/core/Document.ts +1159 -240
  302. package/src/core/DocumentGenerator.ts +106 -26
  303. package/src/core/DocumentParser.ts +681 -114
  304. package/src/core/RelationshipManager.ts +446 -442
  305. package/src/elements/Comment.ts +123 -18
  306. package/src/elements/CommentManager.ts +121 -22
  307. package/src/elements/EndnoteManager.ts +4 -1
  308. package/src/elements/Field.ts +134 -43
  309. package/src/elements/FieldHelpers.ts +20 -4
  310. package/src/elements/Footer.ts +62 -67
  311. package/src/elements/FootnoteManager.ts +4 -1
  312. package/src/elements/Header.ts +62 -67
  313. package/src/elements/HeaderFooterManager.ts +56 -8
  314. package/src/elements/Hyperlink.ts +150 -55
  315. package/src/elements/Image.ts +51 -7
  316. package/src/elements/ImageManager.ts +24 -0
  317. package/src/elements/Paragraph.ts +161 -40
  318. package/src/elements/Revision.ts +446 -47
  319. package/src/elements/Run.ts +44 -6
  320. package/src/elements/Section.ts +25 -9
  321. package/src/elements/Shape.ts +52 -44
  322. package/src/elements/StructuredDocumentTag.ts +83 -8
  323. package/src/elements/Table.ts +151 -50
  324. package/src/elements/TableCell.ts +57 -17
  325. package/src/elements/TableOfContents.ts +10 -9
  326. package/src/elements/TableRow.ts +35 -3
  327. package/src/elements/TextBox.ts +58 -43
  328. package/src/formatting/AbstractNumbering.ts +4 -2
  329. package/src/formatting/NumberingLevel.ts +50 -12
  330. package/src/formatting/NumberingManager.ts +32 -3
  331. package/src/formatting/Style.ts +14 -0
  332. package/src/formatting/StylesManager.ts +29 -2
  333. package/src/helpers/CleanupHelper.ts +122 -30
  334. package/src/images/ImageOptimizer.ts +64 -5
  335. package/src/index.ts +7 -0
  336. package/src/internal.ts +1 -0
  337. package/src/processors/CompatibilityUpgrader.ts +15 -2
  338. package/src/processors/InMemoryRevisionAcceptor.ts +276 -17
  339. package/src/processors/RevisionWalker.ts +83 -7
  340. package/src/processors/SelectiveRevisionAcceptor.ts +164 -28
  341. package/src/processors/cnfStyleDecoder.ts +18 -12
  342. package/src/processors/stripTrackedChanges.ts +47 -10
  343. package/src/utils/deepClone.ts +48 -14
  344. package/src/utils/units.ts +3 -1
  345. package/src/utils/validation.ts +30 -32
  346. package/src/utils/xmlSanitization.ts +218 -22
  347. package/src/validation/RevisionAutoFixer.ts +109 -65
  348. package/src/validation/RevisionValidator.ts +48 -23
  349. package/src/xml/XMLBuilder.ts +82 -22
  350. package/src/xml/XMLParser.ts +87 -20
  351. package/src/zip/ZipHandler.ts +61 -21
  352. package/src/zip/ZipReader.ts +135 -13
  353. package/src/zip/errors.ts +15 -0
  354. package/src/zip/types.ts +44 -2
@@ -66,6 +66,13 @@ export interface HyperlinkProperties {
66
66
  text?: string;
67
67
  /** Text formatting */
68
68
  formatting?: RunFormatting;
69
+ /**
70
+ * Use the supplied formatting exactly as given instead of layering it over
71
+ * the default hyperlink styling. Set when the formatting comes from parsed
72
+ * XML, so load→save does not inject direct formatting that overrides the
73
+ * document's Hyperlink character style.
74
+ */
75
+ preserveFormatting?: boolean;
69
76
  /** Tooltip text */
70
77
  tooltip?: string;
71
78
  /** Relationship ID (set by Document when saving) */
@@ -87,7 +94,12 @@ export class Hyperlink {
87
94
  private url?: string;
88
95
  private anchor?: string;
89
96
  private text: string;
90
- private run: Run;
97
+ /**
98
+ * Display-text runs. Word splits hyperlink text into multiple runs when
99
+ * formatting varies mid-link, so a single-run model would drop per-run
100
+ * formatting on round-trip. Always holds at least one run.
101
+ */
102
+ private runs: Run[];
91
103
  private tooltip?: string;
92
104
  private relationshipId?: string;
93
105
  private formatting: RunFormatting;
@@ -137,7 +149,7 @@ export class Hyperlink {
137
149
  if (this._isEmpty) {
138
150
  this.text = '';
139
151
  this.formatting = {};
140
- this.run = new Run('', {});
152
+ this.runs = [new Run('', {})];
141
153
  return;
142
154
  }
143
155
 
@@ -159,16 +171,22 @@ export class Hyperlink {
159
171
  this.text = validation.cleanedText;
160
172
  }
161
173
 
162
- // Create run with default hyperlink styling (Verdana 12pt blue underlined)
163
- this.formatting = {
164
- font: 'Verdana',
165
- size: 12,
166
- color: '0000FF', // Standard hyperlink blue
167
- underline: 'single',
168
- ...properties.formatting,
169
- };
170
-
171
- this.run = new Run(this.text, this.formatting);
174
+ // Default hyperlink styling (Verdana 12pt blue underlined) is for
175
+ // programmatically created links. Parsed links must keep exactly the
176
+ // formatting found in the source XML — merging these defaults underneath
177
+ // would add direct formatting that overrides the document's Hyperlink
178
+ // character style on round-trip.
179
+ this.formatting = properties.preserveFormatting
180
+ ? { ...properties.formatting }
181
+ : {
182
+ font: 'Verdana',
183
+ size: 12,
184
+ color: '0000FF', // Standard hyperlink blue
185
+ underline: 'single',
186
+ ...properties.formatting,
187
+ };
188
+
189
+ this.runs = [new Run(this.text, this.formatting)];
172
190
  }
173
191
 
174
192
  /**
@@ -239,7 +257,7 @@ export class Hyperlink {
239
257
  const revisionId = this.trackingContext!.getRevisionManager().consumeNextId();
240
258
  const author = this.trackingContext!.getAuthor();
241
259
 
242
- this.run.setPropertyChangeRevision({
260
+ this.runs[0]!.setPropertyChangeRevision({
243
261
  id: revisionId,
244
262
  author,
245
263
  date: new Date(),
@@ -357,14 +375,26 @@ export class Hyperlink {
357
375
  /**
358
376
  * Gets the display text
359
377
  *
360
- * This method delegates to the internal run to ensure the returned text
378
+ * This method delegates to the internal runs to ensure the returned text
361
379
  * is always accurate and matches what will be in the generated XML,
362
- * per ECMA-376 Part 1 §17.16.22.
380
+ * per ECMA-376 Part 1 §17.16.22. Multi-run hyperlinks concatenate the
381
+ * text of every run in document order.
363
382
  *
364
383
  * @returns The display text including any special characters (tabs, breaks, etc.)
365
384
  */
366
385
  getText(): string {
367
- return this.run.getText();
386
+ return this.runs.map((run) => run.getText()).join('');
387
+ }
388
+
389
+ /**
390
+ * Replaces the display text. Collapses to a single run (keeping the first
391
+ * run's formatting) because the caller-supplied text supersedes any
392
+ * per-run split the source document had.
393
+ */
394
+ private replaceRunText(text: string): void {
395
+ const first = this.runs[0]!;
396
+ first.setText(text);
397
+ this.runs = [first];
368
398
  }
369
399
 
370
400
  /**
@@ -379,11 +409,18 @@ export class Hyperlink {
379
409
  warnToConsole: true,
380
410
  });
381
411
 
382
- // Use cleaned text if available
383
- const cleanedText = validation.cleanedText || text;
412
+ // Use cleaned text if available.
413
+ // `??` (not `||`) so a cleaned result of '' (input that was entirely XML
414
+ // markup) is kept rather than falling back to the original markup-laden text.
415
+ const cleanedText = validation.cleanedText ?? text;
384
416
 
385
417
  const previousValue = this.text;
386
418
 
419
+ // Assigning display text clears the empty/invisible flag — otherwise
420
+ // toXML() would keep emitting a childless w:hyperlink and silently drop
421
+ // the text that was just set.
422
+ this._isEmpty = false;
423
+
387
424
  // Skip if text unchanged
388
425
  if (previousValue === cleanedText) {
389
426
  return this;
@@ -398,7 +435,7 @@ export class Hyperlink {
398
435
 
399
436
  // Apply the change to this hyperlink
400
437
  this.text = cleanedText;
401
- this.run.setText(cleanedText);
438
+ this.replaceRunText(cleanedText);
402
439
 
403
440
  // Create delete/insert revision pair
404
441
  const deletion = Revision.createDeletion(author, [oldHyperlink]);
@@ -418,7 +455,7 @@ export class Hyperlink {
418
455
 
419
456
  // Non-tracking path (original behavior)
420
457
  this.text = cleanedText;
421
- this.run.setText(cleanedText);
458
+ this.replaceRunText(cleanedText);
422
459
  return this;
423
460
  }
424
461
 
@@ -428,11 +465,35 @@ export class Hyperlink {
428
465
  * @param run - The run to use for this hyperlink
429
466
  */
430
467
  setRun(run: Run): this {
431
- this.run = run;
468
+ this.runs = [run];
432
469
  this.text = run.getText();
433
470
  return this;
434
471
  }
435
472
 
473
+ /**
474
+ * Replaces all runs of this hyperlink (multi-run display text)
475
+ * Used by DocumentParser when a w:hyperlink holds multiple w:r children —
476
+ * Word splits the display text into one run per formatting change, and
477
+ * each run must keep its own rPr for round-trip fidelity.
478
+ * @param runs - Runs in document order (ignored if empty)
479
+ */
480
+ setRuns(runs: Run[]): this {
481
+ if (runs.length === 0) {
482
+ return this;
483
+ }
484
+ this.runs = [...runs];
485
+ this.text = this.getText();
486
+ return this;
487
+ }
488
+
489
+ /**
490
+ * Gets all runs carrying the display text
491
+ * Single-run hyperlinks return a one-element array.
492
+ */
493
+ getRuns(): Run[] {
494
+ return [...this.runs];
495
+ }
496
+
436
497
  /**
437
498
  * Gets the tooltip
438
499
  */
@@ -524,7 +585,7 @@ export class Hyperlink {
524
585
  // Validate that clearing URL doesn't create empty hyperlink
525
586
  if (!url && !this.anchor) {
526
587
  throw new Error(
527
- `Cannot set URL to undefined: Hyperlink "${this.run.getText()}" has no anchor. ` +
588
+ `Cannot set URL to undefined: Hyperlink "${this.getText()}" has no anchor. ` +
528
589
  `Clearing the URL would create an invalid hyperlink per ECMA-376 §17.16.22. ` +
529
590
  `Either provide a new URL or delete the hyperlink entirely.`
530
591
  );
@@ -549,9 +610,9 @@ export class Hyperlink {
549
610
  // Apply the change to this hyperlink
550
611
  this.url = url;
551
612
  this.relationshipId = undefined;
552
- if (this.run.getText() === oldUrl) {
613
+ if (this.getText() === oldUrl) {
553
614
  this.text = url || this.anchor || 'Link';
554
- this.run.setText(this.text);
615
+ this.replaceRunText(this.text);
555
616
  }
556
617
 
557
618
  // Create delete/insert revision pair
@@ -576,10 +637,10 @@ export class Hyperlink {
576
637
 
577
638
  // Update text ONLY if it was auto-generated from the old URL
578
639
  // This preserves user-provided text (even if it's "Link")
579
- // Use run.getText() to ensure we check the actual current text, not stale cache
580
- if (this.run.getText() === oldUrl) {
640
+ // Use getText() to ensure we check the actual current text, not stale cache
641
+ if (this.getText() === oldUrl) {
581
642
  this.text = url || this.anchor || 'Link';
582
- this.run.setText(this.text);
643
+ this.replaceRunText(this.text);
583
644
  }
584
645
 
585
646
  return this;
@@ -600,7 +661,7 @@ export class Hyperlink {
600
661
  // Validate that clearing anchor doesn't create empty hyperlink
601
662
  if (!anchor && !this.url) {
602
663
  throw new Error(
603
- `Cannot set anchor to undefined: Hyperlink "${this.run.getText()}" has no URL. ` +
664
+ `Cannot set anchor to undefined: Hyperlink "${this.getText()}" has no URL. ` +
604
665
  `Clearing the anchor would create an invalid hyperlink per ECMA-376 §17.16.22. ` +
605
666
  `Either provide a new anchor or delete the hyperlink entirely.`
606
667
  );
@@ -632,9 +693,9 @@ export class Hyperlink {
632
693
  this.url = undefined;
633
694
  this.relationshipId = undefined;
634
695
  }
635
- if (this.run.getText() === oldAnchor) {
696
+ if (this.getText() === oldAnchor) {
636
697
  this.text = anchor || this.url || 'Link';
637
- this.run.setText(this.text);
698
+ this.replaceRunText(this.text);
638
699
  }
639
700
 
640
701
  // Create delete/insert revision pair
@@ -664,10 +725,10 @@ export class Hyperlink {
664
725
  }
665
726
 
666
727
  // Update text ONLY if it was auto-generated from the old anchor
667
- // Use run.getText() to ensure we check the actual current text, not stale cache
668
- if (this.run.getText() === oldAnchor) {
728
+ // Use getText() to ensure we check the actual current text, not stale cache
729
+ if (this.getText() === oldAnchor) {
669
730
  this.text = anchor || this.url || 'Link';
670
- this.run.setText(this.text);
731
+ this.replaceRunText(this.text);
671
732
  }
672
733
 
673
734
  return this;
@@ -675,9 +736,10 @@ export class Hyperlink {
675
736
 
676
737
  /**
677
738
  * Gets the run
739
+ * Multi-run hyperlinks return the first run — use getRuns() for all of them.
678
740
  */
679
741
  getRun(): Run {
680
- return this.run;
742
+ return this.runs[0]!;
681
743
  }
682
744
 
683
745
  /**
@@ -707,16 +769,34 @@ export class Hyperlink {
707
769
  // Merge mode (default, backwards-compatible): merge with existing
708
770
  this.formatting = { ...this.formatting, ...formatting };
709
771
  }
710
- // Create new run with updated formatting, preserving current text
711
- const currentText = this.run.getText();
712
- this.run = new Run(currentText, this.formatting);
713
- this.text = currentText; // Keep cache in sync
772
+ // Rebuild every run, layering the patch over each run's own rPr so
773
+ // multi-run links keep per-run distinctions the caller did not touch.
774
+ // Replace mode discards per-run rPr by design.
775
+ this.runs = this.runs.map((run) =>
776
+ Run.createFromContent(
777
+ run.getContent(),
778
+ options?.replace ? { ...formatting } : { ...run.getFormatting(), ...formatting }
779
+ )
780
+ );
781
+ this.text = this.getText(); // Keep cache in sync
714
782
  if (this.trackingContext?.isEnabled()) {
715
783
  this._applyFormattingRPrChange(previousFormatting);
716
784
  }
717
785
  return this;
718
786
  }
719
787
 
788
+ /**
789
+ * Layers a formatting patch over every display-text run. Content is
790
+ * rebuilt via Run.createFromContent so tabs/breaks survive, and each
791
+ * run keeps its other rPr values — stamping the hyperlink-level
792
+ * formatting over them would erase per-run formatting in multi-run links.
793
+ */
794
+ private patchRunFormatting(patch: RunFormatting): void {
795
+ this.runs = this.runs.map((run) =>
796
+ Run.createFromContent(run.getContent(), { ...run.getFormatting(), ...patch })
797
+ );
798
+ }
799
+
720
800
  /**
721
801
  * Gets run formatting (returns this hyperlink for fluent API)
722
802
  * @returns This hyperlink for method chaining
@@ -798,7 +878,7 @@ export class Hyperlink {
798
878
  setColor(color: string): this {
799
879
  const previousFormatting = { ...this.formatting };
800
880
  this.formatting.color = color;
801
- this.run = new Run(this.text, this.formatting);
881
+ this.patchRunFormatting({ color });
802
882
  if (this.trackingContext?.isEnabled() && previousFormatting.color !== color) {
803
883
  this._applyFormattingRPrChange(previousFormatting);
804
884
  }
@@ -813,7 +893,7 @@ export class Hyperlink {
813
893
  setUnderline(underline: boolean | 'single' | 'double' | 'dotted' | 'thick' | 'dash'): this {
814
894
  const previousFormatting = { ...this.formatting };
815
895
  this.formatting.underline = underline;
816
- this.run = new Run(this.text, this.formatting);
896
+ this.patchRunFormatting({ underline });
817
897
  if (this.trackingContext?.isEnabled() && previousFormatting.underline !== underline) {
818
898
  this._applyFormattingRPrChange(previousFormatting);
819
899
  }
@@ -828,7 +908,7 @@ export class Hyperlink {
828
908
  setBold(bold = true): this {
829
909
  const previousFormatting = { ...this.formatting };
830
910
  this.formatting.bold = bold;
831
- this.run = new Run(this.text, this.formatting);
911
+ this.patchRunFormatting({ bold });
832
912
  if (this.trackingContext?.isEnabled() && previousFormatting.bold !== bold) {
833
913
  this._applyFormattingRPrChange(previousFormatting);
834
914
  }
@@ -843,7 +923,7 @@ export class Hyperlink {
843
923
  setItalic(italic = true): this {
844
924
  const previousFormatting = { ...this.formatting };
845
925
  this.formatting.italic = italic;
846
- this.run = new Run(this.text, this.formatting);
926
+ this.patchRunFormatting({ italic });
847
927
  if (this.trackingContext?.isEnabled() && previousFormatting.italic !== italic) {
848
928
  this._applyFormattingRPrChange(previousFormatting);
849
929
  }
@@ -858,7 +938,7 @@ export class Hyperlink {
858
938
  setFont(font: string): this {
859
939
  const previousFormatting = { ...this.formatting };
860
940
  this.formatting.font = font;
861
- this.run = new Run(this.text, this.formatting);
941
+ this.patchRunFormatting({ font });
862
942
  if (this.trackingContext?.isEnabled() && previousFormatting.font !== font) {
863
943
  this._applyFormattingRPrChange(previousFormatting);
864
944
  }
@@ -873,7 +953,7 @@ export class Hyperlink {
873
953
  setSize(size: number): this {
874
954
  const previousFormatting = { ...this.formatting };
875
955
  this.formatting.size = size;
876
- this.run = new Run(this.text, this.formatting);
956
+ this.patchRunFormatting({ size });
877
957
  if (this.trackingContext?.isEnabled() && previousFormatting.size !== size) {
878
958
  this._applyFormattingRPrChange(previousFormatting);
879
959
  }
@@ -1007,8 +1087,13 @@ export class Hyperlink {
1007
1087
  }
1008
1088
 
1009
1089
  // Fix 5: Fix common typos
1090
+ // Compare against the value immediately before this replace (not this.url)
1091
+ // so 'Upgraded HTTP to HTTPS' is reported only when the http→https
1092
+ // rewrite actually fired — an earlier fix (space encoding, double-slash
1093
+ // collapse) on an already-https URL must not trigger this message.
1094
+ const beforeUpgrade = fixedUrl;
1010
1095
  fixedUrl = fixedUrl.replace(/^http:\/\//i, 'https://'); // Prefer HTTPS
1011
- if (fixedUrl !== this.url && fixedUrl.startsWith('https://')) {
1096
+ if (fixedUrl !== beforeUpgrade) {
1012
1097
  fixed.push('Upgraded HTTP to HTTPS');
1013
1098
  }
1014
1099
 
@@ -1134,21 +1219,32 @@ export class Hyperlink {
1134
1219
  tooltip: this.tooltip,
1135
1220
  relationshipId: this.relationshipId,
1136
1221
  formatting: { ...this.formatting },
1222
+ // this.formatting is already fully resolved — re-merging constructor
1223
+ // defaults would resurrect styling the original no longer carries
1224
+ preserveFormatting: true,
1137
1225
  tgtFrame: this.tgtFrame,
1138
1226
  history: this.history,
1139
1227
  docLocation: this.docLocation,
1228
+ // Carry the empty/invisible flag so a clone of a self-closing hyperlink
1229
+ // serializes self-closing too (matters for the deletion half of tracked
1230
+ // changes, which snapshots the source via clone()).
1231
+ isEmpty: this._isEmpty,
1140
1232
  });
1141
1233
 
1142
- // Copy the run with its formatting
1143
- if (this.run) {
1144
- cloned.run = new Run(this.run.getText(), { ...this.run.getFormatting() });
1234
+ // Copy every run with its content and formatting (multi-run links keep
1235
+ // one cloned run per source run)
1236
+ cloned.runs = this.runs.map((run) => {
1237
+ const copy = run.clone();
1145
1238
 
1146
1239
  // Preserve rPrChange from the original run (formatting tracked changes)
1147
- const existingRPrChange = this.run.getPropertyChangeRevision();
1240
+ // Run.clone() copies content/formatting but not the revision
1241
+ const existingRPrChange = run.getPropertyChangeRevision();
1148
1242
  if (existingRPrChange) {
1149
- cloned.run.setPropertyChangeRevision({ ...existingRPrChange });
1243
+ copy.setPropertyChangeRevision({ ...existingRPrChange });
1150
1244
  }
1151
- }
1245
+ return copy;
1246
+ });
1247
+ cloned.text = cloned.getText();
1152
1248
 
1153
1249
  return cloned;
1154
1250
  }
@@ -1231,13 +1327,12 @@ export class Hyperlink {
1231
1327
  };
1232
1328
  }
1233
1329
 
1234
- // Generate run XML
1235
- const runXml = this.run.toXML();
1236
-
1330
+ // One <w:r> child per run, each with its own <w:rPr> — collapsing to a
1331
+ // single run would drop per-run formatting in multi-run display text
1237
1332
  return {
1238
1333
  name: 'w:hyperlink',
1239
1334
  attributes,
1240
- children: [runXml],
1335
+ children: this.runs.map((run) => run.toXML()),
1241
1336
  };
1242
1337
  }
1243
1338
 
@@ -335,8 +335,11 @@ export class Image {
335
335
  private flipV = false;
336
336
  private border?: ImageBorder;
337
337
 
338
- // Set true when a serialization-affecting setter (setBorder/setSize) mutates this image
339
- // after parse, so an owning ImageRun knows to refresh its captured raw run XML.
338
+ // Set true when a serialization-affecting setter mutates this image after parse,
339
+ // so an owning ImageRun knows to refresh its captured raw run XML. Identity setters
340
+ // the parse/save pipeline invokes on every image (setRelationshipId/setDocPrId) and
341
+ // the parser-internal _setRawPassthrough must NOT set this, or unmodified documents
342
+ // would lose raw-XML round-trip fidelity.
340
343
  private _mutated = false;
341
344
 
342
345
  // Group A: Simple attribute preservation (ECMA-376 compliance)
@@ -967,6 +970,7 @@ export class Image {
967
970
  this.height = Math.round(width * ratio);
968
971
  }
969
972
  this.width = width;
973
+ this._mutated = true;
970
974
  return this;
971
975
  }
972
976
 
@@ -976,6 +980,7 @@ export class Image {
976
980
  this.width = Math.round(height * ratio);
977
981
  }
978
982
  this.height = height;
983
+ this._mutated = true;
979
984
  return this;
980
985
  }
981
986
 
@@ -1018,6 +1023,7 @@ export class Image {
1018
1023
 
1019
1024
  setAltText(altText: string): this {
1020
1025
  this.description = altText;
1026
+ this._mutated = true;
1021
1027
  return this;
1022
1028
  }
1023
1029
 
@@ -1027,6 +1033,7 @@ export class Image {
1027
1033
 
1028
1034
  setTitle(title: string): this {
1029
1035
  this.title = title;
1036
+ this._mutated = true;
1030
1037
  return this;
1031
1038
  }
1032
1039
 
@@ -1035,10 +1042,11 @@ export class Image {
1035
1042
  }
1036
1043
 
1037
1044
  rotate(degrees: number): this {
1045
+ // wp:extent/a:ext describe the pre-rotation bounding box per ECMA-376
1046
+ // §20.1.7.6 — a:xfrm/@rot rotates about the center, so swapping the
1047
+ // extents for 90/270 would stretch the bitmap before rotating it.
1038
1048
  this.rotation = ((degrees % 360) + 360) % 360;
1039
- if (this.rotation === 90 || this.rotation === 270) {
1040
- [this.width, this.height] = [this.height, this.width];
1041
- }
1049
+ this._mutated = true;
1042
1050
  return this;
1043
1051
  }
1044
1052
 
@@ -1048,6 +1056,7 @@ export class Image {
1048
1056
 
1049
1057
  setFlipH(flip: boolean): this {
1050
1058
  this.flipH = flip;
1059
+ this._mutated = true;
1051
1060
  return this;
1052
1061
  }
1053
1062
 
@@ -1057,6 +1066,7 @@ export class Image {
1057
1066
 
1058
1067
  setFlipV(flip: boolean): this {
1059
1068
  this.flipV = flip;
1069
+ this._mutated = true;
1060
1070
  return this;
1061
1071
  }
1062
1072
 
@@ -1071,6 +1081,7 @@ export class Image {
1071
1081
  }
1072
1082
  setPresetGeometry(geom: PresetGeometry): this {
1073
1083
  this.presetGeometry = geom;
1084
+ this._mutated = true;
1074
1085
  return this;
1075
1086
  }
1076
1087
 
@@ -1079,6 +1090,7 @@ export class Image {
1079
1090
  }
1080
1091
  setCompressionState(state: BlipCompressionState): this {
1081
1092
  this.compressionState = state;
1093
+ this._mutated = true;
1082
1094
  return this;
1083
1095
  }
1084
1096
 
@@ -1087,6 +1099,7 @@ export class Image {
1087
1099
  }
1088
1100
  setBwMode(mode: string): this {
1089
1101
  this.bwMode = mode;
1102
+ this._mutated = true;
1090
1103
  return this;
1091
1104
  }
1092
1105
 
@@ -1107,6 +1120,7 @@ export class Image {
1107
1120
  this.inlineDistB = distB;
1108
1121
  this.inlineDistL = distL;
1109
1122
  this.inlineDistR = distR;
1123
+ this._mutated = true;
1110
1124
  return this;
1111
1125
  }
1112
1126
 
@@ -1115,6 +1129,7 @@ export class Image {
1115
1129
  }
1116
1130
  setNoChangeAspect(val: boolean): this {
1117
1131
  this.noChangeAspect = val;
1132
+ this._mutated = true;
1118
1133
  return this;
1119
1134
  }
1120
1135
 
@@ -1123,6 +1138,7 @@ export class Image {
1123
1138
  }
1124
1139
  setHidden(val: boolean): this {
1125
1140
  this.hidden = val;
1141
+ this._mutated = true;
1126
1142
  return this;
1127
1143
  }
1128
1144
 
@@ -1131,6 +1147,7 @@ export class Image {
1131
1147
  }
1132
1148
  setBlipFillDpi(dpi: number | undefined): this {
1133
1149
  this.blipFillDpi = dpi;
1150
+ this._mutated = true;
1134
1151
  return this;
1135
1152
  }
1136
1153
 
@@ -1139,6 +1156,7 @@ export class Image {
1139
1156
  }
1140
1157
  setBlipFillRotWithShape(val: boolean | undefined): this {
1141
1158
  this.blipFillRotWithShape = val;
1159
+ this._mutated = true;
1142
1160
  return this;
1143
1161
  }
1144
1162
 
@@ -1147,6 +1165,7 @@ export class Image {
1147
1165
  }
1148
1166
  setPicLocks(locks: Partial<Record<PicLockAttribute, boolean>>): this {
1149
1167
  this.picLocks = locks;
1168
+ this._mutated = true;
1150
1169
  return this;
1151
1170
  }
1152
1171
 
@@ -1155,6 +1174,7 @@ export class Image {
1155
1174
  }
1156
1175
  setPicNonVisualProps(props: PicNonVisualProperties): this {
1157
1176
  this.picNonVisualProps = props;
1177
+ this._mutated = true;
1158
1178
  return this;
1159
1179
  }
1160
1180
 
@@ -1163,6 +1183,7 @@ export class Image {
1163
1183
  }
1164
1184
  setIsLinked(val: boolean): this {
1165
1185
  this.isLinked = val;
1186
+ this._mutated = true;
1166
1187
  return this;
1167
1188
  }
1168
1189
 
@@ -1171,6 +1192,7 @@ export class Image {
1171
1192
  }
1172
1193
  setSvgRelationshipId(id: string | undefined): this {
1173
1194
  this.svgRelationshipId = id;
1195
+ this._mutated = true;
1174
1196
  return this;
1175
1197
  }
1176
1198
 
@@ -1199,6 +1221,7 @@ export class Image {
1199
1221
 
1200
1222
  setEffectExtent(left: number, top: number, right: number, bottom: number): this {
1201
1223
  this.effectExtent = { left, top, right, bottom };
1224
+ this._mutated = true;
1202
1225
  return this;
1203
1226
  }
1204
1227
 
@@ -1219,6 +1242,7 @@ export class Image {
1219
1242
  distanceLeft: distances?.left,
1220
1243
  distanceRight: distances?.right,
1221
1244
  };
1245
+ this._mutated = true;
1222
1246
  return this;
1223
1247
  }
1224
1248
 
@@ -1278,6 +1302,7 @@ export class Image {
1278
1302
  this.validatePositionOffset(vertical.offset, 'vertical');
1279
1303
 
1280
1304
  this.position = { horizontal, vertical };
1305
+ this._mutated = true;
1281
1306
  return this;
1282
1307
  }
1283
1308
 
@@ -1341,6 +1366,7 @@ export class Image {
1341
1366
 
1342
1367
  setAnchor(options: ImageAnchor): this {
1343
1368
  this.anchor = options;
1369
+ this._mutated = true;
1344
1370
  return this;
1345
1371
  }
1346
1372
 
@@ -1351,6 +1377,7 @@ export class Image {
1351
1377
  setCrop(left: number, top: number, right: number, bottom: number): this {
1352
1378
  const clamp = (val: number) => Math.max(0, Math.min(100, val));
1353
1379
  this.crop = { left: clamp(left), top: clamp(top), right: clamp(right), bottom: clamp(bottom) };
1380
+ this._mutated = true;
1354
1381
  return this;
1355
1382
  }
1356
1383
 
@@ -1370,6 +1397,7 @@ export class Image {
1370
1397
  ? Math.max(0, Math.min(100, options.transparency))
1371
1398
  : undefined,
1372
1399
  };
1400
+ this._mutated = true;
1373
1401
  return this;
1374
1402
  }
1375
1403
 
@@ -1476,6 +1504,7 @@ export class Image {
1476
1504
  setBehindText(behind = true): this {
1477
1505
  if (this.anchor) {
1478
1506
  this.anchor.behindDoc = behind;
1507
+ this._mutated = true;
1479
1508
  } else {
1480
1509
  this.setAnchor({
1481
1510
  behindDoc: behind,
@@ -1534,6 +1563,7 @@ export class Image {
1534
1563
  removeBorder(): this {
1535
1564
  this.border = undefined;
1536
1565
  this._rawPassthrough.delete('zero-width-ln');
1566
+ this._mutated = true;
1537
1567
  return this;
1538
1568
  }
1539
1569
 
@@ -1898,7 +1928,6 @@ export class Image {
1898
1928
  if (this.wrap.distanceBottom !== undefined) wrapAttrs.distB = this.wrap.distanceBottom;
1899
1929
  if (this.wrap.distanceLeft !== undefined) wrapAttrs.distL = this.wrap.distanceLeft;
1900
1930
  if (this.wrap.distanceRight !== undefined) wrapAttrs.distR = this.wrap.distanceRight;
1901
- if (this.wrap.side) wrapAttrs.wrapText = this.wrap.side;
1902
1931
 
1903
1932
  let wrapElementName: string;
1904
1933
  switch (this.wrap.type) {
@@ -1921,6 +1950,19 @@ export class Image {
1921
1950
  wrapElementName = 'wrapSquare';
1922
1951
  }
1923
1952
 
1953
+ if (
1954
+ wrapElementName === 'wrapSquare' ||
1955
+ wrapElementName === 'wrapTight' ||
1956
+ wrapElementName === 'wrapThrough'
1957
+ ) {
1958
+ // wrapText is use="required" on CT_WrapSquare/CT_WrapTight/CT_WrapThrough,
1959
+ // so default to bothSides when setWrap() was called without a side.
1960
+ wrapAttrs.wrapText = this.wrap.side ?? 'bothSides';
1961
+ }
1962
+ // wrapText is NOT declared on CT_WrapNone or CT_WrapTopBottom — emitting
1963
+ // it there (e.g. from a side defaulted during parse) makes Word report
1964
+ // the document as corrupt, so it is intentionally never set for those.
1965
+
1924
1966
  // Group B: Include wrap polygon passthrough as children
1925
1967
  const wrapChildren: XMLElement[] = [];
1926
1968
  if (this._rawPassthrough.has('wrap-polygon')) {
@@ -1972,7 +2014,9 @@ export class Image {
1972
2014
  locked: this.anchor?.locked ? 1 : 0,
1973
2015
  layoutInCell: this.anchor?.layoutInCell ? 1 : 0,
1974
2016
  allowOverlap: this.anchor?.allowOverlap ? 1 : 0,
1975
- relativeHeight: this.anchor?.relativeHeight,
2017
+ // Required by CT_Anchor even when only setPosition() was called;
2018
+ // matches the parser fallback so position-only floats stay schema-valid.
2019
+ relativeHeight: this.anchor?.relativeHeight ?? 251658240,
1976
2020
  simplePos: this.anchor?.simplePos ? '1' : '0',
1977
2021
  distT: (this.anchor?.distT ?? 0).toString(),
1978
2022
  distB: (this.anchor?.distB ?? 0).toString(),