@rowsncolumns/toolkit 24.4.2 → 24.4.4
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/cjs/excel-parser/cfb-decryptor.d.ts +6 -0
- package/dist/cjs/excel-parser/cfb-decryptor.d.ts.map +1 -1
- package/dist/cjs/excel-parser/cfb-decryptor.js +6 -0
- package/dist/cjs/excel-parser/helpers.d.ts +9 -1
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
- package/dist/cjs/excel-parser/helpers.js +37 -0
- package/dist/cjs/excel-parser/interface.d.ts +15 -2
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/base/index.d.ts +2 -0
- package/dist/cjs/excel-parser/plugins/base/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/base/index.js +3 -0
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts +19 -0
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.js +676 -304
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +43 -1
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/styles.js +343 -45
- package/dist/cjs/excel-parser/plugins/xls/index.d.ts +55 -1
- package/dist/cjs/excel-parser/plugins/xls/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xls/index.js +518 -6
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +4 -0
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +33 -14
- package/dist/cjs/excel-parser/plugins/xlsx/chartex.d.ts +37 -0
- package/dist/cjs/excel-parser/plugins/xlsx/chartex.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/chartex.js +244 -0
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +10 -0
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.js +68 -9
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +28 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +155 -20
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +70 -0
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +295 -0
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +35 -2
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +737 -257
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +125 -37
- package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts +3 -0
- package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.js +4 -1
- package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts +15 -1
- package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/styles.js +102 -6
- package/dist/cjs/excel-parser/registry.d.ts +7 -0
- package/dist/cjs/excel-parser/registry.d.ts.map +1 -1
- package/dist/cjs/excel-parser/registry.js +29 -0
- package/dist/cjs/excel-parser/types.d.ts +17 -2
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +3 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +24 -14
- package/dist/cjs/excel-parser/worker.js +131 -131
- package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/cjs/exporter/ods/ods-export.js +432 -175
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/chart-builder.js +215 -39
- package/dist/cjs/exporter/xlsx/chartex-builder.d.ts +36 -0
- package/dist/cjs/exporter/xlsx/chartex-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/chartex-builder.js +109 -0
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +61 -0
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/form-controls-builder.js +197 -0
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts +8 -0
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/pivot-builder.js +141 -42
- package/dist/cjs/exporter/xlsx/style-builder.js +9 -3
- package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts +7 -1
- package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/threaded-comments-builder.js +15 -3
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +11 -2
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +991 -297
- package/dist/cjs/helpers/implicit-intersection.d.ts +16 -0
- package/dist/cjs/helpers/implicit-intersection.d.ts.map +1 -0
- package/dist/cjs/helpers/implicit-intersection.js +197 -0
- package/dist/cjs/print-settings.d.ts +105 -0
- package/dist/cjs/print-settings.d.ts.map +1 -0
- package/dist/cjs/print-settings.js +494 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/excel-parser/cfb-decryptor.d.ts +6 -0
- package/dist/esm/excel-parser/cfb-decryptor.d.ts.map +1 -1
- package/dist/esm/excel-parser/cfb-decryptor.js +6 -0
- package/dist/esm/excel-parser/helpers.d.ts +9 -1
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
- package/dist/esm/excel-parser/helpers.js +34 -0
- package/dist/esm/excel-parser/interface.d.ts +15 -2
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/base/index.d.ts +2 -0
- package/dist/esm/excel-parser/plugins/base/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/base/index.js +3 -0
- package/dist/esm/excel-parser/plugins/ods/index.d.ts +19 -0
- package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.js +347 -33
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts +43 -1
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/styles.js +276 -2
- package/dist/esm/excel-parser/plugins/xls/index.d.ts +55 -1
- package/dist/esm/excel-parser/plugins/xls/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xls/index.js +441 -3
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +4 -0
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/index.js +30 -9
- package/dist/esm/excel-parser/plugins/xlsx/chartex.d.ts +37 -0
- package/dist/esm/excel-parser/plugins/xlsx/chartex.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/chartex.js +199 -0
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +10 -0
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.js +69 -6
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +28 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.js +99 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +70 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +246 -0
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +35 -2
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +454 -32
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/pivots.js +86 -6
- package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts +3 -0
- package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/sheet-views.js +2 -0
- package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts +15 -1
- package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/styles.js +92 -2
- package/dist/esm/excel-parser/registry.d.ts +7 -0
- package/dist/esm/excel-parser/registry.d.ts.map +1 -1
- package/dist/esm/excel-parser/registry.js +19 -0
- package/dist/esm/excel-parser/types.d.ts +17 -2
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +3 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +19 -10
- package/dist/esm/excel-parser/worker.js +12 -18
- package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/esm/exporter/ods/ods-export.js +306 -35
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +161 -28
- package/dist/esm/exporter/xlsx/chartex-builder.d.ts +36 -0
- package/dist/esm/exporter/xlsx/chartex-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/chartex-builder.js +97 -0
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +61 -0
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/form-controls-builder.js +173 -0
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts +8 -0
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/pivot-builder.js +104 -19
- package/dist/esm/exporter/xlsx/style-builder.js +9 -3
- package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts +7 -1
- package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/threaded-comments-builder.js +15 -3
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +11 -2
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +675 -64
- package/dist/esm/helpers/implicit-intersection.d.ts +16 -0
- package/dist/esm/helpers/implicit-intersection.d.ts.map +1 -0
- package/dist/esm/helpers/implicit-intersection.js +184 -0
- package/dist/esm/print-settings.d.ts +105 -0
- package/dist/esm/print-settings.d.ts.map +1 -0
- package/dist/esm/print-settings.js +422 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/xlsx.d.ts +14 -0
|
@@ -2,17 +2,21 @@ import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFo
|
|
|
2
2
|
import { zipSync } from "fflate";
|
|
3
3
|
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
4
4
|
import { buildAutoFilterConditionXml } from "../../autofilter-condition";
|
|
5
|
+
import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
|
|
6
|
+
import { buildChartExGraphicFrameXml, buildChartExXml, CHART_EX_CONTENT_TYPE, CHART_EX_REL_TYPE, isChartExType, } from "./chartex-builder";
|
|
5
7
|
import { applyPowerQueryParts } from "./power-query-builder";
|
|
8
|
+
import { encodeImplicitIntersection } from "../../helpers/implicit-intersection";
|
|
6
9
|
import { qualifyFutureFunctions } from "./future-functions";
|
|
7
10
|
import { PERSON_CONTENT_TYPE, PERSON_REL_TYPE, PersonRegistry, THREADED_COMMENT_REL_TYPE, THREADED_COMMENTS_CONTENT_TYPE, ThreadedCommentsCollector, } from "./threaded-comments-builder";
|
|
8
11
|
import { CFBEncryptor } from "../../excel-parser/cfb-encryptor";
|
|
9
12
|
import { columnPixelToChars, imageUrlToBase64 } from "../../helpers";
|
|
13
|
+
import { buildPrintAreaFormula, buildPrintTitlesFormula, isBuiltinPrintName, isHeaderFooterEmpty, PRINT_AREA_NAME, PRINT_TITLES_NAME, } from "../../print-settings";
|
|
10
14
|
import { generateThemeXml } from "../../helpers/theme-xml-generator";
|
|
11
15
|
import theme1Xml from "../../theme1";
|
|
12
16
|
import { tokenize as defaultTokenizer } from "@rowsncolumns/calculator";
|
|
13
17
|
import { CellXfsRegistry, deriveEffectiveNumberFormatFromFormula, } from "@rowsncolumns/spreadsheet-state";
|
|
14
18
|
import { buildChartXml } from "./chart-builder";
|
|
15
|
-
import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, } from "./pivot-builder";
|
|
19
|
+
import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, calculatedFieldReferences, } from "./pivot-builder";
|
|
16
20
|
import { buildSlicerCacheXml, buildSlicerXml } from "./slicer-builder";
|
|
17
21
|
import { buildSparklineGroupsXml } from "./sparkline-builder";
|
|
18
22
|
import { StyleBuilder } from "./style-builder";
|
|
@@ -21,7 +25,7 @@ const isStyleReference = (value) => !!value && typeof value === "object" && "sid
|
|
|
21
25
|
const DEFAULT_COLUMN_WIDTH_PX = 100;
|
|
22
26
|
const DEFAULT_ROW_HEIGHT_PX = 21;
|
|
23
27
|
const pixelToPoints = (px) => Math.max(1, Number(((px * 72) / 96).toFixed(2)));
|
|
24
|
-
export const createExcelFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme, defaultFont, embeds = [], charts = [], slicers = [], pivotTables = [], externalLinks = [], namedRanges = [], namedCellStyles, customTableStyles, iterativeCalculationOptions, ignoreFormulaPatterns, citations, comments, password, locale, use1904 = false, properties, powerQuery, }) => {
|
|
28
|
+
export const createExcelFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme, defaultFont, embeds = [], charts = [], slicers = [], pivotTables = [], externalLinks = [], namedRanges = [], protectedRanges = [], namedCellStyles, customTableStyles, iterativeCalculationOptions, ignoreFormulaPatterns, citations, comments, password, locale, use1904 = false, properties, powerQuery, }) => {
|
|
25
29
|
const sortedSheets = sortSheetsByIndex(sheets, true);
|
|
26
30
|
const sharedStrings = new SharedStrings();
|
|
27
31
|
// Workbook-scoped author registry shared across every sheet's threaded
|
|
@@ -51,6 +55,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
51
55
|
const embedsBySheet = groupEmbedsBySheet(embeds ?? []);
|
|
52
56
|
const textBoxesBySheet = groupTextBoxesBySheet(embeds ?? []);
|
|
53
57
|
const shapesBySheet = groupShapesBySheet(embeds ?? []);
|
|
58
|
+
const formControlsBySheet = groupFormControlsBySheet(embeds ?? []);
|
|
59
|
+
const ctrlProps = { parts: [] };
|
|
54
60
|
const chartsBySheet = groupChartsBySheet(charts ?? []);
|
|
55
61
|
// Remap slicer tableIds to match prepared table IDs
|
|
56
62
|
const remappedSlicers = slicers.map((slicer) => {
|
|
@@ -116,6 +122,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
116
122
|
const embedImages = embedsBySheet.get(sheet.sheetId) ?? [];
|
|
117
123
|
const textBoxes = textBoxesBySheet.get(sheet.sheetId) ?? [];
|
|
118
124
|
const sheetShapes = shapesBySheet.get(sheet.sheetId) ?? [];
|
|
125
|
+
const sheetFormControls = formControlsBySheet.get(sheet.sheetId) ?? [];
|
|
119
126
|
const sheetCharts = chartsBySheet.get(sheet.sheetId) ?? [];
|
|
120
127
|
const sheetSlicers = slicersBySheet.get(sheet.sheetId) ?? [];
|
|
121
128
|
const worksheetSlicerRelIds = [];
|
|
@@ -183,12 +190,26 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
183
190
|
fieldIndex.set(name, fieldNameList.length);
|
|
184
191
|
fieldNameList.push(name);
|
|
185
192
|
};
|
|
193
|
+
const calculatedNames = new Set((pivot.calculatedFields ?? []).map((c) => c.name));
|
|
186
194
|
for (const r of pivot.rows ?? [])
|
|
187
195
|
registerField(r.field);
|
|
188
196
|
for (const c of pivot.columns ?? [])
|
|
189
197
|
registerField(c.field);
|
|
190
|
-
for (const v of pivot.values ?? [])
|
|
191
|
-
|
|
198
|
+
for (const v of pivot.values ?? []) {
|
|
199
|
+
if (!calculatedNames.has(v.field))
|
|
200
|
+
registerField(v.field);
|
|
201
|
+
}
|
|
202
|
+
// Source fields a calculated field reads must exist in the cache
|
|
203
|
+
// for Excel to evaluate the formula; the calculated fields
|
|
204
|
+
// themselves come last, as Excel orders them.
|
|
205
|
+
for (const c of pivot.calculatedFields ?? []) {
|
|
206
|
+
for (const ref of calculatedFieldReferences(c.formula)) {
|
|
207
|
+
if (!calculatedNames.has(ref))
|
|
208
|
+
registerField(ref);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const c of pivot.calculatedFields ?? [])
|
|
212
|
+
registerField(c.name);
|
|
192
213
|
const cacheXml = buildPivotCacheDefinitionXml(pivot, sourceSheetName, fieldNameList);
|
|
193
214
|
const tableXml = buildPivotTableXml(pivot, cacheIndex, fieldIndex);
|
|
194
215
|
if (!cacheXml || !tableXml)
|
|
@@ -233,6 +254,9 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
233
254
|
sheetData,
|
|
234
255
|
sheets: sortedSheets,
|
|
235
256
|
locale,
|
|
257
|
+
protectedRanges,
|
|
258
|
+
formControls: sheetFormControls,
|
|
259
|
+
ctrlProps,
|
|
236
260
|
});
|
|
237
261
|
return {
|
|
238
262
|
index,
|
|
@@ -305,6 +329,10 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
305
329
|
const drawingSheets = [];
|
|
306
330
|
const imageExtensions = new Set();
|
|
307
331
|
const chartEntries = [];
|
|
332
|
+
// Excel 2016+ chartex parts (treemap / sunburst) — numbered apart from
|
|
333
|
+
// `chart{N}.xml`, as Excel does.
|
|
334
|
+
const chartExEntries = [];
|
|
335
|
+
let globalChartExIndex = 1;
|
|
308
336
|
for (const entry of worksheetEntries) {
|
|
309
337
|
const placements = [...entry.cellImages, ...entry.embedImages];
|
|
310
338
|
const textBoxesForSheet = entry.textBoxes ?? [];
|
|
@@ -324,6 +352,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
324
352
|
shapes: shapesForSheet,
|
|
325
353
|
startImageIndex: globalImageIndex,
|
|
326
354
|
startChartIndex: globalChartIndex,
|
|
355
|
+
startChartExIndex: globalChartExIndex,
|
|
327
356
|
charts: chartsForSheet,
|
|
328
357
|
slicers: slicersForSheet,
|
|
329
358
|
sheetNameById,
|
|
@@ -341,13 +370,20 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
341
370
|
for (const chartEntry of drawingParts.charts) {
|
|
342
371
|
chartEntries.push(chartEntry);
|
|
343
372
|
}
|
|
373
|
+
for (const chartExEntry of drawingParts.chartExs) {
|
|
374
|
+
chartExEntries.push(chartExEntry);
|
|
375
|
+
}
|
|
344
376
|
globalImageIndex = drawingParts.nextImageIndex;
|
|
345
377
|
globalChartIndex = drawingParts.nextChartIndex;
|
|
378
|
+
globalChartExIndex = drawingParts.nextChartExIndex;
|
|
346
379
|
drawingSheets.push(entry.index + 1);
|
|
347
380
|
}
|
|
348
381
|
for (const chartEntry of chartEntries) {
|
|
349
382
|
files[chartEntry.path] = encodeXml(chartEntry.xml);
|
|
350
383
|
}
|
|
384
|
+
for (const chartExEntry of chartExEntries) {
|
|
385
|
+
files[chartExEntry.path] = encodeXml(chartExEntry.xml);
|
|
386
|
+
}
|
|
351
387
|
for (const slicerCacheEntry of slicerCacheEntries) {
|
|
352
388
|
files[slicerCacheEntry.path] = encodeXml(slicerCacheEntry.xml);
|
|
353
389
|
}
|
|
@@ -381,6 +417,10 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
381
417
|
const commentsSheets = worksheetEntries
|
|
382
418
|
.filter((entry) => entry.commentsXml)
|
|
383
419
|
.map((entry) => entry.sheet.sheetId ?? entry.index + 1);
|
|
420
|
+
const hasVmlDrawings = worksheetEntries.some((entry) => entry.vmlDrawingXml);
|
|
421
|
+
for (const part of ctrlProps.parts) {
|
|
422
|
+
files[part.path] = encodeXml(part.xml);
|
|
423
|
+
}
|
|
384
424
|
const threadedCommentsSheets = worksheetEntries
|
|
385
425
|
.filter((entry) => entry.threadedCommentsXml)
|
|
386
426
|
.map((entry) => entry.sheet.sheetId ?? entry.index + 1);
|
|
@@ -390,6 +430,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
390
430
|
drawingSheets,
|
|
391
431
|
mediaExtensions: Array.from(imageExtensions),
|
|
392
432
|
chartCount: chartEntries.length,
|
|
433
|
+
chartExCount: chartExEntries.length,
|
|
393
434
|
slicerCount: slicerEntries.length,
|
|
394
435
|
slicerCacheCount: slicerCacheEntries.length,
|
|
395
436
|
pivotTableCount: pivotTableEntries.length,
|
|
@@ -398,6 +439,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
398
439
|
threadedCommentsSheets,
|
|
399
440
|
hasPersons: !personRegistry.isEmpty(),
|
|
400
441
|
externalLinkCount: externalLinks.length,
|
|
442
|
+
hasVmlDrawings,
|
|
443
|
+
ctrlPropCount: ctrlProps.parts.length,
|
|
401
444
|
});
|
|
402
445
|
files["[Content_Types].xml"] = encodeXml(contentTypesXml);
|
|
403
446
|
// Preserved Power Query machinery (DataMashup customXml, connections.xml, queryTable parts) is
|
|
@@ -546,6 +589,8 @@ class DataValidationCollector {
|
|
|
546
589
|
return `<dataValidations count="${this.entries.size}">${items.join("")}</dataValidations>`;
|
|
547
590
|
}
|
|
548
591
|
}
|
|
592
|
+
/** First VML shape id the note shapes take (`_x0000_s1026`, `_x0000_s1027`, …). */
|
|
593
|
+
const NOTE_VML_SHAPE_ID_BASE = 1026;
|
|
549
594
|
class CommentsCollector {
|
|
550
595
|
entries = [];
|
|
551
596
|
add(address, text, author = "System") {
|
|
@@ -570,13 +615,18 @@ class CommentsCollector {
|
|
|
570
615
|
.join("");
|
|
571
616
|
return `${XML_DECLARATION}<comments xmlns="${WORKSHEET_NS}"><authors>${authorsXml}</authors><commentList>${commentsXml}</commentList></comments>`;
|
|
572
617
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
618
|
+
/**
|
|
619
|
+
* VML shape ids the note shapes occupy. Excel allocates notes and form
|
|
620
|
+
* controls from one per-sheet counter, so the form-control allocator must
|
|
621
|
+
* never hand these out again.
|
|
622
|
+
*/
|
|
623
|
+
vmlShapeIds() {
|
|
624
|
+
return this.entries.map((_, index) => NOTE_VML_SHAPE_ID_BASE + index);
|
|
625
|
+
}
|
|
626
|
+
/** One `<v:shape>` per note (Excel's hidden `Note` object). */
|
|
627
|
+
toVmlShapes() {
|
|
628
|
+
return this.entries.map((entry, index) => {
|
|
629
|
+
const shapeId = `_x0000_s${NOTE_VML_SHAPE_ID_BASE + index}`;
|
|
580
630
|
// Parse cell address to get row and column
|
|
581
631
|
const match = entry.address.match(/^([A-Z]+)(\d+)$/);
|
|
582
632
|
if (!match)
|
|
@@ -587,11 +637,11 @@ class CommentsCollector {
|
|
|
587
637
|
.split("")
|
|
588
638
|
.reduce((acc, char) => acc * 26 + (char.charCodeAt(0) - 64), 0) - 1;
|
|
589
639
|
return `<v:shape id="${shapeId}" type="#_x0000_t202" style='position:absolute;margin-left:59.25pt;margin-top:1.5pt;width:108pt;height:59.25pt;z-index:${index + 1};visibility:hidden' fillcolor="#ffffe1" o:insetmode="auto"><v:fill color2="#ffffe1"/><v:shadow on="t" color="black" obscured="t"/><v:path o:connecttype="none"/><v:textbox style='mso-direction-alt:auto'><div style='text-align:left'></div></v:textbox><x:ClientData ObjectType="Note"><x:MoveWithCells/><x:SizeWithCells/><x:Anchor>1, 15, 0, 2, 3, 15, 3, 16</x:Anchor><x:AutoFill>False</x:AutoFill><x:Row>${row - 1}</x:Row><x:Column>${colIndex}</x:Column></x:ClientData></v:shape>`;
|
|
590
|
-
})
|
|
591
|
-
.join("");
|
|
592
|
-
return `<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout><v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>${shapes}</xml>`;
|
|
640
|
+
}).filter(Boolean);
|
|
593
641
|
}
|
|
594
642
|
}
|
|
643
|
+
/** VML `<v:shapetype>` for legacy notes (`#_x0000_t202`). */
|
|
644
|
+
const NOTE_VML_SHAPETYPE = `<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>`;
|
|
595
645
|
const INVALID_XML_CHAR_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g;
|
|
596
646
|
const sanitizeXmlText = (value) => {
|
|
597
647
|
if (!value)
|
|
@@ -643,7 +693,7 @@ const getRowsForSheet = (sheetData, sheetId) => {
|
|
|
643
693
|
sheetData[Number(sheetId)] ??
|
|
644
694
|
[]);
|
|
645
695
|
};
|
|
646
|
-
const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, tables, relationships, drawingContext, conditionalFormats, dataValidationsById, dataValidations, ignoreFormulaPatterns, citations, comments, personRegistry, slicerRelIds, sheetData, sheets, locale, }) => {
|
|
696
|
+
const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, tables, relationships, drawingContext, conditionalFormats, dataValidationsById, dataValidations, ignoreFormulaPatterns, citations, comments, personRegistry, slicerRelIds, sheetData, sheets, locale, protectedRanges, formControls = [], ctrlProps = { parts: [] }, }) => {
|
|
647
697
|
const tableNameMap = buildTableNameMap(tables);
|
|
648
698
|
const bounds = {
|
|
649
699
|
minRow: Number.POSITIVE_INFINITY,
|
|
@@ -806,14 +856,36 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
806
856
|
}
|
|
807
857
|
// Generate comments XML (notes) and add relationships if there are comments
|
|
808
858
|
const commentsXml = commentsCollector.toXml();
|
|
809
|
-
let legacyDrawingXml = "";
|
|
810
859
|
if (commentsXml) {
|
|
811
860
|
relationships.add(COMMENTS_REL_TYPE, `../comments${sheet.sheetId}.xml`);
|
|
861
|
+
}
|
|
862
|
+
// Notes and form controls share the sheet's one legacy VML part
|
|
863
|
+
// (`<legacyDrawing>`); a control without its VML shape is dropped by Excel.
|
|
864
|
+
const noteVmlShapes = commentsCollector.toVmlShapes();
|
|
865
|
+
const formControlBuild = buildSheetFormControls({
|
|
866
|
+
sheet,
|
|
867
|
+
controls: formControls,
|
|
868
|
+
relationships,
|
|
869
|
+
ctrlProps,
|
|
870
|
+
zIndexStart: noteVmlShapes.length,
|
|
871
|
+
reservedShapeIds: commentsCollector.vmlShapeIds(),
|
|
872
|
+
});
|
|
873
|
+
const vmlDrawingXml = buildVmlDrawingXml({
|
|
874
|
+
shapeTypes: [
|
|
875
|
+
...(noteVmlShapes.length ? [NOTE_VML_SHAPETYPE] : []),
|
|
876
|
+
...(formControlBuild.vmlShapes.length ? [FORM_CONTROL_VML_SHAPETYPE] : []),
|
|
877
|
+
],
|
|
878
|
+
shapes: [...noteVmlShapes, ...formControlBuild.vmlShapes],
|
|
879
|
+
});
|
|
880
|
+
let legacyDrawingXml = "";
|
|
881
|
+
if (vmlDrawingXml) {
|
|
812
882
|
const legacyDrawingRelId = relationships.add("http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", `../drawings/vmlDrawing${sheet.sheetId}.vml`);
|
|
813
883
|
legacyDrawingXml = `<legacyDrawing r:id="${legacyDrawingRelId}"/>`;
|
|
814
884
|
}
|
|
885
|
+
const controlsXml = formControlBuild.controlsXml;
|
|
815
886
|
const sheetDataXml = `<sheetData>${rowXmlChunks.join("")}</sheetData>`;
|
|
816
|
-
const
|
|
887
|
+
const protectionXml = buildSheetProtectionXml(sheet, protectedRanges);
|
|
888
|
+
const printSettingsXml = buildPrintSettingsXml(sheet.printSettings);
|
|
817
889
|
// Build row data map for sparkline detection
|
|
818
890
|
const rowDataMap = {};
|
|
819
891
|
for (let rowIndex = 1; rowIndex < maxRows; rowIndex++) {
|
|
@@ -847,13 +919,13 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
847
919
|
explicitDefaultColumnWidthPx > 0) {
|
|
848
920
|
sheetFormatPrAttrs.push(`defaultColWidth="${columnPixelToChars(explicitDefaultColumnWidthPx).toFixed(2)}"`);
|
|
849
921
|
}
|
|
850
|
-
const worksheetXml = `${XML_DECLARATION}<worksheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<dimension ref="${dimension}"/>${sheetViews}<sheetFormatPr ${sheetFormatPrAttrs.join(" ")}/>${columnXml}${sheetDataXml}${basicFilterXml}${mergeXml}${conditionalFormattingXml}${dataValidationsXml}${hyperlinksXml}${
|
|
922
|
+
const worksheetXml = `${XML_DECLARATION}<worksheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<dimension ref="${dimension}"/>${sheetViews}<sheetFormatPr ${sheetFormatPrAttrs.join(" ")}/>${columnXml}${sheetDataXml}${protectionXml}${basicFilterXml}${mergeXml}${conditionalFormattingXml}${dataValidationsXml}${hyperlinksXml}${printSettingsXml}${drawingXml}${legacyDrawingXml}${controlsXml}${tablePartsXml}${extLstXml}</worksheet>`;
|
|
851
923
|
return {
|
|
852
924
|
xml: worksheetXml,
|
|
853
925
|
relationshipsXml: relationships.toXml(),
|
|
854
926
|
images: collectedImages,
|
|
855
927
|
commentsXml,
|
|
856
|
-
vmlDrawingXml
|
|
928
|
+
vmlDrawingXml,
|
|
857
929
|
threadedCommentsXml,
|
|
858
930
|
};
|
|
859
931
|
};
|
|
@@ -1814,7 +1886,9 @@ function rewriteStructuredReferenceFormula(formula, tableNameMap, tables, rowNum
|
|
|
1814
1886
|
}
|
|
1815
1887
|
function sanitizeFormulaText(formula) {
|
|
1816
1888
|
const normalized = sanitizeXmlText(formula);
|
|
1817
|
-
|
|
1889
|
+
// `@x` is persisted as `_xlfn.SINGLE(x)`; encoding first lets the
|
|
1890
|
+
// future-function pass add the prefix.
|
|
1891
|
+
return qualifyFutureFunctions(encodeImplicitIntersection(normalized.replace(/\r\n|\r|\n/g, " ")));
|
|
1818
1892
|
}
|
|
1819
1893
|
function escapeXmlBase(value) {
|
|
1820
1894
|
return sanitizeXmlText(value)
|
|
@@ -1976,6 +2050,11 @@ const buildSheetViewsXml = (sheet) => {
|
|
|
1976
2050
|
zoomScale !== 100) {
|
|
1977
2051
|
attrs.push(`zoomScale="${zoomScale}"`);
|
|
1978
2052
|
}
|
|
2053
|
+
// `rightToLeft` — only an explicit true is written (Excel's default is
|
|
2054
|
+
// LTR and the attribute is omitted), mirroring the Rust exporter.
|
|
2055
|
+
if (sheet.rightToLeft) {
|
|
2056
|
+
attrs.push(`rightToLeft="1"`);
|
|
2057
|
+
}
|
|
1979
2058
|
const panes = [];
|
|
1980
2059
|
const frozenRows = sheet.frozenRowCount ?? 0;
|
|
1981
2060
|
const frozenColumns = sheet.frozenColumnCount ?? 0;
|
|
@@ -2230,6 +2309,57 @@ const sanitizeTableName = (name) => {
|
|
|
2230
2309
|
}
|
|
2231
2310
|
return cleaned;
|
|
2232
2311
|
};
|
|
2312
|
+
/**
|
|
2313
|
+
* `SUBTOTAL()` function numbers (the 101+ "ignore hidden rows" family Excel
|
|
2314
|
+
* writes into totals-row cells) → `totalsRowFunction`.
|
|
2315
|
+
*/
|
|
2316
|
+
const SUBTOTAL_TOTALS_ROW_FUNCTIONS = {
|
|
2317
|
+
"101": "average",
|
|
2318
|
+
"102": "countNums",
|
|
2319
|
+
"103": "count",
|
|
2320
|
+
"104": "max",
|
|
2321
|
+
"105": "min",
|
|
2322
|
+
"107": "stdDev",
|
|
2323
|
+
"109": "sum",
|
|
2324
|
+
"110": "var",
|
|
2325
|
+
};
|
|
2326
|
+
/**
|
|
2327
|
+
* Totals-row metadata for one `<tableColumn>`. Explicit column metadata
|
|
2328
|
+
* (imported from Excel, or set by the host) wins — an explicit `none` means
|
|
2329
|
+
* "no totals function" and is not second-guessed from the cell (the Rust
|
|
2330
|
+
* exporter's `totals_for_column` agrees). Otherwise it is derived from the
|
|
2331
|
+
* totals-row cell so tables whose totals row was added in-app (a "Total"
|
|
2332
|
+
* label + `=SUBTOTAL(109,[Col])` per numeric column) open in Excel with the
|
|
2333
|
+
* matching function selected in the totals dropdown. A formula that is not
|
|
2334
|
+
* a plain `SUBTOTAL(n, ref)` is left to the cell.
|
|
2335
|
+
*/
|
|
2336
|
+
const resolveTableColumnTotals = (column, totalsCell) => {
|
|
2337
|
+
if (column?.totalsRowLabel) {
|
|
2338
|
+
return { totalsRowLabel: column.totalsRowLabel };
|
|
2339
|
+
}
|
|
2340
|
+
if (column?.totalsRowFunction) {
|
|
2341
|
+
if (column.totalsRowFunction === "none") {
|
|
2342
|
+
return undefined;
|
|
2343
|
+
}
|
|
2344
|
+
if (column.totalsRowFunction === "custom" && !column.totalsRowFormula) {
|
|
2345
|
+
return undefined;
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
totalsRowFunction: column.totalsRowFunction,
|
|
2349
|
+
totalsRowFormula: column.totalsRowFormula ?? undefined,
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
if (!totalsCell)
|
|
2353
|
+
return undefined;
|
|
2354
|
+
const formula = getExtendedValueFormula(getCellUserEnteredValue(totalsCell));
|
|
2355
|
+
if (formula) {
|
|
2356
|
+
const match = /^=?\s*SUBTOTAL\(\s*(\d{3})\s*,[^,]*\)\s*$/i.exec(formula);
|
|
2357
|
+
const fn = match ? SUBTOTAL_TOTALS_ROW_FUNCTIONS[match[1]] : undefined;
|
|
2358
|
+
return fn ? { totalsRowFunction: fn } : undefined;
|
|
2359
|
+
}
|
|
2360
|
+
const label = getExtendedValueString(getCellUserEnteredValue(totalsCell));
|
|
2361
|
+
return label && label.trim() !== "" ? { totalsRowLabel: label } : undefined;
|
|
2362
|
+
};
|
|
2233
2363
|
const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
2234
2364
|
const rangeRef = table.ref;
|
|
2235
2365
|
const columnCount = Math.max(table.columns?.length ?? 0, (table.range.endColumnIndex ?? table.range.startColumnIndex) -
|
|
@@ -2275,6 +2405,7 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
|
2275
2405
|
// Fallback to table.columns name or generic name
|
|
2276
2406
|
return table.columns?.[index]?.name ?? `Column${index + 1}`;
|
|
2277
2407
|
});
|
|
2408
|
+
const totalsRowIndex = table.range.endRowIndex;
|
|
2278
2409
|
const tableColumns = `<tableColumns count="${columnCount}">${columnNames
|
|
2279
2410
|
.map((name, idx) => {
|
|
2280
2411
|
const columnFormula = table.columns?.[idx]?.formula;
|
|
@@ -2284,7 +2415,22 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
|
2284
2415
|
const formulaXml = normalizedFormula
|
|
2285
2416
|
? `<calculatedColumnFormula>${escapeXmlText(stripFormula(normalizedFormula))}</calculatedColumnFormula>`
|
|
2286
2417
|
: "";
|
|
2287
|
-
|
|
2418
|
+
// Explicit per-column totals metadata exports whether or not the totals
|
|
2419
|
+
// row is shown: Excel keeps it on a table whose totals row is toggled
|
|
2420
|
+
// off so re-enabling restores each column's choice. Deriving it from
|
|
2421
|
+
// the last row only applies when that row IS the totals row.
|
|
2422
|
+
const totals = resolveTableColumnTotals(table.columns?.[idx], totalsRowCount > 0
|
|
2423
|
+
? rows[totalsRowIndex]?.values?.[table.range.startColumnIndex + idx]
|
|
2424
|
+
: undefined);
|
|
2425
|
+
const totalsAttrs = totals?.totalsRowLabel
|
|
2426
|
+
? ` totalsRowLabel="${escapeXmlAttr(totals.totalsRowLabel)}"`
|
|
2427
|
+
: totals?.totalsRowFunction
|
|
2428
|
+
? ` totalsRowFunction="${totals.totalsRowFunction}"`
|
|
2429
|
+
: "";
|
|
2430
|
+
const totalsFormulaXml = totals?.totalsRowFunction === "custom" && totals.totalsRowFormula
|
|
2431
|
+
? `<totalsRowFormula>${escapeXmlText(stripFormula(normalizeCalculatedColumnFormula(totals.totalsRowFormula, table)))}</totalsRowFormula>`
|
|
2432
|
+
: "";
|
|
2433
|
+
return `<tableColumn id="${idx + 1}" name="${escapeXmlAttr(name)}"${totalsAttrs}>${formulaXml}${totalsFormulaXml}</tableColumn>`;
|
|
2288
2434
|
})
|
|
2289
2435
|
.join("")}</tableColumns>`;
|
|
2290
2436
|
const isBuiltInStyle = table.theme && /^TableStyle(Light|Medium|Dark)\d+$/.test(table.theme);
|
|
@@ -2332,6 +2478,106 @@ const buildWorksheetAutoFilterXml = (sheet, rows, styleBuilder) => {
|
|
|
2332
2478
|
}
|
|
2333
2479
|
return `<autoFilter ref="${ref}"/>`;
|
|
2334
2480
|
};
|
|
2481
|
+
/** `CT_SheetProtection` boolean attributes in Excel's attribute order. */
|
|
2482
|
+
const SHEET_PROTECTION_FLAG_ATTRS = [
|
|
2483
|
+
"sheet",
|
|
2484
|
+
"objects",
|
|
2485
|
+
"scenarios",
|
|
2486
|
+
"formatCells",
|
|
2487
|
+
"formatColumns",
|
|
2488
|
+
"formatRows",
|
|
2489
|
+
"insertColumns",
|
|
2490
|
+
"insertRows",
|
|
2491
|
+
"insertHyperlinks",
|
|
2492
|
+
"deleteColumns",
|
|
2493
|
+
"deleteRows",
|
|
2494
|
+
"selectLockedCells",
|
|
2495
|
+
"sort",
|
|
2496
|
+
"autoFilter",
|
|
2497
|
+
"pivotTables",
|
|
2498
|
+
"selectUnlockedCells",
|
|
2499
|
+
];
|
|
2500
|
+
/**
|
|
2501
|
+
* `<sheetProtection …/>` from `Sheet.sheetProtection`: the password hash first
|
|
2502
|
+
* (legacy `password` or the agile quartet, verbatim), then only the action
|
|
2503
|
+
* flags the model carries (`true` → `"1"`, `false` → `"0"`, absent → omitted so
|
|
2504
|
+
* Excel's schema default applies). Mirrors `rnc-export`'s `sheet_protection_xml`.
|
|
2505
|
+
*/
|
|
2506
|
+
const buildSheetProtectionElementXml = (protection) => {
|
|
2507
|
+
const attrs = [];
|
|
2508
|
+
if (protection.algorithmName) {
|
|
2509
|
+
attrs.push(`algorithmName="${escapeXmlAttr(protection.algorithmName)}"`);
|
|
2510
|
+
}
|
|
2511
|
+
if (protection.hashValue) {
|
|
2512
|
+
attrs.push(`hashValue="${escapeXmlAttr(protection.hashValue)}"`);
|
|
2513
|
+
}
|
|
2514
|
+
if (protection.saltValue) {
|
|
2515
|
+
attrs.push(`saltValue="${escapeXmlAttr(protection.saltValue)}"`);
|
|
2516
|
+
}
|
|
2517
|
+
if (protection.spinCount != null) {
|
|
2518
|
+
attrs.push(`spinCount="${protection.spinCount}"`);
|
|
2519
|
+
}
|
|
2520
|
+
if (protection.password) {
|
|
2521
|
+
attrs.push(`password="${escapeXmlAttr(protection.password)}"`);
|
|
2522
|
+
}
|
|
2523
|
+
for (const flag of SHEET_PROTECTION_FLAG_ATTRS) {
|
|
2524
|
+
const value = protection[flag];
|
|
2525
|
+
if (typeof value === "boolean") {
|
|
2526
|
+
attrs.push(`${flag}="${value ? 1 : 0}"`);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return attrs.length ? `<sheetProtection ${attrs.join(" ")}/>` : "";
|
|
2530
|
+
};
|
|
2531
|
+
/**
|
|
2532
|
+
* The sheet's protection block — `<sheetProtection>` followed by
|
|
2533
|
+
* `<protectedRanges>` — or `""` when the sheet is unprotected and has no
|
|
2534
|
+
* allow-edit ranges. Ranges missing a bound (a sheet-wide `protect-sheet`
|
|
2535
|
+
* entry only carries `sheetId`) have no `sqref` and are skipped; the `name`
|
|
2536
|
+
* is the range's id, synthesized as `Range{n}` when absent (schema requires it).
|
|
2537
|
+
*/
|
|
2538
|
+
const buildSheetProtectionXml = (sheet, protectedRanges) => {
|
|
2539
|
+
const protectionXml = sheet.sheetProtection
|
|
2540
|
+
? buildSheetProtectionElementXml(sheet.sheetProtection)
|
|
2541
|
+
: "";
|
|
2542
|
+
const rangeItems = [];
|
|
2543
|
+
for (const protectedRange of protectedRanges ?? []) {
|
|
2544
|
+
const range = protectedRange.range;
|
|
2545
|
+
if (range.sheetId !== sheet.sheetId)
|
|
2546
|
+
continue;
|
|
2547
|
+
if (range.startRowIndex == null ||
|
|
2548
|
+
range.endRowIndex == null ||
|
|
2549
|
+
range.startColumnIndex == null ||
|
|
2550
|
+
range.endColumnIndex == null) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
// Excel writes a single cell as `D10`, a block as `A2:C5`.
|
|
2554
|
+
const isSingleCell = range.startRowIndex === range.endRowIndex &&
|
|
2555
|
+
range.startColumnIndex === range.endColumnIndex;
|
|
2556
|
+
const sqref = isSingleCell
|
|
2557
|
+
? cellToAddress({
|
|
2558
|
+
rowIndex: range.startRowIndex,
|
|
2559
|
+
columnIndex: range.startColumnIndex,
|
|
2560
|
+
})
|
|
2561
|
+
: cellRangeToAddress(undefined, {
|
|
2562
|
+
startRowIndex: range.startRowIndex,
|
|
2563
|
+
endRowIndex: range.endRowIndex,
|
|
2564
|
+
startColumnIndex: range.startColumnIndex,
|
|
2565
|
+
endColumnIndex: range.endColumnIndex,
|
|
2566
|
+
});
|
|
2567
|
+
if (!sqref)
|
|
2568
|
+
continue;
|
|
2569
|
+
const id = protectedRange.protectedRangeId;
|
|
2570
|
+
const name = id != null && String(id) !== "" ? String(id) : `Range${rangeItems.length + 1}`;
|
|
2571
|
+
const securityDescriptor = protectedRange.securityDescriptor
|
|
2572
|
+
? ` securityDescriptor="${escapeXmlAttr(protectedRange.securityDescriptor)}"`
|
|
2573
|
+
: "";
|
|
2574
|
+
rangeItems.push(`<protectedRange sqref="${escapeXmlAttr(sqref)}" name="${escapeXmlAttr(name)}"${securityDescriptor}/>`);
|
|
2575
|
+
}
|
|
2576
|
+
const rangesXml = rangeItems.length
|
|
2577
|
+
? `<protectedRanges>${rangeItems.join("")}</protectedRanges>`
|
|
2578
|
+
: "";
|
|
2579
|
+
return `${protectionXml}${rangesXml}`;
|
|
2580
|
+
};
|
|
2335
2581
|
const buildSheetPropertiesXml = (sheet) => {
|
|
2336
2582
|
const tabColor = serializeColorXml(sheet.tabColor ?? undefined);
|
|
2337
2583
|
// <outlinePr> mirrors the sheet's `outlinePr` setting; both attrs
|
|
@@ -2348,11 +2594,105 @@ const buildSheetPropertiesXml = (sheet) => {
|
|
|
2348
2594
|
const outlineXml = outlineAttrs.length
|
|
2349
2595
|
? `<outlinePr ${outlineAttrs.join(" ")}/>`
|
|
2350
2596
|
: "";
|
|
2351
|
-
|
|
2597
|
+
// `<pageSetUpPr fitToPage="1">` is what makes Excel honour
|
|
2598
|
+
// `<pageSetup fitToWidth/fitToHeight>` instead of `scale`.
|
|
2599
|
+
const pageSetUpPrXml = sheet.printSettings?.pageSetup?.fitToPage
|
|
2600
|
+
? `<pageSetUpPr fitToPage="1"/>`
|
|
2601
|
+
: "";
|
|
2602
|
+
if (!tabColor && !outlineXml && !pageSetUpPrXml)
|
|
2352
2603
|
return "";
|
|
2353
|
-
const inner = `${tabColor ? `<tabColor ${tabColor}/>` : ""}${outlineXml}`;
|
|
2604
|
+
const inner = `${tabColor ? `<tabColor ${tabColor}/>` : ""}${outlineXml}${pageSetUpPrXml}`;
|
|
2354
2605
|
return `<sheetPr>${inner}</sheetPr>`;
|
|
2355
2606
|
};
|
|
2607
|
+
const xmlBoolAttr = (name, value) => value == null ? "" : ` ${name}="${value ? "1" : "0"}"`;
|
|
2608
|
+
const xmlNumAttr = (name, value) => typeof value === "number" && Number.isFinite(value)
|
|
2609
|
+
? ` ${name}="${value}"`
|
|
2610
|
+
: "";
|
|
2611
|
+
/**
|
|
2612
|
+
* The `CT_Worksheet` print tail — `<printOptions>`, `<pageMargins>`,
|
|
2613
|
+
* `<pageSetup>`, `<headerFooter>`, `<rowBreaks>`, `<colBreaks>` — in schema
|
|
2614
|
+
* order (after `<hyperlinks>`, before `<drawing>`). `<pageMargins>` is
|
|
2615
|
+
* mandatory in practice (Excel always writes it), so the default margins go
|
|
2616
|
+
* out when the sheet has none of its own.
|
|
2617
|
+
*/
|
|
2618
|
+
const buildPrintSettingsXml = (print) => {
|
|
2619
|
+
const parts = [];
|
|
2620
|
+
const options = print?.printOptions;
|
|
2621
|
+
const optionAttrs = xmlBoolAttr("horizontalCentered", options?.horizontalCentered) +
|
|
2622
|
+
xmlBoolAttr("verticalCentered", options?.verticalCentered) +
|
|
2623
|
+
xmlBoolAttr("headings", options?.headings) +
|
|
2624
|
+
xmlBoolAttr("gridLines", options?.gridLines);
|
|
2625
|
+
if (optionAttrs)
|
|
2626
|
+
parts.push(`<printOptions${optionAttrs}/>`);
|
|
2627
|
+
const margins = print?.pageMargins;
|
|
2628
|
+
parts.push(margins &&
|
|
2629
|
+
Object.values(margins).some((v) => typeof v === "number")
|
|
2630
|
+
? `<pageMargins left="${margins.left ?? 0.7}" right="${margins.right ?? 0.7}" top="${margins.top ?? 0.75}" bottom="${margins.bottom ?? 0.75}" header="${margins.header ?? 0.3}" footer="${margins.footer ?? 0.3}"/>`
|
|
2631
|
+
: DEFAULT_PAGE_MARGINS);
|
|
2632
|
+
const setup = print?.pageSetup;
|
|
2633
|
+
if (setup) {
|
|
2634
|
+
// Excel keeps `firstPageNumber` as a dormant value until the user picks
|
|
2635
|
+
// a first page number (`useFirstPageNumber="1"`); a bare number means
|
|
2636
|
+
// "Auto", so the flag is written only when the model has it on.
|
|
2637
|
+
const useFirst = setup.useFirstPageNumber === true ? true : undefined;
|
|
2638
|
+
const setupAttrs = xmlNumAttr("paperSize", setup.paperSize) +
|
|
2639
|
+
xmlNumAttr("scale", setup.scale) +
|
|
2640
|
+
xmlNumAttr("firstPageNumber", setup.firstPageNumber) +
|
|
2641
|
+
xmlNumAttr("fitToWidth", setup.fitToWidth) +
|
|
2642
|
+
xmlNumAttr("fitToHeight", setup.fitToHeight) +
|
|
2643
|
+
(setup.pageOrder ? ` pageOrder="${setup.pageOrder}"` : "") +
|
|
2644
|
+
(setup.orientation ? ` orientation="${setup.orientation}"` : "") +
|
|
2645
|
+
xmlBoolAttr("blackAndWhite", setup.blackAndWhite) +
|
|
2646
|
+
xmlBoolAttr("draft", setup.draft) +
|
|
2647
|
+
xmlBoolAttr("useFirstPageNumber", useFirst);
|
|
2648
|
+
if (setupAttrs)
|
|
2649
|
+
parts.push(`<pageSetup${setupAttrs}/>`);
|
|
2650
|
+
}
|
|
2651
|
+
const hf = print?.headerFooter;
|
|
2652
|
+
if (hf && !isHeaderFooterEmpty(hf)) {
|
|
2653
|
+
const hfAttrs = xmlBoolAttr("differentOddEven", hf.differentOddEven) +
|
|
2654
|
+
xmlBoolAttr("differentFirst", hf.differentFirst) +
|
|
2655
|
+
xmlBoolAttr("scaleWithDoc", hf.scaleWithDoc) +
|
|
2656
|
+
xmlBoolAttr("alignWithMargins", hf.alignWithMargins);
|
|
2657
|
+
const children = [
|
|
2658
|
+
"oddHeader",
|
|
2659
|
+
"oddFooter",
|
|
2660
|
+
"evenHeader",
|
|
2661
|
+
"evenFooter",
|
|
2662
|
+
"firstHeader",
|
|
2663
|
+
"firstFooter",
|
|
2664
|
+
]
|
|
2665
|
+
.map((key) => {
|
|
2666
|
+
const text = hf[key];
|
|
2667
|
+
return typeof text === "string" && text
|
|
2668
|
+
? `<${key}>${escapeXmlText(text)}</${key}>`
|
|
2669
|
+
: "";
|
|
2670
|
+
})
|
|
2671
|
+
.join("");
|
|
2672
|
+
parts.push(children
|
|
2673
|
+
? `<headerFooter${hfAttrs}>${children}</headerFooter>`
|
|
2674
|
+
: `<headerFooter${hfAttrs}/>`);
|
|
2675
|
+
}
|
|
2676
|
+
const breaks = (ids, max) => {
|
|
2677
|
+
const valid = Array.from(new Set((ids ?? []).filter((id) => Number.isInteger(id) && id > 0))).sort((a, b) => a - b);
|
|
2678
|
+
if (!valid.length)
|
|
2679
|
+
return "";
|
|
2680
|
+
return valid
|
|
2681
|
+
.map((id) => `<brk id="${id}" max="${max}" man="1"/>`)
|
|
2682
|
+
.join("");
|
|
2683
|
+
};
|
|
2684
|
+
const rowBrks = breaks(print?.rowBreaks, 16383);
|
|
2685
|
+
if (rowBrks) {
|
|
2686
|
+
const count = rowBrks.split("<brk ").length - 1;
|
|
2687
|
+
parts.push(`<rowBreaks count="${count}" manualBreakCount="${count}">${rowBrks}</rowBreaks>`);
|
|
2688
|
+
}
|
|
2689
|
+
const colBrks = breaks(print?.columnBreaks, 1048575);
|
|
2690
|
+
if (colBrks) {
|
|
2691
|
+
const count = colBrks.split("<brk ").length - 1;
|
|
2692
|
+
parts.push(`<colBreaks count="${count}" manualBreakCount="${count}">${colBrks}</colBreaks>`);
|
|
2693
|
+
}
|
|
2694
|
+
return parts.join("");
|
|
2695
|
+
};
|
|
2356
2696
|
const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
|
|
2357
2697
|
if (!filterSpecs?.length)
|
|
2358
2698
|
return "";
|
|
@@ -2420,15 +2760,26 @@ const gridRangeToRef = (range) => {
|
|
|
2420
2760
|
};
|
|
2421
2761
|
const groupEmbedsBySheet = (embeds) => {
|
|
2422
2762
|
const result = new Map();
|
|
2423
|
-
|
|
2763
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2424
2764
|
if (!embed.position?.overlayPosition)
|
|
2425
|
-
|
|
2765
|
+
return;
|
|
2426
2766
|
if (embed.spec?.type !== "image")
|
|
2427
|
-
|
|
2767
|
+
return;
|
|
2428
2768
|
const overlay = embed.position.overlayPosition;
|
|
2769
|
+
const imageSpec = embed.spec;
|
|
2770
|
+
const adjustments = {
|
|
2771
|
+
crop: imageSpec.crop ?? undefined,
|
|
2772
|
+
brightness: imageSpec.brightness ?? undefined,
|
|
2773
|
+
contrast: imageSpec.contrast ?? undefined,
|
|
2774
|
+
transparency: imageSpec.transparency ?? undefined,
|
|
2775
|
+
};
|
|
2429
2776
|
const placement = {
|
|
2430
2777
|
kind: "embed",
|
|
2431
|
-
imageUrl:
|
|
2778
|
+
imageUrl: imageSpec.imageUrl,
|
|
2779
|
+
adjustments: Object.values(adjustments).some((v) => v !== undefined)
|
|
2780
|
+
? adjustments
|
|
2781
|
+
: undefined,
|
|
2782
|
+
order,
|
|
2432
2783
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2433
2784
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2434
2785
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2439,19 +2790,20 @@ const groupEmbedsBySheet = (embeds) => {
|
|
|
2439
2790
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2440
2791
|
list.push(placement);
|
|
2441
2792
|
result.set(embed.position.sheetId, list);
|
|
2442
|
-
}
|
|
2793
|
+
});
|
|
2443
2794
|
return result;
|
|
2444
2795
|
};
|
|
2445
2796
|
const groupTextBoxesBySheet = (embeds) => {
|
|
2446
2797
|
const result = new Map();
|
|
2447
|
-
|
|
2798
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2448
2799
|
if (!embed.position?.overlayPosition)
|
|
2449
|
-
|
|
2800
|
+
return;
|
|
2450
2801
|
if (embed.spec?.type !== "textbox")
|
|
2451
|
-
|
|
2802
|
+
return;
|
|
2452
2803
|
const overlay = embed.position.overlayPosition;
|
|
2453
2804
|
const placement = {
|
|
2454
2805
|
textBox: embed,
|
|
2806
|
+
order,
|
|
2455
2807
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2456
2808
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2457
2809
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2462,19 +2814,144 @@ const groupTextBoxesBySheet = (embeds) => {
|
|
|
2462
2814
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2463
2815
|
list.push(placement);
|
|
2464
2816
|
result.set(embed.position.sheetId, list);
|
|
2465
|
-
}
|
|
2817
|
+
});
|
|
2466
2818
|
return result;
|
|
2467
2819
|
};
|
|
2820
|
+
/** Form-control embeds (`spec.type === "formControl"`) by sheet, in host order. */
|
|
2821
|
+
const groupFormControlsBySheet = (embeds) => {
|
|
2822
|
+
const result = new Map();
|
|
2823
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2824
|
+
if (!embed.position?.overlayPosition)
|
|
2825
|
+
return;
|
|
2826
|
+
if (embed.spec?.type !== "formControl")
|
|
2827
|
+
return;
|
|
2828
|
+
const overlay = embed.position.overlayPosition;
|
|
2829
|
+
const placement = {
|
|
2830
|
+
shape: embed,
|
|
2831
|
+
order,
|
|
2832
|
+
rowIndex: overlay.anchorCell.rowIndex,
|
|
2833
|
+
columnIndex: overlay.anchorCell.columnIndex,
|
|
2834
|
+
offsetX: overlay.offsetXPixels ?? 0,
|
|
2835
|
+
offsetY: overlay.offsetYPixels ?? 0,
|
|
2836
|
+
width: overlay.widthPixels ?? 120,
|
|
2837
|
+
height: overlay.heightPixels ?? 20,
|
|
2838
|
+
};
|
|
2839
|
+
const list = result.get(embed.position.sheetId) ?? [];
|
|
2840
|
+
list.push(placement);
|
|
2841
|
+
result.set(embed.position.sheetId, list);
|
|
2842
|
+
});
|
|
2843
|
+
return result;
|
|
2844
|
+
};
|
|
2845
|
+
/**
|
|
2846
|
+
* Write a sheet's form controls: one ctrlProp part + sheet relationship per
|
|
2847
|
+
* control, the `<controls>` worksheet element, and the VML shapes Excel
|
|
2848
|
+
* draws them with (merged into the sheet's `vmlDrawing` alongside notes).
|
|
2849
|
+
*/
|
|
2850
|
+
const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIndexStart, reservedShapeIds, }) => {
|
|
2851
|
+
if (!controls.length)
|
|
2852
|
+
return { controlsXml: "", vmlShapes: [] };
|
|
2853
|
+
const controlElements = [];
|
|
2854
|
+
const vmlShapes = [];
|
|
2855
|
+
// Excel numbers notes and form-control shapes from one per-sheet counter
|
|
2856
|
+
// (1025 upward); a control that arrived with its own id keeps it unless it
|
|
2857
|
+
// collides with a note shape or another control.
|
|
2858
|
+
const usedShapeIds = new Set(reservedShapeIds);
|
|
2859
|
+
let nextShapeId = 1025;
|
|
2860
|
+
const allocateShapeId = (wanted) => {
|
|
2861
|
+
if (wanted && Number.isInteger(wanted) && !usedShapeIds.has(wanted)) {
|
|
2862
|
+
usedShapeIds.add(wanted);
|
|
2863
|
+
return wanted;
|
|
2864
|
+
}
|
|
2865
|
+
while (usedShapeIds.has(nextShapeId))
|
|
2866
|
+
nextShapeId += 1;
|
|
2867
|
+
usedShapeIds.add(nextShapeId);
|
|
2868
|
+
return nextShapeId++;
|
|
2869
|
+
};
|
|
2870
|
+
const sumSizes = (count, getSize) => {
|
|
2871
|
+
let total = 0;
|
|
2872
|
+
for (let index = 0; index < count; index++)
|
|
2873
|
+
total += getSize(index);
|
|
2874
|
+
return total;
|
|
2875
|
+
};
|
|
2876
|
+
controls.forEach((placement, position) => {
|
|
2877
|
+
const spec = placement.shape.spec;
|
|
2878
|
+
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
2879
|
+
const fromColumn = Math.max(0, placement.columnIndex - 1);
|
|
2880
|
+
const offsetX = Math.max(0, placement.offsetX ?? 0);
|
|
2881
|
+
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
2882
|
+
const widthPx = Math.max(5, placement.width ?? 120);
|
|
2883
|
+
const heightPx = Math.max(5, placement.height ?? 20);
|
|
2884
|
+
const columnSize = (index) => getColumnWidthPx(sheet, index, true);
|
|
2885
|
+
const rowSize = (index) => getRowHeightPx(sheet, index, true);
|
|
2886
|
+
const toColumn = resolveAnchorEndpoint({
|
|
2887
|
+
startIndex: fromColumn,
|
|
2888
|
+
startOffset: offsetX,
|
|
2889
|
+
span: widthPx,
|
|
2890
|
+
getSize: columnSize,
|
|
2891
|
+
});
|
|
2892
|
+
const toRow = resolveAnchorEndpoint({
|
|
2893
|
+
startIndex: fromRow,
|
|
2894
|
+
startOffset: offsetY,
|
|
2895
|
+
span: heightPx,
|
|
2896
|
+
getSize: rowSize,
|
|
2897
|
+
});
|
|
2898
|
+
const partIndex = ctrlProps.parts.length + 1;
|
|
2899
|
+
const path = `xl/ctrlProps/ctrlProp${partIndex}.xml`;
|
|
2900
|
+
ctrlProps.parts.push({ path, xml: buildCtrlPropXml(spec) });
|
|
2901
|
+
const relId = relationships.add(CTRL_PROP_REL_TYPE, `../ctrlProps/ctrlProp${partIndex}.xml`);
|
|
2902
|
+
const shapeId = allocateShapeId(spec.shapeId);
|
|
2903
|
+
controlElements.push(buildControlElementXml({
|
|
2904
|
+
spec,
|
|
2905
|
+
relId,
|
|
2906
|
+
shapeId,
|
|
2907
|
+
from: {
|
|
2908
|
+
col: fromColumn,
|
|
2909
|
+
colOff: offsetX * EMUS_PER_PIXEL,
|
|
2910
|
+
row: fromRow,
|
|
2911
|
+
rowOff: offsetY * EMUS_PER_PIXEL,
|
|
2912
|
+
},
|
|
2913
|
+
to: {
|
|
2914
|
+
col: toColumn.index,
|
|
2915
|
+
colOff: toColumn.offset * EMUS_PER_PIXEL,
|
|
2916
|
+
row: toRow.index,
|
|
2917
|
+
rowOff: toRow.offset * EMUS_PER_PIXEL,
|
|
2918
|
+
},
|
|
2919
|
+
}));
|
|
2920
|
+
vmlShapes.push(buildFormControlVmlShape({
|
|
2921
|
+
spec,
|
|
2922
|
+
shapeId,
|
|
2923
|
+
zIndex: zIndexStart + position + 1,
|
|
2924
|
+
geometry: {
|
|
2925
|
+
leftPx: sumSizes(fromColumn, columnSize) + offsetX,
|
|
2926
|
+
topPx: sumSizes(fromRow, rowSize) + offsetY,
|
|
2927
|
+
widthPx,
|
|
2928
|
+
heightPx,
|
|
2929
|
+
anchor: [
|
|
2930
|
+
fromColumn,
|
|
2931
|
+
Math.round(offsetX),
|
|
2932
|
+
fromRow,
|
|
2933
|
+
Math.round(offsetY),
|
|
2934
|
+
toColumn.index,
|
|
2935
|
+
Math.round(toColumn.offset),
|
|
2936
|
+
toRow.index,
|
|
2937
|
+
Math.round(toRow.offset),
|
|
2938
|
+
],
|
|
2939
|
+
},
|
|
2940
|
+
}));
|
|
2941
|
+
});
|
|
2942
|
+
return { controlsXml: buildControlsXml(controlElements), vmlShapes };
|
|
2943
|
+
};
|
|
2468
2944
|
const groupShapesBySheet = (embeds) => {
|
|
2469
2945
|
const result = new Map();
|
|
2470
|
-
|
|
2946
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2471
2947
|
if (!embed.position?.overlayPosition)
|
|
2472
|
-
|
|
2948
|
+
return;
|
|
2473
2949
|
if (embed.spec?.type !== "shape")
|
|
2474
|
-
|
|
2950
|
+
return;
|
|
2475
2951
|
const overlay = embed.position.overlayPosition;
|
|
2476
2952
|
const placement = {
|
|
2477
2953
|
shape: embed,
|
|
2954
|
+
order,
|
|
2478
2955
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2479
2956
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2480
2957
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2485,7 +2962,7 @@ const groupShapesBySheet = (embeds) => {
|
|
|
2485
2962
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2486
2963
|
list.push(placement);
|
|
2487
2964
|
result.set(embed.position.sheetId, list);
|
|
2488
|
-
}
|
|
2965
|
+
});
|
|
2489
2966
|
return result;
|
|
2490
2967
|
};
|
|
2491
2968
|
const groupChartsBySheet = (charts) => {
|
|
@@ -3077,7 +3554,7 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
|
|
|
3077
3554
|
rules.push(dataBarXml);
|
|
3078
3555
|
return rules.length ? rules : undefined;
|
|
3079
3556
|
};
|
|
3080
|
-
const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, charts, slicers, sheet, sheetNameById, }) => {
|
|
3557
|
+
const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, }) => {
|
|
3081
3558
|
if (!placements.length &&
|
|
3082
3559
|
!textBoxes.length &&
|
|
3083
3560
|
!shapes.length &&
|
|
@@ -3089,10 +3566,27 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3089
3566
|
const media = [];
|
|
3090
3567
|
let imageIndex = startImageIndex;
|
|
3091
3568
|
let chartIndex = startChartIndex;
|
|
3569
|
+
let chartExIndex = startChartExIndex;
|
|
3092
3570
|
let pictureId = 1;
|
|
3093
3571
|
let frameId = 1;
|
|
3094
3572
|
const chartEntries = [];
|
|
3573
|
+
const chartExEntries = [];
|
|
3095
3574
|
for (const placement of charts) {
|
|
3575
|
+
if (isChartExType(placement.chart.spec.chartType)) {
|
|
3576
|
+
// Treemap / sunburst live in a chartex part with an mc:AlternateContent anchor.
|
|
3577
|
+
const chartExXml = buildChartExXml(placement.chart, sheetNameById);
|
|
3578
|
+
if (!chartExXml)
|
|
3579
|
+
continue;
|
|
3580
|
+
const relId = relationships.add(CHART_EX_REL_TYPE, `../charts/chartEx${chartExIndex}.xml`);
|
|
3581
|
+
anchors.push(createChartAnchor({ sheet, placement, relId, frameId, chartEx: true }));
|
|
3582
|
+
chartExEntries.push({
|
|
3583
|
+
path: `xl/charts/chartEx${chartExIndex}.xml`,
|
|
3584
|
+
xml: chartExXml,
|
|
3585
|
+
});
|
|
3586
|
+
chartExIndex += 1;
|
|
3587
|
+
frameId += 1;
|
|
3588
|
+
continue;
|
|
3589
|
+
}
|
|
3096
3590
|
const chartXml = buildChartXml(placement.chart, sheetNameById);
|
|
3097
3591
|
if (!chartXml)
|
|
3098
3592
|
continue;
|
|
@@ -3118,20 +3612,32 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3118
3612
|
}));
|
|
3119
3613
|
frameId += 1;
|
|
3120
3614
|
}
|
|
3615
|
+
// Embeds (text boxes, shapes, images) keep the host's `embeds` order in the
|
|
3616
|
+
// drawing part — anchor order IS Excel's z-order, so an image drawn over a
|
|
3617
|
+
// shape must be emitted after it regardless of kind. Placements without an
|
|
3618
|
+
// order (in-cell images) go last, in their own order.
|
|
3619
|
+
const embedAnchors = [];
|
|
3620
|
+
const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
|
|
3121
3621
|
for (const placement of textBoxes) {
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3622
|
+
embedAnchors.push({
|
|
3623
|
+
order: orderOf(placement.order),
|
|
3624
|
+
xml: createTextBoxAnchor({
|
|
3625
|
+
sheet,
|
|
3626
|
+
placement,
|
|
3627
|
+
frameId,
|
|
3628
|
+
}),
|
|
3629
|
+
});
|
|
3127
3630
|
frameId += 1;
|
|
3128
3631
|
}
|
|
3129
3632
|
for (const placement of shapes) {
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3633
|
+
embedAnchors.push({
|
|
3634
|
+
order: orderOf(placement.order),
|
|
3635
|
+
xml: createShapeAnchor({
|
|
3636
|
+
sheet,
|
|
3637
|
+
placement,
|
|
3638
|
+
frameId,
|
|
3639
|
+
}),
|
|
3640
|
+
});
|
|
3135
3641
|
frameId += 1;
|
|
3136
3642
|
}
|
|
3137
3643
|
for (const placement of placements) {
|
|
@@ -3148,23 +3654,31 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3148
3654
|
extension: buffer.extension,
|
|
3149
3655
|
});
|
|
3150
3656
|
const relId = relationships.add(IMAGE_REL_TYPE, `../media/image${imageIndex}.${buffer.extension}`);
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3657
|
+
embedAnchors.push({
|
|
3658
|
+
order: orderOf(placement.order),
|
|
3659
|
+
xml: createTwoCellAnchor({
|
|
3660
|
+
sheet,
|
|
3661
|
+
placement,
|
|
3662
|
+
relId,
|
|
3663
|
+
pictureId,
|
|
3664
|
+
}),
|
|
3665
|
+
});
|
|
3157
3666
|
imageIndex += 1;
|
|
3158
3667
|
pictureId += 1;
|
|
3159
3668
|
}
|
|
3669
|
+
embedAnchors.sort((a, b) => a.order - b.order);
|
|
3670
|
+
for (const entry of embedAnchors)
|
|
3671
|
+
anchors.push(entry.xml);
|
|
3160
3672
|
const drawingXml = `${XML_DECLARATION}<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${REL_NS}">${anchors.join("")}</xdr:wsDr>`;
|
|
3161
3673
|
return {
|
|
3162
3674
|
xml: drawingXml,
|
|
3163
3675
|
relsXml: relationships.toXml(),
|
|
3164
3676
|
media,
|
|
3165
3677
|
charts: chartEntries,
|
|
3678
|
+
chartExs: chartExEntries,
|
|
3166
3679
|
nextImageIndex: imageIndex,
|
|
3167
3680
|
nextChartIndex: chartIndex,
|
|
3681
|
+
nextChartExIndex: chartExIndex,
|
|
3168
3682
|
};
|
|
3169
3683
|
};
|
|
3170
3684
|
const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
|
|
@@ -3190,9 +3704,55 @@ const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
|
|
|
3190
3704
|
});
|
|
3191
3705
|
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3192
3706
|
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3193
|
-
|
|
3707
|
+
const { blipChildren, srcRectXml } = buildPictureAdjustmentsXml(placement.adjustments);
|
|
3708
|
+
const blipXml = blipChildren
|
|
3709
|
+
? `<a:blip r:embed="${relId}">${blipChildren}</a:blip>`
|
|
3710
|
+
: `<a:blip r:embed="${relId}"/>`;
|
|
3711
|
+
return `<xdr:twoCellAnchor editAs="oneCell">${fromXml}${toXml}<xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${pictureId}" name="Picture ${pictureId}"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill>${blipXml}${srcRectXml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:twoCellAnchor>`;
|
|
3194
3712
|
};
|
|
3195
|
-
|
|
3713
|
+
/** Fraction (0…1) → DrawingML per-mille percent (100000 = 100%). */
|
|
3714
|
+
const fractionToPct = (value) => Math.round(Math.max(-1, Math.min(1, value)) * 100000);
|
|
3715
|
+
/**
|
|
3716
|
+
* `<a:blip>` children (`<a:alphaModFix>`, `<a:lum>`) and the `<a:srcRect>`
|
|
3717
|
+
* sibling for an image's picture-format adjustments; both empty when the
|
|
3718
|
+
* spec carries none.
|
|
3719
|
+
*/
|
|
3720
|
+
const buildPictureAdjustmentsXml = (adjustments) => {
|
|
3721
|
+
if (!adjustments)
|
|
3722
|
+
return { blipChildren: "", srcRectXml: "" };
|
|
3723
|
+
let blipChildren = "";
|
|
3724
|
+
const { transparency, brightness, contrast, crop } = adjustments;
|
|
3725
|
+
if (typeof transparency === "number" && transparency > 0) {
|
|
3726
|
+
blipChildren += `<a:alphaModFix amt="${fractionToPct(1 - Math.min(1, transparency))}"/>`;
|
|
3727
|
+
}
|
|
3728
|
+
const hasBrightness = typeof brightness === "number" && brightness !== 0;
|
|
3729
|
+
const hasContrast = typeof contrast === "number" && contrast !== 0;
|
|
3730
|
+
if (hasBrightness || hasContrast) {
|
|
3731
|
+
blipChildren += `<a:lum${hasBrightness ? ` bright="${fractionToPct(brightness)}"` : ""}${hasContrast ? ` contrast="${fractionToPct(contrast)}"` : ""}/>`;
|
|
3732
|
+
}
|
|
3733
|
+
let srcRectXml = "";
|
|
3734
|
+
if (crop) {
|
|
3735
|
+
const edges = ["l", "t", "r", "b"]
|
|
3736
|
+
.map((edge) => {
|
|
3737
|
+
const value = {
|
|
3738
|
+
l: crop.left,
|
|
3739
|
+
t: crop.top,
|
|
3740
|
+
r: crop.right,
|
|
3741
|
+
b: crop.bottom,
|
|
3742
|
+
}[edge];
|
|
3743
|
+
// Negative edges are Excel's "out-crop" (padding around the
|
|
3744
|
+
// bitmap) and round-trip like positive ones.
|
|
3745
|
+
return typeof value === "number" && Number.isFinite(value) && value !== 0
|
|
3746
|
+
? ` ${edge}="${fractionToPct(value)}"`
|
|
3747
|
+
: "";
|
|
3748
|
+
})
|
|
3749
|
+
.join("");
|
|
3750
|
+
if (edges)
|
|
3751
|
+
srcRectXml = `<a:srcRect${edges}/>`;
|
|
3752
|
+
}
|
|
3753
|
+
return { blipChildren, srcRectXml };
|
|
3754
|
+
};
|
|
3755
|
+
const createChartAnchor = ({ placement, relId, frameId, sheet, chartEx = false, }) => {
|
|
3196
3756
|
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
3197
3757
|
const fromColumn = Math.max(0, placement.columnIndex - 1);
|
|
3198
3758
|
const offsetX = Math.max(0, placement.offsetX ?? 0);
|
|
@@ -3213,6 +3773,14 @@ const createChartAnchor = ({ placement, relId, frameId, sheet, }) => {
|
|
|
3213
3773
|
});
|
|
3214
3774
|
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3215
3775
|
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3776
|
+
if (chartEx) {
|
|
3777
|
+
return `<xdr:twoCellAnchor>${fromXml}${toXml}${buildChartExGraphicFrameXml({
|
|
3778
|
+
relId,
|
|
3779
|
+
frameId,
|
|
3780
|
+
widthEmu: Math.round(widthPx * EMUS_PER_PIXEL),
|
|
3781
|
+
heightEmu: Math.round(heightPx * EMUS_PER_PIXEL),
|
|
3782
|
+
})}<xdr:clientData/></xdr:twoCellAnchor>`;
|
|
3783
|
+
}
|
|
3216
3784
|
return `<xdr:twoCellAnchor>${fromXml}${toXml}<xdr:graphicFrame><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="Chart ${frameId}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="${REL_NS}" r:id="${relId}"/></a:graphicData></a:graphic></xdr:graphicFrame><xdr:clientData/></xdr:twoCellAnchor>`;
|
|
3217
3785
|
};
|
|
3218
3786
|
const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
|
|
@@ -3857,7 +4425,10 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
|
|
|
3857
4425
|
sheets.forEach((sheet, index) => {
|
|
3858
4426
|
sheetNameById.set(sheet.sheetId ?? index + 1, sheetNames[index]);
|
|
3859
4427
|
});
|
|
3860
|
-
const definedNamesXml = buildDefinedNamesXml(namedRanges, sheetNameById)
|
|
4428
|
+
const definedNamesXml = buildDefinedNamesXml(namedRanges, sheetNameById, sheets.map((sheet, index) => ({
|
|
4429
|
+
sheetName: sheetNames[index] ?? `Sheet${index + 1}`,
|
|
4430
|
+
printSettings: sheet.printSettings,
|
|
4431
|
+
})));
|
|
3861
4432
|
const calcPrXml = buildCalcPrXml(iterativeCalculationOptions);
|
|
3862
4433
|
const slicerCachesXml = buildWorkbookSlicerCachesXml(slicerCacheRelIds);
|
|
3863
4434
|
// Phase D2 — <pivotCaches> element. Each <pivotCache> pairs a
|
|
@@ -3886,12 +4457,48 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
|
|
|
3886
4457
|
: `<workbookPr/>`;
|
|
3887
4458
|
return `${XML_DECLARATION}<workbook xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${workbookPrXml}${calcPrXml}<bookViews><workbookView activeTab="0" firstSheet="0"/></bookViews><sheets>${sheetEntries}</sheets>${definedNamesXml}${externalReferencesXml}${pivotCachesXml}${slicerCachesXml}</workbook>`;
|
|
3888
4459
|
};
|
|
3889
|
-
const buildDefinedNamesXml = (namedRanges, sheetNameById) => {
|
|
3890
|
-
if (!namedRanges?.length)
|
|
3891
|
-
return "";
|
|
4460
|
+
const buildDefinedNamesXml = (namedRanges, sheetNameById, printScopes = []) => {
|
|
3892
4461
|
const entries = [];
|
|
3893
|
-
|
|
3894
|
-
|
|
4462
|
+
// The builtin print names are sheet-scoped (`localSheetId` = position in
|
|
4463
|
+
// `<sheets>`); they come from `sheet.printSettings`, never from
|
|
4464
|
+
// `namedRanges` (a global `_xlnm.Print_Area` makes Excel repair the file).
|
|
4465
|
+
// Documents imported before `printSettings` existed still carry the print
|
|
4466
|
+
// area as a `_xlnm.Print_Area` named range on the sheet; it is converted
|
|
4467
|
+
// to the sheet-scoped name here unless the sheet already has a print area.
|
|
4468
|
+
const sheetIndexByName = new Map(printScopes.map(({ sheetName }, index) => [sheetName, index]));
|
|
4469
|
+
const legacyPrintAreas = new Map();
|
|
4470
|
+
for (const namedRange of namedRanges ?? []) {
|
|
4471
|
+
if (namedRange?.name !== PRINT_AREA_NAME || !namedRange.range?.sheetId) {
|
|
4472
|
+
continue;
|
|
4473
|
+
}
|
|
4474
|
+
const sheetName = sheetNameById.get(namedRange.range.sheetId);
|
|
4475
|
+
const index = sheetName ? sheetIndexByName.get(sheetName) : undefined;
|
|
4476
|
+
if (index == null)
|
|
4477
|
+
continue;
|
|
4478
|
+
legacyPrintAreas.set(index, [
|
|
4479
|
+
...(legacyPrintAreas.get(index) ?? []),
|
|
4480
|
+
namedRange.range,
|
|
4481
|
+
]);
|
|
4482
|
+
}
|
|
4483
|
+
printScopes.forEach(({ sheetName, printSettings }, index) => {
|
|
4484
|
+
const printArea = printSettings?.printArea?.length
|
|
4485
|
+
? printSettings.printArea
|
|
4486
|
+
: legacyPrintAreas.get(index);
|
|
4487
|
+
const area = printArea?.length
|
|
4488
|
+
? buildPrintAreaFormula(sheetName, printArea)
|
|
4489
|
+
: undefined;
|
|
4490
|
+
if (area) {
|
|
4491
|
+
entries.push(`<definedName name="${PRINT_AREA_NAME}" localSheetId="${index}">${escapeXmlText(area)}</definedName>`);
|
|
4492
|
+
}
|
|
4493
|
+
const titles = printSettings?.printTitles
|
|
4494
|
+
? buildPrintTitlesFormula(sheetName, printSettings.printTitles)
|
|
4495
|
+
: undefined;
|
|
4496
|
+
if (titles) {
|
|
4497
|
+
entries.push(`<definedName name="${PRINT_TITLES_NAME}" localSheetId="${index}">${escapeXmlText(titles)}</definedName>`);
|
|
4498
|
+
}
|
|
4499
|
+
});
|
|
4500
|
+
for (const namedRange of namedRanges ?? []) {
|
|
4501
|
+
if (!namedRange?.name || isBuiltinPrintName(namedRange.name))
|
|
3895
4502
|
continue;
|
|
3896
4503
|
const hiddenAttr = namedRange.hidden ? ` hidden="1"` : "";
|
|
3897
4504
|
if (namedRange.range?.sheetId) {
|
|
@@ -3993,7 +4600,7 @@ const buildWorkbookRelationships = (sheetCount, slicerCacheCount = 0, externalLi
|
|
|
3993
4600
|
slicerCacheRelationshipIds: slicerCacheRelIds,
|
|
3994
4601
|
};
|
|
3995
4602
|
};
|
|
3996
|
-
const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExtensions, chartCount, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, }) => {
|
|
4603
|
+
const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
|
|
3997
4604
|
const sheetOverrides = Array.from({ length: sheetCount }, (_, index) => {
|
|
3998
4605
|
return `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
|
3999
4606
|
}).join("");
|
|
@@ -4006,6 +4613,9 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
|
|
|
4006
4613
|
const chartOverrides = Array.from({ length: chartCount }, (_, index) => {
|
|
4007
4614
|
return `<Override PartName="/xl/charts/chart${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>`;
|
|
4008
4615
|
}).join("");
|
|
4616
|
+
const chartExOverrides = Array.from({ length: chartExCount }, (_, index) => {
|
|
4617
|
+
return `<Override PartName="/xl/charts/chartEx${index + 1}.xml" ContentType="${CHART_EX_CONTENT_TYPE}"/>`;
|
|
4618
|
+
}).join("");
|
|
4009
4619
|
const slicerOverrides = Array.from({ length: slicerCount }, (_, index) => {
|
|
4010
4620
|
return `<Override PartName="/xl/slicers/slicer${index + 1}.xml" ContentType="application/vnd.ms-excel.slicer+xml"/>`;
|
|
4011
4621
|
}).join("");
|
|
@@ -4030,10 +4640,11 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
|
|
|
4030
4640
|
const mediaDefaults = mediaExtensions
|
|
4031
4641
|
.map((extension) => `<Default Extension="${extension}" ContentType="image/${extension}"/>`)
|
|
4032
4642
|
.join("");
|
|
4033
|
-
const vmlDefault = commentsSheets.length
|
|
4643
|
+
const vmlDefault = hasVmlDrawings || commentsSheets.length
|
|
4034
4644
|
? `<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>`
|
|
4035
4645
|
: "";
|
|
4036
|
-
|
|
4646
|
+
const ctrlPropOverrides = Array.from({ length: ctrlPropCount }, (_, index) => `<Override PartName="/xl/ctrlProps/ctrlProp${index + 1}.xml" ContentType="${CTRL_PROP_CONTENT_TYPE}"/>`).join("");
|
|
4647
|
+
return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
|
|
4037
4648
|
};
|
|
4038
4649
|
const buildRootRelationshipsXml = () => {
|
|
4039
4650
|
return `${XML_DECLARATION}<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="${REL_NS}/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId3" Type="${REL_NS}/core-properties" Target="docProps/core.xml"/></Relationships>`;
|