@cj-tech-master/excelts 9.4.2 → 9.5.0

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 (618) hide show
  1. package/dist/browser/index.browser.d.ts +8 -5
  2. package/dist/browser/index.browser.js +19 -1
  3. package/dist/browser/index.d.ts +4 -2
  4. package/dist/browser/index.js +9 -1
  5. package/dist/browser/modules/excel/chart/cache-populator.d.ts +49 -0
  6. package/dist/browser/modules/excel/chart/cache-populator.js +1171 -0
  7. package/dist/browser/modules/excel/chart/chart-api.d.ts +92 -0
  8. package/dist/browser/modules/excel/chart/chart-api.js +364 -0
  9. package/dist/browser/modules/excel/chart/chart-builder.d.ts +48 -0
  10. package/dist/browser/modules/excel/chart/chart-builder.js +2432 -0
  11. package/dist/browser/modules/excel/chart/chart-ex-builder.d.ts +36 -0
  12. package/dist/browser/modules/excel/chart/chart-ex-builder.js +903 -0
  13. package/dist/browser/modules/excel/chart/chart-ex-parser.d.ts +8 -0
  14. package/dist/browser/modules/excel/chart/chart-ex-parser.js +1205 -0
  15. package/dist/browser/modules/excel/chart/chart-ex-renderer.d.ts +187 -0
  16. package/dist/browser/modules/excel/chart/chart-ex-renderer.js +5352 -0
  17. package/dist/browser/modules/excel/chart/chart-ex-types.d.ts +531 -0
  18. package/dist/browser/modules/excel/chart/chart-ex-types.js +11 -0
  19. package/dist/browser/modules/excel/chart/chart-images.d.ts +78 -0
  20. package/dist/browser/modules/excel/chart/chart-images.js +363 -0
  21. package/dist/browser/modules/excel/chart/chart-presets.d.ts +392 -0
  22. package/dist/browser/modules/excel/chart/chart-presets.js +179 -0
  23. package/dist/browser/modules/excel/chart/chart-renderer.d.ts +550 -0
  24. package/dist/browser/modules/excel/chart/chart-renderer.js +6440 -0
  25. package/dist/browser/modules/excel/chart/chart-sidecar.d.ts +21 -0
  26. package/dist/browser/modules/excel/chart/chart-sidecar.js +427 -0
  27. package/dist/browser/modules/excel/chart/chart-utils.d.ts +306 -0
  28. package/dist/browser/modules/excel/chart/chart-utils.js +821 -0
  29. package/dist/browser/modules/excel/chart/chart.d.ts +504 -0
  30. package/dist/browser/modules/excel/chart/chart.js +1320 -0
  31. package/dist/browser/modules/excel/chart/glyph-rasterizer.d.ts +62 -0
  32. package/dist/browser/modules/excel/chart/glyph-rasterizer.js +658 -0
  33. package/dist/browser/modules/excel/chart/index.d.ts +54 -0
  34. package/dist/browser/modules/excel/chart/index.js +46 -0
  35. package/dist/browser/modules/excel/chart/install.d.ts +44 -0
  36. package/dist/browser/modules/excel/chart/install.js +91 -0
  37. package/dist/browser/modules/excel/chart/shape-properties.d.ts +156 -0
  38. package/dist/browser/modules/excel/chart/shape-properties.js +1557 -0
  39. package/dist/browser/modules/excel/chart/stroke-font.d.ts +36 -0
  40. package/dist/browser/modules/excel/chart/stroke-font.js +1556 -0
  41. package/dist/browser/modules/excel/chart/topojson.d.ts +98 -0
  42. package/dist/browser/modules/excel/chart/topojson.js +236 -0
  43. package/dist/browser/modules/excel/chart/types.d.ts +2559 -0
  44. package/dist/browser/modules/excel/chart/types.js +8 -0
  45. package/dist/browser/modules/excel/chart-host-registry.d.ts +157 -0
  46. package/dist/browser/modules/excel/chart-host-registry.js +90 -0
  47. package/dist/browser/modules/excel/chartsheet.d.ts +102 -0
  48. package/dist/browser/modules/excel/chartsheet.js +196 -0
  49. package/dist/browser/modules/excel/defined-names.d.ts +35 -0
  50. package/dist/browser/modules/excel/defined-names.js +44 -4
  51. package/dist/browser/modules/excel/errors.d.ts +6 -0
  52. package/dist/browser/modules/excel/errors.js +9 -0
  53. package/dist/browser/modules/excel/form-control.d.ts +6 -0
  54. package/dist/browser/modules/excel/form-control.js +17 -0
  55. package/dist/browser/modules/excel/image.js +12 -2
  56. package/dist/browser/modules/excel/pivot-chart.d.ts +7 -0
  57. package/dist/browser/modules/excel/pivot-chart.js +53 -0
  58. package/dist/browser/modules/excel/pivot-table.d.ts +55 -0
  59. package/dist/browser/modules/excel/pivot-table.js +35 -0
  60. package/dist/browser/modules/excel/range.js +5 -1
  61. package/dist/browser/modules/excel/sparkline/index.d.ts +7 -0
  62. package/dist/browser/modules/excel/sparkline/index.js +7 -0
  63. package/dist/browser/modules/excel/sparkline/sparkline.d.ts +206 -0
  64. package/dist/browser/modules/excel/sparkline/sparkline.js +750 -0
  65. package/dist/browser/modules/excel/stream/worksheet-writer.js +3 -2
  66. package/dist/browser/modules/excel/table.js +42 -6
  67. package/dist/browser/modules/excel/types.d.ts +72 -0
  68. package/dist/browser/modules/excel/utils/address.d.ts +18 -0
  69. package/dist/browser/modules/excel/utils/address.js +28 -0
  70. package/dist/browser/modules/excel/utils/drawing-utils.js +11 -6
  71. package/dist/browser/modules/excel/utils/guid.d.ts +15 -0
  72. package/dist/browser/modules/excel/utils/guid.js +35 -0
  73. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +74 -0
  74. package/dist/browser/modules/excel/utils/ooxml-paths.js +206 -9
  75. package/dist/browser/modules/excel/utils/ooxml-validator/check-chart-sidecar.d.ts +35 -0
  76. package/dist/browser/modules/excel/utils/ooxml-validator/check-chart-sidecar.js +101 -0
  77. package/dist/browser/modules/excel/utils/ooxml-validator/check-chart.d.ts +32 -0
  78. package/dist/browser/modules/excel/utils/ooxml-validator/check-chart.js +2125 -0
  79. package/dist/browser/modules/excel/utils/ooxml-validator/check-chartsheet.d.ts +9 -0
  80. package/dist/browser/modules/excel/utils/ooxml-validator/check-chartsheet.js +26 -0
  81. package/dist/browser/modules/excel/utils/ooxml-validator/check-content-types.d.ts +16 -0
  82. package/dist/browser/modules/excel/utils/ooxml-validator/check-content-types.js +181 -0
  83. package/dist/browser/modules/excel/utils/ooxml-validator/check-drawing.d.ts +34 -0
  84. package/dist/browser/modules/excel/utils/ooxml-validator/check-drawing.js +267 -0
  85. package/dist/browser/modules/excel/utils/ooxml-validator/check-pivot.d.ts +14 -0
  86. package/dist/browser/modules/excel/utils/ooxml-validator/check-pivot.js +104 -0
  87. package/dist/browser/modules/excel/utils/ooxml-validator/check-relationships.d.ts +18 -0
  88. package/dist/browser/modules/excel/utils/ooxml-validator/check-relationships.js +184 -0
  89. package/dist/browser/modules/excel/utils/ooxml-validator/check-structure.d.ts +21 -0
  90. package/dist/browser/modules/excel/utils/ooxml-validator/check-structure.js +56 -0
  91. package/dist/browser/modules/excel/utils/ooxml-validator/check-styles.d.ts +15 -0
  92. package/dist/browser/modules/excel/utils/ooxml-validator/check-styles.js +89 -0
  93. package/dist/browser/modules/excel/utils/ooxml-validator/check-table.d.ts +31 -0
  94. package/dist/browser/modules/excel/utils/ooxml-validator/check-table.js +177 -0
  95. package/dist/browser/modules/excel/utils/ooxml-validator/check-workbook.d.ts +19 -0
  96. package/dist/browser/modules/excel/utils/ooxml-validator/check-workbook.js +163 -0
  97. package/dist/browser/modules/excel/utils/ooxml-validator/check-worksheet.d.ts +25 -0
  98. package/dist/browser/modules/excel/utils/ooxml-validator/check-worksheet.js +569 -0
  99. package/dist/browser/modules/excel/utils/ooxml-validator/context.d.ts +85 -0
  100. package/dist/browser/modules/excel/utils/ooxml-validator/context.js +191 -0
  101. package/dist/browser/modules/excel/utils/ooxml-validator/index.d.ts +31 -0
  102. package/dist/browser/modules/excel/utils/ooxml-validator/index.js +102 -0
  103. package/dist/browser/modules/excel/utils/ooxml-validator/path-utils.d.ts +67 -0
  104. package/dist/browser/modules/excel/utils/ooxml-validator/path-utils.js +156 -0
  105. package/dist/browser/modules/excel/utils/ooxml-validator/reporter.d.ts +41 -0
  106. package/dist/browser/modules/excel/utils/ooxml-validator/reporter.js +61 -0
  107. package/dist/browser/modules/excel/utils/ooxml-validator/types.d.ts +109 -0
  108. package/dist/browser/modules/excel/utils/ooxml-validator/types.js +12 -0
  109. package/dist/browser/modules/excel/utils/ooxml-validator/xml-utils.d.ts +38 -0
  110. package/dist/browser/modules/excel/utils/ooxml-validator/xml-utils.js +100 -0
  111. package/dist/browser/modules/excel/workbook.browser.d.ts +248 -30
  112. package/dist/browser/modules/excel/workbook.browser.js +966 -31
  113. package/dist/browser/modules/excel/workbook.d.ts +43 -0
  114. package/dist/browser/modules/excel/workbook.js +48 -0
  115. package/dist/browser/modules/excel/worksheet.d.ts +157 -3
  116. package/dist/browser/modules/excel/worksheet.js +394 -35
  117. package/dist/browser/modules/excel/xlsx/rel-type.d.ts +40 -0
  118. package/dist/browser/modules/excel/xlsx/rel-type.js +41 -1
  119. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +1 -0
  120. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.js +11 -2
  121. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.js +12 -10
  122. package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +96 -22
  123. package/dist/browser/modules/excel/xlsx/xform/chart/chart-space-xform.d.ts +353 -0
  124. package/dist/browser/modules/excel/xlsx/xform/chart/chart-space-xform.js +6000 -0
  125. package/dist/browser/modules/excel/xlsx/xform/comment/threaded-comments-xform.d.ts +60 -0
  126. package/dist/browser/modules/excel/xlsx/xform/comment/threaded-comments-xform.js +213 -0
  127. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +150 -11
  128. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +20 -1
  129. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +1 -1
  130. package/dist/browser/modules/excel/xlsx/xform/drawing/drawing-xform.d.ts +30 -0
  131. package/dist/browser/modules/excel/xlsx/xform/drawing/drawing-xform.js +109 -5
  132. package/dist/browser/modules/excel/xlsx/xform/drawing/graphic-frame-xform.d.ts +54 -0
  133. package/dist/browser/modules/excel/xlsx/xform/drawing/graphic-frame-xform.js +225 -0
  134. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +3 -1
  135. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +18 -3
  136. package/dist/browser/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.d.ts +46 -0
  137. package/dist/browser/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +294 -12
  138. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +13 -2
  139. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +32 -6
  140. package/dist/browser/modules/excel/xlsx/xform/sheet/chartsheet-xform.d.ts +185 -0
  141. package/dist/browser/modules/excel/xlsx/xform/sheet/chartsheet-xform.js +441 -0
  142. package/dist/browser/modules/excel/xlsx/xform/sheet/ext-lst-xform.d.ts +1 -0
  143. package/dist/browser/modules/excel/xlsx/xform/sheet/ext-lst-xform.js +51 -2
  144. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +196 -20
  145. package/dist/browser/modules/excel/xlsx/xform/table/auto-filter-xform.js +16 -1
  146. package/dist/browser/modules/excel/xlsx/xform/table/table-column-xform.js +17 -2
  147. package/dist/browser/modules/excel/xlsx/xform/xsd-values.d.ts +63 -0
  148. package/dist/browser/modules/excel/xlsx/xform/xsd-values.js +101 -0
  149. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +115 -21
  150. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +4422 -78
  151. package/dist/browser/modules/pdf/builder/document-builder.d.ts +74 -0
  152. package/dist/browser/modules/pdf/builder/document-builder.js +507 -2
  153. package/dist/browser/modules/pdf/builder/pdf-editor.js +48 -3
  154. package/dist/browser/modules/pdf/excel-bridge.d.ts +69 -0
  155. package/dist/browser/modules/pdf/excel-bridge.js +683 -12
  156. package/dist/browser/modules/pdf/font/font-manager.d.ts +25 -0
  157. package/dist/browser/modules/pdf/font/font-manager.js +39 -0
  158. package/dist/browser/modules/pdf/index.d.ts +5 -2
  159. package/dist/browser/modules/pdf/index.js +3 -1
  160. package/dist/browser/modules/pdf/render/chart-surface.d.ts +33 -0
  161. package/dist/browser/modules/pdf/render/chart-surface.js +200 -0
  162. package/dist/browser/modules/pdf/render/layout-engine.d.ts +22 -1
  163. package/dist/browser/modules/pdf/render/layout-engine.js +436 -56
  164. package/dist/browser/modules/pdf/render/page-renderer.js +169 -28
  165. package/dist/browser/modules/pdf/render/pdf-exporter.js +117 -7
  166. package/dist/browser/modules/pdf/types.d.ts +227 -23
  167. package/dist/browser/modules/pdf/types.js +4 -0
  168. package/dist/browser/modules/pdf/word-bridge.d.ts +47 -0
  169. package/dist/browser/modules/pdf/word-bridge.js +304 -0
  170. package/dist/browser/modules/word/constants.d.ts +179 -0
  171. package/dist/browser/modules/word/constants.js +231 -0
  172. package/dist/browser/modules/word/content-types.d.ts +27 -0
  173. package/dist/browser/modules/word/content-types.js +53 -0
  174. package/dist/browser/modules/word/digital-signatures.d.ts +87 -0
  175. package/dist/browser/modules/word/digital-signatures.js +134 -0
  176. package/dist/browser/modules/word/document.d.ts +728 -0
  177. package/dist/browser/modules/word/document.js +1795 -0
  178. package/dist/browser/modules/word/docx-packager.d.ts +14 -0
  179. package/dist/browser/modules/word/docx-packager.js +822 -0
  180. package/dist/browser/modules/word/docx-reader.d.ts +11 -0
  181. package/dist/browser/modules/word/docx-reader.js +4929 -0
  182. package/dist/browser/modules/word/encryption.d.ts +102 -0
  183. package/dist/browser/modules/word/encryption.js +274 -0
  184. package/dist/browser/modules/word/errors.d.ts +49 -0
  185. package/dist/browser/modules/word/errors.js +68 -0
  186. package/dist/browser/modules/word/font-obfuscation.d.ts +31 -0
  187. package/dist/browser/modules/word/font-obfuscation.js +83 -0
  188. package/dist/browser/modules/word/html-renderer.d.ts +38 -0
  189. package/dist/browser/modules/word/html-renderer.js +782 -0
  190. package/dist/browser/modules/word/index.base.d.ts +19 -0
  191. package/dist/browser/modules/word/index.base.js +51 -0
  192. package/dist/browser/modules/word/index.browser.d.ts +4 -0
  193. package/dist/browser/modules/word/index.browser.js +4 -0
  194. package/dist/browser/modules/word/index.d.ts +4 -0
  195. package/dist/browser/modules/word/index.js +4 -0
  196. package/dist/browser/modules/word/internal-utils.d.ts +23 -0
  197. package/dist/browser/modules/word/internal-utils.js +54 -0
  198. package/dist/browser/modules/word/relationships.d.ts +31 -0
  199. package/dist/browser/modules/word/relationships.js +56 -0
  200. package/dist/browser/modules/word/types.d.ts +2325 -0
  201. package/dist/browser/modules/word/types.js +10 -0
  202. package/dist/browser/modules/word/units.d.ts +49 -0
  203. package/dist/browser/modules/word/units.js +111 -0
  204. package/dist/browser/modules/word/writers/chart-writer.d.ts +10 -0
  205. package/dist/browser/modules/word/writers/chart-writer.js +385 -0
  206. package/dist/browser/modules/word/writers/checkbox-writer.d.ts +9 -0
  207. package/dist/browser/modules/word/writers/checkbox-writer.js +42 -0
  208. package/dist/browser/modules/word/writers/comment-writer.d.ts +15 -0
  209. package/dist/browser/modules/word/writers/comment-writer.js +70 -0
  210. package/dist/browser/modules/word/writers/document-writer.d.ts +16 -0
  211. package/dist/browser/modules/word/writers/document-writer.js +461 -0
  212. package/dist/browser/modules/word/writers/footnote-writer.d.ts +11 -0
  213. package/dist/browser/modules/word/writers/footnote-writer.js +72 -0
  214. package/dist/browser/modules/word/writers/header-footer-writer.d.ts +13 -0
  215. package/dist/browser/modules/word/writers/header-footer-writer.js +129 -0
  216. package/dist/browser/modules/word/writers/image-writer.d.ts +10 -0
  217. package/dist/browser/modules/word/writers/image-writer.js +185 -0
  218. package/dist/browser/modules/word/writers/math-writer.d.ts +9 -0
  219. package/dist/browser/modules/word/writers/math-writer.js +428 -0
  220. package/dist/browser/modules/word/writers/numbering-writer.d.ts +10 -0
  221. package/dist/browser/modules/word/writers/numbering-writer.js +125 -0
  222. package/dist/browser/modules/word/writers/paragraph-writer.d.ts +13 -0
  223. package/dist/browser/modules/word/writers/paragraph-writer.js +516 -0
  224. package/dist/browser/modules/word/writers/parts-writer.d.ts +26 -0
  225. package/dist/browser/modules/word/writers/parts-writer.js +660 -0
  226. package/dist/browser/modules/word/writers/run-writer.d.ts +15 -0
  227. package/dist/browser/modules/word/writers/run-writer.js +649 -0
  228. package/dist/browser/modules/word/writers/section-writer.d.ts +10 -0
  229. package/dist/browser/modules/word/writers/section-writer.js +238 -0
  230. package/dist/browser/modules/word/writers/styles-writer.d.ts +10 -0
  231. package/dist/browser/modules/word/writers/styles-writer.js +242 -0
  232. package/dist/browser/modules/word/writers/table-writer.d.ts +10 -0
  233. package/dist/browser/modules/word/writers/table-writer.js +503 -0
  234. package/dist/browser/modules/word/writers/textbox-writer.d.ts +9 -0
  235. package/dist/browser/modules/word/writers/textbox-writer.js +53 -0
  236. package/dist/browser/modules/word/writers/toc-writer.d.ts +9 -0
  237. package/dist/browser/modules/word/writers/toc-writer.js +79 -0
  238. package/dist/browser/modules/xml/encode.d.ts +56 -7
  239. package/dist/browser/modules/xml/encode.js +157 -11
  240. package/dist/cjs/index.js +13 -2
  241. package/dist/cjs/modules/excel/chart/cache-populator.js +1178 -0
  242. package/dist/cjs/modules/excel/chart/chart-api.js +371 -0
  243. package/dist/cjs/modules/excel/chart/chart-builder.js +2440 -0
  244. package/dist/cjs/modules/excel/chart/chart-ex-builder.js +907 -0
  245. package/dist/cjs/modules/excel/chart/chart-ex-parser.js +1208 -0
  246. package/dist/cjs/modules/excel/chart/chart-ex-renderer.js +5364 -0
  247. package/dist/cjs/modules/excel/chart/chart-ex-types.js +12 -0
  248. package/dist/cjs/modules/excel/chart/chart-images.js +366 -0
  249. package/dist/cjs/modules/excel/chart/chart-presets.js +184 -0
  250. package/dist/cjs/modules/excel/chart/chart-renderer.js +6450 -0
  251. package/dist/cjs/modules/excel/chart/chart-sidecar.js +433 -0
  252. package/dist/cjs/modules/excel/chart/chart-utils.js +845 -0
  253. package/dist/cjs/modules/excel/chart/chart.js +1324 -0
  254. package/dist/cjs/modules/excel/chart/glyph-rasterizer.js +664 -0
  255. package/dist/cjs/modules/excel/chart/index.js +101 -0
  256. package/dist/cjs/modules/excel/chart/install.js +95 -0
  257. package/dist/cjs/modules/excel/chart/shape-properties.js +1577 -0
  258. package/dist/cjs/modules/excel/chart/stroke-font.js +1559 -0
  259. package/dist/cjs/modules/excel/chart/topojson.js +239 -0
  260. package/dist/cjs/modules/excel/chart/types.js +9 -0
  261. package/dist/cjs/modules/excel/chart-host-registry.js +96 -0
  262. package/dist/cjs/modules/excel/chartsheet.js +199 -0
  263. package/dist/cjs/modules/excel/defined-names.js +44 -4
  264. package/dist/cjs/modules/excel/errors.js +11 -1
  265. package/dist/cjs/modules/excel/form-control.js +17 -0
  266. package/dist/cjs/modules/excel/image.js +12 -2
  267. package/dist/cjs/modules/excel/pivot-chart.js +56 -0
  268. package/dist/cjs/modules/excel/pivot-table.js +35 -0
  269. package/dist/cjs/modules/excel/range.js +5 -1
  270. package/dist/cjs/modules/excel/sparkline/index.js +23 -0
  271. package/dist/cjs/modules/excel/sparkline/sparkline.js +756 -0
  272. package/dist/cjs/modules/excel/stream/worksheet-writer.js +3 -2
  273. package/dist/cjs/modules/excel/table.js +42 -6
  274. package/dist/cjs/modules/excel/utils/address.js +29 -0
  275. package/dist/cjs/modules/excel/utils/drawing-utils.js +11 -6
  276. package/dist/cjs/modules/excel/utils/guid.js +38 -0
  277. package/dist/cjs/modules/excel/utils/ooxml-paths.js +246 -9
  278. package/dist/cjs/modules/excel/utils/ooxml-validator/check-chart-sidecar.js +103 -0
  279. package/dist/cjs/modules/excel/utils/ooxml-validator/check-chart.js +2128 -0
  280. package/dist/cjs/modules/excel/utils/ooxml-validator/check-chartsheet.js +29 -0
  281. package/dist/cjs/modules/excel/utils/ooxml-validator/check-content-types.js +184 -0
  282. package/dist/cjs/modules/excel/utils/ooxml-validator/check-drawing.js +270 -0
  283. package/dist/cjs/modules/excel/utils/ooxml-validator/check-pivot.js +107 -0
  284. package/dist/cjs/modules/excel/utils/ooxml-validator/check-relationships.js +188 -0
  285. package/dist/cjs/modules/excel/utils/ooxml-validator/check-structure.js +60 -0
  286. package/dist/cjs/modules/excel/utils/ooxml-validator/check-styles.js +92 -0
  287. package/dist/cjs/modules/excel/utils/ooxml-validator/check-table.js +180 -0
  288. package/dist/cjs/modules/excel/utils/ooxml-validator/check-workbook.js +166 -0
  289. package/dist/cjs/modules/excel/utils/ooxml-validator/check-worksheet.js +572 -0
  290. package/dist/cjs/modules/excel/utils/ooxml-validator/context.js +196 -0
  291. package/dist/cjs/modules/excel/utils/ooxml-validator/index.js +105 -0
  292. package/dist/cjs/modules/excel/utils/ooxml-validator/path-utils.js +168 -0
  293. package/dist/cjs/modules/excel/utils/ooxml-validator/reporter.js +66 -0
  294. package/dist/cjs/modules/excel/utils/ooxml-validator/types.js +13 -0
  295. package/dist/cjs/modules/excel/utils/ooxml-validator/xml-utils.js +110 -0
  296. package/dist/cjs/modules/excel/workbook.browser.js +973 -38
  297. package/dist/cjs/modules/excel/workbook.js +48 -0
  298. package/dist/cjs/modules/excel/worksheet.js +393 -34
  299. package/dist/cjs/modules/excel/xlsx/rel-type.js +41 -1
  300. package/dist/cjs/modules/excel/xlsx/xform/book/defined-name-xform.js +11 -2
  301. package/dist/cjs/modules/excel/xlsx/xform/book/external-link-xform.js +12 -10
  302. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +96 -22
  303. package/dist/cjs/modules/excel/xlsx/xform/chart/chart-space-xform.js +6003 -0
  304. package/dist/cjs/modules/excel/xlsx/xform/comment/threaded-comments-xform.js +219 -0
  305. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +149 -10
  306. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +20 -1
  307. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +1 -1
  308. package/dist/cjs/modules/excel/xlsx/xform/drawing/drawing-xform.js +109 -5
  309. package/dist/cjs/modules/excel/xlsx/xform/drawing/graphic-frame-xform.js +228 -0
  310. package/dist/cjs/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +18 -3
  311. package/dist/cjs/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +294 -12
  312. package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +32 -6
  313. package/dist/cjs/modules/excel/xlsx/xform/sheet/chartsheet-xform.js +444 -0
  314. package/dist/cjs/modules/excel/xlsx/xform/sheet/ext-lst-xform.js +51 -2
  315. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +195 -19
  316. package/dist/cjs/modules/excel/xlsx/xform/table/auto-filter-xform.js +16 -1
  317. package/dist/cjs/modules/excel/xlsx/xform/table/table-column-xform.js +17 -2
  318. package/dist/cjs/modules/excel/xlsx/xform/xsd-values.js +106 -0
  319. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +4420 -76
  320. package/dist/cjs/modules/pdf/builder/document-builder.js +506 -1
  321. package/dist/cjs/modules/pdf/builder/pdf-editor.js +48 -3
  322. package/dist/cjs/modules/pdf/excel-bridge.js +684 -12
  323. package/dist/cjs/modules/pdf/font/font-manager.js +39 -0
  324. package/dist/cjs/modules/pdf/index.js +5 -1
  325. package/dist/cjs/modules/pdf/render/chart-surface.js +203 -0
  326. package/dist/cjs/modules/pdf/render/layout-engine.js +437 -56
  327. package/dist/cjs/modules/pdf/render/page-renderer.js +169 -28
  328. package/dist/cjs/modules/pdf/render/pdf-exporter.js +115 -5
  329. package/dist/cjs/modules/pdf/types.js +5 -0
  330. package/dist/cjs/modules/pdf/word-bridge.js +307 -0
  331. package/dist/cjs/modules/word/constants.js +234 -0
  332. package/dist/cjs/modules/word/content-types.js +57 -0
  333. package/dist/cjs/modules/word/digital-signatures.js +140 -0
  334. package/dist/cjs/modules/word/document.js +1909 -0
  335. package/dist/cjs/modules/word/docx-packager.js +825 -0
  336. package/dist/cjs/modules/word/docx-reader.js +4932 -0
  337. package/dist/cjs/modules/word/encryption.js +282 -0
  338. package/dist/cjs/modules/word/errors.js +88 -0
  339. package/dist/cjs/modules/word/font-obfuscation.js +88 -0
  340. package/dist/cjs/modules/word/html-renderer.js +785 -0
  341. package/dist/cjs/modules/word/index.base.js +199 -0
  342. package/dist/cjs/modules/word/index.browser.js +20 -0
  343. package/dist/cjs/modules/word/index.js +20 -0
  344. package/dist/cjs/modules/word/internal-utils.js +59 -0
  345. package/dist/cjs/modules/word/relationships.js +60 -0
  346. package/dist/cjs/modules/word/types.js +11 -0
  347. package/dist/cjs/modules/word/units.js +135 -0
  348. package/dist/cjs/modules/word/writers/chart-writer.js +388 -0
  349. package/dist/cjs/modules/word/writers/checkbox-writer.js +45 -0
  350. package/dist/cjs/modules/word/writers/comment-writer.js +74 -0
  351. package/dist/cjs/modules/word/writers/document-writer.js +465 -0
  352. package/dist/cjs/modules/word/writers/footnote-writer.js +76 -0
  353. package/dist/cjs/modules/word/writers/header-footer-writer.js +134 -0
  354. package/dist/cjs/modules/word/writers/image-writer.js +188 -0
  355. package/dist/cjs/modules/word/writers/math-writer.js +431 -0
  356. package/dist/cjs/modules/word/writers/numbering-writer.js +128 -0
  357. package/dist/cjs/modules/word/writers/paragraph-writer.js +521 -0
  358. package/dist/cjs/modules/word/writers/parts-writer.js +671 -0
  359. package/dist/cjs/modules/word/writers/run-writer.js +655 -0
  360. package/dist/cjs/modules/word/writers/section-writer.js +241 -0
  361. package/dist/cjs/modules/word/writers/styles-writer.js +245 -0
  362. package/dist/cjs/modules/word/writers/table-writer.js +506 -0
  363. package/dist/cjs/modules/word/writers/textbox-writer.js +56 -0
  364. package/dist/cjs/modules/word/writers/toc-writer.js +82 -0
  365. package/dist/cjs/modules/xml/encode.js +158 -11
  366. package/dist/esm/index.browser.js +20 -2
  367. package/dist/esm/index.js +9 -1
  368. package/dist/esm/modules/excel/chart/cache-populator.js +1171 -0
  369. package/dist/esm/modules/excel/chart/chart-api.js +364 -0
  370. package/dist/esm/modules/excel/chart/chart-builder.js +2432 -0
  371. package/dist/esm/modules/excel/chart/chart-ex-builder.js +903 -0
  372. package/dist/esm/modules/excel/chart/chart-ex-parser.js +1205 -0
  373. package/dist/esm/modules/excel/chart/chart-ex-renderer.js +5352 -0
  374. package/dist/esm/modules/excel/chart/chart-ex-types.js +11 -0
  375. package/dist/esm/modules/excel/chart/chart-images.js +363 -0
  376. package/dist/esm/modules/excel/chart/chart-presets.js +179 -0
  377. package/dist/esm/modules/excel/chart/chart-renderer.js +6440 -0
  378. package/dist/esm/modules/excel/chart/chart-sidecar.js +427 -0
  379. package/dist/esm/modules/excel/chart/chart-utils.js +821 -0
  380. package/dist/esm/modules/excel/chart/chart.js +1320 -0
  381. package/dist/esm/modules/excel/chart/glyph-rasterizer.js +658 -0
  382. package/dist/esm/modules/excel/chart/index.js +46 -0
  383. package/dist/esm/modules/excel/chart/install.js +91 -0
  384. package/dist/esm/modules/excel/chart/shape-properties.js +1557 -0
  385. package/dist/esm/modules/excel/chart/stroke-font.js +1556 -0
  386. package/dist/esm/modules/excel/chart/topojson.js +236 -0
  387. package/dist/esm/modules/excel/chart/types.js +8 -0
  388. package/dist/esm/modules/excel/chart-host-registry.js +90 -0
  389. package/dist/esm/modules/excel/chartsheet.js +196 -0
  390. package/dist/esm/modules/excel/defined-names.js +44 -4
  391. package/dist/esm/modules/excel/errors.js +9 -0
  392. package/dist/esm/modules/excel/form-control.js +17 -0
  393. package/dist/esm/modules/excel/image.js +12 -2
  394. package/dist/esm/modules/excel/pivot-chart.js +53 -0
  395. package/dist/esm/modules/excel/pivot-table.js +35 -0
  396. package/dist/esm/modules/excel/range.js +5 -1
  397. package/dist/esm/modules/excel/sparkline/index.js +7 -0
  398. package/dist/esm/modules/excel/sparkline/sparkline.js +750 -0
  399. package/dist/esm/modules/excel/stream/worksheet-writer.js +3 -2
  400. package/dist/esm/modules/excel/table.js +42 -6
  401. package/dist/esm/modules/excel/utils/address.js +28 -0
  402. package/dist/esm/modules/excel/utils/drawing-utils.js +11 -6
  403. package/dist/esm/modules/excel/utils/guid.js +35 -0
  404. package/dist/esm/modules/excel/utils/ooxml-paths.js +206 -9
  405. package/dist/esm/modules/excel/utils/ooxml-validator/check-chart-sidecar.js +101 -0
  406. package/dist/esm/modules/excel/utils/ooxml-validator/check-chart.js +2125 -0
  407. package/dist/esm/modules/excel/utils/ooxml-validator/check-chartsheet.js +26 -0
  408. package/dist/esm/modules/excel/utils/ooxml-validator/check-content-types.js +181 -0
  409. package/dist/esm/modules/excel/utils/ooxml-validator/check-drawing.js +267 -0
  410. package/dist/esm/modules/excel/utils/ooxml-validator/check-pivot.js +104 -0
  411. package/dist/esm/modules/excel/utils/ooxml-validator/check-relationships.js +184 -0
  412. package/dist/esm/modules/excel/utils/ooxml-validator/check-structure.js +56 -0
  413. package/dist/esm/modules/excel/utils/ooxml-validator/check-styles.js +89 -0
  414. package/dist/esm/modules/excel/utils/ooxml-validator/check-table.js +177 -0
  415. package/dist/esm/modules/excel/utils/ooxml-validator/check-workbook.js +163 -0
  416. package/dist/esm/modules/excel/utils/ooxml-validator/check-worksheet.js +569 -0
  417. package/dist/esm/modules/excel/utils/ooxml-validator/context.js +191 -0
  418. package/dist/esm/modules/excel/utils/ooxml-validator/index.js +102 -0
  419. package/dist/esm/modules/excel/utils/ooxml-validator/path-utils.js +156 -0
  420. package/dist/esm/modules/excel/utils/ooxml-validator/reporter.js +61 -0
  421. package/dist/esm/modules/excel/utils/ooxml-validator/types.js +12 -0
  422. package/dist/esm/modules/excel/utils/ooxml-validator/xml-utils.js +100 -0
  423. package/dist/esm/modules/excel/workbook.browser.js +969 -34
  424. package/dist/esm/modules/excel/workbook.js +48 -0
  425. package/dist/esm/modules/excel/worksheet.js +394 -35
  426. package/dist/esm/modules/excel/xlsx/rel-type.js +41 -1
  427. package/dist/esm/modules/excel/xlsx/xform/book/defined-name-xform.js +11 -2
  428. package/dist/esm/modules/excel/xlsx/xform/book/external-link-xform.js +12 -10
  429. package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +96 -22
  430. package/dist/esm/modules/excel/xlsx/xform/chart/chart-space-xform.js +6000 -0
  431. package/dist/esm/modules/excel/xlsx/xform/comment/threaded-comments-xform.js +213 -0
  432. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +150 -11
  433. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +20 -1
  434. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +1 -1
  435. package/dist/esm/modules/excel/xlsx/xform/drawing/drawing-xform.js +109 -5
  436. package/dist/esm/modules/excel/xlsx/xform/drawing/graphic-frame-xform.js +225 -0
  437. package/dist/esm/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +18 -3
  438. package/dist/esm/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +294 -12
  439. package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +32 -6
  440. package/dist/esm/modules/excel/xlsx/xform/sheet/chartsheet-xform.js +441 -0
  441. package/dist/esm/modules/excel/xlsx/xform/sheet/ext-lst-xform.js +51 -2
  442. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +196 -20
  443. package/dist/esm/modules/excel/xlsx/xform/table/auto-filter-xform.js +16 -1
  444. package/dist/esm/modules/excel/xlsx/xform/table/table-column-xform.js +17 -2
  445. package/dist/esm/modules/excel/xlsx/xform/xsd-values.js +101 -0
  446. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +4422 -78
  447. package/dist/esm/modules/pdf/builder/document-builder.js +507 -2
  448. package/dist/esm/modules/pdf/builder/pdf-editor.js +48 -3
  449. package/dist/esm/modules/pdf/excel-bridge.js +683 -12
  450. package/dist/esm/modules/pdf/font/font-manager.js +39 -0
  451. package/dist/esm/modules/pdf/index.js +3 -1
  452. package/dist/esm/modules/pdf/render/chart-surface.js +200 -0
  453. package/dist/esm/modules/pdf/render/layout-engine.js +436 -56
  454. package/dist/esm/modules/pdf/render/page-renderer.js +169 -28
  455. package/dist/esm/modules/pdf/render/pdf-exporter.js +117 -7
  456. package/dist/esm/modules/pdf/types.js +4 -0
  457. package/dist/esm/modules/pdf/word-bridge.js +304 -0
  458. package/dist/esm/modules/word/constants.js +231 -0
  459. package/dist/esm/modules/word/content-types.js +53 -0
  460. package/dist/esm/modules/word/digital-signatures.js +134 -0
  461. package/dist/esm/modules/word/document.js +1795 -0
  462. package/dist/esm/modules/word/docx-packager.js +822 -0
  463. package/dist/esm/modules/word/docx-reader.js +4929 -0
  464. package/dist/esm/modules/word/encryption.js +274 -0
  465. package/dist/esm/modules/word/errors.js +68 -0
  466. package/dist/esm/modules/word/font-obfuscation.js +83 -0
  467. package/dist/esm/modules/word/html-renderer.js +782 -0
  468. package/dist/esm/modules/word/index.base.js +51 -0
  469. package/dist/esm/modules/word/index.browser.js +4 -0
  470. package/dist/esm/modules/word/index.js +4 -0
  471. package/dist/esm/modules/word/internal-utils.js +54 -0
  472. package/dist/esm/modules/word/relationships.js +56 -0
  473. package/dist/esm/modules/word/types.js +10 -0
  474. package/dist/esm/modules/word/units.js +111 -0
  475. package/dist/esm/modules/word/writers/chart-writer.js +385 -0
  476. package/dist/esm/modules/word/writers/checkbox-writer.js +42 -0
  477. package/dist/esm/modules/word/writers/comment-writer.js +70 -0
  478. package/dist/esm/modules/word/writers/document-writer.js +461 -0
  479. package/dist/esm/modules/word/writers/footnote-writer.js +72 -0
  480. package/dist/esm/modules/word/writers/header-footer-writer.js +129 -0
  481. package/dist/esm/modules/word/writers/image-writer.js +185 -0
  482. package/dist/esm/modules/word/writers/math-writer.js +428 -0
  483. package/dist/esm/modules/word/writers/numbering-writer.js +125 -0
  484. package/dist/esm/modules/word/writers/paragraph-writer.js +516 -0
  485. package/dist/esm/modules/word/writers/parts-writer.js +660 -0
  486. package/dist/esm/modules/word/writers/run-writer.js +649 -0
  487. package/dist/esm/modules/word/writers/section-writer.js +238 -0
  488. package/dist/esm/modules/word/writers/styles-writer.js +242 -0
  489. package/dist/esm/modules/word/writers/table-writer.js +503 -0
  490. package/dist/esm/modules/word/writers/textbox-writer.js +53 -0
  491. package/dist/esm/modules/word/writers/toc-writer.js +79 -0
  492. package/dist/esm/modules/xml/encode.js +157 -11
  493. package/dist/iife/excelts.iife.js +11789 -687
  494. package/dist/iife/excelts.iife.js.map +1 -1
  495. package/dist/iife/excelts.iife.min.js +52 -44
  496. package/dist/types/index.browser.d.ts +8 -5
  497. package/dist/types/index.d.ts +4 -2
  498. package/dist/types/modules/excel/chart/cache-populator.d.ts +49 -0
  499. package/dist/types/modules/excel/chart/chart-api.d.ts +92 -0
  500. package/dist/types/modules/excel/chart/chart-builder.d.ts +48 -0
  501. package/dist/types/modules/excel/chart/chart-ex-builder.d.ts +36 -0
  502. package/dist/types/modules/excel/chart/chart-ex-parser.d.ts +8 -0
  503. package/dist/types/modules/excel/chart/chart-ex-renderer.d.ts +187 -0
  504. package/dist/types/modules/excel/chart/chart-ex-types.d.ts +531 -0
  505. package/dist/types/modules/excel/chart/chart-images.d.ts +78 -0
  506. package/dist/types/modules/excel/chart/chart-presets.d.ts +392 -0
  507. package/dist/types/modules/excel/chart/chart-renderer.d.ts +550 -0
  508. package/dist/types/modules/excel/chart/chart-sidecar.d.ts +21 -0
  509. package/dist/types/modules/excel/chart/chart-utils.d.ts +306 -0
  510. package/dist/types/modules/excel/chart/chart.d.ts +504 -0
  511. package/dist/types/modules/excel/chart/glyph-rasterizer.d.ts +62 -0
  512. package/dist/types/modules/excel/chart/index.d.ts +54 -0
  513. package/dist/types/modules/excel/chart/install.d.ts +44 -0
  514. package/dist/types/modules/excel/chart/shape-properties.d.ts +156 -0
  515. package/dist/types/modules/excel/chart/stroke-font.d.ts +36 -0
  516. package/dist/types/modules/excel/chart/topojson.d.ts +98 -0
  517. package/dist/types/modules/excel/chart/types.d.ts +2559 -0
  518. package/dist/types/modules/excel/chart-host-registry.d.ts +157 -0
  519. package/dist/types/modules/excel/chartsheet.d.ts +102 -0
  520. package/dist/types/modules/excel/defined-names.d.ts +35 -0
  521. package/dist/types/modules/excel/errors.d.ts +6 -0
  522. package/dist/types/modules/excel/form-control.d.ts +6 -0
  523. package/dist/types/modules/excel/pivot-chart.d.ts +7 -0
  524. package/dist/types/modules/excel/pivot-table.d.ts +55 -0
  525. package/dist/types/modules/excel/sparkline/index.d.ts +7 -0
  526. package/dist/types/modules/excel/sparkline/sparkline.d.ts +206 -0
  527. package/dist/types/modules/excel/types.d.ts +72 -0
  528. package/dist/types/modules/excel/utils/address.d.ts +18 -0
  529. package/dist/types/modules/excel/utils/guid.d.ts +15 -0
  530. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +74 -0
  531. package/dist/types/modules/excel/utils/ooxml-validator/check-chart-sidecar.d.ts +35 -0
  532. package/dist/types/modules/excel/utils/ooxml-validator/check-chart.d.ts +32 -0
  533. package/dist/types/modules/excel/utils/ooxml-validator/check-chartsheet.d.ts +9 -0
  534. package/dist/types/modules/excel/utils/ooxml-validator/check-content-types.d.ts +16 -0
  535. package/dist/types/modules/excel/utils/ooxml-validator/check-drawing.d.ts +34 -0
  536. package/dist/types/modules/excel/utils/ooxml-validator/check-pivot.d.ts +14 -0
  537. package/dist/types/modules/excel/utils/ooxml-validator/check-relationships.d.ts +18 -0
  538. package/dist/types/modules/excel/utils/ooxml-validator/check-structure.d.ts +21 -0
  539. package/dist/types/modules/excel/utils/ooxml-validator/check-styles.d.ts +15 -0
  540. package/dist/types/modules/excel/utils/ooxml-validator/check-table.d.ts +31 -0
  541. package/dist/types/modules/excel/utils/ooxml-validator/check-workbook.d.ts +19 -0
  542. package/dist/types/modules/excel/utils/ooxml-validator/check-worksheet.d.ts +25 -0
  543. package/dist/types/modules/excel/utils/ooxml-validator/context.d.ts +85 -0
  544. package/dist/types/modules/excel/utils/ooxml-validator/index.d.ts +31 -0
  545. package/dist/types/modules/excel/utils/ooxml-validator/path-utils.d.ts +67 -0
  546. package/dist/types/modules/excel/utils/ooxml-validator/reporter.d.ts +41 -0
  547. package/dist/types/modules/excel/utils/ooxml-validator/types.d.ts +109 -0
  548. package/dist/types/modules/excel/utils/ooxml-validator/xml-utils.d.ts +38 -0
  549. package/dist/types/modules/excel/workbook.browser.d.ts +248 -30
  550. package/dist/types/modules/excel/workbook.d.ts +43 -0
  551. package/dist/types/modules/excel/worksheet.d.ts +157 -3
  552. package/dist/types/modules/excel/xlsx/rel-type.d.ts +40 -0
  553. package/dist/types/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +1 -0
  554. package/dist/types/modules/excel/xlsx/xform/chart/chart-space-xform.d.ts +353 -0
  555. package/dist/types/modules/excel/xlsx/xform/comment/threaded-comments-xform.d.ts +60 -0
  556. package/dist/types/modules/excel/xlsx/xform/drawing/drawing-xform.d.ts +30 -0
  557. package/dist/types/modules/excel/xlsx/xform/drawing/graphic-frame-xform.d.ts +54 -0
  558. package/dist/types/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +3 -1
  559. package/dist/types/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.d.ts +46 -0
  560. package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +13 -2
  561. package/dist/types/modules/excel/xlsx/xform/sheet/chartsheet-xform.d.ts +185 -0
  562. package/dist/types/modules/excel/xlsx/xform/sheet/ext-lst-xform.d.ts +1 -0
  563. package/dist/types/modules/excel/xlsx/xform/xsd-values.d.ts +63 -0
  564. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +115 -21
  565. package/dist/types/modules/pdf/builder/document-builder.d.ts +74 -0
  566. package/dist/types/modules/pdf/excel-bridge.d.ts +69 -0
  567. package/dist/types/modules/pdf/font/font-manager.d.ts +25 -0
  568. package/dist/types/modules/pdf/index.d.ts +5 -2
  569. package/dist/types/modules/pdf/render/chart-surface.d.ts +33 -0
  570. package/dist/types/modules/pdf/render/layout-engine.d.ts +22 -1
  571. package/dist/types/modules/pdf/types.d.ts +227 -23
  572. package/dist/types/modules/pdf/word-bridge.d.ts +47 -0
  573. package/dist/types/modules/word/constants.d.ts +179 -0
  574. package/dist/types/modules/word/content-types.d.ts +27 -0
  575. package/dist/types/modules/word/digital-signatures.d.ts +87 -0
  576. package/dist/types/modules/word/document.d.ts +728 -0
  577. package/dist/types/modules/word/docx-packager.d.ts +14 -0
  578. package/dist/types/modules/word/docx-reader.d.ts +11 -0
  579. package/dist/types/modules/word/encryption.d.ts +102 -0
  580. package/dist/types/modules/word/errors.d.ts +49 -0
  581. package/dist/types/modules/word/font-obfuscation.d.ts +31 -0
  582. package/dist/types/modules/word/html-renderer.d.ts +38 -0
  583. package/dist/types/modules/word/index.base.d.ts +19 -0
  584. package/dist/types/modules/word/index.browser.d.ts +4 -0
  585. package/dist/types/modules/word/index.d.ts +4 -0
  586. package/dist/types/modules/word/internal-utils.d.ts +23 -0
  587. package/dist/types/modules/word/relationships.d.ts +31 -0
  588. package/dist/types/modules/word/types.d.ts +2325 -0
  589. package/dist/types/modules/word/units.d.ts +49 -0
  590. package/dist/types/modules/word/writers/chart-writer.d.ts +10 -0
  591. package/dist/types/modules/word/writers/checkbox-writer.d.ts +9 -0
  592. package/dist/types/modules/word/writers/comment-writer.d.ts +15 -0
  593. package/dist/types/modules/word/writers/document-writer.d.ts +16 -0
  594. package/dist/types/modules/word/writers/footnote-writer.d.ts +11 -0
  595. package/dist/types/modules/word/writers/header-footer-writer.d.ts +13 -0
  596. package/dist/types/modules/word/writers/image-writer.d.ts +10 -0
  597. package/dist/types/modules/word/writers/math-writer.d.ts +9 -0
  598. package/dist/types/modules/word/writers/numbering-writer.d.ts +10 -0
  599. package/dist/types/modules/word/writers/paragraph-writer.d.ts +13 -0
  600. package/dist/types/modules/word/writers/parts-writer.d.ts +26 -0
  601. package/dist/types/modules/word/writers/run-writer.d.ts +15 -0
  602. package/dist/types/modules/word/writers/section-writer.d.ts +10 -0
  603. package/dist/types/modules/word/writers/styles-writer.d.ts +10 -0
  604. package/dist/types/modules/word/writers/table-writer.d.ts +10 -0
  605. package/dist/types/modules/word/writers/textbox-writer.d.ts +9 -0
  606. package/dist/types/modules/word/writers/toc-writer.d.ts +9 -0
  607. package/dist/types/modules/xml/encode.d.ts +56 -7
  608. package/package.json +29 -11
  609. package/dist/browser/modules/excel/utils/ooxml-validator.d.ts +0 -48
  610. package/dist/browser/modules/excel/utils/ooxml-validator.js +0 -493
  611. package/dist/browser/modules/excel/utils/passthrough-manager.d.ts +0 -77
  612. package/dist/browser/modules/excel/utils/passthrough-manager.js +0 -129
  613. package/dist/cjs/modules/excel/utils/ooxml-validator.js +0 -499
  614. package/dist/cjs/modules/excel/utils/passthrough-manager.js +0 -133
  615. package/dist/esm/modules/excel/utils/ooxml-validator.js +0 -493
  616. package/dist/esm/modules/excel/utils/passthrough-manager.js +0 -129
  617. package/dist/types/modules/excel/utils/ooxml-validator.d.ts +0 -48
  618. package/dist/types/modules/excel/utils/passthrough-manager.d.ts +0 -77
@@ -10,16 +10,22 @@
10
10
  */
11
11
  import { ZipParser } from "../../archive/unzip/zip-parser.js";
12
12
  import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
13
- import { ExcelStreamStateError, ExcelFileError, ImageError, ExcelNotSupportedError, XmlParseError, TableError } from "../errors.js";
13
+ // Chart serialisation / deserialisation goes through the chart-host-registry
14
+ // slot so the chart module is only pulled into consumer bundles when they
15
+ // explicitly `import "@cj-tech-master/excelts/chart"`. Type imports are
16
+ // erased at runtime; runtime entry points route through `getChartSupport()`.
17
+ import { getChartSupport } from "../chart-host-registry.js";
18
+ import { ExcelStreamStateError, ExcelFileError, ImageError, ExcelNotSupportedError, XmlParseError, TableError, ChartOptionsError } from "../errors.js";
14
19
  import { filterDrawingAnchors } from "../utils/drawing-utils.js";
15
20
  import { rewriteExternalRefs } from "../utils/external-link-formula.js";
16
- import { commentsPath, ctrlPropPath, drawingPath, drawingRelsPath, externalLinkPath, externalLinkRelsPath, externalLinkRelTargetFromWorkbook, OOXML_REL_TARGETS, pivotCacheDefinitionRelTargetFromWorkbook, pivotTablePathFromName, isCommentsPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getExternalLinkIndexFromPath, getExternalLinkIndexFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getVmlDrawingHFNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingPath, vmlDrawingHFPath, vmlDrawingHFRelsPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
17
- import { PassthroughManager } from "../utils/passthrough-manager.js";
21
+ import { commentsPath, chartsheetPath, chartsheetRelsPath, getChartsheetNoFromPath, getChartsheetNoFromRelsPath, ctrlPropPath, drawingPath, drawingRelsPath, externalLinkPath, externalLinkRelsPath, externalLinkRelTargetFromWorkbook, OOXML_REL_TARGETS, pivotCacheDefinitionRelTargetFromWorkbook, pivotTablePathFromName, isCommentsPath, chartPath, chartRelsPath, chartStylePath, chartColorsPath, chartExStylePath, chartExColorsPath, chartStyleRelTarget, chartExStyleRelTarget, chartExPath, chartExRelsPath, getChartExNumberFromPath, getChartExNumberFromRelsPath, chartColorsRelTarget, chartExColorsRelTarget, chartRelTargetFromDrawing, chartExRelTargetFromDrawing, chartUserShapesPath, chartUserShapesRelTarget, getChartNumberFromPath, getChartNumberFromRelsPath, getChartStyleNumberFromPath, getChartColorsNumberFromPath, getChartExStyleNumberFromPath, getChartExColorsNumberFromPath, getDrawingNameFromPath, getChartUserShapesNameFromPath, getDrawingNameFromRelsPath, getExternalLinkIndexFromPath, getExternalLinkIndexFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getVmlDrawingHFNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, resolveRelTarget, vmlDrawingPath, vmlDrawingHFPath, vmlDrawingHFRelsPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
22
+ import { validateXlsxBuffer } from "../utils/ooxml-validator/index.js";
18
23
  import { StreamBuf } from "../utils/stream-buf.js";
19
24
  import { RelType } from "./rel-type.js";
20
25
  import { ExternalLinkXform } from "./xform/book/external-link-xform.js";
21
26
  import { WorkbookXform } from "./xform/book/workbook-xform.js";
22
27
  import { CommentsXform } from "./xform/comment/comments-xform.js";
28
+ import { parsePersonList, parseThreadedComments, renderPersonList, renderThreadedComments } from "./xform/comment/threaded-comments-xform.js";
23
29
  import { AppXform } from "./xform/core/app-xform.js";
24
30
  import { ContentTypesXform } from "./xform/core/content-types-xform.js";
25
31
  import { CoreXform } from "./xform/core/core-xform.js";
@@ -32,6 +38,7 @@ import { VmlDrawingXform } from "./xform/drawing/vml-drawing-xform.js";
32
38
  import { PivotCacheDefinitionXform } from "./xform/pivot-table/pivot-cache-definition-xform.js";
33
39
  import { PivotCacheRecordsXform } from "./xform/pivot-table/pivot-cache-records-xform.js";
34
40
  import { PivotTableXform } from "./xform/pivot-table/pivot-table-xform.js";
41
+ import { ChartsheetXform } from "./xform/sheet/chartsheet-xform.js";
35
42
  import { WorkSheetXform } from "./xform/sheet/worksheet-xform.js";
36
43
  import { SharedStringsXform } from "./xform/strings/shared-strings-xform.js";
37
44
  import { StylesXform } from "./xform/style/styles-xform.js";
@@ -40,7 +47,10 @@ import { theme1Xml } from "./xml/theme1.js";
40
47
  import { PassThrough } from "../../stream/index.js";
41
48
  import { concatUint8Arrays } from "../../../utils/binary.js";
42
49
  import { bufferToString, base64ToUint8Array } from "../../../utils/utils.js";
50
+ import { uuidV4 } from "../../../utils/uuid.js";
51
+ import { xmlEncode, xmlEncodeAttr } from "../../xml/encode.js";
43
52
  import { XmlStreamWriter } from "../../xml/stream-writer.js";
53
+ import { XmlWriter } from "../../xml/writer.js";
44
54
  class StreamingZipWriterAdapter {
45
55
  constructor(options) {
46
56
  this.events = new Map();
@@ -270,6 +280,3491 @@ function upsertSheet(link, sheetName) {
270
280
  link.sheetNames.push(sheetName);
271
281
  }
272
282
  }
283
+ function snapshotChartModel(model) {
284
+ try {
285
+ return JSON.stringify(model);
286
+ }
287
+ catch {
288
+ return undefined;
289
+ }
290
+ }
291
+ /**
292
+ * Extract leading XML comments that appear immediately before a target
293
+ * element's open tag in an OOXML chart part. We use this to preserve
294
+ * vendor / annotation comments (e.g. style provenance markers) when the
295
+ * chart writer falls back to a structured rebuild — `BaseXform.parseStreamDirect`
296
+ * does not surface `comment` events, so the structured model has no
297
+ * memory of them.
298
+ *
299
+ * Returns the substring of comment nodes (whitespace stripped, joined
300
+ * by no separator). Empty string when no comment precedes the open tag.
301
+ */
302
+ /**
303
+ * Build the chartsheet-drawing XML that wraps a single classic or
304
+ * ChartEx chart occupying the entire chartsheet canvas.
305
+ *
306
+ * Chartsheets have no cell grid — `sheetData` is empty and there are
307
+ * no `<cols>` / `<row>` sizing entries for Excel to lay an anchor
308
+ * against. A cell-based `<xdr:twoCellAnchor from="A1" to="R31"/>`
309
+ * (what the generic `DrawingXform` emits) therefore resolves to a
310
+ * 0×0 bounding box on a chartsheet, and Excel renders a blank
311
+ * white canvas with no chart inside. Using `<xdr:absoluteAnchor>`
312
+ * with concrete EMU coordinates is how Excel itself writes
313
+ * chartsheet drawings — the anchor's `pos`/`ext` pair gives the
314
+ * engine something real to lay the graphic against, while the
315
+ * inner `<xdr:graphicFrame>/<xdr:xfrm>` repeats the extent so the
316
+ * graphic is sized to fill the anchor.
317
+ *
318
+ * ChartEx drawings additionally need an `<mc:AlternateContent>`
319
+ * wrapper around the `<xdr:graphicFrame>` — the `cx` namespace is
320
+ * a Microsoft extension that legacy-Excel loaders don't understand,
321
+ * so the Fallback branch emits a placeholder shape (the same
322
+ * "This chart isn't available in your version of Excel" message
323
+ * Office uses).
324
+ */
325
+ function renderChartsheetDrawingXml(options) {
326
+ const { chartRId, chartName, isChartEx, extCx, extCy } = options;
327
+ const escName = xmlEncodeAttr(chartName);
328
+ const escRId = xmlEncodeAttr(chartRId);
329
+ const cNvPrExtLst = isChartEx
330
+ ? `<a:extLst><a:ext uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}"><a16:creationId xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" id="{${uuidV4().toUpperCase()}}"/></a:ext></a:extLst>`
331
+ : "";
332
+ const graphicFrame = `<xdr:graphicFrame macro="">` +
333
+ `<xdr:nvGraphicFramePr>` +
334
+ (cNvPrExtLst
335
+ ? `<xdr:cNvPr id="1" name="${escName}">${cNvPrExtLst}</xdr:cNvPr>`
336
+ : `<xdr:cNvPr id="1" name="${escName}"/>`) +
337
+ `<xdr:cNvGraphicFramePr/>` +
338
+ `</xdr:nvGraphicFramePr>` +
339
+ `<xdr:xfrm>` +
340
+ `<a:off x="0" y="0"/>` +
341
+ `<a:ext cx="${extCx}" cy="${extCy}"/>` +
342
+ `</xdr:xfrm>` +
343
+ `<a:graphic>` +
344
+ (isChartEx
345
+ ? `<a:graphicData uri="http://schemas.microsoft.com/office/drawing/2014/chartex">` +
346
+ `<cx:chart xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="${escRId}"/>` +
347
+ `</a:graphicData>`
348
+ : `<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">` +
349
+ `<c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="${escRId}"/>` +
350
+ `</a:graphicData>`) +
351
+ `</a:graphic>` +
352
+ `</xdr:graphicFrame>`;
353
+ const fallbackShape = `<xdr:sp macro="" textlink="">` +
354
+ `<xdr:nvSpPr>` +
355
+ `<xdr:cNvPr id="0" name=""/>` +
356
+ `<xdr:cNvSpPr><a:spLocks noTextEdit="1"/></xdr:cNvSpPr>` +
357
+ `</xdr:nvSpPr>` +
358
+ `<xdr:spPr>` +
359
+ `<a:xfrm>` +
360
+ `<a:off x="0" y="0"/>` +
361
+ `<a:ext cx="${extCx}" cy="${extCy}"/>` +
362
+ `</a:xfrm>` +
363
+ `<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` +
364
+ `<a:solidFill><a:prstClr val="white"/></a:solidFill>` +
365
+ `<a:ln w="1"><a:solidFill><a:prstClr val="black"/></a:solidFill></a:ln>` +
366
+ `</xdr:spPr>` +
367
+ `<xdr:txBody>` +
368
+ `<a:bodyPr vertOverflow="clip" horzOverflow="clip"/>` +
369
+ `<a:lstStyle/>` +
370
+ `<a:p><a:r><a:rPr lang="en-US" sz="1100"/>` +
371
+ `<a:t>This chart isn&apos;t available in your version of Excel.\n\n` +
372
+ `Editing this shape or saving this workbook into a different file format will permanently break the chart.</a:t>` +
373
+ `</a:r></a:p>` +
374
+ `</xdr:txBody>` +
375
+ `</xdr:sp>`;
376
+ const anchorBody = isChartEx
377
+ ? `<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">` +
378
+ `<mc:Choice xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" Requires="cx1">` +
379
+ graphicFrame +
380
+ `</mc:Choice>` +
381
+ `<mc:Fallback>` +
382
+ fallbackShape +
383
+ `</mc:Fallback>` +
384
+ `</mc:AlternateContent>`
385
+ : graphicFrame;
386
+ return (`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
387
+ `<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">` +
388
+ `<xdr:absoluteAnchor>` +
389
+ `<xdr:pos x="0" y="0"/>` +
390
+ `<xdr:ext cx="${extCx}" cy="${extCy}"/>` +
391
+ anchorBody +
392
+ `<xdr:clientData/>` +
393
+ `</xdr:absoluteAnchor>` +
394
+ `</xdr:wsDr>`);
395
+ }
396
+ function extractLeadingComments(originalXml, openTagRegex) {
397
+ const m = openTagRegex.exec(originalXml);
398
+ if (!m) {
399
+ return "";
400
+ }
401
+ const before = originalXml.slice(0, m.index);
402
+ // Walk backwards collecting consecutive `<!--…-->` blocks (with
403
+ // optional whitespace between them and the open tag).
404
+ const comments = [];
405
+ let cursor = before.length;
406
+ while (cursor > 0) {
407
+ // Skip trailing whitespace
408
+ let head = cursor;
409
+ while (head > 0 && /\s/.test(before.charAt(head - 1))) {
410
+ head--;
411
+ }
412
+ // Look for `-->` ending right at `head`
413
+ if (head < 3 || before.slice(head - 3, head) !== "-->") {
414
+ break;
415
+ }
416
+ // Find the matching `<!--` start
417
+ const start = before.lastIndexOf("<!--", head - 3);
418
+ if (start < 0) {
419
+ break;
420
+ }
421
+ comments.unshift(before.slice(start, head));
422
+ cursor = start;
423
+ }
424
+ return comments.join("");
425
+ }
426
+ /**
427
+ * Render a chart part (classic) to bytes via `XmlWriter`, then splice
428
+ * preserved leading comments from the original raw XML in front of the
429
+ * `<c:chart>` open tag. If the original has no leading comments or no
430
+ * `rawData` is available, returns the unmodified rendered bytes.
431
+ */
432
+ function renderChartWithLeadingComments(entry, xform) {
433
+ const writer = new XmlWriter();
434
+ xform.render(writer, entry.model);
435
+ let xml = writer.toString();
436
+ if (entry.rawData) {
437
+ const originalXml = new TextDecoder().decode(entry.rawData);
438
+ const comments = extractLeadingComments(originalXml, /<c:chart(?:\s|>)/);
439
+ if (comments) {
440
+ xml = xml.replace(/<c:chart(\s|>)/, `${comments}<c:chart$1`);
441
+ }
442
+ }
443
+ return new TextEncoder().encode(xml);
444
+ }
445
+ /**
446
+ * Splice preserved leading comments from a ChartEx raw XML buffer into
447
+ * a freshly-rendered structural rebuild output.
448
+ */
449
+ function spliceChartExLeadingComments(renderedXml, originalRawXml) {
450
+ if (!originalRawXml) {
451
+ return renderedXml;
452
+ }
453
+ const comments = extractLeadingComments(originalRawXml, /<cx:chart(?:\s|>)/);
454
+ if (!comments) {
455
+ return renderedXml;
456
+ }
457
+ return renderedXml.replace(/<cx:chart(\s|>)/, `${comments}<cx:chart$1`);
458
+ }
459
+ function shouldPassthroughChartEntry(entry) {
460
+ if (!entry.rawData || entry.dirty) {
461
+ return false;
462
+ }
463
+ if (entry.modelSnapshot === undefined) {
464
+ return true;
465
+ }
466
+ return snapshotChartModel(entry.model) === entry.modelSnapshot;
467
+ }
468
+ function shouldPassthroughChartExEntry(entry) {
469
+ if (!entry.rawData || entry.dirty) {
470
+ return false;
471
+ }
472
+ if (entry.modelSnapshot === undefined) {
473
+ return true;
474
+ }
475
+ return snapshotChartModel(entry.model) === entry.modelSnapshot;
476
+ }
477
+ function stripChartExRawXml(model) {
478
+ return { ...model, rawXml: undefined };
479
+ }
480
+ function isStrictTemplateMode(options) {
481
+ return options?.templateMode === "strict" || options?.strictTemplateMode === true;
482
+ }
483
+ /**
484
+ * Decide whether `writeBuffer` should run the OOXML self-check after
485
+ * producing bytes. Resolves the `validate` option against the current
486
+ * environment:
487
+ *
488
+ * - Explicit `true` / `false` → honoured as-is.
489
+ * - `undefined` (default) → `true` in non-production Node.js
490
+ * when NOT running under vitest. We
491
+ * detect vitest via `process.env.VITEST`
492
+ * to avoid adding multi-second
493
+ * validation overhead to fixture
494
+ * `beforeAll` hooks that produce
495
+ * hundreds of workbooks (the chartEx
496
+ * preset corpus alone builds ~100
497
+ * fixtures per run — at ~450 ms each
498
+ * that is a 45 s penalty on every full
499
+ * suite execution). Vitest tests that
500
+ * need validation call
501
+ * `expectValidXlsx()` explicitly.
502
+ * `false` in production and in the
503
+ * browser where `process` is absent.
504
+ *
505
+ * Kept as a module-level helper so the resolution rule is testable in
506
+ * isolation and so subclasses can override by passing an explicit
507
+ * `validate` flag rather than re-implementing the default logic.
508
+ */
509
+ function shouldAutoValidate(explicit) {
510
+ if (explicit !== undefined) {
511
+ return explicit;
512
+ }
513
+ // In the browser `process` is undefined; skip the overhead there.
514
+ if (typeof process === "undefined" || !process.env) {
515
+ return false;
516
+ }
517
+ if (process.env.NODE_ENV === "production") {
518
+ return false;
519
+ }
520
+ // Vitest sets VITEST=true automatically in its worker processes.
521
+ // Skip the auto-check there; tests opt-in via `expectValidXlsx`.
522
+ if (process.env.VITEST === "true") {
523
+ return false;
524
+ }
525
+ return true;
526
+ }
527
+ /**
528
+ * Run `validateXlsxBuffer` on writer output and emit a consolidated
529
+ * `console.warn` for every detected problem. Never throws: a validator
530
+ * exception is degraded to a warning so writers that intentionally
531
+ * produce non-conformant xlsx (e.g. for negative-path tests) keep
532
+ * working. The message includes the actionable opt-out so downstream
533
+ * consumers know how to silence it without grepping docs.
534
+ */
535
+ async function runWriteBufferSelfCheck(bytes) {
536
+ try {
537
+ const report = await validateXlsxBuffer(bytes, { maxProblems: 20 });
538
+ if (report.ok) {
539
+ return;
540
+ }
541
+ const summary = report.problems
542
+ .map((p, i) => ` ${i + 1}. [${p.kind}] ${p.file ?? "<package>"}: ${p.message}`)
543
+ .join("\n");
544
+ // eslint-disable-next-line no-console
545
+ console.warn(`[excelts] writeBuffer() produced xlsx with ${report.problems.length} OOXML issue(s):\n` +
546
+ `${summary}\n` +
547
+ `Pass \`{ validate: false }\` to silence this self-check, or set NODE_ENV=production.`);
548
+ }
549
+ catch (err) {
550
+ // eslint-disable-next-line no-console
551
+ console.warn(`[excelts] writeBuffer() self-check threw unexpectedly and was skipped: ${String(err)}`);
552
+ }
553
+ }
554
+ function hasChartEntryChanged(entry) {
555
+ if (!entry.rawData) {
556
+ return false;
557
+ }
558
+ if (entry.dirty) {
559
+ return true;
560
+ }
561
+ if (entry.modelSnapshot === undefined) {
562
+ return false;
563
+ }
564
+ return snapshotChartModel(entry.model) !== entry.modelSnapshot;
565
+ }
566
+ function hasChartExEntryChanged(entry) {
567
+ if (!entry.rawData) {
568
+ return false;
569
+ }
570
+ if (entry.dirty) {
571
+ return true;
572
+ }
573
+ if (entry.modelSnapshot === undefined) {
574
+ return false;
575
+ }
576
+ return snapshotChartModel(entry.model) !== entry.modelSnapshot;
577
+ }
578
+ function shouldRequireChartRawPatch(entry, strictTemplateMode) {
579
+ return !!entry.requireRawPatch || (strictTemplateMode && hasChartEntryChanged(entry));
580
+ }
581
+ function shouldRequireChartExRawPatch(entry, strictTemplateMode) {
582
+ return !!entry.requireRawPatch || (strictTemplateMode && hasChartExEntryChanged(entry));
583
+ }
584
+ /**
585
+ * Assemble the error message thrown when a loaded chartEx part cannot be
586
+ * raw-patched but the caller required it (either `requireRawPatch` on the
587
+ * entry or `strictTemplateMode` at the writer). Surfaces any unknown XML
588
+ * elements the parser noticed so the author can decide whether to relax
589
+ * the requirement or adjust the mutation shape.
590
+ */
591
+ function buildChartExStrictFailureMessage(entryName, model) {
592
+ const base = `ChartEx ${entryName} requires raw XML patching ` +
593
+ `(requireRawPatch/strict template mode), but the mutation cannot be safely applied as a raw XML patch.`;
594
+ const unknown = model?.unknownElements;
595
+ return appendUnknownElementsSummary(base, unknown);
596
+ }
597
+ /**
598
+ * Classic-chart counterpart of {@link buildChartExStrictFailureMessage}.
599
+ * Pulls `unknownElements` off the {@link ChartModel} so the same
600
+ * "you are about to silently drop these vendor extensions" warning is
601
+ * surfaced when strict template mode refuses a re-render.
602
+ */
603
+ function buildChartStrictFailureMessage(entryName, model) {
604
+ const base = `Chart ${entryName} requires raw XML patching ` +
605
+ `(requireRawPatch/strict template mode), but the mutation cannot be safely applied as a raw XML patch.`;
606
+ const unknown = model?.unknownElements;
607
+ return appendUnknownElementsSummary(base, unknown);
608
+ }
609
+ function appendUnknownElementsSummary(base, unknown) {
610
+ if (!unknown || unknown.length === 0) {
611
+ return base;
612
+ }
613
+ // De-duplicate by path; real files often repeat the same extension element
614
+ // across multiple series/axes and noise doesn't help diagnosis.
615
+ const uniquePaths = Array.from(new Set(unknown.map(entry => entry.path))).slice(0, 8);
616
+ const extra = unknown.length > uniquePaths.length
617
+ ? ` (showing ${uniquePaths.length} of ${unknown.length})`
618
+ : "";
619
+ return (`${base} The loaded part contains unstructured XML at: ${uniquePaths.join(", ")}${extra}. ` +
620
+ `Rebuilding the part would discard these extensions; adjust the mutation to a ` +
621
+ `patch-friendly shape or relax strictTemplateMode.`);
622
+ }
623
+ function tryPatchChartExRawXml(entry, forceRawPatch = false) {
624
+ if (!entry.rawData ||
625
+ (!entry.preferRawPatch && !forceRawPatch) ||
626
+ !hasChartExEntryChanged(entry)) {
627
+ return undefined;
628
+ }
629
+ const patchPlan = getChartExRawPatchPlan(entry);
630
+ if (patchPlan === undefined) {
631
+ return undefined;
632
+ }
633
+ const raw = new TextDecoder().decode(entry.rawData);
634
+ const chartRange = findXmlBlock(raw, "cx:chartSpace");
635
+ if (!chartRange) {
636
+ return undefined;
637
+ }
638
+ const chartBlock = raw.slice(chartRange.start, chartRange.end);
639
+ const patchedChartBlock = patchRawChartExChartBlock(chartBlock, entry.model, patchPlan);
640
+ if (patchedChartBlock === undefined) {
641
+ return undefined;
642
+ }
643
+ const patched = raw.slice(0, chartRange.start) + patchedChartBlock + raw.slice(chartRange.end);
644
+ return patched !== raw ? new TextEncoder().encode(patched) : undefined;
645
+ }
646
+ function getChartExRawPatchPlan(entry) {
647
+ if (entry.modelSnapshot === undefined) {
648
+ return {
649
+ title: true,
650
+ data: true,
651
+ legend: true,
652
+ autoTitleDeleted: true,
653
+ chartSpaceSpPr: true,
654
+ plotAreaSpPr: true,
655
+ plotSurface: true,
656
+ series: true,
657
+ axes: true
658
+ };
659
+ }
660
+ let previous;
661
+ try {
662
+ previous = JSON.parse(entry.modelSnapshot);
663
+ }
664
+ catch {
665
+ return undefined;
666
+ }
667
+ const current = entry.model;
668
+ if (!sameJson(stripPatchableChartExFields(previous), stripPatchableChartExFields(current))) {
669
+ return undefined;
670
+ }
671
+ const prevChart = previous.chartSpace?.chart;
672
+ const curChart = current.chartSpace?.chart;
673
+ const series = buildChartExSeriesRawPatchPlan(previous, current);
674
+ const axes = buildChartExAxisRawPatchPlan(prevChart?.plotArea?.axis ?? [], curChart?.plotArea?.axis ?? []);
675
+ const plan = {
676
+ data: !sameJson(previous.chartSpace?.chartData, current.chartSpace?.chartData),
677
+ title: !sameJson(prevChart?.title, curChart?.title),
678
+ legend: !sameJson(prevChart?.legend, curChart?.legend),
679
+ autoTitleDeleted: !sameJson(prevChart?.autoTitleDeleted, curChart?.autoTitleDeleted),
680
+ // Chart-frame styling lives on `CT_ChartSpace/spPr` in Chart2014,
681
+ // not on `CT_Chart`. Diff the correct slot; the `ChartExChart.spPr`
682
+ // field has been removed from the type.
683
+ chartSpaceSpPr: !sameJson(previous.chartSpace?.spPr, current.chartSpace?.spPr),
684
+ plotAreaSpPr: !sameJson(prevChart?.plotArea?.spPr, curChart?.plotArea?.spPr),
685
+ plotSurface: !sameJson(prevChart?.plotArea?.plotAreaRegion?.plotSurface, curChart?.plotArea?.plotAreaRegion?.plotSurface),
686
+ series,
687
+ axes
688
+ };
689
+ return plan.data ||
690
+ plan.title ||
691
+ plan.legend ||
692
+ plan.autoTitleDeleted ||
693
+ plan.chartSpaceSpPr ||
694
+ plan.plotAreaSpPr ||
695
+ plan.plotSurface ||
696
+ hasRawPatchListChanges(plan.series) ||
697
+ hasRawPatchListChanges(plan.axes)
698
+ ? plan
699
+ : undefined;
700
+ }
701
+ function buildChartExSeriesRawPatchPlan(previous, current) {
702
+ const previousSeries = extractChartExSeries(previous);
703
+ const currentSeries = extractChartExSeries(current);
704
+ return currentSeries.map((series, index) => {
705
+ const prev = previousSeries[index] ?? {};
706
+ return {
707
+ hidden: !sameJson(prev.hidden, series.hidden),
708
+ ownerIdx: !sameJson(prev.ownerIdx, series.ownerIdx),
709
+ tx: !sameJson(prev.tx, series.tx),
710
+ dataRefs: !sameJson(prev.dataRefs, series.dataRefs),
711
+ layoutPr: !sameJson(prev.layoutPr, series.layoutPr),
712
+ axisId: !sameJson(prev.axisId, series.axisId),
713
+ dataLabels: !sameJson(prev.dataLabels, series.dataLabels),
714
+ spPr: !sameJson(prev.spPr, series.spPr),
715
+ dataPoints: !sameJson(prev.dataPt, series.dataPt)
716
+ };
717
+ });
718
+ }
719
+ function buildChartExAxisRawPatchPlan(previousAxes, currentAxes) {
720
+ const previousById = new Map(previousAxes.map(axis => [axis.axisId, axis]));
721
+ return currentAxes.map(axis => {
722
+ const prev = previousById.get(axis.axisId) ?? {};
723
+ return {
724
+ hidden: !sameJson(prev.hidden, axis.hidden),
725
+ majorTickMark: !sameJson(prev.majorTickMark, axis.majorTickMark),
726
+ minorTickMark: !sameJson(prev.minorTickMark, axis.minorTickMark),
727
+ numFmt: !sameJson(prev.numFmt, axis.numFmt),
728
+ title: !sameJson(prev.title, axis.title),
729
+ valScaling: !sameJson(prev.valScaling, axis.valScaling),
730
+ catScaling: !sameJson(prev.catScaling, axis.catScaling),
731
+ spPr: !sameJson(prev.spPr, axis.spPr),
732
+ txPr: !sameJson(prev.txPr, axis.txPr)
733
+ };
734
+ });
735
+ }
736
+ function stripPatchableChartExFields(model) {
737
+ const clone = JSON.parse(JSON.stringify(model));
738
+ clone.rawXml = undefined;
739
+ // Vendor / extension metadata the parser recorded but the raw patcher
740
+ // does not rewrite. Letting them differ in the diff keeps
741
+ // `getChartExRawPatchPlan` from giving up on fast-path patches for
742
+ // loaded templates that carry c14/c15/c16 extensions. The patcher
743
+ // never touches these bytes, so the raw XML already preserves them
744
+ // verbatim.
745
+ clone.unknownElements = undefined;
746
+ if (clone.chartSpace) {
747
+ clone.chartSpace.chartData = undefined;
748
+ clone.chartSpace.clrMapOvr = undefined;
749
+ clone.chartSpace.extLst = undefined;
750
+ }
751
+ if (clone.chartSpace?.chart) {
752
+ clone.chartSpace.chart.title = undefined;
753
+ clone.chartSpace.chart.legend = undefined;
754
+ clone.chartSpace.chart.autoTitleDeleted = undefined;
755
+ // NOTE: `chart.spPr` was previously cleared here, but the field
756
+ // has been removed from `ChartExChart` (see the migration in
757
+ // chart-ex-parser); the writer now emits chart-frame styling from
758
+ // `chartSpace.spPr` only.
759
+ if (clone.chartSpace.chart.plotArea) {
760
+ clone.chartSpace.chart.plotArea.spPr = undefined;
761
+ clone.chartSpace.chart.plotArea.axis = undefined;
762
+ if (clone.chartSpace.chart.plotArea.plotAreaRegion) {
763
+ clone.chartSpace.chart.plotArea.plotAreaRegion.layout = undefined;
764
+ clone.chartSpace.chart.plotArea.plotAreaRegion.plotSurface = undefined;
765
+ clone.chartSpace.chart.plotArea.plotAreaRegion.series = (clone.chartSpace.chart.plotArea.plotAreaRegion.series ?? []).map(stripPatchableChartExSeriesFields);
766
+ }
767
+ if (clone.chartSpace.chart.plotArea.series) {
768
+ clone.chartSpace.chart.plotArea.series = clone.chartSpace.chart.plotArea.series.map(stripPatchableChartExSeriesFields);
769
+ }
770
+ }
771
+ }
772
+ return clone;
773
+ }
774
+ function stripPatchableChartExSeriesFields(series) {
775
+ return {
776
+ ...series,
777
+ hidden: undefined,
778
+ ownerIdx: undefined,
779
+ tx: undefined,
780
+ spPr: undefined,
781
+ dataRefs: undefined,
782
+ layoutPr: undefined,
783
+ axisId: undefined,
784
+ dataLabels: undefined,
785
+ dataPt: undefined
786
+ };
787
+ }
788
+ function extractChartExSeries(model) {
789
+ const plotArea = model.chartSpace?.chart?.plotArea;
790
+ return plotArea?.plotAreaRegion?.series ?? plotArea?.series ?? [];
791
+ }
792
+ function patchRawChartExChartBlock(block, model, patchPlan) {
793
+ let patched = block;
794
+ const chart = model.chartSpace?.chart;
795
+ if (!chart) {
796
+ return undefined;
797
+ }
798
+ if (patchPlan.data) {
799
+ const dataRange = findXmlBlock(patched, "cx:chartData");
800
+ if (!dataRange) {
801
+ return undefined;
802
+ }
803
+ const dataXml = buildRawChartExDataXml(model.chartSpace?.chartData);
804
+ patched = patched.slice(0, dataRange.start) + dataXml + patched.slice(dataRange.end);
805
+ }
806
+ if (patchPlan.title) {
807
+ const titleText = chart.title?.text?.paragraphs?.[0]?.runs?.[0]?.text;
808
+ patched =
809
+ titleText !== undefined
810
+ ? replaceOrInsertBeforeGeneric(patched, "cx:title", buildRawChartExTitleXml(titleText), ["cx:autoTitleDeleted", "cx:plotArea", "cx:legend", "cx:spPr"], "cx:chart")
811
+ : removeXmlBlock(patched, "cx:title");
812
+ }
813
+ if (patchPlan.autoTitleDeleted) {
814
+ patched =
815
+ chart.autoTitleDeleted !== undefined
816
+ ? replaceOrInsertBeforeGeneric(patched, "cx:autoTitleDeleted", `<cx:autoTitleDeleted val="${chart.autoTitleDeleted ? "1" : "0"}"/>`, ["cx:plotArea", "cx:legend", "cx:spPr"], "cx:chart")
817
+ : removeXmlBlock(patched, "cx:autoTitleDeleted");
818
+ }
819
+ if (patchPlan.legend) {
820
+ patched =
821
+ chart.legend !== undefined
822
+ ? replaceOrInsertBeforeGeneric(patched, "cx:legend", buildRawChartExLegendXml(chart.legend), ["cx:spPr"], "cx:chart")
823
+ : removeXmlBlock(patched, "cx:legend");
824
+ }
825
+ if (patchPlan.chartSpaceSpPr) {
826
+ // Target `<cx:chartSpace>` (the root element) rather than
827
+ // `<cx:chart>`. Chart-frame styling belongs on the chartSpace
828
+ // parent per Chart2014; previous versions of this patcher
829
+ // incorrectly wrote it inside `<cx:chart>`, producing output
830
+ // strict validators reject. The siblings list is CT_ChartSpace's
831
+ // child order after `cx:chart`: `cx:spPr, cx:txPr, cx:externalData,
832
+ // cx:printSettings, cx:extLst`.
833
+ patched = patchGenericChild(patched, "cx:spPr", buildRawShapePropertiesXml(model.chartSpace?.spPr, "cx"), ["cx:txPr", "cx:externalData", "cx:printSettings", "cx:extLst"], "cx:chartSpace");
834
+ }
835
+ if (patchPlan.plotAreaSpPr || patchPlan.plotSurface) {
836
+ const plotRange = findXmlBlock(patched, "cx:plotArea");
837
+ if (!plotRange) {
838
+ return undefined;
839
+ }
840
+ let plotBlock = patched.slice(plotRange.start, plotRange.end);
841
+ const plotArea = chart.plotArea;
842
+ if (patchPlan.plotAreaSpPr) {
843
+ // `CT_PlotArea` sequence: `plotAreaRegion?` → `axis*` → `spPr?` →
844
+ // `extLst?`. `spPr` is the next-to-last child, so its only
845
+ // follower is `extLst`.
846
+ plotBlock = patchGenericChild(plotBlock, "cx:spPr", buildRawShapePropertiesXml(plotArea?.spPr, "cx"), ["cx:extLst"], "cx:plotArea");
847
+ }
848
+ if (patchPlan.plotSurface) {
849
+ // `CT_PlotAreaRegion` (Chart2014): `plotSurface?` → `series*` →
850
+ // `extLst?`. The `spPr` is a child of `<cx:plotSurface>`, NOT a
851
+ // direct child of `<cx:plotAreaRegion>`. Previously the raw
852
+ // patcher wrote a bare `<cx:spPr>` under `<cx:plotAreaRegion>`
853
+ // (schema violation) and also had a separate
854
+ // `plotAreaRegionLayout` patch that emitted `<cx:layout>`
855
+ // there (also invalid — layout only lives on `<cx:plotArea>` /
856
+ // `<cx:title>` via the manualLayout extension).
857
+ //
858
+ // The correct form is:
859
+ // <cx:plotAreaRegion>
860
+ // <cx:plotSurface>
861
+ // <cx:spPr>…</cx:spPr>
862
+ // </cx:plotSurface>
863
+ // <cx:series/>
864
+ // …
865
+ // </cx:plotAreaRegion>
866
+ const regionRange = findXmlBlock(plotBlock, "cx:plotAreaRegion");
867
+ if (!regionRange) {
868
+ return undefined;
869
+ }
870
+ let regionBlock = plotBlock.slice(regionRange.start, regionRange.end);
871
+ const region = plotArea?.plotAreaRegion;
872
+ const surfaceSpPrXml = buildRawShapePropertiesXml(region?.plotSurface, "cx");
873
+ const plotSurfaceXml = surfaceSpPrXml
874
+ ? `<cx:plotSurface>${surfaceSpPrXml}</cx:plotSurface>`
875
+ : undefined;
876
+ regionBlock = patchGenericChild(regionBlock, "cx:plotSurface", plotSurfaceXml, ["cx:series", "cx:extLst"], "cx:plotAreaRegion");
877
+ plotBlock =
878
+ plotBlock.slice(0, regionRange.start) + regionBlock + plotBlock.slice(regionRange.end);
879
+ }
880
+ patched = patched.slice(0, plotRange.start) + plotBlock + patched.slice(plotRange.end);
881
+ }
882
+ if (hasRawPatchListChanges(patchPlan.series)) {
883
+ const next = patchRawChartExSeries(patched, chart, patchPlan);
884
+ if (next === undefined) {
885
+ return undefined;
886
+ }
887
+ patched = next;
888
+ }
889
+ if (hasRawPatchListChanges(patchPlan.axes)) {
890
+ const next = patchRawChartExAxes(patched, chart, patchPlan.axes);
891
+ if (next === undefined) {
892
+ return undefined;
893
+ }
894
+ patched = next;
895
+ }
896
+ return patched;
897
+ }
898
+ function tryPatchChartRawXml(entry, forceRawPatch = false) {
899
+ if (!entry.rawData || (!entry.preferRawPatch && !forceRawPatch) || !hasChartEntryChanged(entry)) {
900
+ return undefined;
901
+ }
902
+ const patchPlan = getChartRawPatchPlan(entry);
903
+ if (patchPlan === undefined) {
904
+ return undefined;
905
+ }
906
+ const raw = new TextDecoder().decode(entry.rawData);
907
+ let patched = raw;
908
+ if (patchPlan.title) {
909
+ const titleText = entry.model.chart?.title?.text?.paragraphs?.[0]?.runs?.[0]?.text;
910
+ const hasTitle = /<c:title>[\s\S]*?<\/c:title>/.test(patched);
911
+ if (titleText !== undefined && hasTitle) {
912
+ patched = patched.replace(/<c:title>[\s\S]*?<\/c:title>/, buildRawChartTitleXml(titleText));
913
+ }
914
+ else if (titleText === undefined && hasTitle) {
915
+ patched = patched.replace(/<c:title>[\s\S]*?<\/c:title>/, "");
916
+ }
917
+ }
918
+ if (patchPlan.legend) {
919
+ const legend = entry.model.chart?.legend;
920
+ if (legend === undefined) {
921
+ patched = patched.replace(/<c:legend>[\s\S]*?<\/c:legend>/, "");
922
+ }
923
+ else if (/<c:legend>[\s\S]*?<\/c:legend>/.test(patched)) {
924
+ patched = patched.replace(/<c:legend>[\s\S]*?<\/c:legend>/, buildRawChartLegendXml(legend.legendPos ?? "b"));
925
+ }
926
+ }
927
+ if (hasRawPatchListChanges(patchPlan.series)) {
928
+ const next = patchRawSeries(patched, entry.model, patchPlan.series);
929
+ if (next === undefined) {
930
+ return undefined;
931
+ }
932
+ patched = next;
933
+ }
934
+ if (patchPlan.groupDataLabels) {
935
+ const next = patchRawChartGroupDataLabels(patched, entry.model);
936
+ if (next === undefined) {
937
+ return undefined;
938
+ }
939
+ patched = next;
940
+ }
941
+ if (patchPlan.groupSimpleFields) {
942
+ const next = patchRawChartGroupSimpleFields(patched, entry.model);
943
+ if (next === undefined) {
944
+ return undefined;
945
+ }
946
+ patched = next;
947
+ }
948
+ if (patchPlan.plotAreaLayout) {
949
+ const next = patchRawPlotAreaLayout(patched, entry.model);
950
+ if (next === undefined) {
951
+ return undefined;
952
+ }
953
+ patched = next;
954
+ }
955
+ if (hasRawPatchListChanges(patchPlan.axes)) {
956
+ const next = patchRawAxes(patched, entry.model, patchPlan.axes);
957
+ if (next === undefined) {
958
+ return undefined;
959
+ }
960
+ patched = next;
961
+ }
962
+ return patched !== raw ? new TextEncoder().encode(patched) : undefined;
963
+ }
964
+ function getChartRawPatchPlan(entry) {
965
+ if (entry.modelSnapshot === undefined) {
966
+ return {
967
+ title: true,
968
+ legend: true,
969
+ series: true,
970
+ axes: true,
971
+ groupDataLabels: true,
972
+ groupSimpleFields: true,
973
+ plotAreaLayout: true
974
+ };
975
+ }
976
+ let previous;
977
+ try {
978
+ previous = JSON.parse(entry.modelSnapshot);
979
+ }
980
+ catch {
981
+ return undefined;
982
+ }
983
+ const current = entry.model;
984
+ const prevChart = previous.chart;
985
+ const curChart = current.chart;
986
+ const plan = {
987
+ title: false,
988
+ legend: false,
989
+ series: buildChartSeriesRawPatchPlan(previous, current),
990
+ axes: buildChartAxisRawPatchPlan(prevChart?.plotArea?.axes ?? [], curChart?.plotArea?.axes ?? []),
991
+ groupDataLabels: false,
992
+ groupSimpleFields: false,
993
+ plotAreaLayout: false
994
+ };
995
+ const curWithoutPatchable = stripPatchableChartFields(current);
996
+ const prevWithoutPatchable = stripPatchableChartFields(previous);
997
+ if (!sameJson(curWithoutPatchable, prevWithoutPatchable)) {
998
+ return undefined;
999
+ }
1000
+ plan.title = plan.title || !sameJson(prevChart?.title, curChart?.title);
1001
+ plan.legend = plan.legend || !sameJson(prevChart?.legend, curChart?.legend);
1002
+ plan.groupDataLabels = !sameJson(extractPatchableGroupDataLabels(previous), extractPatchableGroupDataLabels(current));
1003
+ plan.groupSimpleFields = !sameJson(extractSimpleGroupFields(previous), extractSimpleGroupFields(current));
1004
+ plan.plotAreaLayout = !sameJson(prevChart?.plotArea?.layout, curChart?.plotArea?.layout);
1005
+ return plan.title ||
1006
+ plan.legend ||
1007
+ hasRawPatchListChanges(plan.series) ||
1008
+ hasRawPatchListChanges(plan.axes) ||
1009
+ plan.groupDataLabels ||
1010
+ plan.groupSimpleFields ||
1011
+ plan.plotAreaLayout
1012
+ ? plan
1013
+ : undefined;
1014
+ }
1015
+ function stripPatchableChartFields(model) {
1016
+ const clone = JSON.parse(JSON.stringify(model));
1017
+ // Top-level fields that tryPatchChartRawXml does not rewrite. Allowing
1018
+ // them to differ between `previous` and `current` means a caller can
1019
+ // load a template that carries c14/c15/c16 extension XML, edit a
1020
+ // title or legend, and still take the fast raw-patch path — without
1021
+ // this the extLst JSON shape shifts (e.g. empty string `""` vs
1022
+ // `undefined` after a round-trip) and the plan gets rejected.
1023
+ //
1024
+ // We deliberately do NOT strip `pivotOptions`: it is structurally
1025
+ // parsed, and the raw patcher has no branch to replay a mutation
1026
+ // into the XML. Keeping it out of the whitelist forces a rebuild so
1027
+ // the user's change is honoured.
1028
+ clone.extLst = undefined;
1029
+ clone.unknownElements = undefined;
1030
+ clone.extraNamespaces = undefined;
1031
+ clone.alternateContentStyle = undefined;
1032
+ clone.clrMapOvr = undefined;
1033
+ clone.protection = undefined;
1034
+ if (clone.chart) {
1035
+ clone.chart.title = undefined;
1036
+ clone.chart.legend = undefined;
1037
+ clone.chart.extLst = undefined;
1038
+ if (clone.chart.plotArea) {
1039
+ clone.chart.plotArea.axes = (clone.chart.plotArea.axes ?? []).map(stripPatchableAxisFields);
1040
+ clone.chart.plotArea.layout = undefined;
1041
+ clone.chart.plotArea.extLst = undefined;
1042
+ for (const group of clone.chart.plotArea.chartTypes ?? []) {
1043
+ group.dataLabels = undefined;
1044
+ group.extLst = undefined;
1045
+ // Simple leaf fields the `patchRawChartGroupSimpleFields`
1046
+ // branch rewrites in place (see `SIMPLE_GROUP_FIELD_TAGS`).
1047
+ // Stripping them from the baseline diff is what makes the
1048
+ // "edit `overlap` then write" path take the fast raw-patch
1049
+ // route instead of a full structural rebuild. Every field
1050
+ // listed here must have a matching entry in
1051
+ // `SIMPLE_GROUP_FIELD_TAGS` — the two are kept symmetric.
1052
+ for (const field of SIMPLE_GROUP_FIELD_NAMES) {
1053
+ group[field] = undefined;
1054
+ }
1055
+ group.series = (group.series ?? []).map((series) => ({
1056
+ ...series,
1057
+ tx: undefined,
1058
+ spPr: undefined,
1059
+ marker: undefined,
1060
+ dataPoints: undefined,
1061
+ trendlines: undefined,
1062
+ errorBars: undefined,
1063
+ cat: undefined,
1064
+ val: undefined,
1065
+ xVal: undefined,
1066
+ yVal: undefined,
1067
+ bubbleSize: undefined,
1068
+ dataLabels: undefined,
1069
+ extLst: undefined
1070
+ }));
1071
+ }
1072
+ }
1073
+ }
1074
+ return clone;
1075
+ }
1076
+ function stripPatchableAxisFields(axis) {
1077
+ return {
1078
+ ...axis,
1079
+ scaling: undefined,
1080
+ delete: undefined,
1081
+ majorGridlines: undefined,
1082
+ minorGridlines: undefined,
1083
+ title: undefined,
1084
+ numFmt: undefined,
1085
+ majorTickMark: undefined,
1086
+ minorTickMark: undefined,
1087
+ tickLblPos: undefined,
1088
+ spPr: undefined,
1089
+ txPr: undefined,
1090
+ crosses: undefined,
1091
+ crossesAt: undefined,
1092
+ auto: undefined,
1093
+ lblAlgn: undefined,
1094
+ lblOffset: undefined,
1095
+ tickLblSkip: undefined,
1096
+ tickMarkSkip: undefined,
1097
+ noMultiLvlLbl: undefined,
1098
+ crossBetween: undefined,
1099
+ majorUnit: undefined,
1100
+ minorUnit: undefined,
1101
+ baseTimeUnit: undefined,
1102
+ majorTimeUnit: undefined,
1103
+ minorTimeUnit: undefined,
1104
+ // `c:extLst` on an axis is always raw XML passthrough in the
1105
+ // structural parser; freezing it out of the diff lets template
1106
+ // edits (scaling / gridlines / title) take the fast raw-patch
1107
+ // path when the template happens to carry c15:axisTitleExtLst or
1108
+ // similar vendor ext markers.
1109
+ extLst: undefined
1110
+ };
1111
+ }
1112
+ function extractPatchableGroupDataLabels(model) {
1113
+ return (model.chart?.plotArea?.chartTypes ?? []).map((group) => group.dataLabels);
1114
+ }
1115
+ function buildChartSeriesRawPatchPlan(previous, current) {
1116
+ const previousSeries = flattenChartSeries(previous);
1117
+ return flattenChartSeries(current).map((series, index) => {
1118
+ const prev = previousSeries[index] ?? {};
1119
+ return {
1120
+ tx: !sameJson(prev.tx, series.tx),
1121
+ spPr: !sameJson(prev.spPr, series.spPr),
1122
+ marker: !sameJson(prev.marker, series.marker),
1123
+ dataPoints: !sameJson(prev.dataPoints, series.dataPoints),
1124
+ trendlines: !sameJson(prev.trendlines, series.trendlines),
1125
+ errorBars: !sameJson(prev.errorBars, series.errorBars),
1126
+ cat: !sameJson(prev.cat, series.cat),
1127
+ val: !sameJson(prev.val, series.val),
1128
+ xVal: !sameJson(prev.xVal, series.xVal),
1129
+ yVal: !sameJson(prev.yVal, series.yVal),
1130
+ bubbleSize: !sameJson(prev.bubbleSize, series.bubbleSize),
1131
+ dataLabels: !sameJson(prev.dataLabels, series.dataLabels)
1132
+ };
1133
+ });
1134
+ }
1135
+ function buildChartAxisRawPatchPlan(previousAxes, currentAxes) {
1136
+ const previousById = new Map(previousAxes.map(axis => [axis.axId, axis]));
1137
+ return currentAxes.map(axis => {
1138
+ const prev = previousById.get(axis.axId) ?? {};
1139
+ return {
1140
+ scaling: !sameJson(prev.scaling, axis.scaling),
1141
+ delete: !sameJson(prev.delete, axis.delete),
1142
+ title: !sameJson(prev.title, axis.title),
1143
+ numFmt: !sameJson(prev.numFmt, axis.numFmt),
1144
+ majorGridlines: !sameJson(prev.majorGridlines, axis.majorGridlines),
1145
+ minorGridlines: !sameJson(prev.minorGridlines, axis.minorGridlines),
1146
+ majorTickMark: !sameJson(prev.majorTickMark, axis.majorTickMark),
1147
+ minorTickMark: !sameJson(prev.minorTickMark, axis.minorTickMark),
1148
+ tickLblPos: !sameJson(prev.tickLblPos, axis.tickLblPos),
1149
+ spPr: !sameJson(prev.spPr, axis.spPr),
1150
+ txPr: !sameJson(prev.txPr, axis.txPr),
1151
+ crosses: !sameJson(prev.crosses, axis.crosses),
1152
+ crossesAt: !sameJson(prev.crossesAt, axis.crossesAt),
1153
+ auto: !sameJson(prev.auto, axis.auto),
1154
+ lblAlgn: !sameJson(prev.lblAlgn, axis.lblAlgn),
1155
+ lblOffset: !sameJson(prev.lblOffset, axis.lblOffset),
1156
+ tickLblSkip: !sameJson(prev.tickLblSkip, axis.tickLblSkip),
1157
+ tickMarkSkip: !sameJson(prev.tickMarkSkip, axis.tickMarkSkip),
1158
+ noMultiLvlLbl: !sameJson(prev.noMultiLvlLbl, axis.noMultiLvlLbl),
1159
+ crossBetween: !sameJson(prev.crossBetween, axis.crossBetween),
1160
+ majorUnit: !sameJson(prev.majorUnit, axis.majorUnit),
1161
+ minorUnit: !sameJson(prev.minorUnit, axis.minorUnit),
1162
+ baseTimeUnit: !sameJson(prev.baseTimeUnit, axis.baseTimeUnit),
1163
+ majorTimeUnit: !sameJson(prev.majorTimeUnit, axis.majorTimeUnit),
1164
+ minorTimeUnit: !sameJson(prev.minorTimeUnit, axis.minorTimeUnit)
1165
+ };
1166
+ });
1167
+ }
1168
+ function flattenChartSeries(model) {
1169
+ return (model.chart?.plotArea?.chartTypes ?? []).flatMap((group) => (group.series ?? []).map((series) => ({
1170
+ tx: series.tx,
1171
+ spPr: series.spPr,
1172
+ marker: series.marker,
1173
+ dataPoints: series.dataPoints,
1174
+ trendlines: series.trendlines,
1175
+ errorBars: series.errorBars,
1176
+ cat: series.cat,
1177
+ val: series.val,
1178
+ xVal: series.xVal,
1179
+ yVal: series.yVal,
1180
+ bubbleSize: series.bubbleSize,
1181
+ dataLabels: series.dataLabels
1182
+ })));
1183
+ }
1184
+ function sameJson(left, right) {
1185
+ return JSON.stringify(left) === JSON.stringify(right);
1186
+ }
1187
+ function hasRawPatchListChanges(plan) {
1188
+ return (plan === true || (Array.isArray(plan) && plan.some(item => Object.values(item).some(Boolean))));
1189
+ }
1190
+ function getRawPatchListItem(plan, index) {
1191
+ return plan === true ? true : Array.isArray(plan) ? (plan[index] ?? false) : false;
1192
+ }
1193
+ function rawPatchFlag(plan, key) {
1194
+ if (plan === true) {
1195
+ return true;
1196
+ }
1197
+ if (plan === false) {
1198
+ return false;
1199
+ }
1200
+ return Boolean(plan[key]);
1201
+ }
1202
+ function buildRawChartTitleXml(text) {
1203
+ // Full text escape (strips C0 control characters beyond `\t\n\r`,
1204
+ // encodes the five reserved entities) so injected titles can't break
1205
+ // out of the `<a:t>` element. Matches the `escapeXml` helper used
1206
+ // elsewhere in this module.
1207
+ const escaped = escapeXml(text);
1208
+ return `<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${escaped}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val="0"/></c:title>`;
1209
+ }
1210
+ function buildRawChartLegendXml(pos) {
1211
+ // Escape the attribute value — `pos` is typed as `LegendPosition`
1212
+ // (a 5-member enum) but the raw-patch path can't enforce that
1213
+ // statically, so a malicious or buggy caller could inject XML via
1214
+ // the attribute. Narrow to the enum set so truly unexpected values
1215
+ // fall back to the schema default `"b"` instead of being echoed
1216
+ // through verbatim.
1217
+ const safe = pos === "b" || pos === "l" || pos === "r" || pos === "t" || pos === "tr" ? pos : "b";
1218
+ return `<c:legend><c:legendPos val="${safe}"/><c:overlay val="0"/></c:legend>`;
1219
+ }
1220
+ function buildRawChartExTitleXml(text) {
1221
+ return `<cx:title><cx:tx><cx:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${escapeXml(text)}</a:t></a:r></a:p></cx:rich></cx:tx><cx:overlay val="0"/></cx:title>`;
1222
+ }
1223
+ function buildRawChartExLegendXml(legend) {
1224
+ // Delegate to the structured ChartEx writer so the raw-patch path
1225
+ // produces a byte-identical serialisation. Previously this function
1226
+ // hand-rolled a self-closing `<cx:legend pos="…" overlay="…"/>`,
1227
+ // silently dropping `align`, `legendEntry*`, `spPr`, `txPr`, and
1228
+ // `extLst` on every styled-legend round-trip. Sharing the writer
1229
+ // guarantees parity with the non-raw path.
1230
+ // Indentation differs from the structured writer's formatted output —
1231
+ // the raw patcher inserts into an inline stream, so strip the
1232
+ // leading indent that `renderChartExLegendXml` prefixes each line
1233
+ // with. The result is semantically identical; just flattened.
1234
+ return getChartSupport()
1235
+ .renderChartExLegendXml(legend)
1236
+ .split("\n")
1237
+ .map(line => line.replace(/^\s*/, ""))
1238
+ .join("");
1239
+ }
1240
+ function buildRawChartExDataXml(chartData) {
1241
+ const parts = ["<cx:chartData>"];
1242
+ // NOTE: `cx:externalData` is a child of `cx:chartSpace` per
1243
+ // Chart2014's `CT_ChartSpace`, NOT of `cx:chartData`. The raw
1244
+ // patcher used to emit it here — that produced a document Office
1245
+ // rejected in strict mode. The structured writer and raw-patch
1246
+ // paths now both emit externalData at the chartSpace level; any
1247
+ // legacy `chartData.externalData` is migrated to
1248
+ // `chartSpace.externalData` at parse time so the deprecated slot
1249
+ // can be ignored here without data loss.
1250
+ for (const entry of chartData?.data ?? []) {
1251
+ parts.push(`<cx:data id="${entry.id}">`);
1252
+ if (entry.strDim) {
1253
+ parts.push(buildRawChartExStringDimensionXml(entry.strDim));
1254
+ }
1255
+ if (entry.numDim) {
1256
+ parts.push(buildRawChartExNumericDimensionXml(entry.numDim));
1257
+ }
1258
+ parts.push("</cx:data>");
1259
+ }
1260
+ parts.push("</cx:chartData>");
1261
+ return parts.join("");
1262
+ }
1263
+ function buildRawChartExStringDimensionXml(dim) {
1264
+ const parts = [`<cx:strDim type="${escapeAttr(dim.type)}">`];
1265
+ if (dim.formula) {
1266
+ parts.push(`<cx:f>${escapeXml(dim.formula)}</cx:f>`);
1267
+ }
1268
+ for (const level of dim.levels ?? []) {
1269
+ const ptCount = level.ptCount ?? level.points?.length ?? 0;
1270
+ if (!level.points?.length) {
1271
+ parts.push(`<cx:lvl ptCount="${ptCount}"/>`);
1272
+ }
1273
+ else {
1274
+ parts.push(`<cx:lvl ptCount="${ptCount}">`);
1275
+ for (const point of level.points) {
1276
+ parts.push(`<cx:pt idx="${point.index}">${escapeXml(String(point.value))}</cx:pt>`);
1277
+ }
1278
+ parts.push("</cx:lvl>");
1279
+ }
1280
+ }
1281
+ parts.push("</cx:strDim>");
1282
+ return parts.join("");
1283
+ }
1284
+ function buildRawChartExNumericDimensionXml(dim) {
1285
+ const parts = [`<cx:numDim type="${escapeAttr(dim.type)}">`];
1286
+ if (dim.formula) {
1287
+ parts.push(`<cx:f>${escapeXml(dim.formula)}</cx:f>`);
1288
+ }
1289
+ for (const level of dim.levels ?? []) {
1290
+ const ptCount = level.ptCount ?? level.points?.length ?? 0;
1291
+ const fmt = level.formatCode ? ` formatCode="${escapeAttr(level.formatCode)}"` : "";
1292
+ if (!level.points?.length) {
1293
+ parts.push(`<cx:lvl ptCount="${ptCount}"${fmt}/>`);
1294
+ }
1295
+ else {
1296
+ parts.push(`<cx:lvl ptCount="${ptCount}"${fmt}>`);
1297
+ for (const point of level.points) {
1298
+ parts.push(`<cx:pt idx="${point.index}">${escapeXml(String(point.value))}</cx:pt>`);
1299
+ }
1300
+ parts.push("</cx:lvl>");
1301
+ }
1302
+ }
1303
+ parts.push("</cx:numDim>");
1304
+ return parts.join("");
1305
+ }
1306
+ function patchRawSeries(raw, model, patchPlan) {
1307
+ // Track the owning chart-type group for each series so doughnut
1308
+ // series can suppress `c:dLblPos` when writing `<c:dLbls>` — Excel
1309
+ // rejects that element on doughnut charts (see
1310
+ // `_renderDoughnutChart` in `chart-space-xform.ts`).
1311
+ const seriesEntries = [];
1312
+ for (const group of model.chart?.plotArea?.chartTypes ?? []) {
1313
+ for (const series of group.series ?? []) {
1314
+ seriesEntries.push({ series, chartType: group.type });
1315
+ }
1316
+ }
1317
+ let index = 0;
1318
+ return replaceXmlBlocks(raw, "c:ser", block => {
1319
+ const entry = seriesEntries[index++];
1320
+ const seriesPlan = getRawPatchListItem(patchPlan, index - 1);
1321
+ return entry && seriesPlan
1322
+ ? patchRawSeriesBlock(block, entry.series, seriesPlan, entry.chartType)
1323
+ : block;
1324
+ });
1325
+ }
1326
+ function patchRawSeriesBlock(block, series, patchPlan, chartType) {
1327
+ let patched = block;
1328
+ if (rawPatchFlag(patchPlan, "tx") && series.tx) {
1329
+ const txXml = buildRawSeriesTxXml(series.tx);
1330
+ patched = replaceOrInsertBefore(patched, "c:tx", txXml, [
1331
+ "c:spPr",
1332
+ "c:cat",
1333
+ "c:xVal",
1334
+ "c:val",
1335
+ "c:yVal",
1336
+ "c:bubbleSize"
1337
+ ]);
1338
+ }
1339
+ if (rawPatchFlag(patchPlan, "spPr")) {
1340
+ patched = patchGenericChild(patched, "c:spPr", buildRawShapePropertiesXml(series.spPr, "c"), [
1341
+ "c:marker",
1342
+ "c:invertIfNegative",
1343
+ "c:pictureOptions",
1344
+ "c:dPt",
1345
+ "c:dLbls",
1346
+ "c:trendline",
1347
+ "c:errBars",
1348
+ "c:cat",
1349
+ "c:xVal",
1350
+ "c:val",
1351
+ "c:yVal",
1352
+ "c:bubbleSize",
1353
+ "c:smooth",
1354
+ "c:shape",
1355
+ "c:extLst"
1356
+ ], "c:ser");
1357
+ }
1358
+ if (rawPatchFlag(patchPlan, "marker")) {
1359
+ patched = patchGenericChild(patched, "c:marker", buildRawMarkerXml(series.marker), [
1360
+ "c:dPt",
1361
+ "c:dLbls",
1362
+ "c:trendline",
1363
+ "c:errBars",
1364
+ "c:cat",
1365
+ "c:xVal",
1366
+ "c:val",
1367
+ "c:yVal",
1368
+ "c:bubbleSize",
1369
+ "c:smooth",
1370
+ "c:extLst"
1371
+ ], "c:ser");
1372
+ }
1373
+ for (const [tag, source] of [
1374
+ ["c:cat", series.cat],
1375
+ ["c:val", series.val],
1376
+ ["c:xVal", series.xVal],
1377
+ ["c:yVal", series.yVal],
1378
+ ["c:bubbleSize", series.bubbleSize]
1379
+ ]) {
1380
+ if (rawPatchFlag(patchPlan, chartSeriesPatchKeyForDataTag(tag))) {
1381
+ if (!source) {
1382
+ patched = removeXmlBlock(patched, tag);
1383
+ continue;
1384
+ }
1385
+ const dataXml = buildRawDataSourceXml(tag, source);
1386
+ if (!dataXml) {
1387
+ return undefined;
1388
+ }
1389
+ patched = replaceOrInsertBefore(patched, tag, dataXml, ["c:smooth", "c:shape", "c:extLst"]);
1390
+ }
1391
+ }
1392
+ if (rawPatchFlag(patchPlan, "dataPoints")) {
1393
+ const dataPointsXml = buildRawDataPointsXml(series.dataPoints);
1394
+ patched = patchRepeatingChildren(patched, "c:dPt", dataPointsXml, [
1395
+ "c:dLbls",
1396
+ "c:trendline",
1397
+ "c:errBars",
1398
+ "c:cat",
1399
+ "c:xVal",
1400
+ "c:val",
1401
+ "c:yVal",
1402
+ "c:bubbleSize",
1403
+ "c:smooth",
1404
+ "c:shape",
1405
+ "c:extLst"
1406
+ ], "c:ser");
1407
+ }
1408
+ if (rawPatchFlag(patchPlan, "trendlines")) {
1409
+ const trendlinesXml = buildRawTrendlinesXml(series.trendlines);
1410
+ patched = patchRepeatingChildren(patched, "c:trendline", trendlinesXml, [
1411
+ "c:errBars",
1412
+ "c:cat",
1413
+ "c:xVal",
1414
+ "c:val",
1415
+ "c:yVal",
1416
+ "c:bubbleSize",
1417
+ "c:smooth",
1418
+ "c:shape",
1419
+ "c:extLst"
1420
+ ], "c:ser");
1421
+ }
1422
+ if (rawPatchFlag(patchPlan, "errorBars")) {
1423
+ const errorBarsXml = buildRawErrorBarsXml(series.errorBars);
1424
+ patched = patchRepeatingChildren(patched, "c:errBars", errorBarsXml, ["c:cat", "c:xVal", "c:val", "c:yVal", "c:bubbleSize", "c:smooth", "c:shape", "c:extLst"], "c:ser");
1425
+ }
1426
+ if (rawPatchFlag(patchPlan, "dataLabels")) {
1427
+ if (series.dataLabels) {
1428
+ patched = replaceOrInsertBefore(patched, "c:dLbls", buildRawDataLabelsXml(series.dataLabels, { suppressDLblPos: chartType === "doughnut" }), [
1429
+ "c:trendline",
1430
+ "c:errBars",
1431
+ "c:cat",
1432
+ "c:xVal",
1433
+ "c:val",
1434
+ "c:yVal",
1435
+ "c:bubbleSize",
1436
+ "c:smooth",
1437
+ "c:shape",
1438
+ "c:extLst"
1439
+ ]);
1440
+ }
1441
+ else {
1442
+ patched = patched.replace(/<c:dLbls>[\s\S]*?<\/c:dLbls>/, "");
1443
+ }
1444
+ }
1445
+ return patched;
1446
+ }
1447
+ function chartSeriesPatchKeyForDataTag(tag) {
1448
+ switch (tag) {
1449
+ case "c:cat":
1450
+ return "cat";
1451
+ case "c:val":
1452
+ return "val";
1453
+ case "c:xVal":
1454
+ return "xVal";
1455
+ case "c:yVal":
1456
+ return "yVal";
1457
+ default:
1458
+ return "bubbleSize";
1459
+ }
1460
+ }
1461
+ /**
1462
+ * Ordered child tag list for each `ChartTypeGroup` block in a classic
1463
+ * chartN.xml, used by `replaceOrRemoveSimpleGroupField` to place a new
1464
+ * leaf in the correct schema position. The ordering mirrors the
1465
+ * `_renderXxxChart` functions in `chart-space-xform.ts` and is the
1466
+ * "schema order" ECMA-376 requires — inserting `c:gapWidth` before
1467
+ * `c:barDir` would produce XML Excel refuses to open.
1468
+ *
1469
+ * Tags not listed (e.g. `c:dLbls`, `c:ser`, `c:extLst`) are inserted
1470
+ * by existing dedicated patchers. Anything in this map is a single
1471
+ * `<c:… val="…"/>` leaf with no child elements.
1472
+ */
1473
+ const CLASSIC_GROUP_CHILD_ORDER = [
1474
+ "c:barDir",
1475
+ "c:grouping",
1476
+ "c:varyColors",
1477
+ "c:ofPieType",
1478
+ "c:radarStyle",
1479
+ "c:scatterStyle",
1480
+ "c:wireframe",
1481
+ "c:ser",
1482
+ "c:dLbls",
1483
+ "c:marker",
1484
+ "c:smooth",
1485
+ "c:dropLines",
1486
+ "c:hiLowLines",
1487
+ "c:upDownBars",
1488
+ "c:bubbleScale",
1489
+ "c:showNegBubbles",
1490
+ "c:sizeRepresents",
1491
+ "c:gapWidth",
1492
+ "c:overlap",
1493
+ "c:serLines",
1494
+ "c:shape",
1495
+ "c:firstSliceAng",
1496
+ "c:holeSize",
1497
+ "c:gapDepth",
1498
+ "c:splitType",
1499
+ "c:splitPos",
1500
+ "c:custSplit",
1501
+ "c:secondPieSize",
1502
+ "c:axId",
1503
+ "c:extLst"
1504
+ ];
1505
+ /**
1506
+ * The subset of {@link CLASSIC_GROUP_CHILD_ORDER} that
1507
+ * `patchRawChartGroupSimpleFields` can rewrite in place. The field
1508
+ * name on the left is the `ChartTypeGroup` model key; the right-hand
1509
+ * value is the OOXML element name (sans `val=` attribute, which this
1510
+ * patcher always uses). Boolean model fields are serialised as
1511
+ * `"1"` / `"0"` to match the ECMA-376 convention.
1512
+ *
1513
+ * Kept in sync with `SIMPLE_GROUP_FIELD_NAMES` (used by
1514
+ * `stripPatchableChartFields`) — every entry in this map must also be
1515
+ * stripped from the baseline diff, otherwise a plain
1516
+ * "previous === current after strip" check would see the mutated
1517
+ * leaf and refuse the raw-patch plan.
1518
+ */
1519
+ const SIMPLE_GROUP_FIELD_TAGS = {
1520
+ barDir: "c:barDir",
1521
+ grouping: "c:grouping",
1522
+ varyColors: "c:varyColors",
1523
+ gapWidth: "c:gapWidth",
1524
+ overlap: "c:overlap",
1525
+ firstSliceAng: "c:firstSliceAng",
1526
+ holeSize: "c:holeSize",
1527
+ gapDepth: "c:gapDepth",
1528
+ scatterStyle: "c:scatterStyle",
1529
+ radarStyle: "c:radarStyle",
1530
+ ofPieType: "c:ofPieType",
1531
+ splitType: "c:splitType",
1532
+ splitPos: "c:splitPos",
1533
+ secondPieSize: "c:secondPieSize",
1534
+ bubbleScale: "c:bubbleScale",
1535
+ showNegBubbles: "c:showNegBubbles",
1536
+ sizeRepresents: "c:sizeRepresents",
1537
+ shape: "c:shape",
1538
+ smooth: "c:smooth",
1539
+ wireframe: "c:wireframe"
1540
+ };
1541
+ const SIMPLE_GROUP_FIELD_NAMES = Object.keys(SIMPLE_GROUP_FIELD_TAGS);
1542
+ /**
1543
+ * Extract the simple-field projection of every chart-type group in a
1544
+ * `ChartModel` for diffing. Produces a stable array of plain objects
1545
+ * (one per group) keyed by field name so
1546
+ * `sameJson(extractSimpleGroupFields(prev), extractSimpleGroupFields(curr))`
1547
+ * answers "did any group simple field change?".
1548
+ */
1549
+ function extractSimpleGroupFields(model) {
1550
+ const groups = model.chart?.plotArea?.chartTypes ?? [];
1551
+ return groups.map((group) => {
1552
+ const out = { type: group.type };
1553
+ for (const key of SIMPLE_GROUP_FIELD_NAMES) {
1554
+ if (group[key] !== undefined) {
1555
+ out[key] = group[key];
1556
+ }
1557
+ }
1558
+ return out;
1559
+ });
1560
+ }
1561
+ /**
1562
+ * Raw-XML patcher for the simple leaf fields of every chart-type
1563
+ * group: `gapWidth`, `overlap`, `varyColors`, `firstSliceAng`,
1564
+ * `holeSize`, `gapDepth`, `radarStyle`, `scatterStyle`, `ofPieType`,
1565
+ * `smooth`, and the other `val="…"` leaves listed in
1566
+ * {@link SIMPLE_GROUP_FIELD_TAGS}. Called by `tryPatchChartRawXml`
1567
+ * when `plan.groupSimpleFields` is true so these common user edits
1568
+ * (tightening bar overlap, rotating a pie, lowering bubble scale)
1569
+ * keep the fast raw-patch path instead of rebuilding the chart XML
1570
+ * structurally and losing any vendor extensions along the way.
1571
+ *
1572
+ * Returns `undefined` when a group block cannot be located or its
1573
+ * type lacks a known tag — signalling to the caller that it should
1574
+ * fall back to a structural rebuild.
1575
+ */
1576
+ function patchRawChartGroupSimpleFields(raw, model) {
1577
+ let patched = raw;
1578
+ for (const group of model.chart?.plotArea?.chartTypes ?? []) {
1579
+ const tag = chartGroupTagName(group);
1580
+ if (!tag) {
1581
+ return undefined;
1582
+ }
1583
+ const range = findXmlBlock(patched, tag);
1584
+ if (!range) {
1585
+ return undefined;
1586
+ }
1587
+ const block = patched.slice(range.start, range.end);
1588
+ // Series blocks can themselves contain elements with the same
1589
+ // tag names (e.g. a custom series dLbls might ship with a stale
1590
+ // `c:smooth`); mask them out while we rewrite the group-level
1591
+ // leaves so our regex replacements don't accidentally target a
1592
+ // series-internal element.
1593
+ const { xml: withoutSeries, seriesBlocks } = preserveSeriesBlocks(block, xml => xml);
1594
+ let current = withoutSeries;
1595
+ for (const fieldName of SIMPLE_GROUP_FIELD_NAMES) {
1596
+ const xmlTag = SIMPLE_GROUP_FIELD_TAGS[fieldName];
1597
+ const value = group[fieldName];
1598
+ current = replaceOrRemoveSimpleGroupField(current, xmlTag, value);
1599
+ }
1600
+ const restored = restoreSeriesBlocks(current, seriesBlocks);
1601
+ patched = patched.slice(0, range.start) + restored + patched.slice(range.end);
1602
+ }
1603
+ return patched;
1604
+ }
1605
+ /**
1606
+ * Replace, insert, or remove a `<c:xxx val="…"/>` leaf inside a
1607
+ * chart-type group block while keeping the block's child order
1608
+ * schema-valid (see {@link CLASSIC_GROUP_CHILD_ORDER}).
1609
+ *
1610
+ * - `value === undefined` → element is removed if present, left
1611
+ * untouched otherwise.
1612
+ * - `value !== undefined` → element is rewritten in place when it
1613
+ * already exists, or inserted before the first schema-later
1614
+ * sibling when it does not.
1615
+ *
1616
+ * Booleans are serialised as `"1"` / `"0"`; numbers use their string
1617
+ * representation; strings pass through with attribute escaping. The
1618
+ * schema only expects these three primitive shapes for simple leaves.
1619
+ */
1620
+ function replaceOrRemoveSimpleGroupField(block, tag, value) {
1621
+ const leafRegex = new RegExp(`<${escapeRegExp(tag)}(?:\\s+[^/>]*)?/>`, "g");
1622
+ if (value === undefined) {
1623
+ // Strip the existing leaf if present, no-op otherwise.
1624
+ return block.replace(leafRegex, "");
1625
+ }
1626
+ const serialised = serialiseSimpleGroupFieldValue(value);
1627
+ const replacement = `<${tag} val="${serialised}"/>`;
1628
+ if (leafRegex.test(block)) {
1629
+ return block.replace(leafRegex, replacement);
1630
+ }
1631
+ // Insert in schema order: find the first sibling that comes after
1632
+ // our tag in CLASSIC_GROUP_CHILD_ORDER and that exists in the
1633
+ // current block.
1634
+ const tagIndex = CLASSIC_GROUP_CHILD_ORDER.indexOf(tag);
1635
+ if (tagIndex < 0) {
1636
+ return block; // unknown tag — do not risk corrupting the XML
1637
+ }
1638
+ const laterSiblings = CLASSIC_GROUP_CHILD_ORDER.slice(tagIndex + 1);
1639
+ for (const sibling of laterSiblings) {
1640
+ const siblingIdx = block.indexOf(`<${sibling}`);
1641
+ if (siblingIdx >= 0) {
1642
+ return block.slice(0, siblingIdx) + replacement + block.slice(siblingIdx);
1643
+ }
1644
+ }
1645
+ // No later sibling found — insert before the closing `</…Chart>`.
1646
+ const closeMatch = /<\/c:\w+Chart>\s*$/.exec(block);
1647
+ if (closeMatch) {
1648
+ const insertAt = closeMatch.index;
1649
+ return block.slice(0, insertAt) + replacement + block.slice(insertAt);
1650
+ }
1651
+ return block;
1652
+ }
1653
+ function escapeRegExp(value) {
1654
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1655
+ }
1656
+ function serialiseSimpleGroupFieldValue(value) {
1657
+ if (typeof value === "boolean") {
1658
+ return value ? "1" : "0";
1659
+ }
1660
+ if (typeof value === "number") {
1661
+ // Guard against `NaN` / `Infinity` leaking into the attribute —
1662
+ // `String(NaN) === "NaN"` produces XML Excel rejects. Callers
1663
+ // that pass an invalid numeric should get an empty string
1664
+ // instead; the caller removes the leaf on absence, so an empty
1665
+ // serialise is equivalent to "don't emit this field".
1666
+ return Number.isFinite(value) ? String(value) : "";
1667
+ }
1668
+ // Strings go through the canonical attribute encoder. Previously
1669
+ // this helper hand-rolled a minimal `& " <` escape chain, which
1670
+ // let newlines / tabs / illegal XML chars / lone surrogates
1671
+ // through verbatim — the raw patcher then produced attribute
1672
+ // values that (a) normalized to a single space on parse (XML 1.0
1673
+ // §3.3.3), losing newlines, or (b) contained chars no parser
1674
+ // accepts. `xmlEncodeAttr` strips the illegal ones and encodes
1675
+ // CR/LF/Tab as numeric character references so round-trip preserves
1676
+ // whitespace.
1677
+ return xmlEncodeAttr(String(value));
1678
+ }
1679
+ function patchRawChartGroupDataLabels(raw, model) {
1680
+ let patched = raw;
1681
+ for (const group of model.chart?.plotArea?.chartTypes ?? []) {
1682
+ const tag = chartGroupTagName(group);
1683
+ if (!tag) {
1684
+ return undefined;
1685
+ }
1686
+ const range = findXmlBlock(patched, tag);
1687
+ if (!range) {
1688
+ return undefined;
1689
+ }
1690
+ const block = patched.slice(range.start, range.end);
1691
+ const replacement = patchRawChartGroupDataLabelsBlock(block, group);
1692
+ patched = patched.slice(0, range.start) + replacement + patched.slice(range.end);
1693
+ }
1694
+ return patched;
1695
+ }
1696
+ function patchRawPlotAreaLayout(raw, model) {
1697
+ const plotArea = model.chart?.plotArea;
1698
+ const range = findXmlBlock(raw, "c:plotArea");
1699
+ if (!range || !plotArea) {
1700
+ return undefined;
1701
+ }
1702
+ const block = raw.slice(range.start, range.end);
1703
+ const layoutXml = plotArea.layout ? buildRawLayoutXml(plotArea.layout) : "";
1704
+ const patched = layoutXml
1705
+ ? replaceOrInsertBeforeGeneric(block, "c:layout", layoutXml, [
1706
+ "c:areaChart",
1707
+ "c:area3DChart",
1708
+ "c:barChart",
1709
+ "c:bar3DChart",
1710
+ "c:lineChart",
1711
+ "c:line3DChart",
1712
+ "c:pieChart",
1713
+ "c:pie3DChart",
1714
+ "c:doughnutChart",
1715
+ "c:scatterChart",
1716
+ "c:bubbleChart",
1717
+ "c:radarChart",
1718
+ "c:stockChart",
1719
+ "c:surfaceChart",
1720
+ "c:surface3DChart",
1721
+ "c:ofPieChart",
1722
+ "c:catAx",
1723
+ "c:valAx",
1724
+ "c:serAx",
1725
+ "c:dateAx",
1726
+ "c:spPr"
1727
+ ], "c:plotArea")
1728
+ : removeXmlBlock(block, "c:layout");
1729
+ return raw.slice(0, range.start) + patched + raw.slice(range.end);
1730
+ }
1731
+ function buildRawLayoutXml(layout, namespace = "c") {
1732
+ if (!layout?.manualLayout) {
1733
+ return `<${namespace}:layout/>`;
1734
+ }
1735
+ const ml = layout.manualLayout;
1736
+ const parts = [`<${namespace}:layout><${namespace}:manualLayout>`];
1737
+ for (const [name, value] of [
1738
+ ["layoutTarget", ml.layoutTarget],
1739
+ ["xMode", ml.xMode],
1740
+ ["yMode", ml.yMode],
1741
+ ["wMode", ml.wMode],
1742
+ ["hMode", ml.hMode],
1743
+ ["x", ml.x],
1744
+ ["y", ml.y],
1745
+ ["w", ml.w],
1746
+ ["h", ml.h]
1747
+ ]) {
1748
+ if (value !== undefined) {
1749
+ parts.push(`<${namespace}:${name} val="${escapeAttr(String(value))}"/>`);
1750
+ }
1751
+ }
1752
+ parts.push(`</${namespace}:manualLayout></${namespace}:layout>`);
1753
+ return parts.join("");
1754
+ }
1755
+ function patchRawChartGroupDataLabelsBlock(block, group) {
1756
+ const withoutSeriesBlocks = preserveSeriesBlocks(block, xml => xml);
1757
+ if (group.dataLabels) {
1758
+ return restoreSeriesBlocks(replaceOrInsertBefore(withoutSeriesBlocks.xml, "c:dLbls", buildRawDataLabelsXml(group.dataLabels, {
1759
+ suppressDLblPos: group.type === "doughnut"
1760
+ }), [
1761
+ "c:gapWidth",
1762
+ "c:overlap",
1763
+ "c:serLines",
1764
+ "c:axId",
1765
+ "c:firstSliceAng",
1766
+ "c:holeSize",
1767
+ "c:extLst"
1768
+ ]), withoutSeriesBlocks.seriesBlocks);
1769
+ }
1770
+ const stripped = withoutSeriesBlocks.xml.replace(/<c:dLbls>[\s\S]*?<\/c:dLbls>/, "");
1771
+ return restoreSeriesBlocks(stripped, withoutSeriesBlocks.seriesBlocks);
1772
+ }
1773
+ function chartGroupTagName(group) {
1774
+ const tagByType = {
1775
+ bar: "c:barChart",
1776
+ bar3D: "c:bar3DChart",
1777
+ line: "c:lineChart",
1778
+ line3D: "c:line3DChart",
1779
+ pie: "c:pieChart",
1780
+ pie3D: "c:pie3DChart",
1781
+ doughnut: "c:doughnutChart",
1782
+ area: "c:areaChart",
1783
+ area3D: "c:area3DChart",
1784
+ scatter: "c:scatterChart",
1785
+ bubble: "c:bubbleChart",
1786
+ radar: "c:radarChart",
1787
+ stock: "c:stockChart",
1788
+ surface: "c:surfaceChart",
1789
+ surface3D: "c:surface3DChart",
1790
+ ofPie: "c:ofPieChart"
1791
+ };
1792
+ return tagByType[group.type];
1793
+ }
1794
+ function preserveSeriesBlocks(block, transform) {
1795
+ const seriesBlocks = [];
1796
+ let cursor = 0;
1797
+ let xml = "";
1798
+ while (cursor < block.length) {
1799
+ const range = findXmlBlock(block, "c:ser", cursor);
1800
+ if (!range) {
1801
+ xml += block.slice(cursor);
1802
+ break;
1803
+ }
1804
+ xml += block.slice(cursor, range.start);
1805
+ const placeholder = `__EXCELTS_SER_${seriesBlocks.length}__`;
1806
+ seriesBlocks.push(transform(block.slice(range.start, range.end)));
1807
+ xml += placeholder;
1808
+ cursor = range.end;
1809
+ }
1810
+ return { xml, seriesBlocks };
1811
+ }
1812
+ function restoreSeriesBlocks(block, seriesBlocks) {
1813
+ return seriesBlocks.reduce((xml, seriesBlock, i) => xml.replace(`__EXCELTS_SER_${i}__`, seriesBlock), block);
1814
+ }
1815
+ function buildRawDataLabelsXml(dataLabels, opts) {
1816
+ const parts = ["<c:dLbls>"];
1817
+ if (Array.isArray(dataLabels.entries)) {
1818
+ for (const entry of dataLabels.entries) {
1819
+ parts.push(buildRawDataLabelEntryXml(entry, opts));
1820
+ }
1821
+ }
1822
+ // ECMA-376 `CT_DLbls` (§21.2.2.49) child order (confirmed against
1823
+ // Microsoft OpenXML `DataLabels.ChildElementInfo`):
1824
+ // dLbl*, delete | (numFmt, spPr, txPr, dLblPos, showLegendKey,
1825
+ // showVal, showCatName, showSerName, showPercent,
1826
+ // showBubbleSize, separator, showLeaderLines, leaderLines),
1827
+ // extLst?.
1828
+ // The earlier raw-builder placed every `show*` flag BEFORE
1829
+ // `dLblPos` / `spPr` / `txPr` and `separator` AFTER
1830
+ // `showLeaderLines` — two schema violations that Excel silently
1831
+ // tolerates but LibreOffice strict mode refuses.
1832
+ if (dataLabels.numFmt?.formatCode) {
1833
+ const sourceLinked = dataLabels.numFmt.sourceLinked === undefined
1834
+ ? "1"
1835
+ : dataLabels.numFmt.sourceLinked
1836
+ ? "1"
1837
+ : "0";
1838
+ parts.push(`<c:numFmt formatCode="${escapeAttr(dataLabels.numFmt.formatCode)}" sourceLinked="${sourceLinked}"/>`);
1839
+ }
1840
+ if (dataLabels.spPr) {
1841
+ parts.push(buildRawShapePropertiesXml(dataLabels.spPr, "c") ?? "");
1842
+ }
1843
+ if (dataLabels.txPr) {
1844
+ parts.push(buildRawTextPropertiesXml(dataLabels.txPr, "c") ?? "");
1845
+ }
1846
+ // Doughnut charts must not emit `c:dLblPos` — Excel rejects the
1847
+ // element on open. See `_renderDoughnutChart` in
1848
+ // `chart-space-xform.ts` for the full rationale and bisect.
1849
+ if (dataLabels.position !== undefined && !opts?.suppressDLblPos) {
1850
+ parts.push(`<c:dLblPos val="${escapeAttr(String(dataLabels.position))}"/>`);
1851
+ }
1852
+ const flags = [
1853
+ ["showLegendKey", dataLabels.showLegendKey],
1854
+ ["showVal", dataLabels.showVal],
1855
+ ["showCatName", dataLabels.showCatName],
1856
+ ["showSerName", dataLabels.showSerName],
1857
+ ["showPercent", dataLabels.showPercent],
1858
+ ["showBubbleSize", dataLabels.showBubbleSize]
1859
+ ];
1860
+ for (const [name, value] of flags) {
1861
+ if (value !== undefined) {
1862
+ parts.push(`<c:${name} val="${value ? "1" : "0"}"/>`);
1863
+ }
1864
+ }
1865
+ if (dataLabels.separator !== undefined) {
1866
+ parts.push(`<c:separator>${escapeXml(String(dataLabels.separator))}</c:separator>`);
1867
+ }
1868
+ if (dataLabels.showLeaderLines !== undefined) {
1869
+ parts.push(`<c:showLeaderLines val="${dataLabels.showLeaderLines ? "1" : "0"}"/>`);
1870
+ }
1871
+ if (dataLabels.extLst) {
1872
+ parts.push(dataLabels.extLst);
1873
+ }
1874
+ parts.push("</c:dLbls>");
1875
+ return parts.join("");
1876
+ }
1877
+ function buildRawDataLabelEntryXml(entry, opts) {
1878
+ // ECMA-376 `CT_DLbl` (§21.2.2.47) is a `choice(delete | …)` — the
1879
+ // two branches are mutually exclusive. Emitting `delete` alongside
1880
+ // any of the display-flag children (layout / tx / numFmt /
1881
+ // dLblPos / show* / separator) violates the schema; Excel's
1882
+ // tolerance varies by build (some strip the label wholesale).
1883
+ const parts = ["<c:dLbl>", `<c:idx val="${entry.index ?? 0}"/>`];
1884
+ if (entry.delete) {
1885
+ parts.push(`<c:delete val="1"/>`);
1886
+ if (entry.extLst) {
1887
+ parts.push(entry.extLst);
1888
+ }
1889
+ parts.push("</c:dLbl>");
1890
+ return parts.join("");
1891
+ }
1892
+ if (entry.layout) {
1893
+ parts.push(buildRawLayoutXml(entry.layout));
1894
+ }
1895
+ if (entry.rawTx) {
1896
+ parts.push(entry.rawTx);
1897
+ }
1898
+ else if (entry.text?.paragraphs?.[0]?.runs?.[0]?.text !== undefined) {
1899
+ parts.push(`<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${escapeXml(String(entry.text.paragraphs[0].runs[0].text))}</a:t></a:r></a:p></c:rich></c:tx>`);
1900
+ }
1901
+ if (entry.numFmt?.formatCode) {
1902
+ parts.push(`<c:numFmt formatCode="${escapeAttr(entry.numFmt.formatCode)}" sourceLinked="${entry.numFmt.sourceLinked ? "1" : "0"}"/>`);
1903
+ }
1904
+ if (entry.spPr) {
1905
+ parts.push(buildRawShapePropertiesXml(entry.spPr, "c") ?? "");
1906
+ }
1907
+ if (entry.txPr) {
1908
+ parts.push(buildRawTextPropertiesXml(entry.txPr, "c") ?? "");
1909
+ }
1910
+ if (entry.position !== undefined && !opts?.suppressDLblPos) {
1911
+ parts.push(`<c:dLblPos val="${escapeAttr(String(entry.position))}"/>`);
1912
+ }
1913
+ for (const [name, value] of [
1914
+ ["showLegendKey", entry.showLegendKey],
1915
+ ["showVal", entry.showVal],
1916
+ ["showCatName", entry.showCatName],
1917
+ ["showSerName", entry.showSerName],
1918
+ ["showPercent", entry.showPercent],
1919
+ ["showBubbleSize", entry.showBubbleSize]
1920
+ ]) {
1921
+ if (value !== undefined) {
1922
+ parts.push(`<c:${name} val="${value ? "1" : "0"}"/>`);
1923
+ }
1924
+ }
1925
+ if (entry.separator !== undefined) {
1926
+ parts.push(`<c:separator>${escapeXml(String(entry.separator))}</c:separator>`);
1927
+ }
1928
+ if (entry.extLst) {
1929
+ parts.push(entry.extLst);
1930
+ }
1931
+ parts.push("</c:dLbl>");
1932
+ return parts.join("");
1933
+ }
1934
+ function patchRawAxes(raw, model, patchPlan) {
1935
+ let patched = raw;
1936
+ for (const [index, axis] of (model.chart?.plotArea?.axes ?? []).entries()) {
1937
+ const axisPlan = getRawPatchListItem(patchPlan, index);
1938
+ if (!axisPlan) {
1939
+ continue;
1940
+ }
1941
+ const tag = axis.axisType === "cat"
1942
+ ? "c:catAx"
1943
+ : axis.axisType === "val"
1944
+ ? "c:valAx"
1945
+ : axis.axisType === "date"
1946
+ ? "c:dateAx"
1947
+ : "c:serAx";
1948
+ const block = findAxisBlock(patched, tag, axis.axId);
1949
+ if (!block) {
1950
+ return undefined;
1951
+ }
1952
+ const axisXml = patchRawAxisBlock(block.xml, axis, axisPlan);
1953
+ if (!axisXml) {
1954
+ return undefined;
1955
+ }
1956
+ patched = patched.slice(0, block.start) + axisXml + patched.slice(block.end);
1957
+ }
1958
+ return patched;
1959
+ }
1960
+ function patchRawAxisBlock(block, axis, patchPlan) {
1961
+ let patched = block;
1962
+ const axisTag = axisTagName(axis);
1963
+ if (rawPatchFlag(patchPlan, "scaling")) {
1964
+ patched = patchGenericChild(patched, "c:scaling", buildRawScalingXml(axis.scaling), ["c:delete", "c:axPos"], axisTag);
1965
+ }
1966
+ if (rawPatchFlag(patchPlan, "delete")) {
1967
+ patched = patchBooleanLeaf(patched, "c:delete", axis.delete, ["c:axPos"], axisTag);
1968
+ }
1969
+ if (rawPatchFlag(patchPlan, "title")) {
1970
+ if (axis.title) {
1971
+ const titleText = axis.title.text?.paragraphs?.[0]?.runs?.[0]?.text;
1972
+ if (titleText !== undefined) {
1973
+ patched = replaceOrInsertBefore(patched, "c:title", buildRawChartTitleXml(titleText), [
1974
+ "c:numFmt",
1975
+ "c:majorGridlines",
1976
+ "c:minorGridlines",
1977
+ "c:majorUnit",
1978
+ "c:minorUnit",
1979
+ "c:majorTickMark",
1980
+ "c:minorTickMark",
1981
+ "c:tickLblPos"
1982
+ ]);
1983
+ }
1984
+ }
1985
+ else {
1986
+ patched = patched.replace(/<c:title>[\s\S]*?<\/c:title>/, "");
1987
+ }
1988
+ }
1989
+ if (rawPatchFlag(patchPlan, "numFmt")) {
1990
+ patched = patchGenericChild(patched, "c:numFmt", buildRawNumFmtXml(axis.numFmt), [
1991
+ "c:majorGridlines",
1992
+ "c:minorGridlines",
1993
+ "c:majorUnit",
1994
+ "c:minorUnit",
1995
+ "c:majorTickMark",
1996
+ "c:minorTickMark",
1997
+ "c:tickLblPos",
1998
+ "c:spPr",
1999
+ "c:txPr",
2000
+ "c:crossAx"
2001
+ ], axisTag);
2002
+ }
2003
+ if (rawPatchFlag(patchPlan, "majorGridlines")) {
2004
+ patched = patchGridlines(patched, "c:majorGridlines", axis.majorGridlines, [
2005
+ "c:minorGridlines",
2006
+ "c:title",
2007
+ "c:numFmt",
2008
+ "c:majorUnit",
2009
+ "c:minorUnit",
2010
+ "c:majorTickMark",
2011
+ "c:minorTickMark",
2012
+ "c:tickLblPos",
2013
+ "c:spPr",
2014
+ "c:txPr",
2015
+ "c:crossAx"
2016
+ ], axisTag);
2017
+ }
2018
+ if (rawPatchFlag(patchPlan, "minorGridlines")) {
2019
+ patched = patchGridlines(patched, "c:minorGridlines", axis.minorGridlines, [
2020
+ "c:title",
2021
+ "c:numFmt",
2022
+ "c:majorUnit",
2023
+ "c:minorUnit",
2024
+ "c:majorTickMark",
2025
+ "c:minorTickMark",
2026
+ "c:tickLblPos",
2027
+ "c:spPr",
2028
+ "c:txPr",
2029
+ "c:crossAx"
2030
+ ], axisTag);
2031
+ }
2032
+ if (rawPatchFlag(patchPlan, "majorTickMark")) {
2033
+ patched = patchValueLeaf(patched, "c:majorTickMark", axis.majorTickMark, ["c:minorTickMark", "c:tickLblPos", "c:spPr", "c:txPr", "c:crossAx"], axisTag);
2034
+ }
2035
+ if (rawPatchFlag(patchPlan, "minorTickMark")) {
2036
+ patched = patchValueLeaf(patched, "c:minorTickMark", axis.minorTickMark, ["c:tickLblPos", "c:spPr", "c:txPr", "c:crossAx"], axisTag);
2037
+ }
2038
+ if (rawPatchFlag(patchPlan, "tickLblPos")) {
2039
+ patched = patchValueLeaf(patched, "c:tickLblPos", axis.tickLblPos, ["c:spPr", "c:txPr", "c:crossAx"], axisTag);
2040
+ }
2041
+ if (rawPatchFlag(patchPlan, "spPr")) {
2042
+ patched = patchGenericChild(patched, "c:spPr", buildRawShapePropertiesXml(axis.spPr, "c"), ["c:txPr", "c:crossAx"], axisTag);
2043
+ }
2044
+ if (rawPatchFlag(patchPlan, "txPr")) {
2045
+ patched = patchGenericChild(patched, "c:txPr", buildRawTextPropertiesXml(axis.txPr, "c"), ["c:crossAx"], axisTag);
2046
+ }
2047
+ if (rawPatchFlag(patchPlan, "crosses")) {
2048
+ patched = patchValueLeaf(patched, "c:crosses", axis.crosses, [
2049
+ "c:crossesAt",
2050
+ "c:auto",
2051
+ "c:lblAlgn",
2052
+ "c:lblOffset",
2053
+ "c:tickLblSkip",
2054
+ "c:tickMarkSkip",
2055
+ "c:noMultiLvlLbl",
2056
+ "c:crossBetween",
2057
+ "c:majorUnit",
2058
+ "c:minorUnit",
2059
+ "c:baseTimeUnit",
2060
+ "c:majorTimeUnit",
2061
+ "c:minorTimeUnit",
2062
+ "c:dispUnits",
2063
+ "c:extLst"
2064
+ ], axisTag);
2065
+ }
2066
+ if (rawPatchFlag(patchPlan, "crossesAt")) {
2067
+ patched = patchValueLeaf(patched, "c:crossesAt", axis.crossesAt, [
2068
+ "c:auto",
2069
+ "c:lblAlgn",
2070
+ "c:lblOffset",
2071
+ "c:tickLblSkip",
2072
+ "c:tickMarkSkip",
2073
+ "c:noMultiLvlLbl",
2074
+ "c:crossBetween",
2075
+ "c:majorUnit",
2076
+ "c:minorUnit",
2077
+ "c:baseTimeUnit",
2078
+ "c:majorTimeUnit",
2079
+ "c:minorTimeUnit",
2080
+ "c:dispUnits",
2081
+ "c:extLst"
2082
+ ], axisTag);
2083
+ }
2084
+ patched = patchAxisTypeSpecificLeaves(patched, axis, patchPlan);
2085
+ return patched;
2086
+ }
2087
+ function axisTagName(axis) {
2088
+ return axis.axisType === "cat"
2089
+ ? "c:catAx"
2090
+ : axis.axisType === "val"
2091
+ ? "c:valAx"
2092
+ : axis.axisType === "date"
2093
+ ? "c:dateAx"
2094
+ : "c:serAx";
2095
+ }
2096
+ function patchAxisTypeSpecificLeaves(block, axis, patchPlan) {
2097
+ const axisTag = axisTagName(axis);
2098
+ let patched = block;
2099
+ if (rawPatchFlag(patchPlan, "auto")) {
2100
+ patched = patchBooleanLeaf(patched, "c:auto", axis.auto, [
2101
+ "c:lblAlgn",
2102
+ "c:lblOffset",
2103
+ "c:tickLblSkip",
2104
+ "c:tickMarkSkip",
2105
+ "c:noMultiLvlLbl",
2106
+ "c:extLst"
2107
+ ], axisTag);
2108
+ }
2109
+ if (rawPatchFlag(patchPlan, "lblAlgn")) {
2110
+ patched = patchValueLeaf(patched, "c:lblAlgn", axis.lblAlgn, ["c:lblOffset", "c:tickLblSkip", "c:tickMarkSkip", "c:noMultiLvlLbl", "c:extLst"], axisTag);
2111
+ }
2112
+ if (rawPatchFlag(patchPlan, "lblOffset")) {
2113
+ patched = patchValueLeaf(patched, "c:lblOffset", axis.lblOffset, ["c:tickLblSkip", "c:tickMarkSkip", "c:noMultiLvlLbl", "c:extLst"], axisTag);
2114
+ }
2115
+ if (rawPatchFlag(patchPlan, "tickLblSkip")) {
2116
+ patched = patchValueLeaf(patched, "c:tickLblSkip", axis.tickLblSkip, ["c:tickMarkSkip", "c:noMultiLvlLbl", "c:extLst"], axisTag);
2117
+ }
2118
+ if (rawPatchFlag(patchPlan, "tickMarkSkip")) {
2119
+ patched = patchValueLeaf(patched, "c:tickMarkSkip", axis.tickMarkSkip, ["c:noMultiLvlLbl", "c:extLst"], axisTag);
2120
+ }
2121
+ if (rawPatchFlag(patchPlan, "noMultiLvlLbl")) {
2122
+ patched = patchBooleanLeaf(patched, "c:noMultiLvlLbl", axis.noMultiLvlLbl, ["c:extLst"], axisTag);
2123
+ }
2124
+ if (rawPatchFlag(patchPlan, "crossBetween")) {
2125
+ patched = patchValueLeaf(patched, "c:crossBetween", axis.crossBetween, ["c:majorUnit", "c:minorUnit", "c:dispUnits", "c:extLst"], axisTag);
2126
+ }
2127
+ if (rawPatchFlag(patchPlan, "majorUnit")) {
2128
+ patched = patchValueLeaf(patched, "c:majorUnit", axis.majorUnit, ["c:minorUnit", "c:dispUnits", "c:extLst"], axisTag);
2129
+ }
2130
+ if (rawPatchFlag(patchPlan, "minorUnit")) {
2131
+ patched = patchValueLeaf(patched, "c:minorUnit", axis.minorUnit, ["c:dispUnits", "c:extLst"], axisTag);
2132
+ }
2133
+ if (rawPatchFlag(patchPlan, "baseTimeUnit")) {
2134
+ patched = patchValueLeaf(patched, "c:baseTimeUnit", axis.baseTimeUnit, ["c:majorUnit", "c:majorTimeUnit", "c:minorUnit", "c:minorTimeUnit", "c:extLst"], axisTag);
2135
+ }
2136
+ if (rawPatchFlag(patchPlan, "majorTimeUnit")) {
2137
+ patched = patchValueLeaf(patched, "c:majorTimeUnit", axis.majorTimeUnit, ["c:minorUnit", "c:minorTimeUnit", "c:extLst"], axisTag);
2138
+ }
2139
+ if (rawPatchFlag(patchPlan, "minorTimeUnit")) {
2140
+ patched = patchValueLeaf(patched, "c:minorTimeUnit", axis.minorTimeUnit, ["c:extLst"], axisTag);
2141
+ }
2142
+ return patched;
2143
+ }
2144
+ function buildRawScalingXml(scaling) {
2145
+ if (!scaling) {
2146
+ return "";
2147
+ }
2148
+ const parts = ["<c:scaling>"];
2149
+ // ECMA-376 `CT_Scaling` sequence is `logBase?, orientation?,
2150
+ // max?, min?, extLst?`. Emitting children in any other order
2151
+ // triggers a "Repaired Records" dialog when Excel opens the
2152
+ // file and causes LibreOffice strict-mode to reject it outright.
2153
+ if (scaling.logBase !== undefined && Number.isFinite(scaling.logBase) && scaling.logBase > 0) {
2154
+ // `CT_LogBase` requires the value be `>= 2` per ECMA-376
2155
+ // §21.2.3.21; `> 0` is the looser guard we use at parse time.
2156
+ // Leave range clamping to the builder.
2157
+ parts.push(`<c:logBase val="${scaling.logBase}"/>`);
2158
+ }
2159
+ if (scaling.orientation !== undefined) {
2160
+ parts.push(`<c:orientation val="${escapeAttr(scaling.orientation)}"/>`);
2161
+ }
2162
+ // Numeric scaling attributes MUST be finite on the wire; the OOXML
2163
+ // grammar requires `xsd:double` / `xsd:unsignedInt`, and writing
2164
+ // `val="NaN"` or `val="Infinity"` produces a file Excel refuses to
2165
+ // open. `String(NaN) === "NaN"`, so the prior direct interpolation
2166
+ // silently passed garbage through. Guard each slot and skip
2167
+ // non-finite values — the schema treats absence as "auto", which
2168
+ // is closer to the author's intent than an invalid literal.
2169
+ if (scaling.max !== undefined && Number.isFinite(scaling.max)) {
2170
+ parts.push(`<c:max val="${scaling.max}"/>`);
2171
+ }
2172
+ if (scaling.min !== undefined && Number.isFinite(scaling.min)) {
2173
+ parts.push(`<c:min val="${scaling.min}"/>`);
2174
+ }
2175
+ parts.push("</c:scaling>");
2176
+ return parts.join("");
2177
+ }
2178
+ function buildRawNumFmtXml(numFmt) {
2179
+ if (!numFmt?.formatCode) {
2180
+ return "";
2181
+ }
2182
+ const sourceLinked = numFmt.sourceLinked === undefined ? "1" : numFmt.sourceLinked ? "1" : "0";
2183
+ return `<c:numFmt formatCode="${escapeAttr(numFmt.formatCode)}" sourceLinked="${sourceLinked}"/>`;
2184
+ }
2185
+ function patchGridlines(block, tag, spPr, beforeTags, parentTag) {
2186
+ const xml = spPr ? `<${tag}>${buildRawShapePropertiesXml(spPr, "c") ?? ""}</${tag}>` : "";
2187
+ return patchGenericChild(block, tag, xml, beforeTags, parentTag);
2188
+ }
2189
+ function patchValueLeaf(block, tag, value, beforeTags, parentTag) {
2190
+ const xml = value === undefined ? "" : `<${tag} val="${escapeAttr(String(value))}"/>`;
2191
+ return patchGenericChild(block, tag, xml, beforeTags, parentTag);
2192
+ }
2193
+ function patchBooleanLeaf(block, tag, value, beforeTags, parentTag) {
2194
+ const xml = value === undefined ? "" : `<${tag} val="${value ? "1" : "0"}"/>`;
2195
+ return patchGenericChild(block, tag, xml, beforeTags, parentTag);
2196
+ }
2197
+ function buildRawSeriesTxXml(tx) {
2198
+ if (tx.strRef?.formula) {
2199
+ return `<c:tx>${buildRawStrRefXml(tx.strRef)}</c:tx>`;
2200
+ }
2201
+ return `<c:tx><c:v>${escapeXml(String(tx.value ?? ""))}</c:v></c:tx>`;
2202
+ }
2203
+ function buildRawMarkerXml(marker) {
2204
+ if (!marker) {
2205
+ return "";
2206
+ }
2207
+ const parts = ["<c:marker>"];
2208
+ if (marker.symbol) {
2209
+ parts.push(`<c:symbol val="${escapeAttr(String(marker.symbol))}"/>`);
2210
+ }
2211
+ if (marker.size !== undefined) {
2212
+ parts.push(`<c:size val="${marker.size}"/>`);
2213
+ }
2214
+ if (marker.spPr) {
2215
+ parts.push(buildRawShapePropertiesXml(marker.spPr, "c") ?? "");
2216
+ }
2217
+ if (marker.extLst) {
2218
+ parts.push(marker.extLst);
2219
+ }
2220
+ parts.push("</c:marker>");
2221
+ return parts.join("");
2222
+ }
2223
+ function buildRawDataPointsXml(dataPoints) {
2224
+ if (!Array.isArray(dataPoints) || dataPoints.length === 0) {
2225
+ return "";
2226
+ }
2227
+ return dataPoints.map(buildRawDataPointXml).join("");
2228
+ }
2229
+ function buildRawDataPointXml(point) {
2230
+ const parts = ["<c:dPt>", `<c:idx val="${point.index ?? 0}"/>`];
2231
+ if (point.invertIfNegative !== undefined) {
2232
+ parts.push(`<c:invertIfNegative val="${point.invertIfNegative ? "1" : "0"}"/>`);
2233
+ }
2234
+ if (point.marker) {
2235
+ parts.push(buildRawMarkerXml(point.marker));
2236
+ }
2237
+ if (point.bubble3D !== undefined) {
2238
+ parts.push(`<c:bubble3D val="${point.bubble3D ? "1" : "0"}"/>`);
2239
+ }
2240
+ if (point.explosion !== undefined) {
2241
+ parts.push(`<c:explosion val="${point.explosion}"/>`);
2242
+ }
2243
+ if (point.spPr) {
2244
+ parts.push(buildRawShapePropertiesXml(point.spPr, "c") ?? "");
2245
+ }
2246
+ if (point.extLst) {
2247
+ parts.push(point.extLst);
2248
+ }
2249
+ parts.push("</c:dPt>");
2250
+ return parts.join("");
2251
+ }
2252
+ function buildRawTrendlinesXml(trendlines) {
2253
+ if (!Array.isArray(trendlines) || trendlines.length === 0) {
2254
+ return "";
2255
+ }
2256
+ return trendlines.map(buildRawTrendlineXml).join("");
2257
+ }
2258
+ function buildRawTrendlineXml(trendline) {
2259
+ const parts = ["<c:trendline>"];
2260
+ if (trendline.name) {
2261
+ parts.push(`<c:name>${escapeXml(String(trendline.name))}</c:name>`);
2262
+ }
2263
+ if (trendline.spPr) {
2264
+ parts.push(buildRawShapePropertiesXml(trendline.spPr, "c") ?? "");
2265
+ }
2266
+ parts.push(`<c:trendlineType val="${escapeAttr(String(trendline.type ?? "linear"))}"/>`);
2267
+ for (const tag of ["order", "period", "forward", "backward", "intercept"]) {
2268
+ if (trendline[tag] !== undefined) {
2269
+ parts.push(`<c:${tag} val="${trendline[tag]}"/>`);
2270
+ }
2271
+ }
2272
+ if (trendline.displayRSqr !== undefined) {
2273
+ parts.push(`<c:dispRSqr val="${trendline.displayRSqr ? "1" : "0"}"/>`);
2274
+ }
2275
+ if (trendline.displayEq !== undefined) {
2276
+ parts.push(`<c:dispEq val="${trendline.displayEq ? "1" : "0"}"/>`);
2277
+ }
2278
+ if (trendline.trendlineLbl) {
2279
+ parts.push(buildRawTrendlineLabelXml(trendline.trendlineLbl));
2280
+ }
2281
+ if (trendline.extLst) {
2282
+ parts.push(trendline.extLst);
2283
+ }
2284
+ parts.push("</c:trendline>");
2285
+ return parts.join("");
2286
+ }
2287
+ function buildRawTrendlineLabelXml(label) {
2288
+ const parts = ["<c:trendlineLbl>"];
2289
+ if (label.layout) {
2290
+ parts.push(buildRawLayoutXml(label.layout));
2291
+ }
2292
+ if (label.rawTx) {
2293
+ parts.push(label.rawTx);
2294
+ }
2295
+ else if (label.text?.paragraphs?.[0]?.runs?.[0]?.text !== undefined) {
2296
+ parts.push(`<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${escapeXml(String(label.text.paragraphs[0].runs[0].text))}</a:t></a:r></a:p></c:rich></c:tx>`);
2297
+ }
2298
+ if (label.numFmt?.formatCode) {
2299
+ parts.push(`<c:numFmt formatCode="${escapeAttr(label.numFmt.formatCode)}" sourceLinked="${label.numFmt.sourceLinked ? "1" : "0"}"/>`);
2300
+ }
2301
+ if (label.spPr) {
2302
+ parts.push(buildRawShapePropertiesXml(label.spPr, "c") ?? "");
2303
+ }
2304
+ if (label.txPr) {
2305
+ parts.push(buildRawTextPropertiesXml(label.txPr, "c") ?? "");
2306
+ }
2307
+ if (label.extLst) {
2308
+ parts.push(label.extLst);
2309
+ }
2310
+ parts.push("</c:trendlineLbl>");
2311
+ return parts.join("");
2312
+ }
2313
+ function buildRawErrorBarsXml(errorBars) {
2314
+ const bars = Array.isArray(errorBars) ? errorBars : errorBars ? [errorBars] : [];
2315
+ return bars.map(buildRawErrorBarXml).join("");
2316
+ }
2317
+ function buildRawErrorBarXml(errorBar) {
2318
+ const parts = ["<c:errBars>"];
2319
+ if (errorBar.errDir) {
2320
+ parts.push(`<c:errDir val="${escapeAttr(String(errorBar.errDir))}"/>`);
2321
+ }
2322
+ parts.push(`<c:errBarType val="${escapeAttr(String(errorBar.barDir ?? "both"))}"/>`);
2323
+ parts.push(`<c:errValType val="${escapeAttr(String(errorBar.errValType ?? "fixedVal"))}"/>`);
2324
+ if (errorBar.noEndCap !== undefined) {
2325
+ parts.push(`<c:noEndCap val="${errorBar.noEndCap ? "1" : "0"}"/>`);
2326
+ }
2327
+ if (errorBar.val !== undefined) {
2328
+ parts.push(`<c:val val="${errorBar.val}"/>`);
2329
+ }
2330
+ if (errorBar.plus) {
2331
+ parts.push(buildRawDataSourceXml("c:plus", errorBar.plus) ?? "");
2332
+ }
2333
+ if (errorBar.minus) {
2334
+ parts.push(buildRawDataSourceXml("c:minus", errorBar.minus) ?? "");
2335
+ }
2336
+ if (errorBar.spPr) {
2337
+ parts.push(buildRawShapePropertiesXml(errorBar.spPr, "c") ?? "");
2338
+ }
2339
+ if (errorBar.extLst) {
2340
+ parts.push(errorBar.extLst);
2341
+ }
2342
+ parts.push("</c:errBars>");
2343
+ return parts.join("");
2344
+ }
2345
+ function buildRawDataSourceXml(tag, source) {
2346
+ if (source.strRef) {
2347
+ return `<${tag}>${buildRawStrRefXml(source.strRef)}</${tag}>`;
2348
+ }
2349
+ if (source.numRef) {
2350
+ return `<${tag}>${buildRawNumRefXml(source.numRef)}</${tag}>`;
2351
+ }
2352
+ return undefined;
2353
+ }
2354
+ function buildRawNumRefXml(ref) {
2355
+ return `<c:numRef><c:f>${escapeXml(ref.formula)}</c:f>${buildRawNumCacheXml(ref.cache)}</c:numRef>`;
2356
+ }
2357
+ function buildRawStrRefXml(ref) {
2358
+ return `<c:strRef><c:f>${escapeXml(ref.formula)}</c:f>${buildRawStrCacheXml(ref.cache)}</c:strRef>`;
2359
+ }
2360
+ function buildRawNumCacheXml(cache) {
2361
+ if (!cache) {
2362
+ return "";
2363
+ }
2364
+ const parts = ["<c:numCache>"];
2365
+ if (cache.formatCode) {
2366
+ parts.push(`<c:formatCode>${escapeXml(cache.formatCode)}</c:formatCode>`);
2367
+ }
2368
+ if (cache.pointCount !== undefined) {
2369
+ parts.push(`<c:ptCount val="${cache.pointCount}"/>`);
2370
+ }
2371
+ for (const point of cache.points ?? []) {
2372
+ if (point.value !== null && point.value !== undefined) {
2373
+ parts.push(`<c:pt idx="${point.index}"><c:v>${escapeXml(String(point.value))}</c:v></c:pt>`);
2374
+ }
2375
+ }
2376
+ parts.push("</c:numCache>");
2377
+ return parts.join("");
2378
+ }
2379
+ function buildRawStrCacheXml(cache) {
2380
+ if (!cache) {
2381
+ return "";
2382
+ }
2383
+ const parts = ["<c:strCache>"];
2384
+ if (cache.pointCount !== undefined) {
2385
+ parts.push(`<c:ptCount val="${cache.pointCount}"/>`);
2386
+ }
2387
+ for (const point of cache.points ?? []) {
2388
+ parts.push(`<c:pt idx="${point.index}"><c:v>${escapeXml(String(point.value))}</c:v></c:pt>`);
2389
+ }
2390
+ parts.push("</c:strCache>");
2391
+ return parts.join("");
2392
+ }
2393
+ function buildRawShapePropertiesXml(spPr, namespace) {
2394
+ if (!spPr) {
2395
+ return "";
2396
+ }
2397
+ if (spPr._rawXml) {
2398
+ return normalizeRawNamespace(spPr._rawXml, "spPr", namespace);
2399
+ }
2400
+ const writer = new XmlWriter();
2401
+ const chartNamespace = namespace;
2402
+ writer.openNode(`${chartNamespace}:spPr`);
2403
+ if (spPr.fill?.noFill) {
2404
+ writer.leafNode("a:noFill");
2405
+ }
2406
+ else if (spPr.fill?.solid) {
2407
+ writer.openNode("a:solidFill");
2408
+ writeRawColor(writer, spPr.fill.solid);
2409
+ writer.closeNode();
2410
+ }
2411
+ else if (spPr.fill?.gradient) {
2412
+ writeRawGradientFill(writer, spPr.fill.gradient);
2413
+ }
2414
+ else if (spPr.fill?.pattern) {
2415
+ const pattern = spPr.fill.pattern;
2416
+ writer.openNode("a:pattFill", { prst: pattern.preset });
2417
+ if (pattern.foreground) {
2418
+ writer.openNode("a:fgClr");
2419
+ writeRawColor(writer, pattern.foreground);
2420
+ writer.closeNode();
2421
+ }
2422
+ if (pattern.background) {
2423
+ writer.openNode("a:bgClr");
2424
+ writeRawColor(writer, pattern.background);
2425
+ writer.closeNode();
2426
+ }
2427
+ writer.closeNode();
2428
+ }
2429
+ if (spPr.line) {
2430
+ const attrs = {};
2431
+ if (spPr.line.width) {
2432
+ attrs.w = String(spPr.line.width);
2433
+ }
2434
+ if (spPr.line.cap) {
2435
+ attrs.cap = spPr.line.cap;
2436
+ }
2437
+ if (spPr.line.compound) {
2438
+ attrs.cmpd = spPr.line.compound;
2439
+ }
2440
+ writer.openNode("a:ln", attrs);
2441
+ if (spPr.line.noFill) {
2442
+ writer.leafNode("a:noFill");
2443
+ }
2444
+ else if (spPr.line.color) {
2445
+ writer.openNode("a:solidFill");
2446
+ writeRawColor(writer, spPr.line.color);
2447
+ writer.closeNode();
2448
+ }
2449
+ if (spPr.line.dash) {
2450
+ writer.leafNode("a:prstDash", { val: spPr.line.dash });
2451
+ }
2452
+ if (spPr.line.join === "round") {
2453
+ writer.leafNode("a:round");
2454
+ }
2455
+ else if (spPr.line.join === "bevel") {
2456
+ writer.leafNode("a:bevel");
2457
+ }
2458
+ else if (spPr.line.join === "miter") {
2459
+ writer.leafNode("a:miter");
2460
+ }
2461
+ writer.closeNode();
2462
+ }
2463
+ if (spPr.effectList) {
2464
+ writeRawEffectList(writer, spPr.effectList);
2465
+ }
2466
+ if (spPr.scene3d) {
2467
+ writeRawScene3D(writer, spPr.scene3d);
2468
+ }
2469
+ if (spPr.sp3d) {
2470
+ writeRawSp3D(writer, spPr.sp3d);
2471
+ }
2472
+ writer.closeNode();
2473
+ return writer.toString();
2474
+ }
2475
+ function buildRawTextPropertiesXml(txPr, namespace) {
2476
+ if (!txPr) {
2477
+ return "";
2478
+ }
2479
+ if (typeof txPr === "string") {
2480
+ return normalizeRawNamespace(txPr, "txPr", namespace);
2481
+ }
2482
+ if (txPr._rawXml) {
2483
+ return normalizeRawNamespace(txPr._rawXml, "txPr", namespace);
2484
+ }
2485
+ const writer = new XmlWriter();
2486
+ writer.openNode(`${namespace}:txPr`);
2487
+ writer.leafNode("a:bodyPr", txPr.rotation !== undefined ? { rot: String(txPr.rotation) } : undefined);
2488
+ writer.leafNode("a:lstStyle");
2489
+ writer.openNode("a:p");
2490
+ writer.openNode("a:pPr");
2491
+ writeRawRunProperties(writer, txPr, "a:defRPr");
2492
+ writer.closeNode();
2493
+ writer.leafNode("a:endParaRPr");
2494
+ writer.closeNode();
2495
+ writer.closeNode();
2496
+ return writer.toString();
2497
+ }
2498
+ function normalizeRawNamespace(rawXml, localName, namespace) {
2499
+ return rawXml
2500
+ .replace(new RegExp(`^<(?:c|cx):${localName}`), `<${namespace}:${localName}`)
2501
+ .replace(new RegExp(`</(?:c|cx):${localName}>$`), `</${namespace}:${localName}>`);
2502
+ }
2503
+ function writeRawRunProperties(writer, props, tag) {
2504
+ const attrs = {};
2505
+ if (props.size !== undefined) {
2506
+ attrs.sz = String(props.size);
2507
+ }
2508
+ if (props.bold !== undefined) {
2509
+ attrs.b = props.bold ? "1" : "0";
2510
+ }
2511
+ if (props.italic !== undefined) {
2512
+ attrs.i = props.italic ? "1" : "0";
2513
+ }
2514
+ if (props.underline !== undefined) {
2515
+ attrs.u =
2516
+ typeof props.underline === "boolean" ? (props.underline ? "sng" : "none") : props.underline;
2517
+ }
2518
+ if (props.strike) {
2519
+ attrs.strike = props.strike;
2520
+ }
2521
+ if (props.rotation !== undefined) {
2522
+ attrs.rot = String(props.rotation);
2523
+ }
2524
+ if (props.baseline !== undefined) {
2525
+ attrs.baseline = String(props.baseline);
2526
+ }
2527
+ if (props.kern !== undefined) {
2528
+ attrs.kern = String(props.kern);
2529
+ }
2530
+ if (props.spacing !== undefined) {
2531
+ attrs.spc = String(props.spacing);
2532
+ }
2533
+ if (props.cap) {
2534
+ attrs.cap = props.cap;
2535
+ }
2536
+ if (props.lang) {
2537
+ attrs.lang = props.lang;
2538
+ }
2539
+ const hasChildren = !!(props.color ||
2540
+ props.fontFamily ||
2541
+ props.eastAsianFamily ||
2542
+ props.complexScriptFamily);
2543
+ if (!hasChildren) {
2544
+ writer.leafNode(tag, attrs);
2545
+ return;
2546
+ }
2547
+ writer.openNode(tag, attrs);
2548
+ if (props.color) {
2549
+ writer.openNode("a:solidFill");
2550
+ writeRawColor(writer, props.color);
2551
+ writer.closeNode();
2552
+ }
2553
+ if (props.fontFamily) {
2554
+ writer.leafNode("a:latin", { typeface: props.fontFamily });
2555
+ }
2556
+ if (props.eastAsianFamily) {
2557
+ writer.leafNode("a:ea", { typeface: props.eastAsianFamily });
2558
+ }
2559
+ if (props.complexScriptFamily) {
2560
+ writer.leafNode("a:cs", { typeface: props.complexScriptFamily });
2561
+ }
2562
+ writer.closeNode();
2563
+ }
2564
+ function writeRawColor(writer, color) {
2565
+ const modifiers = buildRawColorModifiersXml(color);
2566
+ const writeColorNode = (tag, val) => {
2567
+ if (!modifiers) {
2568
+ writer.leafNode(tag, { val });
2569
+ return;
2570
+ }
2571
+ writer.openNode(tag, { val });
2572
+ writer.writeRaw(modifiers);
2573
+ writer.closeNode();
2574
+ };
2575
+ if (color.srgb) {
2576
+ writeColorNode("a:srgbClr", color.srgb);
2577
+ }
2578
+ else if (color.theme !== undefined) {
2579
+ const themeNames = [
2580
+ "dk1",
2581
+ "lt1",
2582
+ "dk2",
2583
+ "lt2",
2584
+ "accent1",
2585
+ "accent2",
2586
+ "accent3",
2587
+ "accent4",
2588
+ "accent5",
2589
+ "accent6",
2590
+ "hlink",
2591
+ "folHlink"
2592
+ ];
2593
+ writeColorNode("a:schemeClr", themeNames[color.theme] ?? "dk1");
2594
+ }
2595
+ else if (color.schemeName) {
2596
+ // Unknown scheme colour tokens (e.g. `phClr`, vendor extensions)
2597
+ // round-trip as `<a:schemeClr>` — keeping the element identity
2598
+ // intact. Previously these fell through to `<a:sysClr>` via the
2599
+ // parser, silently changing the DrawingML colour kind.
2600
+ writeColorNode("a:schemeClr", color.schemeName);
2601
+ }
2602
+ else if (color.sysClr) {
2603
+ writeColorNode("a:sysClr", color.sysClr);
2604
+ }
2605
+ else if (color.prstClr) {
2606
+ writeColorNode("a:prstClr", color.prstClr);
2607
+ }
2608
+ }
2609
+ function writeRawGradientFill(writer, gradient) {
2610
+ if (!Array.isArray(gradient.stops) || gradient.stops.length < 2) {
2611
+ return;
2612
+ }
2613
+ writer.openNode("a:gradFill");
2614
+ writer.openNode("a:gsLst");
2615
+ for (const stop of gradient.stops) {
2616
+ // OOXML `<a:gs pos>` is hundredths of a percent (0–100000). See
2617
+ // the matching fixes in `chart-space-xform.ts` and
2618
+ // `chart-ex-renderer.ts`; the previous `×1000` multiplier was
2619
+ // 100× too small and produced gradients in Excel at wildly
2620
+ // wrong positions.
2621
+ const encoded = Math.max(0, Math.min(100000, Math.round(stop.position * 100000)));
2622
+ writer.openNode("a:gs", { pos: String(encoded) });
2623
+ writeRawColor(writer, stop.color);
2624
+ writer.closeNode();
2625
+ }
2626
+ writer.closeNode();
2627
+ if (gradient.type === "circle" || gradient.type === "rect" || gradient.type === "shape") {
2628
+ // Preserve parsed `fillToRect` focal rectangle when present;
2629
+ // default to Excel's centred form (all components at 50%).
2630
+ // `CT_FillToRectangle` sides are `ST_Percentage`, which permits
2631
+ // negative values (focal point outside the shape). Don't clamp
2632
+ // to `[0, 100000]` — negative focal points were being lost on
2633
+ // round-trip before this fix.
2634
+ const rect = gradient.fillToRect;
2635
+ const pct = (v, def) => {
2636
+ if (v === undefined) {
2637
+ return def;
2638
+ }
2639
+ return Math.round(v * 100000);
2640
+ };
2641
+ writer.openNode("a:path", { path: gradient.type });
2642
+ writer.leafNode("a:fillToRect", {
2643
+ l: String(pct(rect?.left, 50000)),
2644
+ t: String(pct(rect?.top, 50000)),
2645
+ r: String(pct(rect?.right, 50000)),
2646
+ b: String(pct(rect?.bottom, 50000))
2647
+ });
2648
+ writer.closeNode();
2649
+ }
2650
+ else {
2651
+ // Emit `scaled` only when the author explicitly set it; mirrors
2652
+ // the structured ChartEx renderer (chart-ex-renderer.ts line
2653
+ // 4782) so both paths produce the same bytes. Previously this
2654
+ // raw writer unconditionally stamped `scaled="1"`, which
2655
+ // overwrote a parsed `scaled="0"` on round-trip — a visible
2656
+ // drift for gradients with the shape-independent orientation
2657
+ // mode. The OOXML default is `false` per `CT_LinearShadeProperties`,
2658
+ // so omitting it when absent is lossless.
2659
+ const linAttrs = {
2660
+ ang: String(Math.round((gradient.angle ?? 0) * 60000))
2661
+ };
2662
+ if (gradient.scaled !== undefined) {
2663
+ linAttrs.scaled = gradient.scaled ? "1" : "0";
2664
+ }
2665
+ writer.leafNode("a:lin", linAttrs);
2666
+ }
2667
+ writer.closeNode();
2668
+ }
2669
+ function writeRawEffectList(writer, effects) {
2670
+ writer.openNode("a:effectLst");
2671
+ if (effects.blur) {
2672
+ const attrs = {};
2673
+ if (effects.blur.radius !== undefined) {
2674
+ attrs.rad = String(effects.blur.radius);
2675
+ }
2676
+ if (effects.blur.grow !== undefined) {
2677
+ attrs.grow = effects.blur.grow ? "1" : "0";
2678
+ }
2679
+ writer.leafNode("a:blur", attrs);
2680
+ }
2681
+ if (effects.outerShadow) {
2682
+ writeRawShadow(writer, "a:outerShdw", effects.outerShadow);
2683
+ }
2684
+ if (effects.innerShadow) {
2685
+ writeRawShadow(writer, "a:innerShdw", effects.innerShadow);
2686
+ }
2687
+ if (effects.presetShadow) {
2688
+ const ps = effects.presetShadow;
2689
+ const attrs = { prst: ps.preset };
2690
+ if (ps.distance !== undefined) {
2691
+ attrs.dist = String(ps.distance);
2692
+ }
2693
+ if (ps.direction !== undefined) {
2694
+ attrs.dir = String(ps.direction);
2695
+ }
2696
+ writer.openNode("a:prstShdw", attrs);
2697
+ if (ps.color) {
2698
+ writeRawColor(writer, ps.color);
2699
+ }
2700
+ writer.closeNode();
2701
+ }
2702
+ if (effects.glow) {
2703
+ writer.openNode("a:glow", { rad: String(effects.glow.radius) });
2704
+ writeRawColor(writer, effects.glow.color);
2705
+ writer.closeNode();
2706
+ }
2707
+ if (effects.softEdge) {
2708
+ writer.leafNode("a:softEdge", { rad: String(effects.softEdge.radius) });
2709
+ }
2710
+ if (effects.reflection) {
2711
+ const reflection = effects.reflection;
2712
+ const attrs = {};
2713
+ for (const [key, value] of [
2714
+ ["blurRad", reflection.blurRadius],
2715
+ ["stA", reflection.startOpacity],
2716
+ ["stPos", reflection.startPosition],
2717
+ ["endA", reflection.endOpacity],
2718
+ ["endPos", reflection.endPosition],
2719
+ ["dist", reflection.distance],
2720
+ ["dir", reflection.direction],
2721
+ ["fadeDir", reflection.fadeDirection],
2722
+ ["sx", reflection.scaleHorizontal],
2723
+ ["sy", reflection.scaleVertical],
2724
+ ["kx", reflection.skewHorizontal],
2725
+ ["ky", reflection.skewVertical],
2726
+ ["algn", reflection.alignment],
2727
+ ["rotWithShape", reflection.rotateWithShape]
2728
+ ]) {
2729
+ if (value !== undefined) {
2730
+ attrs[key] = typeof value === "boolean" ? (value ? "1" : "0") : String(value);
2731
+ }
2732
+ }
2733
+ writer.leafNode("a:reflection", attrs);
2734
+ }
2735
+ writer.closeNode();
2736
+ }
2737
+ function writeRawShadow(writer, tag, shadow) {
2738
+ const attrs = {};
2739
+ for (const [key, value] of [
2740
+ ["blurRad", shadow.blurRadius],
2741
+ ["dist", shadow.distance],
2742
+ ["dir", shadow.direction],
2743
+ ["algn", shadow.alignment],
2744
+ ["rotWithShape", shadow.rotateWithShape],
2745
+ ["sx", shadow.scaleHorizontal],
2746
+ ["sy", shadow.scaleVertical],
2747
+ ["kx", shadow.skewHorizontal],
2748
+ ["ky", shadow.skewVertical]
2749
+ ]) {
2750
+ if (value !== undefined) {
2751
+ attrs[key] = typeof value === "boolean" ? (value ? "1" : "0") : String(value);
2752
+ }
2753
+ }
2754
+ writer.openNode(tag, attrs);
2755
+ writeRawColor(writer, shadow.color);
2756
+ writer.closeNode();
2757
+ }
2758
+ function writeRawScene3D(writer, scene) {
2759
+ writer.openNode("a:scene3d");
2760
+ if (scene.camera) {
2761
+ const camera = scene.camera;
2762
+ const attrs = { prst: camera.preset };
2763
+ if (camera.fov !== undefined) {
2764
+ attrs.fov = String(camera.fov);
2765
+ }
2766
+ if (camera.zoom !== undefined) {
2767
+ attrs.zoom = String(camera.zoom);
2768
+ }
2769
+ if (camera.rotation) {
2770
+ writer.openNode("a:camera", attrs);
2771
+ writer.leafNode("a:rot", {
2772
+ lat: String(camera.rotation.lat),
2773
+ lon: String(camera.rotation.lon),
2774
+ rev: String(camera.rotation.rev)
2775
+ });
2776
+ writer.closeNode();
2777
+ }
2778
+ else {
2779
+ writer.leafNode("a:camera", attrs);
2780
+ }
2781
+ }
2782
+ if (scene.lightRig) {
2783
+ const lightRig = scene.lightRig;
2784
+ const attrs = { rig: lightRig.rig, dir: lightRig.direction };
2785
+ if (lightRig.rotation) {
2786
+ writer.openNode("a:lightRig", attrs);
2787
+ writer.leafNode("a:rot", {
2788
+ lat: String(lightRig.rotation.lat),
2789
+ lon: String(lightRig.rotation.lon),
2790
+ rev: String(lightRig.rotation.rev)
2791
+ });
2792
+ writer.closeNode();
2793
+ }
2794
+ else {
2795
+ writer.leafNode("a:lightRig", attrs);
2796
+ }
2797
+ }
2798
+ writer.closeNode();
2799
+ }
2800
+ function writeRawSp3D(writer, sp3d) {
2801
+ const attrs = {};
2802
+ if (sp3d.z !== undefined) {
2803
+ attrs.z = String(sp3d.z);
2804
+ }
2805
+ if (sp3d.extrusionHeight !== undefined) {
2806
+ attrs.extrusionH = String(sp3d.extrusionHeight);
2807
+ }
2808
+ if (sp3d.contourWidth !== undefined) {
2809
+ attrs.contourW = String(sp3d.contourWidth);
2810
+ }
2811
+ if (sp3d.material) {
2812
+ attrs.prstMaterial = sp3d.material;
2813
+ }
2814
+ const hasChildren = !!(sp3d.bevelTop ||
2815
+ sp3d.bevelBottom ||
2816
+ sp3d.extrusionColor ||
2817
+ sp3d.contourColor);
2818
+ if (!hasChildren) {
2819
+ writer.leafNode("a:sp3d", attrs);
2820
+ return;
2821
+ }
2822
+ writer.openNode("a:sp3d", attrs);
2823
+ if (sp3d.bevelTop) {
2824
+ writeRawBevel(writer, "a:bevelT", sp3d.bevelTop);
2825
+ }
2826
+ if (sp3d.bevelBottom) {
2827
+ writeRawBevel(writer, "a:bevelB", sp3d.bevelBottom);
2828
+ }
2829
+ if (sp3d.extrusionColor) {
2830
+ writer.openNode("a:extrusionClr");
2831
+ writeRawColor(writer, sp3d.extrusionColor);
2832
+ writer.closeNode();
2833
+ }
2834
+ if (sp3d.contourColor) {
2835
+ writer.openNode("a:contourClr");
2836
+ writeRawColor(writer, sp3d.contourColor);
2837
+ writer.closeNode();
2838
+ }
2839
+ writer.closeNode();
2840
+ }
2841
+ function writeRawBevel(writer, tag, bevel) {
2842
+ const attrs = {};
2843
+ if (bevel.width !== undefined) {
2844
+ attrs.w = String(bevel.width);
2845
+ }
2846
+ if (bevel.height !== undefined) {
2847
+ attrs.h = String(bevel.height);
2848
+ }
2849
+ if (bevel.preset) {
2850
+ attrs.prst = bevel.preset;
2851
+ }
2852
+ writer.leafNode(tag, attrs);
2853
+ }
2854
+ function buildRawColorModifiersXml(color) {
2855
+ // Each modifier must serialise as `<a:* val="N"/>` where `N` is a
2856
+ // valid `xsd:int`. Previously the raw patcher interpolated model
2857
+ // values directly, so `NaN` / `Infinity` / unrounded floats leaked
2858
+ // into the attribute and Excel's strict reader rejected the file
2859
+ // with "invalid attribute value for xs:int". The structured renderer
2860
+ // (`renderColorModifiers` in chart-ex-renderer.ts) guards with
2861
+ // `Number.isFinite` + `Math.round` — mirror that here so both write
2862
+ // paths produce identical bytes, then share the helper.
2863
+ const parts = [];
2864
+ const emitInt = (tag, value) => {
2865
+ if (value === undefined || !Number.isFinite(value)) {
2866
+ return;
2867
+ }
2868
+ parts.push(`<a:${tag} val="${Math.round(value)}"/>`);
2869
+ };
2870
+ emitInt("alpha", color.alpha);
2871
+ // `tint` on the public `ChartColor` is a 0..1 fraction; convert to
2872
+ // the DrawingML 0..100000 per-thousand integer here. DrawingML also
2873
+ // permits NEGATIVE tint (shade toward black) per
2874
+ // `CT_PositiveFixedPercentage` — the structured path preserves the
2875
+ // sign, so we do too.
2876
+ if (color.tint !== undefined && Number.isFinite(color.tint)) {
2877
+ parts.push(`<a:tint val="${Math.round(color.tint * 100000)}"/>`);
2878
+ }
2879
+ emitInt("shade", color.shade);
2880
+ emitInt("satMod", color.satMod);
2881
+ emitInt("lumMod", color.lumMod);
2882
+ emitInt("lumOff", color.lumOff);
2883
+ return parts.join("");
2884
+ }
2885
+ function patchRawChartExSeries(raw, chart, patchPlan) {
2886
+ const seriesModels = extractChartExSeries({ chartSpace: { chart } });
2887
+ let index = 0;
2888
+ return replaceXmlBlocks(raw, "cx:series", block => {
2889
+ const series = seriesModels[index++];
2890
+ const seriesPlan = getRawPatchListItem(patchPlan.series, index - 1);
2891
+ return series && seriesPlan ? patchRawChartExSeriesBlock(block, series, seriesPlan) : block;
2892
+ });
2893
+ }
2894
+ function patchRawChartExSeriesBlock(block, series, patchPlan) {
2895
+ // Child sequence per Chart2014 `CT_Series`:
2896
+ //
2897
+ // tx? → spPr? → txPr? → valueColors? → valueColorPositions? →
2898
+ // dataPt* → dataLabels? → dataId* → layoutPr? → axisId* → extLst?
2899
+ //
2900
+ // The sibling arrays below describe the elements that must come
2901
+ // AFTER the element being inserted so `replaceOrInsertBeforeGeneric`
2902
+ // can splice into the right position. Previous versions used
2903
+ // sibling lists that put `dataId` before `dataLabels` / `dataPt` —
2904
+ // reversing the schema order and producing files strict validators
2905
+ // reject. Use the real schema order so raw-patch output matches
2906
+ // what `renderSeries` produces for the same model.
2907
+ const afterTx = [
2908
+ "cx:spPr",
2909
+ "cx:txPr",
2910
+ "cx:valueColors",
2911
+ "cx:valueColorPositions",
2912
+ "cx:dataPt",
2913
+ "cx:dataLabels",
2914
+ "cx:dataId",
2915
+ "cx:layoutPr",
2916
+ "cx:axisId",
2917
+ "cx:extLst"
2918
+ ];
2919
+ const afterSpPr = [
2920
+ "cx:txPr",
2921
+ "cx:valueColors",
2922
+ "cx:valueColorPositions",
2923
+ "cx:dataPt",
2924
+ "cx:dataLabels",
2925
+ "cx:dataId",
2926
+ "cx:layoutPr",
2927
+ "cx:axisId",
2928
+ "cx:extLst"
2929
+ ];
2930
+ const afterDataPt = ["cx:dataLabels", "cx:dataId", "cx:layoutPr", "cx:axisId", "cx:extLst"];
2931
+ const afterDataLabels = ["cx:dataId", "cx:layoutPr", "cx:axisId", "cx:extLst"];
2932
+ const afterDataId = ["cx:layoutPr", "cx:axisId", "cx:extLst"];
2933
+ const afterLayoutPr = ["cx:axisId", "cx:extLst"];
2934
+ const afterAxisId = ["cx:extLst"];
2935
+ let patched = block;
2936
+ if (rawPatchFlag(patchPlan, "hidden")) {
2937
+ patched = patchOpeningTagBooleanAttribute(patched, "cx:series", "hidden", series.hidden);
2938
+ }
2939
+ if (rawPatchFlag(patchPlan, "ownerIdx")) {
2940
+ patched = patchOpeningTagIntegerAttribute(patched, "cx:series", "ownerIdx", series.ownerIdx);
2941
+ }
2942
+ if (rawPatchFlag(patchPlan, "tx")) {
2943
+ patched = patchGenericChild(patched, "cx:tx", buildRawChartExSeriesTxXml(series.tx), afterTx, "cx:series");
2944
+ }
2945
+ if (rawPatchFlag(patchPlan, "spPr")) {
2946
+ patched = patchGenericChild(patched, "cx:spPr", buildRawShapePropertiesXml(series.spPr, "cx"), afterSpPr, "cx:series");
2947
+ }
2948
+ if (rawPatchFlag(patchPlan, "dataPoints")) {
2949
+ const dataPointXml = (series.dataPt ?? [])
2950
+ .map((point) => {
2951
+ const spPrXml = buildRawShapePropertiesXml(point.spPr, "cx") ?? "";
2952
+ return `<cx:dataPt idx="${point.idx}">${spPrXml}</cx:dataPt>`;
2953
+ })
2954
+ .join("");
2955
+ patched = patchRepeatingChildren(patched, "cx:dataPt", dataPointXml, afterDataPt, "cx:series");
2956
+ }
2957
+ if (rawPatchFlag(patchPlan, "dataLabels")) {
2958
+ patched = patchGenericChild(patched, "cx:dataLabels", buildRawChartExDataLabelsXml(series.dataLabels), afterDataLabels, "cx:series");
2959
+ }
2960
+ if (rawPatchFlag(patchPlan, "dataRefs")) {
2961
+ const dataRefsXml = (series.dataRefs ?? [])
2962
+ .map((ref) => ref.dataId !== undefined
2963
+ ? `<cx:dataId val="${ref.dataId}"/>`
2964
+ : ref.axisId !== undefined
2965
+ ? `<cx:axisId val="${ref.axisId}"/>`
2966
+ : "")
2967
+ .join("");
2968
+ patched = patchRepeatingChildren(patched, "cx:dataId", dataRefsXml, afterDataId, "cx:series");
2969
+ }
2970
+ if (rawPatchFlag(patchPlan, "layoutPr")) {
2971
+ patched = patchGenericChild(patched, "cx:layoutPr", buildRawChartExLayoutPropertiesXml(series.layoutId, series.layoutPr), afterLayoutPr, "cx:series");
2972
+ }
2973
+ if (rawPatchFlag(patchPlan, "axisId")) {
2974
+ const axisIdsXml = (series.axisId ?? [])
2975
+ .map((id) => `<cx:axisId val="${id}"/>`)
2976
+ .join("");
2977
+ patched = patchRepeatingChildren(patched, "cx:axisId", axisIdsXml, afterAxisId, "cx:series");
2978
+ }
2979
+ return patched;
2980
+ }
2981
+ function buildRawChartExSeriesTxXml(tx) {
2982
+ if (!tx) {
2983
+ return "";
2984
+ }
2985
+ if (tx.rich) {
2986
+ // Round-trip parity with the structured writer — ChartEx series
2987
+ // names authored as rich text (per-run formatting, bold / colour
2988
+ // / font-family overrides) used to be silently dropped by the raw
2989
+ // patcher: only `tx.value` and `tx.strRef` were handled, so a
2990
+ // mutation that preserved `tx.rich` on the model would re-emit
2991
+ // `<cx:tx/>` without a `<cx:rich>` child, collapsing the label to
2992
+ // an unstyled placeholder. Emit a minimal `<cx:tx><cx:rich>…`
2993
+ // subtree carrying the paragraph / run structure. The rPr helper
2994
+ // is a pragmatic subset (size / bold / italic / color) — features
2995
+ // beyond that flight through the structured path, which is the
2996
+ // default when `preferRawPatch` isn't opt-in.
2997
+ return `<cx:tx>${buildRawChartExRichTextXml(tx.rich)}</cx:tx>`;
2998
+ }
2999
+ if (tx.value !== undefined) {
3000
+ return `<cx:tx><cx:txData><cx:v>${escapeXml(String(tx.value))}</cx:v></cx:txData></cx:tx>`;
3001
+ }
3002
+ if (tx.strRef !== undefined) {
3003
+ // `tx.strRef` is declared as `string | { formula: string; cached?: string }`
3004
+ // on `ChartExSeries.tx`. The previous writer coerced via
3005
+ // `String(tx.strRef)`, which produced the literal `"[object Object]"`
3006
+ // for the structured form — silently corrupting the formula on every
3007
+ // series that carried a `{ formula, cached }` pair through the raw
3008
+ // patch path.
3009
+ let formula;
3010
+ let cached;
3011
+ if (typeof tx.strRef === "string") {
3012
+ formula = tx.strRef;
3013
+ }
3014
+ else if (tx.strRef &&
3015
+ typeof tx.strRef === "object" &&
3016
+ typeof tx.strRef.formula === "string") {
3017
+ formula = tx.strRef.formula;
3018
+ cached = typeof tx.strRef.cached === "string" ? tx.strRef.cached : undefined;
3019
+ }
3020
+ else {
3021
+ // Degenerate shape (unknown form) — drop the element rather than
3022
+ // emit `<cx:f>[object Object]</cx:f>` and corrupt the formula.
3023
+ return "";
3024
+ }
3025
+ const cachedEl = cached !== undefined ? `<cx:v>${escapeXml(cached)}</cx:v>` : "";
3026
+ return `<cx:tx><cx:txData><cx:f>${escapeXml(formula)}</cx:f>${cachedEl}</cx:txData></cx:tx>`;
3027
+ }
3028
+ return "";
3029
+ }
3030
+ /**
3031
+ * Minimal `<cx:rich>` emitter used by the ChartEx raw patcher when a
3032
+ * series `tx` carries a `rich` paragraph tree. Mirrors the structured
3033
+ * renderer's output shape (`renderRichText` in `chart-ex-renderer`)
3034
+ * for the attributes the raw patch path needs — size / bold / italic
3035
+ * and the text colour — so round-trip parity is preserved for the
3036
+ * common "bold label" case. Features outside this subset (mixed font
3037
+ * families, east-Asian runs, paragraph properties) flow through the
3038
+ * structured writer, which the mutation helper invokes by default;
3039
+ * `preferRawPatch` callers who need the full set should stay on
3040
+ * structural rebuilds.
3041
+ */
3042
+ function buildRawChartExRichTextXml(rich) {
3043
+ if (!rich || !Array.isArray(rich.paragraphs)) {
3044
+ return "";
3045
+ }
3046
+ const parts = ["<cx:rich>", "<a:bodyPr/>", "<a:lstStyle/>"];
3047
+ for (const p of rich.paragraphs) {
3048
+ parts.push("<a:p>");
3049
+ for (const run of p.runs ?? []) {
3050
+ const rPr = buildRawChartExRunPropertiesXml(run.properties);
3051
+ // Preserve significant whitespace — matches the structured
3052
+ // writer's `xml:space="preserve"` rule (see `needsXmlSpacePreserve`).
3053
+ const text = typeof run.text === "string" ? run.text : "";
3054
+ const needsPreserve = /^\s|\s$|[\t\n\r]/.test(text);
3055
+ const tAttrs = needsPreserve ? ' xml:space="preserve"' : "";
3056
+ parts.push(`<a:r>${rPr}<a:t${tAttrs}>${escapeXml(text)}</a:t></a:r>`);
3057
+ }
3058
+ parts.push('<a:endParaRPr lang="en-US"/>');
3059
+ parts.push("</a:p>");
3060
+ }
3061
+ parts.push("</cx:rich>");
3062
+ return parts.join("");
3063
+ }
3064
+ function buildRawChartExRunPropertiesXml(props) {
3065
+ if (!props || typeof props !== "object") {
3066
+ return "";
3067
+ }
3068
+ const attrs = [];
3069
+ if (typeof props.size === "number" && Number.isFinite(props.size)) {
3070
+ attrs.push(`sz="${props.size}"`);
3071
+ }
3072
+ if (props.bold !== undefined) {
3073
+ attrs.push(`b="${props.bold ? 1 : 0}"`);
3074
+ }
3075
+ if (props.italic !== undefined) {
3076
+ attrs.push(`i="${props.italic ? 1 : 0}"`);
3077
+ }
3078
+ // Inline colour child only — the full `<a:solidFill>` emitter is
3079
+ // intentionally out of scope for the raw patcher (structural
3080
+ // rebuild handles anything beyond srgbClr / theme).
3081
+ const color = props.color;
3082
+ let colorChild = "";
3083
+ if (color && typeof color === "object") {
3084
+ if (typeof color.srgb === "string") {
3085
+ colorChild = `<a:solidFill><a:srgbClr val="${escapeAttr(color.srgb)}"/></a:solidFill>`;
3086
+ }
3087
+ else if (typeof color.theme === "number") {
3088
+ // `color.theme` is a 0-based index into the workbook's theme
3089
+ // palette — 0..3 are bg/lt1/dk2/lt2, 4..9 are accent1..accent6,
3090
+ // 10..11 are hlink / folHlink. The previous implementation
3091
+ // emitted `accent${color.theme}`, which produced nonsense
3092
+ // (`accent4` for `theme=4` instead of `accent1`; `accent0` for
3093
+ // `theme=0` which is not even a valid DrawingML scheme slot).
3094
+ // Route through the canonical helper shared with the
3095
+ // structural emitters so the mapping stays in one place.
3096
+ colorChild = `<a:solidFill><a:schemeClr val="${escapeAttr(getChartSupport().themeIndexToName(color.theme))}"/></a:solidFill>`;
3097
+ }
3098
+ }
3099
+ if (attrs.length === 0 && !colorChild) {
3100
+ return "";
3101
+ }
3102
+ const attrStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
3103
+ return colorChild ? `<a:rPr${attrStr}>${colorChild}</a:rPr>` : `<a:rPr${attrStr}/>`;
3104
+ }
3105
+ function buildRawChartExLayoutPropertiesXml(layoutId, layoutPr) {
3106
+ if (!layoutPr) {
3107
+ return "";
3108
+ }
3109
+ if (layoutPr._rawXml && !hasStructuredChartExLayoutProperties(layoutPr)) {
3110
+ return layoutPr._rawXml;
3111
+ }
3112
+ const parts = ["<cx:layoutPr>"];
3113
+ if (layoutPr.parentLabelLayout && (layoutId === "sunburst" || layoutId === "treemap")) {
3114
+ parts.push(`<cx:parentLabelLayout val="${escapeAttr(layoutPr.parentLabelLayout)}"/>`);
3115
+ }
3116
+ if (layoutPr.subtotals && layoutId === "waterfall") {
3117
+ parts.push("<cx:subtotals>");
3118
+ for (const subtotal of layoutPr.subtotals) {
3119
+ parts.push(`<cx:subtotal idx="${subtotal.idx}"/>`);
3120
+ }
3121
+ parts.push("</cx:subtotals>");
3122
+ }
3123
+ if (layoutId === "waterfall" && layoutPr.connectorLines !== undefined) {
3124
+ parts.push(`<cx:connectorLines val="${layoutPr.connectorLines ? "1" : "0"}"/>`);
3125
+ }
3126
+ if (layoutPr.binning) {
3127
+ const binning = layoutPr.binning;
3128
+ const attrs = [
3129
+ binning.intervalClosed === "l" || binning.intervalClosed === "r"
3130
+ ? `intervalClosed="${escapeAttr(binning.intervalClosed)}"`
3131
+ : undefined,
3132
+ binning.underflow !== undefined && Number.isFinite(binning.underflow)
3133
+ ? `underflow="${binning.underflow}"`
3134
+ : undefined,
3135
+ binning.overflow !== undefined && Number.isFinite(binning.overflow)
3136
+ ? `overflow="${binning.overflow}"`
3137
+ : undefined
3138
+ ].filter((attr) => !!attr);
3139
+ parts.push(`<cx:binning${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>`);
3140
+ // CT_Binning schema order: choice(auto|categories|manual)
3141
+ // followed by optional binSize and binCount. Previously the raw
3142
+ // patcher emitted `<cx:auto/>` then `<cx:binSize/>` then
3143
+ // `<cx:binCount/>` then `<cx:categories/>` / `<cx:manual/>` — but
3144
+ // `categories`/`manual` are mutually exclusive with `auto`, and
3145
+ // emitting them after binSize/binCount puts them out of the
3146
+ // schema sequence. The parser's priority chain at
3147
+ // `chart-ex-parser.ts:parseLayoutProperties` resolves
3148
+ // auto > categories > manual, so the stray trailing elements
3149
+ // never round-tripped back anyway. Mirror the structured
3150
+ // renderer's order: one discriminator first, then the numeric
3151
+ // children.
3152
+ if (binning.binType === "auto") {
3153
+ parts.push("<cx:auto/>");
3154
+ }
3155
+ else if (binning.binType === "categories") {
3156
+ parts.push("<cx:categories/>");
3157
+ }
3158
+ else if (binning.binType === "manual") {
3159
+ parts.push("<cx:manual/>");
3160
+ }
3161
+ if (binning.binSize !== undefined && Number.isFinite(binning.binSize)) {
3162
+ parts.push(`<cx:binSize val="${binning.binSize}"/>`);
3163
+ }
3164
+ if (binning.binCount !== undefined && Number.isFinite(binning.binCount)) {
3165
+ parts.push(`<cx:binCount val="${binning.binCount}"/>`);
3166
+ }
3167
+ parts.push("</cx:binning>");
3168
+ }
3169
+ // `paretoLine` is only a valid child when the enclosing layout is a
3170
+ // pareto (clusteredColumn with pareto overlay, or the standalone
3171
+ // `paretoLine` layoutId). Emit the explicit boolean — including
3172
+ // `false` — so round-trip of user-suppressed pareto overlays
3173
+ // matches the structured writer. Previously `if (layoutPr.paretoLine)`
3174
+ // silently dropped the `false` case, re-enabling the line on save.
3175
+ if (layoutPr.paretoLine !== undefined &&
3176
+ (layoutId === "clusteredColumn" || layoutId === "paretoLine")) {
3177
+ parts.push(`<cx:paretoLine val="${layoutPr.paretoLine ? "1" : "0"}"/>`);
3178
+ }
3179
+ if (layoutId === "boxWhisker") {
3180
+ for (const [name, value] of [
3181
+ ["quartileMethod", layoutPr.quartileMethod],
3182
+ ["showMeanLine", layoutPr.showMeanLine],
3183
+ ["showMeanMarker", layoutPr.showMeanMarker],
3184
+ ["showInnerPoints", layoutPr.showInnerPoints],
3185
+ ["showOutlierPoints", layoutPr.showOutlierPoints]
3186
+ ]) {
3187
+ if (value !== undefined) {
3188
+ parts.push(`<cx:${name} val="${typeof value === "boolean" ? (value ? "1" : "0") : escapeAttr(String(value))}"/>`);
3189
+ }
3190
+ }
3191
+ }
3192
+ if (layoutId === "regionMap") {
3193
+ for (const [name, value] of [
3194
+ ["projection", layoutPr.projection],
3195
+ ["regionLabels", layoutPr.regionLabels],
3196
+ ["geoMappingLevel", layoutPr.geoMappingLevel]
3197
+ ]) {
3198
+ if (value !== undefined) {
3199
+ parts.push(`<cx:${name} val="${escapeAttr(String(value))}"/>`);
3200
+ }
3201
+ }
3202
+ }
3203
+ if (layoutPr.extLst) {
3204
+ parts.push(layoutPr.extLst);
3205
+ }
3206
+ parts.push("</cx:layoutPr>");
3207
+ return parts.join("");
3208
+ }
3209
+ function hasStructuredChartExLayoutProperties(layoutPr) {
3210
+ // `increaseSpPr` / `decreaseSpPr` / `totalSpPr` are **preview-only**
3211
+ // fields consumed by the SVG/PDF renderer to colour waterfall bars;
3212
+ // Chart2014 has no schema slot for them (per-point styling lives on
3213
+ // `<cx:dataPt>` instead). Do NOT treat setting one as a "structured
3214
+ // mutation" — doing so would force the raw patcher onto the
3215
+ // structured rebuild path and discard `_rawXml`, silently dropping
3216
+ // every other property the raw bytes carried. The structured
3217
+ // renderer (`hasStructuredLayoutProperties` in chart-ex-renderer.ts)
3218
+ // uses the same exclusion list; keeping the two helpers in sync
3219
+ // prevents asymmetric behaviour between raw-patch and rebuild.
3220
+ return [
3221
+ layoutPr.parentLabelLayout,
3222
+ layoutPr.subtotals,
3223
+ layoutPr.connectorLines,
3224
+ layoutPr.binning,
3225
+ layoutPr.paretoLine,
3226
+ layoutPr.quartileMethod,
3227
+ layoutPr.showMeanLine,
3228
+ layoutPr.showMeanMarker,
3229
+ layoutPr.showInnerPoints,
3230
+ layoutPr.showOutlierPoints,
3231
+ layoutPr.projection,
3232
+ layoutPr.regionLabels,
3233
+ layoutPr.geoMappingLevel
3234
+ ].some(value => value !== undefined);
3235
+ }
3236
+ function buildRawChartExDataLabelsXml(dataLabels) {
3237
+ if (!dataLabels) {
3238
+ return "";
3239
+ }
3240
+ const parts = ["<cx:dataLabels>"];
3241
+ if (dataLabels.visibility) {
3242
+ const attrs = [
3243
+ dataLabels.visibility.seriesName !== undefined
3244
+ ? `seriesName="${dataLabels.visibility.seriesName ? "1" : "0"}"`
3245
+ : undefined,
3246
+ dataLabels.visibility.categoryName !== undefined
3247
+ ? `categoryName="${dataLabels.visibility.categoryName ? "1" : "0"}"`
3248
+ : undefined,
3249
+ dataLabels.visibility.value !== undefined
3250
+ ? `value="${dataLabels.visibility.value ? "1" : "0"}"`
3251
+ : undefined,
3252
+ dataLabels.visibility.numFmt !== undefined
3253
+ ? `numFmt="${dataLabels.visibility.numFmt ? "1" : "0"}"`
3254
+ : undefined
3255
+ ].filter((attr) => !!attr);
3256
+ parts.push(`<cx:visibility ${attrs.join(" ")}/>`);
3257
+ }
3258
+ if (dataLabels.position) {
3259
+ parts.push(`<cx:dataLabel pos="${escapeAttr(dataLabels.position)}"/>`);
3260
+ }
3261
+ if (dataLabels.separator) {
3262
+ parts.push(`<cx:separator>${escapeXml(String(dataLabels.separator))}</cx:separator>`);
3263
+ }
3264
+ if (dataLabels.numFmt) {
3265
+ parts.push(`<cx:numFmt formatCode="${escapeAttr(String(dataLabels.numFmt))}"/>`);
3266
+ }
3267
+ if (dataLabels.spPr) {
3268
+ parts.push(buildRawShapePropertiesXml(dataLabels.spPr, "cx") ?? "");
3269
+ }
3270
+ if (dataLabels.txPr) {
3271
+ parts.push(buildRawTextPropertiesXml(dataLabels.txPr, "cx") ?? "");
3272
+ }
3273
+ parts.push("</cx:dataLabels>");
3274
+ return parts.join("");
3275
+ }
3276
+ function patchRawChartExAxes(raw, chart, patchPlan) {
3277
+ let patched = raw;
3278
+ for (const [index, axis] of (chart.plotArea?.axis ?? []).entries()) {
3279
+ const axisPlan = getRawPatchListItem(patchPlan, index);
3280
+ if (!axisPlan) {
3281
+ continue;
3282
+ }
3283
+ const range = findChartExAxisBlock(patched, axis.axisId);
3284
+ if (!range) {
3285
+ return undefined;
3286
+ }
3287
+ const axisXml = patchRawChartExAxisBlock(range.xml, axis, axisPlan);
3288
+ patched = patched.slice(0, range.start) + axisXml + patched.slice(range.end);
3289
+ }
3290
+ return patched;
3291
+ }
3292
+ function patchRawChartExAxisBlock(block, axis, patchPlan) {
3293
+ // `CT_Axis` child sequence (Chart2014):
3294
+ //
3295
+ // (catScaling | valScaling) → title → units →
3296
+ // majorTickMarks → minorTickMarks →
3297
+ // majorGridlines → minorGridlines →
3298
+ // numFmt → txPr → spPr → extLst
3299
+ //
3300
+ // (The structured renderer emits `txPr` before `spPr` to match
3301
+ // Excel's real output; some schema mirrors put spPr first, but
3302
+ // Excel itself serialises txPr first and readers accept both. The
3303
+ // raw patcher mirrors the structured renderer so both paths land
3304
+ // byte-identical XML for the same model.)
3305
+ //
3306
+ // Sibling lists describe every element that must come AFTER the
3307
+ // element being inserted. Older versions of the patcher used
3308
+ // sibling arrays that put `majorTickMarks` before
3309
+ // `title`/`valScaling`/`catScaling`, inverting the schema — strict
3310
+ // validators rejected the output and Excel's own reader silently
3311
+ // dropped whichever element landed out of position.
3312
+ const afterScaling = [
3313
+ "cx:title",
3314
+ "cx:units",
3315
+ "cx:majorTickMarks",
3316
+ "cx:majorTickMark",
3317
+ "cx:minorTickMarks",
3318
+ "cx:minorTickMark",
3319
+ "cx:majorGridlines",
3320
+ "cx:minorGridlines",
3321
+ "cx:numFmt",
3322
+ "cx:txPr",
3323
+ "cx:spPr",
3324
+ "cx:extLst"
3325
+ ];
3326
+ const afterTitle = [
3327
+ "cx:units",
3328
+ "cx:majorTickMarks",
3329
+ "cx:majorTickMark",
3330
+ "cx:minorTickMarks",
3331
+ "cx:minorTickMark",
3332
+ "cx:majorGridlines",
3333
+ "cx:minorGridlines",
3334
+ "cx:numFmt",
3335
+ "cx:txPr",
3336
+ "cx:spPr",
3337
+ "cx:extLst"
3338
+ ];
3339
+ const afterMajorTicks = [
3340
+ "cx:minorTickMarks",
3341
+ "cx:minorTickMark",
3342
+ "cx:majorGridlines",
3343
+ "cx:minorGridlines",
3344
+ "cx:numFmt",
3345
+ "cx:txPr",
3346
+ "cx:spPr",
3347
+ "cx:extLst"
3348
+ ];
3349
+ const afterMinorTicks = [
3350
+ "cx:majorGridlines",
3351
+ "cx:minorGridlines",
3352
+ "cx:numFmt",
3353
+ "cx:txPr",
3354
+ "cx:spPr",
3355
+ "cx:extLst"
3356
+ ];
3357
+ const afterNumFmt = ["cx:txPr", "cx:spPr", "cx:extLst"];
3358
+ const afterTxPr = ["cx:spPr", "cx:extLst"];
3359
+ const afterSpPr = ["cx:extLst"];
3360
+ let patched = block;
3361
+ if (rawPatchFlag(patchPlan, "hidden")) {
3362
+ // `CT_Axis/@hidden` is an **attribute** on the opening `<cx:axis>`
3363
+ // tag per ECMA-376 Chart2014, not a child element. Previously
3364
+ // this raw-patch path emitted `<cx:hidden val="1"/>` as a child,
3365
+ // which strict validators reject. Replay the mutation as an
3366
+ // attribute tweak on the opening tag. When `axis.hidden` is
3367
+ // `undefined` the attribute is removed entirely; explicit `false`
3368
+ // lands `hidden="0"` so files that carried an affirmative
3369
+ // visibility marker round-trip byte-identically.
3370
+ patched = patchXmlAttribute(patched, "cx:axis", "hidden", axis.hidden);
3371
+ // Clean up any stale child `<cx:hidden/>` bytes left over from
3372
+ // legacy output that predated the attribute rewrite — the parser
3373
+ // accepts both forms (see `chart-ex-parser.ts:parseAxis`), so
3374
+ // round-tripping an older file must eliminate the legacy form.
3375
+ patched = removeXmlBlock(patched, "cx:hidden");
3376
+ }
3377
+ if (rawPatchFlag(patchPlan, "valScaling")) {
3378
+ patched = patchGenericChild(patched, "cx:valScaling", buildRawChartExScalingXml("valScaling", axis.valScaling), afterScaling, "cx:axis");
3379
+ }
3380
+ if (rawPatchFlag(patchPlan, "catScaling")) {
3381
+ patched = patchGenericChild(patched, "cx:catScaling", buildRawChartExScalingXml("catScaling", axis.catScaling), afterScaling, "cx:axis");
3382
+ }
3383
+ if (rawPatchFlag(patchPlan, "title")) {
3384
+ if (axis.title) {
3385
+ const text = axis.title.text?.paragraphs?.[0]?.runs?.[0]?.text;
3386
+ if (text !== undefined) {
3387
+ patched = patchGenericChild(patched, "cx:title", buildRawChartExTitleXml(text), afterTitle, "cx:axis");
3388
+ }
3389
+ }
3390
+ else {
3391
+ patched = removeXmlBlock(patched, "cx:title");
3392
+ }
3393
+ }
3394
+ if (rawPatchFlag(patchPlan, "majorTickMark")) {
3395
+ // `cx:majorTickMark` in the Chart2014 schema is the **plural**
3396
+ // `majorTickMarks`. Earlier versions of this library emitted the
3397
+ // classic-chart singular form; the raw patcher now always lands
3398
+ // the plural, and strips any stale singular leftover so repeated
3399
+ // patches don't duplicate the element.
3400
+ patched = removeXmlBlock(patched, "cx:majorTickMark");
3401
+ patched = patchValueLeaf(patched, "cx:majorTickMarks", axis.majorTickMark, afterMajorTicks, "cx:axis");
3402
+ }
3403
+ if (rawPatchFlag(patchPlan, "minorTickMark")) {
3404
+ // Plural form — see `majorTickMark` note above.
3405
+ patched = removeXmlBlock(patched, "cx:minorTickMark");
3406
+ patched = patchValueLeaf(patched, "cx:minorTickMarks", axis.minorTickMark, afterMinorTicks, "cx:axis");
3407
+ }
3408
+ if (rawPatchFlag(patchPlan, "numFmt")) {
3409
+ patched = patchGenericChild(patched, "cx:numFmt", buildRawChartExNumFmtXml(axis.numFmt), afterNumFmt, "cx:axis");
3410
+ }
3411
+ if (rawPatchFlag(patchPlan, "txPr")) {
3412
+ patched = patchGenericChild(patched, "cx:txPr", buildRawTextPropertiesXml(axis.txPr, "cx"), afterTxPr, "cx:axis");
3413
+ }
3414
+ if (rawPatchFlag(patchPlan, "spPr")) {
3415
+ patched = patchGenericChild(patched, "cx:spPr", buildRawShapePropertiesXml(axis.spPr, "cx"), afterSpPr, "cx:axis");
3416
+ }
3417
+ return patched;
3418
+ }
3419
+ function buildRawChartExNumFmtXml(numFmt) {
3420
+ if (!numFmt?.formatCode) {
3421
+ return "";
3422
+ }
3423
+ const attrs = [`formatCode="${escapeAttr(numFmt.formatCode)}"`];
3424
+ if (numFmt.sourceLinked !== undefined) {
3425
+ attrs.push(`sourceLinked="${numFmt.sourceLinked ? "1" : "0"}"`);
3426
+ }
3427
+ return `<cx:numFmt ${attrs.join(" ")}/>`;
3428
+ }
3429
+ function buildRawChartExScalingXml(tag, scaling) {
3430
+ if (!scaling) {
3431
+ return "";
3432
+ }
3433
+ const attrs = Object.entries(scaling)
3434
+ .filter(([, value]) => value !== undefined)
3435
+ .map(([key, value]) => `${key}="${escapeAttr(String(value))}"`);
3436
+ return `<cx:${tag}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}/>`;
3437
+ }
3438
+ function findChartExAxisBlock(raw, axisId) {
3439
+ let cursor = 0;
3440
+ while (cursor < raw.length) {
3441
+ const range = findXmlBlock(raw, "cx:axis", cursor);
3442
+ if (!range) {
3443
+ return undefined;
3444
+ }
3445
+ const xml = raw.slice(range.start, range.end);
3446
+ if (new RegExp(`<cx:axis\\s+[^>]*id=["']${axisId}["']`).test(xml)) {
3447
+ return { ...range, xml };
3448
+ }
3449
+ cursor = range.end;
3450
+ }
3451
+ return undefined;
3452
+ }
3453
+ function replaceOrInsertBefore(block, tag, replacement, beforeTags) {
3454
+ const range = findXmlBlock(block, tag);
3455
+ if (range) {
3456
+ return block.slice(0, range.start) + replacement + block.slice(range.end);
3457
+ }
3458
+ const insertAt = beforeTags
3459
+ .map(t => block.indexOf(`<${t}`))
3460
+ .filter(i => i >= 0)
3461
+ .sort((a, b) => a - b)[0];
3462
+ if (insertAt !== undefined) {
3463
+ return block.slice(0, insertAt) + replacement + block.slice(insertAt);
3464
+ }
3465
+ const close = block.lastIndexOf("</c:ser>") >= 0
3466
+ ? block.lastIndexOf("</c:ser>")
3467
+ : block.lastIndexOf("</c:catAx>");
3468
+ return close >= 0 ? block.slice(0, close) + replacement + block.slice(close) : block;
3469
+ }
3470
+ function replaceOrInsertBeforeGeneric(block, tag, replacement, beforeTags, parentTag) {
3471
+ const range = findXmlBlock(block, tag);
3472
+ if (range) {
3473
+ return block.slice(0, range.start) + replacement + block.slice(range.end);
3474
+ }
3475
+ const insertAt = beforeTags
3476
+ .map(t => block.indexOf(`<${t}`))
3477
+ .filter(i => i >= 0)
3478
+ .sort((a, b) => a - b)[0];
3479
+ if (insertAt !== undefined) {
3480
+ return block.slice(0, insertAt) + replacement + block.slice(insertAt);
3481
+ }
3482
+ const close = block.lastIndexOf(`</${parentTag}>`);
3483
+ return close >= 0 ? block.slice(0, close) + replacement + block.slice(close) : block;
3484
+ }
3485
+ function patchGenericChild(block, tag, replacement, beforeTags, parentTag) {
3486
+ if (replacement === undefined || replacement === "") {
3487
+ return removeXmlBlock(block, tag);
3488
+ }
3489
+ return replaceOrInsertBeforeGeneric(block, tag, replacement, beforeTags, parentTag);
3490
+ }
3491
+ function patchOpeningTagBooleanAttribute(block, tag, attr, value) {
3492
+ const openEnd = block.indexOf(">");
3493
+ if (openEnd < 0 || !block.startsWith(`<${tag}`)) {
3494
+ return block;
3495
+ }
3496
+ const head = block
3497
+ .slice(0, openEnd + 1)
3498
+ .replace(new RegExp(`\\s${attr}=("[^"]*"|'[^']*')`, "g"), "");
3499
+ if (value === undefined) {
3500
+ return head + block.slice(openEnd + 1);
3501
+ }
3502
+ // Emit an explicit `val="0"` / `val="1"` for both boolean states so
3503
+ // the raw patch path matches the structured renderer (which emits
3504
+ // `hidden="0"` on `<cx:series>` when the author set `hidden:
3505
+ // false`). Previously the `false` case dropped the attribute
3506
+ // entirely — technically equivalent to the schema default, but
3507
+ // asymmetric with the structured writer: files round-tripping
3508
+ // through raw-patch lost an explicitly-false marker that the
3509
+ // structural path preserved.
3510
+ const selfClosing = head.endsWith("/>");
3511
+ const insertion = ` ${attr}="${value ? "1" : "0"}"`;
3512
+ const rewritten = selfClosing
3513
+ ? head.replace(/\/>$/, `${insertion}/>`)
3514
+ : head.replace(/>$/, `${insertion}>`);
3515
+ return rewritten + block.slice(openEnd + 1);
3516
+ }
3517
+ /**
3518
+ * Replace (or remove) a numeric attribute on the opening tag of `block`.
3519
+ * Used to patch ChartEx series attributes such as `ownerIdx` that live on
3520
+ * `<cx:series …>` rather than as structured children. Matches the element
3521
+ * only when the block begins with `<{tag}` so nested tags with the same
3522
+ * attribute name are left alone. Preserves the `/` on self-closing tags.
3523
+ */
3524
+ function patchOpeningTagIntegerAttribute(block, tag, attr, value) {
3525
+ const openEnd = block.indexOf(">");
3526
+ if (openEnd < 0 || !block.startsWith(`<${tag}`)) {
3527
+ return block;
3528
+ }
3529
+ const head = block.slice(0, openEnd + 1);
3530
+ const selfClosing = head.endsWith("/>");
3531
+ const strippedHead = head.replace(new RegExp(`\\s${attr}=("[^"]*"|'[^']*')`, "g"), "");
3532
+ if (value === undefined || !Number.isFinite(value)) {
3533
+ return strippedHead + block.slice(openEnd + 1);
3534
+ }
3535
+ const insertion = ` ${attr}="${value}"`;
3536
+ const rewritten = selfClosing
3537
+ ? strippedHead.replace(/\/>$/, `${insertion}/>`)
3538
+ : strippedHead.replace(/>$/, `${insertion}>`);
3539
+ return rewritten + block.slice(openEnd + 1);
3540
+ }
3541
+ function patchRepeatingChildren(block, tag, replacement, beforeTags, parentTag) {
3542
+ const stripped = removeXmlBlocks(block, tag);
3543
+ if (!replacement) {
3544
+ return stripped;
3545
+ }
3546
+ return replaceOrInsertBeforeGeneric(stripped, tag, replacement, beforeTags, parentTag);
3547
+ }
3548
+ function removeXmlBlock(block, tag) {
3549
+ const range = findXmlBlock(block, tag);
3550
+ return range ? block.slice(0, range.start) + block.slice(range.end) : block;
3551
+ }
3552
+ /**
3553
+ * Patch a single attribute on the opening tag of `elementTag` inside
3554
+ * the supplied `block`. Intended for raw-XML patching where the
3555
+ * attribute, not a child element, carries the field — e.g.
3556
+ * `CT_Axis/@hidden` in the Chart2014 schema.
3557
+ *
3558
+ * - `value === undefined` removes the attribute (if present).
3559
+ * - `value === true | false` lands `attr="1"` / `attr="0"` — the
3560
+ * OOXML `xsd:boolean` lexical form.
3561
+ * - `value: string` lands literally (escaped).
3562
+ *
3563
+ * The function only mutates the **first** matching opening tag; it
3564
+ * does not recurse into nested elements of the same name (axes in a
3565
+ * combo-chart plotArea are iterated by the caller, each block already
3566
+ * narrowed to a single `<cx:axis …>` opening). Returns the block
3567
+ * unchanged when `elementTag` can't be found — callers rely on the
3568
+ * identity comparison `patched !== block` to detect successful writes.
3569
+ */
3570
+ function patchXmlAttribute(block, elementTag, attrName, value) {
3571
+ // Match the opening tag, allowing leading whitespace in attributes
3572
+ // and both self-closing and regular element forms. Escape the full
3573
+ // element name for regex safety (covers all special regex characters).
3574
+ const escapedTag = elementTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3575
+ const tagRe = new RegExp(`<${escapedTag}\\b([^>]*)(/?)>`);
3576
+ const match = tagRe.exec(block);
3577
+ if (!match) {
3578
+ return block;
3579
+ }
3580
+ const [fullMatch, attrSegment, selfClose] = match;
3581
+ const attrRe = new RegExp(`\\s${attrName}="[^"]*"`);
3582
+ const stripped = attrSegment.replace(attrRe, "");
3583
+ const serialised = value === undefined
3584
+ ? ""
3585
+ : typeof value === "boolean"
3586
+ ? ` ${attrName}="${value ? "1" : "0"}"`
3587
+ : ` ${attrName}="${escapeAttr(value)}"`;
3588
+ const rebuilt = `<${elementTag}${stripped}${serialised}${selfClose}>`;
3589
+ if (rebuilt === fullMatch) {
3590
+ return block;
3591
+ }
3592
+ return block.slice(0, match.index) + rebuilt + block.slice(match.index + fullMatch.length);
3593
+ }
3594
+ function removeXmlBlocks(block, tag) {
3595
+ let patched = block;
3596
+ let range = findXmlBlock(patched, tag);
3597
+ while (range) {
3598
+ patched = patched.slice(0, range.start) + patched.slice(range.end);
3599
+ range = findXmlBlock(patched, tag, range.start);
3600
+ }
3601
+ return patched;
3602
+ }
3603
+ function replaceXmlBlocks(raw, tag, replace) {
3604
+ let cursor = 0;
3605
+ let output = "";
3606
+ while (cursor < raw.length) {
3607
+ const range = findXmlBlock(raw, tag, cursor);
3608
+ if (!range) {
3609
+ output += raw.slice(cursor);
3610
+ return output;
3611
+ }
3612
+ output += raw.slice(cursor, range.start);
3613
+ const replacement = replace(raw.slice(range.start, range.end));
3614
+ if (replacement === undefined) {
3615
+ return undefined;
3616
+ }
3617
+ output += replacement;
3618
+ cursor = range.end;
3619
+ }
3620
+ return output;
3621
+ }
3622
+ function findAxisBlock(raw, tag, axId) {
3623
+ let cursor = 0;
3624
+ while (cursor < raw.length) {
3625
+ const range = findXmlBlock(raw, tag, cursor);
3626
+ if (!range) {
3627
+ return undefined;
3628
+ }
3629
+ const xml = raw.slice(range.start, range.end);
3630
+ if (new RegExp(`<c:axId\\s+val=["']${axId}["']\\s*/>`).test(xml)) {
3631
+ return { ...range, xml };
3632
+ }
3633
+ cursor = range.end;
3634
+ }
3635
+ return undefined;
3636
+ }
3637
+ /**
3638
+ * Locate the `<tag …>…</tag>` (or self-closing `<tag …/>`) block in
3639
+ * `raw` starting at or after `offset`. Returns `{ start, end }` on hit,
3640
+ * `undefined` on miss.
3641
+ *
3642
+ * Two correctness concerns this implementation guards against:
3643
+ * 1. **Prefix collision** — `indexOf("<tag")` matches `<tag2>` or
3644
+ * `<tagX>` as well as the literal `<tag>` / `<tag `/`<tag/`. We
3645
+ * require the character immediately after the tag name to be one
3646
+ * of `>`, `/`, or whitespace so `<c:chart>` can't match
3647
+ * `<c:chartSpace>` and `<c:ax>` can't match `<c:axId>`.
3648
+ * 2. **Nested same-name elements** — `<c:extLst><c:ext>…<c:extLst>
3649
+ * …</c:extLst></c:ext></c:extLst>` used to match the inner
3650
+ * close. Walk open/close tokens with a depth counter to find the
3651
+ * matching end.
3652
+ *
3653
+ * Limitations: the scanner treats XML tokens lexically and will fail
3654
+ * on same-name occurrences inside CDATA or XML comments. ChartEx XML
3655
+ * does not use either, so this remains a safe shortcut.
3656
+ */
3657
+ function findXmlBlock(raw, tag, offset = 0) {
3658
+ const openToken = `<${tag}`;
3659
+ const closeToken = `</${tag}>`;
3660
+ let pos = offset;
3661
+ let start = -1;
3662
+ // First, find the first legitimate open tag — one whose next char is
3663
+ // `>`, `/`, or whitespace. Prefix collisions (`<tag2>`) are silently
3664
+ // skipped.
3665
+ while (pos < raw.length) {
3666
+ const candidate = raw.indexOf(openToken, pos);
3667
+ if (candidate < 0) {
3668
+ return undefined;
3669
+ }
3670
+ const nextChar = raw[candidate + openToken.length];
3671
+ if (nextChar === ">" || nextChar === "/" || /\s/.test(nextChar ?? "")) {
3672
+ start = candidate;
3673
+ break;
3674
+ }
3675
+ pos = candidate + openToken.length;
3676
+ }
3677
+ if (start < 0) {
3678
+ return undefined;
3679
+ }
3680
+ // Find the end of the open tag. `>` inside a quoted attribute would
3681
+ // confuse this, but Chart XML attributes never contain `>`.
3682
+ const openEnd = raw.indexOf(">", start);
3683
+ if (openEnd < 0) {
3684
+ return undefined;
3685
+ }
3686
+ if (raw[openEnd - 1] === "/") {
3687
+ return { start, end: openEnd + 1 };
3688
+ }
3689
+ // Walk forward balancing opens and closes for this same tag name.
3690
+ // A nested `<tag>` inside the element bumps the depth; `</tag>`
3691
+ // decrements. When depth hits zero we've found the matching close.
3692
+ let depth = 1;
3693
+ let scan = openEnd + 1;
3694
+ while (scan < raw.length && depth > 0) {
3695
+ const nextOpen = (() => {
3696
+ let p = scan;
3697
+ while (p < raw.length) {
3698
+ const c = raw.indexOf(openToken, p);
3699
+ if (c < 0) {
3700
+ return -1;
3701
+ }
3702
+ const next = raw[c + openToken.length];
3703
+ if (next === ">" || next === "/" || /\s/.test(next ?? "")) {
3704
+ return c;
3705
+ }
3706
+ p = c + openToken.length;
3707
+ }
3708
+ return -1;
3709
+ })();
3710
+ const nextClose = raw.indexOf(closeToken, scan);
3711
+ if (nextClose < 0) {
3712
+ return undefined;
3713
+ }
3714
+ if (nextOpen >= 0 && nextOpen < nextClose) {
3715
+ // Another open of the same tag — but only count it if it's a
3716
+ // real element (not self-closing, which shouldn't change depth).
3717
+ const oeNext = raw.indexOf(">", nextOpen);
3718
+ if (oeNext < 0) {
3719
+ return undefined;
3720
+ }
3721
+ if (raw[oeNext - 1] !== "/") {
3722
+ depth++;
3723
+ }
3724
+ scan = oeNext + 1;
3725
+ }
3726
+ else {
3727
+ depth--;
3728
+ if (depth === 0) {
3729
+ return { start, end: nextClose + closeToken.length };
3730
+ }
3731
+ scan = nextClose + closeToken.length;
3732
+ }
3733
+ }
3734
+ return undefined;
3735
+ }
3736
+ function escapeXml(value) {
3737
+ // Route through the canonical XML encoder so every raw-patch / XML
3738
+ // builder call site benefits from the same strict sanitisation:
3739
+ //
3740
+ // - strips XML 1.0-forbidden control characters (`#x0`-`#x1F`
3741
+ // except `\t \n \r`, `#x7F` DEL, `#xFFFE`, `#xFFFF`);
3742
+ // - strips lone surrogate halves (previously `U+D800`-`U+DFFF`
3743
+ // outside a valid pair could leak into attribute / text
3744
+ // content and corrupt the output encoding);
3745
+ // - escapes all five XML structural entities (`< > & " '`).
3746
+ //
3747
+ // The previous local implementation only handled `& < >` plus a
3748
+ // partial control-char strip. That was enough for the reserved-
3749
+ // trio case but left `"` untouched in attribute values (callers
3750
+ // compensated with a manual `.replace(/"/g, "&quot;")`), and
3751
+ // lone surrogates survived — producing bytes no XML parser can
3752
+ // reopen.
3753
+ //
3754
+ // Element-text call sites used to emit `"` / `'` verbatim; the
3755
+ // new encoder produces `&quot;` / `&apos;`. Both are valid XML
3756
+ // and round-trip identically through any parser, but byte-level
3757
+ // diffs against the old output will show the extra entities.
3758
+ return xmlEncode(value);
3759
+ }
3760
+ function escapeAttr(value) {
3761
+ // Attribute values need the extra step of escaping `\t \n \r` as
3762
+ // numeric character references; without it, XML 1.0 §3.3.3
3763
+ // attribute-value normalisation replaces them with a single
3764
+ // literal space at parse time, silently losing any embedded
3765
+ // newline / tab in (e.g.) a chart title.
3766
+ return xmlEncodeAttr(value);
3767
+ }
273
3768
  /**
274
3769
  * XLSX class - handles Excel file operations
275
3770
  * Works in both Node.js and Browser environments
@@ -326,6 +3821,8 @@ class XLSX {
326
3821
  async writeToZip(zip, options) {
327
3822
  const { model } = this.workbook;
328
3823
  this.prepareModel(model, options);
3824
+ this.prepareChartsheets(model);
3825
+ this.prepareChartExSidecars(model);
329
3826
  await this.addContentTypes(zip, model);
330
3827
  await this.addOfficeRels(zip, model);
331
3828
  await this.addWorkbookRels(zip, model);
@@ -336,10 +3833,13 @@ class XLSX {
336
3833
  await this.addWorksheets(zip, model);
337
3834
  await this.addSharedStrings(zip, model);
338
3835
  await this.addDrawings(zip, model);
3836
+ await this.addChartsheets(zip, model);
3837
+ const strictTemplateMode = isStrictTemplateMode(options);
3838
+ await this.addCharts(zip, model, strictTemplateMode);
3839
+ await this.addChartExEntries(zip, model, strictTemplateMode);
339
3840
  await this.addTables(zip, model);
340
3841
  await this.addPivotTables(zip, model);
341
3842
  await this.addExternalLinks(zip, model);
342
- this.addPassthrough(zip, model);
343
3843
  await this.addThemes(zip, model);
344
3844
  await this.addStyles(zip, model);
345
3845
  await this.addFeaturePropertyBag(zip, model);
@@ -347,6 +3847,44 @@ class XLSX {
347
3847
  await this.addMedia(zip, model);
348
3848
  await this.addApp(zip, model);
349
3849
  await this.addCore(zip, model);
3850
+ await this.addPersons(zip, model);
3851
+ await this.addSlicerAndTimelineParts(zip, model);
3852
+ }
3853
+ /**
3854
+ * Emit the raw slicer/timeline parts captured on load. Pure
3855
+ * byte-copy — excelts does not modify these parts. The partner
3856
+ * Content-Types and rels are covered separately (content types in
3857
+ * `addContentTypes`, sheet/workbook rels by the corresponding
3858
+ * xforms consuming the existing `xl/_rels/*.rels` captured on
3859
+ * load).
3860
+ */
3861
+ async addSlicerAndTimelineParts(zip, model) {
3862
+ for (const source of [
3863
+ model.slicerParts,
3864
+ model.slicerCacheParts,
3865
+ model.timelineParts,
3866
+ model.timelineCacheParts
3867
+ ]) {
3868
+ if (!source) {
3869
+ continue;
3870
+ }
3871
+ for (const [path, bytes] of Object.entries(source)) {
3872
+ zip.append(bytes, { name: path });
3873
+ }
3874
+ }
3875
+ }
3876
+ /**
3877
+ * Write the workbook-level `xl/persons/person.xml` part when the
3878
+ * model carries Office 365 threaded-comment authors. No-op when the
3879
+ * persons list is empty so legacy files without threaded comments
3880
+ * stay byte-identical.
3881
+ */
3882
+ async addPersons(zip, model) {
3883
+ const persons = model.persons;
3884
+ if (!persons || persons.length === 0) {
3885
+ return;
3886
+ }
3887
+ zip.append(renderPersonList(persons), { name: "xl/persons/person.xml" });
350
3888
  }
351
3889
  // ===========================================================================
352
3890
  // Stream/Buffer operations - shared by all platforms
@@ -467,8 +4005,6 @@ class XLSX {
467
4005
  mediaIndex: {},
468
4006
  drawings: {},
469
4007
  drawingRels: {},
470
- // Raw drawing XML data for passthrough (when drawing contains chart references)
471
- rawDrawings: {},
472
4008
  comments: {},
473
4009
  tables: {},
474
4010
  vmlDrawings: {},
@@ -476,8 +4012,22 @@ class XLSX {
476
4012
  pivotTableRels: {},
477
4013
  pivotCacheDefinitions: {},
478
4014
  pivotCacheRecords: {},
479
- // Passthrough storage for unknown/unsupported files (charts, etc.)
480
- passthrough: {},
4015
+ // Parsed chart entries keyed by chart number
4016
+ chartEntries: {},
4017
+ // Parsed chart rels keyed by chart number
4018
+ chartRels: {},
4019
+ // Raw chart style bytes keyed by style number
4020
+ chartStyles: {},
4021
+ // Raw chart colors bytes keyed by colors number
4022
+ chartColors: {},
4023
+ chartExStyles: {},
4024
+ chartExColors: {},
4025
+ // Raw chartEx entries (Office 2016+ extended charts) keyed by chartEx number
4026
+ chartExEntries: {},
4027
+ // Parsed chartEx rels keyed by chartEx number
4028
+ chartExRels: {},
4029
+ // Structured chartEx entries (built via addChartEx) keyed by chartEx number
4030
+ chartExStructuredEntries: {},
481
4031
  // External workbook links — parsed from xl/externalLinks/externalLinkN.xml
482
4032
  // during _processDefaultEntry, then reconciled into a dense
483
4033
  // ExternalLinkModel[] by reconcile() using workbookRels + <externalReferences>.
@@ -485,7 +4035,10 @@ class XLSX {
485
4035
  // Raw rels from each externalLinkN.rels file, keyed by index.
486
4036
  // Contains the actual Target path (e.g. "测试.xlsx", "file:///...")
487
4037
  // and TargetMode ("External" / "Internal").
488
- externalLinkRelsByIndex: {}
4038
+ externalLinkRelsByIndex: {},
4039
+ // Chartsheets keyed by sheet number
4040
+ chartsheets: {},
4041
+ chartsheetRels: {}
489
4042
  };
490
4043
  }
491
4044
  /**
@@ -511,20 +4064,6 @@ class XLSX {
511
4064
  });
512
4065
  return concatUint8Arrays(chunks);
513
4066
  }
514
- /**
515
- * Check if a drawing has chart references in its relationships
516
- */
517
- drawingHasChartReference(drawing) {
518
- return (drawing.rels && drawing.rels.some((rel) => rel.Target && rel.Target.includes("/charts/")));
519
- }
520
- /**
521
- * Check if a drawing rels list references charts.
522
- * Used to decide whether we need to keep raw drawing XML for passthrough.
523
- */
524
- drawingRelsHasChartReference(drawingRels) {
525
- return (Array.isArray(drawingRels) &&
526
- drawingRels.some(rel => typeof rel?.Target === "string" && rel.Target.includes("/charts/")));
527
- }
528
4067
  /**
529
4068
  * Process a known OOXML entry (workbook, styles, shared strings, etc.)
530
4069
  * Returns true if handled, false if should be passed to _processDefaultEntry
@@ -535,6 +4074,11 @@ class XLSX {
535
4074
  await this._processWorksheetEntry(stream, model, sheetNo, options, entryName);
536
4075
  return true;
537
4076
  }
4077
+ const chartsheetNo = getChartsheetNoFromPath(entryName);
4078
+ if (chartsheetNo !== undefined) {
4079
+ await this._processChartsheetEntry(stream, model, chartsheetNo);
4080
+ return true;
4081
+ }
538
4082
  switch (entryName) {
539
4083
  case OOXML_PATHS.rootRels:
540
4084
  model.globalRels = await this.parseRels(stream);
@@ -587,8 +4131,56 @@ class XLSX {
587
4131
  }
588
4132
  return true;
589
4133
  }
590
- default:
4134
+ case "xl/persons/person.xml": {
4135
+ // Office 365 threaded-comment person directory. Parsed here so
4136
+ // reconcile can attach the list to the workbook. Silently
4137
+ // ignored when malformed — threaded comments degrade to
4138
+ // "unknown author" rather than breaking the whole load.
4139
+ const data = await this.collectStreamData(stream);
4140
+ const raw = new TextDecoder().decode(data);
4141
+ model.persons = parsePersonList(raw);
4142
+ return true;
4143
+ }
4144
+ default: {
4145
+ // Catch threaded-comment per-sheet parts (the path contains a
4146
+ // variable sheet index so they can't be matched in the switch).
4147
+ const threadedMatch = /^xl\/threadedComments\/threadedComment(\d+)\.xml$/.exec(entryName);
4148
+ if (threadedMatch) {
4149
+ const sheetIndex = parseInt(threadedMatch[1], 10);
4150
+ const data = await this.collectStreamData(stream);
4151
+ const raw = new TextDecoder().decode(data);
4152
+ model.threadedCommentsByIndex ?? (model.threadedCommentsByIndex = {});
4153
+ model.threadedCommentsByIndex[sheetIndex] = parseThreadedComments(raw);
4154
+ return true;
4155
+ }
4156
+ // Raw-passthrough capture for slicers and timelines — two
4157
+ // coordinated Office dashboard features excelts does not
4158
+ // structurally model but must not destroy on round-trip.
4159
+ // Each family has two part types (the control itself + its
4160
+ // cache); both are captured into maps on the workbook model
4161
+ // so the writer can emit them verbatim later.
4162
+ if (/^xl\/slicers\/slicer\d+\.xml$/.test(entryName)) {
4163
+ model.slicerParts ?? (model.slicerParts = {});
4164
+ model.slicerParts[entryName] = await this.collectStreamData(stream);
4165
+ return true;
4166
+ }
4167
+ if (/^xl\/slicerCaches\/slicerCache\d+\.xml$/.test(entryName)) {
4168
+ model.slicerCacheParts ?? (model.slicerCacheParts = {});
4169
+ model.slicerCacheParts[entryName] = await this.collectStreamData(stream);
4170
+ return true;
4171
+ }
4172
+ if (/^xl\/timelines\/timeline\d+\.xml$/.test(entryName)) {
4173
+ model.timelineParts ?? (model.timelineParts = {});
4174
+ model.timelineParts[entryName] = await this.collectStreamData(stream);
4175
+ return true;
4176
+ }
4177
+ if (/^xl\/timelineCaches\/timelineCache\d+\.xml$/.test(entryName)) {
4178
+ model.timelineCacheParts ?? (model.timelineCacheParts = {});
4179
+ model.timelineCacheParts[entryName] = await this.collectStreamData(stream);
4180
+ return true;
4181
+ }
591
4182
  return false;
4183
+ }
592
4184
  }
593
4185
  }
594
4186
  async loadFromZipEntries(entries, options) {
@@ -647,7 +4239,14 @@ class XLSX {
647
4239
  zip.pipe(stream);
648
4240
  await this.writeToZip(zip, options);
649
4241
  await this._finalize(zip);
650
- return stream.read() || new Uint8Array(0);
4242
+ const bytes = stream.read() || new Uint8Array(0);
4243
+ // Optional OOXML self-check. Enabled by default in non-production
4244
+ // Node.js environments; disabled in the browser and in production.
4245
+ // See `XlsxWriteOptions.validate` for the resolution rules.
4246
+ if (shouldAutoValidate(options.validate)) {
4247
+ await runWriteBufferSelfCheck(bytes);
4248
+ }
4249
+ return bytes;
651
4250
  }
652
4251
  // ===========================================================================
653
4252
  // Media handling - base implementation (buffer/base64 only)
@@ -728,15 +4327,35 @@ class XLSX {
728
4327
  drawingXform.reconcile(drawing, drawingOptions);
729
4328
  }
730
4329
  });
731
- // Trim raw drawings for non-chart drawings to avoid bloating the serialized workbook model.
732
- if (model.rawDrawings && model.drawingRels) {
733
- for (const name of Object.keys(model.rawDrawings)) {
734
- const drawingRel = model.drawingRels[name];
735
- if (drawingRel && !this.drawingRelsHasChartReference(drawingRel)) {
736
- delete model.rawDrawings[name];
4330
+ // Reconcile chart references in drawing anchors
4331
+ Object.keys(model.drawings).forEach(name => {
4332
+ const drawing = model.drawings[name];
4333
+ const drawingRel = model.drawingRels[name];
4334
+ if (!drawingRel) {
4335
+ return;
4336
+ }
4337
+ const relMap = {};
4338
+ for (const rel of drawingRel) {
4339
+ relMap[rel.Id] = rel;
4340
+ }
4341
+ for (const anchor of drawing.anchors ?? []) {
4342
+ if (anchor.graphicFrame?.rId) {
4343
+ const rel = relMap[anchor.graphicFrame.rId];
4344
+ if (rel?.Target) {
4345
+ // Extract chart number from target like "../charts/chart1.xml"
4346
+ const match = /chart(\d+)\.xml/.exec(rel.Target);
4347
+ if (match) {
4348
+ anchor.chartNumber = parseInt(match[1], 10);
4349
+ }
4350
+ // Extract chartEx number from target like "../charts/chartEx1.xml"
4351
+ const matchEx = /chartEx(\d+)\.xml/.exec(rel.Target);
4352
+ if (matchEx) {
4353
+ anchor.chartExNumber = parseInt(matchEx[1], 10);
4354
+ }
4355
+ }
737
4356
  }
738
4357
  }
739
- }
4358
+ });
740
4359
  // reconcile tables with the default styles
741
4360
  const tableOptions = {
742
4361
  styles: model.styles
@@ -764,7 +4383,59 @@ class XLSX {
764
4383
  model.worksheets.forEach((worksheet) => {
765
4384
  worksheet.relationships = model.worksheetRels[worksheet.sheetNo];
766
4385
  worksheetXform.reconcile(worksheet, sheetOptions);
4386
+ // Attach any threaded comments that arrived in a separate
4387
+ // `xl/threadedComments/threadedComment{N}.xml` part. The sheet
4388
+ // index in that path maps to `worksheet.sheetNo`, not
4389
+ // `worksheet.id` — Excel uses the package-relative file number,
4390
+ // same as classic `xl/comments{N}.xml`.
4391
+ const threaded = model.threadedCommentsByIndex?.[worksheet.sheetNo];
4392
+ if (threaded) {
4393
+ worksheet.threadedComments = threaded;
4394
+ }
767
4395
  });
4396
+ // Reconcile chartsheets — link their drawing references and
4397
+ // preserve every relationship so the writer can round-trip
4398
+ // every r:id referenced by raw-captured children (legacyDrawing,
4399
+ // picture, legacyDrawingHF, drawingHF, etc.). Previously only
4400
+ // the drawing rel was hooked up and everything else was
4401
+ // silently discarded on save, leaving any raw-captured child
4402
+ // with a dangling r:id pointing at a now-missing part.
4403
+ const chartsheetsList = model.chartsheetsList || [];
4404
+ for (const cs of chartsheetsList) {
4405
+ const csRels = model.chartsheetRels[cs.sheetNo];
4406
+ if (csRels) {
4407
+ // Keep the full rels list attached to the model so
4408
+ // `addChartsheets` can re-emit it. Copy so downstream
4409
+ // mutations don't leak back into `model.chartsheetRels`.
4410
+ cs.relationships = [...csRels];
4411
+ }
4412
+ if (cs.drawing && csRels) {
4413
+ const drawingRel = csRels.find((r) => r.Id === cs.drawing.rId);
4414
+ if (drawingRel) {
4415
+ const match = drawingRel.Target.match(/\/drawings\/([a-zA-Z0-9]+)[.][a-zA-Z]{3,4}$/);
4416
+ if (match) {
4417
+ cs.drawingName = match[1];
4418
+ // Resolve drawing → chart number from drawing rels
4419
+ const drawingRelArr = model.drawingRels[cs.drawingName];
4420
+ if (drawingRelArr) {
4421
+ for (const dr of drawingRelArr) {
4422
+ const chartMatch = /chart(\d+)\.xml/.exec(dr.Target);
4423
+ if (chartMatch) {
4424
+ cs.chartNumber = parseInt(chartMatch[1], 10);
4425
+ break;
4426
+ }
4427
+ const chartExMatch = /chartEx(\d+)\.xml/.exec(dr.Target);
4428
+ if (chartExMatch) {
4429
+ cs.chartExNumber = parseInt(chartExMatch[1], 10);
4430
+ break;
4431
+ }
4432
+ }
4433
+ }
4434
+ }
4435
+ }
4436
+ }
4437
+ }
4438
+ model.chartsheets = chartsheetsList;
768
4439
  // Reconcile external workbook links before workbookRels / externalReferences
769
4440
  // are dropped. Joins 3 sources:
770
4441
  // 1. model.externalReferences — ordered list of { rId } from workbook.xml
@@ -772,6 +4443,17 @@ class XLSX {
772
4443
  // 3. model.externalLinksByIndex — parsed externalLinkN.xml parts
773
4444
  // 4. model.externalLinkRelsByIndex — parsed externalLinkN.xml.rels parts
774
4445
  this._reconcileExternalLinks(model);
4446
+ // Preserve parsed chart data through to the workbook model.
4447
+ // chartEntries, chartRels, chartStyles, chartColors are kept as-is.
4448
+ // Reconcile chart user-shapes drawing parts onto their owning
4449
+ // ChartEntry. Each chart rels file may reference an overlay drawing
4450
+ // via `RelType.ChartUserShapes`; we copy those bytes from
4451
+ // `model.drawingRaw` (populated by `_processDrawingEntry`) onto the
4452
+ // chart entry so writers can emit them back, and so the Chart API
4453
+ // can expose them via `Chart.userShapesXml`. Regular worksheet
4454
+ // drawings are untouched — this reconcile only moves bytes for
4455
+ // chart-overlay parts.
4456
+ this._reconcileChartUserShapes(model);
775
4457
  // delete unnecessary parts
776
4458
  delete model.worksheetHash;
777
4459
  delete model.worksheetRels;
@@ -785,6 +4467,7 @@ class XLSX {
785
4467
  delete model.mediaIndex;
786
4468
  delete model.drawings;
787
4469
  delete model.drawingRels;
4470
+ delete model.drawingRaw;
788
4471
  delete model.vmlDrawings;
789
4472
  delete model.pivotTableRels;
790
4473
  delete model.metadata;
@@ -792,6 +4475,56 @@ class XLSX {
792
4475
  delete model.externalReferences;
793
4476
  delete model.externalLinksByIndex;
794
4477
  delete model.externalLinkRelsByIndex;
4478
+ delete model.chartsheetRels;
4479
+ delete model.chartsheetsList;
4480
+ }
4481
+ /**
4482
+ * Copy the raw bytes of each chart's user-shapes drawing part onto
4483
+ * the owning `ChartEntry.userShapesXml` so the writer can emit them
4484
+ * back verbatim (and so {@link Chart.userShapesXml} can surface them
4485
+ * to user code). Runs after all ZIP entries have been processed
4486
+ * because chart rels and drawing bytes stream in independent order.
4487
+ *
4488
+ * Skips charts that have no `ChartUserShapes` rel. The bytes stay
4489
+ * keyed by drawing name (e.g. `drawing3`) inside `model.drawingRaw`
4490
+ * since a workbook may have many user-shape drawings across
4491
+ * different charts; we look up each chart's target through its
4492
+ * rels file.
4493
+ */
4494
+ _reconcileChartUserShapes(model) {
4495
+ var _a;
4496
+ const chartRelsMap = model.chartRels;
4497
+ const drawingRaw = model.drawingRaw;
4498
+ const chartEntries = model.chartEntries;
4499
+ if (!chartRelsMap || !drawingRaw || !chartEntries) {
4500
+ return;
4501
+ }
4502
+ for (const [chartNum, rels] of Object.entries(chartRelsMap)) {
4503
+ if (!Array.isArray(rels)) {
4504
+ continue;
4505
+ }
4506
+ const entry = chartEntries[chartNum];
4507
+ if (!entry) {
4508
+ continue;
4509
+ }
4510
+ const userShapesRel = rels.find(rel => rel && typeof rel === "object" && rel.Type === RelType.ChartUserShapes);
4511
+ if (!userShapesRel?.Target) {
4512
+ continue;
4513
+ }
4514
+ // Target like `../drawings/drawing3.xml` or `../drawings/chartUserShape2.xml`.
4515
+ const match = /drawings\/([^/]+)\.xml$/i.exec(String(userShapesRel.Target));
4516
+ if (!match) {
4517
+ continue;
4518
+ }
4519
+ const drawingName = match[1];
4520
+ const bytes = drawingRaw[drawingName];
4521
+ if (bytes) {
4522
+ entry.userShapesXml = bytes;
4523
+ // Make sure the chart model carries the r:id so subsequent reads
4524
+ // via Chart.userShapesXml can round-trip without extra setup.
4525
+ (_a = entry.model).userShapesRelId ?? (_a.userShapesRelId = userShapesRel.Id);
4526
+ }
4527
+ }
795
4528
  }
796
4529
  /**
797
4530
  * Join the three on-disk sources that together describe external workbook
@@ -993,6 +4726,7 @@ class XLSX {
993
4726
  const defaultMetric = this._determineMetric(pt.dataFields);
994
4727
  const completePivotTable = {
995
4728
  ...pt,
4729
+ name: pt.name ?? `PivotTable${tableNumber}`,
996
4730
  tableNumber,
997
4731
  cacheId: String(pt.cacheId),
998
4732
  cacheDefinition: cacheData?.definition,
@@ -1068,6 +4802,14 @@ class XLSX {
1068
4802
  model.worksheetHash[path] = worksheet;
1069
4803
  model.worksheets.push(worksheet);
1070
4804
  }
4805
+ async _processChartsheetEntry(stream, model, sheetNo) {
4806
+ const xform = new ChartsheetXform();
4807
+ const chartsheet = await xform.parseStream(stream);
4808
+ if (chartsheet) {
4809
+ chartsheet.sheetNo = sheetNo;
4810
+ model.chartsheets[sheetNo] = chartsheet;
4811
+ }
4812
+ }
1071
4813
  async _processCommentEntry(stream, model, zipPath) {
1072
4814
  const xform = new CommentsXform();
1073
4815
  const comments = await xform.parseStream(stream);
@@ -1139,8 +4881,30 @@ class XLSX {
1139
4881
  const xmlString = this.bufferToString(data);
1140
4882
  const drawing = await xform.parseStream(this.createTextStream(xmlString));
1141
4883
  model.drawings[name] = drawing;
1142
- // Store raw data; reconcile() may later drop it if charts are not referenced.
1143
- model.rawDrawings[name] = data;
4884
+ // Also stash the original bytes chart user-shape drawings use a
4885
+ // distinct schema (`c:relSizeAnchor` / `c:userShapes` instead of
4886
+ // `xdr:twoCellAnchor`) and are post-reconciled onto their owning
4887
+ // ChartEntry so the bytes can be written back verbatim. Regular
4888
+ // worksheet drawings don't read this map.
4889
+ if (!model.drawingRaw) {
4890
+ model.drawingRaw = {};
4891
+ }
4892
+ model.drawingRaw[name] = data;
4893
+ }
4894
+ /**
4895
+ * Stash raw bytes of a chart-overlay drawing part. `c:userShapes`
4896
+ * parts live under `xl/drawings/chartUserShape{N}.xml` in files we
4897
+ * write ourselves and can use arbitrary names in foreign files (the
4898
+ * rel target is the only authoritative reference). The bytes are
4899
+ * keyed by the stem so `_reconcileChartUserShapes` can match them
4900
+ * against each chart's `ChartUserShapes` rel Target.
4901
+ */
4902
+ async _processChartUserShapesEntry(_stream, model, name, rawData) {
4903
+ const data = rawData ?? (await this.collectStreamData(_stream));
4904
+ if (!model.drawingRaw) {
4905
+ model.drawingRaw = {};
4906
+ }
4907
+ model.drawingRaw[name] = data;
1144
4908
  }
1145
4909
  async _processDrawingRelsEntry(entry, model, name) {
1146
4910
  const xform = new RelationshipsXform();
@@ -1241,6 +5005,34 @@ class XLSX {
1241
5005
  const relationships = await this.parseRels(stream);
1242
5006
  model.externalLinkRelsByIndex[index] = relationships ?? [];
1243
5007
  }
5008
+ async _processChartEntry(stream, model, chartNumber, rawData) {
5009
+ const data = rawData ?? (await this.collectStreamData(stream));
5010
+ // Parse into model for high-level API access
5011
+ const xform = getChartSupport().createChartSpaceXform();
5012
+ const xmlString = this.bufferToString(data);
5013
+ const chart = await xform.parseStream(this.createTextStream(xmlString));
5014
+ if (chart) {
5015
+ model.chartEntries[chartNumber] = {
5016
+ chartNumber,
5017
+ model: chart,
5018
+ rawData: data,
5019
+ modelSnapshot: snapshotChartModel(chart)
5020
+ };
5021
+ }
5022
+ }
5023
+ async _processChartRelsEntry(stream, model, chartNumber) {
5024
+ const xform = new RelationshipsXform();
5025
+ const relationships = await xform.parseStream(stream);
5026
+ model.chartRels[chartNumber] = relationships;
5027
+ }
5028
+ async _processChartStyleEntry(stream, model, styleNumber) {
5029
+ const data = await this.collectStreamData(stream);
5030
+ model.chartStyles[styleNumber] = data;
5031
+ }
5032
+ async _processChartColorsEntry(stream, model, colorsNumber) {
5033
+ const data = await this.collectStreamData(stream);
5034
+ model.chartColors[colorsNumber] = data;
5035
+ }
1244
5036
  // ===========================================================================
1245
5037
  // loadFromFiles - shared logic for loading from pre-extracted ZIP data
1246
5038
  // ===========================================================================
@@ -1261,7 +5053,6 @@ class XLSX {
1261
5053
  : this.createTextStream(this.bufferToString(entry.data));
1262
5054
  const handled = await this._processKnownEntry(stream, model, entryName, options);
1263
5055
  if (!handled) {
1264
- // Pass raw entry data for drawings to enable passthrough
1265
5056
  await this._processDefaultEntry(stream, model, entryName, entry.data);
1266
5057
  }
1267
5058
  }
@@ -1280,6 +5071,12 @@ class XLSX {
1280
5071
  await this._processWorksheetRelsEntry(stream, model, sheetNo);
1281
5072
  return true;
1282
5073
  }
5074
+ const chartsheetRelsNo = getChartsheetNoFromRelsPath(entryName);
5075
+ if (chartsheetRelsNo !== undefined) {
5076
+ const rels = await this.parseRels(stream);
5077
+ model.chartsheetRels[chartsheetRelsNo] = rels;
5078
+ return true;
5079
+ }
1283
5080
  const mediaFilename = getMediaFilenameFromPath(entryName);
1284
5081
  if (mediaFilename) {
1285
5082
  await this._processMediaEntry(stream, model, mediaFilename);
@@ -1288,7 +5085,11 @@ class XLSX {
1288
5085
  const drawingName = getDrawingNameFromPath(entryName);
1289
5086
  if (drawingName) {
1290
5087
  await this._processDrawingEntry(stream, model, drawingName, rawData);
1291
- // rawData is now stored inside _processDrawingEntry
5088
+ return true;
5089
+ }
5090
+ const chartUserShapesName = getChartUserShapesNameFromPath(entryName);
5091
+ if (chartUserShapesName) {
5092
+ await this._processChartUserShapesEntry(stream, model, chartUserShapesName, rawData);
1292
5093
  return true;
1293
5094
  }
1294
5095
  const drawingRelsName = getDrawingNameFromRelsPath(entryName);
@@ -1365,27 +5166,108 @@ class XLSX {
1365
5166
  await this._processExternalLinkRelsEntry(stream, model, externalLinkRelsIndex);
1366
5167
  return true;
1367
5168
  }
1368
- // Store passthrough files (charts, etc.) for preservation
1369
- if (PassthroughManager.isPassthroughPath(entryName)) {
1370
- // If raw data is available (loadFromFiles path), use it directly
5169
+ // Chart files parse natively before the passthrough catch-all
5170
+ const chartNumber = getChartNumberFromPath(entryName);
5171
+ if (chartNumber !== undefined) {
5172
+ await this._processChartEntry(stream, model, chartNumber, rawData);
5173
+ return true;
5174
+ }
5175
+ const chartRelsNumber = getChartNumberFromRelsPath(entryName);
5176
+ if (chartRelsNumber !== undefined) {
5177
+ await this._processChartRelsEntry(stream, model, chartRelsNumber);
5178
+ return true;
5179
+ }
5180
+ const chartStyleNumber = getChartStyleNumberFromPath(entryName);
5181
+ if (chartStyleNumber !== undefined) {
1371
5182
  if (rawData) {
1372
- model.passthrough[entryName] = rawData;
5183
+ model.chartStyles[chartStyleNumber] = rawData;
1373
5184
  }
1374
5185
  else {
1375
- await this._processPassthroughEntry(stream, model, entryName);
5186
+ await this._processChartStyleEntry(stream, model, chartStyleNumber);
5187
+ }
5188
+ return true;
5189
+ }
5190
+ const chartColorsNumber = getChartColorsNumberFromPath(entryName);
5191
+ if (chartColorsNumber !== undefined) {
5192
+ if (rawData) {
5193
+ model.chartColors[chartColorsNumber] = rawData;
5194
+ }
5195
+ else {
5196
+ await this._processChartColorsEntry(stream, model, chartColorsNumber);
5197
+ }
5198
+ return true;
5199
+ }
5200
+ const chartExStyleNumber = getChartExStyleNumberFromPath(entryName);
5201
+ if (chartExStyleNumber !== undefined) {
5202
+ model.chartExStyles[chartExStyleNumber] = rawData ?? (await this.collectStreamData(stream));
5203
+ return true;
5204
+ }
5205
+ const chartExColorsNumber = getChartExColorsNumberFromPath(entryName);
5206
+ if (chartExColorsNumber !== undefined) {
5207
+ model.chartExColors[chartExColorsNumber] = rawData ?? (await this.collectStreamData(stream));
5208
+ return true;
5209
+ }
5210
+ // ChartEx files (Office 2016+ extended charts) — raw bytes plus best-effort structured model
5211
+ const chartExNumber = getChartExNumberFromPath(entryName);
5212
+ if (chartExNumber !== undefined) {
5213
+ const data = rawData ?? (await this.collectStreamData(stream));
5214
+ const rawXml = this.bufferToString(data);
5215
+ model.chartExEntries[chartExNumber] = data;
5216
+ try {
5217
+ const parsed = getChartSupport().parseChartEx(rawXml);
5218
+ model.chartExStructuredEntries[chartExNumber] = {
5219
+ chartExNumber,
5220
+ model: parsed,
5221
+ rawData: data,
5222
+ modelSnapshot: snapshotChartModel(parsed)
5223
+ };
5224
+ }
5225
+ catch {
5226
+ // Keep legacy-safe passthrough if a third-party chartEx part is not parseable.
1376
5227
  }
1377
5228
  return true;
1378
5229
  }
5230
+ const chartExRelsNumber = getChartExNumberFromRelsPath(entryName);
5231
+ if (chartExRelsNumber !== undefined) {
5232
+ const relsXform = new RelationshipsXform();
5233
+ const relationships = await relsXform.parseStream(stream);
5234
+ model.chartExRels[chartExRelsNumber] = relationships;
5235
+ return true;
5236
+ }
5237
+ // Raw-passthrough catch-all for Office 2010+ slicer/timeline
5238
+ // dashboard controls and their associated rels. excelts does not
5239
+ // model these structurally yet; capturing the bytes here prevents
5240
+ // silent data loss on round-trip when a dashboard workbook comes
5241
+ // through. Same idea covers the two-level rels files produced by
5242
+ // Excel (the `_rels` subfolder sits next to each part).
5243
+ if (/^xl\/slicers\/slicer\d+\.xml$/.test(entryName) ||
5244
+ /^xl\/slicerCaches\/slicerCache\d+\.xml$/.test(entryName) ||
5245
+ /^xl\/timelines\/timeline\d+\.xml$/.test(entryName) ||
5246
+ /^xl\/timelineCaches\/timelineCache\d+\.xml$/.test(entryName) ||
5247
+ /^xl\/slicers\/_rels\/slicer\d+\.xml\.rels$/.test(entryName) ||
5248
+ /^xl\/slicerCaches\/_rels\/slicerCache\d+\.xml\.rels$/.test(entryName) ||
5249
+ /^xl\/timelines\/_rels\/timeline\d+\.xml\.rels$/.test(entryName) ||
5250
+ /^xl\/timelineCaches\/_rels\/timelineCache\d+\.xml\.rels$/.test(entryName)) {
5251
+ const targetMap = entryName.startsWith("xl/slicers/") && !entryName.includes("/_rels/")
5252
+ ? (model.slicerParts ?? (model.slicerParts = {}))
5253
+ : entryName.startsWith("xl/slicerCaches/") && !entryName.includes("/_rels/")
5254
+ ? (model.slicerCacheParts ?? (model.slicerCacheParts = {}))
5255
+ : entryName.startsWith("xl/timelines/") && !entryName.includes("/_rels/")
5256
+ ? (model.timelineParts ?? (model.timelineParts = {}))
5257
+ : entryName.startsWith("xl/timelineCaches/") && !entryName.includes("/_rels/")
5258
+ ? (model.timelineCacheParts ?? (model.timelineCacheParts = {}))
5259
+ : entryName.startsWith("xl/slicers/_rels/")
5260
+ ? (model.slicerParts ?? (model.slicerParts = {}))
5261
+ : entryName.startsWith("xl/slicerCaches/_rels/")
5262
+ ? (model.slicerCacheParts ?? (model.slicerCacheParts = {}))
5263
+ : entryName.startsWith("xl/timelines/_rels/")
5264
+ ? (model.timelineParts ?? (model.timelineParts = {}))
5265
+ : (model.timelineCacheParts ?? (model.timelineCacheParts = {}));
5266
+ targetMap[entryName] = rawData ?? (await this.collectStreamData(stream));
5267
+ return true;
5268
+ }
1379
5269
  return false;
1380
5270
  }
1381
- /**
1382
- * Store a passthrough file for preservation during read/write cycles.
1383
- * These files are not parsed but stored as raw bytes to be written back unchanged.
1384
- */
1385
- async _processPassthroughEntry(stream, model, entryName) {
1386
- const data = await this.collectStreamData(stream);
1387
- model.passthrough[entryName] = data;
1388
- }
1389
5271
  // ===========================================================================
1390
5272
  // Write methods - shared by all platforms
1391
5273
  // ===========================================================================
@@ -1454,6 +5336,16 @@ class XLSX {
1454
5336
  Target: OOXML_REL_TARGETS.workbookMetadata
1455
5337
  });
1456
5338
  }
5339
+ // Office 365 threaded comments need a workbook-level person
5340
+ // directory. The rel Target is `persons/person.xml` (relative to
5341
+ // the xl/ workbook home, matching how Excel writes it).
5342
+ if (model.hasPersons) {
5343
+ relationships.push({
5344
+ Id: `rId${count++}`,
5345
+ Type: XLSX.RelType.Person,
5346
+ Target: "persons/person.xml"
5347
+ });
5348
+ }
1457
5349
  // R9-B6: Deduplicate pivot cache relationships by cacheId. When multiple pivot
1458
5350
  // tables share the same cache, only one workbook relationship should be created.
1459
5351
  // Also assigns rId to each pivot table (R9-B7: typed on PivotTable interface).
@@ -1483,6 +5375,15 @@ class XLSX {
1483
5375
  Target: worksheetRelTarget(worksheet.fileIndex)
1484
5376
  });
1485
5377
  });
5378
+ // Add chartsheet relationships
5379
+ (model.chartsheets || []).forEach((cs) => {
5380
+ cs.rId = `rId${count++}`;
5381
+ relationships.push({
5382
+ Id: cs.rId,
5383
+ Type: RelType.Chartsheet,
5384
+ Target: `chartsheets/sheet${cs.sheetNo}.xml`
5385
+ });
5386
+ });
1486
5387
  // External workbook link rels are written AFTER worksheets on purpose:
1487
5388
  // Excel tolerates either order, but stable ordering (worksheets then
1488
5389
  // externalLinks) keeps the emitted workbook.xml.rels diff-friendly for
@@ -1554,6 +5455,16 @@ class XLSX {
1554
5455
  if (worksheet.comments.length > 0) {
1555
5456
  await this._renderToZip(zip, commentsPath(fileIndex), commentsXform, worksheet);
1556
5457
  }
5458
+ // Office 365 threaded comments sit in their own part tree
5459
+ // alongside classic VML comments. Written straight from the
5460
+ // structured model without going through an xform instance —
5461
+ // the payload is small and the shape maps 1:1 onto the output.
5462
+ if (worksheet.threadedComments && worksheet.threadedComments.length > 0) {
5463
+ const xml = renderThreadedComments(worksheet.threadedComments);
5464
+ zip.append(xml, {
5465
+ name: `xl/threadedComments/threadedComment${fileIndex}.xml`
5466
+ });
5467
+ }
1557
5468
  // Generate unified VML drawing (contains both notes and form controls)
1558
5469
  const hasComments = worksheet.comments.length > 0;
1559
5470
  const hasFormControls = worksheet.formControls && worksheet.formControls.length > 0;
@@ -1600,31 +5511,367 @@ class XLSX {
1600
5511
  }
1601
5512
  }
1602
5513
  }
5514
+ async addChartsheets(zip, model) {
5515
+ const chartsheetXform = new ChartsheetXform();
5516
+ const relsXform = new RelationshipsXform();
5517
+ const vmlDrawingXform = new VmlDrawingXform();
5518
+ // Track VML drawing zip paths we re-emit for chartsheets so we
5519
+ // don't accidentally write the same VML part twice when a single
5520
+ // VML file is referenced by multiple chartsheets. Writing a ZIP
5521
+ // entry twice produces a package with duplicate central-directory
5522
+ // entries — most consumers tolerate it (reading the last), but
5523
+ // validators flag it and `unzip -l` shows the duplication.
5524
+ const emittedVmlPaths = new Set();
5525
+ for (const cs of model.chartsheets || []) {
5526
+ await this._renderToZip(zip, chartsheetPath(cs.sheetNo), chartsheetXform, cs);
5527
+ // Chartsheet rels. A chartsheet may carry rels beyond the
5528
+ // drawing reference — `legacyDrawing`, `legacyDrawingHF`,
5529
+ // `drawingHF`, `picture`, etc. — and those rels are referenced
5530
+ // by `r:id` attributes inside the raw-captured `rawChildren`
5531
+ // blocks. If we only emit the drawing rel (the previous
5532
+ // implementation), every other r:id goes dangling at save,
5533
+ // corrupting the package.
5534
+ //
5535
+ // Strategy:
5536
+ // 1. Start with the preserved `relationships` list from load
5537
+ // (missing for newly-created chartsheets).
5538
+ // 2. Overlay / insert the current drawing rel — the drawing
5539
+ // target may have been rewritten (e.g. chartsheet renamed
5540
+ // or its drawing renumbered) so we replace any prior
5541
+ // entry with the same Id.
5542
+ const baseRels = Array.isArray(cs.relationships) ? [...cs.relationships] : [];
5543
+ if (cs.drawing) {
5544
+ const drawingRel = {
5545
+ Id: cs.drawing.rId,
5546
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
5547
+ Target: `../drawings/${cs.drawingName}.xml`
5548
+ };
5549
+ const existingIdx = baseRels.findIndex((r) => r?.Id === cs.drawing.rId);
5550
+ if (existingIdx >= 0) {
5551
+ baseRels[existingIdx] = drawingRel;
5552
+ }
5553
+ else {
5554
+ baseRels.push(drawingRel);
5555
+ }
5556
+ }
5557
+ if (baseRels.length > 0) {
5558
+ await this._renderToZip(zip, chartsheetRelsPath(cs.sheetNo), relsXform, baseRels);
5559
+ }
5560
+ // Re-emit any VML drawing parts this chartsheet's rels reference.
5561
+ // The worksheet loop only emits VML for worksheets that own
5562
+ // comments / form controls / header images; a chartsheet that
5563
+ // carries its own `<legacyDrawing r:id="…"/>` would preserve its
5564
+ // rel target on write but leave the VML body missing from the
5565
+ // package — a dangling relationship. Walk the chartsheet's rels,
5566
+ // resolve each VML target against the chartsheet path, and emit
5567
+ // the parsed body captured at load time.
5568
+ if (model.vmlDrawings) {
5569
+ const baseDir = `xl/chartsheets/`;
5570
+ for (const rel of baseRels) {
5571
+ if (rel?.Type !== RelType.VmlDrawing || !rel.Target) {
5572
+ continue;
5573
+ }
5574
+ const vmlPath = resolveRelTarget(baseDir, rel.Target);
5575
+ if (emittedVmlPaths.has(vmlPath)) {
5576
+ continue;
5577
+ }
5578
+ const vmlModel = model.vmlDrawings[vmlPath];
5579
+ if (!vmlModel) {
5580
+ continue;
5581
+ }
5582
+ emittedVmlPaths.add(vmlPath);
5583
+ await this._renderToZip(zip, vmlPath, vmlDrawingXform, vmlModel);
5584
+ // `prepareChartsheets` already flipped `model.hasChartsheetVml`
5585
+ // before content-types was written, so no further signalling
5586
+ // is needed here.
5587
+ }
5588
+ }
5589
+ }
5590
+ }
1603
5591
  async addDrawings(zip, model) {
1604
5592
  const drawingXform = new DrawingXform();
1605
5593
  const relsXform = new RelationshipsXform();
1606
- const rawDrawings = model.rawDrawings || {};
1607
5594
  for (const worksheet of model.worksheets) {
1608
5595
  const { drawing } = worksheet;
1609
5596
  if (drawing) {
1610
- // Check if drawing rels contain chart references using helper
1611
- const hasChartReference = this.drawingHasChartReference(drawing);
1612
- if (hasChartReference && rawDrawings[drawing.name]) {
1613
- // Use raw data for drawings with chart references (passthrough)
1614
- zip.append(rawDrawings[drawing.name], { name: drawingPath(drawing.name) });
5597
+ const filteredAnchors = filterDrawingAnchors(drawing.anchors ?? []);
5598
+ const drawingForWrite = drawing.anchors
5599
+ ? { ...drawing, anchors: filteredAnchors }
5600
+ : drawing;
5601
+ drawingXform.prepare(drawingForWrite);
5602
+ await this._renderToZip(zip, drawingPath(drawing.name), drawingXform, drawingForWrite);
5603
+ await this._renderToZip(zip, drawingRelsPath(drawing.name), relsXform, drawing.rels);
5604
+ }
5605
+ }
5606
+ // Chartsheet drawings — each chartsheet references a drawing
5607
+ // containing a single chart that fills the entire sheet. Unlike
5608
+ // worksheet-embedded charts (where a `<xdr:twoCellAnchor>` with
5609
+ // `<xdr:from>/<xdr:to>` cell references pins the chart to a
5610
+ // rectangle of cells, whose dimensions Excel computes from the
5611
+ // sheet's column widths and row heights), a chartsheet has no
5612
+ // cell grid — its `sheetData` is empty. A cell-based anchor on
5613
+ // a chartsheet therefore resolves to a 0×0 rectangle and Excel
5614
+ // renders an empty white canvas instead of the chart.
5615
+ //
5616
+ // Excel's own output for chartsheet drawings uses
5617
+ // `<xdr:absoluteAnchor>` with concrete EMU `pos`/`ext` values
5618
+ // (≈ 10.84″ × 6.67″ — standard A4 landscape minus default
5619
+ // margins), AND repeats the same `<a:ext>` on the inner
5620
+ // `<xdr:graphicFrame>/<xdr:xfrm>` so both the anchor-level and
5621
+ // frame-level sizes are non-zero. Omitting either produces the
5622
+ // blank-canvas rendering bug users see. We emit the same byte
5623
+ // layout here verbatim rather than route through `DrawingXform`,
5624
+ // which is tuned for the worksheet twoCellAnchor case.
5625
+ const CHARTSHEET_EMU_CX = 9906000; // ≈ 10.84 inches
5626
+ const CHARTSHEET_EMU_CY = 6096000; // ≈ 6.67 inches
5627
+ for (const cs of model.chartsheets || []) {
5628
+ if (cs.drawingName && (cs.chartNumber || cs.chartExNumber)) {
5629
+ const chartRId = "rId1";
5630
+ const isChartEx = !cs.chartNumber && !!cs.chartExNumber;
5631
+ const chartName = isChartEx ? `Chart ${cs.chartExNumber}` : `Chart ${cs.chartNumber}`;
5632
+ const drawingXml = renderChartsheetDrawingXml({
5633
+ chartRId,
5634
+ chartName,
5635
+ isChartEx,
5636
+ extCx: CHARTSHEET_EMU_CX,
5637
+ extCy: CHARTSHEET_EMU_CY
5638
+ });
5639
+ const drawingRels = [
5640
+ {
5641
+ Id: chartRId,
5642
+ Type: isChartEx ? RelType.ChartEx : RelType.Chart,
5643
+ Target: isChartEx
5644
+ ? chartExRelTargetFromDrawing(cs.chartExNumber)
5645
+ : chartRelTargetFromDrawing(cs.chartNumber)
5646
+ }
5647
+ ];
5648
+ zip.append(drawingXml, { name: drawingPath(cs.drawingName) });
5649
+ await this._renderToZip(zip, drawingRelsPath(cs.drawingName), relsXform, drawingRels);
5650
+ }
5651
+ }
5652
+ }
5653
+ async addCharts(zip, model, strictTemplateMode = false) {
5654
+ const relsXform = new RelationshipsXform();
5655
+ for (const [n, chartEntry] of Object.entries(model.chartEntries || {})) {
5656
+ if (shouldPassthroughChartEntry(chartEntry)) {
5657
+ zip.append(chartEntry.rawData, { name: chartPath(n) });
5658
+ }
5659
+ else {
5660
+ const requireRawPatch = shouldRequireChartRawPatch(chartEntry, strictTemplateMode);
5661
+ const patched = tryPatchChartRawXml(chartEntry, requireRawPatch);
5662
+ if (patched) {
5663
+ zip.append(patched, { name: chartPath(n) });
1615
5664
  }
1616
5665
  else {
1617
- // Use regenerated XML for normal drawings (images, shapes)
1618
- const filteredAnchors = filterDrawingAnchors(drawing.anchors ?? []);
1619
- const drawingForWrite = drawing.anchors
1620
- ? { ...drawing, anchors: filteredAnchors }
1621
- : drawing;
1622
- drawingXform.prepare(drawingForWrite);
1623
- await this._renderToZip(zip, drawingPath(drawing.name), drawingXform, drawingForWrite);
5666
+ if (requireRawPatch) {
5667
+ throw new ChartOptionsError(buildChartStrictFailureMessage(n, chartEntry.model));
5668
+ }
5669
+ // Render via buffered path so we can splice preserved leading
5670
+ // XML comments (e.g. vendor provenance markers) from the
5671
+ // original raw bytes back in front of `<c:chart>`. The SAX-
5672
+ // backed xform parser drops `comment` events so the
5673
+ // structured model has no memory of them.
5674
+ const buffered = renderChartWithLeadingComments(chartEntry, getChartSupport().createChartSpaceXform());
5675
+ zip.append(buffered, { name: chartPath(n) });
1624
5676
  }
1625
- await this._renderToZip(zip, drawingRelsPath(drawing.name), relsXform, drawing.rels);
1626
5677
  }
5678
+ // Write chart style (raw bytes)
5679
+ if (model.chartStyles?.[n]) {
5680
+ zip.append(model.chartStyles[n], { name: chartStylePath(n) });
5681
+ }
5682
+ // Write chart colors (raw bytes)
5683
+ if (model.chartColors?.[n]) {
5684
+ zip.append(model.chartColors[n], { name: chartColorsPath(n) });
5685
+ }
5686
+ // Build chart rels
5687
+ const rels = [];
5688
+ // Collect original rels first (excluding style/colors which we regenerate)
5689
+ // We keep their original Ids to avoid breaking r:id references inside chart XML
5690
+ const originalRels = model.chartRels?.[n];
5691
+ const usedIds = new Set();
5692
+ if (Array.isArray(originalRels)) {
5693
+ for (const rel of originalRels) {
5694
+ if (rel.Type !== RelType.ChartStyle && rel.Type !== RelType.ChartColors) {
5695
+ rels.push(rel);
5696
+ usedIds.add(rel.Id);
5697
+ }
5698
+ }
5699
+ }
5700
+ // Fold in rels allocated during chart registration — notably the
5701
+ // image relationships added by `resolvePendingChartImages` for
5702
+ // `pictureFill.image`. The chart XML already embeds the `r:id`
5703
+ // assigned during registration, so we must preserve those ids
5704
+ // verbatim (don't rewrite) and only skip duplicates that were
5705
+ // already round-tripped through `originalRels`.
5706
+ const entryRels = chartEntry.rels;
5707
+ if (Array.isArray(entryRels)) {
5708
+ for (const rel of entryRels) {
5709
+ if (!rel?.Id || usedIds.has(rel.Id)) {
5710
+ continue;
5711
+ }
5712
+ rels.push(rel);
5713
+ usedIds.add(rel.Id);
5714
+ }
5715
+ }
5716
+ // Allocate new rIds for style/colors that don't conflict with existing ones
5717
+ let rIdCount = 1;
5718
+ const nextRId = () => {
5719
+ let id = `rId${rIdCount++}`;
5720
+ while (usedIds.has(id)) {
5721
+ id = `rId${rIdCount++}`;
5722
+ }
5723
+ usedIds.add(id);
5724
+ return id;
5725
+ };
5726
+ // Add style rel if style exists
5727
+ if (model.chartStyles?.[n]) {
5728
+ rels.push({
5729
+ Id: nextRId(),
5730
+ Type: RelType.ChartStyle,
5731
+ Target: chartStyleRelTarget(n)
5732
+ });
5733
+ }
5734
+ // Add colors rel if colors exist
5735
+ if (model.chartColors?.[n]) {
5736
+ rels.push({
5737
+ Id: nextRId(),
5738
+ Type: RelType.ChartColors,
5739
+ Target: chartColorsRelTarget(n)
5740
+ });
5741
+ }
5742
+ // Write c:userShapes overlay drawing part — preserves annotation
5743
+ // shapes attached to the chart. Bytes can come from a loaded file
5744
+ // (captured onto `chartEntry.userShapesXml` by
5745
+ // `_reconcileChartUserShapes`) or from a programmatic call to
5746
+ // `Chart.setUserShapesXml`. We always emit the bytes at a canonical
5747
+ // path (`xl/drawings/chartUserShape{n}.xml`) and rewrite the rel
5748
+ // Target accordingly so the chart XML's existing `r:id` still
5749
+ // resolves.
5750
+ if (chartEntry.userShapesXml) {
5751
+ zip.append(chartEntry.userShapesXml, { name: chartUserShapesPath(n) });
5752
+ const targetPath = chartUserShapesRelTarget(n);
5753
+ const existingRel = rels.find(r => r?.Type === RelType.ChartUserShapes);
5754
+ if (existingRel) {
5755
+ existingRel.Target = targetPath;
5756
+ }
5757
+ else {
5758
+ // No existing rel — allocate one, preferring the r:id the model
5759
+ // already embeds in `<c:userShapes r:id="…"/>` so the chart XML
5760
+ // doesn't need a rewrite.
5761
+ const relId = chartEntry.model.userShapesRelId ?? nextRId();
5762
+ usedIds.add(relId);
5763
+ rels.push({ Id: relId, Type: RelType.ChartUserShapes, Target: targetPath });
5764
+ }
5765
+ }
5766
+ // Write chart rels if any
5767
+ if (rels.length > 0) {
5768
+ await this._renderToZip(zip, chartRelsPath(n), relsXform, rels);
5769
+ }
5770
+ }
5771
+ }
5772
+ async addChartExEntries(zip, model, strictTemplateMode = false) {
5773
+ const relsXform = new RelationshipsXform();
5774
+ const rawEntries = model.chartExEntries || {};
5775
+ const structured = (model.chartExStructuredEntries ?? {});
5776
+ const written = new Set();
5777
+ // 1. Loaded chartEx entries — byte-preserve while clean, render structured XML once edited.
5778
+ for (const [n, rawBytes] of Object.entries(rawEntries)) {
5779
+ const structuredEntry = structured[n];
5780
+ if (structuredEntry && !shouldPassthroughChartExEntry(structuredEntry)) {
5781
+ const requireRawPatch = shouldRequireChartExRawPatch(structuredEntry, strictTemplateMode);
5782
+ const patched = tryPatchChartExRawXml(structuredEntry, requireRawPatch);
5783
+ if (patched) {
5784
+ zip.append(patched, { name: chartExPath(n) });
5785
+ }
5786
+ else {
5787
+ if (requireRawPatch) {
5788
+ throw new ChartOptionsError(buildChartExStrictFailureMessage(n, structuredEntry.model));
5789
+ }
5790
+ const renderedXml = getChartSupport().renderChartEx(stripChartExRawXml(structuredEntry.model));
5791
+ // Splice preserved leading XML comments from original raw
5792
+ // bytes back in front of `<cx:chart>`. The chartEx parser
5793
+ // calls `parseXml(...)` without `{ comments: true }` so the
5794
+ // structured model has no memory of them.
5795
+ const originalRawXml = rawBytes
5796
+ ? new TextDecoder().decode(rawBytes)
5797
+ : structuredEntry.model.rawXml;
5798
+ const finalXml = spliceChartExLeadingComments(renderedXml, originalRawXml);
5799
+ zip.append(finalXml, {
5800
+ name: chartExPath(n)
5801
+ });
5802
+ }
5803
+ }
5804
+ else {
5805
+ zip.append(rawBytes, { name: chartExPath(n) });
5806
+ }
5807
+ written.add(n);
5808
+ // Write chartEx rels if present
5809
+ const rels = model.chartExRels?.[n];
5810
+ const chartExRels = this._buildChartExRels(n, rels, model);
5811
+ if (chartExRels.length > 0) {
5812
+ await this._renderToZip(zip, chartExRelsPath(n), relsXform, chartExRels);
5813
+ }
5814
+ this._appendChartExSidecars(zip, model, n, structuredEntry);
5815
+ }
5816
+ // 2. Structured chartEx entries — built programmatically via addChartEx()
5817
+ for (const [n, entry] of Object.entries(structured)) {
5818
+ if (written.has(n)) {
5819
+ continue;
5820
+ }
5821
+ // Data-ref → `_xlchart.vN.M` defined-name rewrite has already
5822
+ // run in `prepareChartExSidecars` so the model's formulas now
5823
+ // point at hidden names and the cached `<cx:lvl>` levels have
5824
+ // been cleared. Force structural rebuild to pick up the
5825
+ // mutated model (any stale `rawXml` from earlier mutations
5826
+ // would mask the rewrite).
5827
+ const xml = getChartSupport().renderChartEx(entry.model, { forceStructural: true });
5828
+ zip.append(xml, { name: chartExPath(n) });
5829
+ this._appendChartExSidecars(zip, model, n, entry);
5830
+ const chartExRels = this._buildChartExRels(n, entry.rels, model, entry);
5831
+ if (chartExRels.length > 0) {
5832
+ await this._renderToZip(zip, chartExRelsPath(n), relsXform, chartExRels);
5833
+ }
5834
+ }
5835
+ }
5836
+ _appendChartExSidecars(zip, model, n, entry) {
5837
+ if (entry?.model.style) {
5838
+ zip.append(new TextEncoder().encode(getChartSupport().buildChartStyle(entry.model.style)), {
5839
+ name: chartExStylePath(n)
5840
+ });
5841
+ }
5842
+ else if (model.chartExStyles?.[n]) {
5843
+ zip.append(model.chartExStyles[n], { name: chartExStylePath(n) });
5844
+ }
5845
+ if (entry?.model.colors) {
5846
+ zip.append(new TextEncoder().encode(getChartSupport().buildChartColors(entry.model.colors)), {
5847
+ name: chartExColorsPath(n)
5848
+ });
5849
+ }
5850
+ else if (model.chartExColors?.[n]) {
5851
+ zip.append(model.chartExColors[n], { name: chartExColorsPath(n) });
5852
+ }
5853
+ }
5854
+ _buildChartExRels(n, existing, model, entry) {
5855
+ const rels = Array.isArray(existing) ? [...existing] : [];
5856
+ const usedIds = new Set(rels.map(rel => rel.Id));
5857
+ const nextRId = () => {
5858
+ let i = 1;
5859
+ while (usedIds.has(`rId${i}`)) {
5860
+ i++;
5861
+ }
5862
+ const id = `rId${i}`;
5863
+ usedIds.add(id);
5864
+ return id;
5865
+ };
5866
+ const hasStyle = !!(model.chartExStyles?.[n] || entry?.model.style);
5867
+ const hasColors = !!(model.chartExColors?.[n] || entry?.model.colors);
5868
+ if (hasStyle && !rels.some(rel => rel.Type === RelType.ChartStyle)) {
5869
+ rels.push({ Id: nextRId(), Type: RelType.ChartStyle, Target: chartExStyleRelTarget(n) });
1627
5870
  }
5871
+ if (hasColors && !rels.some(rel => rel.Type === RelType.ChartColors)) {
5872
+ rels.push({ Id: nextRId(), Type: RelType.ChartColors, Target: chartExColorsRelTarget(n) });
5873
+ }
5874
+ return rels;
1628
5875
  }
1629
5876
  async addTables(zip, model) {
1630
5877
  const tableXform = new TableXform();
@@ -1646,7 +5893,7 @@ class XLSX {
1646
5893
  * relative** `Target` whenever the user supplied one. This is the single
1647
5894
  * line that makes Office / WPS resolve the referenced workbook relative
1648
5895
  * to the current file's directory (not the `%USERPROFILE%\Documents`
1649
- * fallback) — the root of the behaviour reported in exceljs#3039.
5896
+ * fallback) — the root of the relative-path external-link behaviour.
1650
5897
  */
1651
5898
  async addExternalLinks(zip, model) {
1652
5899
  const externalLinks = (model.externalLinks ?? []);
@@ -1670,15 +5917,6 @@ class XLSX {
1670
5917
  ]);
1671
5918
  }
1672
5919
  }
1673
- /**
1674
- * Write passthrough files (charts, etc.) that were preserved during read.
1675
- * These files are written back unchanged to preserve unsupported features.
1676
- */
1677
- addPassthrough(zip, model) {
1678
- const passthroughManager = new PassthroughManager();
1679
- passthroughManager.fromRecord(model.passthrough || {});
1680
- passthroughManager.writeToZip(zip);
1681
- }
1682
5920
  async addPivotTables(zip, model) {
1683
5921
  if (!model.pivotTables.length) {
1684
5922
  return;
@@ -1785,6 +6023,20 @@ class XLSX {
1785
6023
  worksheetOptions.drawings = model.drawings = [];
1786
6024
  worksheetOptions.commentRefs = model.commentRefs = [];
1787
6025
  worksheetOptions.formControlRefs = model.formControlRefs = [];
6026
+ // Collect the list of worksheets that carry Office 365 threaded
6027
+ // comments so the Content Types override list can include them
6028
+ // and the ZIP writer knows which per-sheet parts to emit. Sheets
6029
+ // with zero threaded comments are skipped entirely — Excel treats
6030
+ // a missing part as "no threaded comments on this sheet".
6031
+ model.threadedCommentSheetIds = [];
6032
+ model.hasPersons = (model.persons?.length ?? 0) > 0;
6033
+ // Raw-passthrough parts captured on load. The Content-Types
6034
+ // override list and the content-types writer need these path
6035
+ // lists so the emitted bytes are registered in the package.
6036
+ model.slicerPartPaths = Object.keys(model.slicerParts ?? {}).filter(p => !p.includes("/_rels/"));
6037
+ model.slicerCachePartPaths = Object.keys(model.slicerCacheParts ?? {}).filter(p => !p.includes("/_rels/"));
6038
+ model.timelinePartPaths = Object.keys(model.timelineParts ?? {}).filter(p => !p.includes("/_rels/"));
6039
+ model.timelineCachePartPaths = Object.keys(model.timelineCacheParts ?? {}).filter(p => !p.includes("/_rels/"));
1788
6040
  model.hasHeaderWatermark = false;
1789
6041
  let tableCount = 0;
1790
6042
  model.tables = [];
@@ -1812,6 +6064,11 @@ class XLSX {
1812
6064
  model.tables.push(table);
1813
6065
  });
1814
6066
  worksheetXform.prepare(worksheet, worksheetOptions);
6067
+ // Register sheets that carry threaded comments so the Content
6068
+ // Types override list and the zip emission loop find them.
6069
+ if (worksheet.threadedComments && worksheet.threadedComments.length > 0) {
6070
+ model.threadedCommentSheetIds.push(worksheet.fileIndex);
6071
+ }
1815
6072
  });
1816
6073
  // ContentTypesXform expects this flag
1817
6074
  model.hasCheckboxes = model.styles.hasCheckboxes;
@@ -1833,11 +6090,98 @@ class XLSX {
1833
6090
  if (worksheetOptions.hasHeaderWatermark) {
1834
6091
  model.hasHeaderWatermark = true;
1835
6092
  }
1836
- // Build passthroughContentTypes for ContentTypesXform using PassthroughManager
1837
- const passthrough = model.passthrough || {};
1838
- const passthroughManager = new PassthroughManager();
1839
- passthroughManager.fromRecord(passthrough);
1840
- model.passthroughContentTypes = passthroughManager.getContentTypes();
6093
+ }
6094
+ prepareChartExSidecars(model) {
6095
+ const structured = (model.chartExStructuredEntries ?? {});
6096
+ for (const [n, entry] of Object.entries(structured)) {
6097
+ // Excel 2016+ requires chartEx `<cx:f>` to reference hidden
6098
+ // `_xlchart.vN.M` defined names, NOT direct worksheet ranges.
6099
+ // Walk the model and rewrite data refs BEFORE workbook.xml is
6100
+ // serialised so the newly-registered defined names end up in
6101
+ // `<definedNames>`. See `rewriteChartExDataRefsToDefinedNames`
6102
+ // for the full rationale.
6103
+ const chartExIndex = parseInt(n, 10);
6104
+ if (Number.isFinite(chartExIndex) &&
6105
+ model.definedNamesInstance &&
6106
+ typeof model.definedNamesInstance.addHidden === "function") {
6107
+ getChartSupport().rewriteChartExDataRefsToDefinedNames(entry.model, chartExIndex, (name, ref) => {
6108
+ model.definedNamesInstance.addHidden(ref, name);
6109
+ });
6110
+ // Re-materialise the array snapshot so addWorkbook picks up the
6111
+ // new hidden `_xlchart.*` names. `definedNames` in the write
6112
+ // model is the serialised form (array); the rewrite added
6113
+ // entries to the live `DefinedNames` instance on the workbook.
6114
+ model.definedNames = model.definedNamesInstance.model;
6115
+ }
6116
+ if (entry.model.style && !model.chartExStyles?.[n]) {
6117
+ model.chartExStyles ?? (model.chartExStyles = {});
6118
+ model.chartExStyles[n] = new TextEncoder().encode(getChartSupport().buildChartStyle(entry.model.style));
6119
+ }
6120
+ if (entry.model.colors && !model.chartExColors?.[n]) {
6121
+ model.chartExColors ?? (model.chartExColors = {});
6122
+ model.chartExColors[n] = new TextEncoder().encode(getChartSupport().buildChartColors(entry.model.colors));
6123
+ }
6124
+ }
6125
+ }
6126
+ prepareChartsheets(model) {
6127
+ if (!model.chartsheets || model.chartsheets.length === 0) {
6128
+ return;
6129
+ }
6130
+ const usedDrawingNumbers = new Set();
6131
+ for (const drawing of model.drawings ?? []) {
6132
+ const match = /^drawing(\d+)$/.exec(drawing.name ?? "");
6133
+ if (match) {
6134
+ usedDrawingNumbers.add(parseInt(match[1], 10));
6135
+ }
6136
+ }
6137
+ for (const cs of model.chartsheets) {
6138
+ const existingMatch = /^drawing(\d+)$/.exec(cs.drawingName ?? "");
6139
+ if (existingMatch) {
6140
+ usedDrawingNumbers.add(parseInt(existingMatch[1], 10));
6141
+ }
6142
+ }
6143
+ const nextDrawingName = () => {
6144
+ let n = 1;
6145
+ while (usedDrawingNumbers.has(n)) {
6146
+ n++;
6147
+ }
6148
+ usedDrawingNumbers.add(n);
6149
+ return `drawing${n}`;
6150
+ };
6151
+ for (const cs of model.chartsheets) {
6152
+ if (!cs.drawingName) {
6153
+ cs.drawingName = nextDrawingName();
6154
+ }
6155
+ if (!cs.drawing) {
6156
+ cs.drawing = { rId: "rId1" };
6157
+ }
6158
+ if (!model.drawings.some((drawing) => drawing.name === cs.drawingName)) {
6159
+ model.drawings.push({ name: cs.drawingName });
6160
+ }
6161
+ }
6162
+ // Signal the content-types writer that the `Default Extension="vml"`
6163
+ // declaration is required when ANY chartsheet carries a VML
6164
+ // relationship (e.g. `<legacyDrawing r:id="…"/>` referencing a
6165
+ // preserved `xl/drawings/vmlDrawing*.vml` part). Previously the
6166
+ // flag was only set inside `addChartsheets`, which runs AFTER
6167
+ // `addContentTypes` — so a chartsheet-only VML dependency silently
6168
+ // shipped without its content-type declaration, and Excel refused
6169
+ // to open the resulting package. Compute it here, during `prepare`,
6170
+ // before any part is written.
6171
+ if (model.vmlDrawings) {
6172
+ for (const cs of model.chartsheets) {
6173
+ if (!Array.isArray(cs.relationships)) {
6174
+ continue;
6175
+ }
6176
+ const hasVmlRel = cs.relationships.some((rel) => rel?.Type === RelType.VmlDrawing &&
6177
+ typeof rel.Target === "string" &&
6178
+ model.vmlDrawings[resolveRelTarget("xl/chartsheets/", rel.Target)] !== undefined);
6179
+ if (hasVmlRel) {
6180
+ model.hasChartsheetVml = true;
6181
+ break;
6182
+ }
6183
+ }
6184
+ }
1841
6185
  }
1842
6186
  }
1843
6187
  XLSX.RelType = RelType;