@svgrid/enterprise 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/LICENSE +94 -24
  2. package/README.md +36 -4
  3. package/dist/SvAdvancedFilter.svelte +1 -1
  4. package/dist/SvAlertsManager.svelte +2 -1
  5. package/dist/SvBoard.svelte +2 -1
  6. package/dist/SvExportMenu.svelte +4 -1
  7. package/dist/SvExportMenu.svelte.d.ts +0 -13
  8. package/dist/SvExpressionEditor.svelte +3 -3
  9. package/dist/SvFormulaBar.svelte +94 -20
  10. package/dist/SvFormulaBar.svelte.d.ts +7 -0
  11. package/dist/SvGridAlerts.svelte +49 -9
  12. package/dist/SvGridBoard.svelte +13 -8
  13. package/dist/SvGridBulkEditDrawer.svelte +26 -2
  14. package/dist/SvGridEditPanel.svelte +3 -0
  15. package/dist/SvGridScheduler.svelte +69 -40
  16. package/dist/SvGridSelectionBar.svelte +10 -3
  17. package/dist/SvPivotDesigner.svelte +198 -40
  18. package/dist/SvPivotDesigner.svelte.d.ts +27 -20
  19. package/dist/SvSheet.svelte +4043 -243
  20. package/dist/SvSheet.svelte.d.ts +76 -3
  21. package/dist/SvSheetChartSetup.svelte +151 -0
  22. package/dist/SvSheetChartSetup.svelte.d.ts +18 -0
  23. package/dist/SvSheetComment.svelte +216 -31
  24. package/dist/SvSheetComment.svelte.d.ts +11 -11
  25. package/dist/SvSheetConditionalFormat.svelte +28 -31
  26. package/dist/SvSheetDataValidation.svelte +66 -42
  27. package/dist/SvSheetEditRanges.svelte +217 -0
  28. package/dist/SvSheetEditRanges.svelte.d.ts +15 -0
  29. package/dist/SvSheetErrors.svelte +125 -0
  30. package/dist/SvSheetErrors.svelte.d.ts +20 -0
  31. package/dist/SvSheetEvaluate.svelte +133 -0
  32. package/dist/SvSheetEvaluate.svelte.d.ts +15 -0
  33. package/dist/SvSheetFilterMenu.svelte +137 -34
  34. package/dist/SvSheetFilterMenu.svelte.d.ts +5 -0
  35. package/dist/SvSheetFindReplace.svelte +19 -19
  36. package/dist/SvSheetFormatCells.svelte +125 -81
  37. package/dist/SvSheetGoalSeek.svelte +17 -16
  38. package/dist/SvSheetInsertFunction.svelte +15 -13
  39. package/dist/SvSheetIteration.svelte +81 -0
  40. package/dist/SvSheetIteration.svelte.d.ts +10 -0
  41. package/dist/SvSheetLink.svelte +99 -0
  42. package/dist/SvSheetLink.svelte.d.ts +17 -0
  43. package/dist/SvSheetListPicker.svelte +4 -2
  44. package/dist/SvSheetListPicker.svelte.d.ts +0 -6
  45. package/dist/SvSheetManageRules.svelte +20 -18
  46. package/dist/SvSheetNameManager.svelte +17 -15
  47. package/dist/SvSheetPageSetup.svelte +159 -0
  48. package/dist/SvSheetPageSetup.svelte.d.ts +13 -0
  49. package/dist/SvSheetPasteSpecial.svelte +22 -20
  50. package/dist/SvSheetPivot.svelte +220 -0
  51. package/dist/SvSheetPivot.svelte.d.ts +21 -0
  52. package/dist/SvSheetProtectSheet.svelte +81 -0
  53. package/dist/SvSheetProtectSheet.svelte.d.ts +11 -0
  54. package/dist/SvSheetRemoveDuplicates.svelte +13 -10
  55. package/dist/SvSheetRibbon.svelte +104 -51
  56. package/dist/SvSheetSizeDialog.svelte +4 -2
  57. package/dist/SvSheetSort.svelte +218 -0
  58. package/dist/SvSheetSort.svelte.d.ts +28 -0
  59. package/dist/SvSheetSparklines.svelte +135 -0
  60. package/dist/SvSheetSparklines.svelte.d.ts +14 -0
  61. package/dist/SvSheetTable.svelte +160 -0
  62. package/dist/SvSheetTable.svelte.d.ts +24 -0
  63. package/dist/SvSheetTabs.svelte +161 -31
  64. package/dist/SvSheetTabs.svelte.d.ts +22 -8
  65. package/dist/SvSheetTextToColumns.svelte +20 -20
  66. package/dist/SvSheetValidationAlert.svelte +8 -6
  67. package/dist/advanced-filter-enable.js +1 -1
  68. package/dist/alerts/alert-engine.d.ts +6 -0
  69. package/dist/alerts/alert-engine.js +21 -4
  70. package/dist/alerts/alert-formats.js +2 -0
  71. package/dist/alerts/alert-types.d.ts +1 -1
  72. package/dist/alerts.d.ts +1 -1
  73. package/dist/alerts.js +2 -2
  74. package/dist/board.js +2 -2
  75. package/dist/bulk-edit.d.ts +3 -1
  76. package/dist/bulk-edit.js +1 -1
  77. package/dist/cdn/svgrid-enterprise.svelte-external.js +39827 -16429
  78. package/dist/designer/assets/GridMenus-8gTNssgV.js +8 -0
  79. package/dist/designer/assets/{SvChartMenu-D9hEeHar.js → SvChartMenu-BDoeqGz6.js} +1 -1
  80. package/dist/designer/assets/{SvDateTimePicker-DARvKUL2.js → SvDateTimePicker-DCPg7WGN.js} +1 -1
  81. package/dist/designer/assets/SvGridCellEditor-DMoWJjEI.js +4 -0
  82. package/dist/designer/assets/{SvGridChart-B8j1SSy3.js → SvGridChart-Cy1ATrWh.js} +2 -2
  83. package/dist/designer/assets/SvGridChartBuilder-eQ3mn0L9.js +1 -0
  84. package/dist/designer/assets/{SvGridChartPanel-pT6ITc2P.js → SvGridChartPanel-CATdVc9X.js} +3 -3
  85. package/dist/designer/assets/{SvGridChartView-bg281UA1.js → SvGridChartView-CmsZtCED.js} +1 -1
  86. package/dist/designer/assets/SvGridDropdown-Cjz0H-fI.js +1 -0
  87. package/dist/designer/assets/{SvTabs-UFCMTHXS.js → SvTabs-2mHC6oaz.js} +1 -1
  88. package/dist/designer/assets/cell-formatting-BbubJTVz.js +1 -0
  89. package/dist/designer/assets/{chart-cIPUsbM5.js → chart-Cn7MnNqA.js} +1 -1
  90. package/dist/designer/assets/{chart-panel-messages-DtTxpBUU.js → chart-panel-messages-Dz7PTIqT.js} +1 -1
  91. package/dist/designer/assets/{chart-summary-Czu_5epb.js → chart-summary-BD1qYB2R.js} +1 -1
  92. package/dist/designer/assets/{chart-trend-D5I5VEe8.js → chart-trend-B8xr5LuD.js} +1 -1
  93. package/dist/designer/assets/{dismissable-BO547MH9.js → dismissable-ChWRPkoa.js} +1 -1
  94. package/dist/designer/assets/{export-format-D-eP4b2l.js → export-format-DGckmgKr.js} +1 -1
  95. package/dist/designer/assets/{focus-trap-CAreAhlU.js → focus-trap-V2-6Fy9I.js} +1 -1
  96. package/dist/designer/assets/{index-DwU7CAMm.js → index-BSjv6Qec.js} +317 -315
  97. package/dist/designer/assets/index-_JVFxt02.css +1 -0
  98. package/dist/designer/assets/row-resize-BInFWCpr.js +1 -0
  99. package/dist/designer/assets/server-block-cache-CXJKkJpy.js +1 -0
  100. package/dist/designer/assets/src-DUQ4xE3x.js +58 -0
  101. package/dist/designer/assets/validate-C3B8QKsM.js +1 -0
  102. package/dist/designer/index.html +17 -16
  103. package/dist/export-pdf.d.ts +75 -0
  104. package/dist/export-pdf.js +216 -15
  105. package/dist/export.js +10 -2
  106. package/dist/gantt/SvGridGantt.svelte +2890 -0
  107. package/dist/gantt/SvGridGantt.svelte.d.ts +30 -0
  108. package/dist/gantt/gantt-config.d.ts +73 -0
  109. package/dist/gantt/gantt-config.js +1 -0
  110. package/dist/gantt/gantt-critical-path.d.ts +67 -0
  111. package/dist/gantt/gantt-critical-path.js +207 -0
  112. package/dist/gantt/gantt-model.d.ts +257 -0
  113. package/dist/gantt/gantt-model.js +651 -0
  114. package/dist/gantt/gantt-resources.d.ts +59 -0
  115. package/dist/gantt/gantt-resources.js +71 -0
  116. package/dist/gantt/gantt.d.ts +7 -0
  117. package/dist/gantt/gantt.js +36 -0
  118. package/dist/gantt/index.d.ts +15 -0
  119. package/dist/gantt/index.js +22 -0
  120. package/dist/gantt/timeline-arrows.d.ts +102 -0
  121. package/dist/gantt/timeline-arrows.js +181 -0
  122. package/dist/import.d.ts +2 -2
  123. package/dist/import.js +1 -1
  124. package/dist/index.d.ts +34 -7
  125. package/dist/index.js +44 -7
  126. package/dist/install.js +8 -1
  127. package/dist/license-core.d.ts +17 -1
  128. package/dist/license-core.js +16 -5
  129. package/dist/license.d.ts +18 -2
  130. package/dist/license.js +71 -6
  131. package/dist/node/studio.js +140 -8
  132. package/dist/pivot-enable.js +2 -2
  133. package/dist/scheduler-dependencies.d.ts +35 -0
  134. package/dist/scheduler-dependencies.js +36 -1
  135. package/dist/scheduler.js +2 -2
  136. package/dist/selection-bar.js +2 -2
  137. package/dist/server/SvGroupCell.svelte +213 -0
  138. package/dist/server/SvGroupCell.svelte.d.ts +62 -0
  139. package/dist/server/SvRowGroupPanel.svelte +322 -0
  140. package/dist/server/SvRowGroupPanel.svelte.d.ts +37 -0
  141. package/dist/server/enable.d.ts +17 -0
  142. package/dist/server/enable.js +49 -0
  143. package/dist/server/index.d.ts +22 -0
  144. package/dist/server/index.js +24 -0
  145. package/dist/server/messages.d.ts +45 -0
  146. package/dist/server/messages.js +35 -0
  147. package/dist/server/server-group-model.d.ts +104 -0
  148. package/dist/server/server-group-model.js +272 -0
  149. package/dist/server/server-pivot.d.ts +46 -0
  150. package/dist/server/server-pivot.js +57 -0
  151. package/dist/server/server-row-model.d.ts +547 -0
  152. package/dist/server/server-row-model.js +1551 -0
  153. package/dist/server/server-selection.d.ts +99 -0
  154. package/dist/server/server-selection.js +215 -0
  155. package/dist/server/svgrid-adapter.d.ts +155 -0
  156. package/dist/server/svgrid-adapter.js +202 -0
  157. package/dist/sheet/ast.d.ts +45 -1
  158. package/dist/sheet/ast.js +14 -0
  159. package/dist/sheet/auto-filter.d.ts +58 -4
  160. package/dist/sheet/auto-filter.js +159 -11
  161. package/dist/sheet/autocomplete.d.ts +9 -0
  162. package/dist/sheet/autocomplete.js +90 -4
  163. package/dist/sheet/cell-images.d.ts +39 -0
  164. package/dist/sheet/cell-images.js +90 -0
  165. package/dist/sheet/cell-types.d.ts +58 -0
  166. package/dist/sheet/cell-types.js +96 -0
  167. package/dist/sheet/coerce.d.ts +40 -0
  168. package/dist/sheet/coerce.js +170 -3
  169. package/dist/sheet/comments.d.ts +43 -9
  170. package/dist/sheet/comments.js +85 -27
  171. package/dist/sheet/conditional-formats.d.ts +29 -6
  172. package/dist/sheet/conditional-formats.js +37 -12
  173. package/dist/sheet/csv.d.ts +32 -0
  174. package/dist/sheet/csv.js +158 -0
  175. package/dist/sheet/culture.d.ts +87 -0
  176. package/dist/sheet/culture.js +247 -0
  177. package/dist/sheet/delta.d.ts +102 -0
  178. package/dist/sheet/delta.js +203 -0
  179. package/dist/sheet/deps.d.ts +45 -1
  180. package/dist/sheet/deps.js +77 -2
  181. package/dist/sheet/document.d.ts +106 -4
  182. package/dist/sheet/document.js +204 -3
  183. package/dist/sheet/engine.d.ts +67 -0
  184. package/dist/sheet/engine.js +19 -0
  185. package/dist/sheet/entry.d.ts +8 -4
  186. package/dist/sheet/entry.js +171 -21
  187. package/dist/sheet/error-check.d.ts +49 -0
  188. package/dist/sheet/error-check.js +83 -0
  189. package/dist/sheet/evaluate-steps.d.ts +48 -0
  190. package/dist/sheet/evaluate-steps.js +196 -0
  191. package/dist/sheet/evaluate.d.ts +67 -0
  192. package/dist/sheet/evaluate.js +725 -13
  193. package/dist/sheet/filter-files.d.ts +37 -0
  194. package/dist/sheet/filter-files.js +465 -0
  195. package/dist/sheet/format-store.d.ts +15 -0
  196. package/dist/sheet/format-store.js +11 -1
  197. package/dist/sheet/from-rows.d.ts +14 -0
  198. package/dist/sheet/from-rows.js +50 -0
  199. package/dist/sheet/function-catalog.d.ts +1 -1
  200. package/dist/sheet/function-catalog.js +296 -5
  201. package/dist/sheet/functions.js +363 -91
  202. package/dist/sheet/index.d.ts +35 -8
  203. package/dist/sheet/index.js +35 -8
  204. package/dist/sheet/links.d.ts +57 -0
  205. package/dist/sheet/links.js +174 -0
  206. package/dist/sheet/messages.d.ts +778 -0
  207. package/dist/sheet/messages.js +806 -0
  208. package/dist/sheet/number-format.d.ts +52 -5
  209. package/dist/sheet/number-format.js +465 -68
  210. package/dist/sheet/objects.d.ts +106 -0
  211. package/dist/sheet/objects.js +149 -0
  212. package/dist/sheet/ods-document.d.ts +44 -0
  213. package/dist/sheet/ods-document.js +1275 -0
  214. package/dist/sheet/open-file.d.ts +26 -0
  215. package/dist/sheet/open-file.js +56 -0
  216. package/dist/sheet/outline.d.ts +82 -0
  217. package/dist/sheet/outline.js +228 -0
  218. package/dist/sheet/packs/array.d.ts +16 -0
  219. package/dist/sheet/packs/array.js +172 -0
  220. package/dist/sheet/packs/database.d.ts +42 -0
  221. package/dist/sheet/packs/database.js +142 -0
  222. package/dist/sheet/packs/engineering.d.ts +2 -0
  223. package/dist/sheet/packs/engineering.js +296 -0
  224. package/dist/sheet/packs/financial.d.ts +5 -0
  225. package/dist/sheet/packs/financial.js +220 -0
  226. package/dist/sheet/packs/math-stats.d.ts +2 -0
  227. package/dist/sheet/packs/math-stats.js +372 -0
  228. package/dist/sheet/packs/special.d.ts +44 -0
  229. package/dist/sheet/packs/special.js +187 -0
  230. package/dist/sheet/packs/stats-dist.d.ts +2 -0
  231. package/dist/sheet/packs/stats-dist.js +785 -0
  232. package/dist/sheet/packs/text-date.d.ts +2 -0
  233. package/dist/sheet/packs/text-date.js +332 -0
  234. package/dist/sheet/packs/trig.d.ts +2 -0
  235. package/dist/sheet/packs/trig.js +73 -0
  236. package/dist/sheet/page-setup.d.ts +73 -0
  237. package/dist/sheet/page-setup.js +73 -0
  238. package/dist/sheet/parse.js +42 -10
  239. package/dist/sheet/paste-special.d.ts +22 -0
  240. package/dist/sheet/paste-special.js +14 -5
  241. package/dist/sheet/pivot-range.d.ts +160 -0
  242. package/dist/sheet/pivot-range.js +333 -0
  243. package/dist/sheet/presence.d.ts +73 -0
  244. package/dist/sheet/presence.js +79 -0
  245. package/dist/sheet/print.d.ts +104 -0
  246. package/dist/sheet/print.js +169 -0
  247. package/dist/sheet/protection.d.ts +55 -1
  248. package/dist/sheet/protection.js +55 -2
  249. package/dist/sheet/rects.d.ts +1 -1
  250. package/dist/sheet/refs.d.ts +48 -0
  251. package/dist/sheet/refs.js +155 -8
  252. package/dist/sheet/ribbon-icons.d.ts +1 -1
  253. package/dist/sheet/ribbon-icons.js +186 -0
  254. package/dist/sheet/ribbon.d.ts +1 -1
  255. package/dist/sheet/ribbon.js +324 -23
  256. package/dist/sheet/sheet-kinds.d.ts +74 -0
  257. package/dist/sheet/sheet-kinds.js +91 -0
  258. package/dist/sheet/shortcuts.d.ts +13 -3
  259. package/dist/sheet/shortcuts.js +63 -12
  260. package/dist/sheet/sort.d.ts +51 -0
  261. package/dist/sheet/sort.js +107 -0
  262. package/dist/sheet/sparklines.d.ts +111 -0
  263. package/dist/sheet/sparklines.js +204 -0
  264. package/dist/sheet/table-styles.d.ts +74 -0
  265. package/dist/sheet/table-styles.js +152 -0
  266. package/dist/sheet/tables.d.ts +19 -0
  267. package/dist/sheet/tables.js +31 -0
  268. package/dist/sheet/tokenize.d.ts +18 -1
  269. package/dist/sheet/tokenize.js +60 -9
  270. package/dist/sheet/validation.d.ts +16 -0
  271. package/dist/sheet/validation.js +37 -0
  272. package/dist/sheet/workbook.d.ts +148 -3
  273. package/dist/sheet/workbook.js +742 -31
  274. package/dist/sheet/xls-cfb.d.ts +32 -0
  275. package/dist/sheet/xls-cfb.js +290 -0
  276. package/dist/sheet/xls-document.d.ts +10 -0
  277. package/dist/sheet/xls-document.js +1243 -0
  278. package/dist/sheet/xls-formula.d.ts +40 -0
  279. package/dist/sheet/xls-formula.js +569 -0
  280. package/dist/sheet/xlsx-document.d.ts +108 -0
  281. package/dist/sheet/xlsx-document.js +1849 -0
  282. package/dist/sheet/xlsx-drawing.d.ts +99 -0
  283. package/dist/sheet/xlsx-drawing.js +420 -0
  284. package/dist/sheet-enable.js +6 -7
  285. package/dist/sheet-text.d.ts +7 -0
  286. package/dist/sheet-text.js +29 -0
  287. package/dist/sources/auth-supabase.js +2 -2
  288. package/dist/sources/realtime-supabase.js +2 -2
  289. package/dist/sources/rest.d.ts +6 -2
  290. package/dist/sources/rest.js +42 -7
  291. package/dist/sources/supabase.d.ts +10 -1
  292. package/dist/sources/supabase.js +73 -40
  293. package/dist/studio/emit-project.js +14 -1
  294. package/dist/studio/index.js +1 -1
  295. package/dist/studio/project.d.ts +11 -2
  296. package/dist/studio/project.js +5 -2
  297. package/dist/studio/ui-components.generated.js +107 -2
  298. package/dist/sveltekit/in-memory.d.ts +2 -2
  299. package/dist/sveltekit/in-memory.js +125 -4
  300. package/dist/sveltekit/index.d.ts +1 -1
  301. package/dist/sveltekit/index.js +1 -1
  302. package/dist/sveltekit/query-plan.d.ts +50 -4
  303. package/dist/sveltekit/query-plan.js +25 -3
  304. package/dist/sveltekit/sql-source.d.ts +6 -0
  305. package/dist/sveltekit/sql-source.js +34 -6
  306. package/dist/sveltekit/sql.d.ts +31 -0
  307. package/dist/sveltekit/sql.js +135 -25
  308. package/dist/sveltekit/transport.d.ts +10 -2
  309. package/dist/sveltekit/transport.js +30 -6
  310. package/dist/upgrade-prompt.d.ts +1 -1
  311. package/dist/version.d.ts +1 -1
  312. package/dist/version.js +1 -1
  313. package/dist/watermark.js +35 -2
  314. package/dist/wc/GridMenus-By9vPuq0.js +1088 -0
  315. package/dist/wc/SvChartMenu-CQNrQGT2.js +308 -0
  316. package/dist/wc/SvDateRangeInput-DR6m7bSM.js +199 -0
  317. package/dist/wc/SvDateTimePicker-D-vRnaMo.js +771 -0
  318. package/dist/wc/SvField-B_kurx_P.js +149 -0
  319. package/dist/wc/SvGrid.helpers-BdVDtjUm.js +135 -0
  320. package/dist/wc/SvGridCellEditor-Bnf1Kr5C.js +560 -0
  321. package/dist/wc/SvGridChart-BqUJh2pG.js +3572 -0
  322. package/dist/wc/SvGridChartBuilder-CGycC5cn.js +988 -0
  323. package/dist/wc/SvGridChartPanel-CRnizsHD.js +1746 -0
  324. package/dist/wc/SvGridChartView-D1pa_xMy.js +47 -0
  325. package/dist/wc/SvGridDropdown-45WsObLL.js +259 -0
  326. package/dist/wc/SvModal-DrfOYYyX.js +189 -0
  327. package/dist/wc/cell-formatting-CBfgahv1.js +86 -0
  328. package/dist/wc/chart-6OXlybVT.js +4341 -0
  329. package/dist/wc/chart-export-CeSADGfi.js +135 -0
  330. package/dist/wc/chart-export-pdf-BGko7MER.js +104 -0
  331. package/dist/wc/chart-summary-2m3ia1-n.js +137 -0
  332. package/dist/wc/chart-trend-CQOQtTMl.js +50 -0
  333. package/dist/wc/chart-validate-CD6ptiwg.js +180 -0
  334. package/dist/wc/client-DQIzjDBG.js +3064 -0
  335. package/dist/wc/column-resize-DUPgynvc.js +101 -0
  336. package/dist/wc/date-format-DS-z_h9v.js +1130 -0
  337. package/dist/wc/dismissable-CQ6XFXsk.js +45 -0
  338. package/dist/wc/editor-contract-peBDxir_.js +22 -0
  339. package/dist/wc/editor-registry-DxGKqcvx.js +24 -0
  340. package/dist/wc/focus-trap-Dbb2tJxb.js +65 -0
  341. package/dist/wc/list-option-DrRyivWp.js +82 -0
  342. package/dist/wc/popover-WsGlvtQv.js +69 -0
  343. package/dist/wc/positioning-BiKLfu1E.js +112 -0
  344. package/dist/wc/react.d.ts +41 -0
  345. package/dist/wc/react.js +90 -0
  346. package/dist/wc/rolldown-runtime-Dy4uBu1J.js +11 -0
  347. package/dist/wc/row-drag-touch-CycRR65n.js +93 -0
  348. package/dist/wc/row-resize-limG_N1z.js +111 -0
  349. package/dist/wc/sv-sheet-element.d.ts +97 -0
  350. package/dist/wc/sv-sheet-element.js +44213 -0
  351. package/dist/wc/svelte-virtualizer.svelte-CXtQy2TQ.js +530 -0
  352. package/dist/wc/validate-BqDthkNr.js +88 -0
  353. package/dist/wc/vue.d.ts +31 -0
  354. package/dist/wc/vue.js +75 -0
  355. package/package.json +30 -5
  356. package/dist/designer/assets/GridMenus-BlsvL_aU.js +0 -8
  357. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +0 -4
  358. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +0 -1
  359. package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +0 -1
  360. package/dist/designer/assets/cell-formatting-wr5wm7US.js +0 -1
  361. package/dist/designer/assets/index-CMFgAoVV.css +0 -1
  362. package/dist/designer/assets/row-resize-DLktcqUK.js +0 -1
  363. package/dist/designer/assets/src-EdI3Xqds.js +0 -58
  364. package/dist/designer/assets/validate-h-aslRV9.js +0 -1
@@ -0,0 +1,1849 @@
1
+ import { formatKeyAt } from './format-store.js';
2
+ import { colToLetters, lettersToCol, parseA1 } from './address.js';
3
+ import { emptyOutline, hiddenByOutline, isOutlined, outlineLevel } from './outline.js';
4
+ import { translateFormula } from './refs.js';
5
+ import { isError, typedError } from './ast.js';
6
+ import { listComments, isThreaded } from './comments.js';
7
+ import { isSafeLinkTarget, listLinks, parseLinkTarget } from './links.js';
8
+ import { PROTECTION_PERMISSIONS, newEditRangeId } from './protection.js';
9
+ import { PAPER_SIZES, defaultPageSetup } from './page-setup.js';
10
+ import { cleanIteration, DEFAULT_ITERATION, TEXT_PREFIX } from './workbook.js';
11
+ import { findTableStyle, DEFAULT_TABLE_STYLE, NO_TABLE_STYLE } from './table-styles.js';
12
+ import { drawingPartsFor, objectsFromDrawing, rectOfRef, REL_DRAWING } from './xlsx-drawing.js';
13
+ import { objectId } from './objects.js';
14
+ import { sparklineLines, sparklineId } from './sparklines.js';
15
+ import { autoFilterXlsx, filteredRows, filtersFromXlsx } from './filter-files.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Shared pieces
18
+ // ---------------------------------------------------------------------------
19
+ const XML_ESCAPE = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' };
20
+ // eslint-disable-next-line no-control-regex
21
+ const INVALID_XML = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g;
22
+ const esc = (s) => s.replace(INVALID_XML, '').replace(/[&<>"']/g, (c) => XML_ESCAPE[c]);
23
+ const NS_MAIN = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
24
+ const NS_REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
25
+ /** Excel's 2009 extensions, which is where a sparkline lives. */
26
+ const NS_X14 = 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main';
27
+ const NS_XM = 'http://schemas.microsoft.com/office/excel/2006/main';
28
+ const NS_PKG_REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
29
+ const REL_WORKSHEET = `${NS_REL}/worksheet`;
30
+ const REL_STYLES = `${NS_REL}/styles`;
31
+ const REL_SHARED = `${NS_REL}/sharedStrings`;
32
+ const REL_COMMENTS = `${NS_REL}/comments`;
33
+ const REL_VML = `${NS_REL}/vmlDrawing`;
34
+ const NS_THREADS = 'http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments';
35
+ const REL_THREADS = 'http://schemas.microsoft.com/office/2017/10/relationships/threadedComment';
36
+ const REL_PERSONS = 'http://schemas.microsoft.com/office/2017/10/relationships/person';
37
+ const REL_METADATA = `${NS_REL}/sheetMetadata`;
38
+ const REL_HYPERLINK = `${NS_REL}/hyperlink`;
39
+ const REL_TABLE = `${NS_REL}/table`;
40
+ /** What Excel puts in the legacy note of a threaded comment, for readers that predate threads. */
41
+ const THREAD_LEGACY_HEAD = '[Threaded comment]\n\nYour version of Excel allows you to read this threaded comment; however, any edits to it will get removed if the file is opened in a newer version of Excel. Learn more: https://go.microsoft.com/fwlink/?linkid=870924\n\n';
42
+ const XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
43
+ const EXCEL_EPOCH_MS = Date.UTC(1899, 11, 30);
44
+ const DAY_MS = 86400000;
45
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
46
+ export const isoToSerial = (iso) => {
47
+ const m = ISO_DATE.exec(iso);
48
+ if (!m)
49
+ return null;
50
+ return Math.round((Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) - EXCEL_EPOCH_MS) / DAY_MS);
51
+ };
52
+ export const serialToIso = (serial) => new Date(EXCEL_EPOCH_MS + Math.round(serial) * DAY_MS).toISOString().slice(0, 10);
53
+ /** `#rrggbb` (or a bare rrggbb) as OOXML ARGB; null for any other colour. */
54
+ function argb(colour) {
55
+ if (!colour)
56
+ return null;
57
+ const m = /^#?([0-9a-f]{6})$/i.exec(colour.trim());
58
+ if (m)
59
+ return `FF${m[1].toUpperCase()}`;
60
+ const short = /^#([0-9a-f]{3})$/i.exec(colour.trim());
61
+ if (short)
62
+ return `FF${short[1].split('').map((c) => c + c).join('').toUpperCase()}`;
63
+ return null;
64
+ }
65
+ const fromArgb = (rgb) => rgb && /^[0-9a-f]{8}$/i.test(rgb) ? `#${rgb.slice(2).toLowerCase()}` : rgb && /^[0-9a-f]{6}$/i.test(rgb) ? `#${rgb.toLowerCase()}` : undefined;
66
+ /** Excel's built-in number formats, the ones a file names by id alone. */
67
+ export const BUILTIN_NUMFMT = {
68
+ 0: 'General', 1: '0', 2: '0.00', 3: '#,##0', 4: '#,##0.00', 9: '0%', 10: '0.00%', 11: '0.00E+00', 12: '# ?/?', 13: '# ??/??',
69
+ 14: 'm/d/yyyy', 15: 'd-mmm-yy', 16: 'd-mmm', 17: 'mmm-yy', 18: 'h:mm AM/PM', 19: 'h:mm:ss AM/PM', 20: 'h:mm', 21: 'h:mm:ss',
70
+ 22: 'm/d/yyyy h:mm', 37: '#,##0 ;(#,##0)', 38: '#,##0 ;[Red](#,##0)', 39: '#,##0.00;(#,##0.00)', 40: '#,##0.00;[Red](#,##0.00)',
71
+ 45: 'mm:ss', 46: '[h]:mm:ss', 47: 'mmss.0', 48: '##0.0E+0', 49: '@',
72
+ };
73
+ /** Is this a date pattern: date tokens outside quotes, no digit placeholders. */
74
+ export function isDateFormat(fmt) {
75
+ if (!fmt)
76
+ return false;
77
+ const bare = fmt.replace(/"[^"]*"/g, '').replace(/\[[^\]]*\]/g, '');
78
+ return /[ymd]/i.test(bare) && !/[#0?]/.test(bare) && !/^General$/i.test(fmt);
79
+ }
80
+ /**
81
+ * A date format that shows a day (a year, month or day token), as against
82
+ * a time alone. The reader turns a whole serial under one of these into the
83
+ * sheet's yyyy-mm-dd text; under h:mm the fraction stays a number, which
84
+ * the sheet shows as the time it is.
85
+ */
86
+ export function isDayFormat(fmt) {
87
+ if (!isDateFormat(fmt))
88
+ return false;
89
+ const bare = fmt.replace(/"[^"]*"/g, '').replace(/\[[^\]]*\]/g, '');
90
+ if (/[yd]/i.test(bare))
91
+ return true;
92
+ // An m beside an h or an s is minutes; any other is a month.
93
+ return /m/i.test(bare.replace(/h+:m+|m+:s+/gi, ''));
94
+ }
95
+ /** `$A$1:$D$10`, the way a print name spells a rectangle. */
96
+ const absRef = ([r1, c1, r2, c2]) => `$${colToLetters(c1)}$${r1 + 1}:$${colToLetters(c2)}$${r2 + 1}`;
97
+ const rectRef = ([r1, c1, r2, c2]) => r1 === r2 && c1 === c2 ? `${colToLetters(c1)}${r1 + 1}` : `${colToLetters(c1)}${r1 + 1}:${colToLetters(c2)}${r2 + 1}`;
98
+ function refRect(ref) {
99
+ const [a, b] = ref.split(':');
100
+ const from = parseA1(a ?? '');
101
+ if (!from || from.row === null)
102
+ return null;
103
+ const to = b ? parseA1(b) : from;
104
+ if (!to || to.row === null)
105
+ return null;
106
+ return [Math.min(from.row, to.row), Math.min(from.col, to.col), Math.max(from.row, to.row), Math.max(from.col, to.col)];
107
+ }
108
+ const sqrefRects = (sqref) => sqref.split(/\s+/).map(refRect).filter((r) => r !== null);
109
+ /** Excel column width units from pixels, and back. Excel's unit is the
110
+ * width of a digit in the default font, close to seven pixels. */
111
+ export const pxToWidth = (px) => Math.max(0, Math.round((px / 7) * 100) / 100);
112
+ export const widthToPx = (w) => Math.round(w * 7);
113
+ export const pxToPt = (px) => Math.round(px * 0.75 * 100) / 100;
114
+ export const ptToPx = (pt) => Math.round(pt / 0.75);
115
+ const formatKey = formatKeyAt;
116
+ function splitFormatKey(key) {
117
+ const gap = key.indexOf(' ');
118
+ if (gap < 0)
119
+ return null;
120
+ const rowId = decodeURIComponent(key.slice(0, gap));
121
+ const columnId = decodeURIComponent(key.slice(gap + 1));
122
+ const row = Number(rowId.slice(1));
123
+ const col = lettersToCol(columnId);
124
+ return rowId.startsWith('r') && Number.isInteger(row) && col >= 0 ? { row, col } : null;
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Writer
128
+ // ---------------------------------------------------------------------------
129
+ /** The styles part: fonts, fills, borders and cell xfs deduplicated, and
130
+ * the differential formats (dxf) conditional formatting points at. */
131
+ function styleRegistry() {
132
+ const numFmts = new Map();
133
+ let nextNumFmt = 164;
134
+ const numFmtId = (code) => {
135
+ if (!code || /^General$/i.test(code))
136
+ return 0;
137
+ const known = Object.entries(BUILTIN_NUMFMT).find(([, c]) => c === code);
138
+ if (known)
139
+ return Number(known[0]);
140
+ let id = numFmts.get(code);
141
+ if (id === undefined) {
142
+ id = nextNumFmt++;
143
+ numFmts.set(code, id);
144
+ }
145
+ return id;
146
+ };
147
+ const fonts = ['<font><sz val="11"/><name val="Calibri"/></font>'];
148
+ const fontIds = new Map([[fonts[0], 0]]);
149
+ const fontXml = (e) => {
150
+ const colour = argb(e.color);
151
+ return '<font>'
152
+ + (e.bold ? '<b/>' : '') + (e.italic ? '<i/>' : '') + (e.underline ? '<u/>' : '') + (e.strike ? '<strike/>' : '')
153
+ + `<sz val="${e.fontSize ? Math.round(e.fontSize * 0.75 * 100) / 100 : 11}"/>`
154
+ + (colour ? `<color rgb="${colour}"/>` : '')
155
+ + `<name val="${esc(e.fontFamily || 'Calibri')}"/>`
156
+ + '</font>';
157
+ };
158
+ const fills = ['<fill><patternFill patternType="none"/></fill>', '<fill><patternFill patternType="gray125"/></fill>'];
159
+ const fillIds = new Map();
160
+ const fillId = (colour) => {
161
+ const c = argb(colour);
162
+ if (!c)
163
+ return 0;
164
+ let id = fillIds.get(c);
165
+ if (id === undefined) {
166
+ id = fills.length;
167
+ fills.push(`<fill><patternFill patternType="solid"><fgColor rgb="${c}"/><bgColor indexed="64"/></patternFill></fill>`);
168
+ fillIds.set(c, id);
169
+ }
170
+ return id;
171
+ };
172
+ const borders = ['<border><left/><right/><top/><bottom/><diagonal/></border>'];
173
+ const borderIds = new Map([[borders[0], 0]]);
174
+ const side = (name, spec) => {
175
+ if (!spec)
176
+ return `<${name}/>`;
177
+ const style = spec.style === 'dashed' ? 'dashed' : spec.style === 'dotted' ? 'dotted' : spec.style === 'double' ? 'double' : (spec.width ?? 2) >= 3 ? 'thick' : (spec.width ?? 2) >= 2 ? 'medium' : 'thin';
178
+ const colour = argb(spec.color);
179
+ return `<${name} style="${style}">${colour ? `<color rgb="${colour}"/>` : ''}</${name}>`;
180
+ };
181
+ const borderXml = (b) => b ? `<border>${side('left', b.left)}${side('right', b.right)}${side('top', b.top)}${side('bottom', b.bottom)}<diagonal/></border>` : borders[0];
182
+ const intern = (list, ids, xml) => {
183
+ let id = ids.get(xml);
184
+ if (id === undefined) {
185
+ id = list.length;
186
+ list.push(xml);
187
+ ids.set(xml, id);
188
+ }
189
+ return id;
190
+ };
191
+ const xfs = ['<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'];
192
+ const xfIds = new Map([[xfs[0], 0]]);
193
+ const xfId = (e) => {
194
+ const nf = numFmtId(e.numFmt);
195
+ const font = intern(fonts, fontIds, fontXml(e));
196
+ const fill = fillId(e.fill);
197
+ const border = intern(borders, borderIds, borderXml(e.border));
198
+ const alignParts = [];
199
+ if (e.align)
200
+ alignParts.push(`horizontal="${e.align}"`);
201
+ if (e.valign)
202
+ alignParts.push(`vertical="${e.valign}"`);
203
+ if (e.wrap)
204
+ alignParts.push('wrapText="1"');
205
+ if (e.indent)
206
+ alignParts.push(`indent="${e.indent}"`);
207
+ const align = alignParts.length ? `<alignment ${alignParts.join(' ')}/>` : '';
208
+ const protection = e.locked === false ? '<protection locked="0"/>' : '';
209
+ if (!nf && !font && !fill && !border && !align && !protection)
210
+ return 0;
211
+ const xml = `<xf numFmtId="${nf}" fontId="${font}" fillId="${fill}" borderId="${border}" xfId="0"`
212
+ + (nf ? ' applyNumberFormat="1"' : '') + (font ? ' applyFont="1"' : '') + (fill ? ' applyFill="1"' : '')
213
+ + (border ? ' applyBorder="1"' : '') + (align ? ' applyAlignment="1"' : '') + (protection ? ' applyProtection="1"' : '')
214
+ + `>${align}${protection}</xf>`;
215
+ return intern(xfs, xfIds, xml);
216
+ };
217
+ const dxfs = [];
218
+ const dxfId = (style) => {
219
+ const colour = argb(style.color);
220
+ const fill = argb(style.fill);
221
+ const font = style.bold || style.italic || style.underline || style.strike || colour
222
+ ? `<font>${style.bold ? '<b/>' : ''}${style.italic ? '<i/>' : ''}${style.underline ? '<u/>' : ''}${style.strike ? '<strike/>' : ''}${colour ? `<color rgb="${colour}"/>` : ''}</font>`
223
+ : '';
224
+ const nf = style.numFmt ? `<numFmt numFmtId="${numFmtId(style.numFmt)}" formatCode="${esc(style.numFmt)}"/>` : '';
225
+ const fillXml = fill ? `<fill><patternFill><bgColor rgb="${fill}"/></patternFill></fill>` : '';
226
+ dxfs.push(`<dxf>${font}${nf}${fillXml}</dxf>`);
227
+ return dxfs.length - 1;
228
+ };
229
+ const build = () => XML_HEAD + `<styleSheet xmlns="${NS_MAIN}">`
230
+ + (numFmts.size ? `<numFmts count="${numFmts.size}">${[...numFmts].map(([code, id]) => `<numFmt numFmtId="${id}" formatCode="${esc(code)}"/>`).join('')}</numFmts>` : '')
231
+ + `<fonts count="${fonts.length}">${fonts.join('')}</fonts>`
232
+ + `<fills count="${fills.length}">${fills.join('')}</fills>`
233
+ + `<borders count="${borders.length}">${borders.join('')}</borders>`
234
+ + '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
235
+ + `<cellXfs count="${xfs.length}">${xfs.join('')}</cellXfs>`
236
+ + '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
237
+ + `<dxfs count="${dxfs.length}">${dxfs.join('')}</dxfs>`
238
+ + '</styleSheet>';
239
+ return { xfId, dxfId, build };
240
+ }
241
+ /** A string cell's text as the workbook stores it. Text that would be read
242
+ * back as a number, a date, a boolean or a formula takes Excel's apostrophe,
243
+ * which says "this is text" and is not part of the value. */
244
+ export function asText(text) {
245
+ if (text === '')
246
+ return '';
247
+ const upper = text.trim().toUpperCase();
248
+ const numberish = text.trim() !== '' && Number.isFinite(Number(text));
249
+ const plain = !numberish && upper !== 'TRUE' && upper !== 'FALSE'
250
+ && isoToSerial(text.trim()) === null
251
+ && !text.startsWith('=') && !text.startsWith("'") && !typedError(text.trim());
252
+ return plain ? text : `'${text}`;
253
+ }
254
+ /** A cell's `<c>` element: a formula with its cached value, or a literal. */
255
+ function cellXml(ref, raw, value, s, spill, inTable) {
256
+ const sAttr = s ? ` s="${s}"` : '';
257
+ const text = raw.trim();
258
+ if (text === '' && spill === 'covered') {
259
+ // A cell a dynamic array spills into: the value Excel caches there, no formula.
260
+ if (isError(value))
261
+ return `<c r="${ref}"${sAttr} t="e"><v>${esc(value.error)}</v></c>`;
262
+ if (typeof value === 'boolean')
263
+ return `<c r="${ref}"${sAttr} t="b"><v>${value ? 1 : 0}</v></c>`;
264
+ if (typeof value === 'number')
265
+ return `<c r="${ref}"${sAttr}><v>${Number.isFinite(value) ? value : 0}</v></c>`;
266
+ if (value === '')
267
+ return s ? `<c r="${ref}"${sAttr}/>` : '';
268
+ return `<c r="${ref}"${sAttr} t="str"><v>${esc(String(value))}</v></c>`;
269
+ }
270
+ if (text === '')
271
+ return s ? `<c r="${ref}"${sAttr}/>` : '';
272
+ if (text.startsWith('=')) {
273
+ // A dynamic array formula is an array formula over its spill with the
274
+ // metadata flag (`cm`) that tells Excel it spills rather than being an
275
+ // old-style CSE array.
276
+ const body = esc(xlsxFormula(text.slice(1), inTable));
277
+ const f = spill && spill !== 'covered' ? `<f t="array" ref="${spill.ref}">${body}</f>` : `<f>${body}</f>`;
278
+ const cm = spill && spill !== 'covered' ? ' cm="1"' : '';
279
+ if (isError(value))
280
+ return `<c r="${ref}"${sAttr}${cm} t="e">${f}<v>${esc(value.error)}</v></c>`;
281
+ if (typeof value === 'boolean')
282
+ return `<c r="${ref}"${sAttr}${cm} t="b">${f}<v>${value ? 1 : 0}</v></c>`;
283
+ if (typeof value === 'number')
284
+ return `<c r="${ref}"${sAttr}${cm}>${f}<v>${Number.isFinite(value) ? value : 0}</v></c>`;
285
+ return `<c r="${ref}"${sAttr}${cm} t="str">${f}<v>${esc(String(value))}</v></c>`;
286
+ }
287
+ // Excel's text prefix is not part of the value: '007 is written as the
288
+ // string 007, which is what the file carries and what Excel shows.
289
+ if (text.startsWith("'")) {
290
+ return `<c r="${ref}"${sAttr} t="inlineStr"><is><t xml:space="preserve">${esc(text.slice(1))}</t></is></c>`;
291
+ }
292
+ // A typed error code IS the error, as it is in Excel, so it goes out as
293
+ // one rather than as the text that spells it.
294
+ if (isError(value) && text.startsWith('#'))
295
+ return `<c r="${ref}"${sAttr} t="e"><v>${esc(value.error)}</v></c>`;
296
+ const serial = isoToSerial(text);
297
+ if (serial !== null)
298
+ return `<c r="${ref}"${sAttr}><v>${serial}</v></c>`;
299
+ // A number under a date or time format is a serial to Excel, which shows
300
+ // it through the format: 0.4375 under h:mm is 10:30 there as here.
301
+ if (typeof value === 'number')
302
+ return `<c r="${ref}"${sAttr}><v>${value}</v></c>`;
303
+ if (typeof value === 'boolean')
304
+ return `<c r="${ref}"${sAttr} t="b"><v>${value ? 1 : 0}</v></c>`;
305
+ return `<c r="${ref}"${sAttr} t="inlineStr"><is><t xml:space="preserve">${esc(text)}</t></is></c>`;
306
+ }
307
+ /** A bound or source as the file spells it: no leading `=`, a literal
308
+ * string in quotes, a date as its serial. */
309
+ function validationFormula(text, allow) {
310
+ if (text === undefined || text.trim() === '')
311
+ return null;
312
+ const t = text.trim();
313
+ if (t.startsWith('='))
314
+ return t.slice(1);
315
+ if (allow === 'list')
316
+ return `"${t.replace(/"/g, '""')}"`;
317
+ if (allow === 'date') {
318
+ const serial = isoToSerial(t);
319
+ if (serial !== null)
320
+ return String(serial);
321
+ }
322
+ if (allow === 'custom')
323
+ return t;
324
+ return Number.isFinite(Number(t)) ? t : `"${t.replace(/"/g, '""')}"`;
325
+ }
326
+ const VALIDATION_TYPE = {
327
+ any: null, whole: 'whole', decimal: 'decimal', list: 'list', date: 'date', textLength: 'textLength', custom: 'custom',
328
+ };
329
+ const VALIDATION_OP = {
330
+ between: 'between', notBetween: 'notBetween', equal: 'equal', notEqual: 'notEqual',
331
+ greater: 'greaterThan', less: 'lessThan', greaterOrEqual: 'greaterThanOrEqual', lessOrEqual: 'lessThanOrEqual',
332
+ };
333
+ const CF_OP = {
334
+ greater: 'greaterThan', less: 'lessThan', between: 'between', notBetween: 'notBetween',
335
+ equal: 'equal', notEqual: 'notEqual', greaterOrEqual: 'greaterThanOrEqual', lessOrEqual: 'lessThanOrEqual',
336
+ };
337
+ const CF_TEXT = { contains: 'containsText', notContains: 'notContainsText', beginsWith: 'beginsWith', endsWith: 'endsWith' };
338
+ const CF_ICONS = { arrows: '3Arrows', traffic: '3TrafficLights1', flags: '3Flags', symbols: '3Symbols2' };
339
+ function cfFormula(text) {
340
+ const t = text.trim();
341
+ if (t.startsWith('='))
342
+ return t.slice(1);
343
+ return Number.isFinite(Number(t)) && t !== '' ? t : `"${t.replace(/"/g, '""')}"`;
344
+ }
345
+ /**
346
+ * One `<cfRule>`, or null when the rule is not one this writer can spell.
347
+ *
348
+ * Null rather than a half-written element: a rule from a newer schema, or
349
+ * one a consumer built by hand with an operator that is not Excel's, used to
350
+ * put the text `undefined` inside `<conditionalFormatting>`, and a file with
351
+ * that in it is one Excel offers to repair. Skipping the rule loses a colour;
352
+ * writing it loses the workbook.
353
+ */
354
+ function cfRuleXml(rule, priority, dxf, anchor) {
355
+ const stop = rule.stopIfTrue ? ' stopIfTrue="1"' : '';
356
+ const styled = 'style' in rule ? ` dxfId="${dxf(rule.style)}"` : '';
357
+ const open = (attrs) => `<cfRule ${attrs}${styled} priority="${priority}"${stop}>`;
358
+ switch (rule.kind) {
359
+ case 'cellIs': {
360
+ if (!CF_OP[rule.operator])
361
+ return null;
362
+ const formulas = `<formula>${esc(cfFormula(rule.value1))}</formula>`
363
+ + (rule.value2 !== undefined && (rule.operator === 'between' || rule.operator === 'notBetween') ? `<formula>${esc(cfFormula(rule.value2))}</formula>` : '');
364
+ return `${open(`type="cellIs" operator="${CF_OP[rule.operator]}"`)}${formulas}</cfRule>`;
365
+ }
366
+ case 'text': {
367
+ const type = CF_TEXT[rule.match];
368
+ if (!type)
369
+ return null;
370
+ const quoted = `"${rule.value.replace(/"/g, '""')}"`;
371
+ const search = `SEARCH(${quoted},${anchor})`;
372
+ const formula = rule.match === 'contains' ? `NOT(ISERROR(${search}))`
373
+ : rule.match === 'notContains' ? `ISERROR(${search})`
374
+ : rule.match === 'beginsWith' ? `LEFT(${anchor},${rule.value.length})=${quoted}`
375
+ : `RIGHT(${anchor},${rule.value.length})=${quoted}`;
376
+ return `${open(`type="${type}" operator="${type}" text="${esc(rule.value)}"`)}<formula>${esc(formula)}</formula></cfRule>`;
377
+ }
378
+ case 'duplicates':
379
+ return `${open(`type="${rule.unique ? 'uniqueValues' : 'duplicateValues'}"`)}</cfRule>`;
380
+ case 'topBottom':
381
+ return `${open(`type="top10" rank="${rule.rank}"${rule.top ? '' : ' bottom="1"'}${rule.percent ? ' percent="1"' : ''}`)}</cfRule>`;
382
+ case 'average':
383
+ return `${open(`type="aboveAverage"${rule.above ? '' : ' aboveAverage="0"'}`)}</cfRule>`;
384
+ case 'formula':
385
+ return `${open('type="expression"')}<formula>${esc(rule.formula.replace(/^=/, ''))}</formula></cfRule>`;
386
+ case 'dataBar': {
387
+ // The negative colour is an extension Excel keeps in x14; the plain
388
+ // part carries the bar colour, and Excel draws negatives in red by
389
+ // itself, as this reader assumes when the extension is absent.
390
+ return `${open('type="dataBar"')}<dataBar><cfvo type="min"/><cfvo type="max"/><color rgb="${argb(rule.color) ?? 'FF638EC6'}"/></dataBar></cfRule>`;
391
+ }
392
+ case 'colorScale': {
393
+ const cfvo = rule.colors.length === 3 ? '<cfvo type="min"/><cfvo type="percentile" val="50"/><cfvo type="max"/>' : '<cfvo type="min"/><cfvo type="max"/>';
394
+ return `${open('type="colorScale"')}<colorScale>${cfvo}${rule.colors.map((c) => `<color rgb="${argb(c) ?? 'FFFFFFFF'}"/>`).join('')}</colorScale></cfRule>`;
395
+ }
396
+ case 'iconSet':
397
+ return CF_ICONS[rule.set]
398
+ ? `${open('type="iconSet"')}<iconSet iconSet="${CF_ICONS[rule.set]}"><cfvo type="percent" val="0"/><cfvo type="percent" val="33"/><cfvo type="percent" val="67"/></iconSet></cfRule>`
399
+ : null;
400
+ default:
401
+ // A kind this writer does not know: better no rule than a broken file.
402
+ return null;
403
+ }
404
+ }
405
+ /** A stable id in the GUID spelling the threaded parts want. */
406
+ function guid(group, n) {
407
+ return `{${group.toString(16).padStart(8, '0')}-0000-0000-0000-${n.toString(16).padStart(12, '0')}}`.toUpperCase();
408
+ }
409
+ /** An ISO time as Excel spells `dT`: UTC to hundredths, no zone. */
410
+ function threadTime(at) {
411
+ const ms = at ? Date.parse(at) : NaN;
412
+ const d = Number.isFinite(ms) ? new Date(ms) : new Date(0);
413
+ return d.toISOString().replace(/\.(\d{2})\dZ$/, '.$1');
414
+ }
415
+ /** The legacy VML part a comment needs before Excel will show it. */
416
+ function vmlXml(comments) {
417
+ const shapes = comments.map(({ row, col }, i) => `<v:shape id="_x0000_s${1025 + i}" type="#_x0000_t202" style="position:absolute;margin-left:80pt;margin-top:${Math.max(0, row - 1) * 15}pt;width:108pt;height:60pt;z-index:${i + 1};visibility:hidden" fillcolor="#ffffe1" o:insetmode="auto">`
418
+ + '<v:fill color2="#ffffe1"/><v:shadow on="t" color="black" obscured="t"/><v:path o:connecttype="none"/><v:textbox style="mso-direction-alt:auto"/>'
419
+ + `<x:ClientData ObjectType="Note"><x:MoveWithCells/><x:SizeWithCells/><x:Anchor>${col + 1}, 15, ${Math.max(0, row - 1)}, 2, ${col + 3}, 15, ${row + 3}, 16</x:Anchor><x:AutoFill>False</x:AutoFill><x:Row>${row}</x:Row><x:Column>${col}</x:Column></x:ClientData>`
420
+ + '</v:shape>').join('');
421
+ return '<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">'
422
+ + '<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>'
423
+ + '<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>'
424
+ + shapes + '</xml>';
425
+ }
426
+ /**
427
+ * The package parts of an .xlsx holding the whole document, keyed by path,
428
+ * ready to zip. Pure: nothing here touches the DOM or the network.
429
+ */
430
+ export function documentToXlsxParts(doc) {
431
+ const wb = doc.workbook;
432
+ const styles = styleRegistry();
433
+ const parts = {};
434
+ const overrides = [];
435
+ let hasVml = false;
436
+ /** Table parts are numbered across the workbook, not per sheet. */
437
+ let tableCount = 0;
438
+ /** Drawings, charts and media are numbered across the workbook too. */
439
+ let drawingCount = 0;
440
+ let chartCount = 0;
441
+ let mediaCount = 0;
442
+ /** The extensions the media written needs declared in [Content_Types]. */
443
+ const mediaExtensions = new Set();
444
+ let hasSpills = false;
445
+ // The people a thread names, one part for the workbook; an entry
446
+ // without an author is a person with no name.
447
+ const persons = [];
448
+ /** Excel's own names for the print area and the title rows, one per sheet that has them. */
449
+ const printNames = [];
450
+ const personId = (name) => { let i = persons.indexOf(name); if (i < 0) {
451
+ i = persons.length;
452
+ persons.push(name);
453
+ } return guid(0, i + 1); };
454
+ const sheetEntries = wb.sheets.map((name, index) => {
455
+ const n = index + 1;
456
+ const state = doc.get(name);
457
+ const formats = state.formats.serialize();
458
+ const byCell = new Map();
459
+ for (const [key, entry] of Object.entries(formats)) {
460
+ const at = splitFormatKey(key);
461
+ if (at)
462
+ byCell.set(`${at.row},${at.col}`, entry);
463
+ }
464
+ const rows = wb.rowCount(name);
465
+ const cols = wb.colCount(name);
466
+ let maxRow = rows - 1;
467
+ let maxCol = cols - 1;
468
+ for (const key of byCell.keys()) {
469
+ const [r, c] = key.split(',').map(Number);
470
+ maxRow = Math.max(maxRow, r);
471
+ maxCol = Math.max(maxCol, c);
472
+ }
473
+ for (const r of state.heights.keys())
474
+ maxRow = Math.max(maxRow, r);
475
+ for (const r of state.hidden.rows)
476
+ maxRow = Math.max(maxRow, r);
477
+ // Dynamic arrays: the anchors with their rectangles, and every cell they cover.
478
+ const spillAt = new Map();
479
+ for (const spill of wb.spills(name)) {
480
+ const [r1, c1, r2, c2] = spill.rect;
481
+ spillAt.set(`${spill.row},${spill.col}`, { ref: rectRef([r1, c1, r2, c2]) });
482
+ for (let r = r1; r <= r2; r += 1)
483
+ for (let c = c1; c <= c2; c += 1)
484
+ if (r !== spill.row || c !== spill.col)
485
+ spillAt.set(`${r},${c}`, 'covered');
486
+ maxRow = Math.max(maxRow, r2);
487
+ maxCol = Math.max(maxCol, c2);
488
+ }
489
+ if (spillAt.size)
490
+ hasSpills = true;
491
+ // <cols>: widths and hidden columns, one element per column that differs.
492
+ const colXml = [];
493
+ const widthCols = new Set();
494
+ for (const letter of Object.keys(state.widths)) {
495
+ const c = lettersToCol(letter);
496
+ if (c >= 0)
497
+ widthCols.add(c);
498
+ }
499
+ for (const c of state.hidden.cols)
500
+ widthCols.add(c);
501
+ const colOutline = state.outline?.cols ?? emptyOutline();
502
+ const colFolded = hiddenByOutline(colOutline, OUTLINE_SIDE);
503
+ for (const c of Object.keys(colOutline.levels))
504
+ widthCols.add(Number(c));
505
+ for (const c of colOutline.collapsed)
506
+ widthCols.add(c);
507
+ for (const c of colFolded)
508
+ widthCols.add(c);
509
+ for (const c of [...widthCols].sort((a, b) => a - b)) {
510
+ const px = state.widths[colToLetters(c)];
511
+ const hidden = state.hidden.cols.has(c) || colFolded.has(c);
512
+ const level = outlineLevel(colOutline, c);
513
+ const collapsed = colOutline.collapsed.includes(c);
514
+ colXml.push(`<col min="${c + 1}" max="${c + 1}"${px !== undefined ? ` width="${pxToWidth(px)}" customWidth="1"` : ' width="12.5"'}${hidden ? ' hidden="1"' : ''}${level > 0 ? ` outlineLevel="${level}"` : ''}${collapsed ? ' collapsed="1"' : ''}/>`);
515
+ }
516
+ // <sheetData>. The rows the AutoFilter folds away go out hidden, as
517
+ // Excel writes them, so the file opens in the same view; the criteria
518
+ // beside the region are what lets Clear Filter bring them back.
519
+ const filtered = filteredRows(doc, name);
520
+ const rowOutline = state.outline?.rows ?? emptyOutline();
521
+ const rowFolded = hiddenByOutline(rowOutline, OUTLINE_SIDE);
522
+ const rowXml = [];
523
+ for (let r = 0; r <= Math.max(maxRow, ...filtered); r += 1) {
524
+ const cells = [];
525
+ for (let c = 0; c <= maxCol; c += 1) {
526
+ const raw = wb.getRaw(name, r, c);
527
+ const entry = byCell.get(`${r},${c}`);
528
+ const spill = spillAt.get(`${r},${c}`);
529
+ if (raw === '' && !entry && !spill)
530
+ continue;
531
+ // A date is stored here as its ISO text and goes out as the number
532
+ // Excel counts. Without a date format on the cell that number is all
533
+ // Excel has: it shows 46204, and reading the file back hands back
534
+ // 46204 rather than the date. So a date cell that carries no format
535
+ // of its own is given Excel's own.
536
+ const dated = !raw.startsWith(TEXT_PREFIX) && isoToSerial(raw.trim()) !== null && !isDateFormat(entry?.numFmt);
537
+ const look = dated ? { ...(entry ?? {}), numFmt: BUILTIN_NUMFMT[14] } : entry;
538
+ const s = look ? styles.xfId(look) : 0;
539
+ const xml = cellXml(`${colToLetters(c)}${r + 1}`, raw, raw.trim() === '' && !spill ? '' : wb.getValue(name, r, c), s, spill, wb.tables.at(name, r, c)?.name);
540
+ if (xml)
541
+ cells.push(xml);
542
+ }
543
+ const height = state.heights.get(r);
544
+ const hidden = state.hidden.rows.has(r) || filtered.has(r) || rowFolded.has(r);
545
+ const level = outlineLevel(rowOutline, r);
546
+ const collapsed = rowOutline.collapsed.includes(r);
547
+ if (!cells.length && height === undefined && !hidden && level === 0 && !collapsed)
548
+ continue;
549
+ rowXml.push(`<row r="${r + 1}"${height !== undefined ? ` ht="${pxToPt(height)}" customHeight="1"` : ''}${hidden ? ' hidden="1"' : ''}${level > 0 ? ` outlineLevel="${level}"` : ''}${collapsed ? ' collapsed="1"' : ''}>${cells.join('')}</row>`);
550
+ }
551
+ // Frozen panes.
552
+ const { rows: fr, cols: fc } = state.freeze;
553
+ let sheetView = `<sheetView workbookViewId="0"${wb.active === name ? ' tabSelected="1"' : ''}/>`;
554
+ if (fr > 0 || fc > 0) {
555
+ const topLeft = `${colToLetters(fc)}${fr + 1}`;
556
+ const pane = fc > 0 && fr > 0 ? 'bottomRight' : fc > 0 ? 'topRight' : 'bottomLeft';
557
+ sheetView = `<sheetView workbookViewId="0"${wb.active === name ? ' tabSelected="1"' : ''}>`
558
+ + `<pane${fc > 0 ? ` xSplit="${fc}"` : ''}${fr > 0 ? ` ySplit="${fr}"` : ''} topLeftCell="${topLeft}" activePane="${pane}" state="frozen"/>`
559
+ + `<selection pane="${pane}" activeCell="${topLeft}" sqref="${topLeft}"/></sheetView>`;
560
+ }
561
+ // The allow list is spelled the file's way round: an attribute is
562
+ // "locked", so an allowed kind is written as 0 and the rest left to
563
+ // their default of 1.
564
+ const allowed = PROTECTION_PERMISSIONS.filter((key) => state.protection.allow[key]).map((key) => ` ${key}="0"`).join('');
565
+ const protection = state.protected ? `<sheetProtection sheet="1" objects="1" scenarios="1"${allowed}/>` : '';
566
+ const editRanges = state.protection.ranges.length
567
+ ? `<protectedRanges>${state.protection.ranges.map((range) => `<protectedRange sqref="${range.rects.map(rectRef).join(' ')}" name="${esc(range.title)}"/>`).join('')}</protectedRanges>`
568
+ : '';
569
+ const autoFilter = autoFilterXlsx(doc, name);
570
+ const merges = state.merges.length
571
+ ? `<mergeCells count="${state.merges.length}">${state.merges.map((m) => `<mergeCell ref="${rectRef(m)}"/>`).join('')}</mergeCells>`
572
+ : '';
573
+ const cf = state.conditionalFormats.map((rule, i) => {
574
+ if (!rule.rects.length)
575
+ return '';
576
+ const anchor = `${colToLetters(rule.rects[0]?.[1] ?? 0)}${(rule.rects[0]?.[0] ?? 0) + 1}`;
577
+ const body = cfRuleXml(rule, i + 1, styles.dxfId, anchor);
578
+ // An empty <conditionalFormatting> is as invalid as a broken rule, so
579
+ // a rule that cannot be written takes its block with it.
580
+ if (!body)
581
+ return '';
582
+ return `<conditionalFormatting sqref="${rule.rects.map(rectRef).join(' ')}">${body}</conditionalFormatting>`;
583
+ }).join('');
584
+ const validations = state.validation.filter((rule) => VALIDATION_TYPE[rule.allow] !== null);
585
+ const dv = validations.length
586
+ ? `<dataValidations count="${validations.length}">${validations.map((rule) => {
587
+ const f1 = validationFormula(rule.value1, rule.allow);
588
+ const f2 = validationFormula(rule.value2, rule.allow);
589
+ const op = rule.allow === 'list' || rule.allow === 'custom' ? '' : ` operator="${VALIDATION_OP[rule.operator ?? 'between']}"`;
590
+ const alert = ` errorStyle="${rule.alert.style === 'warning' ? 'warning' : 'stop'}" showErrorMessage="1"`
591
+ + (rule.alert.title ? ` errorTitle="${esc(rule.alert.title)}"` : '') + (rule.alert.message ? ` error="${esc(rule.alert.message)}"` : '');
592
+ const prompt = rule.input
593
+ ? ' showInputMessage="1"' + (rule.input.title ? ` promptTitle="${esc(rule.input.title)}"` : '') + (rule.input.message ? ` prompt="${esc(rule.input.message)}"` : '')
594
+ : '';
595
+ const dropdown = rule.allow === 'list' && !rule.inCellDropdown ? ' showDropDown="1"' : '';
596
+ return `<dataValidation type="${VALIDATION_TYPE[rule.allow]}"${op} allowBlank="${rule.ignoreBlank ? 1 : 0}"${dropdown}${alert}${prompt} sqref="${rule.rects.map(rectRef).join(' ')}">`
597
+ + (f1 !== null ? `<formula1>${esc(f1)}</formula1>` : '') + (f2 !== null ? `<formula2>${esc(f2)}</formula2>` : '') + '</dataValidation>';
598
+ }).join('')}</dataValidations>`
599
+ : '';
600
+ // Comments: the legacy part and its VML for every one, and for a
601
+ // thread the threaded part too, with the legacy text Excel itself
602
+ // writes for readers that predate threads.
603
+ const comments = listComments(state.notes);
604
+ let legacyDrawing = '';
605
+ /** The sheet's relationships, from the comment parts and the links. */
606
+ let sheetRels = '';
607
+ if (comments.length) {
608
+ hasVml = true;
609
+ const authors = [''];
610
+ const authorId = (name) => { let i = authors.indexOf(name); if (i < 0) {
611
+ i = authors.length;
612
+ authors.push(name);
613
+ } return i; };
614
+ const threaded = comments.filter((c) => isThreaded(c.thread));
615
+ const threadIds = new Map();
616
+ const legacy = comments.map((c) => {
617
+ const ref = `${colToLetters(c.col)}${c.row + 1}`;
618
+ if (!isThreaded(c.thread))
619
+ return `<comment ref="${ref}" authorId="0"><text><t xml:space="preserve">${esc(c.thread.text)}</t></text></comment>`;
620
+ const id = guid(n, threadIds.size + 1);
621
+ threadIds.set(ref, id);
622
+ const text = THREAD_LEGACY_HEAD + `Comment:\n ${c.thread.text}` + (c.thread.replies ?? []).map((r) => `\nReply:\n ${r.text}`).join('');
623
+ return `<comment ref="${ref}" authorId="${authorId(`tc=${id}`)}"><text><t xml:space="preserve">${esc(text)}</t></text></comment>`;
624
+ }).join('');
625
+ parts[`xl/comments${n}.xml`] = XML_HEAD + `<comments xmlns="${NS_MAIN}"><authors>${authors.map((a) => `<author>${esc(a)}</author>`).join('')}</authors><commentList>${legacy}</commentList></comments>`;
626
+ parts[`xl/drawings/vmlDrawing${n}.vml`] = vmlXml(comments);
627
+ let rels = `<Relationship Id="rId1" Type="${REL_VML}" Target="../drawings/vmlDrawing${n}.vml"/>`
628
+ + `<Relationship Id="rId2" Type="${REL_COMMENTS}" Target="../comments${n}.xml"/>`;
629
+ if (threaded.length) {
630
+ let replyCount = 0;
631
+ const one = (ref, entry, id, parentId, done) => `<threadedComment ref="${ref}" dT="${threadTime(entry.at)}" personId="${personId(entry.author ?? '')}" id="${id}"${parentId ? ` parentId="${parentId}"` : ''}${done ? ' done="1"' : ''}><text>${esc(entry.text)}</text></threadedComment>`;
632
+ const xml = threaded.map((c) => {
633
+ const ref = `${colToLetters(c.col)}${c.row + 1}`;
634
+ const id = threadIds.get(ref);
635
+ return one(ref, c.thread, id, null, Boolean(c.thread.resolved))
636
+ + (c.thread.replies ?? []).map((r) => one(ref, r, guid(n + 1000, (replyCount += 1)), id, false)).join('');
637
+ }).join('');
638
+ parts[`xl/threadedComments/threadedComment${n}.xml`] = XML_HEAD + `<ThreadedComments xmlns="${NS_THREADS}" xmlns:x="${NS_MAIN}">${xml}</ThreadedComments>`;
639
+ rels += `<Relationship Id="rId3" Type="${REL_THREADS}" Target="../threadedComments/threadedComment${n}.xml"/>`;
640
+ overrides.push(`<Override PartName="/xl/threadedComments/threadedComment${n}.xml" ContentType="application/vnd.ms-excel.threadedcomments+xml"/>`);
641
+ }
642
+ sheetRels = rels;
643
+ legacyDrawing = '<legacyDrawing r:id="rId1"/>';
644
+ overrides.push(`<Override PartName="/xl/comments${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>`);
645
+ }
646
+ // Hyperlinks. An external one is a relationship with `TargetMode`
647
+ // External, the way Excel writes it, so the URL lives in the rels part
648
+ // and the cell carries only its id; an address on this workbook is a
649
+ // `location` on the element itself and needs no relationship. The ids
650
+ // start at 4 because the comment parts above take 1 to 3 when they are
651
+ // there.
652
+ // Tables: one part each, a relationship from the sheet, and the
653
+ // `tableParts` list that ties them together. This is what makes Excel
654
+ // show the block as a table rather than as cells that look like one,
655
+ // and what makes a structured reference in the file resolve.
656
+ const sheetTables = (doc.workbook.serialize().tables ?? []).filter((t) => t.sheet.toLowerCase() === name.toLowerCase());
657
+ let tableParts = '';
658
+ if (sheetTables.length) {
659
+ const refs = [];
660
+ sheetTables.forEach((table, i) => {
661
+ const id = tableCount + i + 1;
662
+ const rel = `rIdT${id}`;
663
+ const last = table.lastRow + (table.hasTotals ? 1 : 0);
664
+ const ref = `${colToLetters(table.firstCol)}${table.headerRow + 1}:${colToLetters(table.lastCol)}${last + 1}`;
665
+ const columns = Array.from({ length: table.lastCol - table.firstCol + 1 }, (_, c) => {
666
+ const header = (doc.workbook.getRaw(name, table.headerRow, table.firstCol + c) || `Column${c + 1}`).replace(/^=/, '');
667
+ return `<tableColumn id="${c + 1}" name="${esc(header)}"/>`;
668
+ }).join('');
669
+ parts[`xl/tables/table${id}.xml`] = XML_HEAD
670
+ + `<table xmlns="${NS_MAIN}" id="${id}" name="${esc(table.name)}" displayName="${esc(table.name)}" ref="${ref}"`
671
+ + ` headerRowCount="1" totalsRowCount="${table.hasTotals ? 1 : 0}">`
672
+ + `<autoFilter ref="${colToLetters(table.firstCol)}${table.headerRow + 1}:${colToLetters(table.lastCol)}${table.lastRow + 1}"/>`
673
+ + `<tableColumns count="${table.lastCol - table.firstCol + 1}">${columns}</tableColumns>`
674
+ // The style by Excel's own name, so the table opens there wearing
675
+ // what it wears here. `None` writes no style element at all.
676
+ + (table.style === NO_TABLE_STYLE
677
+ ? ''
678
+ : `<tableStyleInfo name="${esc(findTableStyle(table.style)?.id ?? DEFAULT_TABLE_STYLE)}" showFirstColumn="0" showLastColumn="0" showRowStripes="1" showColumnStripes="0"/>`)
679
+ + '</table>';
680
+ sheetRels += `<Relationship Id="${rel}" Type="${REL_TABLE}" Target="../tables/table${id}.xml"/>`;
681
+ overrides.push(`<Override PartName="/xl/tables/table${id}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"/>`);
682
+ refs.push(`<tablePart r:id="${rel}"/>`);
683
+ });
684
+ tableCount += sheetTables.length;
685
+ tableParts = `<tableParts count="${refs.length}">${refs.join('')}</tableParts>`;
686
+ }
687
+ const links = listLinks(state.links);
688
+ let hyperlinks = '';
689
+ if (links.length) {
690
+ const refs = [];
691
+ links.forEach(({ row, col, link }, i) => {
692
+ const ref = `${colToLetters(col)}${row + 1}`;
693
+ const tip = link.tip ? ` tooltip="${esc(link.tip)}"` : '';
694
+ const target = parseLinkTarget(link.target);
695
+ // A target the shell will not follow is not written into the file
696
+ // either: a workbook that carries one is a workbook that hands it
697
+ // to whoever opens it next.
698
+ if (!target)
699
+ return;
700
+ if (target.kind === 'external') {
701
+ const id = `rId${4 + i}`;
702
+ sheetRels += `<Relationship Id="${id}" Type="${REL_HYPERLINK}" Target="${esc(target.href)}" TargetMode="External"/>`;
703
+ refs.push(`<hyperlink ref="${ref}" r:id="${id}"${tip}/>`);
704
+ }
705
+ else {
706
+ refs.push(`<hyperlink ref="${ref}" location="${esc(link.target)}"${tip}/>`);
707
+ }
708
+ });
709
+ hyperlinks = `<hyperlinks>${refs.join('')}</hyperlinks>`;
710
+ }
711
+ // Sparklines, which Excel keeps in the worksheet's extension list
712
+ // rather than in a part of their own: one group per definition, one
713
+ // entry per cell, each with the line of numbers it reads.
714
+ let sparklineExt = '';
715
+ const groups = state.sparklines ?? [];
716
+ if (groups.length) {
717
+ const quotedSheet = /^[A-Za-z_][A-Za-z0-9_.]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
718
+ const xml = groups.map((group) => {
719
+ const lines = sparklineLines(group).map((line) => {
720
+ const from = `${colToLetters(line.data[1])}${line.data[0] + 1}`;
721
+ const to = `${colToLetters(line.data[3])}${line.data[2] + 1}`;
722
+ return `<x14:sparkline><xm:f>${esc(`${quotedSheet}!${from}:${to}`)}</xm:f>`
723
+ + `<xm:sqref>${colToLetters(line.col)}${line.row + 1}</xm:sqref></x14:sparkline>`;
724
+ }).join('');
725
+ if (!lines)
726
+ return '';
727
+ const type = group.type === 'column' ? ' type="column"' : group.type === 'winloss' ? ' type="stacked"' : '';
728
+ return `<x14:sparklineGroup${type}${group.sameScale ? ' minAxisType="group" maxAxisType="group"' : ''}${group.markers ? ' markers="1"' : ''} displayEmptyCellsAs="gap">`
729
+ + (group.color ? `<x14:colorSeries rgb="${argb(group.color)}"/>` : '')
730
+ + (group.negativeColor ? `<x14:colorNegative rgb="${argb(group.negativeColor)}"/>` : '')
731
+ + `<x14:sparklines>${lines}</x14:sparklines></x14:sparklineGroup>`;
732
+ }).join('');
733
+ if (xml) {
734
+ sparklineExt = '<extLst><ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">'
735
+ + `<x14:sparklineGroups xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">${xml}</x14:sparklineGroups>`
736
+ + '</ext></extLst>';
737
+ }
738
+ }
739
+ // The objects on this sheet: a picture's bytes in xl/media, a chart's
740
+ // definition in its own part, both anchored by one drawing part.
741
+ let drawing = '';
742
+ const objects = state.objects ?? [];
743
+ if (objects.length) {
744
+ drawingCount += 1;
745
+ const built = drawingPartsFor(objects, {
746
+ sheet: name,
747
+ index: drawingCount,
748
+ chartsSoFar: chartCount,
749
+ mediaSoFar: mediaCount,
750
+ valueAt: (row, col) => wb.getValue(name, row, col),
751
+ textAt: (row, col) => {
752
+ const value = wb.getValue(name, row, col);
753
+ return isError(value) ? value.error : String(value ?? '');
754
+ },
755
+ });
756
+ if (built.drawingPath) {
757
+ chartCount += Object.keys(built.parts).filter((path) => path.startsWith('xl/charts/')).length;
758
+ mediaCount += Object.keys(built.parts).filter((path) => path.startsWith('xl/media/')).length;
759
+ Object.assign(parts, built.parts);
760
+ parts[built.drawingPath] = XML_HEAD + built.drawingXml;
761
+ parts[`xl/drawings/_rels/drawing${drawingCount}.xml.rels`] = XML_HEAD + built.drawingRels;
762
+ overrides.push(...built.overrides);
763
+ for (const extension of built.extensions)
764
+ mediaExtensions.add(extension);
765
+ const id = `rIdD${drawingCount}`;
766
+ sheetRels += `<Relationship Id="${id}" Type="${REL_DRAWING}" Target="../drawings/drawing${drawingCount}.xml"/>`;
767
+ drawing = `<drawing r:id="${id}"/>`;
768
+ }
769
+ else {
770
+ drawingCount -= 1;
771
+ }
772
+ }
773
+ if (sheetRels) {
774
+ parts[`xl/worksheets/_rels/sheet${n}.xml.rels`] = XML_HEAD + `<Relationships xmlns="${NS_PKG_REL}">${sheetRels}</Relationships>`;
775
+ }
776
+ // Page Layout: what prints and how. `printOptions` only when something is on.
777
+ const ps = state.pageSetup;
778
+ const printOptions = ps.gridlines || ps.headings ? `<printOptions${ps.gridlines ? ' gridLines="1"' : ''}${ps.headings ? ' headings="1"' : ''}/>` : '';
779
+ const m = ps.margins;
780
+ const pageMargins = `<pageMargins left="${m.left}" right="${m.right}" top="${m.top}" bottom="${m.bottom}" header="${m.header}" footer="${m.footer}"/>`;
781
+ const pageSetup = `<pageSetup paperSize="${PAPER_SIZES[ps.paper]?.code ?? 9}"${ps.scale !== 100 ? ` scale="${ps.scale}"` : ''} orientation="${ps.orientation}"/>`;
782
+ const quoted = /^[A-Za-z_][A-Za-z0-9_.]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
783
+ if (ps.printArea?.length)
784
+ printNames.push(`<definedName name="_xlnm.Print_Area" localSheetId="${index}">${esc(ps.printArea.map((r) => `${quoted}!${absRef(r)}`).join(','))}</definedName>`);
785
+ if (ps.printTitleRows)
786
+ printNames.push(`<definedName name="_xlnm.Print_Titles" localSheetId="${index}">${esc(`${quoted}!$${ps.printTitleRows[0] + 1}:$${ps.printTitleRows[1] + 1}`)}</definedName>`);
787
+ parts[`xl/worksheets/sheet${n}.xml`] = XML_HEAD
788
+ + `<worksheet xmlns="${NS_MAIN}" xmlns:r="${NS_REL}">`
789
+ + `<sheetViews>${sheetView}</sheetViews><sheetFormatPr defaultRowHeight="15"/>`
790
+ + (colXml.length ? `<cols>${colXml.join('')}</cols>` : '')
791
+ + `<sheetData>${rowXml.join('')}</sheetData>`
792
+ // Order matters in a worksheet: `drawing` comes after the page setup
793
+ // and before the legacy drawing the comments use.
794
+ + protection + editRanges + autoFilter + merges + cf + dv + hyperlinks + printOptions + pageMargins + pageSetup + drawing + legacyDrawing + tableParts + sparklineExt
795
+ + '</worksheet>';
796
+ overrides.push(`<Override PartName="/xl/worksheets/sheet${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`);
797
+ return { name, n, hidden: state.sheetHidden };
798
+ });
799
+ const activeTab = Math.max(0, wb.sheets.indexOf(wb.active));
800
+ const names = wb.names.list();
801
+ const iteration = wb.iteration;
802
+ parts['xl/workbook.xml'] = XML_HEAD
803
+ + `<workbook xmlns="${NS_MAIN}" xmlns:r="${NS_REL}">`
804
+ + `<bookViews><workbookView activeTab="${activeTab}"/></bookViews>`
805
+ + `<sheets>${sheetEntries.map((s) => `<sheet name="${esc(s.name)}" sheetId="${s.n}"${s.hidden ? ' state="hidden"' : ''} r:id="rId${s.n}"/>`).join('')}</sheets>`
806
+ + (names.length || printNames.length ? `<definedNames>${names.map((d) => `<definedName name="${esc(d.name)}">${esc(d.refersTo.replace(/^=/, ''))}</definedName>`).join('')}${printNames.join('')}</definedNames>` : '')
807
+ // Iterative calculation, which Excel keeps here rather than per sheet.
808
+ // Written only when it is on: a file with no calcPr is a file with the
809
+ // defaults, and that is what an untouched workbook means.
810
+ + (iteration?.enabled
811
+ ? `<calcPr calcId="191029" iterate="1" iterateCount="${iteration.maxIterations}" iterateDelta="${iteration.maxChange}"/>`
812
+ : '')
813
+ + '</workbook>';
814
+ parts['xl/_rels/workbook.xml.rels'] = XML_HEAD + `<Relationships xmlns="${NS_PKG_REL}">`
815
+ + sheetEntries.map((s) => `<Relationship Id="rId${s.n}" Type="${REL_WORKSHEET}" Target="worksheets/sheet${s.n}.xml"/>`).join('')
816
+ + `<Relationship Id="rId${sheetEntries.length + 1}" Type="${REL_STYLES}" Target="styles.xml"/>`
817
+ + (persons.length ? `<Relationship Id="rId${sheetEntries.length + 2}" Type="${REL_PERSONS}" Target="persons/person.xml"/>` : '')
818
+ + (hasSpills ? `<Relationship Id="rId${sheetEntries.length + 3}" Type="${REL_METADATA}" Target="metadata.xml"/>` : '')
819
+ + '</Relationships>';
820
+ if (hasSpills) {
821
+ // The one piece of metadata a dynamic array needs: cell metadata record 1
822
+ // says "this array formula spills", which is what `cm="1"` points at.
823
+ parts['xl/metadata.xml'] = XML_HEAD + `<metadata xmlns="${NS_MAIN}" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">`
824
+ + '<metadataTypes count="1"><metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1" pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1" clearComments="1" assign="1" coerce="1" cellMeta="1"/></metadataTypes>'
825
+ + '<futureMetadata name="XLDAPR" count="1"><bk><extLst><ext uri="{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}"><xda:dynamicArrayProperties fDynamic="1" fCollapsed="0"/></ext></extLst></bk></futureMetadata>'
826
+ + '<cellMetadata count="1"><bk><rc t="1" v="0"/></bk></cellMetadata></metadata>';
827
+ overrides.push('<Override PartName="/xl/metadata.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"/>');
828
+ }
829
+ if (persons.length) {
830
+ parts['xl/persons/person.xml'] = XML_HEAD + `<personList xmlns="${NS_THREADS}" xmlns:x="${NS_MAIN}">`
831
+ + persons.map((name, i) => `<person displayName="${esc(name)}" id="${guid(0, i + 1)}" userId="${esc(name)}" providerId="None"/>`).join('')
832
+ + '</personList>';
833
+ overrides.push('<Override PartName="/xl/persons/person.xml" ContentType="application/vnd.ms-excel.person+xml"/>');
834
+ }
835
+ parts['xl/styles.xml'] = styles.build();
836
+ parts['_rels/.rels'] = XML_HEAD + `<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_REL}/officeDocument" Target="xl/workbook.xml"/></Relationships>`;
837
+ parts['[Content_Types].xml'] = XML_HEAD + '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
838
+ + '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
839
+ + '<Default Extension="xml" ContentType="application/xml"/>'
840
+ + (hasVml ? '<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>' : '')
841
+ + [...mediaExtensions].map((extension) => `<Default Extension="${extension}" ContentType="image/${extension === 'jpeg' ? 'jpeg' : extension}"/>`).join('')
842
+ + '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
843
+ + '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
844
+ + overrides.join('')
845
+ + '</Types>';
846
+ return parts;
847
+ }
848
+ // ---------------------------------------------------------------------------
849
+ // Reader
850
+ // ---------------------------------------------------------------------------
851
+ function parseXml(xml, what) {
852
+ if (typeof DOMParser === 'undefined')
853
+ throw new Error(`@svgrid/enterprise: reading an xlsx needs DOMParser, which this environment does not have (${what})`);
854
+ const doc = new DOMParser().parseFromString(xml.replace(/^/, ''), 'application/xml');
855
+ if (doc.getElementsByTagName('parsererror').length)
856
+ throw new Error(`@svgrid/enterprise: ${what} is not well-formed XML`);
857
+ return doc;
858
+ }
859
+ /** Children by local name, so a namespaced and a bare document read alike. */
860
+ function kids(node, name) {
861
+ if (!node)
862
+ return [];
863
+ const out = [];
864
+ for (const child of Array.from(node.children))
865
+ if (child.localName === name)
866
+ out.push(child);
867
+ return out;
868
+ }
869
+ const kid = (node, name) => kids(node, name)[0] ?? null;
870
+ const attr = (el, name) => el?.getAttribute(name) ?? null;
871
+ const num = (el, name) => {
872
+ const v = attr(el, name);
873
+ if (v === null)
874
+ return null;
875
+ const n = Number(v);
876
+ return Number.isFinite(n) ? n : null;
877
+ };
878
+ const flag = (el, name) => { const v = attr(el, name); return v === '1' || v === 'true'; };
879
+ /** Excel's `dT` (UTC, no zone) as ISO 8601; null when it does not parse. */
880
+ function readThreadTime(dT) {
881
+ if (!dT)
882
+ return null;
883
+ const ms = Date.parse(/[zZ]|[+-]\d\d:\d\d$/.test(dT) ? dT : `${dT}Z`);
884
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
885
+ }
886
+ /** Every `<t>` under a rich-text element, joined. */
887
+ function textOf(el) {
888
+ if (!el)
889
+ return '';
890
+ let out = '';
891
+ const walk = (node) => {
892
+ for (const child of Array.from(node.children)) {
893
+ if (child.localName === 't')
894
+ out += child.textContent ?? '';
895
+ else if (child.localName === 'r' || child.localName === 'rPh')
896
+ walk(child);
897
+ }
898
+ };
899
+ walk(el);
900
+ return out;
901
+ }
902
+ /**
903
+ * A relationship's target EXACTLY as written, for an external hyperlink:
904
+ * `readRels` resolves a target against the part that holds it, which is
905
+ * right for a part inside the package and wrong for `https://…`.
906
+ */
907
+ function rawRelTarget(parts, relsPath, id) {
908
+ const xml = parts[relsPath];
909
+ if (!xml)
910
+ return null;
911
+ for (const rel of kids(parseXml(xml, relsPath).documentElement, 'Relationship')) {
912
+ if (rel.getAttribute('Id') === id)
913
+ return rel.getAttribute('Target');
914
+ }
915
+ return null;
916
+ }
917
+ /** Resolve a relationship target against the part that holds the rels. */
918
+ function resolvePath(base, target) {
919
+ if (target.startsWith('/'))
920
+ return target.slice(1);
921
+ const dir = base.split('/').slice(0, -1);
922
+ for (const piece of target.split('/')) {
923
+ if (piece === '..')
924
+ dir.pop();
925
+ else if (piece !== '.' && piece !== '')
926
+ dir.push(piece);
927
+ }
928
+ return dir.join('/');
929
+ }
930
+ function readRels(parts, relsPath, base) {
931
+ const out = new Map();
932
+ const xml = parts[relsPath];
933
+ if (!xml)
934
+ return out;
935
+ for (const rel of kids(parseXml(xml, relsPath).documentElement, 'Relationship')) {
936
+ const id = attr(rel, 'Id');
937
+ const type = attr(rel, 'Type');
938
+ const target = attr(rel, 'Target');
939
+ if (id && type && target)
940
+ out.set(id, { type, target: resolvePath(base, target) });
941
+ }
942
+ return out;
943
+ }
944
+ function readFontInto(font, into, sizes) {
945
+ if (!font)
946
+ return;
947
+ if (kid(font, 'b'))
948
+ into.bold = true;
949
+ if (kid(font, 'i'))
950
+ into.italic = true;
951
+ if (kid(font, 'u'))
952
+ into.underline = true;
953
+ if (kid(font, 'strike'))
954
+ into.strike = true;
955
+ const colour = fromArgb(attr(kid(font, 'color'), 'rgb'));
956
+ if (colour)
957
+ into.color = colour;
958
+ if (sizes) {
959
+ const entry = into;
960
+ const sz = num(kid(font, 'sz'), 'val');
961
+ if (sz !== null && Math.abs(sz - 11) > 0.01)
962
+ entry.fontSize = Math.round((sz / 0.75) * 100) / 100;
963
+ const name = attr(kid(font, 'name'), 'val');
964
+ if (name && name !== 'Calibri')
965
+ entry.fontFamily = name;
966
+ }
967
+ }
968
+ function readStyles(xml) {
969
+ const empty = { xfs: [], dxfs: [], numFmtOf: () => undefined };
970
+ if (!xml)
971
+ return empty;
972
+ const root = parseXml(xml, 'xl/styles.xml').documentElement;
973
+ const numFmts = new Map();
974
+ for (const nf of kids(kid(root, 'numFmts'), 'numFmt')) {
975
+ const id = num(nf, 'numFmtId');
976
+ const code = attr(nf, 'formatCode');
977
+ if (id !== null && code)
978
+ numFmts.set(id, code);
979
+ }
980
+ const codeOf = (id) => {
981
+ if (id === null || id === 0)
982
+ return undefined;
983
+ const code = numFmts.get(id) ?? BUILTIN_NUMFMT[id];
984
+ // LibreOffice writes General as a numbered format of its own. It is the
985
+ // absence of a format, not a format, and storing it would make every
986
+ // imported cell look as though someone had chosen one.
987
+ return code && code.toLowerCase() !== 'general' ? code : undefined;
988
+ };
989
+ const fonts = kids(kid(root, 'fonts'), 'font');
990
+ const fills = kids(kid(root, 'fills'), 'fill').map((fill) => {
991
+ const pattern = kid(fill, 'patternFill');
992
+ if (!pattern || attr(pattern, 'patternType') === 'none')
993
+ return undefined;
994
+ return fromArgb(attr(kid(pattern, 'fgColor'), 'rgb'));
995
+ });
996
+ const borders = kids(kid(root, 'borders'), 'border').map((border) => {
997
+ const out = {};
998
+ for (const side of ['left', 'right', 'top', 'bottom']) {
999
+ const el = kid(border, side);
1000
+ const style = attr(el, 'style');
1001
+ if (!el || !style || style === 'none')
1002
+ continue;
1003
+ const spec = {};
1004
+ if (style === 'dashed' || style === 'mediumDashed' || style === 'dashDot')
1005
+ spec.style = 'dashed';
1006
+ else if (style === 'dotted' || style === 'hair')
1007
+ spec.style = 'dotted';
1008
+ else if (style === 'double')
1009
+ spec.style = 'double';
1010
+ if (style === 'thick')
1011
+ spec.width = 3;
1012
+ else if (style === 'medium' || style.startsWith('medium'))
1013
+ spec.width = 2;
1014
+ else
1015
+ spec.width = 1;
1016
+ const colour = fromArgb(attr(kid(el, 'color'), 'rgb'));
1017
+ if (colour)
1018
+ spec.color = colour;
1019
+ out[side] = spec;
1020
+ }
1021
+ return Object.keys(out).length ? out : undefined;
1022
+ });
1023
+ const xfNumFmt = [];
1024
+ const xfs = kids(kid(root, 'cellXfs'), 'xf').map((xf) => {
1025
+ const entry = {};
1026
+ const nfId = num(xf, 'numFmtId');
1027
+ xfNumFmt.push(nfId);
1028
+ const code = codeOf(nfId);
1029
+ if (code)
1030
+ entry.numFmt = code;
1031
+ readFontInto(fonts[num(xf, 'fontId') ?? 0] ?? null, entry, true);
1032
+ const fill = fills[num(xf, 'fillId') ?? 0];
1033
+ if (fill)
1034
+ entry.fill = fill;
1035
+ const border = borders[num(xf, 'borderId') ?? 0];
1036
+ if (border)
1037
+ entry.border = border;
1038
+ const align = kid(xf, 'alignment');
1039
+ const horizontal = attr(align, 'horizontal');
1040
+ if (horizontal === 'left' || horizontal === 'center' || horizontal === 'right')
1041
+ entry.align = horizontal;
1042
+ const vertical = attr(align, 'vertical');
1043
+ // The middle is the sheet's default, so only top and bottom are kept.
1044
+ if (vertical === 'top' || vertical === 'bottom')
1045
+ entry.valign = vertical;
1046
+ if (flag(align, 'wrapText'))
1047
+ entry.wrap = true;
1048
+ const indent = num(align, 'indent');
1049
+ if (indent)
1050
+ entry.indent = indent;
1051
+ const protection = kid(xf, 'protection');
1052
+ if (protection && attr(protection, 'locked') === '0')
1053
+ entry.locked = false;
1054
+ return entry;
1055
+ });
1056
+ const dxfs = kids(kid(root, 'dxfs'), 'dxf').map((dxf) => {
1057
+ const style = {};
1058
+ readFontInto(kid(dxf, 'font'), style, false);
1059
+ const fill = fromArgb(attr(kid(kid(kid(dxf, 'fill'), 'patternFill'), 'bgColor'), 'rgb'))
1060
+ ?? fromArgb(attr(kid(kid(kid(dxf, 'fill'), 'patternFill'), 'fgColor'), 'rgb'));
1061
+ if (fill)
1062
+ style.fill = fill;
1063
+ const nf = kid(dxf, 'numFmt');
1064
+ const code = attr(nf, 'formatCode') ?? codeOf(num(nf, 'numFmtId'));
1065
+ if (code)
1066
+ style.numFmt = code;
1067
+ return style;
1068
+ });
1069
+ return { xfs, dxfs, numFmtOf: (xf) => codeOf(xfNumFmt[xf] ?? null) };
1070
+ }
1071
+ function readSharedStrings(xml) {
1072
+ if (!xml)
1073
+ return [];
1074
+ return kids(parseXml(xml, 'xl/sharedStrings.xml').documentElement, 'si').map(textOf);
1075
+ }
1076
+ /** A formula as the engine spells it: a leading `=`, no `_xlfn.` prefixes. */
1077
+ /** Excel's own defaults: the summary line comes after its detail on both
1078
+ * axes, which is `summaryBelow="1"` and `summaryRight="1"`. */
1079
+ const OUTLINE_SIDE = { summaryBelow: true };
1080
+ const engineFormula = (text) => `=${text.replace(/_xlfn\./g, '').replace(/_xlws\./g, '')}`;
1081
+ /**
1082
+ * Excel's "future functions", which it stores under a prefix.
1083
+ *
1084
+ * Anything added after the file format was frozen is written as
1085
+ * `_xlfn.NAME`, and the ones that only make sense on a worksheet as
1086
+ * `_xlfn._xlws.NAME`. A file that spells them plainly opens in Excel with
1087
+ * `#NAME?` in every one of those cells, which is why this list exists: the
1088
+ * reader already strips the prefixes, and the writer has to put them back.
1089
+ */
1090
+ const XLWS_FUNCTIONS = new Set(['FILTER', 'SORT']);
1091
+ /**
1092
+ * Future functions this engine does NOT evaluate but still spells the way
1093
+ * the file wants.
1094
+ *
1095
+ * A cell this engine answers with `#NAME?` is one Excel can still work
1096
+ * out, so dropping the prefix on the way back out would turn a formula
1097
+ * that merely looks broken here into one that is broken there too. Keeping
1098
+ * the name here costs nothing and makes the round trip lossless.
1099
+ *
1100
+ * Anything on this list is a candidate for implementing later; nothing
1101
+ * should arrive here by accident, which is what the test that reads it
1102
+ * guards.
1103
+ */
1104
+ export const PRESERVED_ONLY = ['RANDARRAY'];
1105
+ export const XLFN_FUNCTIONS = new Set([
1106
+ // Excel 365 and the dynamic arrays.
1107
+ 'LET', 'LAMBDA', 'MAP', 'BYROW', 'BYCOL', 'REDUCE', 'SCAN', 'MAKEARRAY',
1108
+ 'UNIQUE', 'SEQUENCE', 'SORTBY', 'TEXTSPLIT',
1109
+ 'TEXTBEFORE', 'TEXTAFTER',
1110
+ 'XLOOKUP', 'XMATCH', 'IMAGE',
1111
+ // Excel 2016.
1112
+ 'TEXTJOIN', 'CONCAT', 'IFS', 'SWITCH', 'MAXIFS', 'MINIFS', 'FORECAST.LINEAR',
1113
+ // Excel 2013: the reciprocal trigonometry, the bitwise set, and the
1114
+ // handful of others that arrived with them.
1115
+ 'ACOT', 'ACOTH', 'COT', 'COTH', 'CSC', 'CSCH', 'SEC', 'SECH',
1116
+ 'BITAND', 'BITOR', 'BITXOR', 'BITLSHIFT', 'BITRSHIFT',
1117
+ 'CEILING.MATH', 'FLOOR.MATH', 'DAYS', 'XOR', 'NUMBERVALUE',
1118
+ 'GAMMA', 'GAUSS', 'PHI', 'PERMUTATIONA', 'SKEW.P', 'UNICHAR', 'UNICODE',
1119
+ // Excel 2010: the statistical family Microsoft renamed with a dot. The
1120
+ // OLD spelling of each (NORMDIST, CHIDIST, TTEST) is NOT a future
1121
+ // function and must stay unprefixed, which is why only the dotted names
1122
+ // are here.
1123
+ 'BETA.DIST', 'BETA.INV', 'BINOM.DIST', 'BINOM.INV',
1124
+ 'CHISQ.DIST', 'CHISQ.DIST.RT', 'CHISQ.INV', 'CHISQ.INV.RT', 'CHISQ.TEST',
1125
+ 'CONFIDENCE.NORM', 'CONFIDENCE.T', 'COVARIANCE.P', 'COVARIANCE.S',
1126
+ 'ERF.PRECISE', 'ERFC.PRECISE', 'EXPON.DIST',
1127
+ 'F.DIST', 'F.DIST.RT', 'F.INV', 'F.INV.RT', 'F.TEST',
1128
+ 'GAMMA.DIST', 'GAMMA.INV', 'GAMMALN.PRECISE', 'HYPGEOM.DIST',
1129
+ 'LOGNORM.DIST', 'LOGNORM.INV', 'MODE.SNGL', 'NEGBINOM.DIST',
1130
+ 'NORM.DIST', 'NORM.INV', 'NORM.S.DIST', 'NORM.S.INV',
1131
+ 'PERCENTILE.EXC', 'PERCENTILE.INC', 'POISSON.DIST',
1132
+ 'QUARTILE.EXC', 'QUARTILE.INC', 'STDEV.P', 'STDEV.S',
1133
+ 'T.DIST', 'T.DIST.2T', 'T.DIST.RT', 'T.INV', 'T.INV.2T', 'T.TEST',
1134
+ 'VAR.P', 'VAR.S', 'WEIBULL.DIST', 'Z.TEST',
1135
+ ...PRESERVED_ONLY,
1136
+ ]);
1137
+ /**
1138
+ * A formula as the FILE spells it: the future functions prefixed.
1139
+ *
1140
+ * Scanned rather than replaced by a regular expression, so that a name
1141
+ * inside a string literal (`="SORT(x)"`) is left alone, and one that is
1142
+ * part of a longer name (`MYSORT(`) is too.
1143
+ */
1144
+ export function xlsxFormula(text, inTable) {
1145
+ let out = '';
1146
+ let i = 0;
1147
+ while (i < text.length) {
1148
+ const ch = text[i];
1149
+ if (ch === '"') {
1150
+ const end = text.indexOf('"', i + 1);
1151
+ const close = end < 0 ? text.length : end + 1;
1152
+ out += text.slice(i, close);
1153
+ i = close;
1154
+ continue;
1155
+ }
1156
+ // `[@Amount]` is the shorthand Excel takes in the formula bar; what it
1157
+ // STORES is the long form, naming the table and the specifier. A file
1158
+ // carrying the shorthand is one other readers refuse.
1159
+ if (ch === '[' && text[i + 1] === '@' && inTable) {
1160
+ const end = text.indexOf(']', i);
1161
+ if (end > 0) {
1162
+ const column = text.slice(i + 2, end).trim();
1163
+ out += column === ''
1164
+ ? `${inTable}[#This Row]`
1165
+ : `${inTable}[[#This Row],[${column}]]`;
1166
+ i = end + 1;
1167
+ continue;
1168
+ }
1169
+ }
1170
+ if (/[A-Za-z_]/.test(ch)) {
1171
+ let j = i;
1172
+ while (j < text.length && /[A-Za-z0-9_.]/.test(text[j]))
1173
+ j += 1;
1174
+ const word = text.slice(i, j);
1175
+ const isCall = text[j] === '(';
1176
+ const before = i > 0 ? text[i - 1] : '';
1177
+ const upper = word.toUpperCase();
1178
+ if (isCall && before !== '.' && before !== '!' && (XLFN_FUNCTIONS.has(upper) || XLWS_FUNCTIONS.has(upper))) {
1179
+ out += XLWS_FUNCTIONS.has(upper) ? `_xlfn._xlws.${word}` : `_xlfn.${word}`;
1180
+ }
1181
+ else {
1182
+ out += word;
1183
+ }
1184
+ i = j;
1185
+ continue;
1186
+ }
1187
+ out += ch;
1188
+ i += 1;
1189
+ }
1190
+ return out;
1191
+ }
1192
+ /** A bound as the file spells it, read back as a literal or a formula. */
1193
+ function readValidationFormula(text, allow) {
1194
+ if (text === null || text.trim() === '')
1195
+ return undefined;
1196
+ const t = text.trim();
1197
+ const quoted = /^"(.*)"$/s.exec(t);
1198
+ if (quoted)
1199
+ return quoted[1].replace(/""/g, '"');
1200
+ if (allow === 'custom')
1201
+ return engineFormula(t);
1202
+ if (Number.isFinite(Number(t)))
1203
+ return allow === 'date' ? serialToIso(Number(t)) : t;
1204
+ return engineFormula(t);
1205
+ }
1206
+ const VALIDATION_ALLOW_BACK = { whole: 'whole', decimal: 'decimal', list: 'list', date: 'date', time: 'decimal', textLength: 'textLength', custom: 'custom' };
1207
+ const VALIDATION_OP_BACK = Object.fromEntries(Object.entries(VALIDATION_OP).map(([k, v]) => [v, k]));
1208
+ const CF_OP_BACK = Object.fromEntries(Object.entries(CF_OP).map(([k, v]) => [v, k]));
1209
+ const CF_TEXT_BACK = Object.fromEntries(Object.entries(CF_TEXT).map(([k, v]) => [v, k]));
1210
+ function iconSetBack(name) {
1211
+ if (!name)
1212
+ return 'arrows';
1213
+ if (/Traffic/i.test(name))
1214
+ return 'traffic';
1215
+ if (/Flag/i.test(name))
1216
+ return 'flags';
1217
+ if (/Symbol/i.test(name))
1218
+ return 'symbols';
1219
+ return 'arrows';
1220
+ }
1221
+ function readCfFormula(text) {
1222
+ const t = text.trim();
1223
+ const quoted = /^"(.*)"$/s.exec(t);
1224
+ if (quoted)
1225
+ return quoted[1].replace(/""/g, '"');
1226
+ return Number.isFinite(Number(t)) && t !== '' ? t : engineFormula(t);
1227
+ }
1228
+ let ruleSeq = 0;
1229
+ const nextId = (prefix) => `${prefix}x${(ruleSeq += 1)}`;
1230
+ /**
1231
+ * The document a package holds, as a saved state: hand it to
1232
+ * `createSheetDocument({ state })` or a shell's `setState`.
1233
+ */
1234
+ export function documentFromXlsxParts(parts) {
1235
+ const workbookXml = parts['xl/workbook.xml'];
1236
+ if (!workbookXml)
1237
+ throw new Error('@svgrid/enterprise: not an xlsx package: xl/workbook.xml is missing');
1238
+ const wbRoot = parseXml(workbookXml, 'xl/workbook.xml').documentElement;
1239
+ const wbRels = readRels(parts, 'xl/_rels/workbook.xml.rels', 'xl/workbook.xml');
1240
+ const stylesPath = [...wbRels.values()].find((r) => r.type === REL_STYLES)?.target ?? 'xl/styles.xml';
1241
+ const sharedPath = [...wbRels.values()].find((r) => r.type === REL_SHARED)?.target ?? 'xl/sharedStrings.xml';
1242
+ const styles = readStyles(parts[stylesPath]);
1243
+ const shared = readSharedStrings(parts[sharedPath]);
1244
+ const personsPath = [...wbRels.values()].find((r) => r.type === REL_PERSONS)?.target;
1245
+ const persons = new Map();
1246
+ if (personsPath && parts[personsPath]) {
1247
+ for (const person of kids(parseXml(parts[personsPath], personsPath).documentElement, 'person')) {
1248
+ const id = attr(person, 'id');
1249
+ if (id)
1250
+ persons.set(id, attr(person, 'displayName') ?? '');
1251
+ }
1252
+ }
1253
+ // <calcPr iterate="1" .../>: Excel's Enable iterative calculation.
1254
+ const calcPr = kid(wbRoot, 'calcPr');
1255
+ const iterateOn = calcPr ? attr(calcPr, 'iterate') === '1' || attr(calcPr, 'iterate') === 'true' : false;
1256
+ const iteration = iterateOn
1257
+ ? cleanIteration({
1258
+ enabled: true,
1259
+ maxIterations: Number(attr(calcPr, 'iterateCount') ?? DEFAULT_ITERATION.maxIterations),
1260
+ maxChange: Number(attr(calcPr, 'iterateDelta') ?? DEFAULT_ITERATION.maxChange),
1261
+ })
1262
+ : null;
1263
+ const sheets = [];
1264
+ const entries = {};
1265
+ /** Tables are workbook-wide, though each part hangs off its own sheet. */
1266
+ const tables = [];
1267
+ const sheetNodes = kids(kid(wbRoot, 'sheets'), 'sheet');
1268
+ sheetNodes.forEach((sheetNode, index) => {
1269
+ const name = attr(sheetNode, 'name') ?? `Sheet${index + 1}`;
1270
+ const rid = sheetNode.getAttributeNS(NS_REL, 'id') ?? attr(sheetNode, 'r:id');
1271
+ const path = (rid && wbRels.get(rid)?.target) ?? `xl/worksheets/sheet${index + 1}.xml`;
1272
+ const xml = parts[path];
1273
+ const cells = [];
1274
+ const entry = {
1275
+ formats: {}, columnWidths: {}, rowHeights: [], hidden: { rows: [], cols: [] }, freeze: { rows: 0, cols: 0 },
1276
+ comments: {}, protected: false, sheetHidden: attr(sheetNode, 'state') === 'hidden' || attr(sheetNode, 'state') === 'veryHidden',
1277
+ merges: [], validation: [], conditionalFormats: [], autoFilter: null,
1278
+ };
1279
+ // Filled from the rows' and columns' `outlineLevel` below, then hung on
1280
+ // the entry once, so an unoutlined sheet carries no `outline` at all.
1281
+ const outlineRows = emptyOutline();
1282
+ const outlineCols = emptyOutline();
1283
+ if (xml) {
1284
+ const root = parseXml(xml, path).documentElement;
1285
+ const put = (r, c, text) => {
1286
+ while (cells.length <= r)
1287
+ cells.push([]);
1288
+ const line = cells[r];
1289
+ while (line.length <= c)
1290
+ line.push('');
1291
+ line[c] = text;
1292
+ };
1293
+ for (const col of kids(kid(root, 'cols'), 'col')) {
1294
+ const min = num(col, 'min');
1295
+ const max = num(col, 'max');
1296
+ if (min === null || max === null)
1297
+ continue;
1298
+ const width = num(col, 'width');
1299
+ const hidden = flag(col, 'hidden');
1300
+ const level = num(col, 'outlineLevel');
1301
+ const collapsedCol = flag(col, 'collapsed');
1302
+ // A run over every column to the sheet's edge is Excel's default
1303
+ // width, not a setting on sixteen thousand columns.
1304
+ const last = Math.min(max, Math.max(min, 256));
1305
+ for (let c = min - 1; c <= last - 1; c += 1) {
1306
+ if (width !== null && flag(col, 'customWidth'))
1307
+ entry.columnWidths[colToLetters(c)] = widthToPx(width);
1308
+ if (level !== null && level > 0)
1309
+ outlineCols.levels[c] = level;
1310
+ if (collapsedCol)
1311
+ outlineCols.collapsed.push(c);
1312
+ // A line a collapsed group folds is not a hidden line: it comes
1313
+ // back when the group opens, so recording it as hidden as well
1314
+ // would leave it behind when it did.
1315
+ if (hidden && level === null)
1316
+ entry.hidden.cols.push(c);
1317
+ }
1318
+ }
1319
+ const sharedFormulas = new Map();
1320
+ /** The rectangles of the array formulas seen so far: cells inside them without a formula hold spilled values, not text. */
1321
+ const arrayRects = [];
1322
+ // A row's own `r` is optional in the format: without one the rows are
1323
+ // in order, which is what a streaming writer produces. The same goes
1324
+ // for a cell's, so both fall back to counting.
1325
+ const defaultHeight = num(kid(root, 'sheetFormatPr'), 'defaultRowHeight');
1326
+ let nextRow = 0;
1327
+ for (const row of kids(kid(root, 'sheetData'), 'row')) {
1328
+ const given = num(row, 'r');
1329
+ const r = given === null ? nextRow : given - 1;
1330
+ if (r < 0)
1331
+ continue;
1332
+ nextRow = r + 1;
1333
+ const ht = num(row, 'ht');
1334
+ // Excel marks a height it was told with `customHeight`; LibreOffice
1335
+ // keeps the height and drops the flag, so a height that differs from
1336
+ // the sheet's default counts either way.
1337
+ const told = ht !== null
1338
+ && (flag(row, 'customHeight') || defaultHeight === null || Math.abs(ht - defaultHeight) > 0.01);
1339
+ if (told)
1340
+ entry.rowHeights.push([r, ptToPx(ht)]);
1341
+ const rowLevel = num(row, 'outlineLevel');
1342
+ if (rowLevel !== null && rowLevel > 0)
1343
+ outlineRows.levels[r] = rowLevel;
1344
+ if (flag(row, 'collapsed'))
1345
+ outlineRows.collapsed.push(r);
1346
+ if (flag(row, 'hidden') && rowLevel === null)
1347
+ entry.hidden.rows.push(r);
1348
+ let nextCol = 0;
1349
+ for (const cell of kids(row, 'c')) {
1350
+ const at = attr(cell, 'r');
1351
+ const ref = at ? parseA1(at) : { row: r, col: nextCol, rowAbs: false, colAbs: false, sheet: null };
1352
+ if (!ref || ref.row === null)
1353
+ continue;
1354
+ const c = ref.col;
1355
+ nextCol = c + 1;
1356
+ const fEl = kid(cell, 'f');
1357
+ // The cached values under an array formula belong to its spill,
1358
+ // not to the cells: read the anchor, skip the rest.
1359
+ if (!fEl && arrayRects.some(([r1, c1, r2, c2]) => ref.row >= r1 && ref.row <= r2 && c >= c1 && c <= c2))
1360
+ continue;
1361
+ if (fEl && attr(fEl, 't') === 'array') {
1362
+ const rect = refRect(attr(fEl, 'ref') ?? '');
1363
+ if (rect)
1364
+ arrayRects.push(rect);
1365
+ }
1366
+ const s = num(cell, 's');
1367
+ const type = attr(cell, 't') ?? 'n';
1368
+ const f = kid(cell, 'f');
1369
+ const v = kid(cell, 'v')?.textContent ?? null;
1370
+ const style = s !== null ? styles.xfs[s] : undefined;
1371
+ if (style && Object.keys(style).length)
1372
+ entry.formats[formatKey(ref.row, c)] = { ...style, border: style.border ? { ...style.border } : undefined };
1373
+ if (entry.formats[formatKey(ref.row, c)]?.border === undefined)
1374
+ delete entry.formats[formatKey(ref.row, c)]?.border;
1375
+ let text = '';
1376
+ if (f) {
1377
+ let body = f.textContent ?? '';
1378
+ const si = attr(f, 'si');
1379
+ if (attr(f, 't') === 'shared' && si !== null) {
1380
+ if (body)
1381
+ sharedFormulas.set(si, { row: ref.row, col: c, text: body });
1382
+ else {
1383
+ const master = sharedFormulas.get(si);
1384
+ if (master)
1385
+ body = String(translateFormula(engineFormula(master.text), ref.row - master.row, c - master.col)).slice(1);
1386
+ }
1387
+ }
1388
+ text = body ? engineFormula(body) : '';
1389
+ }
1390
+ if (!text) {
1391
+ // A string cell holds TEXT even when the text reads as a number,
1392
+ // which is the whole point of a part number like 007. The
1393
+ // apostrophe is how the workbook spells that, and without it the
1394
+ // round trip would hand back the number 7.
1395
+ if (type === 's')
1396
+ text = asText(shared[Number(v)] ?? '');
1397
+ else if (type === 'inlineStr')
1398
+ text = asText(textOf(kid(cell, 'is')));
1399
+ else if (type === 'str')
1400
+ text = asText(v ?? '');
1401
+ else if (type === 'b')
1402
+ text = v === '1' || v === 'true' ? 'TRUE' : 'FALSE';
1403
+ else if (type === 'e')
1404
+ text = v ?? '';
1405
+ else if (v !== null && v !== '') {
1406
+ const n = Number(v);
1407
+ text = Number.isFinite(n) && isDayFormat(s !== null ? styles.numFmtOf(s) : undefined) && n >= 0 && Number.isInteger(n) ? serialToIso(n) : v;
1408
+ }
1409
+ }
1410
+ if (text !== '')
1411
+ put(ref.row, c, text);
1412
+ }
1413
+ }
1414
+ const pane = kid(kid(kid(root, 'sheetViews'), 'sheetView'), 'pane');
1415
+ if (pane && attr(pane, 'state') === 'frozen')
1416
+ entry.freeze = { rows: num(pane, 'ySplit') ?? 0, cols: num(pane, 'xSplit') ?? 0 };
1417
+ const printOptions = kid(root, 'printOptions');
1418
+ const pageMargins = kid(root, 'pageMargins');
1419
+ const pageSetupEl = kid(root, 'pageSetup');
1420
+ if (printOptions || pageMargins || pageSetupEl) {
1421
+ const ps = defaultPageSetup();
1422
+ if (flag(printOptions, 'gridLines'))
1423
+ ps.gridlines = true;
1424
+ if (flag(printOptions, 'headings'))
1425
+ ps.headings = true;
1426
+ if (pageMargins) {
1427
+ for (const side of ['left', 'right', 'top', 'bottom', 'header', 'footer']) {
1428
+ const v = num(pageMargins, side);
1429
+ if (v !== null)
1430
+ ps.margins[side] = v;
1431
+ }
1432
+ }
1433
+ if (attr(pageSetupEl, 'orientation') === 'landscape')
1434
+ ps.orientation = 'landscape';
1435
+ const code = num(pageSetupEl, 'paperSize');
1436
+ const paper = Object.keys(PAPER_SIZES).find((p) => PAPER_SIZES[p].code === code);
1437
+ if (paper)
1438
+ ps.paper = paper;
1439
+ const scale = num(pageSetupEl, 'scale');
1440
+ if (scale !== null && scale > 0)
1441
+ ps.scale = scale;
1442
+ entry.pageSetup = ps;
1443
+ }
1444
+ const sheetProtection = kid(root, 'sheetProtection');
1445
+ if (sheetProtection && flag(sheetProtection, 'sheet')) {
1446
+ entry.protected = true;
1447
+ const allow = {};
1448
+ for (const key of PROTECTION_PERMISSIONS) {
1449
+ const v = attr(sheetProtection, key);
1450
+ if (v === '0' || v === 'false')
1451
+ allow[key] = true;
1452
+ }
1453
+ entry.protection = { allow, ranges: [] };
1454
+ }
1455
+ for (const range of kids(kid(root, 'protectedRanges'), 'protectedRange')) {
1456
+ const rects = (attr(range, 'sqref') ?? '').split(/\s+/).map(refRect).filter((r) => r !== null);
1457
+ if (!rects.length)
1458
+ continue;
1459
+ entry.protection ?? (entry.protection = { allow: {}, ranges: [] });
1460
+ entry.protection.ranges.push({ id: newEditRangeId(), title: attr(range, 'name') ?? `Range${entry.protection.ranges.length + 1}`, rects });
1461
+ }
1462
+ const afNode = kid(root, 'autoFilter');
1463
+ const af = attr(afNode, 'ref');
1464
+ const afRect = af ? refRect(af) : null;
1465
+ if (afRect && afNode) {
1466
+ // The criteria come back with the region. Excel writes the rows a
1467
+ // filter hides as hidden rows; the document releases the ones its
1468
+ // criteria hide again (see releaseFilteredRows), and the rest stay
1469
+ // hidden by hand, which shows the rows Excel showed.
1470
+ const isDateColumn = (col) => {
1471
+ let dates = 0;
1472
+ let filled = 0;
1473
+ for (let r = afRect[0] + 1; r <= afRect[2]; r += 1) {
1474
+ const raw = cells[r]?.[col] ?? '';
1475
+ if (raw === '')
1476
+ continue;
1477
+ filled += 1;
1478
+ if (isoToSerial(raw.trim()) !== null)
1479
+ dates += 1;
1480
+ }
1481
+ return filled > 0 && dates === filled;
1482
+ };
1483
+ entry.autoFilter = { range: afRect, filters: filtersFromXlsx(afNode, afRect, isDateColumn).filters };
1484
+ }
1485
+ for (const merge of kids(kid(root, 'mergeCells'), 'mergeCell')) {
1486
+ const rect = refRect(attr(merge, 'ref') ?? '');
1487
+ if (rect)
1488
+ entry.merges.push([rect[0], rect[1], rect[2], rect[3]]);
1489
+ }
1490
+ for (const block of kids(root, 'conditionalFormatting')) {
1491
+ const rects = sqrefRects(attr(block, 'sqref') ?? '');
1492
+ if (!rects.length)
1493
+ continue;
1494
+ const ruleNodes = kids(block, 'cfRule').map((node) => ({ node, priority: num(node, 'priority') ?? 0 }));
1495
+ for (const { node, priority } of ruleNodes) {
1496
+ const type = attr(node, 'type');
1497
+ const dxf = num(node, 'dxfId');
1498
+ const style = dxf !== null ? { ...(styles.dxfs[dxf] ?? {}) } : {};
1499
+ const formulas = kids(node, 'formula').map((f) => f.textContent ?? '');
1500
+ const base = { id: nextId('cf'), rects, ...(flag(node, 'stopIfTrue') ? { stopIfTrue: true } : {}) };
1501
+ let rule = null;
1502
+ if (type === 'cellIs') {
1503
+ const operator = CF_OP_BACK[attr(node, 'operator') ?? ''] ?? 'equal';
1504
+ rule = { ...base, kind: 'cellIs', operator, value1: readCfFormula(formulas[0] ?? '0'), ...(formulas[1] !== undefined ? { value2: readCfFormula(formulas[1]) } : {}), style };
1505
+ }
1506
+ else if (type && CF_TEXT_BACK[type]) {
1507
+ rule = { ...base, kind: 'text', match: CF_TEXT_BACK[type], value: attr(node, 'text') ?? '', style };
1508
+ }
1509
+ else if (type === 'duplicateValues' || type === 'uniqueValues') {
1510
+ rule = { ...base, kind: 'duplicates', ...(type === 'uniqueValues' ? { unique: true } : {}), style };
1511
+ }
1512
+ else if (type === 'top10') {
1513
+ rule = { ...base, kind: 'topBottom', top: !flag(node, 'bottom'), rank: num(node, 'rank') ?? 10, ...(flag(node, 'percent') ? { percent: true } : {}), style };
1514
+ }
1515
+ else if (type === 'aboveAverage') {
1516
+ rule = { ...base, kind: 'average', above: attr(node, 'aboveAverage') !== '0', style };
1517
+ }
1518
+ else if (type === 'expression') {
1519
+ rule = { ...base, kind: 'formula', formula: engineFormula(formulas[0] ?? 'FALSE'), style };
1520
+ }
1521
+ else if (type === 'dataBar') {
1522
+ rule = { ...base, kind: 'dataBar', color: fromArgb(attr(kid(kid(node, 'dataBar'), 'color'), 'rgb')) ?? '#638ec6' };
1523
+ }
1524
+ else if (type === 'colorScale') {
1525
+ const colours = kids(kid(node, 'colorScale'), 'color').map((c) => fromArgb(attr(c, 'rgb')) ?? '#ffffff');
1526
+ if (colours.length >= 3)
1527
+ rule = { ...base, kind: 'colorScale', colors: [colours[0], colours[1], colours[2]] };
1528
+ else if (colours.length === 2)
1529
+ rule = { ...base, kind: 'colorScale', colors: [colours[0], colours[1]] };
1530
+ }
1531
+ else if (type === 'iconSet') {
1532
+ rule = { ...base, kind: 'iconSet', set: iconSetBack(attr(kid(node, 'iconSet'), 'iconSet')) };
1533
+ }
1534
+ if (rule)
1535
+ entry.conditionalFormats.push(Object.assign(rule, { priority }));
1536
+ }
1537
+ }
1538
+ // Excel orders rules by priority across the blocks; ours by position.
1539
+ entry.conditionalFormats.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
1540
+ for (const rule of entry.conditionalFormats)
1541
+ delete rule.priority;
1542
+ for (const dv of kids(kid(root, 'dataValidations'), 'dataValidation')) {
1543
+ const allow = VALIDATION_ALLOW_BACK[attr(dv, 'type') ?? ''] ?? 'any';
1544
+ const rects = sqrefRects(attr(dv, 'sqref') ?? '');
1545
+ if (!rects.length || allow === 'any')
1546
+ continue;
1547
+ const errorStyle = attr(dv, 'errorStyle');
1548
+ const rule = {
1549
+ id: nextId('v'),
1550
+ rects,
1551
+ allow,
1552
+ ignoreBlank: attr(dv, 'allowBlank') === null ? true : flag(dv, 'allowBlank'),
1553
+ inCellDropdown: allow === 'list' && !flag(dv, 'showDropDown'),
1554
+ alert: { style: errorStyle === 'warning' || errorStyle === 'information' ? 'warning' : 'stop' },
1555
+ };
1556
+ const title = attr(dv, 'errorTitle');
1557
+ const message = attr(dv, 'error');
1558
+ if (title)
1559
+ rule.alert.title = title;
1560
+ if (message)
1561
+ rule.alert.message = message;
1562
+ const promptTitle = attr(dv, 'promptTitle');
1563
+ const prompt = attr(dv, 'prompt');
1564
+ if (promptTitle || prompt)
1565
+ rule.input = { ...(promptTitle ? { title: promptTitle } : {}), ...(prompt ? { message: prompt } : {}) };
1566
+ if (allow !== 'list' && allow !== 'custom')
1567
+ rule.operator = VALIDATION_OP_BACK[attr(dv, 'operator') ?? 'between'] ?? 'between';
1568
+ const f1 = readValidationFormula(kid(dv, 'formula1')?.textContent ?? null, allow);
1569
+ const f2 = readValidationFormula(kid(dv, 'formula2')?.textContent ?? null, allow);
1570
+ if (f1 !== undefined)
1571
+ rule.value1 = f1;
1572
+ if (f2 !== undefined)
1573
+ rule.value2 = f2;
1574
+ entry.validation.push(rule);
1575
+ }
1576
+ // Comments, through the sheet's relationships: a threaded part wins
1577
+ // over the legacy note that stands in for it.
1578
+ const relsPath = path.replace(/worksheets\/([^/]+)$/, 'worksheets/_rels/$1.rels');
1579
+ const rels = readRels(parts, relsPath, path);
1580
+ // Sparklines, out of the worksheet's extension list.
1581
+ const sparklineGroups = [];
1582
+ for (const group of root.getElementsByTagNameNS(NS_X14, 'sparklineGroup')) {
1583
+ const cells = [];
1584
+ for (const one of group.getElementsByTagNameNS(NS_X14, 'sparkline')) {
1585
+ const formula = one.getElementsByTagNameNS(NS_XM, 'f')[0]?.textContent ?? '';
1586
+ const at = one.getElementsByTagNameNS(NS_XM, 'sqref')[0]?.textContent ?? '';
1587
+ const data = rectOfRef(formula);
1588
+ const cell = parseA1(at.replace(/\$/g, '').trim());
1589
+ if (!data || !cell || cell.row === null)
1590
+ continue;
1591
+ cells.push({ row: cell.row, col: cell.col, data: data.rect });
1592
+ }
1593
+ if (!cells.length)
1594
+ continue;
1595
+ // One group covers a run of cells and the block they read; both come
1596
+ // back as the outer rectangle of the entries, which is how the group
1597
+ // was written.
1598
+ const location = cells.reduce((box, c) => [Math.min(box[0], c.row), Math.min(box[1], c.col), Math.max(box[2], c.row), Math.max(box[3], c.col)], [cells[0].row, cells[0].col, cells[0].row, cells[0].col]);
1599
+ const data = cells.reduce((box, c) => [Math.min(box[0], c.data[0]), Math.min(box[1], c.data[1]), Math.max(box[2], c.data[2]), Math.max(box[3], c.data[3])], cells[0].data);
1600
+ const kind = attr(group, 'type');
1601
+ const colour = fromArgb(attr(group.getElementsByTagNameNS(NS_X14, 'colorSeries')[0] ?? null, 'rgb'));
1602
+ const negative = fromArgb(attr(group.getElementsByTagNameNS(NS_X14, 'colorNegative')[0] ?? null, 'rgb'));
1603
+ sparklineGroups.push({
1604
+ id: sparklineId(),
1605
+ location,
1606
+ data,
1607
+ type: kind === 'column' ? 'column' : kind === 'stacked' ? 'winloss' : 'line',
1608
+ ...(colour ? { color: colour } : {}),
1609
+ ...(negative ? { negativeColor: negative } : {}),
1610
+ ...(attr(group, 'minAxisType') === 'group' ? { sameScale: true } : {}),
1611
+ ...(attr(group, 'markers') === '1' ? { markers: true } : {}),
1612
+ });
1613
+ }
1614
+ if (sparklineGroups.length)
1615
+ entry.sparklines = sparklineGroups;
1616
+ // The drawing, through the same relationships: the pictures and the
1617
+ // charts anchored over this sheet.
1618
+ const drawingId = kid(root, 'drawing')?.getAttribute('r:id') ?? kid(root, 'drawing')?.getAttributeNS(NS_REL, 'id');
1619
+ const drawingPath = drawingId ? rels.get(drawingId)?.target : undefined;
1620
+ const drawingXml = drawingPath ? parts[drawingPath] : undefined;
1621
+ if (drawingXml && drawingPath) {
1622
+ const objects = objectsFromDrawing(drawingXml, {
1623
+ parse: parseXml,
1624
+ rels: readRels(parts, drawingPath.replace(/drawings\/([^/]+)$/, 'drawings/_rels/$1.rels'), drawingPath),
1625
+ parts,
1626
+ newId: objectId,
1627
+ columnWidth: (col) => entry.columnWidths[colToLetters(col)] ?? 64,
1628
+ });
1629
+ if (objects.length)
1630
+ entry.objects = objects;
1631
+ }
1632
+ // Tables, through the sheet's relationships: the part carries the
1633
+ // range, the header count and whether there is a totals row.
1634
+ for (const part of kids(kid(root, 'tableParts'), 'tablePart')) {
1635
+ const id = part.getAttribute('r:id') ?? part.getAttributeNS(NS_REL, 'id');
1636
+ const target = id ? rels.get(id)?.target : undefined;
1637
+ const xml = target ? parts[target] : undefined;
1638
+ if (!xml)
1639
+ continue;
1640
+ const table = parseXml(xml, target).documentElement;
1641
+ const ref = attr(table, 'ref') ?? '';
1642
+ const [fromText, toText] = ref.split(':');
1643
+ const from = parseA1(fromText ?? '');
1644
+ const to = parseA1(toText ?? fromText ?? '');
1645
+ if (!from || from.row === null || !to || to.row === null)
1646
+ continue;
1647
+ const totals = num(table, 'totalsRowCount') ?? 0;
1648
+ tables.push({
1649
+ name: attr(table, 'displayName') || attr(table, 'name') || `Table${tables.length + 1}`,
1650
+ sheet: name,
1651
+ headerRow: from.row,
1652
+ firstCol: from.col,
1653
+ lastCol: to.col,
1654
+ lastRow: to.row - (totals > 0 ? 1 : 0),
1655
+ hasTotals: totals > 0,
1656
+ // A style Excel knows keeps its name; one it does not, or none at
1657
+ // all, reads as no style rather than as a guess.
1658
+ style: findTableStyle(attr(kid(table, 'tableStyleInfo'), 'name') ?? undefined)?.id ?? NO_TABLE_STYLE,
1659
+ });
1660
+ // A filtered table keeps its filter inside the table part, which is
1661
+ // where LibreOffice leaves it. Without this the arrows are lost on
1662
+ // the way in, though the table itself survives.
1663
+ if (!entry.autoFilter) {
1664
+ const range = refRect(attr(kid(table, 'autoFilter'), 'ref') ?? '');
1665
+ if (range)
1666
+ entry.autoFilter = { range, filters: {} };
1667
+ }
1668
+ }
1669
+ // Hyperlinks: an external one points at a relationship whose target is
1670
+ // the URL, an internal one carries its address as `location`.
1671
+ for (const link of kids(kid(root, 'hyperlinks'), 'hyperlink')) {
1672
+ const at = parseA1(attr(link, 'ref')?.split(':')[0] ?? '');
1673
+ if (!at || at.row === null)
1674
+ continue;
1675
+ const id = link.getAttribute('r:id') ?? link.getAttributeNS(NS_REL, 'id');
1676
+ const target = id ? rawRelTarget(parts, relsPath, id) : attr(link, 'location');
1677
+ // A file is the easiest way to hand someone a link they did not
1678
+ // write. One carrying a scheme the shell will not follow is dropped
1679
+ // on the way in, so it is not in the document to be followed, shown
1680
+ // or written out again.
1681
+ if (!target || !isSafeLinkTarget(target))
1682
+ continue;
1683
+ const tip = attr(link, 'tooltip');
1684
+ entry.links ?? (entry.links = {});
1685
+ const rowId = `r${at.row}`;
1686
+ entry.links[rowId] = { ...(entry.links[rowId] ?? {}), [colToLetters(at.col)]: { target, ...(tip ? { tip } : {}) } };
1687
+ }
1688
+ const threadsPath = [...rels.values()].find((r) => r.type === REL_THREADS)?.target;
1689
+ const threadsXml = threadsPath ? parts[threadsPath] : undefined;
1690
+ const threaded = new Set();
1691
+ if (threadsXml) {
1692
+ const roots = new Map();
1693
+ for (const tc of kids(parseXml(threadsXml, threadsPath).documentElement, 'threadedComment')) {
1694
+ const ref = parseA1(attr(tc, 'ref') ?? '');
1695
+ const id = attr(tc, 'id');
1696
+ if (!ref || ref.row === null || !id)
1697
+ continue;
1698
+ const at = readThreadTime(attr(tc, 'dT'));
1699
+ const author = persons.get(attr(tc, 'personId') ?? '');
1700
+ const item = { text: kid(tc, 'text')?.textContent ?? '', ...(author ? { author } : {}), ...(at ? { at } : {}) };
1701
+ const parentId = attr(tc, 'parentId');
1702
+ const parent = parentId ? roots.get(parentId) : undefined;
1703
+ if (parent) {
1704
+ parent.thread.replies = [...(parent.thread.replies ?? []), item];
1705
+ }
1706
+ else {
1707
+ roots.set(id, { row: ref.row, col: ref.col, thread: { ...item, ...(flag(tc, 'done') ? { resolved: true } : {}) } });
1708
+ }
1709
+ }
1710
+ for (const { row, col, thread } of roots.values()) {
1711
+ const rowId = `r${row}`;
1712
+ entry.comments[rowId] = { ...(entry.comments[rowId] ?? {}), [colToLetters(col)]: thread };
1713
+ threaded.add(`${row},${col}`);
1714
+ }
1715
+ }
1716
+ const commentsPath = [...rels.values()].find((r) => r.type === REL_COMMENTS)?.target;
1717
+ const commentsXml = commentsPath ? parts[commentsPath] : undefined;
1718
+ if (commentsXml) {
1719
+ for (const comment of kids(kid(parseXml(commentsXml, commentsPath).documentElement, 'commentList'), 'comment')) {
1720
+ const ref = parseA1(attr(comment, 'ref') ?? '');
1721
+ if (!ref || ref.row === null || threaded.has(`${ref.row},${ref.col}`))
1722
+ continue;
1723
+ const text = textOf(kid(comment, 'text'));
1724
+ if (!text)
1725
+ continue;
1726
+ const rowId = `r${ref.row}`;
1727
+ entry.comments[rowId] = { ...(entry.comments[rowId] ?? {}), [colToLetters(ref.col)]: text };
1728
+ }
1729
+ }
1730
+ }
1731
+ if (isOutlined(outlineRows) || isOutlined(outlineCols)) {
1732
+ entry.outline = { rows: outlineRows, cols: outlineCols };
1733
+ }
1734
+ sheets.push({ name, cells });
1735
+ entries[name] = entry;
1736
+ });
1737
+ const names = {};
1738
+ for (const dn of kids(kid(wbRoot, 'definedNames'), 'definedName')) {
1739
+ const name = attr(dn, 'name');
1740
+ const refersTo = dn.textContent?.trim();
1741
+ if (!name || !refersTo)
1742
+ continue;
1743
+ // Excel's own names: the print area and the title rows go to their
1744
+ // sheet's page setup; the rest of the underscored ones stay where they were.
1745
+ if (name === '_xlnm.Print_Area' || name === '_xlnm.Print_Titles') {
1746
+ const sheetName = sheets[num(dn, 'localSheetId') ?? -1]?.name;
1747
+ const entry = sheetName ? entries[sheetName] : undefined;
1748
+ if (!entry)
1749
+ continue;
1750
+ entry.pageSetup ?? (entry.pageSetup = defaultPageSetup());
1751
+ const areas = refersTo.split(',').map((piece) => piece.slice(piece.lastIndexOf('!') + 1).replace(/\$/g, ''));
1752
+ if (name === '_xlnm.Print_Area') {
1753
+ const rects = areas.map(refRect).filter((r) => r !== null);
1754
+ if (rects.length)
1755
+ entry.pageSetup.printArea = rects;
1756
+ }
1757
+ else {
1758
+ const rows = /^(\d+):(\d+)$/.exec(areas[0] ?? '');
1759
+ if (rows)
1760
+ entry.pageSetup.printTitleRows = [Number(rows[1]) - 1, Number(rows[2]) - 1];
1761
+ }
1762
+ continue;
1763
+ }
1764
+ if (!name.startsWith('_xlnm.'))
1765
+ names[name] = refersTo;
1766
+ }
1767
+ const activeTab = num(kid(kid(wbRoot, 'bookViews'), 'workbookView'), 'activeTab') ?? 0;
1768
+ const active = sheets[activeTab]?.name ?? sheets[0]?.name ?? 'Sheet1';
1769
+ if (!sheets.length)
1770
+ sheets.push({ name: 'Sheet1', cells: [] });
1771
+ return {
1772
+ version: 1,
1773
+ workbook: { sheets, active, names, ...(tables.length ? { tables } : {}), ...(iteration ? { iteration } : {}) },
1774
+ sheets: entries,
1775
+ };
1776
+ }
1777
+ /**
1778
+ * A picture's bytes travel through the parts map as the `data:` URL the
1779
+ * document holds, because everything else in the package is text and a map
1780
+ * of strings is what the tests read. The zip is where they become bytes
1781
+ * again, and the only place that has to know.
1782
+ */
1783
+ const MEDIA_PREFIX = 'xl/media/';
1784
+ /** The media type a media part's extension stands for. */
1785
+ function mediaTypeOf(path) {
1786
+ const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
1787
+ return extension === 'jpg' || extension === 'jpeg' ? 'image/jpeg' : `image/${extension || 'png'}`;
1788
+ }
1789
+ let zipPromise = null;
1790
+ /** The jszip a caller gave, the one on the page, or the optional peer. */
1791
+ export async function loadZip(given) {
1792
+ if (given)
1793
+ return given;
1794
+ const g = globalThis;
1795
+ if (g.JSZip)
1796
+ return g.JSZip;
1797
+ if (!zipPromise) {
1798
+ zipPromise = (async () => {
1799
+ let mod;
1800
+ try {
1801
+ // @ts-ignore - "jszip" is an optional peerDependency
1802
+ mod = await import('jszip');
1803
+ }
1804
+ catch {
1805
+ throw new Error('@svgrid/enterprise: reading and writing .xlsx and .ods needs the "jszip" peer dependency. Install it with: pnpm add jszip');
1806
+ }
1807
+ return (mod.default ?? mod);
1808
+ })();
1809
+ }
1810
+ return zipPromise;
1811
+ }
1812
+ /** The document as an .xlsx Blob. */
1813
+ export async function documentToXlsx(doc, JSZip) {
1814
+ const Zip = await loadZip(JSZip);
1815
+ const zip = new Zip();
1816
+ for (const [path, content] of Object.entries(documentToXlsxParts(doc))) {
1817
+ if (path.startsWith(MEDIA_PREFIX)) {
1818
+ const comma = content.indexOf(',');
1819
+ zip.file(path, comma >= 0 ? content.slice(comma + 1) : content, { base64: true });
1820
+ }
1821
+ else {
1822
+ zip.file(path, content);
1823
+ }
1824
+ }
1825
+ return zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1826
+ }
1827
+ /** The document an .xlsx file holds, as a saved state. */
1828
+ export async function documentFromXlsx(file, JSZip) {
1829
+ const Zip = await loadZip(JSZip);
1830
+ const zip = await Zip.loadAsync(file);
1831
+ const parts = {};
1832
+ const pending = [];
1833
+ zip.forEach((path, entry) => {
1834
+ if (entry.dir)
1835
+ return;
1836
+ const clean = path.replace(/^\//, '');
1837
+ // A picture comes back as the data URL an image object holds, so the
1838
+ // rest of the reader never has to think about bytes.
1839
+ if (clean.startsWith(MEDIA_PREFIX)) {
1840
+ pending.push(entry.async('base64').then((base64) => { parts[clean] = `data:${mediaTypeOf(clean)};base64,${base64}`; }));
1841
+ return;
1842
+ }
1843
+ if (!/\.(xml|rels|vml)$/i.test(path))
1844
+ return;
1845
+ pending.push(entry.async('string').then((text) => { parts[clean] = text; }));
1846
+ });
1847
+ await Promise.all(pending);
1848
+ return documentFromXlsxParts(parts);
1849
+ }