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
@@ -162,9 +162,12 @@ export class XMLBuilder {
162
162
  // Add attributes
163
163
  if (element.attributes) {
164
164
  for (const [key, value] of Object.entries(element.attributes)) {
165
- if (value !== undefined && value !== null && value !== false) {
166
- // Handle boolean attributes
167
- const attrValue = value === true ? key : String(value);
165
+ if (value !== undefined && value !== null) {
166
+ // Booleans serialize as ST_OnOff literals (ECMA-376 §22.9.2.13).
167
+ // Explicit false must emit "0" dropping it would flip an explicit
168
+ // override to inherited/on under CT_OnOff presence semantics.
169
+ // Callers signal absence with undefined, never false.
170
+ const attrValue = value === true ? '1' : value === false ? '0' : String(value);
168
171
  // Use escapeXmlAttribute for attribute values (Issue #8)
169
172
  xml += ` ${key}="${XMLBuilder.escapeXmlAttribute(attrValue)}"`;
170
173
  }
@@ -245,18 +248,47 @@ export class XMLBuilder {
245
248
  .replace(/'/g, ''');
246
249
  }
247
250
 
251
+ /** The five predefined XML 1.0 named entities */
252
+ private static readonly NAMED_ENTITIES: Record<string, string> = {
253
+ lt: '<',
254
+ gt: '>',
255
+ quot: '"',
256
+ apos: "'",
257
+ amp: '&',
258
+ };
259
+
248
260
  /**
249
261
  * Unescapes XML entities back to original characters
262
+ * Decodes the five predefined named entities plus numeric character
263
+ * references (&#8217; / &#xA0;), which XML 1.0 §4.1 requires every
264
+ * conforming processor to expand — third-party producers emit them, and
265
+ * leaving them raw double-escapes the ampersand on save.
250
266
  * @param text Text with XML entities
251
267
  * @returns Unescaped text
252
268
  */
253
269
  static unescapeXml(text: string): string {
254
- return text
255
- .replace(/&lt;/g, '<')
256
- .replace(/&gt;/g, '>')
257
- .replace(/&quot;/g, '"')
258
- .replace(/&apos;/g, "'")
259
- .replace(/&amp;/g, '&'); // Must be last to avoid double-unescaping
270
+ // Single pass so text produced by one decode is never re-decoded by a
271
+ // later one (e.g. '&#38;amp;' must yield '&amp;', not '&').
272
+ return text.replace(
273
+ /&(?:(lt|gt|quot|apos|amp)|#x([0-9A-Fa-f]+)|#(\d+));/g,
274
+ (match, named?: string, hex?: string, dec?: string) => {
275
+ if (named !== undefined) {
276
+ return XMLBuilder.NAMED_ENTITIES[named] ?? match;
277
+ }
278
+ const code = hex !== undefined ? parseInt(hex, 16) : Number(dec);
279
+ // Code points outside the XML 1.0 Char production (NUL, surrogates,
280
+ // beyond U+10FFFF) would make the decoded string unserializable —
281
+ // keep the raw reference instead of injecting an invalid character.
282
+ const isXmlChar =
283
+ code === 0x9 ||
284
+ code === 0xa ||
285
+ code === 0xd ||
286
+ (code >= 0x20 && code <= 0xd7ff) ||
287
+ (code >= 0xe000 && code <= 0xfffd) ||
288
+ (code >= 0x10000 && code <= 0x10ffff);
289
+ return isXmlChar ? String.fromCodePoint(code) : match;
290
+ }
291
+ );
260
292
  }
261
293
 
262
294
  /**
@@ -633,11 +665,7 @@ export class XMLBuilder {
633
665
  const builder = new XMLBuilder();
634
666
  const element = XMLBuilder.objectToElement(obj, rootName);
635
667
  if (element) {
636
- if (typeof element === 'string') {
637
- builder.text(element);
638
- } else {
639
- builder.elements.push(element);
640
- }
668
+ builder.elements.push(element);
641
669
  }
642
670
  return builder.build();
643
671
  }
@@ -649,22 +677,21 @@ export class XMLBuilder {
649
677
  private static objectToElement(
650
678
  obj: ParsedXmlObject | string | number | boolean | null | undefined,
651
679
  name: string
652
- ): XMLElement | string | null {
680
+ ): XMLElement | null {
653
681
  if (obj === null || obj === undefined) {
654
682
  return null;
655
683
  }
656
684
 
657
- if (typeof obj !== 'object' || obj === null) {
658
- return String(obj);
685
+ if (typeof obj !== 'object') {
686
+ // The parser collapses text-only elements to their primitive value, so
687
+ // the wrapping tag must be re-emitted here — bare text inside container
688
+ // elements (e.g. w:r) is invalid WordprocessingML.
689
+ return { name, children: [String(obj)] };
659
690
  }
660
691
 
661
692
  const attributes: Record<string, string | number | boolean> = {};
662
693
  const children: (XMLElement | string)[] = [];
663
694
 
664
- if (obj['#text'] && Object.keys(obj).length === 1) {
665
- return String(obj['#text']);
666
- }
667
-
668
695
  for (const key in obj) {
669
696
  if (key.startsWith('@_')) {
670
697
  const attrName = key.substring(2);
@@ -674,7 +701,40 @@ export class XMLBuilder {
674
701
  }
675
702
  } else if (key === '#text') {
676
703
  children.push(String(obj[key]));
677
- } else {
704
+ }
705
+ }
706
+
707
+ // _orderedChildren is parser metadata, never document content. When
708
+ // present, it restores the original interleaving of same-named siblings
709
+ // (e.g. w:t / w:tab / w:t in a run) that the keyed object form loses.
710
+ const orderedChildren = obj['_orderedChildren'] as
711
+ | { type: string; index: number }[]
712
+ | undefined;
713
+
714
+ if (orderedChildren && orderedChildren.length > 0) {
715
+ for (const childInfo of orderedChildren) {
716
+ const childValue = obj[childInfo.type];
717
+ if (childValue === undefined) {
718
+ continue;
719
+ }
720
+ const item = Array.isArray(childValue)
721
+ ? childValue[childInfo.index]
722
+ : childInfo.index === 0
723
+ ? childValue
724
+ : undefined;
725
+ if (item === undefined) {
726
+ continue;
727
+ }
728
+ const childElement = XMLBuilder.objectToElement(item, childInfo.type);
729
+ if (childElement) {
730
+ children.push(childElement);
731
+ }
732
+ }
733
+ } else {
734
+ for (const key in obj) {
735
+ if (key.startsWith('@_') || key === '#text' || key === '_orderedChildren') {
736
+ continue;
737
+ }
678
738
  const childObj = obj[key];
679
739
  if (Array.isArray(childObj)) {
680
740
  childObj.forEach((item) => {
@@ -139,8 +139,9 @@ export class XMLParser {
139
139
  continue;
140
140
  }
141
141
 
142
- // Find the end of opening tag
143
- const openEnd = xml.indexOf('>', startIdx);
142
+ // Find the end of opening tag (quote-aware: attribute values may
143
+ // legally contain a raw '>')
144
+ const openEnd = XMLParser.findTagEnd(xml, startIdx);
144
145
  if (openEnd === -1) break;
145
146
 
146
147
  // Check if self-closing
@@ -333,8 +334,9 @@ export class XMLParser {
333
334
  const startIdx = xml.indexOf(startTag);
334
335
  if (startIdx === -1) return undefined;
335
336
 
336
- // Find the end of the opening tag
337
- const openEnd = xml.indexOf('>', startIdx);
337
+ // Find the end of the opening tag (quote-aware: attribute values may
338
+ // legally contain a raw '>')
339
+ const openEnd = XMLParser.findTagEnd(xml, startIdx);
338
340
  if (openEnd === -1) return undefined;
339
341
 
340
342
  // Find the closing tag
@@ -344,6 +346,28 @@ export class XMLParser {
344
346
  return xml.substring(openEnd + 1, endIdx);
345
347
  }
346
348
 
349
+ /**
350
+ * Finds the first '>' at or after startPos that is not inside a quoted
351
+ * attribute value. XML 1.0 §2.3 permits a raw '>' inside attribute values
352
+ * (only '<', '&', and the delimiting quote must be escaped), so a bare
353
+ * indexOf('>') can end a tag header mid-attribute.
354
+ * @private
355
+ */
356
+ private static findTagEnd(xml: string, startPos: number): number {
357
+ let quote: string | undefined;
358
+ for (let pos = startPos; pos < xml.length; pos++) {
359
+ const ch = xml[pos];
360
+ if (quote) {
361
+ if (ch === quote) quote = undefined;
362
+ } else if (ch === '"' || ch === "'") {
363
+ quote = ch;
364
+ } else if (ch === '>') {
365
+ return pos;
366
+ }
367
+ }
368
+ return -1;
369
+ }
370
+
347
371
  /**
348
372
  * Extracts a complete self-closing tag with its attributes
349
373
  * Handles cases where multiple similar tags exist (e.g., <w:sz.../> and <w:szCs.../>)
@@ -371,19 +395,17 @@ export class XMLParser {
371
395
 
372
396
  // Valid separators after tag name: space, '/', or '>'
373
397
  if (charAfterTag === ' ' || charAfterTag === '/' || charAfterTag === '>') {
374
- // Found the exact tag, now find its end
375
- const endIdx = xml.indexOf('/>', startIdx);
376
- if (endIdx === -1) {
377
- // Try finding a closing tag instead (non-self-closing)
378
- const closeTagStart = xml.indexOf('>', startIdx);
379
- if (closeTagStart === -1) return undefined;
380
-
381
- // Return attributes portion
382
- return xml.substring(startIdx + startPattern.length, closeTagStart);
383
- }
384
-
385
- // Return attributes portion (between tag name and />)
386
- return xml.substring(startIdx + startPattern.length, endIdx);
398
+ // Found the exact tag. Bound the search to this element's own tag
399
+ // header: a global indexOf('/>') can land inside a later sibling
400
+ // when the target is written in expanded form (<tag ...></tag>),
401
+ // attributing the sibling's attributes to this tag.
402
+ const tagEnd = XMLParser.findTagEnd(xml, startIdx + startPattern.length);
403
+ if (tagEnd === -1) return undefined;
404
+
405
+ // Self-closing: exclude the trailing '/'; expanded form: attributes
406
+ // run up to the '>' of the open tag.
407
+ const attrEnd = xml[tagEnd - 1] === '/' ? tagEnd - 1 : tagEnd;
408
+ return xml.substring(startIdx + startPattern.length, attrEnd);
387
409
  }
388
410
 
389
411
  // Not the exact tag (e.g., found "w:sz" when looking for "w:s")
@@ -477,6 +499,27 @@ export class XMLParser {
477
499
  return { value: {}, endPos: xml.length };
478
500
  }
479
501
 
502
+ // Skip processing instructions (e.g. <?mso-application ...?>); without
503
+ // this, '?' fails the element-name match and the PI body is mangled
504
+ // into text one character at a time
505
+ if (xml.substring(openTagStart, openTagStart + 2) === '<?') {
506
+ const piEnd = xml.indexOf('?>', openTagStart + 2);
507
+ if (piEnd !== -1) {
508
+ return XMLParser.parseElementToObject(xml, piEnd + 2, options, depth);
509
+ }
510
+ return { value: {}, endPos: xml.length };
511
+ }
512
+
513
+ // Skip CDATA found where an element is expected (e.g. at the root);
514
+ // CDATA inside element content is preserved by the content loop below
515
+ if (xml.substring(openTagStart, openTagStart + 9) === '<![CDATA[') {
516
+ const cdataEnd = xml.indexOf(']]>', openTagStart + 9);
517
+ if (cdataEnd !== -1) {
518
+ return XMLParser.parseElementToObject(xml, cdataEnd + 3, options, depth);
519
+ }
520
+ return { value: {}, endPos: xml.length };
521
+ }
522
+
480
523
  // Extract element name
481
524
  const nameMatch = /^([a-zA-Z0-9:_-]+)/.exec(xml.substring(openTagStart + 1));
482
525
  if (!nameMatch) {
@@ -485,7 +528,8 @@ export class XMLParser {
485
528
 
486
529
  const originalElementName: string = nameMatch[1] || '';
487
530
  let elementName: string = originalElementName;
488
- const tagHeaderEnd = xml.indexOf('>', openTagStart);
531
+ // Quote-aware scan: attribute values may legally contain a raw '>'
532
+ const tagHeaderEnd = XMLParser.findTagEnd(xml, openTagStart);
489
533
  if (tagHeaderEnd === -1) {
490
534
  return { value: {}, endPos: xml.length };
491
535
  }
@@ -558,6 +602,19 @@ export class XMLParser {
558
602
  }
559
603
  }
560
604
 
605
+ // CDATA: append the payload verbatim — its content is by definition
606
+ // already unescaped, so entity decoding must not be applied
607
+ if (content.startsWith('<![CDATA[', nextTag)) {
608
+ const cdataEnd = content.indexOf(']]>', nextTag + 9);
609
+ if (cdataEnd === -1) {
610
+ textContent += content.substring(nextTag + 9);
611
+ break;
612
+ }
613
+ textContent += content.substring(nextTag + 9, cdataEnd);
614
+ pos = cdataEnd + 3;
615
+ continue;
616
+ }
617
+
561
618
  // Parse child element (increment depth for children)
562
619
  const childResult = XMLParser.parseElementToObject(content, nextTag, options, depth + 1);
563
620
  const childObj = childResult.value as ParsedXMLObject;
@@ -602,6 +659,11 @@ export class XMLParser {
602
659
  // Add children
603
660
  if (children.length > 0) {
604
661
  const coalescedChildren = XMLParser.coalesceChildren(children, options);
662
+ // Mixed content with no attributes leaves elementValue as a bare
663
+ // string; fold it into object form so merging children keeps the text
664
+ if (typeof elementValue === 'string') {
665
+ elementValue = { [options.textNodeName]: elementValue };
666
+ }
605
667
  if (typeof elementValue === 'object' && !Array.isArray(elementValue)) {
606
668
  elementValue = { ...elementValue, ...coalescedChildren };
607
669
  } else {
@@ -705,10 +767,15 @@ export class XMLParser {
705
767
  // Add prefix to attribute name
706
768
  const prefixedName = options.attributeNamePrefix + attrName;
707
769
 
770
+ // Unescape XML entities so stored attribute values are the actual
771
+ // decoded strings, matching text-node handling and extractAttribute.
772
+ // This prevents double-escaping when the value is later re-serialized.
773
+ const decodedValue = XMLBuilder.unescapeXml(attrValue);
774
+
708
775
  // Parse attribute value
709
776
  attributes[prefixedName] = options.parseAttributeValue
710
- ? XMLParser.parseValue(attrValue)
711
- : attrValue;
777
+ ? XMLParser.parseValue(decodedValue)
778
+ : decodedValue;
712
779
  }
713
780
 
714
781
  return attributes;
@@ -14,7 +14,9 @@ import {
14
14
  SizeLimitOptions,
15
15
  DEFAULT_SIZE_LIMITS,
16
16
  } from './types.js';
17
+ import { ResourceLimitError } from './errors.js';
17
18
  import { getGlobalLogger, createScopedLogger, ILogger } from '../utils/logger.js';
19
+ import { normalizePath } from '../utils/validation.js';
18
20
 
19
21
  // Create scoped logger for ZipHandler operations
20
22
  function getLogger(): ILogger {
@@ -41,16 +43,19 @@ export class ZipHandler {
41
43
  * Validates document size against configured limits
42
44
  * @param sizeMB - Size in megabytes
43
45
  * @param limits - Size limit options (merged with defaults)
44
- * @throws Error if size exceeds maximum
46
+ * @throws {ResourceLimitError} If the compressed size exceeds maxSizeMB
45
47
  */
46
48
  private validateDocumentSize(sizeMB: number, limits: Required<SizeLimitOptions>): void {
47
49
  const logger = getLogger();
48
50
  const { warningSizeMB, maxSizeMB } = limits;
49
51
 
50
- // Check maximum size (if enabled)
52
+ // Check maximum size (if enabled). Throw the typed ResourceLimitError so this
53
+ // compressed-size guard is catchable by the same type as the uncompressed/ratio/
54
+ // entry-count guards enforced downstream in ZipReader — callers can distinguish a
55
+ // hostile/oversized payload from a genuinely corrupt archive.
51
56
  if (maxSizeMB > 0 && sizeMB > maxSizeMB) {
52
57
  logger.error('Document exceeds maximum size', { sizeMB: sizeMB.toFixed(1), maxSizeMB });
53
- throw new Error(
58
+ throw new ResourceLimitError(
54
59
  `Document size (${sizeMB.toFixed(1)}MB) exceeds maximum supported size (${maxSizeMB}MB). ` +
55
60
  `This would likely cause out-of-memory errors. Consider:\n` +
56
61
  `- Compressing/optimizing images\n` +
@@ -98,15 +103,26 @@ export class ZipHandler {
98
103
  const limits = this.getSizeLimits(options);
99
104
  this.validateDocumentSize(sizeMB, limits);
100
105
 
101
- await this.reader.loadFromFile(filePath, options);
102
-
103
- // Copy all files from reader to writer for modification
104
- const files = this.reader.getAllFiles();
105
- this.writer.clear();
106
- this.writer.addFiles(files);
107
-
108
- this.mode = 'modify';
109
- logger.info('DOCX file loaded', { fileCount: files.size, sizeMB: sizeMB.toFixed(2) });
106
+ try {
107
+ await this.reader.loadFromFile(filePath, options);
108
+
109
+ // Copy all files from reader to writer for modification
110
+ const files = this.reader.getAllFiles();
111
+ this.writer.clear();
112
+ this.writer.addFiles(files);
113
+
114
+ this.mode = 'modify';
115
+ logger.info('DOCX file loaded', { fileCount: files.size, sizeMB: sizeMB.toFixed(2) });
116
+ } catch (error) {
117
+ // A failed reload must not leave the handler serving the previously loaded
118
+ // archive. Reset to a deterministically empty state so getFile/save/etc.
119
+ // cannot silently return stale content. A fresh reader discards any
120
+ // partially-extracted files and the stale `loaded` flag.
121
+ this.writer.clear();
122
+ this.reader = new ZipReader();
123
+ this.mode = 'write';
124
+ throw error;
125
+ }
110
126
  }
111
127
 
112
128
  /**
@@ -125,15 +141,26 @@ export class ZipHandler {
125
141
  const limits = this.getSizeLimits(options);
126
142
  this.validateDocumentSize(sizeMB, limits);
127
143
 
128
- await this.reader.loadFromBuffer(buffer, options);
129
-
130
- // Copy all files from reader to writer for modification
131
- const files = this.reader.getAllFiles();
132
- this.writer.clear();
133
- this.writer.addFiles(files);
134
-
135
- this.mode = 'modify';
136
- logger.info('DOCX buffer loaded', { fileCount: files.size, sizeMB: sizeMB.toFixed(2) });
144
+ try {
145
+ await this.reader.loadFromBuffer(buffer, options);
146
+
147
+ // Copy all files from reader to writer for modification
148
+ const files = this.reader.getAllFiles();
149
+ this.writer.clear();
150
+ this.writer.addFiles(files);
151
+
152
+ this.mode = 'modify';
153
+ logger.info('DOCX buffer loaded', { fileCount: files.size, sizeMB: sizeMB.toFixed(2) });
154
+ } catch (error) {
155
+ // A failed reload must not leave the handler serving the previously loaded
156
+ // archive. Reset to a deterministically empty state so getFile/save/etc.
157
+ // cannot silently return stale content. A fresh reader discards any
158
+ // partially-extracted files and the stale `loaded` flag.
159
+ this.writer.clear();
160
+ this.reader = new ZipReader();
161
+ this.mode = 'write';
162
+ throw error;
163
+ }
137
164
  }
138
165
 
139
166
  // ==================== FILE OPERATIONS ====================
@@ -300,6 +327,13 @@ export class ZipHandler {
300
327
  if (!file) {
301
328
  return false;
302
329
  }
330
+ // Guard against degenerate rename where source and destination collapse to
331
+ // the same archive key (identical strings or backslash/forward-slash variants).
332
+ // Without this, addFile would overwrite the single entry and the subsequent
333
+ // removeFile would delete it, silently destroying the file.
334
+ if (normalizePath(oldPath) === normalizePath(newPath)) {
335
+ return true;
336
+ }
303
337
  this.addFile(newPath, file.content, {
304
338
  binary: file.isBinary,
305
339
  date: file.date,
@@ -336,6 +370,12 @@ export class ZipHandler {
336
370
  if (!this.copyFile(srcPath, destPath)) {
337
371
  return false;
338
372
  }
373
+ // Guard against degenerate move where source and destination collapse to the
374
+ // same archive key. copyFile already re-added the (identical) entry, so the
375
+ // removeFile below would delete it, silently destroying the file.
376
+ if (normalizePath(srcPath) === normalizePath(destPath)) {
377
+ return true;
378
+ }
339
379
  this.removeFile(srcPath);
340
380
  return true;
341
381
  }