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