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