@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,3238 @@
|
|
|
1
|
+
import { tokenize as defaultTokenizer } from "@rowsncolumns/calculator";
|
|
2
|
+
import { CellXfsRegistry, deriveEffectiveNumberFormatFromFormula, } from "@rowsncolumns/spreadsheet-state";
|
|
3
|
+
import { cellRangeToAddress, cellToAddress, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, escapeCharacters, isNil, sortSheetsByIndex, addressToSelection, } from "@rowsncolumns/utils";
|
|
4
|
+
import { defaultSpreadsheetTheme } from "../../excel-parser/theme-default";
|
|
5
|
+
import { imageUrlToBase64 } from "../../helpers";
|
|
6
|
+
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
7
|
+
import { buildOdsSparklineGroupsXml } from "./sparkline-builder";
|
|
8
|
+
const BLANK_LABEL = "(Blanks)";
|
|
9
|
+
const isStyleReference = (value) => !!value && typeof value === "object" && "sid" in value;
|
|
10
|
+
/**
|
|
11
|
+
* Walk a sparse 1-indexed `DimensionProperties` array and return every
|
|
12
|
+
* outline group span. Mirrors the level-transition scan the gutter and
|
|
13
|
+
* collapse hooks use; the difference is that here we also resolve each
|
|
14
|
+
* group's `collapsed` state from its summary cell so the ODS exporter
|
|
15
|
+
* can emit `table:display="false"` on the wrapper.
|
|
16
|
+
*
|
|
17
|
+
* `summaryAfter` follows the sheet's `outlinePr.summaryBelow`/
|
|
18
|
+
* `summaryRight` setting — when true (Excel default), the summary cell
|
|
19
|
+
* sits at `endIndex + 1`; when false, at `startIndex - 1`.
|
|
20
|
+
*/
|
|
21
|
+
const findOutlineGroupSpans = (metadata, count, summaryAfter) => {
|
|
22
|
+
const spans = [];
|
|
23
|
+
if (!metadata?.length || count <= 0)
|
|
24
|
+
return spans;
|
|
25
|
+
const levelAt = (i) => i >= 1 && i <= count ? metadata[i]?.outlineLevel ?? 0 : 0;
|
|
26
|
+
const openStarts = new Map();
|
|
27
|
+
let prevLevel = 0;
|
|
28
|
+
// Walk one past `count` so a group that runs to the last row/column
|
|
29
|
+
// still closes cleanly.
|
|
30
|
+
for (let i = 1; i <= count + 1; i++) {
|
|
31
|
+
const curr = levelAt(i);
|
|
32
|
+
if (curr > prevLevel) {
|
|
33
|
+
for (let level = prevLevel + 1; level <= curr; level++) {
|
|
34
|
+
openStarts.set(level, i);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (curr < prevLevel) {
|
|
38
|
+
for (let level = prevLevel; level > curr; level--) {
|
|
39
|
+
const startIndex = openStarts.get(level) ?? i;
|
|
40
|
+
const endIndex = i - 1;
|
|
41
|
+
const summaryIndex = summaryAfter ? i : startIndex - 1;
|
|
42
|
+
const collapsed = !!metadata[summaryIndex]?.collapsed;
|
|
43
|
+
spans.push({ level, startIndex, endIndex, collapsed });
|
|
44
|
+
openStarts.delete(level);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
prevLevel = curr;
|
|
48
|
+
}
|
|
49
|
+
return spans;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Build "opens-at" and "closes-at" indices for the group spans.
|
|
53
|
+
* Returns lookup maps the row/column emitter consults at each index to
|
|
54
|
+
* know how many group tags need to be opened or closed. Opens are
|
|
55
|
+
* sorted outermost-first (lower level number first); closes are sorted
|
|
56
|
+
* innermost-first (higher level number first) so the XML nests properly.
|
|
57
|
+
*/
|
|
58
|
+
const buildOutlineGroupIndex = (spans) => {
|
|
59
|
+
const opensAt = new Map();
|
|
60
|
+
const closesAt = new Map();
|
|
61
|
+
for (const span of spans) {
|
|
62
|
+
const opens = opensAt.get(span.startIndex) ?? [];
|
|
63
|
+
opens.push(span);
|
|
64
|
+
opensAt.set(span.startIndex, opens);
|
|
65
|
+
const closes = closesAt.get(span.endIndex) ?? [];
|
|
66
|
+
closes.push(span);
|
|
67
|
+
closesAt.set(span.endIndex, closes);
|
|
68
|
+
}
|
|
69
|
+
for (const opens of opensAt.values()) {
|
|
70
|
+
opens.sort((a, b) => a.level - b.level);
|
|
71
|
+
}
|
|
72
|
+
for (const closes of closesAt.values()) {
|
|
73
|
+
closes.sort((a, b) => b.level - a.level);
|
|
74
|
+
}
|
|
75
|
+
return { opensAt, closesAt };
|
|
76
|
+
};
|
|
77
|
+
const outlineGroupOpenTag = (span, tag) => {
|
|
78
|
+
const display = span.collapsed ? ` table:display="false"` : "";
|
|
79
|
+
return `<${tag}${display}>`;
|
|
80
|
+
};
|
|
81
|
+
const XML_DECLARATION = `<?xml version="1.0" encoding="UTF-8"?>`;
|
|
82
|
+
const ODS_MIMETYPE = "application/vnd.oasis.opendocument.spreadsheet";
|
|
83
|
+
const CREATOR = "Rows n Columns";
|
|
84
|
+
const PIXELS_PER_INCH = 96;
|
|
85
|
+
const CM_PER_INCH = 2.54;
|
|
86
|
+
const ODS_NS = {
|
|
87
|
+
office: "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
|
88
|
+
style: "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
|
89
|
+
text: "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
|
90
|
+
table: "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
|
|
91
|
+
draw: "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
|
|
92
|
+
fo: "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
|
93
|
+
xlink: "http://www.w3.org/1999/xlink",
|
|
94
|
+
svg: "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
|
|
95
|
+
number: "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
|
|
96
|
+
of: "urn:oasis:names:tc:opendocument:xmlns:of:1.2",
|
|
97
|
+
config: "urn:oasis:names:tc:opendocument:xmlns:config:1.0",
|
|
98
|
+
chart: "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
|
|
99
|
+
calcext: "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0",
|
|
100
|
+
dc: "http://purl.org/dc/elements/1.1/",
|
|
101
|
+
};
|
|
102
|
+
const INVALID_XML_CHAR_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g;
|
|
103
|
+
const textEncoder = new TextEncoder();
|
|
104
|
+
const encodeXml = (content) => textEncoder.encode(content);
|
|
105
|
+
const sanitizeXmlText = (value) => {
|
|
106
|
+
if (!value)
|
|
107
|
+
return value;
|
|
108
|
+
return value.replace(INVALID_XML_CHAR_REGEX, "");
|
|
109
|
+
};
|
|
110
|
+
const escapeXml = (value) => sanitizeXmlText(value)
|
|
111
|
+
.replace(/&/g, "&")
|
|
112
|
+
.replace(/</g, "<")
|
|
113
|
+
.replace(/>/g, ">")
|
|
114
|
+
.replace(/"/g, """)
|
|
115
|
+
.replace(/'/g, "'");
|
|
116
|
+
const escapeXmlAddress = (value) => sanitizeXmlText(value)
|
|
117
|
+
.replace(/&/g, "&")
|
|
118
|
+
.replace(/</g, "<")
|
|
119
|
+
.replace(/>/g, ">")
|
|
120
|
+
.replace(/"/g, """);
|
|
121
|
+
const pxToCm = (px) => (px * CM_PER_INCH) / PIXELS_PER_INCH;
|
|
122
|
+
const DEFAULT_COLUMN_WIDTH_PX = 100;
|
|
123
|
+
const DEFAULT_ROW_HEIGHT_PX = 21;
|
|
124
|
+
class OdsStyleBuilder {
|
|
125
|
+
theme;
|
|
126
|
+
cellStyles = new Map();
|
|
127
|
+
usedStyleNames = new Set();
|
|
128
|
+
rowStyles = new Map();
|
|
129
|
+
columnStyles = new Map();
|
|
130
|
+
textStyles = new Map();
|
|
131
|
+
numberStyles = new Map();
|
|
132
|
+
styleXmlChunks = [];
|
|
133
|
+
numberXmlChunks = [];
|
|
134
|
+
// NEW: separate storage for conditional format styles (go to styles.xml)
|
|
135
|
+
conditionalStyleXmlChunks = [];
|
|
136
|
+
styleIndex = 1;
|
|
137
|
+
numberIndex = 1;
|
|
138
|
+
constructor(theme) {
|
|
139
|
+
this.theme = theme;
|
|
140
|
+
}
|
|
141
|
+
registerCell(format) {
|
|
142
|
+
if (!format)
|
|
143
|
+
return undefined;
|
|
144
|
+
const key = stableStringify(normalizeCellFormat(format));
|
|
145
|
+
const existing = this.cellStyles.get(key);
|
|
146
|
+
if (existing)
|
|
147
|
+
return existing;
|
|
148
|
+
return this.registerCellWithName(format, key);
|
|
149
|
+
}
|
|
150
|
+
registerRow(heightPx, hidden) {
|
|
151
|
+
if (!heightPx && !hidden)
|
|
152
|
+
return undefined;
|
|
153
|
+
const key = stableStringify({ heightPx, hidden });
|
|
154
|
+
const existing = this.rowStyles.get(key);
|
|
155
|
+
if (existing)
|
|
156
|
+
return existing;
|
|
157
|
+
const styleName = `ro${this.styleIndex++}`;
|
|
158
|
+
const styleXml = buildRowStyleXml(styleName, heightPx, hidden);
|
|
159
|
+
this.rowStyles.set(key, styleName);
|
|
160
|
+
this.styleXmlChunks.push(styleXml);
|
|
161
|
+
return styleName;
|
|
162
|
+
}
|
|
163
|
+
registerColumn(widthPx, hidden) {
|
|
164
|
+
if (!widthPx && !hidden)
|
|
165
|
+
return undefined;
|
|
166
|
+
const key = stableStringify({ widthPx, hidden });
|
|
167
|
+
const existing = this.columnStyles.get(key);
|
|
168
|
+
if (existing)
|
|
169
|
+
return existing;
|
|
170
|
+
const styleName = `co${this.styleIndex++}`;
|
|
171
|
+
const styleXml = buildColumnStyleXml(styleName, widthPx, hidden);
|
|
172
|
+
this.columnStyles.set(key, styleName);
|
|
173
|
+
this.styleXmlChunks.push(styleXml);
|
|
174
|
+
return styleName;
|
|
175
|
+
}
|
|
176
|
+
registerText(format) {
|
|
177
|
+
if (!format || Object.keys(format).length === 0)
|
|
178
|
+
return undefined;
|
|
179
|
+
const key = `text_${stableStringify(format)}`;
|
|
180
|
+
const existing = this.textStyles.get(key);
|
|
181
|
+
if (existing)
|
|
182
|
+
return existing;
|
|
183
|
+
const styleName = `T${this.styleIndex++}`;
|
|
184
|
+
const styleXml = buildTextStyleXml(styleName, format, this.theme);
|
|
185
|
+
this.textStyles.set(key, styleName);
|
|
186
|
+
this.styleXmlChunks.push(styleXml);
|
|
187
|
+
return styleName;
|
|
188
|
+
}
|
|
189
|
+
registerConditionalFormat(format, nameHint) {
|
|
190
|
+
if (!format)
|
|
191
|
+
return undefined;
|
|
192
|
+
const key = `cf_${stableStringify(normalizeCellFormat(format))}`;
|
|
193
|
+
const existing = this.cellStyles.get(key);
|
|
194
|
+
if (existing)
|
|
195
|
+
return existing;
|
|
196
|
+
const styleName = this.allocateStyleName(nameHint);
|
|
197
|
+
const styleXml = buildConditionalCellStyleXml(styleName, format, this.theme);
|
|
198
|
+
this.cellStyles.set(key, styleName);
|
|
199
|
+
// Store in separate array for styles.xml
|
|
200
|
+
this.conditionalStyleXmlChunks.push(styleXml);
|
|
201
|
+
return styleName;
|
|
202
|
+
}
|
|
203
|
+
registerNumberStyle(numberFormat) {
|
|
204
|
+
if (!numberFormat)
|
|
205
|
+
return undefined;
|
|
206
|
+
const key = stableStringify(numberFormat);
|
|
207
|
+
const existing = this.numberStyles.get(key);
|
|
208
|
+
if (existing)
|
|
209
|
+
return existing;
|
|
210
|
+
const styleName = `N${this.numberIndex++}`;
|
|
211
|
+
const xml = buildNumberStyleXml(styleName, numberFormat);
|
|
212
|
+
this.numberStyles.set(key, styleName);
|
|
213
|
+
this.numberXmlChunks.push(xml);
|
|
214
|
+
return styleName;
|
|
215
|
+
}
|
|
216
|
+
toAutomaticStylesXml() {
|
|
217
|
+
return `${this.numberXmlChunks.join("")}${this.styleXmlChunks.join("")}`;
|
|
218
|
+
}
|
|
219
|
+
// NEW: Get conditional styles for styles.xml
|
|
220
|
+
toConditionalStylesXml() {
|
|
221
|
+
return this.conditionalStyleXmlChunks.join("");
|
|
222
|
+
}
|
|
223
|
+
registerCellWithName(format, key, nameHint) {
|
|
224
|
+
const existing = this.cellStyles.get(key);
|
|
225
|
+
if (existing)
|
|
226
|
+
return existing;
|
|
227
|
+
const styleName = this.allocateStyleName(nameHint);
|
|
228
|
+
const numberStyleName = format.numberFormat
|
|
229
|
+
? this.registerNumberStyle(format.numberFormat)
|
|
230
|
+
: undefined;
|
|
231
|
+
const styleXml = buildCellStyleXml(styleName, format, numberStyleName, this.theme);
|
|
232
|
+
this.cellStyles.set(key, styleName);
|
|
233
|
+
this.styleXmlChunks.push(styleXml);
|
|
234
|
+
return styleName;
|
|
235
|
+
}
|
|
236
|
+
allocateStyleName(nameHint) {
|
|
237
|
+
if (nameHint && !this.usedStyleNames.has(nameHint)) {
|
|
238
|
+
this.usedStyleNames.add(nameHint);
|
|
239
|
+
return nameHint;
|
|
240
|
+
}
|
|
241
|
+
let name = `ce${this.styleIndex++}`;
|
|
242
|
+
while (this.usedStyleNames.has(name)) {
|
|
243
|
+
name = `ce${this.styleIndex++}`;
|
|
244
|
+
}
|
|
245
|
+
this.usedStyleNames.add(name);
|
|
246
|
+
return name;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// NEW: Build style for conditional formatting (different parent)
|
|
250
|
+
const buildConditionalCellStyleXml = (name, format, theme) => {
|
|
251
|
+
const textProps = [];
|
|
252
|
+
const cellProps = [];
|
|
253
|
+
const textFormat = format.textFormat ?? undefined;
|
|
254
|
+
if (textFormat?.bold) {
|
|
255
|
+
textProps.push(`fo:font-weight="bold"`);
|
|
256
|
+
}
|
|
257
|
+
if (textFormat?.italic) {
|
|
258
|
+
textProps.push(`fo:font-style="italic"`);
|
|
259
|
+
}
|
|
260
|
+
if (textFormat?.underline) {
|
|
261
|
+
textProps.push(`style:text-underline-style="solid"`);
|
|
262
|
+
}
|
|
263
|
+
if (textFormat?.strikethrough) {
|
|
264
|
+
textProps.push(`style:text-line-through-style="solid"`);
|
|
265
|
+
}
|
|
266
|
+
if (textFormat?.baselineOffset === "superscript") {
|
|
267
|
+
textProps.push(`style:text-position="super 58%"`);
|
|
268
|
+
}
|
|
269
|
+
else if (textFormat?.baselineOffset === "subscript") {
|
|
270
|
+
textProps.push(`style:text-position="sub 58%"`);
|
|
271
|
+
}
|
|
272
|
+
const textColor = normalizeColor(textFormat?.color ?? undefined, theme);
|
|
273
|
+
if (textColor) {
|
|
274
|
+
textProps.push(`fo:color="${textColor}"`);
|
|
275
|
+
}
|
|
276
|
+
const background = normalizeColor(format.backgroundColor ?? undefined, theme);
|
|
277
|
+
if (background) {
|
|
278
|
+
cellProps.push(`fo:background-color="${background}"`);
|
|
279
|
+
}
|
|
280
|
+
// Note: parent-style-name="Default" is crucial for LibreOffice to recognize it
|
|
281
|
+
return `<style:style style:name="${name}" style:family="table-cell" style:parent-style-name="Default"><style:table-cell-properties${cellProps.length ? " " + cellProps.join(" ") : ""}/><style:text-properties${textProps.length ? " " + textProps.join(" ") : ""}/></style:style>`;
|
|
282
|
+
};
|
|
283
|
+
export const createODSFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme: _theme, embeds = [], charts = [], namedRanges = [], iterativeCalculationOptions, ignoreFormulaPatterns, citations, locale, }) => {
|
|
284
|
+
const sortedSheets = sortSheetsByIndex(sheets, true);
|
|
285
|
+
const styleBuilder = new OdsStyleBuilder(_theme ?? defaultSpreadsheetTheme);
|
|
286
|
+
const preparedTables = prepareTables(_tables ?? []);
|
|
287
|
+
const embedsBySheet = groupEmbedsBySheet(embeds ?? []);
|
|
288
|
+
const chartsBySheet = groupChartsBySheet(charts ?? []);
|
|
289
|
+
const namedRangeNames = new Set((namedRanges ?? [])
|
|
290
|
+
.map((range) => range.name)
|
|
291
|
+
.filter((name) => typeof name === "string" && isValidNamedRangeName(name))
|
|
292
|
+
.map((name) => name.toUpperCase()));
|
|
293
|
+
const sheetNameById = new Map();
|
|
294
|
+
sortedSheets.forEach((sheet, index) => {
|
|
295
|
+
sheetNameById.set(sheet.sheetId ?? index + 1, sheet.title ?? `Sheet${index + 1}`);
|
|
296
|
+
});
|
|
297
|
+
const dataValidationsById = _dataValidations?.reduce((acc, rule) => {
|
|
298
|
+
if (rule.id != null) {
|
|
299
|
+
acc[String(rule.id)] = rule;
|
|
300
|
+
}
|
|
301
|
+
return acc;
|
|
302
|
+
}, {}) ?? undefined;
|
|
303
|
+
const mediaEntries = [];
|
|
304
|
+
const sheetXmlEntries = [];
|
|
305
|
+
let chartObjectIndex = 1;
|
|
306
|
+
for (const sheet of sortedSheets) {
|
|
307
|
+
const rows = getRowsForSheet(sheetData, sheet.sheetId);
|
|
308
|
+
const sheetTables = preparedTables.filter((table) => table.sheetId === sheet.sheetId);
|
|
309
|
+
const sheetConditionalFormats = (_conditionalFormats ?? []).filter((rule) => rule.ranges?.some((range) => range.sheetId === sheet.sheetId));
|
|
310
|
+
const embedImages = embedsBySheet.get(sheet.sheetId) ?? [];
|
|
311
|
+
const sheetCharts = chartsBySheet.get(sheet.sheetId) ?? [];
|
|
312
|
+
const sheetName = sheetNameById.get(sheet.sheetId ?? 0) ?? sheet.title ?? "Sheet";
|
|
313
|
+
const sheetCitations = citations?.filter((citation) => citation.range.sheetId === sheet.sheetId);
|
|
314
|
+
const result = await buildWorksheetXml({
|
|
315
|
+
sheet,
|
|
316
|
+
sheetName,
|
|
317
|
+
rows,
|
|
318
|
+
sourceSharedStrings,
|
|
319
|
+
cellXfs,
|
|
320
|
+
styleBuilder,
|
|
321
|
+
tables: sheetTables,
|
|
322
|
+
conditionalFormats: sheetConditionalFormats,
|
|
323
|
+
dataValidationsById,
|
|
324
|
+
embeds: embedImages,
|
|
325
|
+
charts: sheetCharts,
|
|
326
|
+
mediaEntries,
|
|
327
|
+
namedRangeNames,
|
|
328
|
+
sheetNameById,
|
|
329
|
+
chartObjectIndex,
|
|
330
|
+
ignoreFormulaPatterns,
|
|
331
|
+
citations: sheetCitations,
|
|
332
|
+
sheetData,
|
|
333
|
+
sheets: sortedSheets,
|
|
334
|
+
locale,
|
|
335
|
+
});
|
|
336
|
+
chartObjectIndex = result.nextChartObjectIndex;
|
|
337
|
+
sheetXmlEntries.push({
|
|
338
|
+
xml: result.xml,
|
|
339
|
+
databaseRanges: result.databaseRanges,
|
|
340
|
+
chartObjects: result.chartObjects,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const validationEntries = buildAllDataValidationEntries(_dataValidations ?? [], sheetNameById);
|
|
344
|
+
const contentValidationsXml = validationEntries.length
|
|
345
|
+
? `<table:content-validations>${validationEntries.join("")}</table:content-validations>`
|
|
346
|
+
: "";
|
|
347
|
+
const calculationSettingsXml = buildCalculationSettingsXml(iterativeCalculationOptions);
|
|
348
|
+
const contentXml = buildContentXml({
|
|
349
|
+
sheetsXml: sheetXmlEntries.map((entry) => entry.xml).join(""),
|
|
350
|
+
databaseRangesXml: buildDatabaseRangesXml(sheetXmlEntries.flatMap((entry) => entry.databaseRanges)),
|
|
351
|
+
namedRangesXml: buildNamedRangesXml(namedRanges, sheetNameById, namedRangeNames),
|
|
352
|
+
contentValidationsXml,
|
|
353
|
+
automaticStylesXml: styleBuilder.toAutomaticStylesXml(),
|
|
354
|
+
calculationSettingsXml,
|
|
355
|
+
});
|
|
356
|
+
const stylesXml = buildStylesXml(styleBuilder.toConditionalStylesXml());
|
|
357
|
+
const metaXml = buildMetaXml();
|
|
358
|
+
const settingsXml = buildSettingsXml(sortedSheets, _theme ?? defaultSpreadsheetTheme);
|
|
359
|
+
const chartObjects = sheetXmlEntries.flatMap((entry) => entry.chartObjects);
|
|
360
|
+
const manifestXml = buildManifestXml(mediaEntries, chartObjects, Boolean(settingsXml));
|
|
361
|
+
const files = {
|
|
362
|
+
"content.xml": encodeXml(contentXml),
|
|
363
|
+
"styles.xml": encodeXml(stylesXml),
|
|
364
|
+
"meta.xml": encodeXml(metaXml),
|
|
365
|
+
"META-INF/manifest.xml": encodeXml(manifestXml),
|
|
366
|
+
};
|
|
367
|
+
if (settingsXml) {
|
|
368
|
+
files["settings.xml"] = encodeXml(settingsXml);
|
|
369
|
+
}
|
|
370
|
+
for (const chartObject of chartObjects) {
|
|
371
|
+
files[`${chartObject.folder}/content.xml`] = encodeXml(chartObject.contentXml);
|
|
372
|
+
}
|
|
373
|
+
for (const entry of mediaEntries) {
|
|
374
|
+
files[entry.path] = entry.data;
|
|
375
|
+
}
|
|
376
|
+
const orderedEntries = [
|
|
377
|
+
{ name: "mimetype", data: new TextEncoder().encode(ODS_MIMETYPE) },
|
|
378
|
+
...Object.entries(files).map(([name, data]) => ({ name, data })),
|
|
379
|
+
];
|
|
380
|
+
return buildZipStore(orderedEntries);
|
|
381
|
+
};
|
|
382
|
+
const getRowsForSheet = (sheetData, sheetId) => {
|
|
383
|
+
return (sheetData[sheetId] ??
|
|
384
|
+
sheetData[String(sheetId)] ??
|
|
385
|
+
sheetData[Number(sheetId)] ??
|
|
386
|
+
[]);
|
|
387
|
+
};
|
|
388
|
+
const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings, cellXfs, styleBuilder, tables, conditionalFormats, dataValidationsById, embeds, charts, mediaEntries, namedRangeNames, sheetNameById, chartObjectIndex, ignoreFormulaPatterns, citations, sheetData, sheets, locale, }) => {
|
|
389
|
+
const bounds = {
|
|
390
|
+
minRow: Number.POSITIVE_INFINITY,
|
|
391
|
+
maxRow: 0,
|
|
392
|
+
minColumn: Number.POSITIVE_INFINITY,
|
|
393
|
+
maxColumn: 0,
|
|
394
|
+
};
|
|
395
|
+
const rowMetadata = sheet.rowMetadata ?? [];
|
|
396
|
+
const maxRows = Math.max(rows.length, rowMetadata.length);
|
|
397
|
+
const rowXmlEntries = [];
|
|
398
|
+
const mergeState = buildMergeState(sheet.merges ?? []);
|
|
399
|
+
// Build annotations map from citations
|
|
400
|
+
const annotationsMap = new Map();
|
|
401
|
+
if (citations) {
|
|
402
|
+
for (const citation of citations) {
|
|
403
|
+
// Citation ranges are worksheet-based (1-based).
|
|
404
|
+
for (let rowIndex = citation.range.startRowIndex; rowIndex <= citation.range.endRowIndex; rowIndex++) {
|
|
405
|
+
for (let colIndex = citation.range.startColumnIndex; colIndex <= citation.range.endColumnIndex; colIndex++) {
|
|
406
|
+
const address = cellToAddress({ rowIndex, columnIndex: colIndex });
|
|
407
|
+
if (address) {
|
|
408
|
+
annotationsMap.set(address, {
|
|
409
|
+
text: citation.citation_string,
|
|
410
|
+
author: citation.created_by || "System",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const conditionalFormattingXml = conditionalFormats.length
|
|
418
|
+
? buildConditionalFormattingXml({
|
|
419
|
+
sheetName,
|
|
420
|
+
conditionalFormats,
|
|
421
|
+
styleBuilder,
|
|
422
|
+
namedRangeNames,
|
|
423
|
+
})
|
|
424
|
+
: "";
|
|
425
|
+
// Collect row data for sparkline detection
|
|
426
|
+
const rowDataMap = {};
|
|
427
|
+
for (let rowIndex = 1; rowIndex < maxRows; rowIndex++) {
|
|
428
|
+
const row = rows[rowIndex];
|
|
429
|
+
const metadata = rowMetadata[rowIndex];
|
|
430
|
+
const rowXml = await buildWorksheetRow({
|
|
431
|
+
rowNumber: rowIndex,
|
|
432
|
+
row,
|
|
433
|
+
rowMetadata: metadata,
|
|
434
|
+
sourceSharedStrings,
|
|
435
|
+
cellXfs,
|
|
436
|
+
styleBuilder,
|
|
437
|
+
mergeState,
|
|
438
|
+
dataValidationsById,
|
|
439
|
+
mediaEntries,
|
|
440
|
+
sheet,
|
|
441
|
+
namedRangeNames,
|
|
442
|
+
tables,
|
|
443
|
+
ignoreFormulaPatterns,
|
|
444
|
+
annotationsMap,
|
|
445
|
+
sheetData,
|
|
446
|
+
sheets,
|
|
447
|
+
locale,
|
|
448
|
+
});
|
|
449
|
+
rowXmlEntries.push(rowXml);
|
|
450
|
+
// Collect row data for sparkline detection
|
|
451
|
+
if (row?.values) {
|
|
452
|
+
const rowValues = {};
|
|
453
|
+
for (const [colIndex, cell] of Object.entries(row.values)) {
|
|
454
|
+
if (cell) {
|
|
455
|
+
rowValues[parseInt(colIndex, 10)] = cell;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (Object.keys(rowValues).length > 0) {
|
|
459
|
+
rowDataMap[rowIndex] = rowValues;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!rowXml)
|
|
463
|
+
continue;
|
|
464
|
+
if (rowXml.minColumn) {
|
|
465
|
+
bounds.minColumn = Math.min(bounds.minColumn, rowXml.minColumn);
|
|
466
|
+
}
|
|
467
|
+
if (rowXml.maxColumn) {
|
|
468
|
+
bounds.maxColumn = Math.max(bounds.maxColumn, rowXml.maxColumn);
|
|
469
|
+
}
|
|
470
|
+
bounds.minRow = Math.min(bounds.minRow, rowIndex);
|
|
471
|
+
bounds.maxRow = Math.max(bounds.maxRow, rowIndex);
|
|
472
|
+
}
|
|
473
|
+
updateBoundsForMerges(bounds, mergeState);
|
|
474
|
+
// Row entries are 0-based here but the rowMetadata is 1-indexed:
|
|
475
|
+
// rowXmlEntries[0] is row 1, rowXmlEntries[i] is row i + 1.
|
|
476
|
+
const maxRowIndex = rowXmlEntries.length;
|
|
477
|
+
const summaryBelow = sheet.outlinePr?.summaryBelow !== false;
|
|
478
|
+
const rowSpans = findOutlineGroupSpans(rowMetadata, maxRowIndex, summaryBelow);
|
|
479
|
+
const { opensAt: rowOpensAt, closesAt: rowClosesAt } = buildOutlineGroupIndex(rowSpans);
|
|
480
|
+
const rowXmlChunks = [];
|
|
481
|
+
for (let index = 0; index < rowXmlEntries.length; index++) {
|
|
482
|
+
const rowIndex = index + 1;
|
|
483
|
+
for (const span of rowOpensAt.get(rowIndex) ?? []) {
|
|
484
|
+
rowXmlChunks.push(outlineGroupOpenTag(span, "table:table-row-group"));
|
|
485
|
+
}
|
|
486
|
+
const entry = rowXmlEntries[index];
|
|
487
|
+
if (entry) {
|
|
488
|
+
rowXmlChunks.push(entry.xml);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Aggregate consecutive empty rows into `number-rows-repeated`, but
|
|
492
|
+
// never cross a group boundary or include a row that any group
|
|
493
|
+
// opens/closes at — that would put a row inside the wrong wrapper.
|
|
494
|
+
let repeat = 1;
|
|
495
|
+
while (index + repeat < rowXmlEntries.length &&
|
|
496
|
+
!rowXmlEntries[index + repeat]) {
|
|
497
|
+
const nextRowIndex = index + repeat + 1;
|
|
498
|
+
if (rowOpensAt.has(nextRowIndex) || rowClosesAt.has(nextRowIndex - 1)) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
repeat += 1;
|
|
502
|
+
}
|
|
503
|
+
rowXmlChunks.push(repeat > 1
|
|
504
|
+
? `<table:table-row table:number-rows-repeated="${repeat}"/>`
|
|
505
|
+
: `<table:table-row/>`);
|
|
506
|
+
// The break condition keeps closes from landing on any row except
|
|
507
|
+
// the last in the repetition, so we only need to emit them once.
|
|
508
|
+
const lastInRepeat = rowIndex + repeat - 1;
|
|
509
|
+
for (const _ of rowClosesAt.get(lastInRepeat) ?? []) {
|
|
510
|
+
rowXmlChunks.push(`</table:table-row-group>`);
|
|
511
|
+
}
|
|
512
|
+
index += repeat - 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
for (const _ of rowClosesAt.get(rowIndex) ?? []) {
|
|
516
|
+
rowXmlChunks.push(`</table:table-row-group>`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const embedFrames = embeds.length
|
|
520
|
+
? await buildEmbedFramesXml({
|
|
521
|
+
embeds,
|
|
522
|
+
mediaEntries,
|
|
523
|
+
sheetName,
|
|
524
|
+
sheet,
|
|
525
|
+
})
|
|
526
|
+
: [];
|
|
527
|
+
const chartFramesResult = charts.length
|
|
528
|
+
? await buildChartFramesXml({
|
|
529
|
+
charts,
|
|
530
|
+
sheet,
|
|
531
|
+
sheetName,
|
|
532
|
+
sheetNameById,
|
|
533
|
+
startIndex: chartObjectIndex,
|
|
534
|
+
})
|
|
535
|
+
: { frames: [], chartObjects: [], nextIndex: chartObjectIndex };
|
|
536
|
+
const shapesXml = embedFrames.length || chartFramesResult.frames.length
|
|
537
|
+
? `<table:shapes>${[...embedFrames, ...chartFramesResult.frames].join("")}</table:shapes>`
|
|
538
|
+
: "";
|
|
539
|
+
const columnsXml = buildColumnsXml(sheet, styleBuilder, bounds);
|
|
540
|
+
// Build sparkline XML from SPARKLINE formulas
|
|
541
|
+
const sparklineXml = buildOdsSparklineGroupsXml(sheet, sheetName, rowDataMap) ?? "";
|
|
542
|
+
const sheetXml = `<table:table table:name="${escapeXml(sheetName)}">${shapesXml}${columnsXml}${rowXmlChunks.join("")}${conditionalFormattingXml}${sparklineXml}</table:table>`;
|
|
543
|
+
return {
|
|
544
|
+
xml: sheetXml,
|
|
545
|
+
databaseRanges: buildDatabaseRangeEntries(tables, sheetName, sheet, rows),
|
|
546
|
+
chartObjects: chartFramesResult.chartObjects,
|
|
547
|
+
nextChartObjectIndex: chartFramesResult.nextIndex,
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
const buildWorksheetRow = async ({ rowNumber, row, rowMetadata, sourceSharedStrings, cellXfs, styleBuilder, mergeState, dataValidationsById, mediaEntries, sheet, namedRangeNames, tables, ignoreFormulaPatterns, annotationsMap, sheetData, sheets, locale, }) => {
|
|
551
|
+
const values = row?.values ?? [];
|
|
552
|
+
const cellXmlChunks = [];
|
|
553
|
+
let minColumn = Number.POSITIVE_INFINITY;
|
|
554
|
+
let maxColumn = 0;
|
|
555
|
+
const hasHiddenFlag = Boolean(rowMetadata?.hiddenByUser || rowMetadata?.hiddenByFilter);
|
|
556
|
+
const defaultRowHeightPx = getDefaultRowHeightPx(sheet);
|
|
557
|
+
const rowSize = rowMetadata?.size && rowMetadata.size > 0 ? rowMetadata.size : undefined;
|
|
558
|
+
const hasExplicitHeight = Boolean(rowSize && Math.abs(rowSize - defaultRowHeightPx) > 0.01);
|
|
559
|
+
const hasCustomHeight = Boolean(rowSize && rowMetadata?.resizedByUser);
|
|
560
|
+
const rowStyle = styleBuilder.registerRow(rowSize && (hasExplicitHeight || hasCustomHeight) ? rowSize : undefined, hasHiddenFlag);
|
|
561
|
+
const maxColumnIndex = Math.max(values.length - 1, mergeState.maxColumnByRow.get(rowNumber) ?? 0);
|
|
562
|
+
for (let columnIndex = 1; columnIndex <= maxColumnIndex; columnIndex++) {
|
|
563
|
+
if (mergeState.coveredCells.has(`${rowNumber}:${columnIndex}`)) {
|
|
564
|
+
cellXmlChunks.push(`<table:covered-table-cell/>`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const cell = values[columnIndex];
|
|
568
|
+
const mergeSpan = mergeState.anchorSpans.get(`${rowNumber}:${columnIndex}`);
|
|
569
|
+
const cellXml = await buildCellXml({
|
|
570
|
+
rowNumber,
|
|
571
|
+
columnIndex,
|
|
572
|
+
cell,
|
|
573
|
+
sourceSharedStrings,
|
|
574
|
+
cellXfs,
|
|
575
|
+
styleBuilder,
|
|
576
|
+
dataValidationsById,
|
|
577
|
+
mergeSpan,
|
|
578
|
+
mediaEntries,
|
|
579
|
+
sheet,
|
|
580
|
+
namedRangeNames,
|
|
581
|
+
tables,
|
|
582
|
+
ignoreFormulaPatterns,
|
|
583
|
+
annotationsMap,
|
|
584
|
+
sheetData,
|
|
585
|
+
sheets,
|
|
586
|
+
locale,
|
|
587
|
+
});
|
|
588
|
+
if (!cellXml) {
|
|
589
|
+
cellXmlChunks.push(`<table:table-cell/>`);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
cellXmlChunks.push(cellXml);
|
|
593
|
+
minColumn = Math.min(minColumn, columnIndex);
|
|
594
|
+
maxColumn = Math.max(maxColumn, columnIndex);
|
|
595
|
+
}
|
|
596
|
+
const hasCells = cellXmlChunks.length > 0;
|
|
597
|
+
const needsMetadata = hasHiddenFlag || hasExplicitHeight || hasCustomHeight;
|
|
598
|
+
if (!hasCells && !needsMetadata) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
const attrs = [];
|
|
602
|
+
if (rowStyle) {
|
|
603
|
+
attrs.push(`table:style-name="${rowStyle}"`);
|
|
604
|
+
}
|
|
605
|
+
const rowXml = `<table:table-row${attrs.length ? " " + attrs.join(" ") : ""}>${cellXmlChunks.join("")}</table:table-row>`;
|
|
606
|
+
return {
|
|
607
|
+
xml: rowXml,
|
|
608
|
+
minColumn: isFinite(minColumn) ? minColumn : undefined,
|
|
609
|
+
maxColumn: maxColumn || undefined,
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
const buildCellXml = async ({ rowNumber, columnIndex, cell, sourceSharedStrings, cellXfs, styleBuilder, dataValidationsById, mergeSpan, mediaEntries, sheet, namedRangeNames, tables, ignoreFormulaPatterns, annotationsMap, sheetData, sheets, locale, }) => {
|
|
613
|
+
const normalizedCell = cell;
|
|
614
|
+
const userEnteredValue = getCellUserEnteredValue(normalizedCell);
|
|
615
|
+
const effectiveValue = getCellEffectiveValue(normalizedCell) ?? userEnteredValue;
|
|
616
|
+
// Match Excel's model: only the user-entered format is serialized.
|
|
617
|
+
// Auto-derived alignment / pattern color / formula precedent format are
|
|
618
|
+
// recomputed at render time. Legacy `ef` is read as a fallback so old
|
|
619
|
+
// files still export with the formatting they originally had.
|
|
620
|
+
const resolveStyleRef = (fmt) => {
|
|
621
|
+
if (!fmt)
|
|
622
|
+
return undefined;
|
|
623
|
+
if (isStyleReference(fmt)) {
|
|
624
|
+
const styleId = fmt.sid ?? fmt.styleId;
|
|
625
|
+
return styleId ? cellXfs?.get(styleId) : undefined;
|
|
626
|
+
}
|
|
627
|
+
return fmt;
|
|
628
|
+
};
|
|
629
|
+
let effectiveFormat = resolveStyleRef(getCellUserEnteredFormat(normalizedCell)) ??
|
|
630
|
+
resolveStyleRef(getCellEffectiveFormat(normalizedCell));
|
|
631
|
+
let formattedValue = getCellFormattedValue(normalizedCell);
|
|
632
|
+
let richTextRuns;
|
|
633
|
+
// The data-validation Invalid marker is UI-only and must never export as
|
|
634
|
+
// a real error cell — strip it so the cell's underlying value exports.
|
|
635
|
+
const valueSource = omitInvalidDataValidationError(effectiveValue) ??
|
|
636
|
+
omitInvalidDataValidationError(userEnteredValue);
|
|
637
|
+
const numberValue = getExtendedValueNumber(valueSource);
|
|
638
|
+
const boolValue = getExtendedValueBool(valueSource);
|
|
639
|
+
const rawFormulaValue = getExtendedValueFormula(userEnteredValue);
|
|
640
|
+
const formulaValue = shouldIgnoreFormula(rawFormulaValue, ignoreFormulaPatterns)
|
|
641
|
+
? undefined
|
|
642
|
+
: rawFormulaValue;
|
|
643
|
+
const errorValue = getExtendedValueError(valueSource);
|
|
644
|
+
let stringValue = getExtendedValueString(valueSource);
|
|
645
|
+
const sharedStringIndex = normalizedCell?.ss;
|
|
646
|
+
const sharedStringValue = sharedStringIndex != null && sourceSharedStrings
|
|
647
|
+
? sourceSharedStrings.get(String(sharedStringIndex))
|
|
648
|
+
: null;
|
|
649
|
+
if (sharedStringValue != null) {
|
|
650
|
+
stringValue =
|
|
651
|
+
typeof sharedStringValue === "string"
|
|
652
|
+
? sharedStringValue
|
|
653
|
+
: sharedStringValue.text ?? "";
|
|
654
|
+
richTextRuns =
|
|
655
|
+
typeof sharedStringValue === "object" &&
|
|
656
|
+
Array.isArray(sharedStringValue.runs)
|
|
657
|
+
? (sharedStringValue.runs)
|
|
658
|
+
: undefined;
|
|
659
|
+
formattedValue = undefined;
|
|
660
|
+
}
|
|
661
|
+
const isDateLike = effectiveFormat?.numberFormat?.type === "DATE" ||
|
|
662
|
+
effectiveFormat?.numberFormat?.type === "DATE_TIME";
|
|
663
|
+
if (isDateLike && formattedValue) {
|
|
664
|
+
stringValue = formattedValue;
|
|
665
|
+
}
|
|
666
|
+
if (formulaValue &&
|
|
667
|
+
(stringValue != null || formattedValue != null) &&
|
|
668
|
+
stringValue == null) {
|
|
669
|
+
stringValue = String(formattedValue ?? "");
|
|
670
|
+
}
|
|
671
|
+
const hasExplicitValue = !isNil(numberValue) ||
|
|
672
|
+
!isNil(boolValue) ||
|
|
673
|
+
!isNil(formulaValue) ||
|
|
674
|
+
!isNil(errorValue) ||
|
|
675
|
+
stringValue != null;
|
|
676
|
+
if (!hasExplicitValue && formattedValue === "") {
|
|
677
|
+
formattedValue = undefined;
|
|
678
|
+
}
|
|
679
|
+
// For formula cells with no explicit number format, derive the format
|
|
680
|
+
// from precedents (same logic as the renderer) so ODS consumers see the
|
|
681
|
+
// value with its inherited format without requiring a recalc.
|
|
682
|
+
let formatForExport = effectiveFormat;
|
|
683
|
+
if (formulaValue &&
|
|
684
|
+
!formatForExport?.numberFormat &&
|
|
685
|
+
sheetData &&
|
|
686
|
+
sheets &&
|
|
687
|
+
sheet?.sheetId != null) {
|
|
688
|
+
const sheetIdByName = new Map();
|
|
689
|
+
for (const s of sheets) {
|
|
690
|
+
if (s?.title)
|
|
691
|
+
sheetIdByName.set(s.title, s.sheetId);
|
|
692
|
+
}
|
|
693
|
+
// A real CellXfsRegistry so the derive helper can resolve precedent
|
|
694
|
+
// cells whose `uf` is a `{ sid }` style reference.
|
|
695
|
+
const cellXfsRegistry = cellXfs ? new CellXfsRegistry(cellXfs) : undefined;
|
|
696
|
+
const derived = deriveEffectiveNumberFormatFromFormula(formulaValue, sheet.sheetId, rowNumber, columnIndex, sheetData, (input) => defaultTokenizer(input), (name) => sheetIdByName.get(name) ?? sheet.sheetId, locale, cellXfsRegistry, undefined, typeof numberValue === "number" ? numberValue : null) ?? undefined;
|
|
697
|
+
if (derived) {
|
|
698
|
+
formatForExport = {
|
|
699
|
+
...(formatForExport ?? {}),
|
|
700
|
+
numberFormat: derived,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const cellStyle = styleBuilder.registerCell(formatForExport);
|
|
705
|
+
const address = cellToAddress({
|
|
706
|
+
rowIndex: rowNumber,
|
|
707
|
+
columnIndex,
|
|
708
|
+
});
|
|
709
|
+
if (!address)
|
|
710
|
+
return undefined;
|
|
711
|
+
const validationRule = resolveCellDataValidation(normalizedCell, dataValidationsById);
|
|
712
|
+
const validationId = validationRule?.id != null ? String(validationRule.id) : "";
|
|
713
|
+
const validationAttr = validationId
|
|
714
|
+
? ` table:content-validation-name="${escapeXml(validationId)}"`
|
|
715
|
+
: "";
|
|
716
|
+
const attrs = [];
|
|
717
|
+
if (cellStyle) {
|
|
718
|
+
attrs.push(`table:style-name="${cellStyle}"`);
|
|
719
|
+
}
|
|
720
|
+
if (mergeSpan) {
|
|
721
|
+
if (mergeSpan.columns > 1) {
|
|
722
|
+
attrs.push(`table:number-columns-spanned="${mergeSpan.columns}"`);
|
|
723
|
+
}
|
|
724
|
+
if (mergeSpan.rows > 1) {
|
|
725
|
+
attrs.push(`table:number-rows-spanned="${mergeSpan.rows}"`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
let valueAttr = "";
|
|
729
|
+
let textNode = "";
|
|
730
|
+
if (errorValue) {
|
|
731
|
+
const errorText = typeof errorValue === "string"
|
|
732
|
+
? errorValue
|
|
733
|
+
: errorValue?.type || errorValue?.message || "#VALUE!";
|
|
734
|
+
valueAttr = ` office:value-type="string" office:string-value="${escapeXml(errorText)}"`;
|
|
735
|
+
textNode = `<text:p>${escapeXml(errorText)}</text:p>`;
|
|
736
|
+
}
|
|
737
|
+
else if (!isNil(numberValue)) {
|
|
738
|
+
valueAttr = ` office:value-type="float" office:value="${numberValue}"`;
|
|
739
|
+
if (stringValue != null) {
|
|
740
|
+
textNode = `<text:p>${escapeXml(String(stringValue))}</text:p>`;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else if (!isNil(boolValue)) {
|
|
744
|
+
valueAttr = ` office:value-type="boolean" office:boolean-value="${boolValue ? "true" : "false"}"`;
|
|
745
|
+
}
|
|
746
|
+
else if (stringValue != null || formattedValue != null) {
|
|
747
|
+
const text = stringValue ?? formattedValue;
|
|
748
|
+
if (text == null)
|
|
749
|
+
return undefined;
|
|
750
|
+
valueAttr = ` office:value-type="string" office:string-value="${escapeXml(String(text))}"`;
|
|
751
|
+
textNode = buildRichTextParagraph(String(text), richTextRuns, styleBuilder);
|
|
752
|
+
}
|
|
753
|
+
if (formulaValue) {
|
|
754
|
+
const resolvedFormula = resolveStructuredReferencesForOds(formulaValue, tables, rowNumber, columnIndex);
|
|
755
|
+
// If formula contains property references (e.g., =A1.propertyName),
|
|
756
|
+
// skip exporting unsupported formula and use formatted value instead
|
|
757
|
+
if (!hasPropertyReference(resolvedFormula)) {
|
|
758
|
+
const formulaAttr = ` table:formula="${escapeXml(toOdsFormula(resolvedFormula, namedRangeNames))}"`;
|
|
759
|
+
if (!valueAttr) {
|
|
760
|
+
valueAttr = ` office:value-type="string" office:string-value=""`;
|
|
761
|
+
}
|
|
762
|
+
valueAttr = `${formulaAttr}${valueAttr}`;
|
|
763
|
+
// Array formula anchor: emit ODF matrix-spanned attributes derived
|
|
764
|
+
// from the A1 spill ref. Spilled cells in the matrix carry no `ue`
|
|
765
|
+
// formula and are emitted as ordinary cells with their cached values.
|
|
766
|
+
const arrayRef = normalizedCell?.af;
|
|
767
|
+
if (arrayRef) {
|
|
768
|
+
const spill = addressToSelection(arrayRef);
|
|
769
|
+
if (spill) {
|
|
770
|
+
const cols = spill.range.endColumnIndex - spill.range.startColumnIndex + 1;
|
|
771
|
+
const rows = spill.range.endRowIndex - spill.range.startRowIndex + 1;
|
|
772
|
+
if (cols > 0) {
|
|
773
|
+
attrs.push(`table:number-matrix-columns-spanned="${cols}"`);
|
|
774
|
+
}
|
|
775
|
+
if (rows > 0) {
|
|
776
|
+
attrs.push(`table:number-matrix-rows-spanned="${rows}"`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const hyperlinkValue = normalizeHyperlinkValue(normalizedCell?.hyperlink);
|
|
783
|
+
if (hyperlinkValue) {
|
|
784
|
+
const href = normalizeOdsHyperlinkHref(hyperlinkValue);
|
|
785
|
+
const display = hyperlinkValue.display ?? stringValue ?? formattedValue ?? "";
|
|
786
|
+
if (!valueAttr) {
|
|
787
|
+
valueAttr = ` office:value-type="string" office:string-value="${escapeXml(String(display ?? ""))}"`;
|
|
788
|
+
}
|
|
789
|
+
textNode = `<text:p><text:a xlink:href="${escapeXml(href)}" xlink:type="simple">${escapeXml(String(display ?? ""))}</text:a></text:p>`;
|
|
790
|
+
}
|
|
791
|
+
const imageXml = await buildCellImageXml({
|
|
792
|
+
imageUrl: normalizedCell?.imageUrl,
|
|
793
|
+
rowNumber,
|
|
794
|
+
columnIndex,
|
|
795
|
+
mergeSpan,
|
|
796
|
+
sheet,
|
|
797
|
+
mediaEntries,
|
|
798
|
+
});
|
|
799
|
+
// Build annotation XML if this cell has a citation
|
|
800
|
+
let annotationXml = "";
|
|
801
|
+
if (annotationsMap && address) {
|
|
802
|
+
const annotation = annotationsMap.get(address);
|
|
803
|
+
if (annotation) {
|
|
804
|
+
const now = new Date().toISOString();
|
|
805
|
+
annotationXml = `<office:annotation><dc:creator>${escapeXml(annotation.author)}</dc:creator><dc:date>${now}</dc:date><text:p>${escapeXml(annotation.text)}</text:p></office:annotation>`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (!valueAttr && !cellStyle && !textNode && !imageXml && !annotationXml) {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
return `<table:table-cell${valueAttr}${validationAttr}${attrs.length ? " " + attrs.join(" ") : ""}>${annotationXml}${textNode}${imageXml}</table:table-cell>`;
|
|
812
|
+
};
|
|
813
|
+
const normalizeHyperlinkValue = (hyperlink) => {
|
|
814
|
+
if (!hyperlink) {
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
if (typeof hyperlink === "string") {
|
|
818
|
+
return { kind: "external", url: hyperlink };
|
|
819
|
+
}
|
|
820
|
+
return hyperlink;
|
|
821
|
+
};
|
|
822
|
+
const normalizeOdsHyperlinkHref = (hyperlink) => {
|
|
823
|
+
if (hyperlink.kind === "external") {
|
|
824
|
+
return hyperlink.url;
|
|
825
|
+
}
|
|
826
|
+
let location = hyperlink.location.trim();
|
|
827
|
+
if (location.startsWith("#")) {
|
|
828
|
+
location = location.slice(1);
|
|
829
|
+
}
|
|
830
|
+
if (location.includes("!")) {
|
|
831
|
+
location = location.replace("!", ".");
|
|
832
|
+
}
|
|
833
|
+
return `#${location}`;
|
|
834
|
+
};
|
|
835
|
+
const shouldIgnoreFormula = (formula, patterns) => {
|
|
836
|
+
if (!formula)
|
|
837
|
+
return false;
|
|
838
|
+
// Always ignore SPARKLINE formulas - they're exported as native ODS sparklines
|
|
839
|
+
if (formula.toUpperCase().startsWith("=SPARKLINE(")) {
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
if (!patterns)
|
|
843
|
+
return false;
|
|
844
|
+
const normalized = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
845
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
846
|
+
return list.some((pattern) => pattern.test(formula) || pattern.test(normalized));
|
|
847
|
+
};
|
|
848
|
+
const hasPropertyReference = (formula) => {
|
|
849
|
+
if (!formula)
|
|
850
|
+
return false;
|
|
851
|
+
// Detects cell references with property access: A1.property, $A$1.nested.value, etc.
|
|
852
|
+
const propertyReferencePattern = /[$]?[A-Za-z]{1,3}[$]?[1-9][0-9]*\./;
|
|
853
|
+
return propertyReferencePattern.test(formula);
|
|
854
|
+
};
|
|
855
|
+
const resolveStructuredReferencesForOds = (formula, tables, rowNumber, columnIndex) => {
|
|
856
|
+
if (!formula || !tables.length)
|
|
857
|
+
return formula;
|
|
858
|
+
const stringRegex = /"([^"]|"")*"/g;
|
|
859
|
+
let result = "";
|
|
860
|
+
let lastIndex = 0;
|
|
861
|
+
let match;
|
|
862
|
+
while ((match = stringRegex.exec(formula)) !== null) {
|
|
863
|
+
const before = formula.slice(lastIndex, match.index);
|
|
864
|
+
result += resolveStructuredRefsInSegment(before, tables, rowNumber, columnIndex);
|
|
865
|
+
result += match[0];
|
|
866
|
+
lastIndex = match.index + match[0].length;
|
|
867
|
+
}
|
|
868
|
+
result += resolveStructuredRefsInSegment(formula.slice(lastIndex), tables, rowNumber, columnIndex);
|
|
869
|
+
return result;
|
|
870
|
+
};
|
|
871
|
+
const resolveStructuredRefsInSegment = (segment, tables, rowNumber, columnIndex) => {
|
|
872
|
+
if (!segment)
|
|
873
|
+
return segment;
|
|
874
|
+
let normalized = segment;
|
|
875
|
+
const tableForCell = findTableForCell(tables, rowNumber, columnIndex);
|
|
876
|
+
for (const table of tables) {
|
|
877
|
+
if (!table.columns?.length)
|
|
878
|
+
continue;
|
|
879
|
+
if (!isRowInTableDataRange(table, rowNumber))
|
|
880
|
+
continue;
|
|
881
|
+
const nameVariants = getTableNameVariants(table);
|
|
882
|
+
for (let idx = 0; idx < table.columns.length; idx++) {
|
|
883
|
+
const columnName = table.columns[idx]?.name;
|
|
884
|
+
if (!columnName)
|
|
885
|
+
continue;
|
|
886
|
+
const columnIndex = table.range.startColumnIndex + idx;
|
|
887
|
+
const cellRef = `${columnIndexToLetter(columnIndex)}${rowNumber}`;
|
|
888
|
+
const escapedColumn = escapeCharacters(columnName);
|
|
889
|
+
if (!escapedColumn)
|
|
890
|
+
continue;
|
|
891
|
+
for (const tableName of nameVariants) {
|
|
892
|
+
const escapedTable = escapeCharacters(tableName);
|
|
893
|
+
if (!escapedTable)
|
|
894
|
+
continue;
|
|
895
|
+
const patterns = [
|
|
896
|
+
`${escapedTable}\\[\\[#This Row\\],\\[${escapedColumn}\\]\\]`,
|
|
897
|
+
`${escapedTable}\\[\\@\\[${escapedColumn}\\]\\]`,
|
|
898
|
+
`${escapedTable}\\[\\@${escapedColumn}\\]`,
|
|
899
|
+
];
|
|
900
|
+
for (const pattern of patterns) {
|
|
901
|
+
normalized = normalized.replace(new RegExp(pattern, "g"), cellRef);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (tableForCell?.id !== table.id) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const unqualifiedPatterns = [
|
|
908
|
+
`\\[\\[#This Row\\],\\[${escapedColumn}\\]\\]`,
|
|
909
|
+
`\\[\\@\\[${escapedColumn}\\]\\]`,
|
|
910
|
+
`\\[\\@${escapedColumn}\\]`,
|
|
911
|
+
];
|
|
912
|
+
for (const pattern of unqualifiedPatterns) {
|
|
913
|
+
normalized = normalized.replace(new RegExp(pattern, "g"), cellRef);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return normalized;
|
|
918
|
+
};
|
|
919
|
+
const getTableNameVariants = (table) => {
|
|
920
|
+
const names = new Set();
|
|
921
|
+
if (table.name)
|
|
922
|
+
names.add(table.name);
|
|
923
|
+
if (table.title)
|
|
924
|
+
names.add(table.title);
|
|
925
|
+
return Array.from(names);
|
|
926
|
+
};
|
|
927
|
+
const isRowInTableDataRange = (table, rowNumber) => {
|
|
928
|
+
const headerOffset = table.headerRow === false ? 0 : 1;
|
|
929
|
+
const totalOffset = table.totalRow ? 1 : 0;
|
|
930
|
+
const start = table.range.startRowIndex + headerOffset;
|
|
931
|
+
const end = table.range.endRowIndex - totalOffset;
|
|
932
|
+
return rowNumber >= start && rowNumber <= end;
|
|
933
|
+
};
|
|
934
|
+
const findTableForCell = (tables, rowNumber, columnIndex) => {
|
|
935
|
+
return tables.find((table) => {
|
|
936
|
+
const range = table.range;
|
|
937
|
+
return (rowNumber >= range.startRowIndex &&
|
|
938
|
+
rowNumber <= range.endRowIndex &&
|
|
939
|
+
columnIndex >= range.startColumnIndex &&
|
|
940
|
+
columnIndex <= range.endColumnIndex);
|
|
941
|
+
});
|
|
942
|
+
};
|
|
943
|
+
const toOdsFormula = (formula, namedRangeNames) => {
|
|
944
|
+
const trimmed = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
945
|
+
const converted = convertExcelFormulaToOds(trimmed, namedRangeNames);
|
|
946
|
+
return `of:=${sanitizeXmlText(converted)}`;
|
|
947
|
+
};
|
|
948
|
+
const buildCalculationSettingsXml = (iterativeCalculationOptions) => {
|
|
949
|
+
if (!iterativeCalculationOptions)
|
|
950
|
+
return "";
|
|
951
|
+
const attrs = [
|
|
952
|
+
`table:use-iteration="${iterativeCalculationOptions.enabled ? "true" : "false"}"`,
|
|
953
|
+
];
|
|
954
|
+
if (typeof iterativeCalculationOptions.maxIterations === "number") {
|
|
955
|
+
attrs.push(`table:iteration-count="${Math.max(0, Math.floor(iterativeCalculationOptions.maxIterations))}"`);
|
|
956
|
+
}
|
|
957
|
+
if (typeof iterativeCalculationOptions.maxChange === "number") {
|
|
958
|
+
attrs.push(`table:minimum-step="${iterativeCalculationOptions.maxChange}"`);
|
|
959
|
+
}
|
|
960
|
+
return `<table:calculation-settings ${attrs.join(" ")}/>`;
|
|
961
|
+
};
|
|
962
|
+
const buildContentXml = ({ sheetsXml, databaseRangesXml, namedRangesXml, contentValidationsXml, automaticStylesXml, calculationSettingsXml, }) => {
|
|
963
|
+
return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:style="${ODS_NS.style}" xmlns:text="${ODS_NS.text}" xmlns:table="${ODS_NS.table}" xmlns:draw="${ODS_NS.draw}" xmlns:fo="${ODS_NS.fo}" xmlns:xlink="${ODS_NS.xlink}" xmlns:svg="${ODS_NS.svg}" xmlns:number="${ODS_NS.number}" xmlns:of="${ODS_NS.of}" xmlns:calcext="${ODS_NS.calcext}" xmlns:dc="${ODS_NS.dc}" office:version="1.2"><office:scripts/><office:automatic-styles>${automaticStylesXml}</office:automatic-styles><office:body><office:spreadsheet>${calculationSettingsXml}${contentValidationsXml}${sheetsXml}${namedRangesXml}${databaseRangesXml}</office:spreadsheet></office:body></office:document-content>`;
|
|
964
|
+
};
|
|
965
|
+
const buildStylesXml = (conditionalStylesXml = "") => {
|
|
966
|
+
return `${XML_DECLARATION}<office:document-styles xmlns:office="${ODS_NS.office}" xmlns:style="${ODS_NS.style}" xmlns:text="${ODS_NS.text}" xmlns:table="${ODS_NS.table}" xmlns:draw="${ODS_NS.draw}" xmlns:fo="${ODS_NS.fo}" xmlns:xlink="${ODS_NS.xlink}" xmlns:svg="${ODS_NS.svg}" xmlns:number="${ODS_NS.number}" office:version="1.2"><office:styles><style:style style:name="Default" style:family="table-cell"><style:paragraph-properties fo:margin-left="0cm" fo:margin-right="0cm"/></style:style>${conditionalStylesXml}</office:styles><office:automatic-styles/><office:master-styles/></office:document-styles>`;
|
|
967
|
+
};
|
|
968
|
+
const buildMetaXml = () => {
|
|
969
|
+
const now = new Date().toISOString();
|
|
970
|
+
return `${XML_DECLARATION}<office:document-meta xmlns:office="${ODS_NS.office}" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" office:version="1.2"><office:meta><dc:creator>${CREATOR}</dc:creator><meta:creation-date>${now}</meta:creation-date></office:meta></office:document-meta>`;
|
|
971
|
+
};
|
|
972
|
+
const buildSettingsXml = (sheets, theme) => {
|
|
973
|
+
const entries = sheets
|
|
974
|
+
.map((sheet, index) => {
|
|
975
|
+
const name = sheet.title ?? `Sheet${index + 1}`;
|
|
976
|
+
const frozenRows = sheet.frozenRowCount ?? 0;
|
|
977
|
+
const frozenColumns = sheet.frozenColumnCount ?? 0;
|
|
978
|
+
const showGrid = sheet.showGridLines ?? true;
|
|
979
|
+
const tabColor = normalizeColor(sheet.tabColor ?? undefined, theme);
|
|
980
|
+
const freezeCell = buildFreezeCell(frozenRows, frozenColumns);
|
|
981
|
+
const items = [];
|
|
982
|
+
if (!showGrid) {
|
|
983
|
+
items.push(`<config:config-item config:name="ShowGrid" config:type="boolean">false</config:config-item>`);
|
|
984
|
+
}
|
|
985
|
+
if (freezeCell) {
|
|
986
|
+
items.push(`<config:config-item config:name="FreezeCell" config:type="string">${freezeCell}</config:config-item>`);
|
|
987
|
+
}
|
|
988
|
+
if (typeof frozenRows === "number" && frozenRows > 0) {
|
|
989
|
+
items.push(`<config:config-item config:name="VerticalSplitPosition" config:type="int">${frozenRows}</config:config-item>`);
|
|
990
|
+
}
|
|
991
|
+
if (typeof frozenColumns === "number" && frozenColumns > 0) {
|
|
992
|
+
items.push(`<config:config-item config:name="HorizontalSplitPosition" config:type="int">${frozenColumns}</config:config-item>`);
|
|
993
|
+
}
|
|
994
|
+
if (tabColor) {
|
|
995
|
+
items.push(`<config:config-item config:name="TabColor" config:type="string">${tabColor}</config:config-item>`);
|
|
996
|
+
}
|
|
997
|
+
if (!items.length) {
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
return `<config:config-item-map-entry config:name="${escapeXml(name)}">${items.join("")}</config:config-item-map-entry>`;
|
|
1001
|
+
})
|
|
1002
|
+
.filter((entry) => Boolean(entry));
|
|
1003
|
+
if (!entries.length) {
|
|
1004
|
+
return undefined;
|
|
1005
|
+
}
|
|
1006
|
+
return `${XML_DECLARATION}<office:document-settings xmlns:office="${ODS_NS.office}" xmlns:config="${ODS_NS.config}" office:version="1.2"><office:settings><config:config-item-set config:name="ooo:view-settings"><config:config-item-map-indexed config:name="Tables">${entries.join("")}</config:config-item-map-indexed></config:config-item-set></office:settings></office:document-settings>`;
|
|
1007
|
+
};
|
|
1008
|
+
const buildManifestXml = (mediaEntries, chartObjects, includeSettings) => {
|
|
1009
|
+
const mediaItems = mediaEntries
|
|
1010
|
+
.map((entry) => `<manifest:file-entry manifest:media-type="${entry.mime}" manifest:full-path="${entry.path}"/>`)
|
|
1011
|
+
.join("");
|
|
1012
|
+
const chartEntries = chartObjects
|
|
1013
|
+
.map((entry) => `<manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.chart" manifest:full-path="${entry.folder}"/><manifest:file-entry manifest:media-type="text/xml" manifest:full-path="${entry.folder}/content.xml"/>`)
|
|
1014
|
+
.join("");
|
|
1015
|
+
const settingsEntry = includeSettings
|
|
1016
|
+
? `<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="settings.xml"/>`
|
|
1017
|
+
: "";
|
|
1018
|
+
return `${XML_DECLARATION}<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2"><manifest:file-entry manifest:media-type="${ODS_MIMETYPE}" manifest:full-path="/"/><manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml"/><manifest:file-entry manifest:media-type="text/xml" manifest:full-path="styles.xml"/><manifest:file-entry manifest:media-type="text/xml" manifest:full-path="meta.xml"/>${settingsEntry}${mediaItems}${chartEntries}</manifest:manifest>`;
|
|
1019
|
+
};
|
|
1020
|
+
const buildFreezeCell = (rows, columns) => {
|
|
1021
|
+
if (!rows && !columns)
|
|
1022
|
+
return undefined;
|
|
1023
|
+
const col = columnIndexToLetter(Math.max(1, columns + 1));
|
|
1024
|
+
const row = Math.max(1, rows + 1);
|
|
1025
|
+
return `${col}${row}`;
|
|
1026
|
+
};
|
|
1027
|
+
const buildColumnsXml = (sheet, styleBuilder, bounds) => {
|
|
1028
|
+
const metadata = sheet.columnMetadata ?? [];
|
|
1029
|
+
const maxColumns = Math.max(metadata.length - 1, bounds.maxColumn || 0);
|
|
1030
|
+
if (!maxColumns)
|
|
1031
|
+
return "";
|
|
1032
|
+
const summaryRight = sheet.outlinePr?.summaryRight !== false;
|
|
1033
|
+
const spans = findOutlineGroupSpans(metadata, maxColumns, summaryRight);
|
|
1034
|
+
const { opensAt, closesAt } = buildOutlineGroupIndex(spans);
|
|
1035
|
+
const parts = [];
|
|
1036
|
+
for (let index = 1; index <= maxColumns; index++) {
|
|
1037
|
+
for (const span of opensAt.get(index) ?? []) {
|
|
1038
|
+
parts.push(outlineGroupOpenTag(span, "table:table-column-group"));
|
|
1039
|
+
}
|
|
1040
|
+
const props = metadata[index];
|
|
1041
|
+
const widthPx = getColumnWidthPx(sheet, index);
|
|
1042
|
+
const hidden = Boolean(props?.hiddenByUser || props?.hiddenByFilter);
|
|
1043
|
+
const styleName = styleBuilder.registerColumn(widthPx, hidden);
|
|
1044
|
+
const attrs = [];
|
|
1045
|
+
if (styleName) {
|
|
1046
|
+
attrs.push(`table:style-name="${styleName}"`);
|
|
1047
|
+
}
|
|
1048
|
+
parts.push(`<table:table-column${attrs.length ? " " + attrs.join(" ") : ""}/>`);
|
|
1049
|
+
for (const _ of closesAt.get(index) ?? []) {
|
|
1050
|
+
parts.push(`</table:table-column-group>`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return parts.join("");
|
|
1054
|
+
};
|
|
1055
|
+
const buildCellStyleXml = (name, format, numberStyleName, theme) => {
|
|
1056
|
+
const textProps = [];
|
|
1057
|
+
const paragraphProps = [];
|
|
1058
|
+
const cellProps = [];
|
|
1059
|
+
const textFormat = format.textFormat ?? undefined;
|
|
1060
|
+
if (textFormat?.bold) {
|
|
1061
|
+
textProps.push(`fo:font-weight="bold"`);
|
|
1062
|
+
}
|
|
1063
|
+
if (textFormat?.italic) {
|
|
1064
|
+
textProps.push(`fo:font-style="italic"`);
|
|
1065
|
+
}
|
|
1066
|
+
if (textFormat?.underline) {
|
|
1067
|
+
textProps.push(`style:text-underline-style="solid"`);
|
|
1068
|
+
}
|
|
1069
|
+
if (textFormat?.strikethrough) {
|
|
1070
|
+
textProps.push(`style:text-line-through-style="solid"`);
|
|
1071
|
+
}
|
|
1072
|
+
if (textFormat?.baselineOffset === "superscript") {
|
|
1073
|
+
textProps.push(`style:text-position="super 58%"`);
|
|
1074
|
+
}
|
|
1075
|
+
else if (textFormat?.baselineOffset === "subscript") {
|
|
1076
|
+
textProps.push(`style:text-position="sub 58%"`);
|
|
1077
|
+
}
|
|
1078
|
+
if (textFormat?.fontFamily) {
|
|
1079
|
+
textProps.push(`fo:font-family="${escapeXml(textFormat.fontFamily)}"`);
|
|
1080
|
+
}
|
|
1081
|
+
if (textFormat?.fontSize) {
|
|
1082
|
+
textProps.push(`fo:font-size="${textFormat.fontSize}pt"`);
|
|
1083
|
+
}
|
|
1084
|
+
const textColor = normalizeColor(textFormat?.color ?? undefined, theme);
|
|
1085
|
+
if (textColor) {
|
|
1086
|
+
textProps.push(`fo:color="${textColor}"`);
|
|
1087
|
+
}
|
|
1088
|
+
if (format.horizontalAlignment) {
|
|
1089
|
+
paragraphProps.push(`fo:text-align="${format.horizontalAlignment}"`);
|
|
1090
|
+
}
|
|
1091
|
+
if (format.verticalAlignment) {
|
|
1092
|
+
paragraphProps.push(`style:vertical-align="${mapVerticalAlignment(format.verticalAlignment)}"`);
|
|
1093
|
+
}
|
|
1094
|
+
if (format.wrapStrategy === "wrap") {
|
|
1095
|
+
cellProps.push(`fo:wrap-option="wrap"`);
|
|
1096
|
+
}
|
|
1097
|
+
else if (format.wrapStrategy === "clip") {
|
|
1098
|
+
cellProps.push(`fo:wrap-option="no-wrap"`, `style:overflow-behavior="clip"`);
|
|
1099
|
+
}
|
|
1100
|
+
if (format.indent) {
|
|
1101
|
+
paragraphProps.push(`fo:margin-left="${format.indent}pt"`);
|
|
1102
|
+
}
|
|
1103
|
+
if (format.textRotation === "vertical") {
|
|
1104
|
+
paragraphProps.push(`style:direction="ttb"`);
|
|
1105
|
+
}
|
|
1106
|
+
else if (typeof format.textRotation === "number") {
|
|
1107
|
+
paragraphProps.push(`style:rotation-angle="${format.textRotation}"`);
|
|
1108
|
+
}
|
|
1109
|
+
// ODS doesn't natively model OOXML's 18 pattern types. When a cell has a
|
|
1110
|
+
// non-solid `fillPattern`, prefer the foreground color as the visible
|
|
1111
|
+
// background (matching LibreOffice's behavior on save). Solid / none /
|
|
1112
|
+
// absent fillPattern falls back to the regular backgroundColor path.
|
|
1113
|
+
const fillPatternForBg = format.fillPattern;
|
|
1114
|
+
const useForegroundForBg = fillPatternForBg &&
|
|
1115
|
+
fillPatternForBg !== "none" &&
|
|
1116
|
+
fillPatternForBg !== "solid" &&
|
|
1117
|
+
format.foregroundColor;
|
|
1118
|
+
const background = useForegroundForBg
|
|
1119
|
+
? normalizeColor(format.foregroundColor ?? undefined, theme)
|
|
1120
|
+
: normalizeColor(format.backgroundColor ?? undefined, theme);
|
|
1121
|
+
if (background) {
|
|
1122
|
+
cellProps.push(`fo:background-color="${background}"`);
|
|
1123
|
+
}
|
|
1124
|
+
const borders = format.borders;
|
|
1125
|
+
if (borders) {
|
|
1126
|
+
const top = borderToCss(borders.top ?? undefined, theme);
|
|
1127
|
+
const bottom = borderToCss(borders.bottom ?? undefined, theme);
|
|
1128
|
+
const left = borderToCss(borders.left ?? undefined, theme);
|
|
1129
|
+
const right = borderToCss(borders.right ?? undefined, theme);
|
|
1130
|
+
const diagUp = borderToCss(borders.diagonalUp ?? undefined, theme);
|
|
1131
|
+
const diagDown = borderToCss(borders.diagonalDown ?? undefined, theme);
|
|
1132
|
+
if (top)
|
|
1133
|
+
cellProps.push(`fo:border-top="${top}"`);
|
|
1134
|
+
if (bottom)
|
|
1135
|
+
cellProps.push(`fo:border-bottom="${bottom}"`);
|
|
1136
|
+
if (left)
|
|
1137
|
+
cellProps.push(`fo:border-left="${left}"`);
|
|
1138
|
+
if (right)
|
|
1139
|
+
cellProps.push(`fo:border-right="${right}"`);
|
|
1140
|
+
if (diagUp)
|
|
1141
|
+
cellProps.push(`style:diagonal-bl-tr="${diagUp}"`);
|
|
1142
|
+
if (diagDown)
|
|
1143
|
+
cellProps.push(`style:diagonal-tl-br="${diagDown}"`);
|
|
1144
|
+
}
|
|
1145
|
+
const dataStyleAttr = numberStyleName
|
|
1146
|
+
? ` style:data-style-name="${numberStyleName}"`
|
|
1147
|
+
: "";
|
|
1148
|
+
return `<style:style style:name="${name}" style:family="table-cell"${dataStyleAttr}><style:table-cell-properties${cellProps.length ? " " + cellProps.join(" ") : ""}/><style:paragraph-properties${paragraphProps.length ? " " + paragraphProps.join(" ") : ""}/><style:text-properties${textProps.length ? " " + textProps.join(" ") : ""}/></style:style>`;
|
|
1149
|
+
};
|
|
1150
|
+
const buildTextStyleXml = (name, textFormat, theme) => {
|
|
1151
|
+
const textProps = [];
|
|
1152
|
+
if (textFormat.bold) {
|
|
1153
|
+
textProps.push(`fo:font-weight="bold"`);
|
|
1154
|
+
}
|
|
1155
|
+
if (textFormat.italic) {
|
|
1156
|
+
textProps.push(`fo:font-style="italic"`);
|
|
1157
|
+
}
|
|
1158
|
+
if (textFormat.underline) {
|
|
1159
|
+
textProps.push(`style:text-underline-style="solid"`);
|
|
1160
|
+
}
|
|
1161
|
+
if (textFormat.strikethrough) {
|
|
1162
|
+
textProps.push(`style:text-line-through-style="solid"`);
|
|
1163
|
+
}
|
|
1164
|
+
if (textFormat.baselineOffset === "superscript") {
|
|
1165
|
+
textProps.push(`style:text-position="super 58%"`);
|
|
1166
|
+
}
|
|
1167
|
+
else if (textFormat.baselineOffset === "subscript") {
|
|
1168
|
+
textProps.push(`style:text-position="sub 58%"`);
|
|
1169
|
+
}
|
|
1170
|
+
if (textFormat.fontFamily) {
|
|
1171
|
+
textProps.push(`fo:font-family="${escapeXml(textFormat.fontFamily)}"`);
|
|
1172
|
+
}
|
|
1173
|
+
if (textFormat.fontSize) {
|
|
1174
|
+
textProps.push(`fo:font-size="${textFormat.fontSize}pt"`);
|
|
1175
|
+
}
|
|
1176
|
+
const textColor = normalizeColor(textFormat.color ?? undefined, theme);
|
|
1177
|
+
if (textColor) {
|
|
1178
|
+
textProps.push(`fo:color="${textColor}"`);
|
|
1179
|
+
}
|
|
1180
|
+
return `<style:style style:name="${name}" style:family="text"><style:text-properties${textProps.length ? " " + textProps.join(" ") : ""}/></style:style>`;
|
|
1181
|
+
};
|
|
1182
|
+
const buildRichTextParagraph = (text, runs, styleBuilder) => {
|
|
1183
|
+
if (!runs?.length) {
|
|
1184
|
+
return `<text:p>${escapeXml(String(text))}</text:p>`;
|
|
1185
|
+
}
|
|
1186
|
+
const parts = [];
|
|
1187
|
+
let cursor = 0;
|
|
1188
|
+
const sortedRuns = [...runs].sort((a, b) => a.startIndex - b.startIndex || a.endIndex - b.endIndex);
|
|
1189
|
+
for (const run of sortedRuns) {
|
|
1190
|
+
if (run.nodeType !== "text")
|
|
1191
|
+
continue;
|
|
1192
|
+
const startIndex = Math.max(0, Math.min(text.length, run.startIndex));
|
|
1193
|
+
const endIndex = Math.max(startIndex, Math.min(text.length, run.endIndex));
|
|
1194
|
+
if (endIndex <= cursor)
|
|
1195
|
+
continue;
|
|
1196
|
+
if (cursor < startIndex) {
|
|
1197
|
+
parts.push(escapeXml(text.slice(cursor, startIndex)));
|
|
1198
|
+
}
|
|
1199
|
+
const runText = text.slice(Math.max(cursor, startIndex), endIndex);
|
|
1200
|
+
const styleName = styleBuilder.registerText(run.format);
|
|
1201
|
+
parts.push(styleName
|
|
1202
|
+
? `<text:span text:style-name="${styleName}">${escapeXml(runText)}</text:span>`
|
|
1203
|
+
: escapeXml(runText));
|
|
1204
|
+
cursor = endIndex;
|
|
1205
|
+
}
|
|
1206
|
+
if (cursor < text.length) {
|
|
1207
|
+
parts.push(escapeXml(text.slice(cursor)));
|
|
1208
|
+
}
|
|
1209
|
+
return `<text:p>${parts.join("")}</text:p>`;
|
|
1210
|
+
};
|
|
1211
|
+
const buildRowStyleXml = (name, heightPx, hidden) => {
|
|
1212
|
+
const props = [];
|
|
1213
|
+
if (heightPx) {
|
|
1214
|
+
props.push(`style:row-height="${pxToCm(heightPx).toFixed(2)}cm"`);
|
|
1215
|
+
}
|
|
1216
|
+
if (hidden) {
|
|
1217
|
+
props.push(`style:visibility="collapse"`);
|
|
1218
|
+
}
|
|
1219
|
+
return `<style:style style:name="${name}" style:family="table-row"><style:table-row-properties${props.length ? " " + props.join(" ") : ""}/></style:style>`;
|
|
1220
|
+
};
|
|
1221
|
+
const buildColumnStyleXml = (name, widthPx, hidden) => {
|
|
1222
|
+
const props = [];
|
|
1223
|
+
if (widthPx) {
|
|
1224
|
+
props.push(`style:column-width="${pxToCm(widthPx).toFixed(2)}cm"`);
|
|
1225
|
+
}
|
|
1226
|
+
if (hidden) {
|
|
1227
|
+
props.push(`style:visibility="collapse"`);
|
|
1228
|
+
}
|
|
1229
|
+
return `<style:style style:name="${name}" style:family="table-column"><style:table-column-properties${props.length ? " " + props.join(" ") : ""}/></style:style>`;
|
|
1230
|
+
};
|
|
1231
|
+
const buildNumberStyleXml = (name, format) => {
|
|
1232
|
+
if (!format)
|
|
1233
|
+
return "";
|
|
1234
|
+
const type = format.type;
|
|
1235
|
+
if (type === "PERCENT") {
|
|
1236
|
+
return `<number:percentage-style style:name="${name}"><number:number number:min-integer-digits="1" number:decimal-places="2"/></number:percentage-style>`;
|
|
1237
|
+
}
|
|
1238
|
+
if (type === "CURRENCY" || type === "ACCOUNTING") {
|
|
1239
|
+
return `<number:currency-style style:name="${name}"><number:currency-symbol>$</number:currency-symbol><number:number number:min-integer-digits="1" number:decimal-places="2"/></number:currency-style>`;
|
|
1240
|
+
}
|
|
1241
|
+
if (type === "DATE") {
|
|
1242
|
+
return `<number:date-style style:name="${name}"><number:year number:style="long"/><number:text>-</number:text><number:month number:style="long"/><number:text>-</number:text><number:day number:style="long"/></number:date-style>`;
|
|
1243
|
+
}
|
|
1244
|
+
if (type === "TIME") {
|
|
1245
|
+
return `<number:time-style style:name="${name}"><number:hours/><number:text>:</number:text><number:minutes/><number:text>:</number:text><number:seconds/></number:time-style>`;
|
|
1246
|
+
}
|
|
1247
|
+
if (type === "DATE_TIME") {
|
|
1248
|
+
return `<number:date-style style:name="${name}"><number:year number:style="long"/><number:text>-</number:text><number:month number:style="long"/><number:text>-</number:text><number:day number:style="long"/><number:text> </number:text><number:hours/><number:text>:</number:text><number:minutes/></number:date-style>`;
|
|
1249
|
+
}
|
|
1250
|
+
if (type === "SCIENTIFIC") {
|
|
1251
|
+
return `<number:scientific-number-style style:name="${name}"><number:number number:min-integer-digits="1"/></number:scientific-number-style>`;
|
|
1252
|
+
}
|
|
1253
|
+
return `<number:number-style style:name="${name}"><number:number number:min-integer-digits="1"/></number:number-style>`;
|
|
1254
|
+
};
|
|
1255
|
+
const buildAllDataValidationEntries = (rules, sheetNameById) => {
|
|
1256
|
+
if (!rules.length)
|
|
1257
|
+
return [];
|
|
1258
|
+
const entries = rules
|
|
1259
|
+
.map((rule) => {
|
|
1260
|
+
const rangeAttr = buildRangeListForValidations(rule.ranges ?? [], sheetNameById);
|
|
1261
|
+
if (!rangeAttr)
|
|
1262
|
+
return undefined;
|
|
1263
|
+
const config = convertDataValidationRule(rule);
|
|
1264
|
+
if (!config)
|
|
1265
|
+
return undefined;
|
|
1266
|
+
const baseCellAddress = buildValidationBaseCellAddress(rule.ranges ?? [], sheetNameById);
|
|
1267
|
+
const attrs = [
|
|
1268
|
+
`table:name="${escapeXml(String(rule.id ?? ""))}"`,
|
|
1269
|
+
`table:target-range-address="${escapeXmlAddress(rangeAttr)}"`,
|
|
1270
|
+
`table:condition="${escapeXml(config.condition)}"`,
|
|
1271
|
+
];
|
|
1272
|
+
if (config.allowEmptyCell !== undefined) {
|
|
1273
|
+
attrs.push(`table:allow-empty-cell="${config.allowEmptyCell ? "true" : "false"}"`);
|
|
1274
|
+
}
|
|
1275
|
+
if (config.displayList) {
|
|
1276
|
+
attrs.push(`table:display-list="${config.displayList}"`);
|
|
1277
|
+
}
|
|
1278
|
+
if (baseCellAddress) {
|
|
1279
|
+
attrs.push(`table:base-cell-address="${escapeXmlAddress(baseCellAddress)}"`);
|
|
1280
|
+
}
|
|
1281
|
+
const help = config.helpMessage
|
|
1282
|
+
? `<table:help-message table:title="${escapeXml(config.helpMessage.title ?? "")}" table:message="${escapeXml(config.helpMessage.message ?? "")}"/>`
|
|
1283
|
+
: "";
|
|
1284
|
+
const error = config.errorMessage
|
|
1285
|
+
? `<table:error-message table:message-type="${config.errorMessage.type}" table:display="true" table:title="${escapeXml(config.errorMessage.title ?? "")}" table:message="${escapeXml(config.errorMessage.message ?? "")}"/>`
|
|
1286
|
+
: "";
|
|
1287
|
+
return `<table:content-validation ${attrs.join(" ")}><table:error-message table:message-type="stop" table:display="true"/></table:content-validation>`;
|
|
1288
|
+
})
|
|
1289
|
+
.filter((entry) => Boolean(entry));
|
|
1290
|
+
return entries;
|
|
1291
|
+
};
|
|
1292
|
+
const buildConditionalFormattingXml = ({ conditionalFormats, sheetName, styleBuilder, namedRangeNames, }) => {
|
|
1293
|
+
const entries = [];
|
|
1294
|
+
let styleCounter = 1;
|
|
1295
|
+
const escapeConditionText = (value) => value.replace(/"/g, '""');
|
|
1296
|
+
const ensureFormula = (formula) => formula.startsWith("=") ? formula : `=${formula}`;
|
|
1297
|
+
const stripFormula = (formula) => formula.startsWith("=") ? formula.slice(1) : formula;
|
|
1298
|
+
const buildFormulaConditionValue = (formula) => {
|
|
1299
|
+
const odsFormula = buildOdsConditionExpression(ensureFormula(formula), namedRangeNames);
|
|
1300
|
+
return odsFormula ? `formula-is(${odsFormula})` : undefined;
|
|
1301
|
+
};
|
|
1302
|
+
const orderedRules = conditionalFormats
|
|
1303
|
+
.map((rule, index) => ({ rule, index }))
|
|
1304
|
+
.sort((left, right) => {
|
|
1305
|
+
const leftPriority = left.rule.priority;
|
|
1306
|
+
const rightPriority = right.rule.priority;
|
|
1307
|
+
if (leftPriority == null && rightPriority == null) {
|
|
1308
|
+
return left.index - right.index;
|
|
1309
|
+
}
|
|
1310
|
+
if (leftPriority == null) {
|
|
1311
|
+
return 1;
|
|
1312
|
+
}
|
|
1313
|
+
if (rightPriority == null) {
|
|
1314
|
+
return -1;
|
|
1315
|
+
}
|
|
1316
|
+
if (leftPriority === rightPriority) {
|
|
1317
|
+
return left.index - right.index;
|
|
1318
|
+
}
|
|
1319
|
+
return leftPriority - rightPriority;
|
|
1320
|
+
})
|
|
1321
|
+
.map(({ rule }) => rule);
|
|
1322
|
+
for (const rule of orderedRules) {
|
|
1323
|
+
if (rule.enabled === false)
|
|
1324
|
+
continue;
|
|
1325
|
+
const ranges = rule.ranges ?? [];
|
|
1326
|
+
if (!ranges.length)
|
|
1327
|
+
continue;
|
|
1328
|
+
const rangeAttr = buildCalcextRangeList(ranges, sheetName);
|
|
1329
|
+
if (!rangeAttr)
|
|
1330
|
+
continue;
|
|
1331
|
+
const baseCellAddress = buildCalcextBaseAddress(ranges[0], sheetName);
|
|
1332
|
+
const baseCellRef = cellToAddress({
|
|
1333
|
+
rowIndex: ranges[0].startRowIndex,
|
|
1334
|
+
columnIndex: ranges[0].startColumnIndex,
|
|
1335
|
+
});
|
|
1336
|
+
const absoluteRangeRef = (() => {
|
|
1337
|
+
const range = ranges[0];
|
|
1338
|
+
const startCol = columnIndexToLetter(range.startColumnIndex);
|
|
1339
|
+
const endCol = columnIndexToLetter(range.endColumnIndex);
|
|
1340
|
+
return `$${startCol}$${range.startRowIndex}:$${endCol}$${range.endRowIndex}`;
|
|
1341
|
+
})();
|
|
1342
|
+
const addConditionEntry = (format, conditionValue) => {
|
|
1343
|
+
if (!format || !conditionValue)
|
|
1344
|
+
return;
|
|
1345
|
+
const styleName = styleBuilder.registerConditionalFormat(format, `ConditionalStyle_${styleCounter++}`);
|
|
1346
|
+
if (!styleName)
|
|
1347
|
+
return;
|
|
1348
|
+
const stopIfTrueAttr = rule.stopIfTrue
|
|
1349
|
+
? `calcext:stop-if-true="true"`
|
|
1350
|
+
: "";
|
|
1351
|
+
const conditionAttrs = [
|
|
1352
|
+
`calcext:apply-style-name="${styleName}"`,
|
|
1353
|
+
`calcext:value="${escapeXml(conditionValue)}"`,
|
|
1354
|
+
baseCellAddress
|
|
1355
|
+
? `calcext:base-cell-address="${escapeXml(baseCellAddress)}"`
|
|
1356
|
+
: "",
|
|
1357
|
+
stopIfTrueAttr,
|
|
1358
|
+
]
|
|
1359
|
+
.filter(Boolean)
|
|
1360
|
+
.join(" ");
|
|
1361
|
+
entries.push(`<calcext:conditional-format calcext:target-range-address="${escapeXml(rangeAttr)}"><calcext:condition ${conditionAttrs}/></calcext:conditional-format>`);
|
|
1362
|
+
};
|
|
1363
|
+
const buildTextLiteralCondition = (raw) => `="${escapeConditionText(raw)}"`;
|
|
1364
|
+
const buildBooleanConditionValue = () => {
|
|
1365
|
+
const booleanRule = rule.booleanRule;
|
|
1366
|
+
if (!booleanRule?.condition?.type || !baseCellRef)
|
|
1367
|
+
return undefined;
|
|
1368
|
+
const { condition } = booleanRule;
|
|
1369
|
+
const values = condition.values ?? [];
|
|
1370
|
+
const first = values[0]?.userEnteredValue ?? "";
|
|
1371
|
+
const second = values[1]?.userEnteredValue ?? "";
|
|
1372
|
+
const isFormulaValue = (value) => value.trim().startsWith("=");
|
|
1373
|
+
const isNumericValue = (value) => value.trim() !== "" && !isNaN(Number(value));
|
|
1374
|
+
const normalizeDateOperand = (raw) => {
|
|
1375
|
+
const trimmed = raw.trim();
|
|
1376
|
+
if (!trimmed)
|
|
1377
|
+
return "";
|
|
1378
|
+
if (trimmed.startsWith("=")) {
|
|
1379
|
+
return stripFormula(trimmed);
|
|
1380
|
+
}
|
|
1381
|
+
if (isNumericValue(trimmed)) {
|
|
1382
|
+
return trimmed;
|
|
1383
|
+
}
|
|
1384
|
+
return `DATEVALUE("${escapeConditionText(trimmed)}")`;
|
|
1385
|
+
};
|
|
1386
|
+
if (condition.type === "CUSTOM_FORMULA") {
|
|
1387
|
+
const formula = condition.values?.[0]?.userEnteredValue ?? booleanRule.formula;
|
|
1388
|
+
if (!formula)
|
|
1389
|
+
return undefined;
|
|
1390
|
+
return buildFormulaConditionValue(formula);
|
|
1391
|
+
}
|
|
1392
|
+
if (condition.type === "TEXT_EQ") {
|
|
1393
|
+
return buildTextLiteralCondition(first);
|
|
1394
|
+
}
|
|
1395
|
+
if (condition.type === "TEXT_NOT_EQ") {
|
|
1396
|
+
return `not-equal("${escapeConditionText(first)}")`;
|
|
1397
|
+
}
|
|
1398
|
+
if (condition.type === "TEXT_CONTAINS") {
|
|
1399
|
+
return `contains-text("${escapeConditionText(first)}")`;
|
|
1400
|
+
}
|
|
1401
|
+
if (condition.type === "TEXT_NOT_CONTAINS") {
|
|
1402
|
+
return `not-contains-text("${escapeConditionText(first)}")`;
|
|
1403
|
+
}
|
|
1404
|
+
if (condition.type === "TEXT_STARTS_WITH") {
|
|
1405
|
+
return `begins-with("${escapeConditionText(first)}")`;
|
|
1406
|
+
}
|
|
1407
|
+
if (condition.type === "TEXT_ENDS_WITH") {
|
|
1408
|
+
return `ends-with("${escapeConditionText(first)}")`;
|
|
1409
|
+
}
|
|
1410
|
+
if (condition.type === "NUMBER_BETWEEN" ||
|
|
1411
|
+
condition.type === "NUMBER_NOT_BETWEEN" ||
|
|
1412
|
+
condition.type === "NUMBER_EQ" ||
|
|
1413
|
+
condition.type === "NUMBER_NOT_EQ" ||
|
|
1414
|
+
condition.type === "NUMBER_GREATER" ||
|
|
1415
|
+
condition.type === "NUMBER_GREATER_THAN_EQ" ||
|
|
1416
|
+
condition.type === "NUMBER_LESS" ||
|
|
1417
|
+
condition.type === "NUMBER_LESS_THAN_EQ") {
|
|
1418
|
+
const needsFormula = isFormulaValue(first) ||
|
|
1419
|
+
isFormulaValue(second) ||
|
|
1420
|
+
(!isNumericValue(first) && first) ||
|
|
1421
|
+
(!isNumericValue(second) && second);
|
|
1422
|
+
const operatorMap = {
|
|
1423
|
+
NUMBER_BETWEEN: "between",
|
|
1424
|
+
NUMBER_NOT_BETWEEN: "not-between",
|
|
1425
|
+
NUMBER_EQ: "equal",
|
|
1426
|
+
NUMBER_NOT_EQ: "not-equal",
|
|
1427
|
+
NUMBER_GREATER: "greater-than",
|
|
1428
|
+
NUMBER_GREATER_THAN_EQ: "greater-than-equal",
|
|
1429
|
+
NUMBER_LESS: "less-than",
|
|
1430
|
+
NUMBER_LESS_THAN_EQ: "less-than-equal",
|
|
1431
|
+
};
|
|
1432
|
+
const operator = operatorMap[condition.type];
|
|
1433
|
+
if (!needsFormula && operator) {
|
|
1434
|
+
if ((operator === "between" || operator === "not-between") &&
|
|
1435
|
+
!(first && second)) {
|
|
1436
|
+
return undefined;
|
|
1437
|
+
}
|
|
1438
|
+
if (operator === "between" || operator === "not-between") {
|
|
1439
|
+
return `${operator}(${first},${second})`;
|
|
1440
|
+
}
|
|
1441
|
+
return `${operator}(${first})`;
|
|
1442
|
+
}
|
|
1443
|
+
const firstExpr = first ? stripFormula(first) : "";
|
|
1444
|
+
const secondExpr = second ? stripFormula(second) : "";
|
|
1445
|
+
if ((condition.type === "NUMBER_BETWEEN" ||
|
|
1446
|
+
condition.type === "NUMBER_NOT_BETWEEN") &&
|
|
1447
|
+
(!firstExpr || !secondExpr)) {
|
|
1448
|
+
return undefined;
|
|
1449
|
+
}
|
|
1450
|
+
if (condition.type === "NUMBER_BETWEEN") {
|
|
1451
|
+
return buildFormulaConditionValue(`AND(${baseCellRef}>=${firstExpr},${baseCellRef}<=${secondExpr})`);
|
|
1452
|
+
}
|
|
1453
|
+
if (condition.type === "NUMBER_NOT_BETWEEN") {
|
|
1454
|
+
return buildFormulaConditionValue(`OR(${baseCellRef}<${firstExpr},${baseCellRef}>${secondExpr})`);
|
|
1455
|
+
}
|
|
1456
|
+
const opMap = {
|
|
1457
|
+
NUMBER_EQ: "=",
|
|
1458
|
+
NUMBER_NOT_EQ: "<>",
|
|
1459
|
+
NUMBER_GREATER: ">",
|
|
1460
|
+
NUMBER_GREATER_THAN_EQ: ">=",
|
|
1461
|
+
NUMBER_LESS: "<",
|
|
1462
|
+
NUMBER_LESS_THAN_EQ: "<=",
|
|
1463
|
+
};
|
|
1464
|
+
const op = opMap[condition.type];
|
|
1465
|
+
if (!op || !firstExpr)
|
|
1466
|
+
return undefined;
|
|
1467
|
+
return (buildFormulaConditionValue(`${baseCellRef}${op}${firstExpr}`) ??
|
|
1468
|
+
undefined);
|
|
1469
|
+
}
|
|
1470
|
+
if (condition.type === "DATE_EQ" ||
|
|
1471
|
+
condition.type === "DATE_NOT_EQ" ||
|
|
1472
|
+
condition.type === "DATE_BEFORE" ||
|
|
1473
|
+
condition.type === "DATE_AFTER" ||
|
|
1474
|
+
condition.type === "DATE_ON_OR_BEFORE" ||
|
|
1475
|
+
condition.type === "DATE_ON_OR_AFTER" ||
|
|
1476
|
+
condition.type === "DATE_BETWEEN" ||
|
|
1477
|
+
condition.type === "DATE_NOT_BETWEEN") {
|
|
1478
|
+
const firstExpr = normalizeDateOperand(first);
|
|
1479
|
+
const secondExpr = normalizeDateOperand(second);
|
|
1480
|
+
if ((condition.type === "DATE_BETWEEN" ||
|
|
1481
|
+
condition.type === "DATE_NOT_BETWEEN") &&
|
|
1482
|
+
(!firstExpr || !secondExpr)) {
|
|
1483
|
+
return undefined;
|
|
1484
|
+
}
|
|
1485
|
+
const opMap = {
|
|
1486
|
+
DATE_EQ: "=",
|
|
1487
|
+
DATE_NOT_EQ: "<>",
|
|
1488
|
+
DATE_BEFORE: "<",
|
|
1489
|
+
DATE_AFTER: ">",
|
|
1490
|
+
DATE_ON_OR_BEFORE: "<=",
|
|
1491
|
+
DATE_ON_OR_AFTER: ">=",
|
|
1492
|
+
};
|
|
1493
|
+
if (condition.type === "DATE_BETWEEN") {
|
|
1494
|
+
return buildFormulaConditionValue(`AND(${baseCellRef}>=${firstExpr},${baseCellRef}<=${secondExpr})`);
|
|
1495
|
+
}
|
|
1496
|
+
if (condition.type === "DATE_NOT_BETWEEN") {
|
|
1497
|
+
return buildFormulaConditionValue(`OR(${baseCellRef}<${firstExpr},${baseCellRef}>${secondExpr})`);
|
|
1498
|
+
}
|
|
1499
|
+
const op = opMap[condition.type];
|
|
1500
|
+
if (!op || !firstExpr)
|
|
1501
|
+
return undefined;
|
|
1502
|
+
return buildFormulaConditionValue(`${baseCellRef}${op}${firstExpr}`);
|
|
1503
|
+
}
|
|
1504
|
+
switch (condition.type) {
|
|
1505
|
+
case "BLANK":
|
|
1506
|
+
return buildFormulaConditionValue(`${baseCellRef}=""`);
|
|
1507
|
+
case "NOT_BLANK":
|
|
1508
|
+
return buildFormulaConditionValue(`${baseCellRef}<>""`);
|
|
1509
|
+
case "ERROR":
|
|
1510
|
+
return buildFormulaConditionValue(`ISERROR(${baseCellRef})`);
|
|
1511
|
+
case "NOT_ERROR":
|
|
1512
|
+
return buildFormulaConditionValue(`NOT(ISERROR(${baseCellRef}))`);
|
|
1513
|
+
case "BOOLEAN":
|
|
1514
|
+
return buildFormulaConditionValue(`OR(${baseCellRef}=TRUE,${baseCellRef}=FALSE)`);
|
|
1515
|
+
case "TEXT_IS_EMAIL":
|
|
1516
|
+
return buildFormulaConditionValue(`AND(ISNUMBER(SEARCH("@",${baseCellRef})),ISNUMBER(SEARCH(".",${baseCellRef})))`);
|
|
1517
|
+
case "TEXT_IS_URL":
|
|
1518
|
+
return buildFormulaConditionValue(`OR(LEFT(${baseCellRef},7)="http://",LEFT(${baseCellRef},8)="https://")`);
|
|
1519
|
+
case "DATE_IS_VALID":
|
|
1520
|
+
return buildFormulaConditionValue(`ISNUMBER(${baseCellRef})`);
|
|
1521
|
+
case "NUMBER_ABOVE_AVERAGE":
|
|
1522
|
+
return buildFormulaConditionValue(`${baseCellRef}>AVERAGE(${absoluteRangeRef})`);
|
|
1523
|
+
case "NUMBER_BELOW_AVERAGE":
|
|
1524
|
+
return buildFormulaConditionValue(`${baseCellRef}<AVERAGE(${absoluteRangeRef})`);
|
|
1525
|
+
case "DATE_TODAY": {
|
|
1526
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1527
|
+
return buildFormulaConditionValue(`${normalized}=TODAY()`);
|
|
1528
|
+
}
|
|
1529
|
+
case "DATE_YESTERDAY": {
|
|
1530
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1531
|
+
return buildFormulaConditionValue(`${normalized}=TODAY()-1`);
|
|
1532
|
+
}
|
|
1533
|
+
case "DATE_TOMORROW": {
|
|
1534
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1535
|
+
return buildFormulaConditionValue(`${normalized}=TODAY()+1`);
|
|
1536
|
+
}
|
|
1537
|
+
case "DATE_LAST_7_DAYS": {
|
|
1538
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1539
|
+
return buildFormulaConditionValue(`AND(${normalized}>=TODAY()-6,${normalized}<=TODAY())`);
|
|
1540
|
+
}
|
|
1541
|
+
case "DATE_THIS_WEEK": {
|
|
1542
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1543
|
+
const weekStart = "TODAY()-WEEKDAY(TODAY(),2)+1";
|
|
1544
|
+
return buildFormulaConditionValue(`AND(${normalized}>=${weekStart},${normalized}<=${weekStart}+6)`);
|
|
1545
|
+
}
|
|
1546
|
+
case "DATE_LAST_WEEK": {
|
|
1547
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1548
|
+
const weekStart = "TODAY()-WEEKDAY(TODAY(),2)+1";
|
|
1549
|
+
return buildFormulaConditionValue(`AND(${normalized}>=${weekStart}-7,${normalized}<=${weekStart}-1)`);
|
|
1550
|
+
}
|
|
1551
|
+
case "DATE_NEXT_WEEK": {
|
|
1552
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1553
|
+
const weekStart = "TODAY()-WEEKDAY(TODAY(),2)+1";
|
|
1554
|
+
return buildFormulaConditionValue(`AND(${normalized}>=${weekStart}+7,${normalized}<=${weekStart}+13)`);
|
|
1555
|
+
}
|
|
1556
|
+
case "DATE_THIS_MONTH": {
|
|
1557
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1558
|
+
return buildFormulaConditionValue(`AND(${normalized}>=EOMONTH(TODAY(),-1)+1,${normalized}<=EOMONTH(TODAY(),0))`);
|
|
1559
|
+
}
|
|
1560
|
+
case "DATE_LAST_MONTH": {
|
|
1561
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1562
|
+
return buildFormulaConditionValue(`AND(${normalized}>=EOMONTH(TODAY(),-2)+1,${normalized}<=EOMONTH(TODAY(),-1))`);
|
|
1563
|
+
}
|
|
1564
|
+
case "DATE_NEXT_MONTH": {
|
|
1565
|
+
const normalized = `INT(${baseCellRef})`;
|
|
1566
|
+
return buildFormulaConditionValue(`AND(${normalized}>=EOMONTH(TODAY(),0)+1,${normalized}<=EOMONTH(TODAY(),1))`);
|
|
1567
|
+
}
|
|
1568
|
+
case "NUMBER_GREATER_THAN_PREVIOUS":
|
|
1569
|
+
return buildFormulaConditionValue(`IFERROR(${baseCellRef}>INDIRECT("R[-1]C",FALSE),TRUE)`);
|
|
1570
|
+
case "NUMBER_GREATER_THAN_EQ_PREVIOUS":
|
|
1571
|
+
return buildFormulaConditionValue(`IFERROR(${baseCellRef}>=INDIRECT("R[-1]C",FALSE),TRUE)`);
|
|
1572
|
+
case "NUMBER_LESS_THAN_PREVIOUS":
|
|
1573
|
+
return buildFormulaConditionValue(`IFERROR(${baseCellRef}<INDIRECT("R[-1]C",FALSE),TRUE)`);
|
|
1574
|
+
case "NUMBER_LESS_THAN_EQ_PREVIOUS":
|
|
1575
|
+
return buildFormulaConditionValue(`IFERROR(${baseCellRef}<=INDIRECT("R[-1]C",FALSE),TRUE)`);
|
|
1576
|
+
case "ONE_OF_LIST": {
|
|
1577
|
+
const entries = values
|
|
1578
|
+
.map((value) => value.userEnteredValue)
|
|
1579
|
+
.filter((value) => typeof value === "string");
|
|
1580
|
+
if (!entries.length)
|
|
1581
|
+
return undefined;
|
|
1582
|
+
const conditions = entries.map((entry) => {
|
|
1583
|
+
if (entry.startsWith("=")) {
|
|
1584
|
+
return `${baseCellRef}=${stripFormula(entry)}`;
|
|
1585
|
+
}
|
|
1586
|
+
if (isNumericValue(entry)) {
|
|
1587
|
+
return `${baseCellRef}=${entry}`;
|
|
1588
|
+
}
|
|
1589
|
+
return `${baseCellRef}="${escapeConditionText(entry)}"`;
|
|
1590
|
+
});
|
|
1591
|
+
return buildFormulaConditionValue(`OR(${conditions.join(",")})`);
|
|
1592
|
+
}
|
|
1593
|
+
case "ONE_OF_RANGE": {
|
|
1594
|
+
const rangeRef = values[0]?.userEnteredValue ?? "";
|
|
1595
|
+
if (!rangeRef)
|
|
1596
|
+
return undefined;
|
|
1597
|
+
return buildFormulaConditionValue(`COUNTIF(${stripFormula(rangeRef)},${baseCellRef})>0`);
|
|
1598
|
+
}
|
|
1599
|
+
default:
|
|
1600
|
+
return undefined;
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
const booleanConditionValue = buildBooleanConditionValue();
|
|
1604
|
+
if (booleanConditionValue) {
|
|
1605
|
+
addConditionEntry(rule.booleanRule?.format, booleanConditionValue);
|
|
1606
|
+
}
|
|
1607
|
+
if (rule.distinctRule && baseCellRef) {
|
|
1608
|
+
const isUnique = rule.distinctRule.type === "UNIQUE";
|
|
1609
|
+
const op = isUnique ? "=" : ">";
|
|
1610
|
+
addConditionEntry(rule.distinctRule.format, buildFormulaConditionValue(`COUNTIF(${absoluteRangeRef},${baseCellRef})${op}1`));
|
|
1611
|
+
}
|
|
1612
|
+
if (rule.topBottomRule && baseCellRef) {
|
|
1613
|
+
const rank = Number.isFinite(rule.topBottomRule.rank)
|
|
1614
|
+
? Math.max(1, rule.topBottomRule.rank)
|
|
1615
|
+
: 10;
|
|
1616
|
+
const isBottom = rule.topBottomRule.type === "BOTTOM";
|
|
1617
|
+
const rankExpr = rule.topBottomRule.isPercent
|
|
1618
|
+
? `ROUNDUP(COUNT(${absoluteRangeRef})*${rank}/100,0)`
|
|
1619
|
+
: String(rank);
|
|
1620
|
+
const order = isBottom ? ",1" : "";
|
|
1621
|
+
addConditionEntry(rule.topBottomRule.format, buildFormulaConditionValue(`RANK(${baseCellRef},${absoluteRangeRef}${order})<=${rankExpr}`));
|
|
1622
|
+
}
|
|
1623
|
+
if (rule.gradientRule && baseCellRef) {
|
|
1624
|
+
const gradient = rule.gradientRule;
|
|
1625
|
+
const thresholdForPoint = (point) => {
|
|
1626
|
+
if (!point?.type)
|
|
1627
|
+
return undefined;
|
|
1628
|
+
const type = String(point.type).toUpperCase();
|
|
1629
|
+
const value = point.value ?? "";
|
|
1630
|
+
switch (type) {
|
|
1631
|
+
case "MIN":
|
|
1632
|
+
return `MIN(${absoluteRangeRef})`;
|
|
1633
|
+
case "MAX":
|
|
1634
|
+
return `MAX(${absoluteRangeRef})`;
|
|
1635
|
+
case "NUMBER":
|
|
1636
|
+
return String(value);
|
|
1637
|
+
case "PERCENT":
|
|
1638
|
+
case "PERCENTILE":
|
|
1639
|
+
return `PERCENTILE.INC(${absoluteRangeRef},${Number(value) / 100})`;
|
|
1640
|
+
default:
|
|
1641
|
+
return undefined;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
const minExpr = thresholdForPoint(gradient.minpoint);
|
|
1645
|
+
const midExpr = thresholdForPoint(gradient.midpoint);
|
|
1646
|
+
const maxExpr = thresholdForPoint(gradient.maxpoint);
|
|
1647
|
+
if (gradient.minpoint?.color && (midExpr || maxExpr)) {
|
|
1648
|
+
const upper = midExpr ?? maxExpr ?? minExpr;
|
|
1649
|
+
if (upper) {
|
|
1650
|
+
addConditionEntry({ backgroundColor: gradient.minpoint.color }, buildFormulaConditionValue(`${baseCellRef}<=${upper}`));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (gradient.midpoint?.color && midExpr && maxExpr) {
|
|
1654
|
+
addConditionEntry({ backgroundColor: gradient.midpoint.color }, buildFormulaConditionValue(`AND(${baseCellRef}>${midExpr},${baseCellRef}<${maxExpr})`));
|
|
1655
|
+
}
|
|
1656
|
+
if (gradient.maxpoint?.color && (maxExpr || midExpr)) {
|
|
1657
|
+
const lower = maxExpr ?? midExpr ?? minExpr;
|
|
1658
|
+
if (lower) {
|
|
1659
|
+
addConditionEntry({ backgroundColor: gradient.maxpoint.color }, buildFormulaConditionValue(`${baseCellRef}>=${lower}`));
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// ODS `calcext:` extension for icon sets and data bars. Same threshold
|
|
1664
|
+
// type vocabulary as ODS gradient, plus a leading "auto-minimum" stop
|
|
1665
|
+
// that LibreOffice expects to match OOXML's leading-cfvo convention.
|
|
1666
|
+
const cfThresholdTypeToOds = (t) => {
|
|
1667
|
+
if (!t?.type)
|
|
1668
|
+
return "";
|
|
1669
|
+
const map = {
|
|
1670
|
+
MIN: "auto-minimum",
|
|
1671
|
+
MAX: "auto-maximum",
|
|
1672
|
+
NUMBER: "value",
|
|
1673
|
+
PERCENT: "percent",
|
|
1674
|
+
PERCENTILE: "percentile",
|
|
1675
|
+
FORMULA: "formula",
|
|
1676
|
+
};
|
|
1677
|
+
const odsType = map[String(t.type).toUpperCase()] ?? "value";
|
|
1678
|
+
const valueAttr = t.value !== undefined && t.value !== null
|
|
1679
|
+
? ` calcext:value="${escapeXml(String(t.value))}"`
|
|
1680
|
+
: ` calcext:value="0"`;
|
|
1681
|
+
return `<calcext:formatting-entry calcext:type="${odsType}"${valueAttr}/>`;
|
|
1682
|
+
};
|
|
1683
|
+
if (rule.iconSetRule) {
|
|
1684
|
+
const { iconSet, thresholds } = rule.iconSetRule;
|
|
1685
|
+
// Prepend the auto-minimum entry that ODS / LibreOffice expects.
|
|
1686
|
+
const entriesXml = [
|
|
1687
|
+
`<calcext:formatting-entry calcext:type="auto-minimum" calcext:value="0"/>`,
|
|
1688
|
+
...thresholds.map((t) => cfThresholdTypeToOds(t)),
|
|
1689
|
+
].join("");
|
|
1690
|
+
entries.push(`<calcext:conditional-format calcext:target-range-address="${escapeXml(rangeAttr)}"><calcext:icon-set calcext:icon-set-type="${iconSet}">${entriesXml}</calcext:icon-set></calcext:conditional-format>`);
|
|
1691
|
+
}
|
|
1692
|
+
if (rule.dataBarRule) {
|
|
1693
|
+
// Data-bar colors are typically authored as hex strings; theme-colored
|
|
1694
|
+
// bars fall back to the LibreOffice default blue rather than resolving
|
|
1695
|
+
// theme indices here (which would require plumbing theme through the
|
|
1696
|
+
// CF builder for one edge case).
|
|
1697
|
+
const colorToHex = (c) => {
|
|
1698
|
+
if (typeof c === "string")
|
|
1699
|
+
return c;
|
|
1700
|
+
return undefined;
|
|
1701
|
+
};
|
|
1702
|
+
const positiveColor = colorToHex(rule.dataBarRule.color) ?? "#638ec6";
|
|
1703
|
+
const negativeColor = colorToHex(rule.dataBarRule.negativeColor);
|
|
1704
|
+
const entriesXml = [
|
|
1705
|
+
cfThresholdTypeToOds(rule.dataBarRule.minpoint),
|
|
1706
|
+
cfThresholdTypeToOds(rule.dataBarRule.maxpoint),
|
|
1707
|
+
].join("");
|
|
1708
|
+
const negAttr = negativeColor
|
|
1709
|
+
? ` calcext:negative-color="${negativeColor}"`
|
|
1710
|
+
: "";
|
|
1711
|
+
entries.push(`<calcext:conditional-format calcext:target-range-address="${escapeXml(rangeAttr)}"><calcext:data-bar calcext:positive-color="${positiveColor}"${negAttr}>${entriesXml}</calcext:data-bar></calcext:conditional-format>`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (!entries.length)
|
|
1715
|
+
return "";
|
|
1716
|
+
return `<calcext:conditional-formats>${entries.join("")}</calcext:conditional-formats>`;
|
|
1717
|
+
};
|
|
1718
|
+
const getFilterVisibleValues = (spec, range, rows, headerRow = true, totalRow = false) => {
|
|
1719
|
+
const values = new Set();
|
|
1720
|
+
const startRowIndex = range.startRowIndex + (headerRow ? 1 : 0);
|
|
1721
|
+
const endRowIndex = range.endRowIndex - (totalRow ? 1 : 0);
|
|
1722
|
+
const columnIndex = range.startColumnIndex + spec.columnIndex;
|
|
1723
|
+
for (let rowIndex = startRowIndex; rowIndex <= endRowIndex; rowIndex++) {
|
|
1724
|
+
const cell = rows[rowIndex]?.values?.[columnIndex];
|
|
1725
|
+
const formattedValue = getCellFormattedValue(cell);
|
|
1726
|
+
const fallbackValue = formattedValue ??
|
|
1727
|
+
getCellEffectiveValue(cell) ??
|
|
1728
|
+
getCellUserEnteredValue(cell);
|
|
1729
|
+
if (isNil(fallbackValue) || fallbackValue === "") {
|
|
1730
|
+
values.add(BLANK_LABEL);
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
values.add(String(fallbackValue));
|
|
1734
|
+
}
|
|
1735
|
+
return [...values];
|
|
1736
|
+
};
|
|
1737
|
+
const buildDatabaseRangeEntries = (tables, sheetName, sheet, rows) => {
|
|
1738
|
+
const entries = [];
|
|
1739
|
+
const basicFilter = sheet.basicFilter;
|
|
1740
|
+
for (const table of tables) {
|
|
1741
|
+
const ref = table.ref;
|
|
1742
|
+
if (!ref)
|
|
1743
|
+
continue;
|
|
1744
|
+
const range = `${escapeSheetName(sheetName)}.${ref}`;
|
|
1745
|
+
const filterButton = table.filterButton !== false;
|
|
1746
|
+
const filterSpecs = table.filterSpecs ??
|
|
1747
|
+
(basicFilter && rangesEqual(table.range, basicFilter.range)
|
|
1748
|
+
? basicFilter.filterSpecs ?? []
|
|
1749
|
+
: []);
|
|
1750
|
+
const sortSpecs = table.sortSpecs ??
|
|
1751
|
+
(basicFilter && rangesEqual(table.range, basicFilter.range)
|
|
1752
|
+
? basicFilter.sortSpecs ?? []
|
|
1753
|
+
: []);
|
|
1754
|
+
const filterXml = buildFilterXml(filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false);
|
|
1755
|
+
const sortXml = buildSortXml(sortSpecs ?? []);
|
|
1756
|
+
const attrs = [
|
|
1757
|
+
`table:name="${escapeXml(table.name)}"`,
|
|
1758
|
+
`table:target-range-address="${escapeXml(range)}"`,
|
|
1759
|
+
`table:orientation="row"`,
|
|
1760
|
+
];
|
|
1761
|
+
if (filterButton) {
|
|
1762
|
+
attrs.push(`table:display-filter-buttons="true"`);
|
|
1763
|
+
}
|
|
1764
|
+
entries.push(`<table:database-range ${attrs.join(" ")}>${filterXml}${sortXml}</table:database-range>`);
|
|
1765
|
+
}
|
|
1766
|
+
if (basicFilter &&
|
|
1767
|
+
!tables.some((table) => rangesEqual(table.range, basicFilter.range))) {
|
|
1768
|
+
const rangeAddress = buildOdsRangeAddress(basicFilter.range, sheetName);
|
|
1769
|
+
if (rangeAddress) {
|
|
1770
|
+
const filterXml = buildFilterXml(basicFilter.filterSpecs ?? [], basicFilter.range, rows, true, false);
|
|
1771
|
+
const sortXml = buildSortXml(basicFilter.sortSpecs ?? []);
|
|
1772
|
+
const name = String(basicFilter.id) ?? `Filter_${sheetName}`;
|
|
1773
|
+
entries.push(`<table:database-range table:name="${escapeXml(name)}" table:target-range-address="${escapeXml(rangeAddress)}" table:orientation="row" table:display-filter-buttons="true">${filterXml}${sortXml}</table:database-range>`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return entries;
|
|
1777
|
+
};
|
|
1778
|
+
const buildDatabaseRangesXml = (entries) => {
|
|
1779
|
+
if (!entries.length)
|
|
1780
|
+
return "";
|
|
1781
|
+
return `<table:database-ranges>${entries.join("")}</table:database-ranges>`;
|
|
1782
|
+
};
|
|
1783
|
+
const buildNamedRangesXml = (namedRanges, sheetNameById, namedRangeNames) => {
|
|
1784
|
+
if (!namedRanges?.length)
|
|
1785
|
+
return "";
|
|
1786
|
+
const ranges = [];
|
|
1787
|
+
const expressions = [];
|
|
1788
|
+
for (const namedRange of namedRanges) {
|
|
1789
|
+
if (!namedRange?.name)
|
|
1790
|
+
continue;
|
|
1791
|
+
if (!isValidNamedRangeName(namedRange.name)) {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
if (namedRange.range?.sheetId) {
|
|
1795
|
+
const sheetName = sheetNameById.get(namedRange.range.sheetId);
|
|
1796
|
+
if (!sheetName)
|
|
1797
|
+
continue;
|
|
1798
|
+
const address = buildOdsRangeAddressAbsolute(namedRange.range, sheetName);
|
|
1799
|
+
if (!address)
|
|
1800
|
+
continue;
|
|
1801
|
+
const baseAddress = buildOdsBaseAddressAbsolute(namedRange.range, sheetName);
|
|
1802
|
+
if (!baseAddress)
|
|
1803
|
+
continue;
|
|
1804
|
+
ranges.push(`<table:named-range table:name="${escapeXml(namedRange.name)}" table:cell-range-address="${escapeXml(address)}" table:base-cell-address="${escapeXml(baseAddress)}"/>`);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
const rawValue = namedRange.value;
|
|
1808
|
+
if (typeof rawValue === "string" && rawValue.trim().length) {
|
|
1809
|
+
const trimmed = rawValue.trim();
|
|
1810
|
+
const expression = trimmed.startsWith("=")
|
|
1811
|
+
? `of:=${sanitizeXmlText(convertExcelFormulaToOds(trimmed.slice(1), namedRangeNames))}`
|
|
1812
|
+
: sanitizeXmlText(trimmed);
|
|
1813
|
+
expressions.push(`<table:named-expression table:name="${escapeXml(namedRange.name)}" table:expression="${escapeXml(expression)}"/>`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (!ranges.length && !expressions.length)
|
|
1817
|
+
return "";
|
|
1818
|
+
return `<table:named-expressions>${ranges.join("")}${expressions.join("")}</table:named-expressions>`;
|
|
1819
|
+
};
|
|
1820
|
+
const buildFilterXml = (filterSpecs, range, rows, headerRow = true, totalRow = false) => {
|
|
1821
|
+
if (!filterSpecs?.length)
|
|
1822
|
+
return "";
|
|
1823
|
+
const conditions = [];
|
|
1824
|
+
for (const spec of filterSpecs) {
|
|
1825
|
+
const criteria = spec.filterCriteria;
|
|
1826
|
+
// A condition filter (greater-than / contains / between / blank / …) takes
|
|
1827
|
+
// precedence over a value list, and is emitted as a single operator-based
|
|
1828
|
+
// <table:filter-condition>. Otherwise fall back to value-list entries.
|
|
1829
|
+
const condition = criteria?.condition;
|
|
1830
|
+
const mapped = condition?.type
|
|
1831
|
+
? mapConditionToFilterCondition(condition.type, condition.values ?? [])
|
|
1832
|
+
: undefined;
|
|
1833
|
+
if (mapped) {
|
|
1834
|
+
const attrs = [
|
|
1835
|
+
`table:field-number="${spec.columnIndex}"`,
|
|
1836
|
+
`table:operator="${mapped.operator}"`,
|
|
1837
|
+
];
|
|
1838
|
+
if (mapped.value !== undefined) {
|
|
1839
|
+
attrs.push(`table:value="${escapeXml(mapped.value)}"`);
|
|
1840
|
+
}
|
|
1841
|
+
if (mapped.value2 !== undefined) {
|
|
1842
|
+
attrs.push(`table:value2="${escapeXml(mapped.value2)}"`);
|
|
1843
|
+
}
|
|
1844
|
+
conditions.push(`<table:filter-condition ${attrs.join(" ")}/>`);
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const visibleValues = criteria?.visibleValues ??
|
|
1848
|
+
getFilterVisibleValues(spec, range, rows, headerRow, totalRow);
|
|
1849
|
+
const hiddenValues = criteria?.hiddenValues ?? [];
|
|
1850
|
+
for (const value of visibleValues) {
|
|
1851
|
+
conditions.push(`<table:filter-condition table:field-number="${spec.columnIndex}" table:operator="=" table:value="${escapeXml(value)}"/>`);
|
|
1852
|
+
}
|
|
1853
|
+
for (const value of hiddenValues) {
|
|
1854
|
+
conditions.push(`<table:filter-condition table:field-number="${spec.columnIndex}" table:operator="!=" table:value="${escapeXml(value)}"/>`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (!conditions.length)
|
|
1858
|
+
return "";
|
|
1859
|
+
return `<table:filter><table:filter-and>${conditions.join("")}</table:filter-and></table:filter>`;
|
|
1860
|
+
};
|
|
1861
|
+
const buildSortXml = (sortSpecs) => {
|
|
1862
|
+
if (!sortSpecs?.length)
|
|
1863
|
+
return "";
|
|
1864
|
+
const entries = sortSpecs.map((spec) => {
|
|
1865
|
+
const order = spec.sortOrder === "DESCENDING" ? "descending" : "ascending";
|
|
1866
|
+
return `<table:sort-by table:field-number="${spec.dimensionIndex}" table:order="${order}"/>`;
|
|
1867
|
+
});
|
|
1868
|
+
return `<table:sort>${entries.join("")}</table:sort>`;
|
|
1869
|
+
};
|
|
1870
|
+
const mapConditionToFilterCondition = (type, values) => {
|
|
1871
|
+
const first = values?.[0]?.userEnteredValue;
|
|
1872
|
+
const second = values?.[1]?.userEnteredValue;
|
|
1873
|
+
switch (type) {
|
|
1874
|
+
case "NUMBER_EQ":
|
|
1875
|
+
case "TEXT_EQ":
|
|
1876
|
+
case "DATE_EQ":
|
|
1877
|
+
return { operator: "=", value: first ?? "" };
|
|
1878
|
+
case "NUMBER_NOT_EQ":
|
|
1879
|
+
case "TEXT_NOT_EQ":
|
|
1880
|
+
case "DATE_NOT_EQ":
|
|
1881
|
+
return { operator: "!=", value: first ?? "" };
|
|
1882
|
+
case "NUMBER_GREATER":
|
|
1883
|
+
case "DATE_AFTER":
|
|
1884
|
+
return { operator: ">", value: first ?? "" };
|
|
1885
|
+
case "NUMBER_GREATER_THAN_EQ":
|
|
1886
|
+
case "DATE_ON_OR_AFTER":
|
|
1887
|
+
return { operator: ">=", value: first ?? "" };
|
|
1888
|
+
case "NUMBER_LESS":
|
|
1889
|
+
case "DATE_BEFORE":
|
|
1890
|
+
return { operator: "<", value: first ?? "" };
|
|
1891
|
+
case "NUMBER_LESS_THAN_EQ":
|
|
1892
|
+
case "DATE_ON_OR_BEFORE":
|
|
1893
|
+
return { operator: "<=", value: first ?? "" };
|
|
1894
|
+
case "NUMBER_BETWEEN":
|
|
1895
|
+
case "DATE_BETWEEN":
|
|
1896
|
+
return { operator: "between", value: first ?? "", value2: second ?? "" };
|
|
1897
|
+
case "NUMBER_NOT_BETWEEN":
|
|
1898
|
+
case "DATE_NOT_BETWEEN":
|
|
1899
|
+
return {
|
|
1900
|
+
operator: "not-between",
|
|
1901
|
+
value: first ?? "",
|
|
1902
|
+
value2: second ?? "",
|
|
1903
|
+
};
|
|
1904
|
+
case "TEXT_CONTAINS":
|
|
1905
|
+
return { operator: "contains", value: first ?? "" };
|
|
1906
|
+
case "TEXT_NOT_CONTAINS":
|
|
1907
|
+
return { operator: "does-not-contain", value: first ?? "" };
|
|
1908
|
+
case "TEXT_STARTS_WITH":
|
|
1909
|
+
return { operator: "begins-with", value: first ?? "" };
|
|
1910
|
+
case "TEXT_ENDS_WITH":
|
|
1911
|
+
return { operator: "ends-with", value: first ?? "" };
|
|
1912
|
+
case "BLANK":
|
|
1913
|
+
return { operator: "empty" };
|
|
1914
|
+
case "NOT_BLANK":
|
|
1915
|
+
return { operator: "non-empty" };
|
|
1916
|
+
case "BOOLEAN":
|
|
1917
|
+
return { operator: "=", value: "TRUE" };
|
|
1918
|
+
default:
|
|
1919
|
+
return undefined;
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
const resolveCellDataValidation = (cell, map) => {
|
|
1923
|
+
if (!cell)
|
|
1924
|
+
return undefined;
|
|
1925
|
+
const raw = cell?.dataValidation;
|
|
1926
|
+
if (!raw)
|
|
1927
|
+
return undefined;
|
|
1928
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
1929
|
+
return map?.[String(raw)];
|
|
1930
|
+
}
|
|
1931
|
+
return raw;
|
|
1932
|
+
};
|
|
1933
|
+
const buildMergeState = (ranges) => {
|
|
1934
|
+
const normalized = normalizeMergeRanges(ranges);
|
|
1935
|
+
const anchorSpans = new Map();
|
|
1936
|
+
const coveredCells = new Set();
|
|
1937
|
+
const maxColumnByRow = new Map();
|
|
1938
|
+
for (const range of normalized) {
|
|
1939
|
+
const rows = range.endRowIndex - range.startRowIndex + 1;
|
|
1940
|
+
const columns = range.endColumnIndex - range.startColumnIndex + 1;
|
|
1941
|
+
anchorSpans.set(`${range.startRowIndex}:${range.startColumnIndex}`, {
|
|
1942
|
+
rows,
|
|
1943
|
+
columns,
|
|
1944
|
+
});
|
|
1945
|
+
for (let r = range.startRowIndex; r <= range.endRowIndex; r++) {
|
|
1946
|
+
for (let c = range.startColumnIndex; c <= range.endColumnIndex; c++) {
|
|
1947
|
+
if (r === range.startRowIndex && c === range.startColumnIndex) {
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
coveredCells.add(`${r}:${c}`);
|
|
1951
|
+
}
|
|
1952
|
+
const current = maxColumnByRow.get(r) ?? 0;
|
|
1953
|
+
maxColumnByRow.set(r, Math.max(current, range.endColumnIndex));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return { anchorSpans, coveredCells, maxColumnByRow, ranges: normalized };
|
|
1957
|
+
};
|
|
1958
|
+
const updateBoundsForMerges = (bounds, state) => {
|
|
1959
|
+
for (const range of state.ranges) {
|
|
1960
|
+
bounds.minRow = Math.min(bounds.minRow, range.startRowIndex);
|
|
1961
|
+
bounds.maxRow = Math.max(bounds.maxRow, range.endRowIndex);
|
|
1962
|
+
bounds.minColumn = Math.min(bounds.minColumn, range.startColumnIndex);
|
|
1963
|
+
bounds.maxColumn = Math.max(bounds.maxColumn, range.endColumnIndex);
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
const sanitizeMergeRange = (range) => {
|
|
1967
|
+
const startRow = sanitizeIndex(range?.startRowIndex);
|
|
1968
|
+
const endRow = sanitizeIndex(range?.endRowIndex);
|
|
1969
|
+
const startColumn = sanitizeIndex(range?.startColumnIndex);
|
|
1970
|
+
const endColumn = sanitizeIndex(range?.endColumnIndex);
|
|
1971
|
+
if (startRow == null ||
|
|
1972
|
+
endRow == null ||
|
|
1973
|
+
startColumn == null ||
|
|
1974
|
+
endColumn == null) {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
if (startRow > endRow || startColumn > endColumn) {
|
|
1978
|
+
return null;
|
|
1979
|
+
}
|
|
1980
|
+
if (startRow === endRow && startColumn === endColumn) {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
return {
|
|
1984
|
+
startRowIndex: startRow,
|
|
1985
|
+
endRowIndex: endRow,
|
|
1986
|
+
startColumnIndex: startColumn,
|
|
1987
|
+
endColumnIndex: endColumn,
|
|
1988
|
+
};
|
|
1989
|
+
};
|
|
1990
|
+
const sanitizeIndex = (value) => {
|
|
1991
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1992
|
+
return null;
|
|
1993
|
+
return Math.max(1, Math.floor(value));
|
|
1994
|
+
};
|
|
1995
|
+
const normalizeMergeRanges = (ranges) => {
|
|
1996
|
+
if (!ranges?.length)
|
|
1997
|
+
return [];
|
|
1998
|
+
const sanitized = ranges
|
|
1999
|
+
.map((range) => sanitizeMergeRange(range))
|
|
2000
|
+
.filter((range) => Boolean(range));
|
|
2001
|
+
if (!sanitized.length)
|
|
2002
|
+
return [];
|
|
2003
|
+
const sorted = sanitized.sort((a, b) => {
|
|
2004
|
+
if (a.startRowIndex !== b.startRowIndex) {
|
|
2005
|
+
return a.startRowIndex - b.startRowIndex;
|
|
2006
|
+
}
|
|
2007
|
+
if (a.startColumnIndex !== b.startColumnIndex) {
|
|
2008
|
+
return a.startColumnIndex - b.startColumnIndex;
|
|
2009
|
+
}
|
|
2010
|
+
if (a.endRowIndex !== b.endRowIndex) {
|
|
2011
|
+
return b.endRowIndex - a.endRowIndex;
|
|
2012
|
+
}
|
|
2013
|
+
return b.endColumnIndex - a.endColumnIndex;
|
|
2014
|
+
});
|
|
2015
|
+
const valid = [];
|
|
2016
|
+
let active = [];
|
|
2017
|
+
for (const merge of sorted) {
|
|
2018
|
+
if (active.length) {
|
|
2019
|
+
active = active.filter((entry) => entry.endRowIndex >= merge.startRowIndex);
|
|
2020
|
+
}
|
|
2021
|
+
const isContained = active.some((entry) => entry.startColumnIndex <= merge.startColumnIndex &&
|
|
2022
|
+
entry.endColumnIndex >= merge.endColumnIndex &&
|
|
2023
|
+
entry.endRowIndex >= merge.endRowIndex);
|
|
2024
|
+
if (!isContained) {
|
|
2025
|
+
valid.push(merge);
|
|
2026
|
+
active.push(merge);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return valid;
|
|
2030
|
+
};
|
|
2031
|
+
const buildRangeList = (ranges, sheetName) => {
|
|
2032
|
+
const refs = ranges
|
|
2033
|
+
.map((range) => buildOdsRangeAddress(range, sheetName))
|
|
2034
|
+
.filter((entry) => Boolean(entry));
|
|
2035
|
+
if (!refs.length)
|
|
2036
|
+
return "";
|
|
2037
|
+
return refs.join(" ");
|
|
2038
|
+
};
|
|
2039
|
+
const buildRangeListForValidations = (ranges, sheetNameById) => {
|
|
2040
|
+
const refs = ranges
|
|
2041
|
+
.map((range) => {
|
|
2042
|
+
const sheetName = sheetNameById.get(range.sheetId ?? 0);
|
|
2043
|
+
if (!sheetName)
|
|
2044
|
+
return undefined;
|
|
2045
|
+
return buildOdsRangeAddress(range, sheetName);
|
|
2046
|
+
})
|
|
2047
|
+
.filter((entry) => Boolean(entry));
|
|
2048
|
+
if (!refs.length)
|
|
2049
|
+
return "";
|
|
2050
|
+
return refs.join(" ");
|
|
2051
|
+
};
|
|
2052
|
+
const buildValidationBaseCellAddress = (ranges, sheetNameById) => {
|
|
2053
|
+
if (!ranges.length)
|
|
2054
|
+
return undefined;
|
|
2055
|
+
const range = ranges[0];
|
|
2056
|
+
const sheetName = sheetNameById.get(range.sheetId ?? 0);
|
|
2057
|
+
if (!sheetName)
|
|
2058
|
+
return undefined;
|
|
2059
|
+
return buildOdsBaseAddress(range, sheetName);
|
|
2060
|
+
};
|
|
2061
|
+
const buildCalcextRangeList = (ranges, sheetName) => {
|
|
2062
|
+
const refs = ranges
|
|
2063
|
+
.map((range) => buildCalcextRangeAddress(range, sheetName))
|
|
2064
|
+
.filter((entry) => Boolean(entry));
|
|
2065
|
+
if (!refs.length)
|
|
2066
|
+
return "";
|
|
2067
|
+
return refs.join(" ");
|
|
2068
|
+
};
|
|
2069
|
+
const buildOdsRangeAddress = (range, sheetName) => {
|
|
2070
|
+
try {
|
|
2071
|
+
const ref = cellRangeToAddress(undefined, range);
|
|
2072
|
+
if (!ref)
|
|
2073
|
+
return undefined;
|
|
2074
|
+
return `${escapeSheetName(sheetName)}.${ref}`;
|
|
2075
|
+
}
|
|
2076
|
+
catch {
|
|
2077
|
+
return undefined;
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
const buildOdsBaseAddress = (range, sheetName) => {
|
|
2081
|
+
try {
|
|
2082
|
+
const ref = cellToAddress({
|
|
2083
|
+
rowIndex: range.startRowIndex,
|
|
2084
|
+
columnIndex: range.startColumnIndex,
|
|
2085
|
+
});
|
|
2086
|
+
if (!ref)
|
|
2087
|
+
return undefined;
|
|
2088
|
+
return `${escapeSheetName(sheetName)}.${ref}`;
|
|
2089
|
+
}
|
|
2090
|
+
catch {
|
|
2091
|
+
return undefined;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
const buildCalcextRangeAddress = (range, sheetName) => {
|
|
2095
|
+
try {
|
|
2096
|
+
const startCol = columnIndexToLetter(range.startColumnIndex);
|
|
2097
|
+
const endCol = columnIndexToLetter(range.endColumnIndex);
|
|
2098
|
+
const start = `$${escapeSheetName(sheetName)}.$${startCol}$${range.startRowIndex}`;
|
|
2099
|
+
const end = `$${escapeSheetName(sheetName)}.$${endCol}$${range.endRowIndex}`;
|
|
2100
|
+
return `${start}:${end}`;
|
|
2101
|
+
}
|
|
2102
|
+
catch {
|
|
2103
|
+
return undefined;
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
const buildCalcextBaseAddress = (range, sheetName) => {
|
|
2107
|
+
try {
|
|
2108
|
+
const ref = cellToAddress({
|
|
2109
|
+
rowIndex: range.startRowIndex,
|
|
2110
|
+
columnIndex: range.startColumnIndex,
|
|
2111
|
+
});
|
|
2112
|
+
if (!ref)
|
|
2113
|
+
return undefined;
|
|
2114
|
+
return `${escapeSheetName(sheetName)}.${ref}`;
|
|
2115
|
+
}
|
|
2116
|
+
catch {
|
|
2117
|
+
return undefined;
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
const buildOdsRangeAddressAbsolute = (range, sheetName) => {
|
|
2121
|
+
try {
|
|
2122
|
+
const startCol = columnIndexToLetter(range.startColumnIndex);
|
|
2123
|
+
const endCol = columnIndexToLetter(range.endColumnIndex);
|
|
2124
|
+
const ref = `.$${startCol}$${range.startRowIndex}:.${`$${endCol}$${range.endRowIndex}`}`;
|
|
2125
|
+
return `$${escapeSheetName(sheetName)}${ref}`;
|
|
2126
|
+
}
|
|
2127
|
+
catch {
|
|
2128
|
+
return undefined;
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
const buildOdsBaseAddressAbsolute = (range, sheetName) => {
|
|
2132
|
+
try {
|
|
2133
|
+
const col = columnIndexToLetter(range.startColumnIndex);
|
|
2134
|
+
const ref = `.$${col}$${range.startRowIndex}`;
|
|
2135
|
+
return `$${escapeSheetName(sheetName)}${ref}`;
|
|
2136
|
+
}
|
|
2137
|
+
catch {
|
|
2138
|
+
return undefined;
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
function isValidNamedRangeName(name) {
|
|
2142
|
+
if (!name)
|
|
2143
|
+
return false;
|
|
2144
|
+
if (/^\d/.test(name))
|
|
2145
|
+
return false;
|
|
2146
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name))
|
|
2147
|
+
return false;
|
|
2148
|
+
if (/^[A-Za-z]{1,3}\d+$/.test(name))
|
|
2149
|
+
return false;
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
const rangesEqual = (left, right) => {
|
|
2153
|
+
if (!left || !right)
|
|
2154
|
+
return false;
|
|
2155
|
+
return (left.startRowIndex === right.startRowIndex &&
|
|
2156
|
+
left.endRowIndex === right.endRowIndex &&
|
|
2157
|
+
left.startColumnIndex === right.startColumnIndex &&
|
|
2158
|
+
left.endColumnIndex === right.endColumnIndex);
|
|
2159
|
+
};
|
|
2160
|
+
const escapeSheetName = (name) => {
|
|
2161
|
+
return `'${name.replace(/'/g, "''")}'`;
|
|
2162
|
+
};
|
|
2163
|
+
const columnIndexToLetter = (index) => {
|
|
2164
|
+
let dividend = Math.max(1, index);
|
|
2165
|
+
let columnName = "";
|
|
2166
|
+
while (dividend > 0) {
|
|
2167
|
+
const modulo = (dividend - 1) % 26;
|
|
2168
|
+
columnName = String.fromCharCode(65 + modulo) + columnName;
|
|
2169
|
+
dividend = Math.floor((dividend - modulo) / 26);
|
|
2170
|
+
}
|
|
2171
|
+
return columnName;
|
|
2172
|
+
};
|
|
2173
|
+
const stableStringify = (value) => {
|
|
2174
|
+
return JSON.stringify(value, (_key, val) => {
|
|
2175
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
2176
|
+
return Object.keys(val)
|
|
2177
|
+
.sort()
|
|
2178
|
+
.reduce((acc, key) => {
|
|
2179
|
+
acc[key] = val[key];
|
|
2180
|
+
return acc;
|
|
2181
|
+
}, {});
|
|
2182
|
+
}
|
|
2183
|
+
return val;
|
|
2184
|
+
});
|
|
2185
|
+
};
|
|
2186
|
+
const normalizeCellFormat = (format) => {
|
|
2187
|
+
const normalized = {};
|
|
2188
|
+
if (format.backgroundColor)
|
|
2189
|
+
normalized.backgroundColor = format.backgroundColor;
|
|
2190
|
+
if (format.borders)
|
|
2191
|
+
normalized.borders = format.borders;
|
|
2192
|
+
if (format.textFormat)
|
|
2193
|
+
normalized.textFormat = format.textFormat;
|
|
2194
|
+
if (format.numberFormat)
|
|
2195
|
+
normalized.numberFormat = format.numberFormat;
|
|
2196
|
+
if (format.horizontalAlignment)
|
|
2197
|
+
normalized.horizontalAlignment = format.horizontalAlignment;
|
|
2198
|
+
if (format.verticalAlignment)
|
|
2199
|
+
normalized.verticalAlignment = format.verticalAlignment;
|
|
2200
|
+
if (format.wrapStrategy)
|
|
2201
|
+
normalized.wrapStrategy = format.wrapStrategy;
|
|
2202
|
+
if (format.indent)
|
|
2203
|
+
normalized.indent = format.indent;
|
|
2204
|
+
if (format.textRotation !== undefined)
|
|
2205
|
+
normalized.textRotation = format.textRotation;
|
|
2206
|
+
return normalized;
|
|
2207
|
+
};
|
|
2208
|
+
const normalizeColor = (color, theme) => {
|
|
2209
|
+
if (!color)
|
|
2210
|
+
return undefined;
|
|
2211
|
+
if (typeof color === "string") {
|
|
2212
|
+
const normalized = color.startsWith("#") ? color : `#${color}`;
|
|
2213
|
+
return normalized.length === 7 ? normalized : undefined;
|
|
2214
|
+
}
|
|
2215
|
+
if (color.rgb) {
|
|
2216
|
+
const rgb = color.rgb;
|
|
2217
|
+
const normalized = rgb.startsWith("#") ? rgb : `#${rgb}`;
|
|
2218
|
+
return normalized.length === 7 ? normalized : undefined;
|
|
2219
|
+
}
|
|
2220
|
+
if (typeof color.theme === "number") {
|
|
2221
|
+
return resolveThemeColor(color, theme);
|
|
2222
|
+
}
|
|
2223
|
+
return undefined;
|
|
2224
|
+
};
|
|
2225
|
+
const THEME_COLOR_KEYS = {
|
|
2226
|
+
0: "background1",
|
|
2227
|
+
1: "text1",
|
|
2228
|
+
2: "background2",
|
|
2229
|
+
3: "text2",
|
|
2230
|
+
4: "accent1",
|
|
2231
|
+
5: "accent2",
|
|
2232
|
+
6: "accent3",
|
|
2233
|
+
7: "accent4",
|
|
2234
|
+
8: "accent5",
|
|
2235
|
+
9: "accent6",
|
|
2236
|
+
10: "hyperlink",
|
|
2237
|
+
};
|
|
2238
|
+
const resolveThemeColor = (color, theme) => {
|
|
2239
|
+
const themeKey = THEME_COLOR_KEYS[color.theme];
|
|
2240
|
+
if (!themeKey)
|
|
2241
|
+
return undefined;
|
|
2242
|
+
const themeColors = theme?.themeColors ?? defaultSpreadsheetTheme.themeColors;
|
|
2243
|
+
const base = themeColors[themeKey];
|
|
2244
|
+
if (!base)
|
|
2245
|
+
return undefined;
|
|
2246
|
+
return applyTint(base, color.tint ?? undefined);
|
|
2247
|
+
};
|
|
2248
|
+
const applyTint = (hex, tint) => {
|
|
2249
|
+
if (tint == null || tint === 0)
|
|
2250
|
+
return hex;
|
|
2251
|
+
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
2252
|
+
if (normalized.length !== 6)
|
|
2253
|
+
return `#${normalized}`;
|
|
2254
|
+
const r = parseInt(normalized.slice(0, 2), 16);
|
|
2255
|
+
const g = parseInt(normalized.slice(2, 4), 16);
|
|
2256
|
+
const b = parseInt(normalized.slice(4, 6), 16);
|
|
2257
|
+
const applyChannel = (channel) => {
|
|
2258
|
+
if (tint < 0) {
|
|
2259
|
+
return Math.round(channel * (1 + tint));
|
|
2260
|
+
}
|
|
2261
|
+
return Math.round(channel * (1 - tint) + 255 * tint);
|
|
2262
|
+
};
|
|
2263
|
+
const toHex = (value) => Math.max(0, Math.min(255, value)).toString(16).padStart(2, "0");
|
|
2264
|
+
return `#${toHex(applyChannel(r))}${toHex(applyChannel(g))}${toHex(applyChannel(b))}`;
|
|
2265
|
+
};
|
|
2266
|
+
const mapVerticalAlignment = (value) => {
|
|
2267
|
+
if (value === "middle")
|
|
2268
|
+
return "middle";
|
|
2269
|
+
return value ?? "bottom";
|
|
2270
|
+
};
|
|
2271
|
+
const buildZipStore = (entries) => {
|
|
2272
|
+
const normalized = entries.map((entry) => {
|
|
2273
|
+
const nameBytes = textEncoder.encode(entry.name);
|
|
2274
|
+
const data = entry.data;
|
|
2275
|
+
const crc = crc32(data);
|
|
2276
|
+
return {
|
|
2277
|
+
name: entry.name,
|
|
2278
|
+
nameBytes,
|
|
2279
|
+
data,
|
|
2280
|
+
crc,
|
|
2281
|
+
localSize: 30 + nameBytes.length + data.length,
|
|
2282
|
+
centralSize: 46 + nameBytes.length,
|
|
2283
|
+
localOffset: 0,
|
|
2284
|
+
};
|
|
2285
|
+
});
|
|
2286
|
+
const totalLocal = normalized.reduce((sum, entry) => sum + entry.localSize, 0);
|
|
2287
|
+
const totalCentral = normalized.reduce((sum, entry) => sum + entry.centralSize, 0);
|
|
2288
|
+
const endSize = 22;
|
|
2289
|
+
const totalSize = totalLocal + totalCentral + endSize;
|
|
2290
|
+
const buffer = new Uint8Array(totalSize);
|
|
2291
|
+
const view = new DataView(buffer.buffer);
|
|
2292
|
+
let cursor = 0;
|
|
2293
|
+
for (const entry of normalized) {
|
|
2294
|
+
entry.localOffset = cursor;
|
|
2295
|
+
cursor = writeUint32(view, cursor, 0x04034b50);
|
|
2296
|
+
cursor = writeUint16(view, cursor, 20);
|
|
2297
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2298
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2299
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2300
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2301
|
+
cursor = writeUint32(view, cursor, entry.crc);
|
|
2302
|
+
cursor = writeUint32(view, cursor, entry.data.length);
|
|
2303
|
+
cursor = writeUint32(view, cursor, entry.data.length);
|
|
2304
|
+
cursor = writeUint16(view, cursor, entry.nameBytes.length);
|
|
2305
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2306
|
+
buffer.set(entry.nameBytes, cursor);
|
|
2307
|
+
cursor += entry.nameBytes.length;
|
|
2308
|
+
buffer.set(entry.data, cursor);
|
|
2309
|
+
cursor += entry.data.length;
|
|
2310
|
+
}
|
|
2311
|
+
const centralOffset = cursor;
|
|
2312
|
+
for (const entry of normalized) {
|
|
2313
|
+
cursor = writeUint32(view, cursor, 0x02014b50);
|
|
2314
|
+
cursor = writeUint16(view, cursor, 20);
|
|
2315
|
+
cursor = writeUint16(view, cursor, 20);
|
|
2316
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2317
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2318
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2319
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2320
|
+
cursor = writeUint32(view, cursor, entry.crc);
|
|
2321
|
+
cursor = writeUint32(view, cursor, entry.data.length);
|
|
2322
|
+
cursor = writeUint32(view, cursor, entry.data.length);
|
|
2323
|
+
cursor = writeUint16(view, cursor, entry.nameBytes.length);
|
|
2324
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2325
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2326
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2327
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2328
|
+
cursor = writeUint32(view, cursor, 0);
|
|
2329
|
+
cursor = writeUint32(view, cursor, entry.localOffset);
|
|
2330
|
+
buffer.set(entry.nameBytes, cursor);
|
|
2331
|
+
cursor += entry.nameBytes.length;
|
|
2332
|
+
}
|
|
2333
|
+
const centralSize = cursor - centralOffset;
|
|
2334
|
+
cursor = writeUint32(view, cursor, 0x06054b50);
|
|
2335
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2336
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2337
|
+
cursor = writeUint16(view, cursor, normalized.length);
|
|
2338
|
+
cursor = writeUint16(view, cursor, normalized.length);
|
|
2339
|
+
cursor = writeUint32(view, cursor, centralSize);
|
|
2340
|
+
cursor = writeUint32(view, cursor, centralOffset);
|
|
2341
|
+
cursor = writeUint16(view, cursor, 0);
|
|
2342
|
+
return buffer.buffer;
|
|
2343
|
+
};
|
|
2344
|
+
const writeUint16 = (view, offset, value) => {
|
|
2345
|
+
view.setUint16(offset, value, true);
|
|
2346
|
+
return offset + 2;
|
|
2347
|
+
};
|
|
2348
|
+
const writeUint32 = (view, offset, value) => {
|
|
2349
|
+
view.setUint32(offset, value >>> 0, true);
|
|
2350
|
+
return offset + 4;
|
|
2351
|
+
};
|
|
2352
|
+
let crcTable = null;
|
|
2353
|
+
const getCrcTable = () => {
|
|
2354
|
+
if (crcTable)
|
|
2355
|
+
return crcTable;
|
|
2356
|
+
const table = new Uint32Array(256);
|
|
2357
|
+
for (let i = 0; i < 256; i++) {
|
|
2358
|
+
let c = i;
|
|
2359
|
+
for (let k = 0; k < 8; k++) {
|
|
2360
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
2361
|
+
}
|
|
2362
|
+
table[i] = c >>> 0;
|
|
2363
|
+
}
|
|
2364
|
+
crcTable = table;
|
|
2365
|
+
return table;
|
|
2366
|
+
};
|
|
2367
|
+
const crc32 = (data) => {
|
|
2368
|
+
const table = getCrcTable();
|
|
2369
|
+
let crc = 0xffffffff;
|
|
2370
|
+
for (let i = 0; i < data.length; i++) {
|
|
2371
|
+
crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
|
2372
|
+
}
|
|
2373
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
2374
|
+
};
|
|
2375
|
+
const borderToCss = (border, theme) => {
|
|
2376
|
+
if (!border?.style)
|
|
2377
|
+
return undefined;
|
|
2378
|
+
const width = border.width ? Math.max(0.5, border.width) : 1;
|
|
2379
|
+
const color = normalizeColor(border.color ?? undefined, theme) ?? "#000000";
|
|
2380
|
+
return `${width}px solid ${color}`;
|
|
2381
|
+
};
|
|
2382
|
+
const buildOdsConditionExpression = (formula, namedRangeNames) => {
|
|
2383
|
+
const trimmed = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
2384
|
+
return sanitizeXmlText(convertExcelFormulaToOds(trimmed, namedRangeNames));
|
|
2385
|
+
};
|
|
2386
|
+
const convertExcelFormulaToOds = (formula, namedRangeNames) => {
|
|
2387
|
+
if (!formula)
|
|
2388
|
+
return formula;
|
|
2389
|
+
const stringRegex = /"([^"]|"")*"/g;
|
|
2390
|
+
let result = "";
|
|
2391
|
+
let lastIndex = 0;
|
|
2392
|
+
let match;
|
|
2393
|
+
while ((match = stringRegex.exec(formula)) !== null) {
|
|
2394
|
+
const before = formula.slice(lastIndex, match.index);
|
|
2395
|
+
result += convertExcelSegmentToOds(convertArgumentSeparators(before), namedRangeNames);
|
|
2396
|
+
result += match[0];
|
|
2397
|
+
lastIndex = match.index + match[0].length;
|
|
2398
|
+
}
|
|
2399
|
+
result += convertExcelSegmentToOds(convertArgumentSeparators(formula.slice(lastIndex)), namedRangeNames);
|
|
2400
|
+
return result;
|
|
2401
|
+
};
|
|
2402
|
+
const convertExcelSegmentToOds = (segment, namedRangeNames) => {
|
|
2403
|
+
if (!segment)
|
|
2404
|
+
return segment;
|
|
2405
|
+
let converted = segment.replace(/('([^']|'')+'|[A-Za-z0-9_]+)!\$?[A-Za-z]{1,3}\$?\d+(?::\$?[A-Za-z]{1,3}\$?\d+)?/g, (match) => {
|
|
2406
|
+
const [sheetPart, refPart] = match.split("!");
|
|
2407
|
+
const sheetName = formatOdsSheetName(sheetPart);
|
|
2408
|
+
if (!sheetName)
|
|
2409
|
+
return match;
|
|
2410
|
+
if (refPart.includes(":")) {
|
|
2411
|
+
const [start, end] = refPart.split(":");
|
|
2412
|
+
return `[$${sheetName}.${start}:$${sheetName}.${end}]`;
|
|
2413
|
+
}
|
|
2414
|
+
return `[$${sheetName}.${refPart}]`;
|
|
2415
|
+
});
|
|
2416
|
+
const parts = splitByBracketedSegments(converted);
|
|
2417
|
+
converted = parts
|
|
2418
|
+
.map((part) => {
|
|
2419
|
+
if (part.isBracket)
|
|
2420
|
+
return part.value;
|
|
2421
|
+
return convertLocalRefsToOds(part.value, namedRangeNames);
|
|
2422
|
+
})
|
|
2423
|
+
.join("");
|
|
2424
|
+
return converted;
|
|
2425
|
+
};
|
|
2426
|
+
const convertArgumentSeparators = (formula) => {
|
|
2427
|
+
if (!formula)
|
|
2428
|
+
return formula;
|
|
2429
|
+
let result = "";
|
|
2430
|
+
let parenDepth = 0;
|
|
2431
|
+
for (let i = 0; i < formula.length; i++) {
|
|
2432
|
+
const char = formula[i];
|
|
2433
|
+
if (char === "(") {
|
|
2434
|
+
parenDepth += 1;
|
|
2435
|
+
result += char;
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
if (char === ")") {
|
|
2439
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
2440
|
+
result += char;
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
if (char === "," && parenDepth > 0) {
|
|
2444
|
+
while (formula[i + 1] === " ") {
|
|
2445
|
+
i += 1;
|
|
2446
|
+
}
|
|
2447
|
+
result += ";";
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
result += char;
|
|
2451
|
+
}
|
|
2452
|
+
return result;
|
|
2453
|
+
};
|
|
2454
|
+
const splitByBracketedSegments = (value) => {
|
|
2455
|
+
const regex = /\[[^\]]+\]/g;
|
|
2456
|
+
const segments = [];
|
|
2457
|
+
let lastIndex = 0;
|
|
2458
|
+
let match;
|
|
2459
|
+
while ((match = regex.exec(value)) !== null) {
|
|
2460
|
+
if (match.index > lastIndex) {
|
|
2461
|
+
segments.push({
|
|
2462
|
+
value: value.slice(lastIndex, match.index),
|
|
2463
|
+
isBracket: false,
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
segments.push({ value: match[0], isBracket: true });
|
|
2467
|
+
lastIndex = match.index + match[0].length;
|
|
2468
|
+
}
|
|
2469
|
+
if (lastIndex < value.length) {
|
|
2470
|
+
segments.push({ value: value.slice(lastIndex), isBracket: false });
|
|
2471
|
+
}
|
|
2472
|
+
return segments;
|
|
2473
|
+
};
|
|
2474
|
+
const convertLocalRefsToOds = (value, namedRangeNames) => {
|
|
2475
|
+
let converted = value.replace(/(\$?[A-Za-z]{1,3}\$?\d+):(\$?[A-Za-z]{1,3}\$?\d+)/g, (match, start, end, offset) => {
|
|
2476
|
+
if (!isValidRefBoundary(value, offset, match.length)) {
|
|
2477
|
+
return match;
|
|
2478
|
+
}
|
|
2479
|
+
if (isNamedRangeToken(start, namedRangeNames))
|
|
2480
|
+
return match;
|
|
2481
|
+
if (isNamedRangeToken(end, namedRangeNames))
|
|
2482
|
+
return match;
|
|
2483
|
+
return `[.${start}:.${end}]`;
|
|
2484
|
+
});
|
|
2485
|
+
const parts = splitByBracketedSegments(converted);
|
|
2486
|
+
converted = parts
|
|
2487
|
+
.map((part) => {
|
|
2488
|
+
if (part.isBracket)
|
|
2489
|
+
return part.value;
|
|
2490
|
+
return part.value.replace(/(\$?[A-Za-z]{1,3}\$?\d+)/g, (match, ref, offset) => {
|
|
2491
|
+
if (!isValidRefBoundary(part.value, offset, match.length)) {
|
|
2492
|
+
return match;
|
|
2493
|
+
}
|
|
2494
|
+
if (isNamedRangeToken(ref, namedRangeNames)) {
|
|
2495
|
+
return match;
|
|
2496
|
+
}
|
|
2497
|
+
return `[.${ref}]`;
|
|
2498
|
+
});
|
|
2499
|
+
})
|
|
2500
|
+
.join("");
|
|
2501
|
+
return converted;
|
|
2502
|
+
};
|
|
2503
|
+
const isValidRefBoundary = (text, offset, length) => {
|
|
2504
|
+
const prev = offset > 0 ? text[offset - 1] : "";
|
|
2505
|
+
const next = offset + length < text.length ? text[offset + length] : "";
|
|
2506
|
+
if (prev && /[A-Za-z0-9_.]/.test(prev))
|
|
2507
|
+
return false;
|
|
2508
|
+
if (next && /[A-Za-z0-9_.]/.test(next))
|
|
2509
|
+
return false;
|
|
2510
|
+
return true;
|
|
2511
|
+
};
|
|
2512
|
+
const isNamedRangeToken = (token, namedRangeNames) => {
|
|
2513
|
+
if (!namedRangeNames || !token)
|
|
2514
|
+
return false;
|
|
2515
|
+
const normalized = token.replace(/\$/g, "").toUpperCase();
|
|
2516
|
+
return namedRangeNames.has(normalized);
|
|
2517
|
+
};
|
|
2518
|
+
const formatOdsSheetName = (sheet) => {
|
|
2519
|
+
if (!sheet)
|
|
2520
|
+
return sheet;
|
|
2521
|
+
let normalized = sheet;
|
|
2522
|
+
if (normalized.startsWith("'") && normalized.endsWith("'")) {
|
|
2523
|
+
normalized = normalized.slice(1, -1).replace(/''/g, "'");
|
|
2524
|
+
}
|
|
2525
|
+
if (/[^A-Za-z0-9_]/.test(normalized)) {
|
|
2526
|
+
return `'${normalized.replace(/'/g, "''")}'`;
|
|
2527
|
+
}
|
|
2528
|
+
return normalized;
|
|
2529
|
+
};
|
|
2530
|
+
const groupEmbedsBySheet = (embeds) => {
|
|
2531
|
+
const result = new Map();
|
|
2532
|
+
for (const embed of embeds ?? []) {
|
|
2533
|
+
if (!embed.position?.overlayPosition)
|
|
2534
|
+
continue;
|
|
2535
|
+
if (embed.spec?.type !== "image")
|
|
2536
|
+
continue;
|
|
2537
|
+
const overlay = embed.position.overlayPosition;
|
|
2538
|
+
const placement = {
|
|
2539
|
+
kind: "embed",
|
|
2540
|
+
imageUrl: embed.spec.imageUrl,
|
|
2541
|
+
rowIndex: overlay.anchorCell.rowIndex,
|
|
2542
|
+
columnIndex: overlay.anchorCell.columnIndex,
|
|
2543
|
+
offsetX: overlay.offsetXPixels ?? 0,
|
|
2544
|
+
offsetY: overlay.offsetYPixels ?? 0,
|
|
2545
|
+
width: overlay.widthPixels ?? 120,
|
|
2546
|
+
height: overlay.heightPixels ?? 120,
|
|
2547
|
+
};
|
|
2548
|
+
const list = result.get(embed.position.sheetId) ?? [];
|
|
2549
|
+
list.push(placement);
|
|
2550
|
+
result.set(embed.position.sheetId, list);
|
|
2551
|
+
}
|
|
2552
|
+
return result;
|
|
2553
|
+
};
|
|
2554
|
+
const groupChartsBySheet = (charts) => {
|
|
2555
|
+
const result = new Map();
|
|
2556
|
+
for (const chart of charts ?? []) {
|
|
2557
|
+
const overlay = chart.position?.overlayPosition;
|
|
2558
|
+
if (!overlay)
|
|
2559
|
+
continue;
|
|
2560
|
+
const placement = {
|
|
2561
|
+
chart,
|
|
2562
|
+
rowIndex: overlay.anchorCell.rowIndex,
|
|
2563
|
+
columnIndex: overlay.anchorCell.columnIndex,
|
|
2564
|
+
offsetX: overlay.offsetXPixels ?? 0,
|
|
2565
|
+
offsetY: overlay.offsetYPixels ?? 0,
|
|
2566
|
+
width: overlay.widthPixels ?? 300,
|
|
2567
|
+
height: overlay.heightPixels ?? 200,
|
|
2568
|
+
};
|
|
2569
|
+
const list = result.get(chart.position.sheetId) ?? [];
|
|
2570
|
+
list.push(placement);
|
|
2571
|
+
result.set(chart.position.sheetId, list);
|
|
2572
|
+
}
|
|
2573
|
+
return result;
|
|
2574
|
+
};
|
|
2575
|
+
const buildEmbedFramesXml = async ({ embeds, mediaEntries, sheetName, sheet, }) => {
|
|
2576
|
+
if (!embeds.length)
|
|
2577
|
+
return [];
|
|
2578
|
+
const frames = [];
|
|
2579
|
+
for (const placement of embeds) {
|
|
2580
|
+
const dataUrl = await resolveImageDataUrl(placement.imageUrl);
|
|
2581
|
+
if (!dataUrl)
|
|
2582
|
+
continue;
|
|
2583
|
+
const buffer = dataUriToBuffer(dataUrl, placement.imageUrl);
|
|
2584
|
+
if (!buffer)
|
|
2585
|
+
continue;
|
|
2586
|
+
const mediaPath = `Pictures/image${mediaEntries.length + 1}.${buffer.extension}`;
|
|
2587
|
+
mediaEntries.push({
|
|
2588
|
+
path: mediaPath,
|
|
2589
|
+
data: buffer.data,
|
|
2590
|
+
mime: buffer.contentType,
|
|
2591
|
+
});
|
|
2592
|
+
const widthPx = Math.max(5, placement.width ?? 120);
|
|
2593
|
+
const heightPx = Math.max(5, placement.height ?? 120);
|
|
2594
|
+
const offsetXPx = placement.offsetX ?? 0;
|
|
2595
|
+
const offsetYPx = placement.offsetY ?? 0;
|
|
2596
|
+
const width = pxToCm(widthPx).toFixed(2);
|
|
2597
|
+
const height = pxToCm(heightPx).toFixed(2);
|
|
2598
|
+
const origin = getCellOriginPx(sheet, placement.rowIndex, placement.columnIndex);
|
|
2599
|
+
const offsetX = pxToCm(origin.x + offsetXPx).toFixed(2);
|
|
2600
|
+
const offsetY = pxToCm(origin.y + offsetYPx).toFixed(2);
|
|
2601
|
+
const anchorRef = cellToAddress({
|
|
2602
|
+
rowIndex: placement.rowIndex,
|
|
2603
|
+
columnIndex: placement.columnIndex,
|
|
2604
|
+
});
|
|
2605
|
+
const anchorCell = anchorRef ?? undefined;
|
|
2606
|
+
const anchorAttr = anchorCell
|
|
2607
|
+
? ` table:anchor-cell-address="${escapeXmlAddress(anchorCell)}"`
|
|
2608
|
+
: "";
|
|
2609
|
+
const endColumn = resolveAnchorEndpoint({
|
|
2610
|
+
startIndex: placement.columnIndex,
|
|
2611
|
+
startOffset: offsetXPx,
|
|
2612
|
+
span: widthPx,
|
|
2613
|
+
getSize: (index) => getColumnWidthPx(sheet, index),
|
|
2614
|
+
});
|
|
2615
|
+
const endRow = resolveAnchorEndpoint({
|
|
2616
|
+
startIndex: placement.rowIndex,
|
|
2617
|
+
startOffset: offsetYPx,
|
|
2618
|
+
span: heightPx,
|
|
2619
|
+
getSize: (index) => getRowHeightPx(sheet, index),
|
|
2620
|
+
});
|
|
2621
|
+
const endRef = cellToAddress({
|
|
2622
|
+
rowIndex: endRow.index,
|
|
2623
|
+
columnIndex: endColumn.index,
|
|
2624
|
+
});
|
|
2625
|
+
const endCell = endRef ?? undefined;
|
|
2626
|
+
const endCellAttr = endCell
|
|
2627
|
+
? ` table:end-cell-address="${escapeXmlAddress(endCell)}"`
|
|
2628
|
+
: "";
|
|
2629
|
+
const endX = pxToCm(endColumn.offset).toFixed(2);
|
|
2630
|
+
const endY = pxToCm(endRow.offset).toFixed(2);
|
|
2631
|
+
frames.push(`<draw:frame draw:name="Image${mediaEntries.length}" svg:width="${width}cm" svg:height="${height}cm" svg:x="${offsetX}cm" svg:y="${offsetY}cm" table:anchor-type="cell"${anchorAttr}${endCellAttr} table:end-x="${endX}cm" table:end-y="${endY}cm"><draw:image xlink:href="${mediaPath}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/></draw:frame>`);
|
|
2632
|
+
}
|
|
2633
|
+
return frames;
|
|
2634
|
+
};
|
|
2635
|
+
const getDefaultColumnWidthPx = (sheet) => {
|
|
2636
|
+
if (sheet?.defaultColumnWidth) {
|
|
2637
|
+
return Number(sheet.defaultColumnWidth) || DEFAULT_COLUMN_WIDTH_PX;
|
|
2638
|
+
}
|
|
2639
|
+
return DEFAULT_COLUMN_WIDTH_PX;
|
|
2640
|
+
};
|
|
2641
|
+
const getColumnWidthPx = (sheet, columnIndex) => {
|
|
2642
|
+
const metadata = sheet.columnMetadata?.[columnIndex];
|
|
2643
|
+
if (metadata?.size && metadata.size > 0) {
|
|
2644
|
+
return metadata.size;
|
|
2645
|
+
}
|
|
2646
|
+
return getDefaultColumnWidthPx(sheet);
|
|
2647
|
+
};
|
|
2648
|
+
const getDefaultRowHeightPx = (sheet) => {
|
|
2649
|
+
if (sheet?.defaultRowHeight) {
|
|
2650
|
+
return Number(sheet.defaultRowHeight) || DEFAULT_ROW_HEIGHT_PX;
|
|
2651
|
+
}
|
|
2652
|
+
return DEFAULT_ROW_HEIGHT_PX;
|
|
2653
|
+
};
|
|
2654
|
+
const getRowHeightPx = (sheet, rowIndex) => {
|
|
2655
|
+
const metadata = sheet.rowMetadata?.[rowIndex];
|
|
2656
|
+
if (metadata?.size && metadata.resizedByUser) {
|
|
2657
|
+
return metadata.size;
|
|
2658
|
+
}
|
|
2659
|
+
return getDefaultRowHeightPx(sheet);
|
|
2660
|
+
};
|
|
2661
|
+
const getCellSizePx = (sheet, rowIndex, columnIndex, mergeSpan) => {
|
|
2662
|
+
const columns = Math.max(1, mergeSpan?.columns ?? 1);
|
|
2663
|
+
const rows = Math.max(1, mergeSpan?.rows ?? 1);
|
|
2664
|
+
let width = 0;
|
|
2665
|
+
for (let col = 0; col < columns; col += 1) {
|
|
2666
|
+
width += getColumnWidthPx(sheet, columnIndex + col);
|
|
2667
|
+
}
|
|
2668
|
+
let height = 0;
|
|
2669
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2670
|
+
height += getRowHeightPx(sheet, rowIndex + row);
|
|
2671
|
+
}
|
|
2672
|
+
return {
|
|
2673
|
+
width: Math.max(5, width),
|
|
2674
|
+
height: Math.max(5, height),
|
|
2675
|
+
};
|
|
2676
|
+
};
|
|
2677
|
+
const getCellOriginPx = (sheet, rowIndex, columnIndex) => {
|
|
2678
|
+
let x = 0;
|
|
2679
|
+
for (let col = 1; col < columnIndex; col += 1) {
|
|
2680
|
+
x += getColumnWidthPx(sheet, col);
|
|
2681
|
+
}
|
|
2682
|
+
let y = 0;
|
|
2683
|
+
for (let row = 1; row < rowIndex; row += 1) {
|
|
2684
|
+
y += getRowHeightPx(sheet, row);
|
|
2685
|
+
}
|
|
2686
|
+
return { x, y };
|
|
2687
|
+
};
|
|
2688
|
+
const resolveAnchorEndpoint = ({ startIndex, startOffset, span, getSize, }) => {
|
|
2689
|
+
let remaining = Math.max(0, span);
|
|
2690
|
+
let index = Math.max(1, startIndex);
|
|
2691
|
+
let offset = Math.max(0, startOffset);
|
|
2692
|
+
while (remaining > 0) {
|
|
2693
|
+
const size = Math.max(1, getSize(index));
|
|
2694
|
+
const available = size - offset;
|
|
2695
|
+
if (remaining <= available) {
|
|
2696
|
+
return {
|
|
2697
|
+
index,
|
|
2698
|
+
offset: offset + remaining,
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
remaining -= available;
|
|
2702
|
+
index += 1;
|
|
2703
|
+
offset = 0;
|
|
2704
|
+
}
|
|
2705
|
+
return { index, offset };
|
|
2706
|
+
};
|
|
2707
|
+
const buildChartObjectXml = (chart, sheetNameById) => {
|
|
2708
|
+
const spec = chart.spec;
|
|
2709
|
+
const series = spec?.series ?? [];
|
|
2710
|
+
if (!series.length)
|
|
2711
|
+
return undefined;
|
|
2712
|
+
const domain = spec?.domains?.[0]?.sources?.[0];
|
|
2713
|
+
const domainRange = domain
|
|
2714
|
+
? buildChartRangeAddress(domain, sheetNameById)
|
|
2715
|
+
: undefined;
|
|
2716
|
+
const seriesRanges = series
|
|
2717
|
+
.map((entry) => entry?.sources?.[0])
|
|
2718
|
+
.map((range) => range ? buildChartRangeAddress(range, sheetNameById) : undefined)
|
|
2719
|
+
.filter((range) => Boolean(range));
|
|
2720
|
+
if (!seriesRanges.length)
|
|
2721
|
+
return undefined;
|
|
2722
|
+
const typeMap = {
|
|
2723
|
+
bar: "chart:bar",
|
|
2724
|
+
column: "chart:bar",
|
|
2725
|
+
line: "chart:line",
|
|
2726
|
+
pie: "chart:circle",
|
|
2727
|
+
scatter: "chart:scatter",
|
|
2728
|
+
area: "chart:area",
|
|
2729
|
+
};
|
|
2730
|
+
const chartClass = typeMap[spec.chartType] ?? "chart:bar";
|
|
2731
|
+
const title = spec.title
|
|
2732
|
+
? `<chart:title><text:p>${escapeXml(String(spec.title))}</text:p></chart:title>`
|
|
2733
|
+
: "";
|
|
2734
|
+
const seriesPalette = [
|
|
2735
|
+
"#4285f4",
|
|
2736
|
+
"#ea4335",
|
|
2737
|
+
"#fbbc04",
|
|
2738
|
+
"#34a853",
|
|
2739
|
+
"#ff6d01",
|
|
2740
|
+
"#46bdc6",
|
|
2741
|
+
];
|
|
2742
|
+
// Series chart styles + (optional) gradient defs. When a series
|
|
2743
|
+
// carries an explicit gradient we emit a `<draw:gradient>` def
|
|
2744
|
+
// alongside it and reference the def via `draw:fill-gradient-name`
|
|
2745
|
+
// on the series's graphic-properties. ODS uses 1/10ths of a degree
|
|
2746
|
+
// for `draw:angle` (so a 90° XLSX gradient is `900` here) and
|
|
2747
|
+
// `linear` / `radial` for the two flavors we round-trip.
|
|
2748
|
+
const gradientDefs = [];
|
|
2749
|
+
const seriesStyles = seriesRanges
|
|
2750
|
+
.map((_range, index) => {
|
|
2751
|
+
const color = seriesPalette[index % seriesPalette.length];
|
|
2752
|
+
const seriesData = series[index];
|
|
2753
|
+
const gradient = seriesData?.gradient;
|
|
2754
|
+
if (gradient && gradient.stops?.length >= 2) {
|
|
2755
|
+
const gradName = `grad${index}`;
|
|
2756
|
+
const startColor = gradient.stops[0].color;
|
|
2757
|
+
const endColor = gradient.stops[gradient.stops.length - 1].color;
|
|
2758
|
+
if (gradient.type === "path") {
|
|
2759
|
+
// Path / radial. ODS center is in 0..100% — use the inset
|
|
2760
|
+
// midpoints if supplied, fall back to dead center.
|
|
2761
|
+
const cx = Math.round((((gradient.left ?? 0) + (1 - (gradient.right ?? 0))) / 2) * 100);
|
|
2762
|
+
const cy = Math.round((((gradient.top ?? 0) + (1 - (gradient.bottom ?? 0))) / 2) * 100);
|
|
2763
|
+
gradientDefs.push(`<draw:gradient draw:name="${gradName}" draw:style="radial" draw:start-color="${startColor}" draw:end-color="${endColor}" draw:start-intensity="100%" draw:end-intensity="100%" draw:cx="${cx}%" draw:cy="${cy}%"/>`);
|
|
2764
|
+
}
|
|
2765
|
+
else {
|
|
2766
|
+
// XLSX `degree` (0..360 clockwise from right) ↔ ODS
|
|
2767
|
+
// `draw:angle` (0..3600 = 1/10° units; 0 is "upward").
|
|
2768
|
+
// We don't try to reconcile the start-of-axis difference —
|
|
2769
|
+
// most viewers tolerate either convention as long as the
|
|
2770
|
+
// two endpoints match.
|
|
2771
|
+
const angle = Math.round((gradient.degree ?? 0) * 10);
|
|
2772
|
+
gradientDefs.push(`<draw:gradient draw:name="${gradName}" draw:style="linear" draw:start-color="${startColor}" draw:end-color="${endColor}" draw:start-intensity="100%" draw:end-intensity="100%" draw:angle="${angle}"/>`);
|
|
2773
|
+
}
|
|
2774
|
+
return `<style:style style:name="series${index}" style:family="chart"><style:graphic-properties svg:stroke-color="${startColor}" svg:stroke-width="0.08cm" draw:fill="gradient" draw:fill-gradient-name="${gradName}"/></style:style>`;
|
|
2775
|
+
}
|
|
2776
|
+
const fillColor = seriesData?.color || color;
|
|
2777
|
+
return `<style:style style:name="series${index}" style:family="chart"><style:graphic-properties svg:stroke-color="${fillColor}" svg:stroke-width="0.08cm" draw:fill="none"/></style:style>`;
|
|
2778
|
+
})
|
|
2779
|
+
.join("");
|
|
2780
|
+
const gradientDefsXml = gradientDefs.join("");
|
|
2781
|
+
let hasSeriesLabels = false;
|
|
2782
|
+
const seriesXml = seriesRanges
|
|
2783
|
+
.map((range, index) => {
|
|
2784
|
+
const labelAddress = buildChartLabelAddress(series[index]?.dataLabel, sheetNameById, chart.position?.sheetId) ||
|
|
2785
|
+
buildChartLabelAddressFromRange(series[index]?.sources?.[0], sheetNameById);
|
|
2786
|
+
const labelAttr = labelAddress
|
|
2787
|
+
? ` chart:label-cell-address="${escapeXmlAddress(labelAddress)}"`
|
|
2788
|
+
: "";
|
|
2789
|
+
if (labelAddress) {
|
|
2790
|
+
hasSeriesLabels = true;
|
|
2791
|
+
}
|
|
2792
|
+
const count = getRangePointCount(series[index]?.sources?.[0]);
|
|
2793
|
+
const pointsXml = count
|
|
2794
|
+
? `<chart:data-point chart:repeated="${count}"/>`
|
|
2795
|
+
: "";
|
|
2796
|
+
return `<chart:series chart:class="${chartClass}" chart:style-name="series${index}" chart:values-cell-range-address="${escapeXmlAddress(range)}"${labelAttr}>${pointsXml}</chart:series>`;
|
|
2797
|
+
})
|
|
2798
|
+
.join("");
|
|
2799
|
+
const axisXml = chartClass === "chart:circle"
|
|
2800
|
+
? ""
|
|
2801
|
+
: `<chart:axis chart:dimension="x" chart:name="primary-x" chart:style-name="axisx"><chart:title/></chart:axis><chart:axis chart:dimension="y" chart:name="primary-y" chart:style-name="axisy"><chart:title/></chart:axis>`;
|
|
2802
|
+
const categoriesAttr = domainRange
|
|
2803
|
+
? ` chart:categories-cell-range-address="${escapeXmlAddress(domainRange)}"`
|
|
2804
|
+
: "";
|
|
2805
|
+
const labelMode = domainRange ? "both" : hasSeriesLabels ? "row" : "none";
|
|
2806
|
+
const plotAreaAttrs = ` chart:data-source-has-labels="${labelMode}"${categoriesAttr}`;
|
|
2807
|
+
const wallFloor = chartClass === "chart:circle"
|
|
2808
|
+
? ""
|
|
2809
|
+
: `<chart:wall chart:style-name="wall1"/><chart:floor chart:style-name="floor1"/>`;
|
|
2810
|
+
const legendXml = chartClass === "chart:circle"
|
|
2811
|
+
? ""
|
|
2812
|
+
: `<chart:legend chart:legend-position="end" chart:legend-align="center" chart:style-name="legend1"/>`;
|
|
2813
|
+
return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:chart="${ODS_NS.chart}" xmlns:table="${ODS_NS.table}" xmlns:text="${ODS_NS.text}" xmlns:style="${ODS_NS.style}" xmlns:draw="${ODS_NS.draw}" xmlns:svg="${ODS_NS.svg}" xmlns:fo="${ODS_NS.fo}" office:version="1.2"><office:automatic-styles>${gradientDefsXml}<style:style style:name="ch1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff"/></style:style><style:style style:name="plot1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="wall1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3" svg:stroke-width="0.01cm"/></style:style><style:style style:name="floor1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="legend1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="10pt"/></style:style><style:style style:name="axisx" style:family="chart"><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style><style:style style:name="axisy" style:family="chart"><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style>${seriesStyles}</office:automatic-styles><office:body><office:chart><chart:chart chart:class="${chartClass}" chart:style-name="ch1">${title}${legendXml}<chart:plot-area chart:style-name="plot1"${plotAreaAttrs}>${axisXml}${seriesXml}${wallFloor}</chart:plot-area></chart:chart></office:chart></office:body></office:document-content>`;
|
|
2814
|
+
};
|
|
2815
|
+
const buildChartRangeAddress = (range, sheetNameById) => {
|
|
2816
|
+
const sheetName = sheetNameById.get(range.sheetId);
|
|
2817
|
+
if (!sheetName)
|
|
2818
|
+
return undefined;
|
|
2819
|
+
const start = cellToAddress({
|
|
2820
|
+
rowIndex: range.startRowIndex,
|
|
2821
|
+
columnIndex: range.startColumnIndex,
|
|
2822
|
+
}, true, true);
|
|
2823
|
+
const end = cellToAddress({
|
|
2824
|
+
rowIndex: range.endRowIndex,
|
|
2825
|
+
columnIndex: range.endColumnIndex,
|
|
2826
|
+
}, true, true);
|
|
2827
|
+
if (!start || !end)
|
|
2828
|
+
return undefined;
|
|
2829
|
+
const escaped = escapeSheetName(sheetName);
|
|
2830
|
+
return `${escaped}.${start}:${end}`;
|
|
2831
|
+
};
|
|
2832
|
+
const getRangePointCount = (range) => {
|
|
2833
|
+
if (!range)
|
|
2834
|
+
return undefined;
|
|
2835
|
+
const rows = Math.abs(range.endRowIndex - range.startRowIndex) + 1;
|
|
2836
|
+
const cols = Math.abs(range.endColumnIndex - range.startColumnIndex) + 1;
|
|
2837
|
+
return Math.max(rows, cols);
|
|
2838
|
+
};
|
|
2839
|
+
const buildChartLabelAddress = (label, sheetNameById, sheetId) => {
|
|
2840
|
+
if (!label)
|
|
2841
|
+
return "";
|
|
2842
|
+
if (label.startsWith("=")) {
|
|
2843
|
+
const formula = label.slice(1).trim();
|
|
2844
|
+
if (formula.includes("."))
|
|
2845
|
+
return formula;
|
|
2846
|
+
const sheetName = sheetId != null ? sheetNameById.get(sheetId) : undefined;
|
|
2847
|
+
return sheetName ? `${escapeSheetName(sheetName)}.${formula}` : formula;
|
|
2848
|
+
}
|
|
2849
|
+
return "";
|
|
2850
|
+
};
|
|
2851
|
+
const buildChartLabelAddressFromRange = (range, sheetNameById) => {
|
|
2852
|
+
if (!range || range.startRowIndex <= 1)
|
|
2853
|
+
return "";
|
|
2854
|
+
const sheetName = sheetNameById.get(range.sheetId);
|
|
2855
|
+
if (!sheetName)
|
|
2856
|
+
return "";
|
|
2857
|
+
const labelRef = cellToAddress({
|
|
2858
|
+
rowIndex: range.startRowIndex - 1,
|
|
2859
|
+
columnIndex: range.startColumnIndex,
|
|
2860
|
+
}, true, true);
|
|
2861
|
+
if (!labelRef)
|
|
2862
|
+
return "";
|
|
2863
|
+
return `${escapeSheetName(sheetName)}.${labelRef}`;
|
|
2864
|
+
};
|
|
2865
|
+
const buildChartFramesXml = async ({ charts, sheet, sheetName, sheetNameById, startIndex, }) => {
|
|
2866
|
+
if (!charts.length) {
|
|
2867
|
+
return { frames: [], chartObjects: [], nextIndex: startIndex };
|
|
2868
|
+
}
|
|
2869
|
+
const frames = [];
|
|
2870
|
+
const chartObjects = [];
|
|
2871
|
+
let index = startIndex;
|
|
2872
|
+
for (const placement of charts) {
|
|
2873
|
+
const chartXml = buildChartObjectXml(placement.chart, sheetNameById);
|
|
2874
|
+
if (!chartXml)
|
|
2875
|
+
continue;
|
|
2876
|
+
const folder = `Object ${index}`;
|
|
2877
|
+
chartObjects.push({ folder, contentXml: chartXml });
|
|
2878
|
+
const widthPx = Math.max(5, placement.width ?? 300);
|
|
2879
|
+
const heightPx = Math.max(5, placement.height ?? 200);
|
|
2880
|
+
const offsetXPx = placement.offsetX ?? 0;
|
|
2881
|
+
const offsetYPx = placement.offsetY ?? 0;
|
|
2882
|
+
const width = pxToCm(widthPx).toFixed(2);
|
|
2883
|
+
const height = pxToCm(heightPx).toFixed(2);
|
|
2884
|
+
const origin = getCellOriginPx(sheet, placement.rowIndex, placement.columnIndex);
|
|
2885
|
+
const offsetX = pxToCm(origin.x + offsetXPx).toFixed(2);
|
|
2886
|
+
const offsetY = pxToCm(origin.y + offsetYPx).toFixed(2);
|
|
2887
|
+
const anchorRef = cellToAddress({
|
|
2888
|
+
rowIndex: placement.rowIndex,
|
|
2889
|
+
columnIndex: placement.columnIndex,
|
|
2890
|
+
});
|
|
2891
|
+
const anchorCell = anchorRef ?? undefined;
|
|
2892
|
+
const anchorAttr = anchorCell
|
|
2893
|
+
? ` table:anchor-cell-address="${escapeXmlAddress(anchorCell)}"`
|
|
2894
|
+
: "";
|
|
2895
|
+
const endColumn = resolveAnchorEndpoint({
|
|
2896
|
+
startIndex: placement.columnIndex,
|
|
2897
|
+
startOffset: offsetXPx,
|
|
2898
|
+
span: widthPx,
|
|
2899
|
+
getSize: (index) => getColumnWidthPx(sheet, index),
|
|
2900
|
+
});
|
|
2901
|
+
const endRow = resolveAnchorEndpoint({
|
|
2902
|
+
startIndex: placement.rowIndex,
|
|
2903
|
+
startOffset: offsetYPx,
|
|
2904
|
+
span: heightPx,
|
|
2905
|
+
getSize: (index) => getRowHeightPx(sheet, index),
|
|
2906
|
+
});
|
|
2907
|
+
const endRef = cellToAddress({
|
|
2908
|
+
rowIndex: endRow.index,
|
|
2909
|
+
columnIndex: endColumn.index,
|
|
2910
|
+
});
|
|
2911
|
+
const endCell = endRef ?? undefined;
|
|
2912
|
+
const endCellAttr = endCell
|
|
2913
|
+
? ` table:end-cell-address="${escapeXmlAddress(endCell)}"`
|
|
2914
|
+
: "";
|
|
2915
|
+
const endX = pxToCm(endColumn.offset).toFixed(2);
|
|
2916
|
+
const endY = pxToCm(endRow.offset).toFixed(2);
|
|
2917
|
+
frames.push(`<draw:frame draw:name="Chart${index}" svg:width="${width}cm" svg:height="${height}cm" svg:x="${offsetX}cm" svg:y="${offsetY}cm" table:anchor-type="cell"${anchorAttr}${endCellAttr} table:end-x="${endX}cm" table:end-y="${endY}cm"><draw:object xlink:href="${folder}" xlink:type="simple"/></draw:frame>`);
|
|
2918
|
+
index += 1;
|
|
2919
|
+
}
|
|
2920
|
+
return { frames, chartObjects, nextIndex: index };
|
|
2921
|
+
};
|
|
2922
|
+
const buildCellImageXml = async ({ imageUrl, rowNumber, columnIndex, mergeSpan, sheet, mediaEntries, }) => {
|
|
2923
|
+
if (!imageUrl)
|
|
2924
|
+
return "";
|
|
2925
|
+
const dataUrl = await resolveImageDataUrl(imageUrl);
|
|
2926
|
+
if (!dataUrl)
|
|
2927
|
+
return "";
|
|
2928
|
+
const buffer = dataUriToBuffer(dataUrl, imageUrl);
|
|
2929
|
+
if (!buffer)
|
|
2930
|
+
return "";
|
|
2931
|
+
const mediaPath = `Pictures/image${mediaEntries.length + 1}.${buffer.extension}`;
|
|
2932
|
+
mediaEntries.push({
|
|
2933
|
+
path: mediaPath,
|
|
2934
|
+
data: buffer.data,
|
|
2935
|
+
mime: buffer.contentType,
|
|
2936
|
+
});
|
|
2937
|
+
const size = getCellSizePx(sheet, rowNumber, columnIndex, mergeSpan);
|
|
2938
|
+
const width = pxToCm(size.width).toFixed(2);
|
|
2939
|
+
const height = pxToCm(size.height).toFixed(2);
|
|
2940
|
+
return `<draw:frame draw:name="CellImage${mediaEntries.length}" svg:width="${width}cm" svg:height="${height}cm" table:anchor-type="cell"><draw:image xlink:href="${mediaPath}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/></draw:frame>`;
|
|
2941
|
+
};
|
|
2942
|
+
const resolveImageDataUrl = async (imageUrl) => {
|
|
2943
|
+
if (imageUrl.startsWith("data:")) {
|
|
2944
|
+
return imageUrl;
|
|
2945
|
+
}
|
|
2946
|
+
try {
|
|
2947
|
+
return await imageUrlToBase64(imageUrl);
|
|
2948
|
+
}
|
|
2949
|
+
catch {
|
|
2950
|
+
return undefined;
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
const dataUriToBuffer = (dataUrl, fallbackName) => {
|
|
2954
|
+
const match = /^data:(.+?);base64,(.+)$/.exec(dataUrl);
|
|
2955
|
+
if (!match)
|
|
2956
|
+
return undefined;
|
|
2957
|
+
const [, mime, base64] = match;
|
|
2958
|
+
const binaryString = decodeBase64(base64);
|
|
2959
|
+
const buffer = new Uint8Array(binaryString.length);
|
|
2960
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
2961
|
+
buffer[i] = binaryString.charCodeAt(i);
|
|
2962
|
+
}
|
|
2963
|
+
const extension = mime.split("/")[1] ?? fallbackName?.split(".").pop() ?? "bin";
|
|
2964
|
+
return {
|
|
2965
|
+
data: buffer,
|
|
2966
|
+
extension: extension.toLowerCase(),
|
|
2967
|
+
contentType: mime,
|
|
2968
|
+
};
|
|
2969
|
+
};
|
|
2970
|
+
const decodeBase64 = (value) => {
|
|
2971
|
+
if (typeof atob === "function") {
|
|
2972
|
+
return atob(value);
|
|
2973
|
+
}
|
|
2974
|
+
const bufferCtor = globalThis.Buffer;
|
|
2975
|
+
if (bufferCtor) {
|
|
2976
|
+
return bufferCtor.from(value, "base64").toString("binary");
|
|
2977
|
+
}
|
|
2978
|
+
throw new Error("Base64 decoding is not supported in this environment");
|
|
2979
|
+
};
|
|
2980
|
+
const prepareTables = (tables) => {
|
|
2981
|
+
const result = [];
|
|
2982
|
+
tables.forEach((table, index) => {
|
|
2983
|
+
const ref = gridRangeToRef(table.range);
|
|
2984
|
+
if (!ref)
|
|
2985
|
+
return;
|
|
2986
|
+
const id = index + 1;
|
|
2987
|
+
const sanitized = sanitizeTableName(table.title ?? `Table${id}`);
|
|
2988
|
+
result.push({
|
|
2989
|
+
...table,
|
|
2990
|
+
id,
|
|
2991
|
+
ref,
|
|
2992
|
+
name: sanitized,
|
|
2993
|
+
});
|
|
2994
|
+
});
|
|
2995
|
+
return result;
|
|
2996
|
+
};
|
|
2997
|
+
const sanitizeTableName = (name) => {
|
|
2998
|
+
const cleaned = name.replace(/[^A-Za-z0-9_]/g, "") || "Table";
|
|
2999
|
+
if (/^\d/.test(cleaned)) {
|
|
3000
|
+
return `T${cleaned}`;
|
|
3001
|
+
}
|
|
3002
|
+
return cleaned;
|
|
3003
|
+
};
|
|
3004
|
+
const gridRangeToRef = (range) => {
|
|
3005
|
+
if (!range)
|
|
3006
|
+
return undefined;
|
|
3007
|
+
try {
|
|
3008
|
+
const ref = cellRangeToAddress(undefined, range);
|
|
3009
|
+
return ref ?? undefined;
|
|
3010
|
+
}
|
|
3011
|
+
catch {
|
|
3012
|
+
return undefined;
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
const convertDataValidationRule = (rule) => {
|
|
3016
|
+
if (!rule?.condition?.type)
|
|
3017
|
+
return undefined;
|
|
3018
|
+
const { condition } = rule;
|
|
3019
|
+
const values = condition.values ?? [];
|
|
3020
|
+
const allowBlank = rule.allowBlank !== false;
|
|
3021
|
+
const displayStyle = rule.displayStyle === "plain"
|
|
3022
|
+
? "none"
|
|
3023
|
+
: rule.displayStyle === "arrow"
|
|
3024
|
+
? "list"
|
|
3025
|
+
: undefined;
|
|
3026
|
+
const inputMessage = rule.inputMessage;
|
|
3027
|
+
const alert = rule.alert;
|
|
3028
|
+
const base = {
|
|
3029
|
+
allowEmptyCell: allowBlank,
|
|
3030
|
+
displayList: displayStyle,
|
|
3031
|
+
helpMessage: inputMessage
|
|
3032
|
+
? { title: inputMessage.title, message: inputMessage.message }
|
|
3033
|
+
: undefined,
|
|
3034
|
+
errorMessage: alert?.message
|
|
3035
|
+
? {
|
|
3036
|
+
type: alert.style ?? "stop",
|
|
3037
|
+
title: alert.message.title,
|
|
3038
|
+
message: alert.message.message,
|
|
3039
|
+
}
|
|
3040
|
+
: undefined,
|
|
3041
|
+
};
|
|
3042
|
+
const ensureFormula = (value) => value.startsWith("=") ? value.slice(1) : value;
|
|
3043
|
+
if (condition.type === "ONE_OF_LIST") {
|
|
3044
|
+
const entries = values
|
|
3045
|
+
.map((value) => value.userEnteredValue)
|
|
3046
|
+
.filter((value) => typeof value === "string");
|
|
3047
|
+
if (!entries.length)
|
|
3048
|
+
return undefined;
|
|
3049
|
+
const args = entries.map((entry) => `"${entry.replace(/"/g, '""')}"`);
|
|
3050
|
+
return {
|
|
3051
|
+
...base,
|
|
3052
|
+
condition: `of:cell-content-is-in-list(${args.join(";")})`, // Remove the = after of:
|
|
3053
|
+
displayList: "unsorted",
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
if (condition.type === "ONE_OF_RANGE") {
|
|
3057
|
+
const references = values
|
|
3058
|
+
.map((value) => value.userEnteredValue)
|
|
3059
|
+
.filter((value) => typeof value === "string");
|
|
3060
|
+
if (!references.length)
|
|
3061
|
+
return undefined;
|
|
3062
|
+
const ref = references[0];
|
|
3063
|
+
const normalized = ref.replace(/^=/, "");
|
|
3064
|
+
return {
|
|
3065
|
+
condition: `of:=cell-content-is-in-list(${normalized})`,
|
|
3066
|
+
...base,
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
if (condition.type === "CUSTOM_FORMULA") {
|
|
3070
|
+
const raw = values[0]?.userEnteredValue ?? "";
|
|
3071
|
+
if (!raw)
|
|
3072
|
+
return undefined;
|
|
3073
|
+
return {
|
|
3074
|
+
condition: `of:=${ensureFormula(raw)}`,
|
|
3075
|
+
...base,
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
if (condition.type === "BOOLEAN") {
|
|
3079
|
+
return {
|
|
3080
|
+
condition: `of:=OR(cell-content()=TRUE(),cell-content()=FALSE())`,
|
|
3081
|
+
...base,
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
if (condition.type === "BLANK") {
|
|
3085
|
+
return {
|
|
3086
|
+
condition: `of:=cell-content()=""`,
|
|
3087
|
+
...base,
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
if (condition.type === "NOT_BLANK") {
|
|
3091
|
+
return {
|
|
3092
|
+
condition: `of:=cell-content()<>""`,
|
|
3093
|
+
...base,
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
if (condition.type === "TEXT_EQ" || condition.type === "TEXT_NOT_EQ") {
|
|
3097
|
+
const raw = values[0]?.userEnteredValue ?? "";
|
|
3098
|
+
if (!raw)
|
|
3099
|
+
return undefined;
|
|
3100
|
+
const op = condition.type === "TEXT_EQ" ? "=" : "<>";
|
|
3101
|
+
return {
|
|
3102
|
+
condition: `of:=cell-content()${op}"${raw.replace(/"/g, '""')}"`,
|
|
3103
|
+
...base,
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
if (condition.type === "TEXT_CONTAINS") {
|
|
3107
|
+
const raw = values[0]?.userEnteredValue ?? "";
|
|
3108
|
+
if (!raw)
|
|
3109
|
+
return undefined;
|
|
3110
|
+
return {
|
|
3111
|
+
condition: `of:=ISNUMBER(SEARCH("${raw.replace(/"/g, '""')}",cell-content()))`,
|
|
3112
|
+
...base,
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
if (condition.type === "TEXT_NOT_CONTAINS") {
|
|
3116
|
+
const raw = values[0]?.userEnteredValue ?? "";
|
|
3117
|
+
if (!raw)
|
|
3118
|
+
return undefined;
|
|
3119
|
+
return {
|
|
3120
|
+
condition: `of:=ISERROR(SEARCH("${raw.replace(/"/g, '""')}",cell-content()))`,
|
|
3121
|
+
...base,
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
if (condition.type === "TEXT_STARTS_WITH") {
|
|
3125
|
+
const raw = values[0]?.userEnteredValue ?? "";
|
|
3126
|
+
if (!raw)
|
|
3127
|
+
return undefined;
|
|
3128
|
+
return {
|
|
3129
|
+
condition: `of:=LEFT(cell-content(),LEN("${raw.replace(/"/g, '""')}"))="${raw.replace(/"/g, '""')}"`,
|
|
3130
|
+
...base,
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
if (condition.type === "TEXT_ENDS_WITH") {
|
|
3134
|
+
const raw = values[0]?.userEnteredValue ?? "";
|
|
3135
|
+
if (!raw)
|
|
3136
|
+
return undefined;
|
|
3137
|
+
return {
|
|
3138
|
+
condition: `of:=RIGHT(cell-content(),LEN("${raw.replace(/"/g, '""')}"))="${raw.replace(/"/g, '""')}"`,
|
|
3139
|
+
...base,
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
if (condition.type === "TEXT_IS_EMAIL") {
|
|
3143
|
+
return {
|
|
3144
|
+
condition: `of:=AND(ISNUMBER(SEARCH("@",cell-content())),ISNUMBER(SEARCH(".",cell-content())))`,
|
|
3145
|
+
...base,
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
if (condition.type === "TEXT_IS_URL") {
|
|
3149
|
+
return {
|
|
3150
|
+
condition: `of:=OR(LEFT(cell-content(),7)="http://",LEFT(cell-content(),8)="https://")`,
|
|
3151
|
+
...base,
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
if (condition.type === "NUMBER_BETWEEN" ||
|
|
3155
|
+
condition.type === "NUMBER_NOT_BETWEEN" ||
|
|
3156
|
+
condition.type === "NUMBER_EQ" ||
|
|
3157
|
+
condition.type === "NUMBER_NOT_EQ" ||
|
|
3158
|
+
condition.type === "NUMBER_GREATER" ||
|
|
3159
|
+
condition.type === "NUMBER_GREATER_THAN_EQ" ||
|
|
3160
|
+
condition.type === "NUMBER_LESS" ||
|
|
3161
|
+
condition.type === "NUMBER_LESS_THAN_EQ") {
|
|
3162
|
+
const first = values[0]?.userEnteredValue ?? "";
|
|
3163
|
+
if (!first)
|
|
3164
|
+
return undefined;
|
|
3165
|
+
const second = values[1]?.userEnteredValue ?? "";
|
|
3166
|
+
const opMap = {
|
|
3167
|
+
NUMBER_BETWEEN: "cell-content-is-between",
|
|
3168
|
+
NUMBER_NOT_BETWEEN: "cell-content-is-not-between",
|
|
3169
|
+
NUMBER_EQ: "cell-content-is-equal-to",
|
|
3170
|
+
NUMBER_NOT_EQ: "cell-content-is-not-equal-to",
|
|
3171
|
+
NUMBER_GREATER: "cell-content-is-greater-than",
|
|
3172
|
+
NUMBER_GREATER_THAN_EQ: "cell-content-is-greater-than-or-equal-to",
|
|
3173
|
+
NUMBER_LESS: "cell-content-is-less-than",
|
|
3174
|
+
NUMBER_LESS_THAN_EQ: "cell-content-is-less-than-or-equal-to",
|
|
3175
|
+
};
|
|
3176
|
+
const fn = opMap[condition.type];
|
|
3177
|
+
if (!fn)
|
|
3178
|
+
return undefined;
|
|
3179
|
+
if (condition.type === "NUMBER_BETWEEN" ||
|
|
3180
|
+
condition.type === "NUMBER_NOT_BETWEEN") {
|
|
3181
|
+
return {
|
|
3182
|
+
condition: `of:=${fn}(${first},${second})`,
|
|
3183
|
+
...base,
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
return {
|
|
3187
|
+
condition: `of:=${fn}(${first})`,
|
|
3188
|
+
...base,
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
if (condition.type === "DATE_EQ" ||
|
|
3192
|
+
condition.type === "DATE_NOT_EQ" ||
|
|
3193
|
+
condition.type === "DATE_BEFORE" ||
|
|
3194
|
+
condition.type === "DATE_AFTER" ||
|
|
3195
|
+
condition.type === "DATE_ON_OR_BEFORE" ||
|
|
3196
|
+
condition.type === "DATE_ON_OR_AFTER" ||
|
|
3197
|
+
condition.type === "DATE_BETWEEN" ||
|
|
3198
|
+
condition.type === "DATE_NOT_BETWEEN") {
|
|
3199
|
+
const first = values[0]?.userEnteredValue ?? "";
|
|
3200
|
+
if (!first)
|
|
3201
|
+
return undefined;
|
|
3202
|
+
const second = values[1]?.userEnteredValue ?? "";
|
|
3203
|
+
const opMap = {
|
|
3204
|
+
DATE_EQ: "cell-content-date-equal-to",
|
|
3205
|
+
DATE_NOT_EQ: "cell-content-date-not-equal-to",
|
|
3206
|
+
DATE_BEFORE: "cell-content-date-before",
|
|
3207
|
+
DATE_AFTER: "cell-content-date-after",
|
|
3208
|
+
DATE_ON_OR_BEFORE: "cell-content-date-on-or-before",
|
|
3209
|
+
DATE_ON_OR_AFTER: "cell-content-date-on-or-after",
|
|
3210
|
+
DATE_BETWEEN: "cell-content-date-is-between",
|
|
3211
|
+
DATE_NOT_BETWEEN: "cell-content-date-is-not-between",
|
|
3212
|
+
};
|
|
3213
|
+
const fn = opMap[condition.type];
|
|
3214
|
+
if (!fn)
|
|
3215
|
+
return undefined;
|
|
3216
|
+
if (condition.type === "DATE_BETWEEN" ||
|
|
3217
|
+
condition.type === "DATE_NOT_BETWEEN") {
|
|
3218
|
+
return {
|
|
3219
|
+
condition: `of:=${fn}(${first},${second})`,
|
|
3220
|
+
...base,
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
return {
|
|
3224
|
+
condition: `of:=${fn}(${first})`,
|
|
3225
|
+
...base,
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
if (condition.type === "DATE_IS_VALID") {
|
|
3229
|
+
return {
|
|
3230
|
+
condition: `of:=ISNUMBER(cell-content())`,
|
|
3231
|
+
...base,
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
return {
|
|
3235
|
+
condition: `of:=cell-content()`,
|
|
3236
|
+
...base,
|
|
3237
|
+
};
|
|
3238
|
+
};
|