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
@@ -9,12 +9,14 @@ import { Endnote, EndnoteType } from '../elements/Endnote.js';
9
9
  import { Footnote, FootnoteType } from '../elements/Footnote.js';
10
10
  import { BookmarkManager } from '../elements/BookmarkManager.js';
11
11
  import { Comment } from '../elements/Comment.js';
12
+ import type { CommentContentItem, CommentParagraph } from '../elements/Comment.js';
12
13
  import { CustomXmlBlock } from '../elements/CustomXml.js';
13
14
  import { PreservedElement } from '../elements/PreservedElement.js';
14
15
  import { RegisteredBodyElement } from '../elements/RegisteredBodyElement.js';
15
16
  import { ElementRegistry } from './ElementRegistry.js';
16
17
  import { MathParagraph } from '../elements/MathElement.js';
17
18
  import { ComplexField, Field } from '../elements/Field.js';
19
+ import type { CachedFieldResultRun } from '../elements/Field.js';
18
20
  import { isHyperlinkInstruction, parseHyperlinkInstruction } from '../elements/FieldHelpers.js';
19
21
  import { Footer } from '../elements/Footer.js';
20
22
  import { Header } from '../elements/Header.js';
@@ -53,6 +55,7 @@ import {
53
55
  parseOnOffAttribute,
54
56
  } from '../utils/parsingHelpers.js';
55
57
  import { halfPointsToPoints } from '../utils/units.js';
58
+ import { normalizeAnchorSizeRel } from '../utils/xmlSanitization.js';
56
59
  import type { ShadingConfig } from '../elements/CommonTypes.js';
57
60
 
58
61
  // Create scoped logger for DocumentParser operations
@@ -133,6 +136,7 @@ export class DocumentParser {
133
136
  themeColor?: string;
134
137
  themeTint?: string;
135
138
  themeShade?: string;
139
+ rawInnerXml?: string;
136
140
  };
137
141
  }> {
138
142
  const logger = getLogger();
@@ -368,9 +372,18 @@ export class DocumentParser {
368
372
  parsed['w:sdt'],
369
373
  relationshipManager,
370
374
  zipHandler,
371
- imageManager
375
+ imageManager,
376
+ elementXml
372
377
  );
373
- if (sdt) bodyElements.push(sdt);
378
+ if (sdt) {
379
+ bodyElements.push(sdt);
380
+ } else {
381
+ // A null result means the content control could not be modeled.
382
+ // Preserve the raw block so the SDT and everything inside it
383
+ // survives the save, mirroring the failed registered-element
384
+ // path below — otherwise the whole control would be deleted.
385
+ bodyElements.push(new PreservedElement(elementXml, 'sdt', 'block'));
386
+ }
374
387
  pos = next.pos + elementXml.length;
375
388
  } else {
376
389
  pos = next.pos + 1;
@@ -1072,6 +1085,7 @@ export class DocumentParser {
1072
1085
  | 'w:permEnd'
1073
1086
  | 'm:oMath'
1074
1087
  | 'w:ruby'
1088
+ | 'w:sdt'
1075
1089
  | 'w:commentRangeStart'
1076
1090
  | 'w:commentRangeEnd';
1077
1091
  pos: number;
@@ -1093,6 +1107,7 @@ export class DocumentParser {
1093
1107
  let permEndIndex = 0;
1094
1108
  let oMathIndex = 0;
1095
1109
  let rubyIndex = 0;
1110
+ let sdtIndex = 0;
1096
1111
  let commentRangeStartIndex = 0;
1097
1112
  let commentRangeEndIndex = 0;
1098
1113
 
@@ -1252,6 +1267,18 @@ export class DocumentParser {
1252
1267
  index: rubyIndex++,
1253
1268
  });
1254
1269
  searchPos = selfClosing ? tagEnd + 1 : findClosingTagEnd(paraContent, 'w:ruby', tagEnd);
1270
+ } else if (tagName === 'w:sdt') {
1271
+ // Inline structured document tag (CT_SdtRun, ECMA-376 §17.5.2.31) —
1272
+ // date pickers, dropdowns, citations inline in a sentence. Preserve
1273
+ // as raw XML; skipping past the closing tag also keeps run indices
1274
+ // aligned with pElement['w:r'] (sdt-nested runs are not direct
1275
+ // paragraph children in the parsed object).
1276
+ children.push({
1277
+ type: 'w:sdt',
1278
+ pos: tagStart,
1279
+ index: sdtIndex++,
1280
+ });
1281
+ searchPos = selfClosing ? tagEnd + 1 : findClosingTagEnd(paraContent, 'w:sdt', tagEnd);
1255
1282
  } else {
1256
1283
  searchPos = tagEnd + 1;
1257
1284
  }
@@ -1340,21 +1367,57 @@ export class DocumentParser {
1340
1367
  if (runXml) {
1341
1368
  paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
1342
1369
  }
1370
+ } else if (runObj['mc:AlternateContent']) {
1371
+ // Word 2010+ emits every text box, WordArt, and wps shape as
1372
+ // <w:r><mc:AlternateContent><mc:Choice>… with a VML fallback
1373
+ // (ECMA-376 Part 3 §10). There is no editing model for these,
1374
+ // so preserve the whole run verbatim — otherwise the shape and
1375
+ // all its w:txbxContent text are deleted on save.
1376
+ const runXml = extractRunXmlAtPosition(child.pos);
1377
+ if (runXml) {
1378
+ paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
1379
+ }
1343
1380
  } else if (runObj['w:drawing']) {
1381
+ let imageRun: ImageRun | null = null;
1344
1382
  if (zipHandler && imageManager) {
1345
- const imageRun = await this.parseDrawingFromObject(
1383
+ imageRun = await this.parseDrawingFromObject(
1346
1384
  runObj['w:drawing'],
1347
1385
  zipHandler,
1348
1386
  relationshipManager,
1349
1387
  imageManager
1350
1388
  );
1351
- if (imageRun) {
1352
- // Preserve the parent run's w:rPr (rFonts, noProof, b, etc.)
1353
- // without this, ImageRun.toXML() emits <w:r><w:drawing/></w:r>
1354
- // and Word recalculates line height with the default font,
1355
- // shifting the image and clipping it into adjacent cells.
1356
- this.parseRunPropertiesFromObject(runObj['w:rPr'], imageRun);
1357
- paragraph.addRun(imageRun);
1389
+ }
1390
+ if (imageRun) {
1391
+ // Preserve the parent run's w:rPr (rFonts, noProof, b, etc.)
1392
+ // without this, ImageRun.toXML() emits <w:r><w:drawing/></w:r>
1393
+ // and Word recalculates line height with the default font,
1394
+ // shifting the image and clipping it into adjacent cells.
1395
+ this.parseRunPropertiesFromObject(runObj['w:rPr'], imageRun);
1396
+ paragraph.addRun(imageRun);
1397
+ // EG_RunInnerContent (ECMA-376 §17.3.3) lets a single run mix a
1398
+ // drawing with text/tab/break siblings. Word isolates drawings,
1399
+ // but other generators do not — parse any sibling content as a
1400
+ // separate Run so its text is not dropped alongside the image.
1401
+ if (
1402
+ runObj['w:t'] ||
1403
+ runObj['w:tab'] ||
1404
+ runObj['w:br'] ||
1405
+ runObj['w:cr'] ||
1406
+ runObj['w:noBreakHyphen']
1407
+ ) {
1408
+ const siblingRun = this.parseRunFromObject(runObj);
1409
+ if (siblingRun) {
1410
+ paragraph.addRun(siblingRun);
1411
+ }
1412
+ }
1413
+ } else {
1414
+ // Charts (c:chart), SmartArt (dgm:relIds), and other
1415
+ // non-picture graphicData have no editing model — preserve
1416
+ // the whole run verbatim so the drawing reference survives
1417
+ // round-trip instead of leaving an orphaned part.
1418
+ const runXml = extractRunXmlAtPosition(child.pos);
1419
+ if (runXml) {
1420
+ paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
1358
1421
  }
1359
1422
  }
1360
1423
  } else if (runObj['w:pict']) {
@@ -1401,16 +1464,22 @@ export class DocumentParser {
1401
1464
  if (child.index < hyperlinkArray.length) {
1402
1465
  const hyperlinkObj = hyperlinkArray[child.index];
1403
1466
 
1404
- // Hyperlinks containing tracked changes (w:del/w:ins inside w:hyperlink)
1405
- // cannot survive parseHyperlinkFromObject round-trip preserve as raw XML
1467
+ // Hyperlinks containing tracked changes (w:del/w:ins/w:moveFrom/
1468
+ // w:moveTo inside w:hyperlink) are always flattened to an editable
1469
+ // Hyperlink object, regardless of the revisionHandling mode (including
1470
+ // 'preserve'). This is a deliberate exception to 'preserve' — see the
1471
+ // flattening note below and the core module documentation.
1406
1472
  const hasRevisionChildren =
1407
1473
  hyperlinkObj['w:del'] ||
1408
1474
  hyperlinkObj['w:ins'] ||
1409
1475
  hyperlinkObj['w:moveFrom'] ||
1410
1476
  hyperlinkObj['w:moveTo'];
1411
1477
  if (hasRevisionChildren) {
1412
- // Flatten revisions to make hyperlink editable (setUrl/setText).
1413
- // Trades revision fidelity inside the hyperlink for editability.
1478
+ // Flatten revisions to make the hyperlink editable (setUrl/setText):
1479
+ // unwrap w:ins/w:moveTo runs (kept), drop w:del/w:moveFrom content.
1480
+ // Applied in every load mode — 'preserve' does not retain revision
1481
+ // markup inside hyperlinks. Trades in-hyperlink revision fidelity
1482
+ // for editability.
1414
1483
  const flattenedObj = { ...hyperlinkObj };
1415
1484
  const allRuns: any[] = [];
1416
1485
 
@@ -1566,6 +1635,14 @@ export class DocumentParser {
1566
1635
  if (elementXml) {
1567
1636
  paragraph.addContent(new PreservedElement(elementXml, child.type, 'inline'));
1568
1637
  }
1638
+ } else if (child.type === 'w:sdt') {
1639
+ // Inline content control — no run-level editing model, so preserve
1640
+ // the whole w:sdt verbatim; dropping it deletes the user-visible
1641
+ // text inside the control on every save.
1642
+ const sdtXml = extractElementXmlAtPosition(child.pos, 'w:sdt');
1643
+ if (sdtXml) {
1644
+ paragraph.addContent(new PreservedElement(sdtXml, 'w:sdt', 'inline'));
1645
+ }
1569
1646
  }
1570
1647
  }
1571
1648
  }
@@ -1634,39 +1711,98 @@ export class DocumentParser {
1634
1711
  // that is NOT inside hyperlinks to avoid duplicate content
1635
1712
  const hyperlinkXmls = XMLParser.extractElements(revisionXml, 'w:hyperlink');
1636
1713
 
1637
- // Create a version of the XML with hyperlinks removed to extract standalone runs
1638
- // Use split().join() instead of replace() to remove ALL occurrences of identical hyperlinks
1639
- // (replace() only removes the first match, causing duplicate content)
1714
+ // Blank out hyperlinks with same-length padding (not removal) so that
1715
+ // standalone-run positions still line up with revisionXml needed below
1716
+ // to restore the original document order of interleaved runs/hyperlinks.
1717
+ // Use split().join() instead of replace() to blank ALL occurrences of
1718
+ // identical hyperlinks (replace() only hits the first match, causing
1719
+ // duplicate content)
1640
1720
  let xmlWithoutHyperlinks = revisionXml;
1641
1721
  for (const hyperlinkXml of hyperlinkXmls) {
1642
- xmlWithoutHyperlinks = xmlWithoutHyperlinks.split(hyperlinkXml).join('');
1722
+ xmlWithoutHyperlinks = xmlWithoutHyperlinks
1723
+ .split(hyperlinkXml)
1724
+ .join(' '.repeat(hyperlinkXml.length));
1643
1725
  }
1644
1726
 
1645
1727
  // Extract runs from the XML without hyperlinks (these are standalone runs)
1646
1728
  const runXmls = XMLParser.extractElements(xmlWithoutHyperlinks, 'w:r');
1647
1729
 
1730
+ // Interleave runs and hyperlinks by source position. Two type-grouped
1731
+ // passes (all runs, then all hyperlinks) would emit every hyperlink
1732
+ // after the last run, reordering revision text on round-trip under
1733
+ // preserve mode.
1734
+ const orderedChildren: { pos: number; type: 'w:r' | 'w:hyperlink'; xml: string }[] = [];
1735
+ let runCursor = 0;
1736
+ for (const runXml of runXmls) {
1737
+ const pos = xmlWithoutHyperlinks.indexOf(runXml, runCursor);
1738
+ orderedChildren.push({ pos: pos === -1 ? runCursor : pos, type: 'w:r', xml: runXml });
1739
+ if (pos !== -1) {
1740
+ runCursor = pos + runXml.length;
1741
+ }
1742
+ }
1743
+ let hyperlinkCursor = 0;
1744
+ for (const hyperlinkXml of hyperlinkXmls) {
1745
+ const pos = revisionXml.indexOf(hyperlinkXml, hyperlinkCursor);
1746
+ orderedChildren.push({
1747
+ pos: pos === -1 ? hyperlinkCursor : pos,
1748
+ type: 'w:hyperlink',
1749
+ xml: hyperlinkXml,
1750
+ });
1751
+ if (pos !== -1) {
1752
+ hyperlinkCursor = pos + hyperlinkXml.length;
1753
+ }
1754
+ }
1755
+ orderedChildren.sort((a, b) => a.pos - b.pos);
1756
+
1648
1757
  // Use RevisionContent to hold both Run and Hyperlink objects
1649
1758
  const content: import('../elements/RevisionContent.js').RevisionContent[] = [];
1650
1759
 
1651
- // Parse standalone runs (not inside hyperlinks)
1652
- for (const runXml of runXmls) {
1653
- // Parse the run object
1760
+ for (const child of orderedChildren) {
1761
+ if (child.type === 'w:hyperlink') {
1762
+ // Parse hyperlink inside revision (for tracked hyperlink changes)
1763
+ const hyperlinkObj = XMLParser.parseToObject(child.xml, { trimValues: false });
1764
+ const hyperlinkResult = this.parseHyperlinkFromObject(
1765
+ hyperlinkObj['w:hyperlink'],
1766
+ relationshipManager
1767
+ );
1768
+ if (hyperlinkResult.hyperlink) {
1769
+ content.push(hyperlinkResult.hyperlink);
1770
+ }
1771
+ // Collect bookmarks from hyperlinks inside revisions
1772
+ result.bookmarkStarts.push(...hyperlinkResult.bookmarkStarts);
1773
+ result.bookmarkEnds.push(...hyperlinkResult.bookmarkEnds);
1774
+ continue;
1775
+ }
1776
+
1777
+ // Parse standalone run (not inside hyperlinks)
1778
+ const runXml = child.xml;
1654
1779
  const runObj = XMLParser.parseToObject(runXml, { trimValues: false });
1655
1780
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1656
1781
  const runElement = runObj['w:r'] as any;
1657
1782
 
1658
1783
  // Check if this run contains a drawing (image)
1659
1784
  if (runElement?.['w:drawing']) {
1785
+ let imageRun: ImageRun | null = null;
1660
1786
  if (zipHandler && imageManager) {
1661
- const imageRun = await this.parseDrawingFromObject(
1787
+ imageRun = await this.parseDrawingFromObject(
1662
1788
  runElement['w:drawing'],
1663
1789
  zipHandler,
1664
1790
  relationshipManager,
1665
1791
  imageManager
1666
1792
  );
1667
- if (imageRun) {
1668
- imageRun.setRawRunXml(runXml);
1669
- content.push(imageRun);
1793
+ }
1794
+ if (imageRun) {
1795
+ imageRun.setRawRunXml(runXml);
1796
+ content.push(imageRun);
1797
+ } else {
1798
+ // Non-picture graphicData (charts, SmartArt) — RevisionContent
1799
+ // cannot hold a PreservedElement, so carry the w:drawing through
1800
+ // as raw XML inside a passthrough run to keep the reference.
1801
+ const drawingXmls = XMLParser.extractElements(runXml, 'w:drawing');
1802
+ if (drawingXmls.length > 0 && drawingXmls[0]) {
1803
+ const run = Run.createFromContent([{ type: 'vml', rawXml: drawingXmls[0] }]);
1804
+ this.parseRunPropertiesFromObject(runElement['w:rPr'], run);
1805
+ content.push(run);
1670
1806
  }
1671
1807
  }
1672
1808
  } else {
@@ -1678,21 +1814,6 @@ export class DocumentParser {
1678
1814
  }
1679
1815
  }
1680
1816
 
1681
- // Parse hyperlinks inside revision (for tracked hyperlink changes)
1682
- for (const hyperlinkXml of hyperlinkXmls) {
1683
- const hyperlinkObj = XMLParser.parseToObject(hyperlinkXml, { trimValues: false });
1684
- const hyperlinkResult = this.parseHyperlinkFromObject(
1685
- hyperlinkObj['w:hyperlink'],
1686
- relationshipManager
1687
- );
1688
- if (hyperlinkResult.hyperlink) {
1689
- content.push(hyperlinkResult.hyperlink);
1690
- }
1691
- // Collect bookmarks from hyperlinks inside revisions
1692
- result.bookmarkStarts.push(...hyperlinkResult.bookmarkStarts);
1693
- result.bookmarkEnds.push(...hyperlinkResult.bookmarkEnds);
1694
- }
1695
-
1696
1817
  // Extract bookmarks directly inside the revision (not nested in hyperlinks)
1697
1818
  const bookmarkStartXmls = XMLParser.extractElements(xmlWithoutHyperlinks, 'w:bookmarkStart');
1698
1819
  const bookmarkEndXmls = XMLParser.extractElements(xmlWithoutHyperlinks, 'w:bookmarkEnd');
@@ -1748,16 +1869,23 @@ export class DocumentParser {
1748
1869
  /**
1749
1870
  * Parses comments from word/comments.xml
1750
1871
  * @param commentsXml - Raw XML content of comments.xml
1872
+ * @param commentsExtendedXml - Optional word/commentsExtended.xml content;
1873
+ * its w15:commentEx entries carry the resolved state (w15:done) keyed by
1874
+ * the w14:paraId of each comment's last paragraph
1751
1875
  * @returns Array of parsed Comment objects
1752
1876
  */
1753
- parseCommentsXml(commentsXml: string): Comment[] {
1877
+ parseCommentsXml(commentsXml: string, commentsExtendedXml?: string): Comment[] {
1754
1878
  const comments: Comment[] = [];
1755
1879
 
1880
+ const doneByParaId = commentsExtendedXml
1881
+ ? this.parseCommentsExtendedDone(commentsExtendedXml)
1882
+ : undefined;
1883
+
1756
1884
  // Extract all w:comment elements
1757
1885
  const commentXmls = XMLParser.extractElements(commentsXml, 'w:comment');
1758
1886
 
1759
1887
  for (const commentXml of commentXmls) {
1760
- const comment = this.parseCommentFromXml(commentXml);
1888
+ const comment = this.parseCommentFromXml(commentXml, doneByParaId);
1761
1889
  if (comment) {
1762
1890
  comments.push(comment);
1763
1891
  }
@@ -1766,12 +1894,35 @@ export class DocumentParser {
1766
1894
  return comments;
1767
1895
  }
1768
1896
 
1897
+ /**
1898
+ * Builds the paraId -> done map from commentsExtended.xml. Word stores
1899
+ * comment resolution there (w15:commentEx w15:done) rather than on
1900
+ * w:comment, which declares no done attribute.
1901
+ */
1902
+ private parseCommentsExtendedDone(commentsExtendedXml: string): Map<string, boolean> {
1903
+ const doneByParaId = new Map<string, boolean>();
1904
+ for (const commentExXml of XMLParser.extractElements(commentsExtendedXml, 'w15:commentEx')) {
1905
+ const paraId = XMLParser.extractAttribute(commentExXml, 'w15:paraId');
1906
+ if (!paraId) {
1907
+ continue;
1908
+ }
1909
+ const done = parseOnOffAttribute(XMLParser.extractAttribute(commentExXml, 'w15:done'));
1910
+ doneByParaId.set(paraId.toUpperCase(), done);
1911
+ }
1912
+ return doneByParaId;
1913
+ }
1914
+
1769
1915
  /**
1770
1916
  * Parses a single comment element from XML
1771
1917
  * @param commentXml - XML string for one w:comment element
1918
+ * @param doneByParaId - Resolved state from commentsExtended.xml, keyed by
1919
+ * uppercased last-paragraph w14:paraId
1772
1920
  * @returns Parsed Comment or null
1773
1921
  */
1774
- private parseCommentFromXml(commentXml: string): Comment | null {
1922
+ private parseCommentFromXml(
1923
+ commentXml: string,
1924
+ doneByParaId?: Map<string, boolean>
1925
+ ): Comment | null {
1775
1926
  try {
1776
1927
  // Extract attributes
1777
1928
  const idAttr = XMLParser.extractAttribute(commentXml, 'w:id');
@@ -1792,20 +1943,45 @@ export class DocumentParser {
1792
1943
  const id = parseInt(idAttr, 10);
1793
1944
  const date = dateAttr ? new Date(dateAttr) : new Date();
1794
1945
  const parentId = parentIdAttr ? parseInt(parentIdAttr, 10) : undefined;
1795
- // Per ECMA-376 §17.17.4, w:done is ST_OnOff — accept 1/0/true/false/on/off
1796
- const done = parseOnOffAttribute(doneAttr);
1797
-
1798
- // Parse content (runs from paragraphs within the comment)
1946
+ // Accept legacy done attributes as ST_OnOff (1/0/true/false/on/off);
1947
+ // commentsExtended.xml (checked below) is the authoritative source
1948
+ let done = parseOnOffAttribute(doneAttr);
1949
+
1950
+ // Parse content per source paragraph so regeneration can restore the
1951
+ // original <w:p> boundaries, paragraph properties, and hyperlink
1952
+ // wrappers — multi-paragraph comments are legal per ECMA-376
1953
+ // §17.13.4.2 and flattening them concatenates words across the lost
1954
+ // paragraph breaks
1799
1955
  const runs: Run[] = [];
1800
- const runXmls = XMLParser.extractElements(commentXml, 'w:r');
1956
+ const paragraphs: CommentParagraph[] = [];
1957
+ const paraXmls = XMLParser.extractElements(commentXml, 'w:p');
1958
+ const runSources = paraXmls.length > 0 ? paraXmls : [commentXml];
1801
1959
 
1802
- for (const runXml of runXmls) {
1803
- const runObj = XMLParser.parseToObject(runXml, { trimValues: false });
1804
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1805
- const run = this.parseRunFromObject(runObj['w:r'] as any);
1806
- if (run) {
1807
- runs.push(run);
1960
+ // commentsExtended.xml references a comment via the w14:paraId of its
1961
+ // last paragraph; when an entry exists it overrides the legacy attr
1962
+ if (doneByParaId && paraXmls.length > 0) {
1963
+ const lastParaXml = paraXmls[paraXmls.length - 1]!;
1964
+ const lastParaId = XMLParser.extractAttribute(lastParaXml, 'w14:paraId');
1965
+ if (lastParaId) {
1966
+ const extendedDone = doneByParaId.get(lastParaId.toUpperCase());
1967
+ if (extendedDone !== undefined) {
1968
+ done = extendedDone;
1969
+ }
1970
+ }
1971
+ }
1972
+
1973
+ for (const sourceXml of runSources) {
1974
+ const content = this.parseCommentParagraphContent(sourceXml);
1975
+ for (const item of content) {
1976
+ if (item instanceof Run) {
1977
+ runs.push(item);
1978
+ } else {
1979
+ runs.push(...item.runs);
1980
+ }
1808
1981
  }
1982
+ const pPr =
1983
+ paraXmls.length > 0 ? XMLParser.extractElements(sourceXml, 'w:pPr')[0] : undefined;
1984
+ paragraphs.push({ pPr, content });
1809
1985
  }
1810
1986
 
1811
1987
  // Create comment with parsed data
@@ -1819,6 +1995,10 @@ export class DocumentParser {
1819
1995
  done,
1820
1996
  });
1821
1997
 
1998
+ if (paraXmls.length > 0) {
1999
+ comment.setParagraphs(paragraphs);
2000
+ }
2001
+
1822
2002
  return comment;
1823
2003
  } catch (error: unknown) {
1824
2004
  defaultLogger.warn(
@@ -1829,15 +2009,76 @@ export class DocumentParser {
1829
2009
  }
1830
2010
  }
1831
2011
 
2012
+ /**
2013
+ * Parses one comment paragraph's inline content in document order.
2014
+ * Top-level runs become Run instances; w:hyperlink wrappers pass through
2015
+ * as raw XML (their r:id targets live in word/_rels/comments.xml.rels,
2016
+ * which is preserved verbatim) with their inner runs parsed alongside so
2017
+ * text extraction still sees the link text.
2018
+ * @param sourceXml - XML of one w:p element (or the whole w:comment when
2019
+ * it has no paragraph children)
2020
+ * @returns Ordered paragraph content
2021
+ */
2022
+ private parseCommentParagraphContent(sourceXml: string): CommentContentItem[] {
2023
+ const positioned: Array<{ pos: number; item: CommentContentItem }> = [];
2024
+
2025
+ const hyperlinkRanges: Array<{ start: number; end: number }> = [];
2026
+ let hyperlinkCursor = 0;
2027
+ for (const hyperlinkXml of XMLParser.extractElements(sourceXml, 'w:hyperlink')) {
2028
+ const start = sourceXml.indexOf(hyperlinkXml, hyperlinkCursor);
2029
+ if (start === -1) {
2030
+ continue;
2031
+ }
2032
+ hyperlinkCursor = start + hyperlinkXml.length;
2033
+ hyperlinkRanges.push({ start, end: hyperlinkCursor });
2034
+
2035
+ const innerRuns: Run[] = [];
2036
+ for (const runXml of XMLParser.extractElements(hyperlinkXml, 'w:r')) {
2037
+ const run = this.parseCommentRun(runXml);
2038
+ if (run) {
2039
+ innerRuns.push(run);
2040
+ }
2041
+ }
2042
+ positioned.push({ pos: start, item: { rawXml: hyperlinkXml, runs: innerRuns } });
2043
+ }
2044
+
2045
+ let runCursor = 0;
2046
+ for (const runXml of XMLParser.extractElements(sourceXml, 'w:r')) {
2047
+ const start = sourceXml.indexOf(runXml, runCursor);
2048
+ if (start === -1) {
2049
+ continue;
2050
+ }
2051
+ runCursor = start + runXml.length;
2052
+ // Runs inside a hyperlink are already covered by its raw passthrough
2053
+ if (hyperlinkRanges.some((range) => start >= range.start && start < range.end)) {
2054
+ continue;
2055
+ }
2056
+ const run = this.parseCommentRun(runXml);
2057
+ if (run) {
2058
+ positioned.push({ pos: start, item: run });
2059
+ }
2060
+ }
2061
+
2062
+ return positioned.sort((a, b) => a.pos - b.pos).map((entry) => entry.item);
2063
+ }
2064
+
2065
+ private parseCommentRun(runXml: string): Run | null {
2066
+ const runObj = XMLParser.parseToObject(runXml, { trimValues: false });
2067
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2068
+ return this.parseRunFromObject(runObj['w:r'] as any);
2069
+ }
2070
+
1832
2071
  /**
1833
2072
  * Parses footnotes.xml into Footnote array
2073
+ * @param partRels - Part-scoped relationships (word/_rels/footnotes.xml.rels)
2074
+ * used to resolve hyperlink r:id targets inside footnote content
1834
2075
  */
1835
- parseFootnotesXml(footnotesXml: string): Footnote[] {
2076
+ parseFootnotesXml(footnotesXml: string, partRels?: RelationshipManager): Footnote[] {
1836
2077
  const footnotes: Footnote[] = [];
1837
2078
  const footnoteXmls = XMLParser.extractElements(footnotesXml, 'w:footnote');
1838
2079
 
1839
2080
  for (const footnoteXml of footnoteXmls) {
1840
- const footnote = this.parseFootnoteFromXml(footnoteXml);
2081
+ const footnote = this.parseFootnoteFromXml(footnoteXml, partRels);
1841
2082
  if (footnote) {
1842
2083
  footnotes.push(footnote);
1843
2084
  }
@@ -1846,7 +2087,10 @@ export class DocumentParser {
1846
2087
  return footnotes;
1847
2088
  }
1848
2089
 
1849
- private parseFootnoteFromXml(footnoteXml: string): Footnote | null {
2090
+ private parseFootnoteFromXml(
2091
+ footnoteXml: string,
2092
+ partRels?: RelationshipManager
2093
+ ): Footnote | null {
1850
2094
  try {
1851
2095
  const idAttr = XMLParser.extractAttribute(footnoteXml, 'w:id');
1852
2096
  const typeAttr = XMLParser.extractAttribute(footnoteXml, 'w:type');
@@ -1870,7 +2114,7 @@ export class DocumentParser {
1870
2114
 
1871
2115
  const paraXmls = XMLParser.extractElements(footnoteXml, 'w:p');
1872
2116
  for (const paraXml of paraXmls) {
1873
- const para = this.parseNoteParaFromXml(paraXml);
2117
+ const para = this.parseNoteParaFromXml(paraXml, partRels);
1874
2118
  if (para) {
1875
2119
  footnote.addParagraph(para);
1876
2120
  }
@@ -1888,13 +2132,15 @@ export class DocumentParser {
1888
2132
 
1889
2133
  /**
1890
2134
  * Parses endnotes.xml into Endnote array
2135
+ * @param partRels - Part-scoped relationships (word/_rels/endnotes.xml.rels)
2136
+ * used to resolve hyperlink r:id targets inside endnote content
1891
2137
  */
1892
- parseEndnotesXml(endnotesXml: string): Endnote[] {
2138
+ parseEndnotesXml(endnotesXml: string, partRels?: RelationshipManager): Endnote[] {
1893
2139
  const endnotes: Endnote[] = [];
1894
2140
  const endnoteXmls = XMLParser.extractElements(endnotesXml, 'w:endnote');
1895
2141
 
1896
2142
  for (const endnoteXml of endnoteXmls) {
1897
- const endnote = this.parseEndnoteFromXml(endnoteXml);
2143
+ const endnote = this.parseEndnoteFromXml(endnoteXml, partRels);
1898
2144
  if (endnote) {
1899
2145
  endnotes.push(endnote);
1900
2146
  }
@@ -1903,7 +2149,7 @@ export class DocumentParser {
1903
2149
  return endnotes;
1904
2150
  }
1905
2151
 
1906
- private parseEndnoteFromXml(endnoteXml: string): Endnote | null {
2152
+ private parseEndnoteFromXml(endnoteXml: string, partRels?: RelationshipManager): Endnote | null {
1907
2153
  try {
1908
2154
  const idAttr = XMLParser.extractAttribute(endnoteXml, 'w:id');
1909
2155
  const typeAttr = XMLParser.extractAttribute(endnoteXml, 'w:type');
@@ -1927,7 +2173,7 @@ export class DocumentParser {
1927
2173
 
1928
2174
  const paraXmls = XMLParser.extractElements(endnoteXml, 'w:p');
1929
2175
  for (const paraXml of paraXmls) {
1930
- const para = this.parseNoteParaFromXml(paraXml);
2176
+ const para = this.parseNoteParaFromXml(paraXml, partRels);
1931
2177
  if (para) {
1932
2178
  endnote.addParagraph(para);
1933
2179
  }
@@ -1945,20 +2191,80 @@ export class DocumentParser {
1945
2191
 
1946
2192
  /**
1947
2193
  * Parses a paragraph from a footnote or endnote element.
1948
- * Uses run-level parsing (like comments) to avoid async dependency on relationship manager.
2194
+ * Uses run-level parsing (like comments) to avoid async dependency on the
2195
+ * document relationship manager. Paragraph properties (pStyle FootnoteText
2196
+ * per ECMA-376 §17.7.4) and w:hyperlink wrappers are parsed so regeneration
2197
+ * after a real edit keeps note styling and working links instead of
2198
+ * flattening hyperlinks to plain runs; r:id targets resolve against the
2199
+ * part-scoped rels when provided.
1949
2200
  */
1950
- private parseNoteParaFromXml(paraXml: string): Paragraph | null {
2201
+ private parseNoteParaFromXml(paraXml: string, partRels?: RelationshipManager): Paragraph | null {
1951
2202
  try {
1952
2203
  const para = new Paragraph();
1953
- const runXmls = XMLParser.extractElements(paraXml, 'w:r');
1954
- for (const runXml of runXmls) {
1955
- const runObj = XMLParser.parseToObject(runXml, { trimValues: false });
1956
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1957
- const run = this.parseRunFromObject(runObj['w:r'] as any);
1958
- if (run) {
1959
- para.addRun(run);
2204
+
2205
+ const paraObj = XMLParser.parseToObject(paraXml, { trimValues: false });
2206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2207
+ const pElement = paraObj['w:p'] as any;
2208
+ if (pElement?.['w:pPr']) {
2209
+ this.parseParagraphPropertiesFromObject(pElement['w:pPr'], para);
2210
+ }
2211
+
2212
+ // Scan children in document order, consuming w:r/w:hyperlink subtrees
2213
+ // whole so hyperlink inner runs are not double-counted as paragraph
2214
+ // runs; unhandled containers (w:ins, w:smartTag, ...) are descended
2215
+ // into so their nested runs still contribute text as before
2216
+ const pPrEnd = paraXml.indexOf('</w:pPr>');
2217
+ const contentStart = pPrEnd !== -1 ? pPrEnd + '</w:pPr>'.length : paraXml.indexOf('>') + 1;
2218
+ const contentEnd = paraXml.lastIndexOf('</w:p>');
2219
+ if (contentEnd <= contentStart) {
2220
+ return para;
2221
+ }
2222
+ const content = paraXml.substring(contentStart, contentEnd);
2223
+
2224
+ let searchPos = 0;
2225
+ while (searchPos < content.length) {
2226
+ const tagStart = content.indexOf('<', searchPos);
2227
+ if (tagStart === -1) break;
2228
+ const tagEnd = content.indexOf('>', tagStart);
2229
+ if (tagEnd === -1) break;
2230
+ const tagBody = content.substring(tagStart + 1, tagEnd);
2231
+ const tagName = tagBody.split(/[\s/>]/)[0];
2232
+ const selfClosing = tagBody.endsWith('/');
2233
+
2234
+ if ((tagName === 'w:r' || tagName === 'w:hyperlink') && !selfClosing) {
2235
+ const closingTag = `</${tagName}>`;
2236
+ const closingPos = content.indexOf(closingTag, tagEnd);
2237
+ const childEnd = closingPos === -1 ? tagEnd + 1 : closingPos + closingTag.length;
2238
+ const childXml = content.substring(tagStart, childEnd);
2239
+ const childObj = XMLParser.parseToObject(childXml, { trimValues: false });
2240
+
2241
+ if (tagName === 'w:r') {
2242
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2243
+ const run = this.parseRunFromObject(childObj['w:r'] as any);
2244
+ if (run) {
2245
+ para.addRun(run);
2246
+ }
2247
+ } else {
2248
+ const result = this.parseHyperlinkFromObject(
2249
+ childObj['w:hyperlink'],
2250
+ partRels ?? RelationshipManager.create()
2251
+ );
2252
+ if (result.hyperlink) {
2253
+ para.addHyperlink(result.hyperlink);
2254
+ }
2255
+ for (const bookmark of result.bookmarkStarts) {
2256
+ para.addBookmarkStart(bookmark);
2257
+ }
2258
+ for (const bookmark of result.bookmarkEnds) {
2259
+ para.addBookmarkEnd(bookmark);
2260
+ }
2261
+ }
2262
+ searchPos = childEnd;
2263
+ } else {
2264
+ searchPos = tagEnd + 1;
1960
2265
  }
1961
2266
  }
2267
+
1962
2268
  return para;
1963
2269
  } catch {
1964
2270
  return null;
@@ -2120,16 +2426,40 @@ export class DocumentParser {
2120
2426
  if (elementIndex < runArray.length) {
2121
2427
  const child = runArray[elementIndex];
2122
2428
  if (child['w:drawing']) {
2429
+ let imageRun: ImageRun | null = null;
2123
2430
  if (zipHandler && imageManager) {
2124
- const imageRun = await this.parseDrawingFromObject(
2431
+ imageRun = await this.parseDrawingFromObject(
2125
2432
  child['w:drawing'],
2126
2433
  zipHandler,
2127
2434
  relationshipManager,
2128
2435
  imageManager
2129
2436
  );
2130
- if (imageRun) {
2131
- this.parseRunPropertiesFromObject(child['w:rPr'], imageRun);
2132
- paragraph.addRun(imageRun);
2437
+ }
2438
+ if (imageRun) {
2439
+ this.parseRunPropertiesFromObject(child['w:rPr'], imageRun);
2440
+ paragraph.addRun(imageRun);
2441
+ // A run may legally mix a drawing with text/tab/break
2442
+ // siblings (ECMA-376 §17.3.3) — parse those as a separate
2443
+ // Run so the text is not dropped alongside the image.
2444
+ if (
2445
+ child['w:t'] ||
2446
+ child['w:tab'] ||
2447
+ child['w:br'] ||
2448
+ child['w:cr'] ||
2449
+ child['w:noBreakHyphen']
2450
+ ) {
2451
+ const siblingRun = this.parseRunFromObject(child);
2452
+ if (siblingRun) {
2453
+ paragraph.addRun(siblingRun);
2454
+ }
2455
+ }
2456
+ } else {
2457
+ // No raw paragraph XML on this path — rebuild the run from
2458
+ // the parsed object so non-picture drawings (charts,
2459
+ // SmartArt) survive round-trip instead of being dropped.
2460
+ const runXml = this.objectToXml({ 'w:r': child });
2461
+ if (runXml) {
2462
+ paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
2133
2463
  }
2134
2464
  }
2135
2465
  } else {
@@ -2181,17 +2511,41 @@ export class DocumentParser {
2181
2511
 
2182
2512
  for (const child of runChildren) {
2183
2513
  if (child['w:drawing']) {
2514
+ let imageRun: ImageRun | null = null;
2184
2515
  if (zipHandler && imageManager) {
2185
2516
  // Parse as image run
2186
- const imageRun = await this.parseDrawingFromObject(
2517
+ imageRun = await this.parseDrawingFromObject(
2187
2518
  child['w:drawing'],
2188
2519
  zipHandler,
2189
2520
  relationshipManager,
2190
2521
  imageManager
2191
2522
  );
2192
- if (imageRun) {
2193
- this.parseRunPropertiesFromObject(child['w:rPr'], imageRun);
2194
- paragraph.addRun(imageRun);
2523
+ }
2524
+ if (imageRun) {
2525
+ this.parseRunPropertiesFromObject(child['w:rPr'], imageRun);
2526
+ paragraph.addRun(imageRun);
2527
+ // A run may legally mix a drawing with text/tab/break siblings
2528
+ // (ECMA-376 §17.3.3) — parse those as a separate Run so the text
2529
+ // is not dropped alongside the image.
2530
+ if (
2531
+ child['w:t'] ||
2532
+ child['w:tab'] ||
2533
+ child['w:br'] ||
2534
+ child['w:cr'] ||
2535
+ child['w:noBreakHyphen']
2536
+ ) {
2537
+ const siblingRun = this.parseRunFromObject(child);
2538
+ if (siblingRun) {
2539
+ paragraph.addRun(siblingRun);
2540
+ }
2541
+ }
2542
+ } else {
2543
+ // No raw paragraph XML on this path — rebuild the run from the
2544
+ // parsed object so non-picture drawings (charts, SmartArt)
2545
+ // survive round-trip instead of being dropped.
2546
+ const runXml = this.objectToXml({ 'w:r': child });
2547
+ if (runXml) {
2548
+ paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
2195
2549
  }
2196
2550
  }
2197
2551
  } else {
@@ -4154,10 +4508,15 @@ export class DocumentParser {
4154
4508
  if (node === undefined || node === null) {
4155
4509
  return '';
4156
4510
  }
4511
+ // XMLParser.parseElementToObject already unescaped entities while
4512
+ // building #text, so the value here is plain text. A second
4513
+ // unescapeXml would corrupt literal entity sequences (e.g. text that
4514
+ // was authored as "&lt;" — stored as "&amp;lt;" — would collapse to
4515
+ // "<"), breaking load→save fidelity.
4157
4516
  if (typeof node === 'object') {
4158
- return XMLBuilder.unescapeXml(node['#text'] || '');
4517
+ return node['#text'] != null ? String(node['#text']) : '';
4159
4518
  }
4160
- return XMLBuilder.unescapeXml(String(node));
4519
+ return String(node);
4161
4520
  };
4162
4521
 
4163
4522
  // Field-character attributes (w:dirty, w:fldLock, w:lock on w:fldChar) are
@@ -4989,6 +5348,11 @@ export class DocumentParser {
4989
5348
  anchor: finalAnchor,
4990
5349
  text: displayText,
4991
5350
  formatting,
5351
+ // Word-authored hyperlinks usually carry only <w:rStyle w:val="Hyperlink"/>
5352
+ // (or no rPr at all) — merging the constructor's default styling
5353
+ // underneath would inject direct formatting that never existed in the
5354
+ // source and override the document's Hyperlink character style
5355
+ preserveFormatting: true,
4992
5356
  tooltip,
4993
5357
  relationshipId: finalRelationshipId,
4994
5358
  tgtFrame,
@@ -4996,9 +5360,20 @@ export class DocumentParser {
4996
5360
  docLocation,
4997
5361
  });
4998
5362
 
4999
- // If we successfully parsed a run with tabs/breaks, use it instead of the default run
5000
- // This preserves TOC structure (text tab text)
5001
- if (parsedRun && parsedRun.getContent().length > 1) {
5363
+ // Replace the constructor-built run with the parsed run(s). Word splits
5364
+ // a hyperlink's display text into one run per formatting change, so
5365
+ // multi-run sources must keep every run with its own rPr — collapsing
5366
+ // to the first run's formatting silently drops bold/italic/color on
5367
+ // the later runs. Single runs are preserved verbatim when they carry
5368
+ // text or structure like TOC tabs/breaks that a plain text run cannot
5369
+ // represent. Empty-text runs keep the constructor run so the
5370
+ // display-text fallback ('[Link]'/URL) survives.
5371
+ if (parsedRuns.length > 1 && (text || parsedRuns.some((r) => r.getContent().length > 1))) {
5372
+ hyperlink.setRuns(parsedRuns);
5373
+ } else if (
5374
+ parsedRun &&
5375
+ ((parsedRuns.length === 1 && text) || parsedRun.getContent().length > 1)
5376
+ ) {
5002
5377
  hyperlink.setRun(parsedRun);
5003
5378
  }
5004
5379
 
@@ -5220,6 +5595,32 @@ export class DocumentParser {
5220
5595
  dirty,
5221
5596
  });
5222
5597
 
5598
+ // Per ECMA-376 §17.16.16 CT_SimpleField, the child runs hold the
5599
+ // field's current (cached) result. Word does not refresh fields on
5600
+ // open by default, so the cached text + per-run formatting must
5601
+ // survive round-trip — otherwise the visible content is silently
5602
+ // replaced by a synthetic placeholder on save.
5603
+ const resultRuns = fieldObj['w:r'];
5604
+ const resultRunArray = Array.isArray(resultRuns)
5605
+ ? resultRuns
5606
+ : resultRuns
5607
+ ? [resultRuns]
5608
+ : [];
5609
+ const cachedRuns: CachedFieldResultRun[] = [];
5610
+ for (const runObj of resultRunArray) {
5611
+ const run = this.parseRunFromObject(runObj);
5612
+ if (run) {
5613
+ const runFormatting = run.getFormatting();
5614
+ cachedRuns.push({
5615
+ text: run.getText(),
5616
+ formatting: Object.keys(runFormatting).length > 0 ? runFormatting : undefined,
5617
+ });
5618
+ }
5619
+ }
5620
+ if (cachedRuns.length > 0) {
5621
+ field.setCachedResultRuns(cachedRuns);
5622
+ }
5623
+
5223
5624
  return field;
5224
5625
  } catch (error: unknown) {
5225
5626
  defaultLogger.warn(
@@ -5464,7 +5865,11 @@ export class DocumentParser {
5464
5865
  // coerces purely-numeric font names ("2010", etc.) to JS
5465
5866
  // numbers; cast through String() so RunFormatting's
5466
5867
  // declared-string font fields keep their type contract.
5467
- if (rFonts['@_w:ascii'] !== undefined) run.setFont(String(rFonts['@_w:ascii']));
5868
+ // Use setFontAscii (not setFont) so only the declared ascii slot is set;
5869
+ // setFont mirrors into hAnsi/cs for programmatic convenience, which would
5870
+ // fabricate those slots on round-trip and override style/theme-inherited
5871
+ // fonts.
5872
+ if (rFonts['@_w:ascii'] !== undefined) run.setFontAscii(String(rFonts['@_w:ascii']));
5468
5873
  // Parse additional font variants per ECMA-376 Part 1 §17.3.2.26
5469
5874
  if (rFonts['@_w:hAnsi'] !== undefined) run.setFontHAnsi(String(rFonts['@_w:hAnsi']));
5470
5875
  if (rFonts['@_w:eastAsia'] !== undefined) run.setFontEastAsia(String(rFonts['@_w:eastAsia']));
@@ -6431,7 +6836,11 @@ export class DocumentParser {
6431
6836
  'wp:cNvGraphicFramePr',
6432
6837
  'a:graphic',
6433
6838
  ]);
6434
- if (anchorExtras) image._setRawPassthrough('anchor-extra', anchorExtras);
6839
+ if (anchorExtras) {
6840
+ // Repair malformed wp14:sizeRelH/sizeRelV (bare-text percentage) so the
6841
+ // preserved passthrough stays schema-valid on save.
6842
+ image._setRawPassthrough('anchor-extra', normalizeAnchorSizeRel(anchorExtras));
6843
+ }
6435
6844
  }
6436
6845
 
6437
6846
  // DocPr extras (a:hlinkClick, a:hlinkHover, a:extLst)
@@ -6496,7 +6905,13 @@ export class DocumentParser {
6496
6905
 
6497
6906
  return {
6498
6907
  type,
6499
- side: wrapObj['@_wrapText'] || 'bothSides',
6908
+ // wrapText only exists on CT_WrapSquare/Tight/Through. For wrapNone and
6909
+ // wrapTopAndBottom it must stay undefined so serialization never emits an
6910
+ // (invalid) wrapText attribute on those elements.
6911
+ side:
6912
+ type === 'none' || type === 'topAndBottom'
6913
+ ? wrapObj['@_wrapText']
6914
+ : wrapObj['@_wrapText'] || 'bothSides',
6500
6915
  // Distance attributes are on the wrap element, not the anchor
6501
6916
  distanceTop: wrapObj['@_distT'] ? parseInt(wrapObj['@_distT'], 10) : undefined,
6502
6917
  distanceBottom: wrapObj['@_distB'] ? parseInt(wrapObj['@_distB'], 10) : undefined,
@@ -7931,7 +8346,8 @@ export class DocumentParser {
7931
8346
  sdtObj: any,
7932
8347
  relationshipManager: RelationshipManager,
7933
8348
  zipHandler: ZipHandler,
7934
- imageManager: ImageManager
8349
+ imageManager: ImageManager,
8350
+ rawSdtXml?: string
7935
8351
  ): Promise<StructuredDocumentTag | TableOfContentsElement | null> {
7936
8352
  try {
7937
8353
  if (!sdtObj) return null;
@@ -8025,10 +8441,18 @@ export class DocumentParser {
8025
8441
  // <w14:checked> is CT_OnOff in the Word 2010+ extension namespace.
8026
8442
  // Honour every ST_OnOff literal ("1"/"0"/"true"/"false"/"on"/"off")
8027
8443
  // and treat a bare self-closing `<w14:checked/>` as true.
8444
+ // CT_SdtCheckboxSymbol pairs the character code with a glyph font;
8445
+ // capture w14:font (String-cast — XMLParser coerces numeric-looking
8446
+ // values) so a custom font like Wingdings survives re-serialization
8447
+ // instead of being rewritten to the MS Gothic default.
8448
+ const rawCheckedFont = checkboxElement?.['w14:checkedState']?.['@_w14:font'];
8449
+ const rawUncheckedFont = checkboxElement?.['w14:uncheckedState']?.['@_w14:font'];
8028
8450
  properties.checkbox = {
8029
8451
  checked: parseOoxmlBoolean(checkboxElement?.['w14:checked'], '@_w14:val'),
8030
8452
  checkedState: String(checkboxElement?.['w14:checkedState']?.['@_w14:val'] ?? ''),
8031
8453
  uncheckedState: String(checkboxElement?.['w14:uncheckedState']?.['@_w14:val'] ?? ''),
8454
+ checkedFont: rawCheckedFont === undefined ? undefined : String(rawCheckedFont),
8455
+ uncheckedFont: rawUncheckedFont === undefined ? undefined : String(rawUncheckedFont),
8032
8456
  };
8033
8457
  } else if (sdtPr['w:picture']) {
8034
8458
  properties.controlType = 'picture';
@@ -8083,6 +8507,43 @@ export class DocumentParser {
8083
8507
  }
8084
8508
  }
8085
8509
 
8510
+ // Capture the original w:sdtPr / w:sdtEndPr markup. The modeled
8511
+ // properties above are a subset of CT_SdtPr (ECMA-376 §17.5.2.38);
8512
+ // rebuilding sdtPr from them on save would drop unmodeled children
8513
+ // (w:rPr, w15:appearance, w15:color, w:temporary, repeatingSection,
8514
+ // ...), and w:sdtEndPr has no model at all. Prefer the verbatim
8515
+ // substring when the caller supplied the raw element XML; nested
8516
+ // SDTs only have the parsed object, so reconstruct from it. Both
8517
+ // outer parts precede w:sdtContent per CT_SdtBlock, so when present
8518
+ // the first occurrence in the raw XML belongs to this SDT, never to
8519
+ // a nested one.
8520
+ let rawSdtPrXml: string | undefined;
8521
+ if (sdtPr !== undefined) {
8522
+ if (rawSdtXml) {
8523
+ const prPos = rawSdtXml.indexOf('<w:sdtPr');
8524
+ if (prPos !== -1) {
8525
+ rawSdtPrXml = this.extractSingleElement(rawSdtXml, 'w:sdtPr', prPos) || undefined;
8526
+ }
8527
+ }
8528
+ if (rawSdtPrXml === undefined) {
8529
+ rawSdtPrXml = this.objectToXml({ 'w:sdtPr': sdtPr }) || undefined;
8530
+ }
8531
+ }
8532
+ let rawSdtEndPrXml: string | undefined;
8533
+ const sdtEndPr = sdtObj['w:sdtEndPr'];
8534
+ if (sdtEndPr !== undefined) {
8535
+ if (rawSdtXml) {
8536
+ const endPrPos = rawSdtXml.indexOf('<w:sdtEndPr');
8537
+ if (endPrPos !== -1) {
8538
+ rawSdtEndPrXml =
8539
+ this.extractSingleElement(rawSdtXml, 'w:sdtEndPr', endPrPos) || undefined;
8540
+ }
8541
+ }
8542
+ if (rawSdtEndPrXml === undefined) {
8543
+ rawSdtEndPrXml = this.objectToXml({ 'w:sdtEndPr': sdtEndPr }) || undefined;
8544
+ }
8545
+ }
8546
+
8086
8547
  // Parse SDT content (sdtContent)
8087
8548
  const content: any[] = [];
8088
8549
  const sdtContent = sdtObj['w:sdtContent'];
@@ -8143,6 +8604,23 @@ export class DocumentParser {
8143
8604
  );
8144
8605
  if (nestedSdt) content.push(nestedSdt);
8145
8606
  }
8607
+ } else if (!elementType.startsWith('@_') && elementType !== '#text') {
8608
+ // CT_SdtContentBlock also admits w:customXml plus
8609
+ // EG_RunLevelElts (bookmark/comment/perm range markers,
8610
+ // w:proofErr, w:ins/w:del, math, move-range markers).
8611
+ // Regenerating document.xml without them unbalances range
8612
+ // pairs and drops content, so pass them through as raw XML —
8613
+ // mirrors the body-level preservation of these same types.
8614
+ const others = sdtContent[elementType];
8615
+ const otherArray = Array.isArray(others)
8616
+ ? others
8617
+ : others !== undefined
8618
+ ? [others]
8619
+ : [];
8620
+ if (elementIndex < otherArray.length) {
8621
+ const rawXml = this.objectToXml({ [elementType]: otherArray[elementIndex] });
8622
+ if (rawXml) content.push(new PreservedElement(rawXml, elementType, 'block'));
8623
+ }
8146
8624
  }
8147
8625
  }
8148
8626
  } else {
@@ -8186,6 +8664,33 @@ export class DocumentParser {
8186
8664
  );
8187
8665
  if (nestedSdt) content.push(nestedSdt);
8188
8666
  }
8667
+
8668
+ // Preserve remaining CT_SdtContentBlock children (range markers,
8669
+ // w:customXml, math, tracked-change wrappers) that the typed
8670
+ // branches above do not model — same raw passthrough as the
8671
+ // ordered path, original order unknown without _orderedChildren.
8672
+ for (const key of Object.keys(sdtContent)) {
8673
+ if (
8674
+ key === 'w:p' ||
8675
+ key === 'w:tbl' ||
8676
+ key === 'w:sdt' ||
8677
+ key === '#text' ||
8678
+ key === '_orderedChildren' ||
8679
+ key.startsWith('@_')
8680
+ ) {
8681
+ continue;
8682
+ }
8683
+ const others = sdtContent[key];
8684
+ const otherArray = Array.isArray(others)
8685
+ ? others
8686
+ : others !== undefined
8687
+ ? [others]
8688
+ : [];
8689
+ for (const otherObj of otherArray) {
8690
+ const rawXml = this.objectToXml({ [key]: otherObj });
8691
+ if (rawXml) content.push(new PreservedElement(rawXml, key, 'block'));
8692
+ }
8693
+ }
8189
8694
  }
8190
8695
  }
8191
8696
 
@@ -8198,7 +8703,10 @@ export class DocumentParser {
8198
8703
  }
8199
8704
  }
8200
8705
 
8201
- return new StructuredDocumentTag(properties, content);
8706
+ const sdt = new StructuredDocumentTag(properties, content);
8707
+ if (rawSdtPrXml !== undefined) sdt._setRawSdtPrXml(rawSdtPrXml);
8708
+ if (rawSdtEndPrXml !== undefined) sdt._setRawSdtEndPrXml(rawSdtEndPrXml);
8709
+ return sdt;
8202
8710
  } catch (error: unknown) {
8203
8711
  defaultLogger.warn(
8204
8712
  '[DocumentParser] Failed to parse SDT:',
@@ -8668,8 +9176,13 @@ export class DocumentParser {
8668
9176
  // FIX (v1.3.1): Use _orderedChildren to maintain document order of elements
8669
9177
  // This fixes TOC tab preservation - tabs must be in correct position
8670
9178
  const buildXml = (o: any, name?: string): string => {
8671
- if (typeof o === 'string') return o;
8672
- if (typeof o !== 'object') return String(o);
9179
+ // Text-only elements parse to a bare primitive (e.g. <w:t>Hi</w:t>
9180
+ // becomes 'Hi'); when a tag name is known the wrapper must be
9181
+ // re-emitted or the reconstructed XML silently loses the element.
9182
+ if (typeof o !== 'object' || o === null) {
9183
+ const text = String(o ?? '');
9184
+ return name ? `<${name}>${text}</${name}>` : text;
9185
+ }
8673
9186
 
8674
9187
  const keys = Object.keys(o);
8675
9188
 
@@ -9058,6 +9571,16 @@ export class DocumentParser {
9058
9571
  }
9059
9572
  }
9060
9573
 
9574
+ // The nested previous <w:sectPr> inside <w:sectPrChange> holds the
9575
+ // section's pre-revision state. Because extractElements is a flat scan,
9576
+ // any per-property call below would otherwise match that nested previous
9577
+ // element when the live sectPr omits the property — resurrecting a
9578
+ // tracked-removed property (e.g. titlePg/bidi) as a live one. Strip the
9579
+ // change subtree so only current properties are read here; the original
9580
+ // string is retained for the sectPrChange extraction further down.
9581
+ const sectPrWithChange = sectPr;
9582
+ sectPr = sectPr.replace(/<w:sectPrChange[\s\S]*?<\/w:sectPrChange>/, '');
9583
+
9061
9584
  const sectionProps: SectionProperties = {};
9062
9585
 
9063
9586
  // Parse page size
@@ -9464,8 +9987,10 @@ export class DocumentParser {
9464
9987
 
9465
9988
  const section = new Section(sectionProps);
9466
9989
 
9467
- // Parse section property change (w:sectPrChange) per ECMA-376 Part 1 §17.13.5.32
9468
- const sectPrChangeElements = XMLParser.extractElements(sectPr, 'w:sectPrChange');
9990
+ // Parse section property change (w:sectPrChange) per ECMA-376 Part 1 §17.13.5.32.
9991
+ // Use the unstripped string: the change subtree was removed from `sectPr`
9992
+ // above so it could not leak into the live property reads.
9993
+ const sectPrChangeElements = XMLParser.extractElements(sectPrWithChange, 'w:sectPrChange');
9469
9994
  if (sectPrChangeElements.length > 0 && sectPrChangeElements[0]) {
9470
9995
  const changeXml = sectPrChangeElements[0];
9471
9996
  const id = XMLParser.extractAttribute(changeXml, 'w:id') || '0';
@@ -9530,22 +10055,36 @@ export class DocumentParser {
9530
10055
  }
9531
10056
  }
9532
10057
 
9533
- // Extract basedOn
9534
- const basedOnElement = XMLParser.extractBetweenTags(styleXml, '<w:basedOn', '</w:basedOn>');
9535
- const basedOn = basedOnElement
9536
- ? XMLParser.extractAttribute(`<w:basedOn${basedOnElement}`, 'w:val')
9537
- : undefined;
10058
+ // Extract basedOn (CT_String is empty-content per ECMA-376 §17.7.4.3, so Word
10059
+ // serializes <w:basedOn w:val="..."/> self-closing; extractBetweenTags only
10060
+ // matches an explicit closing tag and would drop the inheritance chain)
10061
+ let basedOn: string | undefined;
10062
+ const basedOnAttrs = XMLParser.extractSelfClosingTag(styleXml, 'w:basedOn');
10063
+ if (basedOnAttrs) {
10064
+ basedOn = XMLParser.extractAttribute(`<w:basedOn${basedOnAttrs}/>`, 'w:val');
10065
+ }
9538
10066
 
9539
- // Extract next
9540
- const nextElement = XMLParser.extractBetweenTags(styleXml, '<w:next', '</w:next>');
9541
- const next = nextElement
9542
- ? XMLParser.extractAttribute(`<w:next${nextElement}`, 'w:val')
9543
- : undefined;
10067
+ // Extract next (same empty-content serialization as basedOn)
10068
+ let next: string | undefined;
10069
+ const nextAttrs = XMLParser.extractSelfClosingTag(styleXml, 'w:next');
10070
+ if (nextAttrs) {
10071
+ next = XMLParser.extractAttribute(`<w:next${nextAttrs}/>`, 'w:val');
10072
+ }
10073
+
10074
+ // Conditional table-style blocks (w:tblStylePr) carry their own pPr/rPr.
10075
+ // Per CT_Style (ECMA-376 §17.7.4.17) the root pPr/rPr precede those
10076
+ // blocks, so a first-match search over the full element would misattribute
10077
+ // a conditional block's formatting (e.g. firstRow bold/centered in banded
10078
+ // table styles) as the style's unconditional base formatting whenever the
10079
+ // root pPr/rPr are absent. Search a copy with the conditional blocks
10080
+ // stripped; parseConditionalFormattingFromXml still receives the full
10081
+ // styleXml and parses those blocks separately.
10082
+ const styleBase = styleXml.replace(/<w:tblStylePr[\s\S]*?<\/w:tblStylePr>/g, '');
9544
10083
 
9545
10084
  // Parse paragraph formatting (w:pPr)
9546
10085
  let paragraphFormatting: ParagraphFormatting | undefined;
9547
10086
  let styleNumPr: { numId?: number; ilvl?: number } | undefined;
9548
- const pPrXml = XMLParser.extractBetweenTags(styleXml, '<w:pPr>', '</w:pPr>');
10087
+ const pPrXml = XMLParser.extractBetweenTags(styleBase, '<w:pPr>', '</w:pPr>');
9549
10088
  if (pPrXml) {
9550
10089
  paragraphFormatting = this.parseParagraphFormattingFromXml(pPrXml);
9551
10090
 
@@ -9567,7 +10106,7 @@ export class DocumentParser {
9567
10106
 
9568
10107
  // Parse run formatting (w:rPr)
9569
10108
  let runFormatting: RunFormatting | undefined;
9570
- const rPrXml = XMLParser.extractBetweenTags(styleXml, '<w:rPr>', '</w:rPr>');
10109
+ const rPrXml = XMLParser.extractBetweenTags(styleBase, '<w:rPr>', '</w:rPr>');
9571
10110
  if (rPrXml) {
9572
10111
  runFormatting = this.parseRunFormattingFromXml(rPrXml);
9573
10112
  }
@@ -11181,15 +11720,43 @@ export class DocumentParser {
11181
11720
  * Parses document background (w:background) per ECMA-376 Part 1 §17.2.1
11182
11721
  * The w:background element appears as a child of w:document, before w:body
11183
11722
  */
11184
- private parseDocumentBackground(
11185
- docXml: string
11186
- ): { color?: string; themeColor?: string; themeTint?: string; themeShade?: string } | undefined {
11187
- const bgMatch = /<w:background([^>]*?)\/>/.exec(docXml);
11188
- if (!bgMatch?.[1]) return undefined;
11189
-
11190
- const attrStr = bgMatch[1];
11191
- const result: { color?: string; themeColor?: string; themeTint?: string; themeShade?: string } =
11192
- {};
11723
+ private parseDocumentBackground(docXml: string):
11724
+ | {
11725
+ color?: string;
11726
+ themeColor?: string;
11727
+ themeTint?: string;
11728
+ themeShade?: string;
11729
+ rawInnerXml?: string;
11730
+ }
11731
+ | undefined {
11732
+ // Match both the self-closing form (flat-color backgrounds) and the
11733
+ // expanded form: Word emits <w:background ...><v:background>...</v:background>
11734
+ // </w:background> for picture/gradient/texture page backgrounds per
11735
+ // ECMA-376 §17.2.1, and that child fill must survive the round-trip.
11736
+ const bgMatch = /<w:background([^>]*?)(\/>|>)/.exec(docXml);
11737
+ if (!bgMatch) return undefined;
11738
+
11739
+ const attrStr = bgMatch[1] ?? '';
11740
+ const result: {
11741
+ color?: string;
11742
+ themeColor?: string;
11743
+ themeTint?: string;
11744
+ themeShade?: string;
11745
+ rawInnerXml?: string;
11746
+ } = {};
11747
+
11748
+ // Expanded form: capture inner content verbatim as a passthrough slot
11749
+ // (VML has no object model here; re-emitted untouched by the generator)
11750
+ if (bgMatch[2] === '>') {
11751
+ const innerStart = bgMatch.index + bgMatch[0].length;
11752
+ const closeIndex = docXml.indexOf('</w:background>', innerStart);
11753
+ if (closeIndex !== -1) {
11754
+ const inner = docXml.substring(innerStart, closeIndex);
11755
+ if (inner.length > 0) {
11756
+ result.rawInnerXml = inner;
11757
+ }
11758
+ }
11759
+ }
11193
11760
 
11194
11761
  const colorMatch = /w:color="([^"]+)"/.exec(attrStr);
11195
11762
  if (colorMatch?.[1]) result.color = colorMatch[1];