@rowsncolumns/toolkit 24.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/LICENSE.md +100 -0
  2. package/Readme.md +27 -0
  3. package/dist/cjs/autofilter-condition.d.ts +43 -0
  4. package/dist/cjs/autofilter-condition.d.ts.map +1 -0
  5. package/dist/cjs/autofilter-condition.js +155 -0
  6. package/dist/cjs/csv-export.d.ts +49 -0
  7. package/dist/cjs/csv-export.d.ts.map +1 -0
  8. package/dist/cjs/csv-export.js +142 -0
  9. package/dist/cjs/csv-import-worker.d.ts +16 -0
  10. package/dist/cjs/csv-import-worker.d.ts.map +1 -0
  11. package/dist/cjs/csv-import-worker.js +60 -0
  12. package/dist/cjs/csv-import.d.ts +38 -0
  13. package/dist/cjs/csv-import.d.ts.map +1 -0
  14. package/dist/cjs/csv-import.js +166 -0
  15. package/dist/cjs/csv-parser/helpers.d.ts +14 -0
  16. package/dist/cjs/csv-parser/helpers.d.ts.map +1 -0
  17. package/dist/cjs/csv-parser/helpers.js +145 -0
  18. package/dist/cjs/csv-parser/types.d.ts +66 -0
  19. package/dist/cjs/csv-parser/types.d.ts.map +1 -0
  20. package/dist/cjs/csv-parser/types.js +14 -0
  21. package/dist/cjs/csv-parser/worker.d.ts +2 -0
  22. package/dist/cjs/csv-parser/worker.d.ts.map +1 -0
  23. package/dist/cjs/csv-parser/worker.js +200 -0
  24. package/dist/cjs/excel-export.d.ts +11 -0
  25. package/dist/cjs/excel-export.d.ts.map +1 -0
  26. package/dist/cjs/excel-export.js +22 -0
  27. package/dist/cjs/excel-import-session.d.ts +51 -0
  28. package/dist/cjs/excel-import-session.d.ts.map +1 -0
  29. package/dist/cjs/excel-import-session.js +307 -0
  30. package/dist/cjs/excel-import-worker.d.ts +7 -0
  31. package/dist/cjs/excel-import-worker.d.ts.map +1 -0
  32. package/dist/cjs/excel-import-worker.js +119 -0
  33. package/dist/cjs/excel-parser/builtin-numfmts.d.ts +24 -0
  34. package/dist/cjs/excel-parser/builtin-numfmts.d.ts.map +1 -0
  35. package/dist/cjs/excel-parser/builtin-numfmts.js +63 -0
  36. package/dist/cjs/excel-parser/cfb-decryptor.d.ts +53 -0
  37. package/dist/cjs/excel-parser/cfb-decryptor.d.ts.map +1 -0
  38. package/dist/cjs/excel-parser/cfb-decryptor.js +553 -0
  39. package/dist/cjs/excel-parser/cfb-encryptor.d.ts +23 -0
  40. package/dist/cjs/excel-parser/cfb-encryptor.d.ts.map +1 -0
  41. package/dist/cjs/excel-parser/cfb-encryptor.js +200 -0
  42. package/dist/cjs/excel-parser/dv-cell-stamp.d.ts +29 -0
  43. package/dist/cjs/excel-parser/dv-cell-stamp.d.ts.map +1 -0
  44. package/dist/cjs/excel-parser/dv-cell-stamp.js +239 -0
  45. package/dist/cjs/excel-parser/helpers.d.ts +28 -0
  46. package/dist/cjs/excel-parser/helpers.d.ts.map +1 -0
  47. package/dist/cjs/excel-parser/helpers.js +188 -0
  48. package/dist/cjs/excel-parser/interface.d.ts +179 -0
  49. package/dist/cjs/excel-parser/interface.d.ts.map +1 -0
  50. package/dist/cjs/excel-parser/interface.js +1 -0
  51. package/dist/cjs/excel-parser/plugins/base/index.d.ts +31 -0
  52. package/dist/cjs/excel-parser/plugins/base/index.d.ts.map +1 -0
  53. package/dist/cjs/excel-parser/plugins/base/index.js +101 -0
  54. package/dist/cjs/excel-parser/plugins/ods/conditional-formatting.d.ts +16 -0
  55. package/dist/cjs/excel-parser/plugins/ods/conditional-formatting.d.ts.map +1 -0
  56. package/dist/cjs/excel-parser/plugins/ods/conditional-formatting.js +488 -0
  57. package/dist/cjs/excel-parser/plugins/ods/data-validations.d.ts +15 -0
  58. package/dist/cjs/excel-parser/plugins/ods/data-validations.d.ts.map +1 -0
  59. package/dist/cjs/excel-parser/plugins/ods/data-validations.js +353 -0
  60. package/dist/cjs/excel-parser/plugins/ods/formula.d.ts +8 -0
  61. package/dist/cjs/excel-parser/plugins/ods/formula.d.ts.map +1 -0
  62. package/dist/cjs/excel-parser/plugins/ods/formula.js +82 -0
  63. package/dist/cjs/excel-parser/plugins/ods/index.d.ts +122 -0
  64. package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -0
  65. package/dist/cjs/excel-parser/plugins/ods/index.js +3642 -0
  66. package/dist/cjs/excel-parser/plugins/ods/sparklines.d.ts +25 -0
  67. package/dist/cjs/excel-parser/plugins/ods/sparklines.d.ts.map +1 -0
  68. package/dist/cjs/excel-parser/plugins/ods/sparklines.js +221 -0
  69. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +60 -0
  70. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -0
  71. package/dist/cjs/excel-parser/plugins/ods/styles.js +808 -0
  72. package/dist/cjs/excel-parser/plugins/ods/types.d.ts +7 -0
  73. package/dist/cjs/excel-parser/plugins/ods/types.d.ts.map +1 -0
  74. package/dist/cjs/excel-parser/plugins/ods/types.js +1 -0
  75. package/dist/cjs/excel-parser/plugins/xls/index.d.ts +5 -0
  76. package/dist/cjs/excel-parser/plugins/xls/index.d.ts.map +1 -0
  77. package/dist/cjs/excel-parser/plugins/xls/index.js +19 -0
  78. package/dist/cjs/excel-parser/plugins/xlsb/biff-record-reader.d.ts +13 -0
  79. package/dist/cjs/excel-parser/plugins/xlsb/biff-record-reader.d.ts.map +1 -0
  80. package/dist/cjs/excel-parser/plugins/xlsb/biff-record-reader.js +49 -0
  81. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +117 -0
  82. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -0
  83. package/dist/cjs/excel-parser/plugins/xlsb/index.js +2972 -0
  84. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts +18 -0
  85. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -0
  86. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.js +527 -0
  87. package/dist/cjs/excel-parser/plugins/xlsm/index.d.ts +5 -0
  88. package/dist/cjs/excel-parser/plugins/xlsm/index.d.ts.map +1 -0
  89. package/dist/cjs/excel-parser/plugins/xlsm/index.js +10 -0
  90. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +60 -0
  91. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -0
  92. package/dist/cjs/excel-parser/plugins/xlsx/charts.js +2178 -0
  93. package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.d.ts +18 -0
  94. package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.d.ts.map +1 -0
  95. package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.js +620 -0
  96. package/dist/cjs/excel-parser/plugins/xlsx/data-validations.d.ts +4 -0
  97. package/dist/cjs/excel-parser/plugins/xlsx/data-validations.d.ts.map +1 -0
  98. package/dist/cjs/excel-parser/plugins/xlsx/data-validations.js +515 -0
  99. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +39 -0
  100. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -0
  101. package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +941 -0
  102. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +320 -0
  103. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -0
  104. package/dist/cjs/excel-parser/plugins/xlsx/index.js +6056 -0
  105. package/dist/cjs/excel-parser/plugins/xlsx/numfmt.d.ts +2 -0
  106. package/dist/cjs/excel-parser/plugins/xlsx/numfmt.d.ts.map +1 -0
  107. package/dist/cjs/excel-parser/plugins/xlsx/numfmt.js +3 -0
  108. package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +40 -0
  109. package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -0
  110. package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +342 -0
  111. package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +8 -0
  112. package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -0
  113. package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +26 -0
  114. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts +26 -0
  115. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -0
  116. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.js +59 -0
  117. package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts +44 -0
  118. package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts.map +1 -0
  119. package/dist/cjs/excel-parser/plugins/xlsx/styles.js +1377 -0
  120. package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +50 -0
  121. package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts.map +1 -0
  122. package/dist/cjs/excel-parser/plugins/xlsx/theme.js +150 -0
  123. package/dist/cjs/excel-parser/plugins/xlsx/threaded-comments.d.ts +47 -0
  124. package/dist/cjs/excel-parser/plugins/xlsx/threaded-comments.d.ts.map +1 -0
  125. package/dist/cjs/excel-parser/plugins/xlsx/threaded-comments.js +141 -0
  126. package/dist/cjs/excel-parser/plugins/xlsx/value-type-detection.d.ts +44 -0
  127. package/dist/cjs/excel-parser/plugins/xlsx/value-type-detection.d.ts.map +1 -0
  128. package/dist/cjs/excel-parser/plugins/xlsx/value-type-detection.js +407 -0
  129. package/dist/cjs/excel-parser/registry.d.ts +5 -0
  130. package/dist/cjs/excel-parser/registry.d.ts.map +1 -0
  131. package/dist/cjs/excel-parser/registry.js +21 -0
  132. package/dist/cjs/excel-parser/style-registry.d.ts +51 -0
  133. package/dist/cjs/excel-parser/style-registry.d.ts.map +1 -0
  134. package/dist/cjs/excel-parser/style-registry.js +175 -0
  135. package/dist/cjs/excel-parser/theme-default.d.ts +3 -0
  136. package/dist/cjs/excel-parser/theme-default.d.ts.map +1 -0
  137. package/dist/cjs/excel-parser/theme-default.js +19 -0
  138. package/dist/cjs/excel-parser/types.d.ts +365 -0
  139. package/dist/cjs/excel-parser/types.d.ts.map +1 -0
  140. package/dist/cjs/excel-parser/types.js +59 -0
  141. package/dist/cjs/excel-parser/utils.d.ts +6 -0
  142. package/dist/cjs/excel-parser/utils.d.ts.map +1 -0
  143. package/dist/cjs/excel-parser/utils.js +30 -0
  144. package/dist/cjs/excel-parser/workbook.d.ts +51 -0
  145. package/dist/cjs/excel-parser/workbook.d.ts.map +1 -0
  146. package/dist/cjs/excel-parser/workbook.js +317 -0
  147. package/dist/cjs/excel-parser/worker.d.ts +2 -0
  148. package/dist/cjs/excel-parser/worker.d.ts.map +1 -0
  149. package/dist/cjs/excel-parser/worker.js +1149 -0
  150. package/dist/cjs/excel-parser/zip-parser-streaming.d.ts +13 -0
  151. package/dist/cjs/excel-parser/zip-parser-streaming.d.ts.map +1 -0
  152. package/dist/cjs/excel-parser/zip-parser-streaming.js +200 -0
  153. package/dist/cjs/excel-parser/zip-parser.d.ts +11 -0
  154. package/dist/cjs/excel-parser/zip-parser.d.ts.map +1 -0
  155. package/dist/cjs/excel-parser/zip-parser.js +49 -0
  156. package/dist/cjs/exporter/invalid-data-validation.d.ts +19 -0
  157. package/dist/cjs/exporter/invalid-data-validation.d.ts.map +1 -0
  158. package/dist/cjs/exporter/invalid-data-validation.js +39 -0
  159. package/dist/cjs/exporter/ods/ods-export.d.ts +26 -0
  160. package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -0
  161. package/dist/cjs/exporter/ods/ods-export.js +3714 -0
  162. package/dist/cjs/exporter/ods/sparkline-builder.d.ts +33 -0
  163. package/dist/cjs/exporter/ods/sparkline-builder.d.ts.map +1 -0
  164. package/dist/cjs/exporter/ods/sparkline-builder.js +221 -0
  165. package/dist/cjs/exporter/xlsx/chart-builder.d.ts +3 -0
  166. package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -0
  167. package/dist/cjs/exporter/xlsx/chart-builder.js +1901 -0
  168. package/dist/cjs/exporter/xlsx/chart-templates.d.ts +1781 -0
  169. package/dist/cjs/exporter/xlsx/chart-templates.d.ts.map +1 -0
  170. package/dist/cjs/exporter/xlsx/chart-templates.js +1798 -0
  171. package/dist/cjs/exporter/xlsx/future-functions.d.ts +10 -0
  172. package/dist/cjs/exporter/xlsx/future-functions.d.ts.map +1 -0
  173. package/dist/cjs/exporter/xlsx/future-functions.js +264 -0
  174. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts +58 -0
  175. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -0
  176. package/dist/cjs/exporter/xlsx/pivot-builder.js +277 -0
  177. package/dist/cjs/exporter/xlsx/power-query-builder.d.ts +26 -0
  178. package/dist/cjs/exporter/xlsx/power-query-builder.d.ts.map +1 -0
  179. package/dist/cjs/exporter/xlsx/power-query-builder.js +306 -0
  180. package/dist/cjs/exporter/xlsx/slicer-builder.d.ts +11 -0
  181. package/dist/cjs/exporter/xlsx/slicer-builder.d.ts.map +1 -0
  182. package/dist/cjs/exporter/xlsx/slicer-builder.js +87 -0
  183. package/dist/cjs/exporter/xlsx/sparkline-builder.d.ts +61 -0
  184. package/dist/cjs/exporter/xlsx/sparkline-builder.d.ts.map +1 -0
  185. package/dist/cjs/exporter/xlsx/sparkline-builder.js +489 -0
  186. package/dist/cjs/exporter/xlsx/style-builder.d.ts +93 -0
  187. package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -0
  188. package/dist/cjs/exporter/xlsx/style-builder.js +813 -0
  189. package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts +58 -0
  190. package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -0
  191. package/dist/cjs/exporter/xlsx/threaded-comments-builder.js +189 -0
  192. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +121 -0
  193. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -0
  194. package/dist/cjs/exporter/xlsx/xlsx-export.js +4596 -0
  195. package/dist/cjs/helpers/theme-xml-generator.d.ts +8 -0
  196. package/dist/cjs/helpers/theme-xml-generator.d.ts.map +1 -0
  197. package/dist/cjs/helpers/theme-xml-generator.js +33 -0
  198. package/dist/cjs/helpers.d.ts +34 -0
  199. package/dist/cjs/helpers.d.ts.map +1 -0
  200. package/dist/cjs/helpers.js +189 -0
  201. package/dist/cjs/index.d.ts +16 -0
  202. package/dist/cjs/index.d.ts.map +1 -0
  203. package/dist/cjs/index.js +15 -0
  204. package/dist/cjs/indexed-colors.d.ts +2 -0
  205. package/dist/cjs/indexed-colors.d.ts.map +1 -0
  206. package/dist/cjs/indexed-colors.js +75 -0
  207. package/dist/cjs/ods-export.d.ts +24 -0
  208. package/dist/cjs/ods-export.d.ts.map +1 -0
  209. package/dist/cjs/ods-export.js +23 -0
  210. package/dist/cjs/parse.d.ts +24 -0
  211. package/dist/cjs/parse.d.ts.map +1 -0
  212. package/dist/cjs/parse.js +68 -0
  213. package/dist/cjs/power-query.d.ts +41 -0
  214. package/dist/cjs/power-query.d.ts.map +1 -0
  215. package/dist/cjs/power-query.js +22 -0
  216. package/dist/cjs/server.d.ts +9 -0
  217. package/dist/cjs/server.d.ts.map +1 -0
  218. package/dist/cjs/server.js +8 -0
  219. package/dist/cjs/theme1.d.ts +3 -0
  220. package/dist/cjs/theme1.d.ts.map +1 -0
  221. package/dist/cjs/theme1.js +2 -0
  222. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  223. package/dist/cjs/types.d.ts +14 -0
  224. package/dist/cjs/types.d.ts.map +1 -0
  225. package/dist/cjs/types.js +14 -0
  226. package/dist/cjs/vitest.config.d.ts +3 -0
  227. package/dist/cjs/vitest.config.d.ts.map +1 -0
  228. package/dist/cjs/vitest.config.js +16 -0
  229. package/dist/esm/autofilter-condition.d.ts +43 -0
  230. package/dist/esm/autofilter-condition.d.ts.map +1 -0
  231. package/dist/esm/autofilter-condition.js +145 -0
  232. package/dist/esm/csv-export.d.ts +49 -0
  233. package/dist/esm/csv-export.d.ts.map +1 -0
  234. package/dist/esm/csv-export.js +126 -0
  235. package/dist/esm/csv-import-worker.d.ts +16 -0
  236. package/dist/esm/csv-import-worker.d.ts.map +1 -0
  237. package/dist/esm/csv-import-worker.js +60 -0
  238. package/dist/esm/csv-import.d.ts +38 -0
  239. package/dist/esm/csv-import.d.ts.map +1 -0
  240. package/dist/esm/csv-import.js +132 -0
  241. package/dist/esm/csv-parser/helpers.d.ts +14 -0
  242. package/dist/esm/csv-parser/helpers.d.ts.map +1 -0
  243. package/dist/esm/csv-parser/helpers.js +0 -0
  244. package/dist/esm/csv-parser/types.d.ts +66 -0
  245. package/dist/esm/csv-parser/types.d.ts.map +1 -0
  246. package/dist/esm/csv-parser/types.js +14 -0
  247. package/dist/esm/csv-parser/worker.d.ts +2 -0
  248. package/dist/esm/csv-parser/worker.d.ts.map +1 -0
  249. package/dist/esm/csv-parser/worker.js +154 -0
  250. package/dist/esm/excel-export.d.ts +11 -0
  251. package/dist/esm/excel-export.d.ts.map +1 -0
  252. package/dist/esm/excel-export.js +13 -0
  253. package/dist/esm/excel-import-session.d.ts +51 -0
  254. package/dist/esm/excel-import-session.d.ts.map +1 -0
  255. package/dist/esm/excel-import-session.js +255 -0
  256. package/dist/esm/excel-import-worker.d.ts +7 -0
  257. package/dist/esm/excel-import-worker.d.ts.map +1 -0
  258. package/dist/esm/excel-import-worker.js +116 -0
  259. package/dist/esm/excel-parser/builtin-numfmts.d.ts +24 -0
  260. package/dist/esm/excel-parser/builtin-numfmts.d.ts.map +1 -0
  261. package/dist/esm/excel-parser/builtin-numfmts.js +59 -0
  262. package/dist/esm/excel-parser/cfb-decryptor.d.ts +53 -0
  263. package/dist/esm/excel-parser/cfb-decryptor.d.ts.map +1 -0
  264. package/dist/esm/excel-parser/cfb-decryptor.js +493 -0
  265. package/dist/esm/excel-parser/cfb-encryptor.d.ts +23 -0
  266. package/dist/esm/excel-parser/cfb-encryptor.d.ts.map +1 -0
  267. package/dist/esm/excel-parser/cfb-encryptor.js +181 -0
  268. package/dist/esm/excel-parser/dv-cell-stamp.d.ts +29 -0
  269. package/dist/esm/excel-parser/dv-cell-stamp.d.ts.map +1 -0
  270. package/dist/esm/excel-parser/dv-cell-stamp.js +155 -0
  271. package/dist/esm/excel-parser/helpers.d.ts +28 -0
  272. package/dist/esm/excel-parser/helpers.d.ts.map +1 -0
  273. package/dist/esm/excel-parser/helpers.js +160 -0
  274. package/dist/esm/excel-parser/interface.d.ts +179 -0
  275. package/dist/esm/excel-parser/interface.d.ts.map +1 -0
  276. package/dist/esm/excel-parser/interface.js +1 -0
  277. package/dist/esm/excel-parser/plugins/base/index.d.ts +31 -0
  278. package/dist/esm/excel-parser/plugins/base/index.d.ts.map +1 -0
  279. package/dist/esm/excel-parser/plugins/base/index.js +64 -0
  280. package/dist/esm/excel-parser/plugins/ods/conditional-formatting.d.ts +16 -0
  281. package/dist/esm/excel-parser/plugins/ods/conditional-formatting.d.ts.map +1 -0
  282. package/dist/esm/excel-parser/plugins/ods/conditional-formatting.js +405 -0
  283. package/dist/esm/excel-parser/plugins/ods/data-validations.d.ts +15 -0
  284. package/dist/esm/excel-parser/plugins/ods/data-validations.d.ts.map +1 -0
  285. package/dist/esm/excel-parser/plugins/ods/data-validations.js +331 -0
  286. package/dist/esm/excel-parser/plugins/ods/formula.d.ts +8 -0
  287. package/dist/esm/excel-parser/plugins/ods/formula.d.ts.map +1 -0
  288. package/dist/esm/excel-parser/plugins/ods/formula.js +82 -0
  289. package/dist/esm/excel-parser/plugins/ods/index.d.ts +122 -0
  290. package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -0
  291. package/dist/esm/excel-parser/plugins/ods/index.js +2896 -0
  292. package/dist/esm/excel-parser/plugins/ods/sparklines.d.ts +25 -0
  293. package/dist/esm/excel-parser/plugins/ods/sparklines.d.ts.map +1 -0
  294. package/dist/esm/excel-parser/plugins/ods/sparklines.js +188 -0
  295. package/dist/esm/excel-parser/plugins/ods/styles.d.ts +60 -0
  296. package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -0
  297. package/dist/esm/excel-parser/plugins/ods/styles.js +732 -0
  298. package/dist/esm/excel-parser/plugins/ods/types.d.ts +7 -0
  299. package/dist/esm/excel-parser/plugins/ods/types.d.ts.map +1 -0
  300. package/dist/esm/excel-parser/plugins/ods/types.js +1 -0
  301. package/dist/esm/excel-parser/plugins/xls/index.d.ts +5 -0
  302. package/dist/esm/excel-parser/plugins/xls/index.d.ts.map +1 -0
  303. package/dist/esm/excel-parser/plugins/xls/index.js +7 -0
  304. package/dist/esm/excel-parser/plugins/xlsb/biff-record-reader.d.ts +13 -0
  305. package/dist/esm/excel-parser/plugins/xlsb/biff-record-reader.d.ts.map +1 -0
  306. package/dist/esm/excel-parser/plugins/xlsb/biff-record-reader.js +48 -0
  307. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +117 -0
  308. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -0
  309. package/dist/esm/excel-parser/plugins/xlsb/index.js +2339 -0
  310. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts +18 -0
  311. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -0
  312. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.js +522 -0
  313. package/dist/esm/excel-parser/plugins/xlsm/index.d.ts +5 -0
  314. package/dist/esm/excel-parser/plugins/xlsm/index.d.ts.map +1 -0
  315. package/dist/esm/excel-parser/plugins/xlsm/index.js +6 -0
  316. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +60 -0
  317. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -0
  318. package/dist/esm/excel-parser/plugins/xlsx/charts.js +2036 -0
  319. package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.d.ts +18 -0
  320. package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.d.ts.map +1 -0
  321. package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.js +621 -0
  322. package/dist/esm/excel-parser/plugins/xlsx/data-validations.d.ts +4 -0
  323. package/dist/esm/excel-parser/plugins/xlsx/data-validations.d.ts.map +1 -0
  324. package/dist/esm/excel-parser/plugins/xlsx/data-validations.js +493 -0
  325. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +39 -0
  326. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -0
  327. package/dist/esm/excel-parser/plugins/xlsx/drawing.js +905 -0
  328. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +320 -0
  329. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -0
  330. package/dist/esm/excel-parser/plugins/xlsx/index.js +4654 -0
  331. package/dist/esm/excel-parser/plugins/xlsx/numfmt.d.ts +2 -0
  332. package/dist/esm/excel-parser/plugins/xlsx/numfmt.d.ts.map +1 -0
  333. package/dist/esm/excel-parser/plugins/xlsx/numfmt.js +3 -0
  334. package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +40 -0
  335. package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -0
  336. package/dist/esm/excel-parser/plugins/xlsx/pivots.js +254 -0
  337. package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +8 -0
  338. package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -0
  339. package/dist/esm/excel-parser/plugins/xlsx/relationships.js +13 -0
  340. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts +26 -0
  341. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -0
  342. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.js +64 -0
  343. package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts +44 -0
  344. package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts.map +1 -0
  345. package/dist/esm/excel-parser/plugins/xlsx/styles.js +1365 -0
  346. package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +50 -0
  347. package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts.map +1 -0
  348. package/dist/esm/excel-parser/plugins/xlsx/theme.js +127 -0
  349. package/dist/esm/excel-parser/plugins/xlsx/threaded-comments.d.ts +47 -0
  350. package/dist/esm/excel-parser/plugins/xlsx/threaded-comments.d.ts.map +1 -0
  351. package/dist/esm/excel-parser/plugins/xlsx/threaded-comments.js +103 -0
  352. package/dist/esm/excel-parser/plugins/xlsx/value-type-detection.d.ts +44 -0
  353. package/dist/esm/excel-parser/plugins/xlsx/value-type-detection.d.ts.map +1 -0
  354. package/dist/esm/excel-parser/plugins/xlsx/value-type-detection.js +399 -0
  355. package/dist/esm/excel-parser/registry.d.ts +5 -0
  356. package/dist/esm/excel-parser/registry.d.ts.map +1 -0
  357. package/dist/esm/excel-parser/registry.js +20 -0
  358. package/dist/esm/excel-parser/style-registry.d.ts +51 -0
  359. package/dist/esm/excel-parser/style-registry.d.ts.map +1 -0
  360. package/dist/esm/excel-parser/style-registry.js +163 -0
  361. package/dist/esm/excel-parser/theme-default.d.ts +3 -0
  362. package/dist/esm/excel-parser/theme-default.d.ts.map +1 -0
  363. package/dist/esm/excel-parser/theme-default.js +19 -0
  364. package/dist/esm/excel-parser/types.d.ts +365 -0
  365. package/dist/esm/excel-parser/types.d.ts.map +1 -0
  366. package/dist/esm/excel-parser/types.js +59 -0
  367. package/dist/esm/excel-parser/utils.d.ts +6 -0
  368. package/dist/esm/excel-parser/utils.d.ts.map +1 -0
  369. package/dist/esm/excel-parser/utils.js +25 -0
  370. package/dist/esm/excel-parser/workbook.d.ts +51 -0
  371. package/dist/esm/excel-parser/workbook.d.ts.map +1 -0
  372. package/dist/esm/excel-parser/workbook.js +192 -0
  373. package/dist/esm/excel-parser/worker.d.ts +2 -0
  374. package/dist/esm/excel-parser/worker.d.ts.map +1 -0
  375. package/dist/esm/excel-parser/worker.js +862 -0
  376. package/dist/esm/excel-parser/zip-parser-streaming.d.ts +13 -0
  377. package/dist/esm/excel-parser/zip-parser-streaming.d.ts.map +1 -0
  378. package/dist/esm/excel-parser/zip-parser-streaming.js +119 -0
  379. package/dist/esm/excel-parser/zip-parser.d.ts +11 -0
  380. package/dist/esm/excel-parser/zip-parser.d.ts.map +1 -0
  381. package/dist/esm/excel-parser/zip-parser.js +43 -0
  382. package/dist/esm/exporter/invalid-data-validation.d.ts +19 -0
  383. package/dist/esm/exporter/invalid-data-validation.d.ts.map +1 -0
  384. package/dist/esm/exporter/invalid-data-validation.js +37 -0
  385. package/dist/esm/exporter/ods/ods-export.d.ts +26 -0
  386. package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -0
  387. package/dist/esm/exporter/ods/ods-export.js +3238 -0
  388. package/dist/esm/exporter/ods/sparkline-builder.d.ts +33 -0
  389. package/dist/esm/exporter/ods/sparkline-builder.d.ts.map +1 -0
  390. package/dist/esm/exporter/ods/sparkline-builder.js +165 -0
  391. package/dist/esm/exporter/xlsx/chart-builder.d.ts +3 -0
  392. package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -0
  393. package/dist/esm/exporter/xlsx/chart-builder.js +1805 -0
  394. package/dist/esm/exporter/xlsx/chart-templates.d.ts +1781 -0
  395. package/dist/esm/exporter/xlsx/chart-templates.d.ts.map +1 -0
  396. package/dist/esm/exporter/xlsx/chart-templates.js +1798 -0
  397. package/dist/esm/exporter/xlsx/future-functions.d.ts +10 -0
  398. package/dist/esm/exporter/xlsx/future-functions.d.ts.map +1 -0
  399. package/dist/esm/exporter/xlsx/future-functions.js +264 -0
  400. package/dist/esm/exporter/xlsx/pivot-builder.d.ts +58 -0
  401. package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -0
  402. package/dist/esm/exporter/xlsx/pivot-builder.js +230 -0
  403. package/dist/esm/exporter/xlsx/power-query-builder.d.ts +26 -0
  404. package/dist/esm/exporter/xlsx/power-query-builder.d.ts.map +1 -0
  405. package/dist/esm/exporter/xlsx/power-query-builder.js +257 -0
  406. package/dist/esm/exporter/xlsx/slicer-builder.d.ts +11 -0
  407. package/dist/esm/exporter/xlsx/slicer-builder.d.ts.map +1 -0
  408. package/dist/esm/exporter/xlsx/slicer-builder.js +84 -0
  409. package/dist/esm/exporter/xlsx/sparkline-builder.d.ts +61 -0
  410. package/dist/esm/exporter/xlsx/sparkline-builder.d.ts.map +1 -0
  411. package/dist/esm/exporter/xlsx/sparkline-builder.js +437 -0
  412. package/dist/esm/exporter/xlsx/style-builder.d.ts +93 -0
  413. package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -0
  414. package/dist/esm/exporter/xlsx/style-builder.js +795 -0
  415. package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts +58 -0
  416. package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -0
  417. package/dist/esm/exporter/xlsx/threaded-comments-builder.js +175 -0
  418. package/dist/esm/exporter/xlsx/xlsx-export.d.ts +121 -0
  419. package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -0
  420. package/dist/esm/exporter/xlsx/xlsx-export.js +4062 -0
  421. package/dist/esm/helpers/theme-xml-generator.d.ts +8 -0
  422. package/dist/esm/helpers/theme-xml-generator.d.ts.map +1 -0
  423. package/dist/esm/helpers/theme-xml-generator.js +33 -0
  424. package/dist/esm/helpers.d.ts +34 -0
  425. package/dist/esm/helpers.d.ts.map +1 -0
  426. package/dist/esm/helpers.js +182 -0
  427. package/dist/esm/index.d.ts +16 -0
  428. package/dist/esm/index.d.ts.map +1 -0
  429. package/dist/esm/index.js +15 -0
  430. package/dist/esm/indexed-colors.d.ts +2 -0
  431. package/dist/esm/indexed-colors.d.ts.map +1 -0
  432. package/dist/esm/indexed-colors.js +75 -0
  433. package/dist/esm/ods-export.d.ts +24 -0
  434. package/dist/esm/ods-export.d.ts.map +1 -0
  435. package/dist/esm/ods-export.js +12 -0
  436. package/dist/esm/parse.d.ts +24 -0
  437. package/dist/esm/parse.d.ts.map +1 -0
  438. package/dist/esm/parse.js +55 -0
  439. package/dist/esm/power-query.d.ts +41 -0
  440. package/dist/esm/power-query.d.ts.map +1 -0
  441. package/dist/esm/power-query.js +19 -0
  442. package/dist/esm/server.d.ts +9 -0
  443. package/dist/esm/server.d.ts.map +1 -0
  444. package/dist/esm/server.js +8 -0
  445. package/dist/esm/theme1.d.ts +3 -0
  446. package/dist/esm/theme1.d.ts.map +1 -0
  447. package/dist/esm/theme1.js +2 -0
  448. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  449. package/dist/esm/types.d.ts +14 -0
  450. package/dist/esm/types.d.ts.map +1 -0
  451. package/dist/esm/types.js +14 -0
  452. package/dist/esm/vitest.config.d.ts +3 -0
  453. package/dist/esm/vitest.config.d.ts.map +1 -0
  454. package/dist/esm/vitest.config.js +16 -0
  455. package/package.json +62 -0
  456. package/xlsx.d.ts +52 -0
@@ -0,0 +1,4654 @@
1
+ import { addressToCell, addressToSelection, alpha2number, checkIfStringIsReference, findMatchingBandProperty, generateSelectionsFromFormula, getCellXfsKey, isAFormula, isBareFormulaExpression, isNil, uuid, } from "@rowsncolumns/utils";
2
+ import { XMLParser } from "fast-xml-parser";
3
+ import { get } from "lodash-es";
4
+ import { Parser as HtmlParser } from "htmlparser2";
5
+ import { createFilterSpecsFromAutoFilter, excelColumnWidthToPixels, findMax, getRangeFromAddressRef, } from "../../helpers";
6
+ import { defaultSpreadsheetTheme } from "../../theme-default";
7
+ import { Unescape } from "../../utils";
8
+ import { StreamingZipParser } from "../../zip-parser-streaming";
9
+ import { SheetDataValidationStamper } from "../../dv-cell-stamp";
10
+ import { BaseWorkbookPlugin } from "../base/index";
11
+ import { processChart } from "./charts";
12
+ import { customFiltersToCondition, dynamicFilterToCondition, } from "../../../autofilter-condition";
13
+ import { getConditionalFormattingFromSheet } from "./conditional-formatting";
14
+ import { getDataValidationsForSheet as parseDataValidationsFromSheetData } from "./data-validations";
15
+ import { processAnchors } from "./drawing";
16
+ import { getRelationsById } from "./relationships";
17
+ import { hasPowerQueryParts, } from "../../../power-query";
18
+ import { extractSheetViewProperties } from "./sheet-views";
19
+ import { EMPTY_SHEET_THREADED_COMMENTS, parsePersons, parseThreadedComments, } from "./threaded-comments";
20
+ import { extractColor, fontNodeToTextFormat, getCellDataForStyleIndex, getCellValueAndFormat, } from "./styles";
21
+ import { extractThemeColors, extractThemeFonts, generateDarkThemeColors, } from "./theme";
22
+ export const XMLTagProcessor = (name, value) => Unescape(value);
23
+ function getWithFallback(source, paths) {
24
+ for (const path of paths) {
25
+ const value = get(source, path);
26
+ if (value !== undefined) {
27
+ return value;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ /**
33
+ * Pull the literal text out of a single `<r>` block produced by
34
+ * fast-xml-parser. The parser inlines `<t>` text either as a string
35
+ * value, an object `{ t$: "...", a$: { ... } }` (when the element
36
+ * carries attributes like `xml:space="preserve"`), or as a raw
37
+ * primitive. Plain strings (no enclosing `<r>` element) are also
38
+ * handled — they appear when fast-xml-parser collapses a leaf node.
39
+ */
40
+ const extractRunText = (part) => {
41
+ if (typeof part === "string")
42
+ return part;
43
+ const node = part?.t ?? part?.["x:t"];
44
+ if (node === undefined)
45
+ return "";
46
+ if (typeof node === "string")
47
+ return node;
48
+ if (typeof node === "object") {
49
+ if (typeof node.t$ === "string")
50
+ return node.t$;
51
+ if (typeof node["#text"] === "string")
52
+ return node["#text"];
53
+ }
54
+ return typeof node === "number" || typeof node === "boolean"
55
+ ? String(node)
56
+ : "";
57
+ };
58
+ /**
59
+ * Convert an `<rPr>` block to a `TextFormat` partial. Supports the
60
+ * subset OOXML uses for rich-text shared strings — bold, italic,
61
+ * underline, strikethrough, font size, font family, color (rgb +
62
+ * theme). Properties absent from the source remain undefined so the
63
+ * cell's base format keeps winning.
64
+ */
65
+ const extractRunFormat = (part) => {
66
+ const rPr = part?.rPr ?? part?.["x:rPr"];
67
+ if (!rPr || typeof rPr !== "object")
68
+ return undefined;
69
+ const format = {};
70
+ const readAttr = (node, key) => {
71
+ if (!node)
72
+ return undefined;
73
+ return (node?.[`a$.${key}`] ??
74
+ node?.a$?.[key] ??
75
+ node?.[`@_${key}`] ??
76
+ node?.[key]);
77
+ };
78
+ // Boolean toggles. OOXML uses presence-as-true (`<b/>`) or a `val`
79
+ // attribute. fast-xml-parser emits `<b/>` as either `b: ""` or
80
+ // `b: { a$: { val: "1" } }`; the heuristic accepts both.
81
+ const truthy = (node) => {
82
+ if (node === undefined || node === null)
83
+ return false;
84
+ const val = readAttr(node, "val");
85
+ if (val === undefined)
86
+ return true; // presence-as-true
87
+ return val === "1" || val === "true" || val === 1 || val === true;
88
+ };
89
+ if (truthy(rPr.b ?? rPr["x:b"]))
90
+ format.bold = true;
91
+ if (truthy(rPr.i ?? rPr["x:i"]))
92
+ format.italic = true;
93
+ if (truthy(rPr.u ?? rPr["x:u"]))
94
+ format.underline = true;
95
+ if (truthy(rPr.strike ?? rPr["x:strike"]))
96
+ format.strikethrough = true;
97
+ const vertAlignVal = readAttr(rPr.vertAlign ?? rPr["x:vertAlign"], "val");
98
+ if (vertAlignVal === "superscript") {
99
+ format.baselineOffset = "superscript";
100
+ }
101
+ else if (vertAlignVal === "subscript") {
102
+ format.baselineOffset = "subscript";
103
+ }
104
+ const szVal = readAttr(rPr.sz ?? rPr["x:sz"], "val");
105
+ if (typeof szVal === "string" || typeof szVal === "number") {
106
+ const n = Number(szVal);
107
+ if (Number.isFinite(n) && n > 0)
108
+ format.fontSize = n;
109
+ }
110
+ const rFontVal = readAttr(rPr.rFont ?? rPr["x:rFont"], "val");
111
+ if (typeof rFontVal === "string" && rFontVal) {
112
+ format.fontFamily = rFontVal;
113
+ }
114
+ const colorNode = rPr.color ?? rPr["x:color"];
115
+ if (colorNode) {
116
+ const rgb = readAttr(colorNode, "rgb");
117
+ const theme = readAttr(colorNode, "theme");
118
+ const tint = readAttr(colorNode, "tint");
119
+ if (typeof rgb === "string" && rgb) {
120
+ // Strip Excel's leading alpha byte ("FFRRGGBB" -> "#RRGGBB")
121
+ const hex = rgb.length === 8 ? rgb.slice(2) : rgb;
122
+ // Stored as a plain CSS string; TextFormat's `color` accepts it.
123
+ format.color = `#${hex.toUpperCase()}`;
124
+ }
125
+ else if (theme !== undefined && theme !== null) {
126
+ const themeNum = Number(theme);
127
+ if (Number.isFinite(themeNum)) {
128
+ const tintNum = tint !== undefined && tint !== null ? Number(tint) : undefined;
129
+ format.color = {
130
+ theme: themeNum,
131
+ tint: Number.isFinite(tintNum) ? tintNum : undefined,
132
+ };
133
+ }
134
+ }
135
+ }
136
+ return format;
137
+ };
138
+ const hasAnyFormatKey = (format) => {
139
+ return (format.bold === true ||
140
+ format.italic === true ||
141
+ format.underline === true ||
142
+ format.strikethrough === true ||
143
+ format.color != null ||
144
+ format.fontFamily != null ||
145
+ format.fontSize != null ||
146
+ format.baselineOffset != null);
147
+ };
148
+ export function getChartReferenceFromAnchor(anchor) {
149
+ const graphicFrame = getWithFallback(anchor, [
150
+ "xdr:graphicFrame",
151
+ "graphicFrame",
152
+ ]);
153
+ if (!graphicFrame) {
154
+ return undefined;
155
+ }
156
+ return getWithFallback(graphicFrame, [
157
+ "a:graphic.a:graphicData.c:chart",
158
+ "a:graphic.a:graphicData.chart",
159
+ "a:graphic.graphicData.c:chart",
160
+ "graphic.a:graphicData.c:chart",
161
+ "graphic.graphicData.c:chart",
162
+ "graphic.graphicData.chart",
163
+ "graphicData.c:chart",
164
+ "graphicData.chart",
165
+ "c:chart",
166
+ "chart",
167
+ ]);
168
+ }
169
+ const XMLOptions2 = {
170
+ ignoreAttributes: false,
171
+ attributesGroupName: "a$",
172
+ attributeNamePrefix: "",
173
+ textNodeName: "t$",
174
+ trimValues: false,
175
+ ignoreDeclaration: true,
176
+ isArray: (tagName) => {
177
+ return /Relationship$/.test(tagName);
178
+ },
179
+ tagValueProcessor: XMLTagProcessor,
180
+ attributeValueProcessor: XMLTagProcessor,
181
+ // Disable entity processing to avoid expansion limits on large Excel files
182
+ processEntities: false,
183
+ };
184
+ export class WorkbookStyles {
185
+ styles;
186
+ fills;
187
+ numFmts;
188
+ borders;
189
+ cellXfs;
190
+ cellStyleXfs;
191
+ fonts;
192
+ dxfs;
193
+ tableStyles;
194
+ /**
195
+ * Workbook-wide named cell styles parsed from `<cellStyles>` in
196
+ * styles.xml. Each entry references a `cellStyleXfs` index. Used
197
+ * primarily for round-trip preservation; consumers that want to
198
+ * surface a "Cell Styles" gallery can iterate this map.
199
+ */
200
+ namedCellStyles;
201
+ indexedColors;
202
+ /**
203
+ * The workbook theme's `<a:fontScheme>` typefaces (major = headings,
204
+ * minor = body), used to resolve `<font><scheme val="major|minor"/>`
205
+ * references that carry no concrete `<name>`. Populated after the
206
+ * theme part is parsed; stays `undefined` when the workbook has no
207
+ * theme, in which case scheme references remain unresolved (mirrors
208
+ * `rnc-import`'s `resolve_scheme_fonts`).
209
+ */
210
+ themeFonts;
211
+ constructor(styleSheet) {
212
+ this.styles = styleSheet ?? {};
213
+ const fillsNode = getWithFallback(this.styles, [
214
+ "fills.fill",
215
+ "fills.x:fill",
216
+ "x:fills.fill",
217
+ "x:fills.x:fill",
218
+ ]);
219
+ this.fills = fillsNode
220
+ ? Array.isArray(fillsNode)
221
+ ? fillsNode
222
+ : [fillsNode]
223
+ : [];
224
+ const numFmtsNode = getWithFallback(this.styles, [
225
+ "numFmts.numFmt",
226
+ "numFmts.x:numFmt",
227
+ "x:numFmts.numFmt",
228
+ "x:numFmts.x:numFmt",
229
+ ]);
230
+ this.numFmts = numFmtsNode
231
+ ? Array.isArray(numFmtsNode)
232
+ ? numFmtsNode
233
+ : [numFmtsNode]
234
+ : [];
235
+ const bordersNode = getWithFallback(this.styles, [
236
+ "borders.border",
237
+ "borders.x:border",
238
+ "x:borders.border",
239
+ "x:borders.x:border",
240
+ ]);
241
+ this.borders = bordersNode
242
+ ? Array.isArray(bordersNode)
243
+ ? bordersNode
244
+ : [bordersNode]
245
+ : [];
246
+ const cellXfsNode = getWithFallback(this.styles, [
247
+ "cellXfs.xf",
248
+ "cellXfs.x:xf",
249
+ "x:cellXfs.xf",
250
+ "x:cellXfs.x:xf",
251
+ ]);
252
+ this.cellXfs = cellXfsNode
253
+ ? Array.isArray(cellXfsNode)
254
+ ? cellXfsNode
255
+ : [cellXfsNode]
256
+ : [];
257
+ const cellStyleXfsNode = getWithFallback(this.styles, [
258
+ "cellStyleXfs.xf",
259
+ "cellStyleXfs.x:xf",
260
+ "x:cellStyleXfs.xf",
261
+ "x:cellStyleXfs.x:xf",
262
+ ]);
263
+ this.cellStyleXfs = cellStyleXfsNode
264
+ ? Array.isArray(cellStyleXfsNode)
265
+ ? cellStyleXfsNode
266
+ : [cellStyleXfsNode]
267
+ : [];
268
+ const fontsNode = getWithFallback(this.styles, [
269
+ "fonts.font",
270
+ "fonts.x:font",
271
+ "x:fonts.font",
272
+ "x:fonts.x:font",
273
+ ]);
274
+ this.fonts = fontsNode
275
+ ? Array.isArray(fontsNode)
276
+ ? fontsNode
277
+ : [fontsNode]
278
+ : [];
279
+ // Parse differential formatting (used by conditional formatting and table styles)
280
+ const dxfsNode = getWithFallback(this.styles, [
281
+ "dxfs.dxf",
282
+ "dxfs.x:dxf",
283
+ "x:dxfs.dxf",
284
+ "x:dxfs.x:dxf",
285
+ ]);
286
+ this.dxfs = dxfsNode
287
+ ? Array.isArray(dxfsNode)
288
+ ? dxfsNode
289
+ : [dxfsNode]
290
+ : [];
291
+ // Parse custom table styles
292
+ this.tableStyles = this.extractTableStyles();
293
+ // Parse the workbook-wide named cell styles ("Heading 1", "Accent 1",
294
+ // "Total", etc.) — the OOXML `<cellStyles>` block maps a friendly
295
+ // name to a `cellStyleXfs` entry. Used by the exporter to round-
296
+ // trip the named-style list. The actual formatting flows through
297
+ // the existing `buildEffectiveXf` inheritance path (a cell whose
298
+ // xf carries `xfId="N"` inherits from `cellStyleXfs[N]`).
299
+ this.namedCellStyles = this.extractNamedCellStyles();
300
+ this.indexedColors = this.extractIndexedColors();
301
+ }
302
+ /**
303
+ * Extract custom table styles from styles.xml
304
+ * Returns a map of style name -> tableStyleElements
305
+ */
306
+ extractTableStyles() {
307
+ const result = new Map();
308
+ const tableStylesNode = getWithFallback(this.styles, [
309
+ "tableStyles.tableStyle",
310
+ "tableStyles.x:tableStyle",
311
+ "x:tableStyles.tableStyle",
312
+ "x:tableStyles.x:tableStyle",
313
+ ]);
314
+ if (!tableStylesNode)
315
+ return result;
316
+ const stylesArray = Array.isArray(tableStylesNode)
317
+ ? tableStylesNode
318
+ : [tableStylesNode];
319
+ for (const style of stylesArray) {
320
+ const name = style.a$?.name;
321
+ if (!name)
322
+ continue;
323
+ // Get tableStyleElements
324
+ const elementsNode = getWithFallback(style, [
325
+ "tableStyleElement",
326
+ "x:tableStyleElement",
327
+ ]);
328
+ const elements = elementsNode
329
+ ? Array.isArray(elementsNode)
330
+ ? elementsNode
331
+ : [elementsNode]
332
+ : [];
333
+ result.set(name, {
334
+ name,
335
+ // Preserve the raw `pivot` attribute so the exporter can flag
336
+ // a style as pivot-targeted on round-trip. Excel sets
337
+ // `pivot="0"` to mean the style applies to tables only;
338
+ // `pivot="1"` for pivot tables.
339
+ pivot: style.a$?.pivot === "1" || style.a$?.pivot === "true" || false,
340
+ elements,
341
+ });
342
+ }
343
+ return result;
344
+ }
345
+ /**
346
+ * Parse the `<cellStyles>` block from styles.xml — each `<cellStyle>`
347
+ * is a name + xfId (pointing into `cellStyleXfs`) plus optional
348
+ * builtinId / iLevel / hidden / customBuiltin attributes.
349
+ */
350
+ extractNamedCellStyles() {
351
+ const node = getWithFallback(this.styles, [
352
+ "cellStyles.cellStyle",
353
+ "cellStyles.x:cellStyle",
354
+ "x:cellStyles.cellStyle",
355
+ "x:cellStyles.x:cellStyle",
356
+ ]);
357
+ if (!node)
358
+ return [];
359
+ const styles = Array.isArray(node) ? node : [node];
360
+ const out = [];
361
+ for (const s of styles) {
362
+ const attrs = s.a$ ?? {};
363
+ const name = attrs.name;
364
+ if (!name)
365
+ continue;
366
+ const xfIdRaw = attrs.xfId;
367
+ const xfId = xfIdRaw != null ? parseInt(xfIdRaw) : 0;
368
+ if (Number.isNaN(xfId))
369
+ continue;
370
+ const entry = {
371
+ name: String(name),
372
+ xfId,
373
+ };
374
+ if (attrs.builtinId != null) {
375
+ const bi = parseInt(attrs.builtinId);
376
+ if (!Number.isNaN(bi))
377
+ entry.builtinId = bi;
378
+ }
379
+ if (attrs.iLevel != null) {
380
+ const il = parseInt(attrs.iLevel);
381
+ if (!Number.isNaN(il))
382
+ entry.iLevel = il;
383
+ }
384
+ if (attrs.hidden === "1" || attrs.hidden === "true") {
385
+ entry.hidden = true;
386
+ }
387
+ if (attrs.customBuiltin === "1" || attrs.customBuiltin === "true") {
388
+ entry.customBuiltin = true;
389
+ }
390
+ out.push(entry);
391
+ }
392
+ return out;
393
+ }
394
+ /**
395
+ * Get banded definition for a custom table style
396
+ */
397
+ getCustomTableStyleBanding(styleName) {
398
+ const style = this.tableStyles.get(styleName);
399
+ if (!style)
400
+ return undefined;
401
+ const bandedDef = {
402
+ rowProperties: {},
403
+ columnProperties: {},
404
+ };
405
+ for (const element of style.elements) {
406
+ const type = element.a$?.type;
407
+ const dxfId = element.a$?.dxfId;
408
+ if (type === undefined || dxfId === undefined)
409
+ continue;
410
+ const dxfIndex = parseInt(dxfId);
411
+ if (isNaN(dxfIndex) || dxfIndex < 0 || dxfIndex >= this.dxfs.length) {
412
+ continue;
413
+ }
414
+ const dxf = this.dxfs[dxfIndex];
415
+ const color = this.extractDxfColor(dxf);
416
+ // Map tableStyleElement types to bandedDefinition properties
417
+ switch (type) {
418
+ case "headerRow":
419
+ if (color)
420
+ bandedDef.rowProperties.headerColor = color;
421
+ break;
422
+ case "totalRow":
423
+ if (color)
424
+ bandedDef.rowProperties.totalRowColor = color;
425
+ break;
426
+ case "firstRowStripe":
427
+ if (color)
428
+ bandedDef.rowProperties.firstBandColor = color;
429
+ break;
430
+ case "secondRowStripe":
431
+ if (color)
432
+ bandedDef.rowProperties.secondBandColor = color;
433
+ break;
434
+ case "firstColumnStripe":
435
+ if (color)
436
+ bandedDef.columnProperties.firstBandColor = color;
437
+ break;
438
+ case "secondColumnStripe":
439
+ if (color)
440
+ bandedDef.columnProperties.secondBandColor = color;
441
+ break;
442
+ }
443
+ }
444
+ // Only return if we found any properties
445
+ const hasRowProps = Object.keys(bandedDef.rowProperties).length > 0;
446
+ const hasColProps = Object.keys(bandedDef.columnProperties).length > 0;
447
+ if (!hasRowProps && !hasColProps)
448
+ return undefined;
449
+ if (!hasRowProps)
450
+ delete bandedDef.rowProperties;
451
+ if (!hasColProps)
452
+ delete bandedDef.columnProperties;
453
+ return bandedDef;
454
+ }
455
+ /**
456
+ * Walk every custom table style and return a self-contained
457
+ * representation where each element carries its resolved dxf
458
+ * format (so the exporter can re-register the dxfs into its own
459
+ * table without keeping a back-reference to this WorkbookStyles
460
+ * instance). Used for `customTableStyles` round-trip — the OOXML
461
+ * tableStyle name + element list survives a save with the same
462
+ * banding colors / font overrides.
463
+ */
464
+ getCustomTableStyleExports() {
465
+ const out = [];
466
+ for (const [name, style] of this.tableStyles) {
467
+ const pivot = !!style.pivot;
468
+ const elements = [];
469
+ for (const element of style.elements ?? []) {
470
+ const attrs = element.a$ ?? {};
471
+ const type = attrs.type;
472
+ const dxfIdRaw = attrs.dxfId;
473
+ if (!type || dxfIdRaw == null)
474
+ continue;
475
+ const dxfIndex = parseInt(dxfIdRaw);
476
+ if (isNaN(dxfIndex) ||
477
+ dxfIndex < 0 ||
478
+ dxfIndex >= this.dxfs.length) {
479
+ continue;
480
+ }
481
+ const dxf = this.dxfs[dxfIndex];
482
+ const entry = { type };
483
+ const sizeRaw = attrs.size;
484
+ if (sizeRaw != null) {
485
+ const size = parseInt(sizeRaw);
486
+ if (!isNaN(size))
487
+ entry.size = size;
488
+ }
489
+ const bg = this.extractDxfColor(dxf);
490
+ if (bg)
491
+ entry.backgroundColor = bg;
492
+ const font = dxf?.font || dxf?.["x:font"];
493
+ if (font) {
494
+ // Font color — first <color> child inside <font>.
495
+ const colorNode = font.color || font["x:color"];
496
+ if (colorNode) {
497
+ const fAttrs = colorNode.a$ ?? colorNode;
498
+ if (fAttrs.theme !== undefined) {
499
+ const theme = parseInt(fAttrs.theme);
500
+ const tint = fAttrs.tint ? parseFloat(fAttrs.tint) : undefined;
501
+ entry.textColor = { theme, tint };
502
+ }
503
+ else if (fAttrs.rgb) {
504
+ const rgb = fAttrs.rgb;
505
+ entry.textColor =
506
+ rgb.length === 8 ? "#" + rgb.slice(2) : "#" + rgb;
507
+ }
508
+ }
509
+ if (font.b !== undefined || font["x:b"] !== undefined) {
510
+ entry.bold = true;
511
+ }
512
+ if (font.i !== undefined || font["x:i"] !== undefined) {
513
+ entry.italic = true;
514
+ }
515
+ }
516
+ // Skip elements that contribute nothing — keeps the output
517
+ // clean when a dxf has only properties we don't yet extract
518
+ // (borders, alignment, etc.).
519
+ if (!entry.backgroundColor &&
520
+ !entry.textColor &&
521
+ !entry.bold &&
522
+ !entry.italic &&
523
+ entry.size == null) {
524
+ continue;
525
+ }
526
+ elements.push(entry);
527
+ }
528
+ if (elements.length === 0)
529
+ continue;
530
+ out.push({ name, ...(pivot ? { pivot: true } : {}), elements });
531
+ }
532
+ return out;
533
+ }
534
+ /**
535
+ * Extract color from dxf fill
536
+ */
537
+ extractDxfColor(dxf) {
538
+ const fill = dxf?.fill || dxf?.["x:fill"];
539
+ if (!fill)
540
+ return undefined;
541
+ const patternFill = fill.patternFill || fill["x:patternFill"];
542
+ if (!patternFill)
543
+ return undefined;
544
+ // Try bgColor first, then fgColor
545
+ const colorNode = patternFill.bgColor ||
546
+ patternFill["x:bgColor"] ||
547
+ patternFill.fgColor ||
548
+ patternFill["x:fgColor"];
549
+ if (!colorNode)
550
+ return undefined;
551
+ const attrs = colorNode.a$ || colorNode;
552
+ // Handle theme color
553
+ if (attrs.theme !== undefined) {
554
+ const theme = parseInt(attrs.theme);
555
+ const tint = attrs.tint ? parseFloat(attrs.tint) : undefined;
556
+ return { theme, tint };
557
+ }
558
+ // Handle RGB color
559
+ if (attrs.rgb) {
560
+ const rgb = attrs.rgb;
561
+ // Convert ARGB to #RGB
562
+ if (rgb.length === 8) {
563
+ return "#" + rgb.slice(2);
564
+ }
565
+ return "#" + rgb;
566
+ }
567
+ // Handle indexed color
568
+ if (attrs.indexed !== undefined) {
569
+ const idx = parseInt(attrs.indexed);
570
+ if (this.indexedColors && this.indexedColors[idx]) {
571
+ return this.indexedColors[idx];
572
+ }
573
+ }
574
+ return undefined;
575
+ }
576
+ static async load(zipParser, xmlParser) {
577
+ try {
578
+ const content = await zipParser.getText("xl/styles.xml");
579
+ const xmlContent = xmlParser.parse(content);
580
+ const styleSheet = getWithFallback(xmlContent, [
581
+ "styleSheet",
582
+ "x:styleSheet",
583
+ ]);
584
+ return new WorkbookStyles(styleSheet);
585
+ }
586
+ catch {
587
+ // Some XLSM/XLSX files omit styles.xml or include malformed style sheets.
588
+ // Continue with empty defaults instead of crashing import.
589
+ return new WorkbookStyles({});
590
+ }
591
+ }
592
+ getFont(fontId) {
593
+ return this.fonts[fontId];
594
+ }
595
+ getFill(fillId) {
596
+ return this.fills[fillId];
597
+ }
598
+ getCellXfs(styleIndex) {
599
+ return this.cellXfs[styleIndex];
600
+ }
601
+ getCellStyleXf(xfId) {
602
+ return this.cellStyleXfs[xfId];
603
+ }
604
+ extractIndexedColors() {
605
+ const indexedColorsNode = getWithFallback(this.styles, [
606
+ "colors.indexedColors.rgbColor",
607
+ "colors.indexedColors.x:rgbColor",
608
+ "x:colors.x:indexedColors.x:rgbColor",
609
+ "x:colors.indexedColors.rgbColor",
610
+ ]);
611
+ if (!indexedColorsNode) {
612
+ return undefined;
613
+ }
614
+ const indexedColorsArray = Array.isArray(indexedColorsNode)
615
+ ? indexedColorsNode
616
+ : [indexedColorsNode];
617
+ const colors = indexedColorsArray.map((entry) => {
618
+ const rgb = (typeof entry?.a$?.rgb === "string" && entry.a$.rgb) ||
619
+ (typeof entry?.rgb === "string" && entry.rgb);
620
+ if (!rgb) {
621
+ return undefined;
622
+ }
623
+ const normalized = rgb.trim();
624
+ if (normalized.length === 8) {
625
+ return "#" + normalized.slice(2);
626
+ }
627
+ if (normalized.length === 6) {
628
+ return "#" + normalized;
629
+ }
630
+ return undefined;
631
+ });
632
+ return colors.length > 0 ? colors : undefined;
633
+ }
634
+ }
635
+ /**
636
+ * Bounds for materializing row/column default styles as phantom cells.
637
+ * Excel paints `<row customFormat="1" s="N">` and `<col style="N">` across
638
+ * the entire row/column, but the sheet model has no row/column format slot,
639
+ * so the importer writes format-only cells instead — clamped to the sheet's
640
+ * used column extent (never past MAX_COLUMNS) with a per-sheet cell budget
641
+ * so a catch-all `<col min="1" max="16384" style="N">` on a million-row
642
+ * sheet can't explode memory.
643
+ */
644
+ const STYLE_MATERIALIZE_MAX_COLUMNS = 4096;
645
+ const STYLE_MATERIALIZE_CELL_BUDGET = 200_000;
646
+ /** The namespace marker identifying a `customXml` item as a Power Query DataMashup part. */
647
+ const DATA_MASHUP_MARKER = "schemas.microsoft.com/DataMashup";
648
+ /**
649
+ * Resolve a relationship `Target` (relative to the directory of the part that owns the rels)
650
+ * into a package-absolute part name, normalizing `.`/`..` segments. A leading `/` is
651
+ * package-absolute (e.g. base `xl` + `../customXml/item1.xml` → `customXml/item1.xml`).
652
+ */
653
+ const resolveRelTarget = (baseDir, target) => {
654
+ if (target.startsWith("/")) {
655
+ return target.substring(1);
656
+ }
657
+ const parts = baseDir ? baseDir.split("/") : [];
658
+ for (const segment of target.split("/")) {
659
+ if (segment === "" || segment === ".")
660
+ continue;
661
+ if (segment === "..") {
662
+ parts.pop();
663
+ }
664
+ else {
665
+ parts.push(segment);
666
+ }
667
+ }
668
+ return parts.join("/");
669
+ };
670
+ /** `xl/tables/table1.xml` → `xl/tables/_rels/table1.xml.rels`. */
671
+ const relsPathFor = (part) => {
672
+ const slash = part.lastIndexOf("/");
673
+ return slash === -1
674
+ ? `_rels/${part}.rels`
675
+ : `${part.substring(0, slash)}/_rels/${part.substring(slash + 1)}.rels`;
676
+ };
677
+ /** The directory of a part name (`xl/tables/table1.xml` → `xl/tables`). */
678
+ const dirOf = (part) => {
679
+ const slash = part.lastIndexOf("/");
680
+ return slash === -1 ? "" : part.substring(0, slash);
681
+ };
682
+ export class XlsxPlugin extends BaseWorkbookPlugin {
683
+ zipParser;
684
+ xmlParser;
685
+ theme;
686
+ sharedStrings;
687
+ /**
688
+ * Per-shared-string-index runs captured from `<si><r>...</r></si>`.
689
+ * Only present for entries that actually carry rich-text segments.
690
+ * Populated by `loadSharedStrings`; consumed by `styles.ts` to set
691
+ * `tf` on cells whose `ss` points at a rich entry.
692
+ */
693
+ richSharedStringRuns;
694
+ sharedFormulaMap;
695
+ workbookStyles;
696
+ relations;
697
+ cellXfs;
698
+ fonts;
699
+ namedRanges;
700
+ xmlPreserveValueParser;
701
+ sheetCommentsCache;
702
+ sheetThreadedCommentsCache;
703
+ /** Lazy `xl/persons/person.xml` parse, shared by every sheet's threads. */
704
+ personsPromise = null;
705
+ workbookXml;
706
+ workSheets = [];
707
+ calcPr;
708
+ workbookPr;
709
+ currentSheetTotalRows;
710
+ /**
711
+ * Per-sheet feature presence recorded during the hyperlink SAX pass.
712
+ * Lets the post-row extractors skip their full-DOM `getSheetData` parse
713
+ * (prohibitively large on big worksheets) when a sheet has none of the
714
+ * relevant elements. Absent entry ⇒ unknown ⇒ extractors parse as before.
715
+ */
716
+ sheetFeatures = new Map();
717
+ /**
718
+ * Single-entry memo for `getSheetData`: consecutive extractor calls for
719
+ * the same sheet reuse one parsed DOM instead of re-decompressing and
720
+ * re-parsing the worksheet XML per extractor. Bounded to one sheet so a
721
+ * multi-sheet workbook never retains more than one worksheet DOM.
722
+ */
723
+ lastSheetDataCache = null;
724
+ /** workbook.xml sheet elements indexed by sheetId (sheet-scoped API). */
725
+ worksheetBySheetId = new Map();
726
+ /**
727
+ * Per-rid memo for `getSheetMetadata` so `getSheet` + a subsequent
728
+ * per-sheet row stream scan the worksheet XML once. Entries are deleted
729
+ * as soon as their consumer finishes (metadata holds a per-row entries
730
+ * map, which is not small on dense sheets), so the eager all-sheet path
731
+ * never accumulates more than one sheet's metadata.
732
+ */
733
+ sheetMetadataCache = new Map();
734
+ /**
735
+ * Per-sheet memo for data validations. Rule ids are minted per parse
736
+ * (`uuid()`), and the row stream stamps those ids onto covered cells — so
737
+ * the stamping pass and the `getDataValidations()` consumers MUST observe
738
+ * the SAME parse or the per-cell references dangle. The workbook-wide
739
+ * `getDataValidations()` aggregates these per-sheet caches, keeping ids
740
+ * stable across per-sheet and workbook-wide callers.
741
+ */
742
+ dataValidationsBySheetCache = new Map();
743
+ /** Per-sheet memo for conditional formatting, mirroring the DV cache. */
744
+ conditionalFormattingBySheetCache = new Map();
745
+ buildMergeRangesByRow(merges) {
746
+ const rangesByRow = new Map();
747
+ for (const range of merges) {
748
+ for (let row = range.startRowIndex; row <= range.endRowIndex; row++) {
749
+ const rowRanges = rangesByRow.get(row);
750
+ if (rowRanges) {
751
+ rowRanges.push(range);
752
+ }
753
+ else {
754
+ rangesByRow.set(row, [range]);
755
+ }
756
+ }
757
+ }
758
+ return rangesByRow;
759
+ }
760
+ getSheetMetadata(rid) {
761
+ let cached = this.sheetMetadataCache.get(rid);
762
+ if (!cached) {
763
+ cached = this.parseSheetMetadata(rid);
764
+ this.sheetMetadataCache.set(rid, cached);
765
+ }
766
+ return cached;
767
+ }
768
+ async parseSheetMetadata(rid) {
769
+ const path = this.getSheetPathByRid(rid);
770
+ const merges = [];
771
+ const rowEntries = new Map();
772
+ let outlineSummaryBelow;
773
+ let outlineSummaryRight;
774
+ const columnMetadata = {};
775
+ const colStyleRanges = [];
776
+ let maxRowIndex = 0;
777
+ let maxColumnIndex = 0;
778
+ let dimensionRange;
779
+ let sheetHasFilterMode = false;
780
+ let zeroHeight = false;
781
+ let defaultRowHeightPx;
782
+ let defaultColumnWidthPx;
783
+ let sheetViewAttrs;
784
+ let paneAttrs;
785
+ let tabColor;
786
+ let autoFilterRef;
787
+ const filterColumns = new Map();
788
+ const getOrCreateFilterColumn = (colId) => {
789
+ let entry = filterColumns.get(colId);
790
+ if (!entry) {
791
+ entry = { visibleValues: [] };
792
+ filterColumns.set(colId, entry);
793
+ }
794
+ return entry;
795
+ };
796
+ let currentFilterColumn = null;
797
+ const tablePartIds = [];
798
+ // ECMA-376 makes row/@r and c/@r optional (streaming writers omit them);
799
+ // when absent, position = previous + 1. Track sequential fallbacks so
800
+ // r-less sheets still report real extents instead of 0.
801
+ let lastScannedRowIndex = 0;
802
+ let scanSeqColIndex = 1;
803
+ const normalizeTag = (name) => name.includes(":") ? name.split(":")[1] : name;
804
+ const parser = new HtmlParser({
805
+ onopentag(tagName, attrs) {
806
+ const name = normalizeTag(tagName);
807
+ if (name === "dimension") {
808
+ const ref = attrs?.ref;
809
+ if (typeof ref === "string") {
810
+ dimensionRange = getRangeFromAddressRef(ref);
811
+ }
812
+ }
813
+ if (name === "sheetPr") {
814
+ sheetHasFilterMode =
815
+ attrs?.filterMode === "1" || attrs?.filterMode === "true";
816
+ }
817
+ // <outlinePr summaryBelow summaryRight/> lives inside sheetPr;
818
+ // it's a sibling element, so it surfaces here as its own event.
819
+ if (name === "outlinePr") {
820
+ if (attrs?.summaryBelow !== undefined) {
821
+ outlineSummaryBelow =
822
+ attrs.summaryBelow === "1" || attrs.summaryBelow === "true";
823
+ }
824
+ if (attrs?.summaryRight !== undefined) {
825
+ outlineSummaryRight =
826
+ attrs.summaryRight === "1" || attrs.summaryRight === "true";
827
+ }
828
+ }
829
+ if (name === "sheetFormatPr") {
830
+ zeroHeight =
831
+ attrs?.zeroHeight === "1" || attrs?.zeroHeight === "true";
832
+ // OOXML row heights are in points; cells / row metadata in this
833
+ // codebase are pixels. 1pt = 96/72 px = 1.333…px.
834
+ if (attrs?.defaultRowHeight !== undefined) {
835
+ const pts = parseFloat(attrs.defaultRowHeight);
836
+ if (Number.isFinite(pts) && pts > 0) {
837
+ defaultRowHeightPx = pts * 1.33;
838
+ }
839
+ }
840
+ // OOXML column widths are in characters; convert through the
841
+ // same helper used for individual column widths.
842
+ if (attrs?.defaultColWidth !== undefined) {
843
+ const chars = parseFloat(attrs.defaultColWidth);
844
+ if (Number.isFinite(chars) && chars > 0) {
845
+ defaultColumnWidthPx = excelColumnWidthToPixels(chars);
846
+ }
847
+ }
848
+ }
849
+ if (name === "tabColor") {
850
+ tabColor = { a$: { ...attrs } };
851
+ }
852
+ if (name === "sheetView" && !sheetViewAttrs) {
853
+ sheetViewAttrs = { ...attrs };
854
+ }
855
+ if (name === "pane" && sheetViewAttrs && !paneAttrs) {
856
+ paneAttrs = { ...attrs };
857
+ }
858
+ if (name === "cols") {
859
+ return;
860
+ }
861
+ if (name === "col") {
862
+ const min = parseInt(attrs.min ?? "0", 10);
863
+ const max = parseInt(attrs.max ?? "0", 10);
864
+ // Column default style: cells in these columns without an
865
+ // explicit `s` (including cells never written to the XML)
866
+ // inherit this xf. Collected separately from DimensionProperties
867
+ // (which has no format slot) and materialized onto cells during
868
+ // processSheetData.
869
+ const colStyleIndex = attrs.style
870
+ ? parseInt(attrs.style, 10)
871
+ : NaN;
872
+ if (Number.isFinite(colStyleIndex) && colStyleIndex > 0) {
873
+ colStyleRanges.push({ min, max, styleIndex: colStyleIndex });
874
+ }
875
+ const isHidden = attrs.hidden === "1" || attrs.hidden === "true";
876
+ const size = attrs.width !== undefined
877
+ ? excelColumnWidthToPixels(parseFloat(attrs.width))
878
+ : undefined;
879
+ const customWidth = attrs.customWidth === "1" ||
880
+ attrs.custom === "1" ||
881
+ attrs.customWidth === "true";
882
+ const outlineLevel = attrs.outlineLevel
883
+ ? parseInt(attrs.outlineLevel, 10) || 0
884
+ : 0;
885
+ const isCollapsed = attrs.collapsed === "1" || attrs.collapsed === "true";
886
+ if (!isHidden &&
887
+ isNil(size) &&
888
+ !customWidth &&
889
+ outlineLevel === 0 &&
890
+ !isCollapsed) {
891
+ return;
892
+ }
893
+ for (let columnIndex = min; columnIndex <= max; columnIndex++) {
894
+ const dimensionProps = {};
895
+ // `hidden` on a grouped column means it's hidden because
896
+ // its parent group is collapsed — route into hiddenByGroup
897
+ // so a future "Expand" can clear it without touching any
898
+ // separate user-driven hide.
899
+ if (isHidden) {
900
+ if (outlineLevel > 0) {
901
+ dimensionProps.hiddenByGroup = true;
902
+ }
903
+ else {
904
+ dimensionProps.hiddenByUser = true;
905
+ }
906
+ }
907
+ if (!isNil(size)) {
908
+ if (customWidth) {
909
+ dimensionProps.resizedByUser = true;
910
+ }
911
+ dimensionProps.size = size;
912
+ }
913
+ if (outlineLevel > 0) {
914
+ dimensionProps.outlineLevel = outlineLevel;
915
+ }
916
+ if (isCollapsed)
917
+ dimensionProps.collapsed = true;
918
+ columnMetadata[columnIndex] = dimensionProps;
919
+ }
920
+ }
921
+ if (name === "row") {
922
+ const parsedRowIndex = parseInt(attrs.r ?? "0", 10);
923
+ const rowIndex = parsedRowIndex > 0 ? parsedRowIndex : lastScannedRowIndex + 1;
924
+ lastScannedRowIndex = rowIndex;
925
+ scanSeqColIndex = 1;
926
+ if (rowIndex) {
927
+ maxRowIndex = Math.max(maxRowIndex, rowIndex);
928
+ const isHidden = attrs.hidden === "1" || attrs.hidden === "true";
929
+ const size = attrs.ht ? parseFloat(attrs.ht) * 1.33 : undefined;
930
+ const customHeight = attrs.customHeight === "1" ||
931
+ attrs.custom === "1" ||
932
+ attrs.customHeight === "true";
933
+ const outlineLevel = attrs.outlineLevel
934
+ ? parseInt(attrs.outlineLevel, 10) || 0
935
+ : 0;
936
+ const isCollapsed = attrs.collapsed === "1" || attrs.collapsed === "true";
937
+ rowEntries.set(rowIndex, {
938
+ hidden: isHidden,
939
+ size,
940
+ custom: customHeight,
941
+ outlineLevel: outlineLevel > 0 ? outlineLevel : undefined,
942
+ collapsed: isCollapsed ? true : undefined,
943
+ });
944
+ }
945
+ }
946
+ if (name === "c") {
947
+ const cellRef = attrs.r;
948
+ if (typeof cellRef === "string") {
949
+ const colStr = cellRef.replace(/[0-9]/g, "");
950
+ const columnIndex = alpha2number(colStr);
951
+ maxColumnIndex = Math.max(maxColumnIndex, columnIndex);
952
+ scanSeqColIndex = columnIndex + 1;
953
+ }
954
+ else {
955
+ maxColumnIndex = Math.max(maxColumnIndex, scanSeqColIndex);
956
+ scanSeqColIndex++;
957
+ }
958
+ }
959
+ if (name === "mergeCell") {
960
+ const ref = attrs.ref;
961
+ if (typeof ref === "string") {
962
+ const range = addressToSelection(ref)?.range;
963
+ if (range)
964
+ merges.push(range);
965
+ }
966
+ }
967
+ if (name === "autoFilter") {
968
+ const ref = attrs.ref;
969
+ if (typeof ref === "string") {
970
+ autoFilterRef = ref;
971
+ }
972
+ }
973
+ if (name === "filterColumn") {
974
+ const colId = parseInt(attrs.colId ?? "0", 10);
975
+ currentFilterColumn = Number.isNaN(colId) ? null : colId;
976
+ }
977
+ if (name === "filter" && currentFilterColumn !== null) {
978
+ const val = attrs.val;
979
+ if (typeof val === "string") {
980
+ getOrCreateFilterColumn(currentFilterColumn).visibleValues.push(val);
981
+ }
982
+ }
983
+ if (name === "colorFilter" && currentFilterColumn !== null) {
984
+ const entry = getOrCreateFilterColumn(currentFilterColumn);
985
+ const dxfIdRaw = attrs.dxfId;
986
+ if (typeof dxfIdRaw === "string" && dxfIdRaw.length) {
987
+ const dxfId = parseInt(dxfIdRaw, 10);
988
+ if (Number.isFinite(dxfId)) {
989
+ entry.colorFilterDxfId = dxfId;
990
+ }
991
+ }
992
+ // OOXML default for cellColor is "1" (true). The attribute is
993
+ // absent when defaulting, so undefined → cellColor mode.
994
+ const cellColorRaw = attrs.cellColor;
995
+ entry.colorFilterCellColor =
996
+ cellColorRaw !== "0" && cellColorRaw !== "false";
997
+ }
998
+ if (name === "iconFilter" && currentFilterColumn !== null) {
999
+ const entry = getOrCreateFilterColumn(currentFilterColumn);
1000
+ const iconSetRaw = attrs.iconSet;
1001
+ if (typeof iconSetRaw === "string" && iconSetRaw.length) {
1002
+ entry.iconFilterSet = iconSetRaw;
1003
+ }
1004
+ // iconId is optional in OOXML; absent means "any non-blank".
1005
+ // We treat absent as 0 to match Excel's default.
1006
+ const iconIdRaw = attrs.iconId;
1007
+ const iconId = typeof iconIdRaw === "string" ? parseInt(iconIdRaw, 10) : 0;
1008
+ entry.iconFilterId = Number.isFinite(iconId) ? iconId : 0;
1009
+ }
1010
+ // <customFilters and="1"> wrapping one or two <customFilter
1011
+ // operator= val=>. Records the AND flag, then each child below.
1012
+ if (name === "customFilters" && currentFilterColumn !== null) {
1013
+ const entry = getOrCreateFilterColumn(currentFilterColumn);
1014
+ entry.customFilters = entry.customFilters ?? [];
1015
+ entry.customFiltersAnd =
1016
+ attrs.and === "1" || attrs.and === "true";
1017
+ }
1018
+ if (name === "customFilter" && currentFilterColumn !== null) {
1019
+ const entry = getOrCreateFilterColumn(currentFilterColumn);
1020
+ entry.customFilters = entry.customFilters ?? [];
1021
+ entry.customFilters.push({
1022
+ operator: attrs.operator,
1023
+ val: attrs.val,
1024
+ });
1025
+ }
1026
+ // <dynamicFilter type="thisMonth"/> (relative dates, above/below avg)
1027
+ if (name === "dynamicFilter" && currentFilterColumn !== null) {
1028
+ const entry = getOrCreateFilterColumn(currentFilterColumn);
1029
+ if (typeof attrs.type === "string") {
1030
+ entry.dynamicFilterType = attrs.type;
1031
+ }
1032
+ }
1033
+ if (name === "tablePart") {
1034
+ const rid = attrs["r:id"];
1035
+ if (typeof rid === "string") {
1036
+ tablePartIds.push(rid);
1037
+ }
1038
+ }
1039
+ },
1040
+ onclosetag(tagName) {
1041
+ const name = normalizeTag(tagName);
1042
+ if (name === "filterColumn") {
1043
+ currentFilterColumn = null;
1044
+ }
1045
+ },
1046
+ onerror(err) {
1047
+ console.error("Streaming sheet metadata parse error", err);
1048
+ },
1049
+ }, { xmlMode: true });
1050
+ await this.zipParser.streamText(path, (chunk) => parser.write(chunk));
1051
+ parser.end();
1052
+ if (dimensionRange) {
1053
+ maxRowIndex = Math.max(maxRowIndex, dimensionRange.endRowIndex);
1054
+ maxColumnIndex = Math.max(maxColumnIndex, dimensionRange.endColumnIndex);
1055
+ }
1056
+ const filterSpecs = Array.from(filterColumns.entries()).map(([columnIndex, raw]) => {
1057
+ const criteria = {};
1058
+ if (raw.visibleValues.length) {
1059
+ criteria.visibleValues = raw.visibleValues;
1060
+ }
1061
+ if (raw.colorFilterDxfId !== undefined) {
1062
+ const color = this.resolveDxfColor(raw.colorFilterDxfId, raw.colorFilterCellColor !== false);
1063
+ if (color) {
1064
+ criteria.colorFilter = {
1065
+ color,
1066
+ mode: raw.colorFilterCellColor !== false ? "cellColor" : "fontColor",
1067
+ };
1068
+ }
1069
+ }
1070
+ if (raw.iconFilterSet) {
1071
+ criteria.iconFilter = {
1072
+ iconSet: raw.iconFilterSet,
1073
+ iconId: raw.iconFilterId ?? 0,
1074
+ };
1075
+ }
1076
+ if (raw.customFilters?.length) {
1077
+ const condition = customFiltersToCondition(raw.customFilters, raw.customFiltersAnd === true);
1078
+ if (condition)
1079
+ criteria.condition = condition;
1080
+ }
1081
+ else if (raw.dynamicFilterType) {
1082
+ const condition = dynamicFilterToCondition(raw.dynamicFilterType);
1083
+ if (condition)
1084
+ criteria.condition = condition;
1085
+ }
1086
+ return { columnIndex, filterCriteria: criteria };
1087
+ });
1088
+ const sheetViewData = sheetViewAttrs || paneAttrs
1089
+ ? {
1090
+ sheetView: {
1091
+ a$: sheetViewAttrs ?? {},
1092
+ pane: { a$: paneAttrs ?? {} },
1093
+ },
1094
+ }
1095
+ : undefined;
1096
+ const autoFilterRange = autoFilterRef
1097
+ ? getRangeFromAddressRef(autoFilterRef)
1098
+ : undefined;
1099
+ return {
1100
+ merges,
1101
+ rowEntries,
1102
+ columnMetadata,
1103
+ colStyleRanges,
1104
+ maxRowIndex,
1105
+ maxColumnIndex,
1106
+ autoFilterRange,
1107
+ filterSpecs,
1108
+ sheetViewData,
1109
+ tabColor,
1110
+ sheetHasFilterMode,
1111
+ zeroHeight,
1112
+ defaultRowHeightPx,
1113
+ defaultColumnWidthPx,
1114
+ tablePartIds,
1115
+ outlineSummaryBelow,
1116
+ outlineSummaryRight,
1117
+ };
1118
+ }
1119
+ /**
1120
+ * Resolve a dxfId (differential format index, used by color filters and
1121
+ * conditional formatting) to a hex color string. When `cellColor` is
1122
+ * true the dxf's fill color is returned; otherwise the font color.
1123
+ * Returns undefined when the dxfId is out of range or the dxf has no
1124
+ * matching color in the requested layer.
1125
+ */
1126
+ resolveDxfColor(dxfId, cellColor) {
1127
+ const dxfs = get(this.workbookStyles?.styles, "dxfs.dxf") ?? [];
1128
+ const dxfsArray = Array.isArray(dxfs) ? dxfs : [dxfs];
1129
+ if (dxfId < 0 || dxfId >= dxfsArray.length)
1130
+ return undefined;
1131
+ const dxf = dxfsArray[dxfId];
1132
+ if (!dxf)
1133
+ return undefined;
1134
+ const indexedColors = this.workbookStyles?.indexedColors;
1135
+ if (cellColor) {
1136
+ // Excel's color filter uses the fill's foreground (fgColor) for the
1137
+ // solid-pattern case, but historically fgColor and bgColor have been
1138
+ // used interchangeably across writers. Try both.
1139
+ const patternFill = dxf.fill?.patternFill ?? dxf.fill;
1140
+ const colorNode = patternFill?.fgColor ?? patternFill?.bgColor;
1141
+ if (!colorNode)
1142
+ return undefined;
1143
+ const color = extractColor(colorNode, indexedColors);
1144
+ return typeof color === "string" ? color : color?.toString?.();
1145
+ }
1146
+ const colorNode = dxf.font?.color;
1147
+ if (!colorNode)
1148
+ return undefined;
1149
+ const color = extractColor(colorNode, indexedColors);
1150
+ return typeof color === "string" ? color : color?.toString?.();
1151
+ }
1152
+ /**
1153
+ * Get the maximum column and row indices covered by drawings (charts, images, etc.)
1154
+ * This is used to extend sheet dimensions when drawings extend beyond cell data bounds.
1155
+ * @param rid Sheet relationship ID
1156
+ * @returns Object with maxColumnIndex and maxRowIndex from drawings
1157
+ */
1158
+ async getDrawingBounds(rid) {
1159
+ const DEFAULT_COLUMN_WIDTH_PX = 100;
1160
+ const DEFAULT_ROW_HEIGHT_PX = 21;
1161
+ let maxColumnIndex = 0;
1162
+ let maxRowIndex = 0;
1163
+ try {
1164
+ const relation = this.relations[rid];
1165
+ if (!relation) {
1166
+ return { maxColumnIndex, maxRowIndex };
1167
+ }
1168
+ const sheetPath = this.resolveSheetPath(relation.target);
1169
+ const sheetRelPath = sheetPath.replace("worksheets", "worksheets/_rels") + ".rels";
1170
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
1171
+ if (!sheetRelContent) {
1172
+ return { maxColumnIndex, maxRowIndex };
1173
+ }
1174
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
1175
+ const relationships = get(sheetRels, "Relationships.Relationship");
1176
+ if (!relationships) {
1177
+ return { maxColumnIndex, maxRowIndex };
1178
+ }
1179
+ const relArray = Array.isArray(relationships)
1180
+ ? relationships
1181
+ : [relationships];
1182
+ // Find drawing relationships
1183
+ const drawingRels = relArray.filter((rel) => rel.a$?.Target?.includes("drawing") ||
1184
+ rel.a$?.Type?.includes("drawing"));
1185
+ for (const drawingRel of drawingRels) {
1186
+ let drawingPath = drawingRel.a$.Target;
1187
+ if (drawingPath.startsWith("../")) {
1188
+ drawingPath = `xl/${drawingPath.replace("../", "")}`;
1189
+ }
1190
+ else if (!drawingPath.startsWith("/")) {
1191
+ drawingPath = `xl/worksheets/${drawingPath}`;
1192
+ }
1193
+ else {
1194
+ drawingPath = drawingPath.startsWith("/")
1195
+ ? drawingPath.substring(1)
1196
+ : `xl/${drawingPath}`;
1197
+ }
1198
+ const drawingContent = await this.zipParser.getText(drawingPath);
1199
+ if (!drawingContent)
1200
+ continue;
1201
+ const drawing = this.xmlParser.parse(drawingContent);
1202
+ // Process twoCellAnchor - these have explicit "to" cell positions
1203
+ const twoCellAnchors = get(drawing, "xdr:wsDr.xdr:twoCellAnchor") ||
1204
+ get(drawing, "wsDr.twoCellAnchor");
1205
+ if (twoCellAnchors) {
1206
+ const anchorArray = Array.isArray(twoCellAnchors)
1207
+ ? twoCellAnchors
1208
+ : [twoCellAnchors];
1209
+ for (const anchor of anchorArray) {
1210
+ // For twoCellAnchor, the "to" cell defines the end position directly
1211
+ const toCol = parseInt(get(anchor, "xdr:to.xdr:col.t$") ||
1212
+ get(anchor, "to.col.t$") ||
1213
+ get(anchor, "xdr:to.xdr:col") ||
1214
+ get(anchor, "to.col") ||
1215
+ "0");
1216
+ const toRow = parseInt(get(anchor, "xdr:to.xdr:row.t$") ||
1217
+ get(anchor, "to.row.t$") ||
1218
+ get(anchor, "xdr:to.xdr:row") ||
1219
+ get(anchor, "to.row") ||
1220
+ "0");
1221
+ // Convert from 0-based XML to 1-based and add 1 for the cell itself
1222
+ const endColumn = toCol + 2;
1223
+ const endRow = toRow + 2;
1224
+ maxColumnIndex = Math.max(maxColumnIndex, endColumn);
1225
+ maxRowIndex = Math.max(maxRowIndex, endRow);
1226
+ }
1227
+ }
1228
+ // Process oneCellAnchor - these have extent (size) in EMUs
1229
+ const oneCellAnchors = get(drawing, "xdr:wsDr.xdr:oneCellAnchor") ||
1230
+ get(drawing, "wsDr.oneCellAnchor");
1231
+ if (oneCellAnchors) {
1232
+ const anchorArray = Array.isArray(oneCellAnchors)
1233
+ ? oneCellAnchors
1234
+ : [oneCellAnchors];
1235
+ for (const anchor of anchorArray) {
1236
+ // Get anchor cell position (0-based in XML)
1237
+ const fromCol = parseInt(get(anchor, "xdr:from.xdr:col.t$") ||
1238
+ get(anchor, "from.col.t$") ||
1239
+ get(anchor, "xdr:from.xdr:col") ||
1240
+ get(anchor, "from.col") ||
1241
+ "0");
1242
+ const fromRow = parseInt(get(anchor, "xdr:from.xdr:row.t$") ||
1243
+ get(anchor, "from.row.t$") ||
1244
+ get(anchor, "xdr:from.xdr:row") ||
1245
+ get(anchor, "from.row") ||
1246
+ "0");
1247
+ // Get extent (size) in EMUs and convert to pixels
1248
+ const EMU_PER_PIXEL = 9525;
1249
+ const cxEmu = parseInt(get(anchor, "xdr:ext.a$.cx") ||
1250
+ get(anchor, "ext.a$.cx") ||
1251
+ get(anchor, "xdr:pic.xdr:spPr.a:xfrm.a:ext.a$.cx") ||
1252
+ get(anchor, "pic.spPr.xfrm.ext.a$.cx") ||
1253
+ "0");
1254
+ const cyEmu = parseInt(get(anchor, "xdr:ext.a$.cy") ||
1255
+ get(anchor, "ext.a$.cy") ||
1256
+ get(anchor, "xdr:pic.xdr:spPr.a:xfrm.a:ext.a$.cy") ||
1257
+ get(anchor, "pic.spPr.xfrm.ext.a$.cy") ||
1258
+ "0");
1259
+ const widthPx = Math.round(cxEmu / EMU_PER_PIXEL);
1260
+ const heightPx = Math.round(cyEmu / EMU_PER_PIXEL);
1261
+ // Calculate columns and rows spanned
1262
+ const columnsSpanned = Math.ceil(widthPx / DEFAULT_COLUMN_WIDTH_PX);
1263
+ const rowsSpanned = Math.ceil(heightPx / DEFAULT_ROW_HEIGHT_PX);
1264
+ // Calculate max indices (convert from 0-based XML to 1-based)
1265
+ const endColumn = fromCol + 1 + columnsSpanned;
1266
+ const endRow = fromRow + 1 + rowsSpanned;
1267
+ maxColumnIndex = Math.max(maxColumnIndex, endColumn);
1268
+ maxRowIndex = Math.max(maxRowIndex, endRow);
1269
+ }
1270
+ }
1271
+ }
1272
+ }
1273
+ catch {
1274
+ // If we can't parse drawings, just return 0 bounds
1275
+ }
1276
+ return { maxColumnIndex, maxRowIndex };
1277
+ }
1278
+ async getHyperlinksForSheet(rId) {
1279
+ const hyperlinksByRow = new Map();
1280
+ const relation = this.relations[rId];
1281
+ if (!relation) {
1282
+ return hyperlinksByRow;
1283
+ }
1284
+ // This SAX pass already scans the whole worksheet XML, so piggyback
1285
+ // feature detection on it: the post-row extractors (sparklines, drawings,
1286
+ // charts, conditional formatting, data validations) each need a full
1287
+ // fast-xml-parser DOM of the sheet — ~4.6GB transient on a 228MB
1288
+ // worksheet — and can be skipped entirely when the sheet has none of the
1289
+ // corresponding elements.
1290
+ const features = {
1291
+ extLst: false,
1292
+ conditionalFormatting: false,
1293
+ dataValidations: false,
1294
+ drawing: false,
1295
+ };
1296
+ const sheetPath = this.resolveSheetPath(relation.target);
1297
+ const sheetRelPath = sheetPath.replace("worksheets", "worksheets/_rels") + ".rels";
1298
+ let sheetRelationships = {};
1299
+ try {
1300
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
1301
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
1302
+ sheetRelationships = getRelationsById(sheetRels);
1303
+ }
1304
+ catch {
1305
+ sheetRelationships = {};
1306
+ }
1307
+ const normalizeTag = (name) => name.includes(":") ? name.split(":")[1] : name;
1308
+ const addHyperlinkForRef = (ref, hyperlink) => {
1309
+ if (ref.includes(":")) {
1310
+ const range = addressToSelection(ref)?.range;
1311
+ if (!range) {
1312
+ return;
1313
+ }
1314
+ for (let row = range.startRowIndex; row <= range.endRowIndex; row++) {
1315
+ const rowMap = hyperlinksByRow.get(row) ?? new Map();
1316
+ for (let column = range.startColumnIndex; column <= range.endColumnIndex; column++) {
1317
+ rowMap.set(column, hyperlink);
1318
+ }
1319
+ hyperlinksByRow.set(row, rowMap);
1320
+ }
1321
+ return;
1322
+ }
1323
+ const cellPosition = addressToCell(ref);
1324
+ if (!cellPosition) {
1325
+ return;
1326
+ }
1327
+ const rowMap = hyperlinksByRow.get(cellPosition.rowIndex) ??
1328
+ new Map();
1329
+ rowMap.set(cellPosition.columnIndex, hyperlink);
1330
+ hyperlinksByRow.set(cellPosition.rowIndex, rowMap);
1331
+ };
1332
+ const parser = new HtmlParser({
1333
+ onopentag(tagName, attrs) {
1334
+ const name = normalizeTag(tagName);
1335
+ switch (name) {
1336
+ case "extLst":
1337
+ features.extLst = true;
1338
+ break;
1339
+ case "conditionalFormatting":
1340
+ features.conditionalFormatting = true;
1341
+ break;
1342
+ case "dataValidations":
1343
+ features.dataValidations = true;
1344
+ break;
1345
+ case "drawing":
1346
+ features.drawing = true;
1347
+ break;
1348
+ }
1349
+ if (name !== "hyperlink") {
1350
+ return;
1351
+ }
1352
+ const ref = attrs?.ref;
1353
+ if (typeof ref !== "string") {
1354
+ return;
1355
+ }
1356
+ const display = attrs.display;
1357
+ const tooltip = attrs.tooltip;
1358
+ const location = attrs.location;
1359
+ if (typeof location === "string" && location.length) {
1360
+ const hyperlink = {
1361
+ kind: "internal",
1362
+ location,
1363
+ ...(display ? { display } : {}),
1364
+ ...(tooltip ? { tooltip } : {}),
1365
+ };
1366
+ addHyperlinkForRef(ref, hyperlink);
1367
+ return;
1368
+ }
1369
+ const relId = attrs["r:id"] ?? attrs.id;
1370
+ if (typeof relId !== "string") {
1371
+ return;
1372
+ }
1373
+ const rel = sheetRelationships[relId];
1374
+ if (!rel?.target) {
1375
+ return;
1376
+ }
1377
+ const hyperlink = {
1378
+ kind: "external",
1379
+ url: rel.target,
1380
+ ...(display ? { display } : {}),
1381
+ ...(tooltip ? { tooltip } : {}),
1382
+ };
1383
+ addHyperlinkForRef(ref, hyperlink);
1384
+ },
1385
+ onerror(err) {
1386
+ console.error("Streaming sheet hyperlink parse error", err);
1387
+ },
1388
+ }, { xmlMode: true });
1389
+ await this.zipParser.streamText(sheetPath, (chunk) => parser.write(chunk));
1390
+ parser.end();
1391
+ this.sheetFeatures.set(rId, features);
1392
+ return hyperlinksByRow;
1393
+ }
1394
+ /**
1395
+ * Parse sparklines from a worksheet's extLst and return a map of cell references to SPARKLINE formulas.
1396
+ * Sparklines are stored in x14:sparklineGroups within the worksheet's extension list.
1397
+ */
1398
+ async getSparklinesForSheet(rId, sheetName) {
1399
+ const sparklinesByRow = new Map();
1400
+ // Sparklines live in the worksheet extLst; skip the full-DOM parse when
1401
+ // the hyperlink SAX pass saw no extLst on this sheet.
1402
+ const features = this.sheetFeatures.get(rId);
1403
+ if (features && !features.extLst) {
1404
+ return sparklinesByRow;
1405
+ }
1406
+ try {
1407
+ const sheetData = await this.getSheetData(rId);
1408
+ const worksheet = get(sheetData, "worksheet") || get(sheetData, "x:worksheet");
1409
+ if (!worksheet) {
1410
+ return sparklinesByRow;
1411
+ }
1412
+ // Look for extLst with sparklineGroups
1413
+ const extList = getWithFallback(worksheet, [
1414
+ "extLst.ext",
1415
+ "x:extLst.x:ext",
1416
+ ]);
1417
+ if (!extList) {
1418
+ return sparklinesByRow;
1419
+ }
1420
+ const toArray = (value) => Array.isArray(value) ? value : value ? [value] : [];
1421
+ const extArray = toArray(extList);
1422
+ // Find the sparkline extension (URI: {05C60535-1F16-4fd2-B633-F4F36F0B64E0})
1423
+ for (const ext of extArray) {
1424
+ const uri = ext?.a$?.uri;
1425
+ if (uri !== "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}") {
1426
+ continue;
1427
+ }
1428
+ // Get sparklineGroups - handle both namespaced and non-namespaced
1429
+ const sparklineGroupsContainer = getWithFallback(ext, [
1430
+ "x14:sparklineGroups",
1431
+ "sparklineGroups",
1432
+ ]);
1433
+ const sparklineGroups = sparklineGroupsContainer
1434
+ ? getWithFallback(sparklineGroupsContainer, [
1435
+ "x14:sparklineGroup",
1436
+ "sparklineGroup",
1437
+ ])
1438
+ : undefined;
1439
+ if (!sparklineGroups) {
1440
+ continue;
1441
+ }
1442
+ const groupsArray = toArray(sparklineGroups);
1443
+ for (const group of groupsArray) {
1444
+ // Get type: line, column, or stacked (win/loss)
1445
+ const type = group?.a$?.type || "line";
1446
+ const chartType = type === "stacked" ? "winloss" : type;
1447
+ // Get color from colorSeries (RGB format without #). Excel
1448
+ // also emits sibling `<x14:colorHigh>`, `<x14:colorLow>` etc.
1449
+ // for the per-point highlights (max/min/first/last/negative).
1450
+ // The renderer + exporter both round-trip these so workbooks
1451
+ // that highlight columns in custom colors don't lose them.
1452
+ const extractColor = (paths) => {
1453
+ const node = getWithFallback(group, paths);
1454
+ const rgb = node?.a$?.rgb;
1455
+ return rgb ? `#${String(rgb).slice(2)}` : undefined;
1456
+ };
1457
+ const color = extractColor(["x14:colorSeries", "colorSeries"]);
1458
+ const colorHigh = extractColor(["x14:colorHigh", "colorHigh"]);
1459
+ const colorLow = extractColor(["x14:colorLow", "colorLow"]);
1460
+ const colorFirst = extractColor(["x14:colorFirst", "colorFirst"]);
1461
+ const colorLast = extractColor(["x14:colorLast", "colorLast"]);
1462
+ const colorMarkers = extractColor([
1463
+ "x14:colorMarkers",
1464
+ "colorMarkers",
1465
+ ]);
1466
+ const colorNegative = extractColor([
1467
+ "x14:colorNegative",
1468
+ "colorNegative",
1469
+ ]);
1470
+ // Axis / display attrs. Each maps 1:1 to a SPARKLINE option
1471
+ // key; the renderer + the exporter both pick them up from
1472
+ // the formula source.
1473
+ const groupAttrs = group?.a$ ?? {};
1474
+ const truthy = (v) => v === "1" || v === "true" || v === true;
1475
+ const numAttr = (v) => {
1476
+ if (v == null)
1477
+ return undefined;
1478
+ const n = typeof v === "number" ? v : parseFloat(v);
1479
+ return Number.isFinite(n) ? n : undefined;
1480
+ };
1481
+ const emptyCellsAs = groupAttrs.displayEmptyCellsAs === "zero" ||
1482
+ groupAttrs.displayEmptyCellsAs === "span" ||
1483
+ groupAttrs.displayEmptyCellsAs === "gap"
1484
+ ? groupAttrs.displayEmptyCellsAs
1485
+ : undefined;
1486
+ const dateAxis = truthy(groupAttrs.dateAxis) ? true : undefined;
1487
+ const markers = truthy(groupAttrs.markers) ? true : undefined;
1488
+ const high = truthy(groupAttrs.high) ? true : undefined;
1489
+ const low = truthy(groupAttrs.low) ? true : undefined;
1490
+ const first = truthy(groupAttrs.first) ? true : undefined;
1491
+ const last = truthy(groupAttrs.last) ? true : undefined;
1492
+ const negative = truthy(groupAttrs.negative) ? true : undefined;
1493
+ const displayXAxis = truthy(groupAttrs.displayXAxis)
1494
+ ? true
1495
+ : undefined;
1496
+ const displayHidden = truthy(groupAttrs.displayHidden)
1497
+ ? true
1498
+ : undefined;
1499
+ const rightToLeft = truthy(groupAttrs.rightToLeft)
1500
+ ? true
1501
+ : undefined;
1502
+ const lineWeight = numAttr(groupAttrs.lineWeight);
1503
+ const manualMax = numAttr(groupAttrs.manualMax);
1504
+ const manualMin = numAttr(groupAttrs.manualMin);
1505
+ const minAxisType = groupAttrs.minAxisType === "group" ||
1506
+ groupAttrs.minAxisType === "custom" ||
1507
+ groupAttrs.minAxisType === "individual"
1508
+ ? groupAttrs.minAxisType
1509
+ : undefined;
1510
+ const maxAxisType = groupAttrs.maxAxisType === "group" ||
1511
+ groupAttrs.maxAxisType === "custom" ||
1512
+ groupAttrs.maxAxisType === "individual"
1513
+ ? groupAttrs.maxAxisType
1514
+ : undefined;
1515
+ // Get sparklines container then sparkline elements
1516
+ const sparklinesContainer = getWithFallback(group, [
1517
+ "x14:sparklines",
1518
+ "sparklines",
1519
+ ]);
1520
+ const sparklines = sparklinesContainer
1521
+ ? getWithFallback(sparklinesContainer, [
1522
+ "x14:sparkline",
1523
+ "sparkline",
1524
+ ])
1525
+ : undefined;
1526
+ if (!sparklines) {
1527
+ continue;
1528
+ }
1529
+ const sparklinesArray = toArray(sparklines);
1530
+ for (const sparkline of sparklinesArray) {
1531
+ // Get data range (xm:f or f) - e.g., "Sheet1!A1:F1"
1532
+ // The value might be in t$ (text content) or directly as a property
1533
+ const dataRangeNode = sparkline?.["xm:f"] || sparkline?.f;
1534
+ const dataRange = typeof dataRangeNode === "string"
1535
+ ? dataRangeNode
1536
+ : dataRangeNode?.t$ || dataRangeNode;
1537
+ // Get cell reference (xm:sqref or sqref) - e.g., "G1"
1538
+ const cellRefNode = sparkline?.["xm:sqref"] || sparkline?.sqref;
1539
+ const cellRef = typeof cellRefNode === "string"
1540
+ ? cellRefNode
1541
+ : cellRefNode?.t$ || cellRefNode;
1542
+ if (!dataRange || !cellRef) {
1543
+ continue;
1544
+ }
1545
+ // Parse the cell reference to get row and column indices
1546
+ const cellPosition = addressToCell(cellRef);
1547
+ if (!cellPosition) {
1548
+ continue;
1549
+ }
1550
+ // addressToCell returns 1-indexed positions (from alpha2number)
1551
+ // which matches how existing cells are stored in rowData.values
1552
+ const rowIdx = cellPosition.rowIndex;
1553
+ const colIdx = cellPosition.columnIndex;
1554
+ // Normalize the data range - remove sheet name if it matches current sheet
1555
+ let normalizedRange = dataRange;
1556
+ if (dataRange.includes("!")) {
1557
+ const [rangeSheetName, rangeRef] = dataRange.split("!");
1558
+ // Remove quotes from sheet name if present
1559
+ const cleanSheetName = rangeSheetName.replace(/^'|'$/g, "");
1560
+ if (cleanSheetName === sheetName) {
1561
+ normalizedRange = rangeRef;
1562
+ }
1563
+ }
1564
+ // Build the SPARKLINE formula
1565
+ let formula = `=SPARKLINE(${normalizedRange}`;
1566
+ const options = [];
1567
+ const pushKV = (key, value) => {
1568
+ if (typeof value === "boolean") {
1569
+ options.push(`"${key}"`, value ? "TRUE" : "FALSE");
1570
+ }
1571
+ else if (typeof value === "number") {
1572
+ options.push(`"${key}"`, String(value));
1573
+ }
1574
+ else {
1575
+ options.push(`"${key}"`, `"${value}"`);
1576
+ }
1577
+ };
1578
+ // Option key names match `SparkLineResultOptions` so the
1579
+ // renderer can read them straight from `options`. The
1580
+ // exporter's `parseSparklineFormula` understands both these
1581
+ // names and the legacy `highpoint`/`lowpoint`/… aliases.
1582
+ if (chartType !== "line")
1583
+ pushKV("charttype", chartType);
1584
+ if (color)
1585
+ pushKV("color", color);
1586
+ if (colorHigh)
1587
+ pushKV("colorHigh", colorHigh);
1588
+ if (colorLow)
1589
+ pushKV("colorLow", colorLow);
1590
+ if (colorFirst)
1591
+ pushKV("colorFirst", colorFirst);
1592
+ if (colorLast)
1593
+ pushKV("colorLast", colorLast);
1594
+ if (colorMarkers)
1595
+ pushKV("colorMarkers", colorMarkers);
1596
+ if (colorNegative)
1597
+ pushKV("colorNegative", colorNegative);
1598
+ if (emptyCellsAs)
1599
+ pushKV("emptycells", emptyCellsAs);
1600
+ if (dateAxis)
1601
+ pushKV("dateaxis", true);
1602
+ if (markers)
1603
+ pushKV("markers", true);
1604
+ if (high)
1605
+ pushKV("high", true);
1606
+ if (low)
1607
+ pushKV("low", true);
1608
+ if (first)
1609
+ pushKV("first", true);
1610
+ if (last)
1611
+ pushKV("last", true);
1612
+ if (negative)
1613
+ pushKV("negative", true);
1614
+ if (displayXAxis)
1615
+ pushKV("axis", true);
1616
+ if (displayHidden)
1617
+ pushKV("displayhidden", true);
1618
+ if (rightToLeft)
1619
+ pushKV("rtl", true);
1620
+ if (lineWeight != null)
1621
+ pushKV("lineweight", lineWeight);
1622
+ if (manualMax != null)
1623
+ pushKV("max", manualMax);
1624
+ if (manualMin != null)
1625
+ pushKV("min", manualMin);
1626
+ if (minAxisType)
1627
+ pushKV("minaxistype", minAxisType);
1628
+ if (maxAxisType)
1629
+ pushKV("maxaxistype", maxAxisType);
1630
+ if (options.length > 0) {
1631
+ formula += `,{${options.join(",")}}`;
1632
+ }
1633
+ formula += ")";
1634
+ // Add to map with metadata for building effective value
1635
+ const rowMap = sparklinesByRow.get(rowIdx) ?? new Map();
1636
+ rowMap.set(colIdx, {
1637
+ formula,
1638
+ dataRange: normalizedRange,
1639
+ chartType: chartType,
1640
+ color,
1641
+ ...(colorHigh ? { colorHigh } : {}),
1642
+ ...(colorLow ? { colorLow } : {}),
1643
+ ...(colorFirst ? { colorFirst } : {}),
1644
+ ...(colorLast ? { colorLast } : {}),
1645
+ ...(colorMarkers ? { colorMarkers } : {}),
1646
+ ...(colorNegative ? { colorNegative } : {}),
1647
+ ...(emptyCellsAs ? { emptyCellsAs } : {}),
1648
+ ...(dateAxis ? { dateAxis } : {}),
1649
+ ...(markers ? { markers } : {}),
1650
+ ...(high ? { high } : {}),
1651
+ ...(low ? { low } : {}),
1652
+ ...(first ? { first } : {}),
1653
+ ...(last ? { last } : {}),
1654
+ ...(negative ? { negative } : {}),
1655
+ ...(displayXAxis ? { displayXAxis } : {}),
1656
+ ...(displayHidden ? { displayHidden } : {}),
1657
+ ...(rightToLeft ? { rightToLeft } : {}),
1658
+ ...(lineWeight != null ? { lineWeight } : {}),
1659
+ ...(manualMax != null ? { manualMax } : {}),
1660
+ ...(manualMin != null ? { manualMin } : {}),
1661
+ ...(minAxisType ? { minAxisType } : {}),
1662
+ ...(maxAxisType ? { maxAxisType } : {}),
1663
+ });
1664
+ sparklinesByRow.set(rowIdx, rowMap);
1665
+ }
1666
+ }
1667
+ }
1668
+ }
1669
+ catch (error) {
1670
+ console.error("Error parsing sparklines:", error);
1671
+ }
1672
+ return sparklinesByRow;
1673
+ }
1674
+ constructor() {
1675
+ super();
1676
+ this.xmlParser = new XMLParser(XMLOptions2);
1677
+ this.xmlPreserveValueParser = new XMLParser({
1678
+ ...XMLOptions2,
1679
+ parseTagValue: false,
1680
+ });
1681
+ this.zipParser = new StreamingZipParser();
1682
+ this.sheetCommentsCache = new Map();
1683
+ this.sheetThreadedCommentsCache = new Map();
1684
+ }
1685
+ async loadWorkbookXml() {
1686
+ const data = await this.zipParser.getText("xl/workbook.xml");
1687
+ this.workbookXml = this.xmlParser.parse(data);
1688
+ this.workSheets = this.extractSheetsFromWorkbook(this.workbookXml);
1689
+ this.worksheetBySheetId = new Map();
1690
+ for (const element of this.workSheets) {
1691
+ const sheetId = Number(element?.a$?.sheetId);
1692
+ if (Number.isFinite(sheetId)) {
1693
+ this.worksheetBySheetId.set(sheetId, element);
1694
+ }
1695
+ }
1696
+ this.calcPr =
1697
+ get(this.workbookXml, "workbook.calcPr") ||
1698
+ get(this.workbookXml, "x:workbook.x:calcPr");
1699
+ this.workbookPr =
1700
+ get(this.workbookXml, "workbook.workbookPr") ||
1701
+ get(this.workbookXml, "x:workbook.x:workbookPr");
1702
+ }
1703
+ extractSheetsFromWorkbook(workbookXml) {
1704
+ let workSheets = get(workbookXml, "workbook.sheets.sheet") ||
1705
+ get(workbookXml, "x:workbook.x:sheets.x:sheet");
1706
+ workSheets = Array.isArray(workSheets) ? workSheets : [workSheets];
1707
+ return workSheets.filter(Boolean);
1708
+ }
1709
+ async loadTheme() {
1710
+ try {
1711
+ const themeContent = await this.zipParser.getText("xl/theme/theme1.xml");
1712
+ const themeXml = this.xmlParser.parse(themeContent);
1713
+ const themeName = get(themeXml, "a:theme.a$.name") || "Office";
1714
+ const { primaryFontFamily, headingFontFamily, majorFontFamily, minorFontFamily, } = extractThemeFonts(themeXml);
1715
+ // Raw scheme typefaces feed `<font><scheme val>` resolution in
1716
+ // styles.ts; absent theme fonts stay undefined (unresolved).
1717
+ this.workbookStyles.themeFonts = {
1718
+ major: majorFontFamily,
1719
+ minor: minorFontFamily,
1720
+ };
1721
+ const themeColors = extractThemeColors(themeXml, defaultSpreadsheetTheme);
1722
+ const darkThemeColors = generateDarkThemeColors(themeColors);
1723
+ return {
1724
+ name: themeName,
1725
+ primaryFontFamily,
1726
+ headingFontFamily,
1727
+ themeColors,
1728
+ darkThemeColors,
1729
+ };
1730
+ }
1731
+ catch (err) {
1732
+ return defaultSpreadsheetTheme;
1733
+ }
1734
+ }
1735
+ async loadSharedStrings() {
1736
+ try {
1737
+ const sharedStringsData = await this.zipParser.getText("xl/sharedStrings.xml");
1738
+ if (!sharedStringsData) {
1739
+ this.richSharedStringRuns = new Map();
1740
+ return [];
1741
+ }
1742
+ const parsedStrings = this.xmlPreserveValueParser.parse(sharedStringsData);
1743
+ const sst = getWithFallback(parsedStrings, [
1744
+ "sst.si",
1745
+ "sst.x:si",
1746
+ "x:sst.si",
1747
+ "x:sst.x:si",
1748
+ ]) || [];
1749
+ const stringArray = Array.isArray(sst) ? sst : [sst];
1750
+ const sharedStrings = new Array(stringArray.length);
1751
+ const richRuns = new Map();
1752
+ stringArray.forEach((item, index) => {
1753
+ const textNode = item?.t ?? item?.["x:t"];
1754
+ if (textNode !== undefined) {
1755
+ if (typeof textNode === "string") {
1756
+ sharedStrings[index] = textNode;
1757
+ }
1758
+ else if (textNode?.t$) {
1759
+ sharedStrings[index] = textNode.t$;
1760
+ }
1761
+ else {
1762
+ sharedStrings[index] = String(textNode);
1763
+ }
1764
+ }
1765
+ else if (item?.r || item?.["x:r"]) {
1766
+ const richNode = item.r ?? item["x:r"];
1767
+ const partsArray = Array.isArray(richNode) ? richNode : [richNode];
1768
+ // Build both the flattened text AND the per-run format
1769
+ // metadata so the cell can carry `tf` back into the model.
1770
+ let runningIndex = 0;
1771
+ const runs = [];
1772
+ const concatenated = partsArray
1773
+ .map((part) => {
1774
+ const text = extractRunText(part);
1775
+ if (!text)
1776
+ return "";
1777
+ const format = extractRunFormat(part);
1778
+ const startIndex = runningIndex;
1779
+ runningIndex += text.length;
1780
+ if (format && hasAnyFormatKey(format)) {
1781
+ runs.push({
1782
+ startIndex,
1783
+ endIndex: runningIndex,
1784
+ format,
1785
+ nodeType: "text",
1786
+ });
1787
+ }
1788
+ return text;
1789
+ })
1790
+ .join("");
1791
+ sharedStrings[index] = concatenated;
1792
+ if (runs.length > 0) {
1793
+ richRuns.set(index, runs);
1794
+ }
1795
+ }
1796
+ else {
1797
+ sharedStrings[index] = "";
1798
+ }
1799
+ });
1800
+ this.richSharedStringRuns = richRuns;
1801
+ return sharedStrings;
1802
+ }
1803
+ catch (err) {
1804
+ this.richSharedStringRuns = new Map();
1805
+ return [];
1806
+ }
1807
+ }
1808
+ async load(buffer, password) {
1809
+ await this.zipParser.load(buffer, password);
1810
+ this.sharedFormulaMap = new Map();
1811
+ this.sheetCommentsCache.clear();
1812
+ this.sheetThreadedCommentsCache.clear();
1813
+ this.personsPromise = null;
1814
+ this.sheetFeatures.clear();
1815
+ this.lastSheetDataCache = null;
1816
+ this.sheetMetadataCache.clear();
1817
+ this.dataValidationsBySheetCache.clear();
1818
+ this.conditionalFormattingBySheetCache.clear();
1819
+ await this.loadWorkbookXml();
1820
+ this.relations = await this.getRelations();
1821
+ this.workbookStyles = await WorkbookStyles.load(this.zipParser, this.xmlParser);
1822
+ this.theme = await this.loadTheme();
1823
+ this.sharedStrings = await this.loadSharedStrings();
1824
+ this.cellXfs = new Map();
1825
+ this.fonts = this.getFonts();
1826
+ this.namedRanges = this._getNamedRanges();
1827
+ }
1828
+ dispose() {
1829
+ void this.zipParser.dispose();
1830
+ this.sheetCommentsCache.clear();
1831
+ this.sheetThreadedCommentsCache.clear();
1832
+ this.personsPromise = null;
1833
+ this.sheetFeatures.clear();
1834
+ this.lastSheetDataCache = null;
1835
+ this.sheetMetadataCache.clear();
1836
+ this.dataValidationsBySheetCache.clear();
1837
+ this.conditionalFormattingBySheetCache.clear();
1838
+ this.worksheetBySheetId.clear();
1839
+ }
1840
+ clearSharedFormula() {
1841
+ if (!this.sharedFormulaMap) {
1842
+ this.sharedFormulaMap = new Map();
1843
+ }
1844
+ this.sharedFormulaMap.clear();
1845
+ }
1846
+ registerFormat(format) {
1847
+ // Create a serialized version of the format to use as a hash key
1848
+ const formatKey = getCellXfsKey(format);
1849
+ // Check if this format already exists
1850
+ if (this.cellXfs.has(formatKey)) {
1851
+ return formatKey;
1852
+ }
1853
+ this.cellXfs.set(formatKey, format);
1854
+ return formatKey;
1855
+ }
1856
+ getConditionaFormattingFromSheet(sheetId, sheetData) {
1857
+ // Get conditional formatting rules for this sheet
1858
+ const sheetRules = getConditionalFormattingFromSheet(sheetData, sheetId, this.workbookStyles.styles, this.workbookStyles.indexedColors);
1859
+ return sheetRules;
1860
+ }
1861
+ async getTablesFromSheet(sheetId, rId, tablePartIds) {
1862
+ const tables = [];
1863
+ if (!tablePartIds.length) {
1864
+ return tables;
1865
+ }
1866
+ // Get sheet relationships to find table files
1867
+ const target = this.relations[rId].target;
1868
+ const sheetPath = target.startsWith("/xl")
1869
+ ? target.replace(/^\//gi, "")
1870
+ : `xl/${this.relations[rId].target}`;
1871
+ const sheetRelPath = sheetPath.replace("worksheets", "worksheets/_rels") + ".rels";
1872
+ try {
1873
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
1874
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
1875
+ const tableRelations = get(sheetRels, "Relationships.Relationship");
1876
+ const tableRelationsArray = Array.isArray(tableRelations)
1877
+ ? tableRelations
1878
+ : [tableRelations];
1879
+ // Process each table in the sheet
1880
+ for (const tableRId of tablePartIds) {
1881
+ // Find this table's relationship
1882
+ const tableRel = tableRelationsArray.find((rel) => rel.a$.Id === tableRId);
1883
+ if (!tableRel)
1884
+ continue;
1885
+ // Get table file path
1886
+ let tablePath = tableRel.a$.Target;
1887
+ if (tablePath.startsWith("../")) {
1888
+ tablePath = `xl/${tablePath.replace("../", "")}`;
1889
+ }
1890
+ else if (!tablePath.startsWith("/")) {
1891
+ tablePath = `xl/worksheets/${tablePath}`;
1892
+ }
1893
+ else {
1894
+ tablePath = tablePath.startsWith("/")
1895
+ ? tablePath.substring(1)
1896
+ : `xl/${tablePath}`;
1897
+ }
1898
+ // Parse table definition
1899
+ const tableContent = await this.zipParser.getText(tablePath);
1900
+ const tableData = this.xmlParser.parse(tableContent || "");
1901
+ // Handle both with and without namespace prefix
1902
+ const tableDefinition = get(tableData, "table") || get(tableData, "x:table");
1903
+ if (tableDefinition) {
1904
+ // Extract table attributes
1905
+ const attrs = tableDefinition.a$ || {};
1906
+ const displayName = attrs.displayName || attrs.name || `Table${tables.length + 1}`;
1907
+ // Clean the title to remove spaces and special characters
1908
+ // const title = displayName.replace(/[^a-zA-Z0-9]/g, "");
1909
+ // Parse table settings
1910
+ const headerRow = attrs.headerRowCount !== "0";
1911
+ const totalRow = attrs.totalsRowCount === "1";
1912
+ const showFirstColumn = attrs.showFirstColumn === "1";
1913
+ const showLastColumn = attrs.showLastColumn === "1";
1914
+ const showRowStripes = attrs.showRowStripes !== "0";
1915
+ const showColumnStripes = attrs.showColumnStripes === "1";
1916
+ // Parse table columns
1917
+ // Handle both with and without namespace prefix
1918
+ const columnDefinitions = get(tableDefinition, "tableColumns.tableColumn") ||
1919
+ get(tableDefinition, "x:tableColumns.x:tableColumn");
1920
+ const columnDefsArray = Array.isArray(columnDefinitions)
1921
+ ? columnDefinitions
1922
+ : columnDefinitions
1923
+ ? [columnDefinitions]
1924
+ : [];
1925
+ const columns = columnDefsArray.map((colDef) => {
1926
+ const name = colDef.a$.name || "";
1927
+ const columnObj = { name };
1928
+ // Check for calculated column
1929
+ // Handle both with and without namespace prefix
1930
+ const calculatedColumnFormula = get(colDef, "calculatedColumnFormula") ||
1931
+ get(colDef, "x:calculatedColumnFormula");
1932
+ if (calculatedColumnFormula) {
1933
+ return {
1934
+ ...columnObj,
1935
+ formula: calculatedColumnFormula,
1936
+ };
1937
+ }
1938
+ return columnObj;
1939
+ });
1940
+ // Parse table style information
1941
+ // Handle both with and without namespace prefix
1942
+ const tableStyle = get(tableDefinition, "tableStyleInfo") ||
1943
+ get(tableDefinition, "x:tableStyleInfo") ||
1944
+ {};
1945
+ const tableStyleAttrs = tableStyle.a$ || {};
1946
+ // Extract filter button settings
1947
+ const filterButton = attrs.filterButton !== "0";
1948
+ const range = getRangeFromAddressRef(attrs.ref);
1949
+ if (range) {
1950
+ // Handle both with and without namespace prefix
1951
+ const autoFilter = tableDefinition.autoFilter ?? tableDefinition["x:autoFilter"];
1952
+ const filterSpecs = createFilterSpecsFromAutoFilter(autoFilter, (dxfId, cellColor) => this.resolveDxfColor(dxfId, cellColor));
1953
+ // Create the table view object
1954
+ const tableView = {
1955
+ id: attrs.id,
1956
+ title: attrs.name,
1957
+ sheetId,
1958
+ headerRow,
1959
+ totalRow,
1960
+ showRowStripes,
1961
+ showColumnStripes,
1962
+ showFirstColumn,
1963
+ showLastColumn,
1964
+ filterButton,
1965
+ columns,
1966
+ range,
1967
+ filterSpecs,
1968
+ };
1969
+ // Add table theme if available
1970
+ if (tableStyleAttrs.name) {
1971
+ const fullStyleName = tableStyleAttrs.name;
1972
+ // Extract base style name (handle custom styles like "TableStyleMedium4 2")
1973
+ const styleName = fullStyleName.split(" ")[0];
1974
+ tableView.theme = styleName;
1975
+ // First try to get custom table style from styles.xml
1976
+ let bandedRange = this.workbookStyles.getCustomTableStyleBanding(fullStyleName);
1977
+ // Fall back to standard table styles
1978
+ if (!bandedRange) {
1979
+ bandedRange = findMatchingBandProperty(styleName, showRowStripes, showColumnStripes);
1980
+ }
1981
+ if (bandedRange) {
1982
+ // Only include row/column properties based on stripe settings
1983
+ if (!showRowStripes && bandedRange.rowProperties) {
1984
+ // Keep header and total row styling even if stripes are off
1985
+ const { headerColor, headerBorder, totalRowColor, totalRowBorder, } = bandedRange.rowProperties;
1986
+ if (headerColor ||
1987
+ headerBorder ||
1988
+ totalRowColor ||
1989
+ totalRowBorder) {
1990
+ bandedRange.rowProperties = {
1991
+ headerColor,
1992
+ headerBorder,
1993
+ totalRowColor,
1994
+ totalRowBorder,
1995
+ };
1996
+ }
1997
+ else {
1998
+ delete bandedRange.rowProperties;
1999
+ }
2000
+ }
2001
+ if (!showColumnStripes) {
2002
+ delete bandedRange.columnProperties;
2003
+ }
2004
+ tableView.bandedRange = bandedRange;
2005
+ }
2006
+ }
2007
+ tables.push(tableView);
2008
+ }
2009
+ }
2010
+ }
2011
+ }
2012
+ catch (err) {
2013
+ // Skip if relation file doesn't exist or has issues
2014
+ console.log("Error parsing table", err);
2015
+ }
2016
+ return tables;
2017
+ }
2018
+ resolveSheetPath(target) {
2019
+ if (target.startsWith("/")) {
2020
+ return target.replace(/^\//, "");
2021
+ }
2022
+ if (target.startsWith("xl/")) {
2023
+ return target;
2024
+ }
2025
+ return `xl/${target}`;
2026
+ }
2027
+ resolveRelationshipTarget(basePath, target) {
2028
+ if (target.startsWith("/")) {
2029
+ return target.replace(/^\//, "");
2030
+ }
2031
+ if (target.startsWith("../")) {
2032
+ return `xl/${target.replace("../", "")}`;
2033
+ }
2034
+ if (target.startsWith("xl/")) {
2035
+ return target;
2036
+ }
2037
+ const baseDir = basePath.substring(0, basePath.lastIndexOf("/"));
2038
+ return `${baseDir}/${target}`;
2039
+ }
2040
+ getSheetPathByRid(rid) {
2041
+ const relation = this.relations[rid];
2042
+ if (!relation) {
2043
+ throw new Error(`Missing worksheet relation for ${rid}`);
2044
+ }
2045
+ return this.resolveSheetPath(relation.target);
2046
+ }
2047
+ async *streamSheetRows(rid) {
2048
+ const path = this.getSheetPathByRid(rid);
2049
+ let inSheetData = false;
2050
+ let currentRow = null;
2051
+ let currentCell = null;
2052
+ let currentValueText = "";
2053
+ let currentFormulaText = "";
2054
+ let currentInlineText = "";
2055
+ let currentFormulaAttrs = null;
2056
+ let currentTag = null;
2057
+ let inInlineStr = false;
2058
+ let inInlineRun = false;
2059
+ let inInlineRunProps = false;
2060
+ let inlineRunStart = 0;
2061
+ let inlineRunText = "";
2062
+ let inlineRunFormat = {};
2063
+ let inlineRuns = [];
2064
+ const normalizeTag = (name) => name.includes(":") ? name.split(":")[1] : name;
2065
+ const normalizeAttrs = (attrs) => Object.keys(attrs).length ? { ...attrs } : {};
2066
+ const truthyAttr = (attrs) => {
2067
+ const val = attrs.val;
2068
+ return val == null || val === "1" || val === "true";
2069
+ };
2070
+ const commitInlineRun = () => {
2071
+ const endIndex = inlineRunStart + inlineRunText.length;
2072
+ if (inlineRunText && hasAnyFormatKey(inlineRunFormat)) {
2073
+ inlineRuns.push({
2074
+ startIndex: inlineRunStart,
2075
+ endIndex,
2076
+ format: { ...inlineRunFormat },
2077
+ nodeType: "text",
2078
+ });
2079
+ }
2080
+ inInlineRun = false;
2081
+ inInlineRunProps = false;
2082
+ inlineRunStart = currentInlineText.length;
2083
+ inlineRunText = "";
2084
+ inlineRunFormat = {};
2085
+ };
2086
+ const applyInlineRunProperty = (name, attrs) => {
2087
+ switch (name) {
2088
+ case "b":
2089
+ if (truthyAttr(attrs))
2090
+ inlineRunFormat.bold = true;
2091
+ break;
2092
+ case "i":
2093
+ if (truthyAttr(attrs))
2094
+ inlineRunFormat.italic = true;
2095
+ break;
2096
+ case "u":
2097
+ if (attrs.val !== "none")
2098
+ inlineRunFormat.underline = true;
2099
+ break;
2100
+ case "strike":
2101
+ if (truthyAttr(attrs))
2102
+ inlineRunFormat.strikethrough = true;
2103
+ break;
2104
+ case "sz": {
2105
+ const fontSize = Number(attrs.val);
2106
+ if (Number.isFinite(fontSize) && fontSize > 0) {
2107
+ inlineRunFormat.fontSize = fontSize;
2108
+ }
2109
+ break;
2110
+ }
2111
+ case "rFont":
2112
+ if (attrs.val)
2113
+ inlineRunFormat.fontFamily = attrs.val;
2114
+ break;
2115
+ case "color":
2116
+ if (attrs.rgb) {
2117
+ const hex = attrs.rgb.length === 8 ? attrs.rgb.slice(2) : attrs.rgb;
2118
+ inlineRunFormat.color = `#${hex.toUpperCase()}`;
2119
+ }
2120
+ else if (attrs.theme != null) {
2121
+ const theme = Number(attrs.theme);
2122
+ const tint = attrs.tint != null && attrs.tint !== ""
2123
+ ? Number(attrs.tint)
2124
+ : undefined;
2125
+ if (Number.isFinite(theme)) {
2126
+ inlineRunFormat.color = {
2127
+ theme,
2128
+ tint: Number.isFinite(tint) ? tint : undefined,
2129
+ };
2130
+ }
2131
+ }
2132
+ break;
2133
+ case "vertAlign":
2134
+ if (attrs.val === "superscript") {
2135
+ inlineRunFormat.baselineOffset = "superscript";
2136
+ }
2137
+ else if (attrs.val === "subscript") {
2138
+ inlineRunFormat.baselineOffset = "subscript";
2139
+ }
2140
+ break;
2141
+ }
2142
+ };
2143
+ // Bounded producer/consumer queue. The zip stream pauses (via the async
2144
+ // onChunk backpressure in StreamingZipParser.streamText) whenever the
2145
+ // consumer falls behind by more than HIGH_WATER_ROWS, so worker memory
2146
+ // stays flat regardless of sheet size instead of buffering every parsed
2147
+ // row before the first yield.
2148
+ const HIGH_WATER_ROWS = 1024;
2149
+ const queue = [];
2150
+ let queueHead = 0;
2151
+ let done = false;
2152
+ let pendingResolve = null;
2153
+ let drainResolve = null;
2154
+ const queuedRows = () => queue.length - queueHead;
2155
+ const releaseDrain = () => {
2156
+ if (drainResolve) {
2157
+ const resolve = drainResolve;
2158
+ drainResolve = null;
2159
+ resolve();
2160
+ }
2161
+ };
2162
+ const notifyDrained = () => {
2163
+ if (queuedRows() < HIGH_WATER_ROWS / 2) {
2164
+ releaseDrain();
2165
+ }
2166
+ };
2167
+ const pushRow = (row) => {
2168
+ if (pendingResolve) {
2169
+ const resolve = pendingResolve;
2170
+ pendingResolve = null;
2171
+ resolve(row);
2172
+ return;
2173
+ }
2174
+ queue.push(row);
2175
+ };
2176
+ const closeQueue = () => {
2177
+ done = true;
2178
+ if (pendingResolve) {
2179
+ const resolve = pendingResolve;
2180
+ pendingResolve = null;
2181
+ resolve(null);
2182
+ }
2183
+ };
2184
+ // Need to capture `this` for use inside parser handlers
2185
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
2186
+ const self = this;
2187
+ const parser = new HtmlParser({
2188
+ onopentag(tagName, attrs) {
2189
+ const name = normalizeTag(tagName);
2190
+ if (!self.currentSheetTotalRows && name === "dimension") {
2191
+ const ref = attrs?.ref;
2192
+ if (typeof ref === "string") {
2193
+ const range = getRangeFromAddressRef(ref);
2194
+ if (range) {
2195
+ self.currentSheetTotalRows = range.endRowIndex;
2196
+ }
2197
+ }
2198
+ }
2199
+ if (name === "sheetData") {
2200
+ inSheetData = true;
2201
+ return;
2202
+ }
2203
+ if (!inSheetData)
2204
+ return;
2205
+ if (name === "row") {
2206
+ currentRow = { a$: normalizeAttrs(attrs), c: [] };
2207
+ return;
2208
+ }
2209
+ if (name === "c" && currentRow) {
2210
+ currentCell = { a$: normalizeAttrs(attrs) };
2211
+ currentRow.c.push(currentCell);
2212
+ return;
2213
+ }
2214
+ if (name === "v" && currentCell) {
2215
+ currentValueText = "";
2216
+ currentTag = "v";
2217
+ return;
2218
+ }
2219
+ if (name === "f" && currentCell) {
2220
+ currentFormulaText = "";
2221
+ currentFormulaAttrs = normalizeAttrs(attrs);
2222
+ currentTag = "f";
2223
+ return;
2224
+ }
2225
+ if (name === "is" && currentCell) {
2226
+ currentInlineText = "";
2227
+ inlineRuns = [];
2228
+ inInlineStr = true;
2229
+ return;
2230
+ }
2231
+ if (name === "r" && currentCell && inInlineStr) {
2232
+ inInlineRun = true;
2233
+ inlineRunStart = currentInlineText.length;
2234
+ inlineRunText = "";
2235
+ inlineRunFormat = {};
2236
+ return;
2237
+ }
2238
+ if (name === "rPr" && inInlineRun) {
2239
+ inInlineRunProps = true;
2240
+ return;
2241
+ }
2242
+ if (inInlineRunProps) {
2243
+ applyInlineRunProperty(name, normalizeAttrs(attrs));
2244
+ return;
2245
+ }
2246
+ if (name === "t" && currentCell && inInlineStr) {
2247
+ currentTag = "inline";
2248
+ }
2249
+ },
2250
+ ontext(text) {
2251
+ if (currentTag === "v") {
2252
+ currentValueText += text;
2253
+ return;
2254
+ }
2255
+ if (currentTag === "f") {
2256
+ currentFormulaText += text;
2257
+ return;
2258
+ }
2259
+ if (currentTag === "inline") {
2260
+ currentInlineText += text;
2261
+ if (inInlineRun) {
2262
+ inlineRunText += text;
2263
+ }
2264
+ }
2265
+ },
2266
+ onclosetag(tagName) {
2267
+ const name = normalizeTag(tagName);
2268
+ if (name === "v" && currentCell) {
2269
+ currentCell.v = { t$: currentValueText };
2270
+ currentValueText = "";
2271
+ currentTag = null;
2272
+ return;
2273
+ }
2274
+ if (name === "f" && currentCell) {
2275
+ currentCell.f = currentFormulaAttrs
2276
+ ? { a$: currentFormulaAttrs, t$: currentFormulaText }
2277
+ : currentFormulaText;
2278
+ currentFormulaText = "";
2279
+ currentFormulaAttrs = null;
2280
+ currentTag = null;
2281
+ return;
2282
+ }
2283
+ if (name === "t") {
2284
+ currentTag = null;
2285
+ return;
2286
+ }
2287
+ if (name === "rPr") {
2288
+ inInlineRunProps = false;
2289
+ return;
2290
+ }
2291
+ if (name === "r" && inInlineRun) {
2292
+ commitInlineRun();
2293
+ return;
2294
+ }
2295
+ if (name === "is" && currentCell) {
2296
+ if (currentInlineText) {
2297
+ if (inlineRuns.length > 0) {
2298
+ const stringIndex = self.sharedStrings.length;
2299
+ self.sharedStrings.push(currentInlineText);
2300
+ if (!self.richSharedStringRuns) {
2301
+ self.richSharedStringRuns = new Map();
2302
+ }
2303
+ self.richSharedStringRuns.set(stringIndex, inlineRuns);
2304
+ currentCell.a$ = { ...(currentCell.a$ ?? {}), t: "s" };
2305
+ currentCell.v = { t$: String(stringIndex) };
2306
+ }
2307
+ else {
2308
+ currentCell.is = { t: { t$: currentInlineText } };
2309
+ }
2310
+ }
2311
+ currentInlineText = "";
2312
+ inlineRuns = [];
2313
+ inInlineStr = false;
2314
+ return;
2315
+ }
2316
+ if (name === "c") {
2317
+ currentCell = null;
2318
+ return;
2319
+ }
2320
+ if (name === "row" && currentRow) {
2321
+ pushRow(currentRow);
2322
+ currentRow = null;
2323
+ return;
2324
+ }
2325
+ if (name === "sheetData") {
2326
+ inSheetData = false;
2327
+ }
2328
+ },
2329
+ onerror(err) {
2330
+ console.error("Streaming XML parse error", err);
2331
+ },
2332
+ }, { xmlMode: true });
2333
+ // Start the zip → SAX pipeline WITHOUT awaiting it so rows are yielded
2334
+ // while decompression is still in flight. Awaiting streamText here first
2335
+ // (the previous behavior) buffered the entire sheet's rows in `queue`
2336
+ // before yield #1 — ~1.3GB on a 5.9M-cell sheet.
2337
+ let aborted = false;
2338
+ let producerError = null;
2339
+ const producerDone = (async () => {
2340
+ try {
2341
+ await this.zipParser.streamText(path, (chunk) => {
2342
+ if (aborted)
2343
+ return;
2344
+ parser.write(chunk);
2345
+ if (queuedRows() >= HIGH_WATER_ROWS) {
2346
+ return new Promise((resolve) => (drainResolve = resolve));
2347
+ }
2348
+ });
2349
+ if (!aborted) {
2350
+ parser.end();
2351
+ }
2352
+ }
2353
+ catch (err) {
2354
+ producerError = err;
2355
+ }
2356
+ finally {
2357
+ closeQueue();
2358
+ }
2359
+ })();
2360
+ try {
2361
+ while (true) {
2362
+ if (queueHead < queue.length) {
2363
+ const row = queue[queueHead];
2364
+ queue[queueHead] = undefined;
2365
+ queueHead++;
2366
+ // Compact consumed slots so the array doesn't grow to sheet length.
2367
+ if (queueHead >= 2048 && queueHead * 2 >= queue.length) {
2368
+ queue.splice(0, queueHead);
2369
+ queueHead = 0;
2370
+ }
2371
+ notifyDrained();
2372
+ yield row;
2373
+ continue;
2374
+ }
2375
+ if (done) {
2376
+ break;
2377
+ }
2378
+ const row = await new Promise((resolve) => (pendingResolve = resolve));
2379
+ if (row === null) {
2380
+ break;
2381
+ }
2382
+ yield row;
2383
+ }
2384
+ await producerDone;
2385
+ if (producerError) {
2386
+ throw producerError;
2387
+ }
2388
+ }
2389
+ finally {
2390
+ // Unstick the producer if the consumer exits early so the stream
2391
+ // callback's pending drain promise can't leak.
2392
+ aborted = true;
2393
+ releaseDrain();
2394
+ }
2395
+ }
2396
+ extractCommentText(comment) {
2397
+ const textNode = getWithFallback(comment, ["text", "x:text"]);
2398
+ if (!textNode) {
2399
+ return "";
2400
+ }
2401
+ const runs = getWithFallback(textNode, ["r", "x:r"]) ??
2402
+ (Array.isArray(textNode) ? textNode : undefined);
2403
+ if (runs) {
2404
+ const runArray = Array.isArray(runs) ? runs : [runs];
2405
+ return runArray
2406
+ .map((run) => {
2407
+ const textValue = getWithFallback(run, ["t", "x:t"]);
2408
+ if (typeof textValue === "string") {
2409
+ return textValue;
2410
+ }
2411
+ if (textValue && typeof textValue === "object") {
2412
+ return getWithFallback(textValue, ["t$", "_", "#text"]) ?? "";
2413
+ }
2414
+ return "";
2415
+ })
2416
+ .join("");
2417
+ }
2418
+ const textValue = getWithFallback(textNode, ["t", "x:t"]);
2419
+ if (typeof textValue === "string") {
2420
+ return textValue;
2421
+ }
2422
+ if (textValue && typeof textValue === "object") {
2423
+ return getWithFallback(textValue, ["t$", "_", "#text"]) ?? "";
2424
+ }
2425
+ return "";
2426
+ }
2427
+ async getCommentsForSheet(rId) {
2428
+ if (this.sheetCommentsCache.has(rId)) {
2429
+ return this.sheetCommentsCache.get(rId);
2430
+ }
2431
+ const comments = {};
2432
+ const relation = this.relations[rId];
2433
+ if (!relation) {
2434
+ this.sheetCommentsCache.set(rId, comments);
2435
+ return comments;
2436
+ }
2437
+ const sheetPath = this.resolveSheetPath(relation.target);
2438
+ const sheetRelPath = sheetPath.replace("worksheets", "worksheets/_rels") + ".rels";
2439
+ let sheetRelContent;
2440
+ try {
2441
+ sheetRelContent = await this.zipParser.getText(sheetRelPath);
2442
+ }
2443
+ catch {
2444
+ this.sheetCommentsCache.set(rId, comments);
2445
+ return comments;
2446
+ }
2447
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
2448
+ const relationships = get(sheetRels, "Relationships.Relationship");
2449
+ const relationshipsArray = Array.isArray(relationships)
2450
+ ? relationships
2451
+ : relationships
2452
+ ? [relationships]
2453
+ : [];
2454
+ const commentRel = relationshipsArray.find((rel) => rel?.a$?.Type?.includes("/comments"));
2455
+ if (!commentRel) {
2456
+ this.sheetCommentsCache.set(rId, comments);
2457
+ return comments;
2458
+ }
2459
+ const commentsPath = this.resolveRelationshipTarget(sheetPath, commentRel.a$.Target);
2460
+ try {
2461
+ const commentsContent = await this.zipParser.getText(commentsPath);
2462
+ const parsedComments = this.xmlPreserveValueParser.parse(commentsContent);
2463
+ const commentList = get(parsedComments, "comments.commentList.comment") ||
2464
+ get(parsedComments, "x:comments.x:commentList.x:comment");
2465
+ const commentArray = Array.isArray(commentList)
2466
+ ? commentList
2467
+ : commentList
2468
+ ? [commentList]
2469
+ : [];
2470
+ for (const comment of commentArray) {
2471
+ const ref = comment?.a$?.ref;
2472
+ if (!ref)
2473
+ continue;
2474
+ const text = this.extractCommentText(comment);
2475
+ if (text) {
2476
+ comments[ref] = text;
2477
+ }
2478
+ }
2479
+ }
2480
+ catch {
2481
+ // Ignore missing comment files
2482
+ }
2483
+ this.sheetCommentsCache.set(rId, comments);
2484
+ return comments;
2485
+ }
2486
+ /**
2487
+ * Workbook-level author list from `xl/persons/person.xml` (located via the
2488
+ * workbook rels' `.../person` relationship). Empty map when the workbook
2489
+ * has no threaded comments.
2490
+ */
2491
+ getPersons() {
2492
+ if (!this.personsPromise) {
2493
+ this.personsPromise = (async () => {
2494
+ try {
2495
+ const personRel = Object.values(this.relations ?? {}).find((rel) => rel?.type?.endsWith("/person"));
2496
+ if (!personRel) {
2497
+ return new Map();
2498
+ }
2499
+ const personsContent = await this.zipParser.getText(this.resolveSheetPath(personRel.target));
2500
+ return parsePersons(this.xmlPreserveValueParser.parse(personsContent));
2501
+ }
2502
+ catch {
2503
+ // Missing/corrupt persons part — authors resolve to "Unknown".
2504
+ return new Map();
2505
+ }
2506
+ })();
2507
+ }
2508
+ return this.personsPromise;
2509
+ }
2510
+ /**
2511
+ * Modern threaded comments for one sheet, parsed from the sheet's
2512
+ * `.../threadedComment` relationship part. Threads are returned in the
2513
+ * exporter's model (`CommentThread[]` + per-cell anchor addresses); the
2514
+ * row stream stamps `commentThreadId` onto the anchor cells and skips
2515
+ * their legacy `tc=` shadow notes. Empty result (and legacy notes kept as
2516
+ * a fallback) when the part is absent or unreadable.
2517
+ */
2518
+ async getThreadedCommentsForSheet(rId) {
2519
+ const cached = this.sheetThreadedCommentsCache.get(rId);
2520
+ if (cached) {
2521
+ return cached;
2522
+ }
2523
+ let result = EMPTY_SHEET_THREADED_COMMENTS;
2524
+ const relation = this.relations[rId];
2525
+ if (!relation) {
2526
+ this.sheetThreadedCommentsCache.set(rId, result);
2527
+ return result;
2528
+ }
2529
+ const sheetPath = this.resolveSheetPath(relation.target);
2530
+ const sheetRelPath = sheetPath.replace("worksheets", "worksheets/_rels") + ".rels";
2531
+ try {
2532
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
2533
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
2534
+ const relationships = get(sheetRels, "Relationships.Relationship");
2535
+ const relationshipsArray = Array.isArray(relationships)
2536
+ ? relationships
2537
+ : relationships
2538
+ ? [relationships]
2539
+ : [];
2540
+ const threadedRel = relationshipsArray.find((rel) => rel?.a$?.Type?.endsWith("/threadedComment"));
2541
+ if (threadedRel) {
2542
+ const threadedPath = this.resolveRelationshipTarget(sheetPath, threadedRel.a$.Target);
2543
+ const threadedContent = await this.zipParser.getText(threadedPath);
2544
+ result = parseThreadedComments(this.xmlPreserveValueParser.parse(threadedContent), await this.getPersons());
2545
+ }
2546
+ }
2547
+ catch {
2548
+ // Missing/corrupt threaded parts — the legacy shadow notes still
2549
+ // import as plain notes, so nothing is lost outright.
2550
+ }
2551
+ this.sheetThreadedCommentsCache.set(rId, result);
2552
+ return result;
2553
+ }
2554
+ /**
2555
+ * Comment threads for one sheet (or the whole workbook when `sheetId` is
2556
+ * omitted), in the model `createExcelFile({ comments })` consumes. Cells
2557
+ * anchor threads via the `commentThreadId` the row stream stamped.
2558
+ */
2559
+ async getCommentThreads(sheetId) {
2560
+ const elements = sheetId === undefined
2561
+ ? this._getSheets()
2562
+ : [this.worksheetBySheetId.get(sheetId)];
2563
+ const threads = [];
2564
+ for (const element of elements) {
2565
+ const rid = element?.a$?.["r:id"];
2566
+ if (!rid)
2567
+ continue;
2568
+ const sheetThreads = await this.getThreadedCommentsForSheet(rid);
2569
+ threads.push(...sheetThreads.threads);
2570
+ }
2571
+ return threads;
2572
+ }
2573
+ getFonts() {
2574
+ const fontSet = new Set();
2575
+ // The Fonts array contains all font definitions used in the workbook.
2576
+ // `<name>` may be namespaced (`<x:name>`), and a font may carry only a
2577
+ // `<scheme val="major|minor"/>` theme reference instead of a concrete
2578
+ // name — resolve those against the theme typefaces so the preload
2579
+ // list is complete.
2580
+ const fonts = this.workbookStyles?.fonts;
2581
+ if (fonts) {
2582
+ const fontArray = Array.isArray(fonts) ? fonts : [fonts];
2583
+ const themeFonts = this.workbookStyles?.themeFonts;
2584
+ for (const font of fontArray) {
2585
+ const name = getWithFallback(font, [
2586
+ "name.a$.val",
2587
+ "x:name.a$.val",
2588
+ ]);
2589
+ if (name) {
2590
+ fontSet.add(name);
2591
+ continue;
2592
+ }
2593
+ const scheme = getWithFallback(font, [
2594
+ "scheme.a$.val",
2595
+ "x:scheme.a$.val",
2596
+ ]);
2597
+ const resolved = scheme === "major"
2598
+ ? themeFonts?.major
2599
+ : scheme === "minor"
2600
+ ? themeFonts?.minor
2601
+ : undefined;
2602
+ if (resolved) {
2603
+ fontSet.add(resolved);
2604
+ }
2605
+ }
2606
+ }
2607
+ // Rich-text shared-string runs carry per-run `<rPr><rFont val>` fonts
2608
+ // that never appear in styles.xml's `<fonts>` table.
2609
+ if (this.richSharedStringRuns) {
2610
+ for (const runs of this.richSharedStringRuns.values()) {
2611
+ for (const run of runs) {
2612
+ const family = run.nodeType === "text" ? run.format?.fontFamily : undefined;
2613
+ if (family) {
2614
+ fontSet.add(family);
2615
+ }
2616
+ }
2617
+ }
2618
+ }
2619
+ return [...fontSet];
2620
+ }
2621
+ getCellXfs() {
2622
+ return this.cellXfs;
2623
+ }
2624
+ /**
2625
+ * Extract column widths and row heights from sheet XML for chart positioning
2626
+ */
2627
+ _extractSheetDimensions(sheetData) {
2628
+ const columnWidths = new Map();
2629
+ const rowHeights = new Map();
2630
+ // Extract column widths from cols/col elements
2631
+ const cols = get(sheetData, "worksheet.cols.col") ||
2632
+ get(sheetData, "worksheet.cols") ||
2633
+ get(sheetData, "x:worksheet.x:cols.x:col") ||
2634
+ [];
2635
+ const colsArray = Array.isArray(cols) ? cols : cols?.col ? [cols.col] : [];
2636
+ for (const col of colsArray) {
2637
+ const attrs = col.a$ || col;
2638
+ if (!attrs)
2639
+ continue;
2640
+ const min = parseInt(attrs.min ?? "0", 10);
2641
+ const max = parseInt(attrs.max ?? attrs.min ?? "0", 10);
2642
+ const width = attrs.width !== undefined
2643
+ ? excelColumnWidthToPixels(parseFloat(attrs.width))
2644
+ : undefined;
2645
+ if (width !== undefined) {
2646
+ for (let i = min; i <= max; i++) {
2647
+ columnWidths.set(i, width);
2648
+ }
2649
+ }
2650
+ }
2651
+ // Extract row heights from sheetData/row elements
2652
+ const rows = get(sheetData, "worksheet.sheetData.row") ||
2653
+ get(sheetData, "x:worksheet.x:sheetData.x:row") ||
2654
+ [];
2655
+ const rowsArray = Array.isArray(rows) ? rows : rows ? [rows] : [];
2656
+ // Sequential fallback for rows without the optional @r attribute
2657
+ // (position = previous row + 1 per ECMA-376).
2658
+ let lastHeightRowIndex = 0;
2659
+ for (const row of rowsArray) {
2660
+ const attrs = row.a$ || row;
2661
+ if (!attrs)
2662
+ continue;
2663
+ const parsedRowIndex = parseInt(attrs.r ?? "0", 10);
2664
+ const rowIndex = parsedRowIndex > 0 ? parsedRowIndex : lastHeightRowIndex + 1;
2665
+ lastHeightRowIndex = rowIndex;
2666
+ // Check if row is hidden - hidden rows have 0 height for chart calculations
2667
+ const isHidden = attrs.hidden === "1" ||
2668
+ attrs.hidden === "true" ||
2669
+ attrs.hidden === true;
2670
+ if (isHidden) {
2671
+ rowHeights.set(rowIndex, 0);
2672
+ continue;
2673
+ }
2674
+ // Get explicit height if defined
2675
+ const height = attrs.ht ? parseFloat(attrs.ht) * 1.33 : undefined; // Convert points to pixels
2676
+ if (height !== undefined) {
2677
+ rowHeights.set(rowIndex, height);
2678
+ }
2679
+ }
2680
+ // Also check for hidden columns
2681
+ for (const col of colsArray) {
2682
+ const attrs = col.a$ || col;
2683
+ if (!attrs)
2684
+ continue;
2685
+ const min = parseInt(attrs.min ?? "0", 10);
2686
+ const max = parseInt(attrs.max ?? attrs.min ?? "0", 10);
2687
+ const isHidden = attrs.hidden === "1" ||
2688
+ attrs.hidden === "true" ||
2689
+ attrs.hidden === true;
2690
+ if (isHidden) {
2691
+ for (let i = min; i <= max; i++) {
2692
+ columnWidths.set(i, 0);
2693
+ }
2694
+ }
2695
+ }
2696
+ return {
2697
+ columnWidths: columnWidths.size > 0 ? columnWidths : undefined,
2698
+ rowHeights: rowHeights.size > 0 ? rowHeights : undefined,
2699
+ defaultColumnWidth: 64,
2700
+ defaultRowHeight: 20,
2701
+ };
2702
+ }
2703
+ async _getCharts(rid, sheetId, sheetData) {
2704
+ const charts = [];
2705
+ const sheetDimensions = this._extractSheetDimensions(sheetData);
2706
+ const target = this.relations[rid].target;
2707
+ const path = target.startsWith("/xl")
2708
+ ? target.replace(/^\//gi, "")
2709
+ : `xl/${this.relations[rid].target}`;
2710
+ // Build a sheet-name → sheetId map so chart series with cross-sheet
2711
+ // refs (e.g. `'Data for the Chart'!$A$7:$A$26` on a chart that
2712
+ // lives on Sheet1) get pinned to the actual data sheet, not the
2713
+ // host sheet. Without this every series source resolves to the
2714
+ // chart's host sheet — empty data, broken chart.
2715
+ const sheetIdByName = {};
2716
+ for (const sheet of this._getSheets()) {
2717
+ const name = sheet?.a$?.name;
2718
+ const sId = sheet?.a$?.sheetId;
2719
+ if (name && sId !== undefined) {
2720
+ sheetIdByName[String(name)] = Number(sId);
2721
+ }
2722
+ }
2723
+ const resolveSheetId = (sheetName) => sheetName ? sheetIdByName[String(sheetName)] : undefined;
2724
+ // Handle both with and without namespace prefix
2725
+ const drawing = get(sheetData, "worksheet.drawing") ||
2726
+ get(sheetData, "drawing") ||
2727
+ get(sheetData, "x:worksheet.x:drawing");
2728
+ if (!drawing) {
2729
+ return [];
2730
+ }
2731
+ const drawings = Array.isArray(drawing) ? drawing : [drawing];
2732
+ try {
2733
+ // Get the sheet's relationships to find drawing relationship
2734
+ const sheetRelPath = path.replace("worksheets", "worksheets/_rels") + ".rels";
2735
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
2736
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
2737
+ const drawingRelations = get(sheetRels, "Relationships.Relationship");
2738
+ const drawingRelationsArray = Array.isArray(drawingRelations)
2739
+ ? drawingRelations
2740
+ : [drawingRelations];
2741
+ for (const drawingElement of drawings) {
2742
+ // Get the drawing relationship ID
2743
+ const drawingId = get(drawingElement, "a$.r:id") || get(drawingElement, "a$.id");
2744
+ // Find the drawing relationship
2745
+ const drawingRel = drawingRelationsArray.find((rel) => rel.a$.Id === drawingId ||
2746
+ rel.a$.id === drawingId ||
2747
+ rel.a$.ID === drawingId);
2748
+ if (!drawingRel)
2749
+ continue;
2750
+ // Build drawing path
2751
+ let drawingPath = drawingRel.a$.Target;
2752
+ if (drawingPath.startsWith("../")) {
2753
+ drawingPath = `xl/${drawingPath.replace("../", "")}`;
2754
+ }
2755
+ else if (!drawingPath.startsWith("/")) {
2756
+ drawingPath = `xl/worksheets/${drawingPath}`;
2757
+ }
2758
+ else {
2759
+ drawingPath = drawingPath.startsWith("/")
2760
+ ? drawingPath.substring(1)
2761
+ : `xl/${drawingPath}`;
2762
+ }
2763
+ const drawingContent = await this.zipParser.getText(drawingPath);
2764
+ if (!drawingContent)
2765
+ continue;
2766
+ // Parse drawing XML
2767
+ const drawingXml = this.xmlParser.parse(drawingContent);
2768
+ // Get drawing relationships to resolve chart references
2769
+ const drawingDir = drawingPath.substring(0, drawingPath.lastIndexOf("/"));
2770
+ const drawingRelsPath = `${drawingDir}/_rels/${drawingPath
2771
+ .split("/")
2772
+ .pop()}.rels`;
2773
+ const drawingRelsContent = await this.zipParser.getText(drawingRelsPath);
2774
+ if (!drawingRelsContent) {
2775
+ console.log("Drawing relationships file not found:", drawingRelsPath);
2776
+ continue;
2777
+ }
2778
+ const drawingRelsXml = this.xmlParser.parse(drawingRelsContent);
2779
+ const drawingRelsList = get(drawingRelsXml, "Relationships.Relationship") || [];
2780
+ const drawingRelsArray = Array.isArray(drawingRelsList)
2781
+ ? drawingRelsList
2782
+ : [drawingRelsList];
2783
+ // Extract anchors
2784
+ const oneCellAnchors = get(drawingXml, "xdr:wsDr.xdr:oneCellAnchor") ||
2785
+ get(drawingXml, "wsDr.oneCellAnchor") ||
2786
+ [];
2787
+ const twoCellAnchors = get(drawingXml, "xdr:wsDr.xdr:twoCellAnchor") ||
2788
+ get(drawingXml, "wsDr.twoCellAnchor") ||
2789
+ [];
2790
+ const anchors = [
2791
+ ...(Array.isArray(oneCellAnchors)
2792
+ ? oneCellAnchors
2793
+ : [oneCellAnchors]),
2794
+ ...(Array.isArray(twoCellAnchors)
2795
+ ? twoCellAnchors
2796
+ : [twoCellAnchors]),
2797
+ ].filter(Boolean);
2798
+ for (const anchor of anchors) {
2799
+ const chartRef = getChartReferenceFromAnchor(anchor);
2800
+ if (!chartRef) {
2801
+ continue;
2802
+ }
2803
+ const chartRId = get(chartRef, "a$.r:id") ||
2804
+ get(chartRef, "a$.id") ||
2805
+ get(chartRef, "a$.rId") ||
2806
+ get(chartRef, "a$.Id");
2807
+ if (!chartRId) {
2808
+ continue;
2809
+ }
2810
+ // Find the chart file using the relationship ID
2811
+ const chartRel = drawingRelsArray.find((rel) => get(rel, "a$.Id") === chartRId || get(rel, "a$.id") === chartRId);
2812
+ if (chartRel) {
2813
+ const chart = await processChart(chartRel.a$.Target, drawingPath, anchor, sheetId, this.zipParser, this.xmlParser, sheetDimensions, resolveSheetId);
2814
+ if (chart) {
2815
+ charts.push(chart);
2816
+ }
2817
+ }
2818
+ }
2819
+ }
2820
+ return charts;
2821
+ }
2822
+ catch (err) {
2823
+ return charts;
2824
+ }
2825
+ }
2826
+ /**
2827
+ * Get drawing of a sheet from sheet data
2828
+ * @param rid
2829
+ * @param sheetData
2830
+ * @returns
2831
+ */
2832
+ async _getDrawings(rid, sheetId, sheetData) {
2833
+ const images = [];
2834
+ const target = this.relations[rid].target;
2835
+ const path = target.startsWith("/xl")
2836
+ ? target.replace(/^\//gi, "")
2837
+ : `xl/${this.relations[rid].target}`;
2838
+ // Handle both with and without namespace prefix
2839
+ const drawing = get(sheetData, "worksheet.drawing") ||
2840
+ get(sheetData, "drawing") ||
2841
+ get(sheetData, "x:worksheet.x:drawing");
2842
+ if (!drawing) {
2843
+ return [];
2844
+ }
2845
+ const drawings = Array.isArray(drawing) ? drawing : [drawing];
2846
+ // Get the sheet's relationships to find drawing relationship
2847
+ // Modified to handle both patterns of relationship files
2848
+ const sheetRelPath = path.replace("worksheets", "worksheets/_rels") + ".rels";
2849
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
2850
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
2851
+ const drawingRelations = get(sheetRels, "Relationships.Relationship");
2852
+ const drawingRelationsArray = Array.isArray(drawingRelations)
2853
+ ? drawingRelations
2854
+ : [drawingRelations];
2855
+ for (const drawingElement of drawings) {
2856
+ // Get the drawing relationship ID
2857
+ const drawingId = get(drawingElement, "a$.r:id") || get(drawingElement, "a$.id");
2858
+ // Find the drawing relationship
2859
+ // Consider both Id and Target properties, and handle different attribute naming patterns
2860
+ const drawingRels = drawingRelationsArray.filter((rel) => rel.a$.Id === drawingId ||
2861
+ rel.a$.id === drawingId ||
2862
+ rel.a$.ID === drawingId ||
2863
+ (rel.a$.Target && rel.a$.Target.includes("drawing")));
2864
+ // 2. For each drawing, get the drawing file and its relationships
2865
+ for (const drawingRel of drawingRels) {
2866
+ let drawingPath = drawingRel.a$.Target;
2867
+ if (drawingPath.startsWith("../")) {
2868
+ // We're in xl/worksheets/, so '../' takes us back to xl/
2869
+ drawingPath = `xl/${drawingPath.replace("../", "")}`;
2870
+ }
2871
+ else if (!drawingPath.startsWith("/")) {
2872
+ // If it's a relative path without '../', it's relative to the worksheets folder
2873
+ drawingPath = `xl/worksheets/${drawingPath}`;
2874
+ }
2875
+ else {
2876
+ // If it starts with '/', remove the leading slash
2877
+ drawingPath = drawingPath.startsWith("/")
2878
+ ? drawingPath.substring(1)
2879
+ : `xl/${drawingPath}`;
2880
+ }
2881
+ const drawingContent = await this.zipParser.getText(drawingPath);
2882
+ // Parse drawing XML
2883
+ const drawing = this.xmlParser.parse(drawingContent ?? "");
2884
+ // Extract image anchors - both absolute and cell anchors
2885
+ // Handle different XML namespaces that might be used
2886
+ const twoCellAnchors = get(drawing, "xdr:wsDr.xdr:twoCellAnchor") ||
2887
+ get(drawing, "wsDr.twoCellAnchor");
2888
+ const oneCellAnchors = get(drawing, "xdr:wsDr.xdr:oneCellAnchor") ||
2889
+ get(drawing, "wsDr.oneCellAnchor");
2890
+ const absoluteAnchors = get(drawing, "xdr:wsDr.xdr:absoluteAnchor") ||
2891
+ get(drawing, "wsDr.absoluteAnchor");
2892
+ // One cell
2893
+ for (const image of await processAnchors(oneCellAnchors, "oneCellAnchor", drawingPath, sheetId, this.zipParser, this.xmlParser)) {
2894
+ images.push(image);
2895
+ }
2896
+ // Two cell
2897
+ for (const image of await processAnchors(twoCellAnchors, "twoCellAnchors", drawingPath, sheetId, this.zipParser, this.xmlParser)) {
2898
+ images.push(image);
2899
+ }
2900
+ // absoluteAnchors
2901
+ for (const image of await processAnchors(absoluteAnchors, "absoluteAnchors", drawingPath, sheetId, this.zipParser, this.xmlParser)) {
2902
+ images.push(image);
2903
+ }
2904
+ }
2905
+ }
2906
+ return images;
2907
+ }
2908
+ async getSheetData(rid) {
2909
+ if (this.lastSheetDataCache?.rid === rid) {
2910
+ return this.lastSheetDataCache.data;
2911
+ }
2912
+ const target = this.relations[rid].target;
2913
+ const path = target.startsWith("/xl")
2914
+ ? target.replace(/^\//gi, "")
2915
+ : `xl/${this.relations[rid].target}`;
2916
+ const data = await this.zipParser.getText(path);
2917
+ const sheet_data = this.xmlParser.parse(data || "");
2918
+ this.lastSheetDataCache = { rid, data: sheet_data };
2919
+ return sheet_data;
2920
+ }
2921
+ getStyles() {
2922
+ return this.workbookStyles?.styles;
2923
+ }
2924
+ getTheme() {
2925
+ return this.theme ?? defaultSpreadsheetTheme;
2926
+ }
2927
+ /**
2928
+ * The workbook default font — styles.xml fonts[0], the font every cell
2929
+ * without an explicit font inherits. Exporters pass this through so the
2930
+ * output workbook keeps the source default instead of a hardcoded one.
2931
+ */
2932
+ getDefaultFont() {
2933
+ const font = this.workbookStyles?.fonts?.[0];
2934
+ if (!font || !this.workbookStyles)
2935
+ return null;
2936
+ return fontNodeToTextFormat(font, this.workbookStyles);
2937
+ }
2938
+ /**
2939
+ * Get shared strings
2940
+ * @returns
2941
+ */
2942
+ getSharedStrings() {
2943
+ return this.sharedStrings ?? [];
2944
+ }
2945
+ async getRelations() {
2946
+ const relData = await this.zipParser.getText("xl/_rels/workbook.xml.rels");
2947
+ return getRelationsById(this.xmlParser.parse(relData));
2948
+ }
2949
+ _getSheets() {
2950
+ return this.workSheets;
2951
+ }
2952
+ _getNamedRanges() {
2953
+ const xml = this.workbookXml;
2954
+ const sheetIdBySheetName = this._getSheets().reduce((acc, element) => {
2955
+ if (element) {
2956
+ const name = element.a$?.name;
2957
+ const sheetId = Number(element.a$.sheetId);
2958
+ acc[name] = sheetId;
2959
+ }
2960
+ return acc;
2961
+ }, {});
2962
+ const namedRanges = [];
2963
+ // Handle both with and without namespace prefix
2964
+ const definedName = get(xml, "workbook.definedNames.definedName") ||
2965
+ get(xml, "x:workbook.x:definedNames.x:definedName");
2966
+ if (definedName) {
2967
+ const defined_names = Array.isArray(definedName)
2968
+ ? definedName
2969
+ : [definedName];
2970
+ for (const defined_name of defined_names) {
2971
+ const name = defined_name.a$?.name;
2972
+ const expression = defined_name.t$ || "";
2973
+ const hidden = defined_name.a$.hidden === "1";
2974
+ // Empty defined names are noise; nothing useful to round-trip.
2975
+ // Hidden defined names ARE preserved — Excel uses them as scratch
2976
+ // names for features like print areas and add-ins, and dropping
2977
+ // them silently breaks those features on a round-trip.
2978
+ if (!expression) {
2979
+ continue;
2980
+ }
2981
+ const isBrokenRef = typeof expression === "string" && expression.includes("#REF!");
2982
+ const isRef = !isBrokenRef && checkIfStringIsReference(expression);
2983
+ if (isRef) {
2984
+ // Cater to string, number, formula and boolean values as named range
2985
+ const selections = generateSelectionsFromFormula(expression);
2986
+ let pushedRange = false;
2987
+ if (selections.length) {
2988
+ for (const selection of selections) {
2989
+ const sheetId = selection.sheetName
2990
+ ? sheetIdBySheetName[selection.sheetName]
2991
+ : undefined;
2992
+ if (!isNil(sheetId)) {
2993
+ namedRanges.push({
2994
+ namedRangeId: uuid(),
2995
+ name,
2996
+ range: {
2997
+ ...selection.range,
2998
+ sheetId,
2999
+ },
3000
+ ...(hidden ? { hidden: true } : {}),
3001
+ });
3002
+ pushedRange = true;
3003
+ }
3004
+ }
3005
+ }
3006
+ // Reference parsed but no selection bound to a known sheet — most
3007
+ // commonly an external-workbook reference like `'[1]Revenue'!$A$17`
3008
+ // where the `[1]` indexes a linked workbook we can't resolve.
3009
+ // Preserve the original expression as value-only so the name
3010
+ // survives the round-trip.
3011
+ if (!pushedRange) {
3012
+ namedRanges.push({
3013
+ name,
3014
+ namedRangeId: uuid(),
3015
+ value: isAFormula(expression) ? `=${expression}` : expression,
3016
+ ...(hidden ? { hidden: true } : {}),
3017
+ });
3018
+ }
3019
+ }
3020
+ else {
3021
+ // Broken ref, formula, or non-reference value. OOXML stores
3022
+ // defined-name expressions WITHOUT the leading "=", so
3023
+ // formula-shaped expressions (e.g. `OFFSET(...)`) get the
3024
+ // "=" prefix the calculator expects; static literals and
3025
+ // broken refs are preserved verbatim.
3026
+ namedRanges.push({
3027
+ name,
3028
+ namedRangeId: uuid(),
3029
+ value: isAFormula(expression) || isBareFormulaExpression(expression)
3030
+ ? `=${expression}`
3031
+ : expression,
3032
+ ...(hidden ? { hidden: true } : {}),
3033
+ });
3034
+ }
3035
+ }
3036
+ }
3037
+ return namedRanges;
3038
+ }
3039
+ /**
3040
+ * Returns true if the workbook signals that Excel should perform a full
3041
+ * recalculation when opened (fullCalcOnLoad/forceFullCalc on calcPr).
3042
+ */
3043
+ requiresRecalc() {
3044
+ const attrs = this.calcPr?.a$;
3045
+ return attrs?.fullCalcOnLoad === "1" || attrs?.forceFullCalc === "1";
3046
+ }
3047
+ /**
3048
+ * `<workbookPr date1904="1"/>` shifts every date serial by 1462 days
3049
+ * relative to the default 1900 epoch. If the workbook isn't explicitly
3050
+ * 1904-anchored we treat it as 1900.
3051
+ */
3052
+ getUse1904Epoch() {
3053
+ const v = this.workbookPr?.a$?.date1904;
3054
+ return v === "1" || v === "true" || v === true;
3055
+ }
3056
+ getIterativeCalculationOptions() {
3057
+ const attrs = this.calcPr?.a$;
3058
+ if (!attrs)
3059
+ return undefined;
3060
+ const enabledAttr = attrs.iterate;
3061
+ const maxIterationsAttr = attrs.iterateCount;
3062
+ const maxChangeAttr = attrs.iterateDelta;
3063
+ if (enabledAttr === undefined &&
3064
+ maxIterationsAttr === undefined &&
3065
+ maxChangeAttr === undefined) {
3066
+ return undefined;
3067
+ }
3068
+ const enabled = enabledAttr === "1" || enabledAttr === "true" || enabledAttr === true;
3069
+ const options = { enabled };
3070
+ if (maxIterationsAttr !== undefined) {
3071
+ const parsed = Number(maxIterationsAttr);
3072
+ if (!Number.isNaN(parsed)) {
3073
+ options.maxIterations = parsed;
3074
+ }
3075
+ }
3076
+ if (maxChangeAttr !== undefined) {
3077
+ const parsed = Number(maxChangeAttr);
3078
+ if (!Number.isNaN(parsed)) {
3079
+ options.maxChange = parsed;
3080
+ }
3081
+ }
3082
+ return options;
3083
+ }
3084
+ getSheetCount() {
3085
+ return this.workSheets.length;
3086
+ }
3087
+ // INTERFACE IMPLEMENTATION METHODS
3088
+ async getSheets(minRowCount = 0, minColumnCount = 0) {
3089
+ const workSheets = this._getSheets();
3090
+ const sheets = [];
3091
+ const tables = [];
3092
+ for (const element of workSheets) {
3093
+ const built = await this.buildSheetFromElement(element, minRowCount, minColumnCount);
3094
+ if (!built) {
3095
+ continue;
3096
+ }
3097
+ sheets.push(built.sheet);
3098
+ tables.push(...built.tables);
3099
+ // The eager all-sheet scan must not retain every sheet's metadata at
3100
+ // once; the row stream re-populates the entry when it needs it.
3101
+ this.sheetMetadataCache.delete(element.a$?.["r:id"]);
3102
+ }
3103
+ return [sheets, tables];
3104
+ }
3105
+ getSheetCatalog() {
3106
+ const catalog = [];
3107
+ this._getSheets().forEach((element, index) => {
3108
+ const name = element?.a$?.name;
3109
+ if (!name) {
3110
+ return;
3111
+ }
3112
+ const state = element.a$.state;
3113
+ const visibility = state === "veryHidden"
3114
+ ? "veryHidden"
3115
+ : state === "hidden"
3116
+ ? "hidden"
3117
+ : "visible";
3118
+ catalog.push({
3119
+ sheetId: Number(element.a$.sheetId),
3120
+ title: name,
3121
+ visibility,
3122
+ hidden: visibility !== "visible",
3123
+ index,
3124
+ });
3125
+ });
3126
+ return catalog;
3127
+ }
3128
+ async getSheet(sheetId, minRowCount = 0, minColumnCount = 0) {
3129
+ const element = this.worksheetBySheetId.get(sheetId);
3130
+ if (!element) {
3131
+ throw new Error(`Unknown sheetId: ${sheetId}`);
3132
+ }
3133
+ const built = await this.buildSheetFromElement(element, minRowCount, minColumnCount);
3134
+ if (!built) {
3135
+ throw new Error(`Unknown sheetId: ${sheetId}`);
3136
+ }
3137
+ // Metadata stays cached so a following per-sheet row stream reuses this
3138
+ // scan; `releaseSheetCaches(sheetId)` drops it when the sheet is done.
3139
+ return [built.sheet, built.tables];
3140
+ }
3141
+ async buildSheetFromElement(element, minRowCount = 0, minColumnCount = 0) {
3142
+ const name = element.a$?.name;
3143
+ if (name) {
3144
+ const state = element.a$.state;
3145
+ const sheetId = Number(element.a$.sheetId);
3146
+ const rid = element.a$["r:id"];
3147
+ const visibility = state === "veryHidden"
3148
+ ? "veryHidden"
3149
+ : state === "hidden"
3150
+ ? "hidden"
3151
+ : "visible";
3152
+ const hidden = visibility !== "visible";
3153
+ const sheet = {
3154
+ sheetId,
3155
+ title: name,
3156
+ hidden,
3157
+ rowMetadata: [],
3158
+ columnMetadata: [],
3159
+ merges: [],
3160
+ };
3161
+ if (visibility !== "visible") {
3162
+ sheet.visibility = visibility;
3163
+ }
3164
+ const metadata = await this.getSheetMetadata(rid);
3165
+ const { merges, rowEntries, columnMetadata, maxRowIndex, maxColumnIndex, autoFilterRange, filterSpecs, sheetViewData, tabColor, sheetHasFilterMode, zeroHeight, defaultRowHeightPx, defaultColumnWidthPx, tablePartIds, outlineSummaryBelow, outlineSummaryRight, } = metadata;
3166
+ if (defaultRowHeightPx !== undefined) {
3167
+ sheet.defaultRowHeight = defaultRowHeightPx;
3168
+ }
3169
+ if (defaultColumnWidthPx !== undefined) {
3170
+ sheet.defaultColumnWidth = defaultColumnWidthPx;
3171
+ }
3172
+ // Only persist `outlinePr` when the workbook diverges from the
3173
+ // Excel defaults (summaryBelow=true, summaryRight=true). Avoids
3174
+ // bloating the model for every imported sheet.
3175
+ if (outlineSummaryBelow === false || outlineSummaryRight === false) {
3176
+ sheet.outlinePr = {
3177
+ ...(outlineSummaryBelow !== undefined && {
3178
+ summaryBelow: outlineSummaryBelow,
3179
+ }),
3180
+ ...(outlineSummaryRight !== undefined && {
3181
+ summaryRight: outlineSummaryRight,
3182
+ }),
3183
+ };
3184
+ }
3185
+ const rowCount = findMax([minRowCount, maxRowIndex || 0]);
3186
+ // Tables
3187
+ const sheetTables = await this.getTablesFromSheet(sheetId, rid, tablePartIds);
3188
+ // Create an efficient O(1) lookup map for table ranges
3189
+ const rowInTableMap = {};
3190
+ // Populate the lookup map
3191
+ sheetTables.forEach((table) => {
3192
+ const range = table.range;
3193
+ if (range) {
3194
+ for (let i = range.startRowIndex; i <= range.endRowIndex; i++) {
3195
+ rowInTableMap[i] = true;
3196
+ }
3197
+ }
3198
+ });
3199
+ // Check if hidden by filter vs. hidden by user
3200
+ // Excel uses the 'filterMode' attribute at the sheet level and 'dynamicFilter' or 'customFilter'
3201
+ // at the column filter level, but the rows hidden by filter are marked with hidden="1" just like
3202
+ // manually hidden rows. The difference is in the context of the filter settings.
3203
+ // counts
3204
+ let columnCount = 0;
3205
+ // gridlines / headers / zeros / zoom
3206
+ const { frozenRowCount, frozenColumnCount, paneTopLeftCell, showGridLines, showRowColHeaders, showZeros, zoomScale, } = extractSheetViewProperties(sheetViewData);
3207
+ sheet.frozenRowCount = frozenRowCount;
3208
+ sheet.frozenColumnCount = frozenColumnCount;
3209
+ if (paneTopLeftCell) {
3210
+ sheet.paneTopLeftCell = paneTopLeftCell;
3211
+ }
3212
+ sheet.showGridLines = showGridLines;
3213
+ // Persist only when not at Excel's defaults so we don't pollute
3214
+ // round-trips for sheets that never customised these.
3215
+ if (!showRowColHeaders)
3216
+ sheet.showRowColHeaders = false;
3217
+ if (!showZeros)
3218
+ sheet.showZeros = false;
3219
+ if (zoomScale !== undefined && zoomScale !== 100) {
3220
+ sheet.zoomScale = zoomScale;
3221
+ }
3222
+ // tab color
3223
+ if (tabColor) {
3224
+ sheet.tabColor = extractColor(tabColor, this.workbookStyles.indexedColors);
3225
+ }
3226
+ // Auto filter
3227
+ if (autoFilterRange) {
3228
+ sheet.basicFilter = {
3229
+ id: uuid(),
3230
+ range: autoFilterRange,
3231
+ filterSpecs,
3232
+ };
3233
+ }
3234
+ // Add merges
3235
+ for (const range of merges) {
3236
+ sheet.merges.push(range);
3237
+ }
3238
+ columnCount = findMax([maxColumnIndex, minColumnCount]);
3239
+ rowEntries.forEach((entry, rowIndex) => {
3240
+ const customHeight = entry.custom === true;
3241
+ // If zeroHeight is set and the row has no custom height *and*
3242
+ // no explicit `ht` value, the row inherits the sheet-level
3243
+ // zero height (collapsed). Rows that carry an explicit `ht`
3244
+ // should keep their height even when the sheet has
3245
+ // `zeroHeight="1"` — Excel treats explicit `ht` as an
3246
+ // override regardless of whether `customHeight="1"` is set.
3247
+ const hasExplicitSize = entry.size != null;
3248
+ const size = zeroHeight && !customHeight && !hasExplicitSize ? 0 : entry.size;
3249
+ const isResized = !isNil(size);
3250
+ const isHidden = entry.hidden === true;
3251
+ const isInFilteredRange = autoFilterRange &&
3252
+ rowIndex >= autoFilterRange.startRowIndex &&
3253
+ rowIndex <= autoFilterRange.endRowIndex;
3254
+ const isInTableRange = !!rowInTableMap[rowIndex];
3255
+ const isHiddenByFilter = isHidden &&
3256
+ ((sheetHasFilterMode && isInFilteredRange) || isInTableRange);
3257
+ const isHiddenByUser = isHidden && !isHiddenByFilter;
3258
+ const outlineLevel = entry.outlineLevel ?? 0;
3259
+ const isCollapsed = entry.collapsed === true;
3260
+ if (!isHidden &&
3261
+ !size &&
3262
+ !isResized &&
3263
+ outlineLevel === 0 &&
3264
+ !isCollapsed) {
3265
+ return;
3266
+ }
3267
+ const dimensionProps = {};
3268
+ // `hidden` on a row inside an outline group is the
3269
+ // "collapsed-parent" hide — keep it on hiddenByGroup so
3270
+ // expanding the group doesn't fight any user/filter hide.
3271
+ if (isHiddenByUser) {
3272
+ if (outlineLevel > 0) {
3273
+ dimensionProps.hiddenByGroup = true;
3274
+ }
3275
+ else {
3276
+ dimensionProps.hiddenByUser = true;
3277
+ }
3278
+ }
3279
+ if (isHiddenByFilter)
3280
+ dimensionProps.hiddenByFilter = true;
3281
+ if (!isNil(size)) {
3282
+ if (customHeight) {
3283
+ dimensionProps.resizedByUser = true;
3284
+ // Keep an immutable record of the source-side `customHeight="1"`
3285
+ // marker so the exporter can emit it back even when the height
3286
+ // equals the sheet default. `resizedByUser` is shared with
3287
+ // runtime engine state and isn't safe as the export gate.
3288
+ dimensionProps.customHeight = true;
3289
+ }
3290
+ dimensionProps.size = size;
3291
+ }
3292
+ if (outlineLevel > 0)
3293
+ dimensionProps.outlineLevel = outlineLevel;
3294
+ if (isCollapsed)
3295
+ dimensionProps.collapsed = true;
3296
+ sheet.rowMetadata[rowIndex] = dimensionProps;
3297
+ });
3298
+ sheet.columnMetadata = [];
3299
+ Object.entries(columnMetadata).forEach(([key, value]) => {
3300
+ sheet.columnMetadata[Number(key)] = value;
3301
+ });
3302
+ // Get drawing bounds to extend sheet dimensions if charts/images extend beyond cell data
3303
+ const drawingBounds = await this.getDrawingBounds(rid);
3304
+ // Update counts, extending if drawings go beyond cell data
3305
+ const finalRowCount = findMax([rowCount, drawingBounds.maxRowIndex]);
3306
+ const finalColumnCount = findMax([
3307
+ columnCount,
3308
+ drawingBounds.maxColumnIndex,
3309
+ ]);
3310
+ if (finalRowCount)
3311
+ sheet.rowCount = finalRowCount;
3312
+ if (finalColumnCount)
3313
+ sheet.columnCount = finalColumnCount;
3314
+ return { sheet, tables: sheetTables };
3315
+ }
3316
+ return undefined;
3317
+ }
3318
+ async *processSheetData(chunkSize = 100, enableCellXfsRegistry = false, _enableSharedStrings = true, onPhase) {
3319
+ const workSheets = this._getSheets();
3320
+ const totalSheets = workSheets.length;
3321
+ let currentSheetIndex = 0;
3322
+ // Process each worksheet
3323
+ for (const element of workSheets) {
3324
+ yield* this.processOneSheet(element, {
3325
+ chunkSize,
3326
+ enableCellXfsRegistry,
3327
+ enableSharedStrings: _enableSharedStrings,
3328
+ onPhase,
3329
+ currentSheetIndex,
3330
+ totalSheets,
3331
+ });
3332
+ // Per-sheet transients must not accumulate across the eager pass.
3333
+ this.releaseSheetCaches(Number(element.a$?.sheetId));
3334
+ currentSheetIndex++;
3335
+ }
3336
+ }
3337
+ async *processSheetDataForSheet(options) {
3338
+ const element = this.worksheetBySheetId.get(options.sheetId);
3339
+ if (!element) {
3340
+ throw new Error(`Unknown sheetId: ${options.sheetId}`);
3341
+ }
3342
+ yield* this.processOneSheet(element, {
3343
+ chunkSize: options.chunkSize ?? 100,
3344
+ enableCellXfsRegistry: options.enableCellXfsRegistry ?? false,
3345
+ enableSharedStrings: options.enableSharedStrings ?? true,
3346
+ onPhase: options.onPhase,
3347
+ currentSheetIndex: 0,
3348
+ totalSheets: 1,
3349
+ });
3350
+ }
3351
+ releaseSheetCaches(sheetId) {
3352
+ const element = this.worksheetBySheetId.get(sheetId);
3353
+ const rid = element?.a$?.["r:id"];
3354
+ if (!rid) {
3355
+ return;
3356
+ }
3357
+ this.sheetMetadataCache.delete(rid);
3358
+ this.sheetCommentsCache.delete(rid);
3359
+ this.sheetThreadedCommentsCache.delete(rid);
3360
+ if (this.lastSheetDataCache?.rid === rid) {
3361
+ this.lastSheetDataCache = null;
3362
+ }
3363
+ }
3364
+ async *processOneSheet(element, { chunkSize, enableCellXfsRegistry, enableSharedStrings: _enableSharedStrings, onPhase, currentSheetIndex, totalSheets, }) {
3365
+ // Shared formulas (`si` ids) are sheet-local in OOXML; reset per sheet
3366
+ // so one sheet's leaders never leak into another sheet's followers.
3367
+ this.clearSharedFormula();
3368
+ {
3369
+ const name = element.a$?.name;
3370
+ const sheetId = Number(element.a$.sheetId);
3371
+ const rid = element.a$["r:id"];
3372
+ this.currentSheetTotalRows = undefined;
3373
+ // The per-sheet scans below can be long and silent on large sheets;
3374
+ // report phases so the host's inactivity watchdog sees liveness.
3375
+ onPhase?.("sheet-metadata", name);
3376
+ const metadata = await this.getSheetMetadata(rid);
3377
+ // Row/column default styles are materialized as phantom format-only
3378
+ // cells (see STYLE_MATERIALIZE_* above). Width is clamped to the
3379
+ // sheet's used extent so trailing catch-all `<col>` ranges don't
3380
+ // paint 16384 columns per row.
3381
+ const materializeMaxColumn = Math.min(metadata.maxColumnIndex, STYLE_MATERIALIZE_MAX_COLUMNS);
3382
+ const colStyleByColumn = new Map();
3383
+ for (const { min, max, styleIndex } of metadata.colStyleRanges) {
3384
+ const clampedMax = Math.min(max, materializeMaxColumn);
3385
+ for (let col = Math.max(min, 1); col <= clampedMax; col++) {
3386
+ colStyleByColumn.set(col, styleIndex);
3387
+ }
3388
+ }
3389
+ const styleCellTemplates = new Map();
3390
+ const getTemplateForStyleIndex = (styleIndex) => {
3391
+ let template = styleCellTemplates.get(styleIndex);
3392
+ if (template === undefined) {
3393
+ template = getCellDataForStyleIndex(styleIndex, this, enableCellXfsRegistry);
3394
+ styleCellTemplates.set(styleIndex, template);
3395
+ }
3396
+ return template;
3397
+ };
3398
+ let styleMaterializeBudget = STYLE_MATERIALIZE_CELL_BUDGET;
3399
+ const mergeRangesByRow = this.buildMergeRangesByRow(metadata.merges);
3400
+ const getMergeRangeForCell = (rowIndex, columnIndex) => {
3401
+ const ranges = mergeRangesByRow.get(rowIndex);
3402
+ if (!ranges)
3403
+ return undefined;
3404
+ for (const range of ranges) {
3405
+ if (columnIndex >= range.startColumnIndex &&
3406
+ columnIndex <= range.endColumnIndex) {
3407
+ return range;
3408
+ }
3409
+ }
3410
+ return undefined;
3411
+ };
3412
+ const sheetComments = await this.getCommentsForSheet(rid);
3413
+ const sheetThreadedComments = await this.getThreadedCommentsForSheet(rid);
3414
+ const commentsByRow = new Map();
3415
+ const threadsByRow = new Map();
3416
+ onPhase?.("sheet-hyperlinks", name);
3417
+ const hyperlinksByRow = await this.getHyperlinksForSheet(rid);
3418
+ onPhase?.("sheet-sparklines", name);
3419
+ const sparklinesByRow = await this.getSparklinesForSheet(rid, name);
3420
+ // Data-validation refs are stamped onto covered cells while rows stream
3421
+ // (see dv-cell-stamp.ts) — hosts resolve a cell's rule ONLY through
3422
+ // `cellData.dataValidation`, so consumers that persist these rows
3423
+ // directly (server-side conversion) need the stamp baked in. Gated on
3424
+ // the SAX feature flags: a sheet with no <dataValidations> and no
3425
+ // extLst can't contribute rules, so DV-free sheets skip the (memoized)
3426
+ // full-DOM validation parse entirely.
3427
+ const dvFeatures = this.sheetFeatures.get(rid);
3428
+ const dvStamper = dvFeatures && !dvFeatures.dataValidations && !dvFeatures.extLst
3429
+ ? null
3430
+ : new SheetDataValidationStamper(await this.getDataValidationsForSheet(sheetId), sheetId);
3431
+ onPhase?.("sheet-rows", name);
3432
+ // Create comments by row. A cell whose comment is the legacy `tc=`
3433
+ // shadow of an imported thread must NOT also get a plain note —
3434
+ // the thread (stamped below via `commentThreadId`) supersedes it.
3435
+ Object.entries(sheetComments).forEach(([ref, text]) => {
3436
+ if (sheetThreadedComments.threadIdByAddress.has(ref)) {
3437
+ return;
3438
+ }
3439
+ const cellPosition = addressToCell(ref);
3440
+ if (!cellPosition) {
3441
+ return;
3442
+ }
3443
+ const rowMap = commentsByRow.get(cellPosition.rowIndex) ?? new Map();
3444
+ rowMap.set(cellPosition.columnIndex, text);
3445
+ commentsByRow.set(cellPosition.rowIndex, rowMap);
3446
+ });
3447
+ // Thread anchors by row, mirroring commentsByRow.
3448
+ sheetThreadedComments.threadIdByAddress.forEach((threadId, ref) => {
3449
+ const cellPosition = addressToCell(ref);
3450
+ if (!cellPosition) {
3451
+ return;
3452
+ }
3453
+ const rowMap = threadsByRow.get(cellPosition.rowIndex) ?? new Map();
3454
+ rowMap.set(cellPosition.columnIndex, threadId);
3455
+ threadsByRow.set(cellPosition.rowIndex, rowMap);
3456
+ });
3457
+ // Constants
3458
+ const CHUNK_SIZE = chunkSize;
3459
+ let chunkIndex = 0;
3460
+ let totalRowsProcessed = 0;
3461
+ let totalRowsKnown = this.currentSheetTotalRows;
3462
+ // ECMA-376 makes row/@r optional (streaming writers omit it); when
3463
+ // absent, a row's position is the previous row's + 1. Track it so
3464
+ // r-less rows don't all land on index NaN and render an empty sheet.
3465
+ let lastRowIndex = 0;
3466
+ let currentChunk = [];
3467
+ let currentChunkRowIndices = [];
3468
+ let startRowIndex = -1;
3469
+ let endRowIndex = -1;
3470
+ const flushChunk = (isLastChunk) => {
3471
+ if (!currentChunk.length) {
3472
+ return;
3473
+ }
3474
+ const result = {
3475
+ sheetId,
3476
+ title: name,
3477
+ currentSheetIndex,
3478
+ totalSheets,
3479
+ chunkIndex,
3480
+ rows: currentChunk,
3481
+ rowIndices: currentChunkRowIndices,
3482
+ startRowIndex,
3483
+ endRowIndex,
3484
+ totalRows: totalRowsKnown ?? (isLastChunk ? totalRowsProcessed : -1),
3485
+ isLastChunk,
3486
+ cellXfs: this.cellXfs,
3487
+ };
3488
+ currentChunk = [];
3489
+ currentChunkRowIndices = [];
3490
+ startRowIndex = -1;
3491
+ endRowIndex = -1;
3492
+ chunkIndex++;
3493
+ return result;
3494
+ };
3495
+ for await (const row of this.streamSheetRows(rid)) {
3496
+ const parsedRowIndex = Number(row.a$?.r);
3497
+ const rowIndex = Number.isFinite(parsedRowIndex) && parsedRowIndex > 0
3498
+ ? parsedRowIndex
3499
+ : lastRowIndex + 1;
3500
+ lastRowIndex = rowIndex;
3501
+ totalRowsProcessed += 1;
3502
+ if (totalRowsKnown === undefined && this.currentSheetTotalRows) {
3503
+ totalRowsKnown = this.currentSheetTotalRows;
3504
+ }
3505
+ if (currentChunk.length === 0) {
3506
+ startRowIndex = rowIndex;
3507
+ }
3508
+ const rowData = {
3509
+ values: [],
3510
+ };
3511
+ // Row default style: `<row customFormat="1" s="N">` — every cell
3512
+ // of the row without its own `s` inherits xf N (Excel paints the
3513
+ // whole row with it, e.g. full-width banner rows). Takes
3514
+ // precedence over any column default style.
3515
+ const rowAttrs = row.a$ ?? {};
3516
+ const hasRowCustomFormat = rowAttrs.customFormat === "1" || rowAttrs.customFormat === "true";
3517
+ const parsedRowStyle = hasRowCustomFormat && rowAttrs.s !== undefined
3518
+ ? parseInt(rowAttrs.s, 10)
3519
+ : NaN;
3520
+ const rowStyleIndex = Number.isFinite(parsedRowStyle) && parsedRowStyle > 0
3521
+ ? parsedRowStyle
3522
+ : undefined;
3523
+ const getInheritedStyleIndex = (columnIndex) => rowStyleIndex ?? colStyleByColumn.get(columnIndex);
3524
+ const cells = row.c ?? row["x:c"];
3525
+ const rowComments = commentsByRow.get(rowIndex);
3526
+ const rowThreads = threadsByRow.get(rowIndex);
3527
+ const rowHyperlinks = hyperlinksByRow.get(rowIndex);
3528
+ const rowCellsByColumnIndex = new Map();
3529
+ if (cells) {
3530
+ const cellArray = Array.isArray(cells) ? cells : [cells];
3531
+ // First pass: build map of cells by column index
3532
+ // Track sequential index for cells without explicit reference (1-based)
3533
+ let seqColIndex = 1;
3534
+ for (const cell of cellArray) {
3535
+ if (!cell || !cell.a$) {
3536
+ seqColIndex++;
3537
+ continue;
3538
+ }
3539
+ const cellRef = cell.a$?.r;
3540
+ let columnIndex;
3541
+ if (cellRef) {
3542
+ const colStr = cellRef.replace(/[0-9]/g, "");
3543
+ columnIndex = alpha2number(colStr);
3544
+ seqColIndex = columnIndex + 1;
3545
+ }
3546
+ else {
3547
+ columnIndex = seqColIndex;
3548
+ seqColIndex++;
3549
+ }
3550
+ // A `<c>` without `s` inherits the row/column default style.
3551
+ if (cell.a$.s === undefined) {
3552
+ const inherited = getInheritedStyleIndex(columnIndex);
3553
+ if (inherited !== undefined) {
3554
+ cell.a$.s = String(inherited);
3555
+ }
3556
+ }
3557
+ rowCellsByColumnIndex.set(columnIndex, cell);
3558
+ }
3559
+ // Second pass: process each cell
3560
+ seqColIndex = 1;
3561
+ for (let k = 0; k < cellArray.length; k++) {
3562
+ const cell = cellArray[k];
3563
+ if (!cell || !cell.a$) {
3564
+ seqColIndex++;
3565
+ continue;
3566
+ }
3567
+ const cellRef = cell.a$.r;
3568
+ let columnIndex;
3569
+ if (cellRef) {
3570
+ const colStr = cellRef.replace(/[0-9]/g, "");
3571
+ columnIndex = alpha2number(colStr);
3572
+ seqColIndex = columnIndex + 1;
3573
+ }
3574
+ else {
3575
+ columnIndex = seqColIndex;
3576
+ seqColIndex++;
3577
+ }
3578
+ const mergeRange = getMergeRangeForCell(rowIndex, columnIndex);
3579
+ let cellValue = getCellValueAndFormat(cell, this, enableCellXfsRegistry, _enableSharedStrings, name, mergeRange
3580
+ ? { range: mergeRange, rowCellsByColumnIndex }
3581
+ : undefined);
3582
+ const hyperlink = rowHyperlinks?.get(columnIndex);
3583
+ if (hyperlink) {
3584
+ if (cellValue) {
3585
+ cellValue = { ...cellValue, hyperlink };
3586
+ }
3587
+ else {
3588
+ cellValue = { hyperlink };
3589
+ }
3590
+ }
3591
+ const comment = rowComments?.get(columnIndex);
3592
+ if (comment) {
3593
+ if (cellValue) {
3594
+ cellValue = { ...cellValue, note: comment };
3595
+ }
3596
+ else {
3597
+ cellValue = { note: comment };
3598
+ }
3599
+ }
3600
+ const threadId = rowThreads?.get(columnIndex);
3601
+ if (threadId) {
3602
+ if (cellValue) {
3603
+ cellValue = { ...cellValue, commentThreadId: threadId };
3604
+ }
3605
+ else {
3606
+ cellValue = { commentThreadId: threadId };
3607
+ }
3608
+ }
3609
+ rowData.values[columnIndex] = cellValue;
3610
+ }
3611
+ }
3612
+ // Materialize the row/column default style onto columns that have
3613
+ // no `<c>` element at all — Excel still paints them. Skips columns
3614
+ // whose cell existed in the XML (its explicit style already won).
3615
+ if (rowStyleIndex !== undefined || colStyleByColumn.size > 0) {
3616
+ for (let columnIndex = 1; columnIndex <= materializeMaxColumn && styleMaterializeBudget > 0; columnIndex++) {
3617
+ if (rowCellsByColumnIndex.has(columnIndex))
3618
+ continue;
3619
+ const inherited = getInheritedStyleIndex(columnIndex);
3620
+ if (inherited === undefined)
3621
+ continue;
3622
+ const template = getTemplateForStyleIndex(inherited);
3623
+ if (!template)
3624
+ continue;
3625
+ rowData.values[columnIndex] = template;
3626
+ styleMaterializeBudget--;
3627
+ }
3628
+ }
3629
+ if (rowHyperlinks) {
3630
+ for (const [columnIndex, hyperlink] of rowHyperlinks.entries()) {
3631
+ if (rowData.values[columnIndex]) {
3632
+ if (!rowData.values[columnIndex]?.hyperlink) {
3633
+ rowData.values[columnIndex] = {
3634
+ ...rowData.values[columnIndex],
3635
+ hyperlink,
3636
+ };
3637
+ }
3638
+ }
3639
+ else {
3640
+ rowData.values[columnIndex] = { hyperlink };
3641
+ }
3642
+ }
3643
+ hyperlinksByRow.delete(rowIndex);
3644
+ }
3645
+ if (rowComments) {
3646
+ for (const [columnIndex, text] of rowComments.entries()) {
3647
+ if (rowData.values[columnIndex]) {
3648
+ if (!rowData.values[columnIndex]?.note) {
3649
+ rowData.values[columnIndex] = {
3650
+ ...rowData.values[columnIndex],
3651
+ note: text,
3652
+ };
3653
+ }
3654
+ }
3655
+ else {
3656
+ rowData.values[columnIndex] = { note: text };
3657
+ }
3658
+ }
3659
+ commentsByRow.delete(rowIndex);
3660
+ }
3661
+ if (rowThreads) {
3662
+ for (const [columnIndex, threadId] of rowThreads.entries()) {
3663
+ if (rowData.values[columnIndex]) {
3664
+ if (!rowData.values[columnIndex]?.commentThreadId) {
3665
+ rowData.values[columnIndex] = {
3666
+ ...rowData.values[columnIndex],
3667
+ commentThreadId: threadId,
3668
+ };
3669
+ }
3670
+ }
3671
+ else {
3672
+ rowData.values[columnIndex] = {
3673
+ commentThreadId: threadId,
3674
+ };
3675
+ }
3676
+ }
3677
+ threadsByRow.delete(rowIndex);
3678
+ }
3679
+ // Inject sparkline formulas with effective values
3680
+ // Both sparklinesByRow and rowData.values use 1-indexed column positions
3681
+ const rowSparklines = sparklinesByRow.get(rowIndex);
3682
+ if (rowSparklines) {
3683
+ for (const [columnIndex, sparkline] of rowSparklines.entries()) {
3684
+ const existingCell = rowData.values[columnIndex];
3685
+ // Only set formula if cell doesn't already have one
3686
+ if (existingCell?.ue?.fv) {
3687
+ continue;
3688
+ }
3689
+ // Try to extract data values from the data range for effective value
3690
+ // Skip cross-sheet references (contain "!") - we don't have access to other sheets
3691
+ // When the group carries displayEmptyCellsAs, keep empty
3692
+ // cells as nulls so the renderer can apply gap/zero/span;
3693
+ // otherwise compact them out (legacy shape).
3694
+ const preserveGaps = sparkline.emptyCellsAs != null;
3695
+ const sparklineData = [];
3696
+ if (!sparkline.dataRange.includes("!")) {
3697
+ // Parse range using addressToSelection for robust handling
3698
+ const selection = addressToSelection(sparkline.dataRange);
3699
+ if (selection) {
3700
+ const { startRowIndex, endRowIndex, startColumnIndex, endColumnIndex, } = selection.range;
3701
+ // Only extract data for same-row horizontal ranges (most common case)
3702
+ // rowIndex is 1-indexed, selection indices are also 1-indexed
3703
+ if (startRowIndex === endRowIndex &&
3704
+ startRowIndex === rowIndex) {
3705
+ for (let col = startColumnIndex; col <= endColumnIndex; col++) {
3706
+ const cell = rowData.values[col];
3707
+ const value = cell?.ev?.nv ?? cell?.ue?.nv;
3708
+ if (typeof value === "number" && !isNaN(value)) {
3709
+ sparklineData.push(value);
3710
+ }
3711
+ else if (preserveGaps) {
3712
+ sparklineData.push(null);
3713
+ }
3714
+ }
3715
+ }
3716
+ }
3717
+ }
3718
+ // Build options object for sparkline result. Keys map 1:1
3719
+ // onto `SparkLineResultOptions` so the renderer doesn't
3720
+ // have to translate between Excel's XML attributes and our
3721
+ // formula option names — it reads straight from `options`.
3722
+ const sparklineOptions = {};
3723
+ if (sparkline.chartType !== "line") {
3724
+ sparklineOptions.charttype = sparkline.chartType;
3725
+ }
3726
+ if (sparkline.color)
3727
+ sparklineOptions.color = sparkline.color;
3728
+ if (sparkline.colorHigh)
3729
+ sparklineOptions.colorHigh = sparkline.colorHigh;
3730
+ if (sparkline.colorLow)
3731
+ sparklineOptions.colorLow = sparkline.colorLow;
3732
+ if (sparkline.colorFirst)
3733
+ sparklineOptions.colorFirst = sparkline.colorFirst;
3734
+ if (sparkline.colorLast)
3735
+ sparklineOptions.colorLast = sparkline.colorLast;
3736
+ if (sparkline.colorMarkers)
3737
+ sparklineOptions.colorMarkers = sparkline.colorMarkers;
3738
+ if (sparkline.colorNegative)
3739
+ sparklineOptions.colorNegative = sparkline.colorNegative;
3740
+ if (sparkline.markers)
3741
+ sparklineOptions.markers = true;
3742
+ if (sparkline.high)
3743
+ sparklineOptions.high = true;
3744
+ if (sparkline.low)
3745
+ sparklineOptions.low = true;
3746
+ if (sparkline.first)
3747
+ sparklineOptions.first = true;
3748
+ if (sparkline.last)
3749
+ sparklineOptions.last = true;
3750
+ if (sparkline.negative)
3751
+ sparklineOptions.negative = true;
3752
+ // x14 axis / display options — same lowercase keys the
3753
+ // SPARKLINE() formula literal uses.
3754
+ if (sparkline.emptyCellsAs)
3755
+ sparklineOptions.emptycells = sparkline.emptyCellsAs;
3756
+ if (sparkline.dateAxis)
3757
+ sparklineOptions.dateaxis = true;
3758
+ if (sparkline.displayXAxis)
3759
+ sparklineOptions.axis = true;
3760
+ if (sparkline.displayHidden)
3761
+ sparklineOptions.displayhidden = true;
3762
+ if (sparkline.rightToLeft)
3763
+ sparklineOptions.rtl = true;
3764
+ if (sparkline.lineWeight != null)
3765
+ sparklineOptions.lineweight = sparkline.lineWeight;
3766
+ if (sparkline.manualMax != null)
3767
+ sparklineOptions.max = sparkline.manualMax;
3768
+ if (sparkline.manualMin != null)
3769
+ sparklineOptions.min = sparkline.manualMin;
3770
+ if (sparkline.minAxisType)
3771
+ sparklineOptions.minaxistype = sparkline.minAxisType;
3772
+ if (sparkline.maxAxisType)
3773
+ sparklineOptions.maxaxistype = sparkline.maxAxisType;
3774
+ // Only set effective value if we have enough data points
3775
+ const cellData = {
3776
+ ...existingCell,
3777
+ ue: { fv: sparkline.formula },
3778
+ };
3779
+ if (sparklineData.filter((v) => v != null).length >= 2) {
3780
+ cellData.ev = {
3781
+ structuredValue: {
3782
+ kind: "sparkline",
3783
+ data: sparklineData,
3784
+ options: sparklineOptions,
3785
+ },
3786
+ };
3787
+ }
3788
+ rowData.values[columnIndex] = cellData;
3789
+ }
3790
+ sparklinesByRow.delete(rowIndex);
3791
+ }
3792
+ dvStamper?.stampRow(rowIndex, rowData.values);
3793
+ currentChunk.push(rowData);
3794
+ currentChunkRowIndices.push(rowIndex);
3795
+ endRowIndex = rowIndex;
3796
+ if (currentChunk.length >= CHUNK_SIZE) {
3797
+ const payload = flushChunk(false);
3798
+ if (payload) {
3799
+ yield payload;
3800
+ }
3801
+ }
3802
+ }
3803
+ // Rows fully absent from the file but covered by a (bounded) DV range
3804
+ // still need phantom `{ dataValidation }` cells or blank template rows
3805
+ // lose their dropdowns.
3806
+ if (dvStamper?.hasRules) {
3807
+ for (const trailing of dvStamper.trailingRows()) {
3808
+ if (currentChunk.length === 0) {
3809
+ startRowIndex = trailing.rowIndex;
3810
+ }
3811
+ currentChunk.push({ values: trailing.values });
3812
+ currentChunkRowIndices.push(trailing.rowIndex);
3813
+ endRowIndex = trailing.rowIndex;
3814
+ totalRowsProcessed += 1;
3815
+ if (currentChunk.length >= CHUNK_SIZE) {
3816
+ const payload = flushChunk(false);
3817
+ if (payload) {
3818
+ yield payload;
3819
+ }
3820
+ }
3821
+ }
3822
+ }
3823
+ const finalPayload = flushChunk(true);
3824
+ if (finalPayload) {
3825
+ yield finalPayload;
3826
+ }
3827
+ }
3828
+ }
3829
+ async getDrawings() {
3830
+ const workSheets = this._getSheets();
3831
+ const allDrawings = [];
3832
+ // Process each worksheet
3833
+ for (const element of workSheets) {
3834
+ const sheetId = Number(element.a$.sheetId);
3835
+ const rid = element.a$["r:id"];
3836
+ // Drawings are referenced from a worksheet-level <drawing> element;
3837
+ // skip the full-DOM parse when the SAX pass saw none.
3838
+ const features = this.sheetFeatures.get(rid);
3839
+ if (features && !features.drawing) {
3840
+ continue;
3841
+ }
3842
+ const sheet_data = await this.getSheetData(rid);
3843
+ const drawings = await this._getDrawings(rid, sheetId, sheet_data);
3844
+ allDrawings.push(...drawings);
3845
+ }
3846
+ return allDrawings;
3847
+ }
3848
+ async getCharts() {
3849
+ const allCharts = [];
3850
+ const workSheets = this._getSheets();
3851
+ // Process each worksheet
3852
+ for (const element of workSheets) {
3853
+ const sheetId = Number(element.a$.sheetId);
3854
+ const rid = element.a$["r:id"];
3855
+ // Charts are anchored inside drawing parts, which the worksheet
3856
+ // references via a <drawing> element; skip when the sheet has none.
3857
+ const features = this.sheetFeatures.get(rid);
3858
+ if (features && !features.drawing) {
3859
+ continue;
3860
+ }
3861
+ const sheet_data = await this.getSheetData(rid);
3862
+ const charts = await this._getCharts(rid, sheetId, sheet_data);
3863
+ allCharts.push(...charts);
3864
+ }
3865
+ return allCharts;
3866
+ }
3867
+ /**
3868
+ * Phase D1 — extract every pivot table across every worksheet.
3869
+ * Walks worksheet rels for `pivotTable` relationships, parses
3870
+ * the pivot XML + cacheDefinition, and returns flat PivotTable[]
3871
+ * suitable for `onChangePivotTables`.
3872
+ */
3873
+ async getPivotTables() {
3874
+ const { parsePivotTablesForSheet } = await import("./pivots");
3875
+ const all = [];
3876
+ const workSheets = this._getSheets();
3877
+ // Cache sheet-name → sheetId mapping (cacheSource refs sheet
3878
+ // by name; PivotTable wants numeric sheetId).
3879
+ const sheetNameToId = new Map();
3880
+ for (const ws of workSheets) {
3881
+ const name = ws.a$?.name;
3882
+ const id = Number(ws.a$?.sheetId);
3883
+ if (name && Number.isFinite(id))
3884
+ sheetNameToId.set(name, id);
3885
+ }
3886
+ let pivotIdCounter = 1;
3887
+ const nextPivotId = () => `imported-pivot-${pivotIdCounter++}`;
3888
+ for (const element of workSheets) {
3889
+ const sheetId = Number(element.a$.sheetId);
3890
+ const rid = element.a$["r:id"];
3891
+ // Find pivotTable rels for this sheet. Sheet rels link to
3892
+ // pivot tables with Type ending in `pivotTable`.
3893
+ const target = this.relations[rid]?.target;
3894
+ if (!target)
3895
+ continue;
3896
+ const path = target.startsWith("/xl")
3897
+ ? target.replace(/^\//gi, "")
3898
+ : `xl/${target}`;
3899
+ const sheetRelPath = path.replace("worksheets", "worksheets/_rels") + ".rels";
3900
+ // Sheets without a rels part (common for plain data sheets) must not
3901
+ // abort pivot extraction for the remaining sheets.
3902
+ let sheetRelContent;
3903
+ try {
3904
+ sheetRelContent = await this.zipParser.getText(sheetRelPath);
3905
+ }
3906
+ catch {
3907
+ continue;
3908
+ }
3909
+ if (!sheetRelContent)
3910
+ continue;
3911
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
3912
+ const relList = get(sheetRels, "Relationships.Relationship");
3913
+ const relArray = Array.isArray(relList) ? relList : relList ? [relList] : [];
3914
+ const pivotRels = relArray
3915
+ .filter((r) => {
3916
+ const type = r?.a$?.Type ?? r?.["@_Type"] ?? "";
3917
+ return typeof type === "string" && type.endsWith("pivotTable");
3918
+ })
3919
+ .map((r) => ({
3920
+ target: String(r?.a$?.Target ?? r?.["@_Target"] ?? ""),
3921
+ }));
3922
+ if (!pivotRels.length)
3923
+ continue;
3924
+ const pivots = await parsePivotTablesForSheet({
3925
+ zipParser: this.zipParser,
3926
+ xmlParser: this.xmlParser,
3927
+ sheetRels: pivotRels,
3928
+ nextPivotId,
3929
+ getSheetId: (name) => sheetNameToId.get(name),
3930
+ defaultSheetId: sheetId,
3931
+ });
3932
+ all.push(...pivots);
3933
+ }
3934
+ return all;
3935
+ }
3936
+ /**
3937
+ * Power Query preservation — read the workbook's Get & Transform machinery verbatim so the
3938
+ * exporter can re-emit it and a round-tripped file keeps its queries: the DataMashup
3939
+ * `customXml` items (+itemProps), `xl/connections.xml`, and each query-backed table's
3940
+ * `queryTable` part with the original column ids / `queryTableFieldId`s. Everything is opaque
3941
+ * XML strings — no QDEFF/M parsing. Mirrors the Rust engine's `rnc-import` `queries` module
3942
+ * (same envelope wire shape). `undefined` when the workbook carries none.
3943
+ */
3944
+ async getPowerQuery() {
3945
+ const customXmlParts = [];
3946
+ let connectionsXml;
3947
+ // Workbook-level parts via the already-parsed workbook rels (targets relative to `xl/`).
3948
+ for (const rel of Object.values(this.relations)) {
3949
+ const type = rel?.type ?? "";
3950
+ if (type.endsWith("/customXml")) {
3951
+ const itemPath = resolveRelTarget("xl", rel.target);
3952
+ if (!this.zipParser.has(itemPath))
3953
+ continue;
3954
+ const itemXml = await this.zipParser.getText(itemPath);
3955
+ // Only DataMashup items are Power Query; other customXml stays dropped, as before.
3956
+ if (!itemXml.includes(DATA_MASHUP_MARKER))
3957
+ continue;
3958
+ let propsXml;
3959
+ const itemRelsPath = relsPathFor(itemPath);
3960
+ if (this.zipParser.has(itemRelsPath)) {
3961
+ const itemRels = this.xmlParser.parse(await this.zipParser.getText(itemRelsPath));
3962
+ const relList = get(itemRels, "Relationships.Relationship");
3963
+ const relArray = Array.isArray(relList)
3964
+ ? relList
3965
+ : relList
3966
+ ? [relList]
3967
+ : [];
3968
+ const propsRel = relArray.find((r) => String(r?.a$?.Type ?? "").endsWith("/customXmlProps"));
3969
+ if (propsRel) {
3970
+ const propsPath = resolveRelTarget(dirOf(itemPath), String(propsRel.a$.Target));
3971
+ if (this.zipParser.has(propsPath)) {
3972
+ propsXml = await this.zipParser.getText(propsPath);
3973
+ }
3974
+ }
3975
+ }
3976
+ customXmlParts.push(propsXml ? { itemXml, propsXml } : { itemXml });
3977
+ }
3978
+ else if (type.endsWith("/connections")) {
3979
+ const path = resolveRelTarget("xl", rel.target);
3980
+ if (this.zipParser.has(path)) {
3981
+ connectionsXml = await this.zipParser.getText(path);
3982
+ }
3983
+ }
3984
+ }
3985
+ // Query tables: sheet rels → table parts → each table's own rels → its queryTable part.
3986
+ const queryTables = [];
3987
+ for (const element of this._getSheets()) {
3988
+ const rid = element.a$?.["r:id"];
3989
+ const target = rid ? this.relations[rid]?.target : undefined;
3990
+ if (!target)
3991
+ continue;
3992
+ const sheetPath = resolveRelTarget("xl", target);
3993
+ const sheetRelsPath = relsPathFor(sheetPath);
3994
+ if (!this.zipParser.has(sheetRelsPath))
3995
+ continue;
3996
+ const sheetRels = this.xmlParser.parse(await this.zipParser.getText(sheetRelsPath));
3997
+ const relList = get(sheetRels, "Relationships.Relationship");
3998
+ const relArray = Array.isArray(relList) ? relList : relList ? [relList] : [];
3999
+ for (const r of relArray) {
4000
+ const relType = String(r?.a$?.Type ?? "");
4001
+ if (!relType.endsWith("/table"))
4002
+ continue;
4003
+ const tablePath = resolveRelTarget(dirOf(sheetPath), String(r.a$.Target));
4004
+ const tableRelsPath = relsPathFor(tablePath);
4005
+ if (!this.zipParser.has(tablePath) || !this.zipParser.has(tableRelsPath)) {
4006
+ continue;
4007
+ }
4008
+ const tableRels = this.xmlParser.parse(await this.zipParser.getText(tableRelsPath));
4009
+ const tableRelList = get(tableRels, "Relationships.Relationship");
4010
+ const tableRelArray = Array.isArray(tableRelList)
4011
+ ? tableRelList
4012
+ : tableRelList
4013
+ ? [tableRelList]
4014
+ : [];
4015
+ const qtRel = tableRelArray.find((t) => String(t?.a$?.Type ?? "").endsWith("/queryTable"));
4016
+ if (!qtRel)
4017
+ continue;
4018
+ const qtPath = resolveRelTarget(dirOf(tablePath), String(qtRel.a$.Target));
4019
+ if (!this.zipParser.has(qtPath))
4020
+ continue;
4021
+ const partXml = await this.zipParser.getText(qtPath);
4022
+ const tableData = this.xmlParser.parse(await this.zipParser.getText(tablePath));
4023
+ const tableDefinition = get(tableData, "table") || get(tableData, "x:table");
4024
+ const attrs = tableDefinition?.a$ ?? {};
4025
+ const tableTitle = String(attrs.displayName || attrs.name || "");
4026
+ if (!tableTitle)
4027
+ continue;
4028
+ const colDefs = get(tableDefinition, "tableColumns.tableColumn") ||
4029
+ get(tableDefinition, "x:tableColumns.x:tableColumn");
4030
+ const colArray = Array.isArray(colDefs) ? colDefs : colDefs ? [colDefs] : [];
4031
+ const columns = colArray.map((c) => {
4032
+ const column = {
4033
+ name: String(c?.a$?.name ?? ""),
4034
+ id: String(c?.a$?.id ?? ""),
4035
+ };
4036
+ if (c?.a$?.queryTableFieldId != null) {
4037
+ column.queryTableFieldId = String(c.a$.queryTableFieldId);
4038
+ }
4039
+ return column;
4040
+ });
4041
+ queryTables.push({ tableTitle, partXml, columns });
4042
+ }
4043
+ }
4044
+ const parts = {};
4045
+ if (customXmlParts.length)
4046
+ parts.customXmlParts = customXmlParts;
4047
+ if (connectionsXml)
4048
+ parts.connectionsXml = connectionsXml;
4049
+ if (queryTables.length)
4050
+ parts.queryTables = queryTables;
4051
+ return hasPowerQueryParts(parts) ? parts : undefined;
4052
+ }
4053
+ async getConditionalFormatting() {
4054
+ // Workbook-wide view is an aggregation over the per-sheet caches so
4055
+ // per-sheet and workbook-wide callers observe the same rule objects.
4056
+ const conditionalFormatting = [];
4057
+ for (const element of this._getSheets()) {
4058
+ conditionalFormatting.push(...(await this.getConditionalFormattingForSheet(Number(element.a$.sheetId))));
4059
+ }
4060
+ return conditionalFormatting;
4061
+ }
4062
+ getConditionalFormattingForSheet(sheetId) {
4063
+ let cached = this.conditionalFormattingBySheetCache.get(sheetId);
4064
+ if (!cached) {
4065
+ cached = this.parseConditionalFormattingForSheet(sheetId);
4066
+ this.conditionalFormattingBySheetCache.set(sheetId, cached);
4067
+ }
4068
+ return cached;
4069
+ }
4070
+ async parseConditionalFormattingForSheet(sheetId) {
4071
+ const element = this.worksheetBySheetId.get(sheetId);
4072
+ if (!element) {
4073
+ return [];
4074
+ }
4075
+ const rid = element.a$["r:id"];
4076
+ // x14 conditional formatting lives in extLst, classic rules in
4077
+ // <conditionalFormatting>; skip the full-DOM parse when neither exists.
4078
+ const features = this.sheetFeatures.get(rid);
4079
+ if (features && !features.conditionalFormatting && !features.extLst) {
4080
+ return [];
4081
+ }
4082
+ const sheet_data = await this.getSheetData(rid);
4083
+ return this.getConditionaFormattingFromSheet(sheetId, sheet_data);
4084
+ }
4085
+ async getDataValidations() {
4086
+ // Workbook-wide view is an aggregation over the per-sheet caches.
4087
+ // The row stream stamps these rules' ids onto covered cells, so every
4088
+ // caller must see the SAME minted ids (see the
4089
+ // `dataValidationsBySheetCache` field doc).
4090
+ const dataValidations = [];
4091
+ for (const element of this._getSheets()) {
4092
+ dataValidations.push(...(await this.getDataValidationsForSheet(Number(element.a$.sheetId))));
4093
+ }
4094
+ return dataValidations;
4095
+ }
4096
+ getDataValidationsForSheet(sheetId) {
4097
+ let cached = this.dataValidationsBySheetCache.get(sheetId);
4098
+ if (!cached) {
4099
+ cached = this.parseDataValidationsForSheet(sheetId);
4100
+ this.dataValidationsBySheetCache.set(sheetId, cached);
4101
+ }
4102
+ return cached;
4103
+ }
4104
+ async parseDataValidationsForSheet(sheetId) {
4105
+ const element = this.worksheetBySheetId.get(sheetId);
4106
+ if (!element) {
4107
+ return [];
4108
+ }
4109
+ const rid = element.a$["r:id"];
4110
+ const sheetName = element.a$?.name;
4111
+ // x14 data validations live in extLst, classic ones in
4112
+ // <dataValidations>; skip the full-DOM parse when neither exists.
4113
+ const features = this.sheetFeatures.get(rid);
4114
+ if (features && !features.dataValidations && !features.extLst) {
4115
+ return [];
4116
+ }
4117
+ const sheet_data = await this.getSheetData(rid);
4118
+ return parseDataValidationsFromSheetData(this, sheetId, sheet_data, sheetName);
4119
+ }
4120
+ getNamedRanges() {
4121
+ return this.namedRanges;
4122
+ }
4123
+ /**
4124
+ * Parse the workbook's metadata files (docProps/core.xml + docProps/app.xml)
4125
+ * and return a flat properties object. Returns undefined if neither file
4126
+ * exists (won't happen for files written by Excel, but possible for
4127
+ * minimal workbooks crafted by other tools).
4128
+ */
4129
+ async getProperties() {
4130
+ const props = {};
4131
+ const tryRead = async (path) => {
4132
+ try {
4133
+ return await this.zipParser.getText(path);
4134
+ }
4135
+ catch {
4136
+ return undefined;
4137
+ }
4138
+ };
4139
+ const coreXml = await tryRead("docProps/core.xml");
4140
+ if (coreXml) {
4141
+ const parsed = this.xmlParser.parse(coreXml);
4142
+ const root = parsed?.["cp:coreProperties"] ??
4143
+ parsed?.coreProperties ??
4144
+ parsed?.["coreProperties"];
4145
+ // Elements with attributes (e.g. <dcterms:created xsi:type="...">VALUE)
4146
+ // are parsed into `{a$: {...}, t$: "VALUE"}` with this config — see
4147
+ // XMLOptions2 (`textNodeName: "t$"`). Bare text nodes come through as
4148
+ // plain strings. Read both shapes.
4149
+ const textOf = (val) => {
4150
+ if (val == null)
4151
+ return undefined;
4152
+ if (typeof val === "string")
4153
+ return val;
4154
+ if (typeof val === "number")
4155
+ return String(val);
4156
+ if (typeof val === "object") {
4157
+ const obj = val;
4158
+ if ("t$" in obj)
4159
+ return textOf(obj["t$"]);
4160
+ if ("#text" in obj)
4161
+ return textOf(obj["#text"]);
4162
+ }
4163
+ return undefined;
4164
+ };
4165
+ if (root && typeof root === "object") {
4166
+ props.title = textOf(root["dc:title"] ?? root.title);
4167
+ props.subject = textOf(root["dc:subject"] ?? root.subject);
4168
+ props.creator = textOf(root["dc:creator"] ?? root.creator);
4169
+ props.keywords = textOf(root["cp:keywords"] ?? root.keywords);
4170
+ props.description = textOf(root["dc:description"] ?? root.description);
4171
+ props.lastModifiedBy = textOf(root["cp:lastModifiedBy"] ?? root.lastModifiedBy);
4172
+ props.category = textOf(root["cp:category"] ?? root.category);
4173
+ props.created = textOf(root["dcterms:created"] ?? root.created);
4174
+ props.modified = textOf(root["dcterms:modified"] ?? root.modified);
4175
+ }
4176
+ }
4177
+ const appXml = await tryRead("docProps/app.xml");
4178
+ if (appXml) {
4179
+ const parsed = this.xmlParser.parse(appXml);
4180
+ const root = parsed?.Properties ?? parsed?.properties;
4181
+ if (root && typeof root === "object") {
4182
+ const textOf = (val) => {
4183
+ if (typeof val === "string")
4184
+ return val;
4185
+ if (typeof val === "number")
4186
+ return String(val);
4187
+ return undefined;
4188
+ };
4189
+ props.company = textOf(root.Company);
4190
+ props.manager = textOf(root.Manager);
4191
+ props.application = textOf(root.Application);
4192
+ }
4193
+ }
4194
+ // Strip empty fields so callers can spread cleanly.
4195
+ for (const key of Object.keys(props)) {
4196
+ if (props[key] === undefined || props[key] === "") {
4197
+ delete props[key];
4198
+ }
4199
+ }
4200
+ return Object.keys(props).length ? props : undefined;
4201
+ }
4202
+ async getSlicers() {
4203
+ return this.getSlicersForWorkbook();
4204
+ }
4205
+ async getSlicersForWorkbook() {
4206
+ const slicers = [];
4207
+ try {
4208
+ // Read workbook relationships to find slicer files
4209
+ const workbookRelsPath = "xl/_rels/workbook.xml.rels";
4210
+ const workbookRelsContent = await this.zipParser.getText(workbookRelsPath);
4211
+ const workbookRels = this.xmlParser.parse(workbookRelsContent);
4212
+ const relationships = get(workbookRels, "Relationships.Relationship");
4213
+ const relationshipsArray = Array.isArray(relationships)
4214
+ ? relationships
4215
+ : relationships
4216
+ ? [relationships]
4217
+ : [];
4218
+ // Find slicer and slicerCache relationships
4219
+ const slicerRels = relationshipsArray.filter((rel) => rel.a$.Type ===
4220
+ "http://schemas.microsoft.com/office/2007/relationships/slicer");
4221
+ const slicerCacheRels = relationshipsArray.filter((rel) => rel.a$.Type ===
4222
+ "http://schemas.microsoft.com/office/2007/relationships/slicerCache");
4223
+ const toArray = (value) => Array.isArray(value) ? value : value ? [value] : [];
4224
+ const slicerPathToSheetIds = new Map();
4225
+ const addSlicerPath = (path, sheetId) => {
4226
+ const existing = slicerPathToSheetIds.get(path) ?? new Set();
4227
+ if (typeof sheetId === "number") {
4228
+ existing.add(sheetId);
4229
+ }
4230
+ slicerPathToSheetIds.set(path, existing);
4231
+ };
4232
+ for (const slicerRel of slicerRels) {
4233
+ const target = slicerRel?.a$?.Target;
4234
+ if (!target)
4235
+ continue;
4236
+ addSlicerPath(this.resolveSheetPath(target));
4237
+ }
4238
+ // Read worksheet relationship files to discover slicer relationships.
4239
+ // Some Excel files only declare slicers at sheet level (not workbook rels).
4240
+ const workSheets = this._getSheets();
4241
+ const sheetRelContext = new Map();
4242
+ for (const sheet of workSheets) {
4243
+ const rid = sheet?.a$?.["r:id"];
4244
+ const currentSheetId = Number(sheet?.a$?.sheetId);
4245
+ if (!rid || Number.isNaN(currentSheetId))
4246
+ continue;
4247
+ const relation = this.relations[rid];
4248
+ if (!relation?.target)
4249
+ continue;
4250
+ const sheetPath = this.resolveSheetPath(relation.target);
4251
+ const sheetRelPath = sheetPath.replace("worksheets", "worksheets/_rels") + ".rels";
4252
+ try {
4253
+ const sheetRelContent = await this.zipParser.getText(sheetRelPath);
4254
+ if (!sheetRelContent)
4255
+ continue;
4256
+ const sheetRels = this.xmlParser.parse(sheetRelContent);
4257
+ const sheetRelations = get(sheetRels, "Relationships.Relationship");
4258
+ const sheetRelationsArray = toArray(sheetRelations);
4259
+ const drawingRel = sheetRelationsArray.find((rel) => rel?.a$?.Type ===
4260
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing");
4261
+ const drawingPath = drawingRel?.a$?.Target
4262
+ ? this.resolveRelationshipTarget(sheetRelPath, drawingRel.a$.Target)
4263
+ : undefined;
4264
+ sheetRelContext.set(currentSheetId, { drawingPath });
4265
+ const sheetSlicerRels = sheetRelationsArray.filter((rel) => rel?.a$?.Type ===
4266
+ "http://schemas.microsoft.com/office/2007/relationships/slicer");
4267
+ for (const rel of sheetSlicerRels) {
4268
+ const target = rel?.a$?.Target;
4269
+ if (!target)
4270
+ continue;
4271
+ addSlicerPath(this.resolveRelationshipTarget(sheetRelPath, target), currentSheetId);
4272
+ }
4273
+ }
4274
+ catch {
4275
+ // Sheet may not have a relationship part.
4276
+ continue;
4277
+ }
4278
+ }
4279
+ if (slicerPathToSheetIds.size === 0) {
4280
+ return slicers;
4281
+ }
4282
+ // Parse slicer cache files to build a map of cache to table IDs
4283
+ const slicerCacheMap = new Map();
4284
+ for (const cacheRel of slicerCacheRels) {
4285
+ try {
4286
+ let cachePath = cacheRel.a$.Target;
4287
+ if (cachePath.startsWith("/")) {
4288
+ cachePath = cachePath.substring(1);
4289
+ }
4290
+ else if (!cachePath.startsWith("xl/")) {
4291
+ cachePath = `xl/${cachePath}`;
4292
+ }
4293
+ const cacheContent = await this.zipParser.getText(cachePath);
4294
+ if (!cacheContent)
4295
+ continue;
4296
+ const cacheXml = this.xmlParser.parse(cacheContent);
4297
+ const cacheDefinition = get(cacheXml, "x14:slicerCacheDefinition") ||
4298
+ get(cacheXml, "slicerCacheDefinition");
4299
+ if (!cacheDefinition)
4300
+ continue;
4301
+ // Extract cache name
4302
+ const cacheName = cacheDefinition.a$?.name || "";
4303
+ // Extract title (sourceName is the column name)
4304
+ const title = cacheDefinition.a$?.sourceName || cacheName;
4305
+ // Look for tableSlicerCache extension
4306
+ const extList = getWithFallback(cacheDefinition, [
4307
+ "x14:extLst.x14:ext",
4308
+ "x14:extLst.ext",
4309
+ "extLst.x14:ext",
4310
+ "extLst.ext",
4311
+ "extLst.x:ext",
4312
+ ]);
4313
+ const extArray = toArray(extList);
4314
+ for (const ext of extArray) {
4315
+ const tableSlicerCache = getWithFallback(ext, [
4316
+ "x15:tableSlicerCache",
4317
+ "tableSlicerCache",
4318
+ "x:tableSlicerCache",
4319
+ ]);
4320
+ if (tableSlicerCache) {
4321
+ const tableIdRaw = tableSlicerCache.a$?.tableId;
4322
+ const tableId = String(tableIdRaw ?? "");
4323
+ const columnIndex = Number(tableSlicerCache.a$?.column) - 1; // Convert to 0-based
4324
+ if (tableId && !isNaN(columnIndex)) {
4325
+ slicerCacheMap.set(cacheName, { tableId, columnIndex, title });
4326
+ }
4327
+ break;
4328
+ }
4329
+ }
4330
+ }
4331
+ catch (err) {
4332
+ console.log("Error parsing slicer cache:", err);
4333
+ }
4334
+ }
4335
+ // Parse slicer files and match them with worksheet positions
4336
+ for (const [slicerPath, linkedSheetIds] of slicerPathToSheetIds) {
4337
+ try {
4338
+ const slicerContent = await this.zipParser.getText(slicerPath);
4339
+ if (!slicerContent)
4340
+ continue;
4341
+ const slicerXml = this.xmlParser.parse(slicerContent);
4342
+ const slicerDefinitionNode = getWithFallback(slicerXml, [
4343
+ "x14:slicer",
4344
+ "slicer",
4345
+ "x14:slicers.x14:slicer",
4346
+ "x14:slicers.slicer",
4347
+ "slicers.x14:slicer",
4348
+ "slicers.slicer",
4349
+ ]);
4350
+ const slicerDefinition = Array.isArray(slicerDefinitionNode)
4351
+ ? slicerDefinitionNode[0]
4352
+ : slicerDefinitionNode;
4353
+ if (!slicerDefinition)
4354
+ continue;
4355
+ const slicerAttrs = slicerDefinition.a$ || {};
4356
+ const cacheName = slicerAttrs.cache || slicerAttrs.name;
4357
+ const caption = slicerAttrs.caption || cacheName;
4358
+ // Look up cache information
4359
+ const cacheInfo = slicerCacheMap.get(cacheName);
4360
+ if (!cacheInfo) {
4361
+ continue;
4362
+ }
4363
+ // Find the slicer position by checking worksheet relationships
4364
+ let position = null;
4365
+ let sheetId;
4366
+ for (const sheet of workSheets) {
4367
+ const currentSheetId = Number(sheet?.a$?.sheetId);
4368
+ if (Number.isNaN(currentSheetId))
4369
+ continue;
4370
+ if (linkedSheetIds.size && !linkedSheetIds.has(currentSheetId)) {
4371
+ continue;
4372
+ }
4373
+ const context = sheetRelContext.get(currentSheetId);
4374
+ if (!context)
4375
+ continue;
4376
+ sheetId = currentSheetId;
4377
+ if (context.drawingPath) {
4378
+ const drawingContent = await this.zipParser.getText(context.drawingPath);
4379
+ if (drawingContent) {
4380
+ const drawingXml = this.xmlParser.parse(drawingContent);
4381
+ const wsDr = getWithFallback(drawingXml, ["xdr:wsDr", "wsDr"]);
4382
+ if (wsDr) {
4383
+ const anchors = [
4384
+ ...toArray(wsDr["xdr:oneCellAnchor"] || wsDr["oneCellAnchor"]),
4385
+ ...toArray(wsDr["xdr:twoCellAnchor"] || wsDr["twoCellAnchor"]),
4386
+ ];
4387
+ let firstSlicerAnchor = null;
4388
+ for (const anchor of anchors) {
4389
+ const alternateContent = getWithFallback(anchor, [
4390
+ "mc:AlternateContent",
4391
+ "AlternateContent",
4392
+ ]);
4393
+ const choices = toArray(alternateContent?.["mc:Choice"] ??
4394
+ alternateContent?.["Choice"]);
4395
+ const graphicFrames = [
4396
+ ...toArray(getWithFallback(anchor, [
4397
+ "xdr:graphicFrame",
4398
+ "graphicFrame",
4399
+ ])),
4400
+ ...choices.flatMap((choice) => toArray(getWithFallback(choice, [
4401
+ "xdr:graphicFrame",
4402
+ "graphicFrame",
4403
+ ]))),
4404
+ ];
4405
+ for (const graphicFrame of graphicFrames) {
4406
+ const graphicData = getWithFallback(graphicFrame, [
4407
+ "a:graphic.a:graphicData",
4408
+ "a:graphic.graphicData",
4409
+ "graphic.a:graphicData",
4410
+ "graphic.graphicData",
4411
+ "a:graphicData",
4412
+ "graphicData",
4413
+ ]);
4414
+ const uri = graphicData?.a$?.uri;
4415
+ const isSlicerGraphic = typeof uri === "string" && uri.includes("/slicer");
4416
+ if (!isSlicerGraphic) {
4417
+ continue;
4418
+ }
4419
+ const nvGraphicFramePr = getWithFallback(graphicFrame, [
4420
+ "xdr:nvGraphicFramePr",
4421
+ "nvGraphicFramePr",
4422
+ ]);
4423
+ const cNvPr = getWithFallback(nvGraphicFramePr, [
4424
+ "xdr:cNvPr",
4425
+ "cNvPr",
4426
+ ]);
4427
+ const shapeName = cNvPr?.a$?.name;
4428
+ if (!firstSlicerAnchor) {
4429
+ firstSlicerAnchor = anchor;
4430
+ }
4431
+ if (shapeName === caption || shapeName === cacheName) {
4432
+ position = this.extractSlicerPosition(anchor, currentSheetId);
4433
+ break;
4434
+ }
4435
+ }
4436
+ if (position)
4437
+ break;
4438
+ }
4439
+ if (!position && firstSlicerAnchor) {
4440
+ position = this.extractSlicerPosition(firstSlicerAnchor, currentSheetId);
4441
+ }
4442
+ }
4443
+ }
4444
+ }
4445
+ break;
4446
+ }
4447
+ // Create default position if we couldn't find one
4448
+ if (!position && sheetId !== undefined) {
4449
+ position = {
4450
+ sheetId,
4451
+ overlayPosition: {
4452
+ anchorCell: {
4453
+ rowIndex: 1,
4454
+ columnIndex: 1,
4455
+ },
4456
+ offsetXPixels: 0,
4457
+ offsetYPixels: 0,
4458
+ widthPixels: 200,
4459
+ heightPixels: 300,
4460
+ },
4461
+ };
4462
+ }
4463
+ if (position) {
4464
+ const slicer = {
4465
+ slicerId: uuid(),
4466
+ spec: {
4467
+ type: "table",
4468
+ tableIds: [cacheInfo.tableId],
4469
+ columnIndex: cacheInfo.columnIndex,
4470
+ title: caption || cacheInfo.title,
4471
+ },
4472
+ position,
4473
+ };
4474
+ slicers.push(slicer);
4475
+ }
4476
+ }
4477
+ catch (err) {
4478
+ console.log("Error parsing slicer:", err);
4479
+ }
4480
+ }
4481
+ }
4482
+ catch (err) {
4483
+ console.log("Error getting slicers from workbook:", err);
4484
+ }
4485
+ return slicers;
4486
+ }
4487
+ extractSlicerPosition(anchor, sheetId) {
4488
+ try {
4489
+ // Handle oneCellAnchor
4490
+ const oneCellFrom = get(anchor, "xdr:from") || get(anchor, "from");
4491
+ const hasTwoCellTarget = get(anchor, "xdr:to") || get(anchor, "to");
4492
+ if (oneCellFrom && !hasTwoCellTarget) {
4493
+ const extNode = get(anchor, "xdr:ext") || get(anchor, "ext");
4494
+ const extAttributes = (extNode && (extNode.a$ ?? extNode)) || {};
4495
+ const col = this.parseIntSafe(get(oneCellFrom, "xdr:col") || get(oneCellFrom, "col"));
4496
+ const row = this.parseIntSafe(get(oneCellFrom, "xdr:row") || get(oneCellFrom, "row"));
4497
+ const colOff = this.parseIntSafe(get(oneCellFrom, "xdr:colOff") || get(oneCellFrom, "colOff"));
4498
+ const rowOff = this.parseIntSafe(get(oneCellFrom, "xdr:rowOff") || get(oneCellFrom, "rowOff"));
4499
+ // Convert EMU to pixels (1 EMU = 1/9525 pixels)
4500
+ const width = extAttributes.cx
4501
+ ? Math.round(parseInt(String(extAttributes.cx), 10) / 9525)
4502
+ : 200;
4503
+ const height = extAttributes.cy
4504
+ ? Math.round(parseInt(String(extAttributes.cy), 10) / 9525)
4505
+ : 300;
4506
+ return {
4507
+ sheetId,
4508
+ overlayPosition: {
4509
+ anchorCell: {
4510
+ rowIndex: row + 1,
4511
+ columnIndex: col + 1,
4512
+ },
4513
+ offsetXPixels: Math.round(colOff / 9525),
4514
+ offsetYPixels: Math.round(rowOff / 9525),
4515
+ widthPixels: width,
4516
+ heightPixels: height,
4517
+ },
4518
+ };
4519
+ }
4520
+ // Handle twoCellAnchor
4521
+ const fromCell = get(anchor, "xdr:from") || get(anchor, "from");
4522
+ const toCell = get(anchor, "xdr:to") || get(anchor, "to");
4523
+ if (fromCell && toCell) {
4524
+ const col = this.parseIntSafe(get(fromCell, "xdr:col") || get(fromCell, "col"));
4525
+ const row = this.parseIntSafe(get(fromCell, "xdr:row") || get(fromCell, "row"));
4526
+ const colOff = this.parseIntSafe(get(fromCell, "xdr:colOff") || get(fromCell, "colOff"));
4527
+ const rowOff = this.parseIntSafe(get(fromCell, "xdr:rowOff") || get(fromCell, "rowOff"));
4528
+ const toCol = this.parseIntSafe(get(toCell, "xdr:col") || get(toCell, "col"));
4529
+ const toRow = this.parseIntSafe(get(toCell, "xdr:row") || get(toCell, "row"));
4530
+ // Estimate size based on cell span
4531
+ const width = Math.max((toCol - col) * 64, 100);
4532
+ const height = Math.max((toRow - row) * 20, 100);
4533
+ return {
4534
+ sheetId,
4535
+ overlayPosition: {
4536
+ anchorCell: {
4537
+ rowIndex: row + 1,
4538
+ columnIndex: col + 1,
4539
+ },
4540
+ offsetXPixels: Math.round(colOff / 9525),
4541
+ offsetYPixels: Math.round(rowOff / 9525),
4542
+ widthPixels: width,
4543
+ heightPixels: height,
4544
+ },
4545
+ };
4546
+ }
4547
+ return null;
4548
+ }
4549
+ catch (err) {
4550
+ console.log("Error extracting slicer position:", err);
4551
+ return null;
4552
+ }
4553
+ }
4554
+ parseIntSafe(value) {
4555
+ const parsed = parseInt(typeof value === "number" ? String(value) : value ?? "0", 10);
4556
+ return Number.isNaN(parsed) ? 0 : parsed;
4557
+ }
4558
+ /**
4559
+ * Get external workbook links from the XLSX file.
4560
+ * These are needed to preserve formulas that reference other workbooks like [1]Sheet!A1
4561
+ */
4562
+ async getExternalLinks() {
4563
+ const externalLinks = [];
4564
+ try {
4565
+ // Read workbook relationships to find external link files
4566
+ const workbookRelsPath = "xl/_rels/workbook.xml.rels";
4567
+ const workbookRelsContent = await this.zipParser.getText(workbookRelsPath);
4568
+ if (!workbookRelsContent) {
4569
+ return externalLinks;
4570
+ }
4571
+ const workbookRels = this.xmlParser.parse(workbookRelsContent);
4572
+ const relationships = get(workbookRels, "Relationships.Relationship");
4573
+ const relationshipsArray = Array.isArray(relationships)
4574
+ ? relationships
4575
+ : relationships
4576
+ ? [relationships]
4577
+ : [];
4578
+ // Find external link relationships
4579
+ const externalLinkRels = relationshipsArray.filter((rel) => rel.a$?.Type ===
4580
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink");
4581
+ // Process each external link
4582
+ for (let i = 0; i < externalLinkRels.length; i++) {
4583
+ const rel = externalLinkRels[i];
4584
+ const target = rel.a$?.Target;
4585
+ if (!target)
4586
+ continue;
4587
+ // Resolve the path
4588
+ const externalLinkPath = target.startsWith("/")
4589
+ ? target.substring(1)
4590
+ : `xl/${target}`;
4591
+ try {
4592
+ // Read the external link XML
4593
+ const xmlContent = await this.zipParser.getText(externalLinkPath);
4594
+ if (!xmlContent)
4595
+ continue;
4596
+ // Parse to extract sheet names and target
4597
+ const parsed = this.xmlParser.parse(xmlContent);
4598
+ const externalBook = get(parsed, "externalLink.externalBook") ||
4599
+ get(parsed, "externalLink.x:externalBook");
4600
+ // Extract sheet names if available
4601
+ const sheetNamesNode = get(externalBook, "sheetNames.sheetName") ||
4602
+ get(externalBook, "x:sheetNames.x:sheetName");
4603
+ const sheetNamesArray = Array.isArray(sheetNamesNode)
4604
+ ? sheetNamesNode
4605
+ : sheetNamesNode
4606
+ ? [sheetNamesNode]
4607
+ : [];
4608
+ const sheetNames = sheetNamesArray
4609
+ .map((sn) => sn?.a$?.val)
4610
+ .filter(Boolean);
4611
+ // Read the relationship file for the external link to get the target path
4612
+ const externalLinkDir = externalLinkPath.substring(0, externalLinkPath.lastIndexOf("/"));
4613
+ const externalLinkName = externalLinkPath.substring(externalLinkPath.lastIndexOf("/") + 1);
4614
+ const relsPath = `${externalLinkDir}/_rels/${externalLinkName}.rels`;
4615
+ let targetPath = "";
4616
+ let relsContent;
4617
+ const relsXml = await this.zipParser.getText(relsPath);
4618
+ if (relsXml) {
4619
+ relsContent = relsXml;
4620
+ const relsData = this.xmlParser.parse(relsXml);
4621
+ const extRels = get(relsData, "Relationships.Relationship");
4622
+ const extRelsArray = Array.isArray(extRels)
4623
+ ? extRels
4624
+ : extRels
4625
+ ? [extRels]
4626
+ : [];
4627
+ // Find the external book relationship
4628
+ const bookRel = extRelsArray.find((r) => r.a$?.Type ===
4629
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath");
4630
+ if (bookRel?.a$?.Target) {
4631
+ targetPath = bookRel.a$.Target;
4632
+ }
4633
+ }
4634
+ // The link index is 1-based (matches [1], [2], etc. in formulas)
4635
+ const linkIndex = i + 1;
4636
+ externalLinks.push({
4637
+ linkIndex,
4638
+ target: targetPath,
4639
+ sheetNames: sheetNames.length > 0 ? sheetNames : undefined,
4640
+ xmlContent,
4641
+ relsContent,
4642
+ });
4643
+ }
4644
+ catch (err) {
4645
+ console.log(`Error parsing external link ${externalLinkPath}:`, err);
4646
+ }
4647
+ }
4648
+ }
4649
+ catch (err) {
4650
+ console.log("Error getting external links:", err);
4651
+ }
4652
+ return externalLinks;
4653
+ }
4654
+ }