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