@rowsncolumns/toolkit 24.4.5 → 24.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/autofilter-condition.d.ts +45 -1
- package/dist/cjs/autofilter-condition.d.ts.map +1 -1
- package/dist/cjs/autofilter-condition.js +135 -12
- package/dist/cjs/excel-parser/helpers.d.ts +15 -1
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
- package/dist/cjs/excel-parser/helpers.js +119 -5
- package/dist/cjs/excel-parser/interface.d.ts +3 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.js +186 -97
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/styles.js +80 -6
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +107 -103
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.js +920 -122
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +24 -9
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +25 -3
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +561 -394
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +312 -21
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/cjs/excel-parser/types.d.ts +9 -1
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +1 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +5 -0
- package/dist/cjs/excel-parser/worker.js +2 -0
- package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/cjs/exporter/ods/ods-export.js +255 -80
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/chart-builder.js +902 -119
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/form-controls-builder.js +30 -8
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/pivot-builder.js +206 -162
- package/dist/cjs/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/style-builder.js +25 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +884 -470
- package/dist/esm/autofilter-condition.d.ts +45 -1
- package/dist/esm/autofilter-condition.d.ts.map +1 -1
- package/dist/esm/autofilter-condition.js +131 -11
- package/dist/esm/excel-parser/helpers.d.ts +15 -1
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
- package/dist/esm/excel-parser/helpers.js +94 -4
- package/dist/esm/excel-parser/interface.d.ts +3 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.js +105 -22
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/styles.js +79 -5
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/index.js +15 -7
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.js +708 -16
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.js +23 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +21 -3
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +192 -54
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/pivots.js +220 -4
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/esm/excel-parser/types.d.ts +9 -1
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +1 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +4 -0
- package/dist/esm/excel-parser/worker.js +2 -0
- package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/esm/exporter/ods/ods-export.js +238 -72
- package/dist/esm/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +674 -58
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/form-controls-builder.js +29 -8
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/pivot-builder.js +187 -139
- package/dist/esm/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/style-builder.js +28 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +600 -206
- package/package.json +10 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, isNil, sortSheetsByIndex, } from "@rowsncolumns/utils";
|
|
2
2
|
import { zipSync } from "fflate";
|
|
3
3
|
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
4
|
-
import {
|
|
5
|
-
import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
|
|
4
|
+
import { buildAutoFilterCriteriaXml } from "../../autofilter-condition";
|
|
5
|
+
import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, controlAnchorFlags, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
|
|
6
6
|
import { buildChartExGraphicFrameXml, buildChartExXml, CHART_EX_CONTENT_TYPE, CHART_EX_REL_TYPE, isChartExType, } from "./chartex-builder";
|
|
7
7
|
import { applyPowerQueryParts } from "./power-query-builder";
|
|
8
8
|
import { encodeImplicitIntersection } from "../../helpers/implicit-intersection";
|
|
@@ -19,7 +19,7 @@ import { buildChartXml } from "./chart-builder";
|
|
|
19
19
|
import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, calculatedFieldReferences, } from "./pivot-builder";
|
|
20
20
|
import { buildSlicerCacheXml, buildSlicerXml } from "./slicer-builder";
|
|
21
21
|
import { buildSparklineGroupsXml } from "./sparkline-builder";
|
|
22
|
-
import { StyleBuilder } from "./style-builder";
|
|
22
|
+
import { StyleBuilder, tableStyleElementsFromBanding, } from "./style-builder";
|
|
23
23
|
const BLANK_LABEL = "(Blanks)";
|
|
24
24
|
const isStyleReference = (value) => !!value && typeof value === "object" && "sid" in value;
|
|
25
25
|
const DEFAULT_COLUMN_WIDTH_PX = 100;
|
|
@@ -38,9 +38,15 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
38
38
|
if (namedCellStyles && namedCellStyles.length > 0) {
|
|
39
39
|
styleBuilder.setNamedCellStyles(namedCellStyles);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// The document facet carries `{ name, bandedRange }` (#668 T-103); a host may still pass the
|
|
42
|
+
// parser's element records. Both become `<tableStyle>` elements.
|
|
43
|
+
const customTableStyleRecords = (customTableStyles ?? []).map((style) => "bandedRange" in style
|
|
44
|
+
? { name: style.name, elements: tableStyleElementsFromBanding(style.bandedRange) }
|
|
45
|
+
: style);
|
|
46
|
+
if (customTableStyleRecords.length > 0) {
|
|
47
|
+
styleBuilder.setCustomTableStyles(customTableStyleRecords);
|
|
43
48
|
}
|
|
49
|
+
const customTableStyleNames = new Set(customTableStyleRecords.map((s) => s.name));
|
|
44
50
|
// Drop tables with invalid coords or overlapping ranges — Excel rejects them
|
|
45
51
|
// and would trigger a recovery prompt. Filter once so the id remap and the
|
|
46
52
|
// prepared output stay in lockstep.
|
|
@@ -52,10 +58,13 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
52
58
|
tableIdMap.set(String(table.id), index + 1);
|
|
53
59
|
});
|
|
54
60
|
const preparedTables = prepareTables(exportableTables);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
61
|
+
// Charts and embeds share one z-order per sheet (`position.zOrder`, else
|
|
62
|
+
// creation order: charts below embeds) — the drawing anchors follow it.
|
|
63
|
+
const chartCount = charts?.length ?? 0;
|
|
64
|
+
const embedsBySheet = groupEmbedsBySheet(embeds ?? [], chartCount);
|
|
65
|
+
const textBoxesBySheet = groupTextBoxesBySheet(embeds ?? [], chartCount);
|
|
66
|
+
const shapesBySheet = groupShapesBySheet(embeds ?? [], chartCount);
|
|
67
|
+
const formControlsBySheet = groupFormControlsBySheet(embeds ?? [], chartCount);
|
|
59
68
|
const ctrlProps = { parts: [] };
|
|
60
69
|
const chartsBySheet = groupChartsBySheet(charts ?? []);
|
|
61
70
|
// Remap slicer tableIds to match prepared table IDs
|
|
@@ -100,6 +109,22 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
100
109
|
const pivotCacheRelsEntries = [];
|
|
101
110
|
const pivotTableEntries = [];
|
|
102
111
|
const pivotTableRelsEntries = [];
|
|
112
|
+
// `<c:pivotSource><c:name>` for PivotCharts: the exported pivot's name on
|
|
113
|
+
// its target sheet (`pivot-builder` names an unnamed pivot the same way).
|
|
114
|
+
// Only pivots the pivot writer emits (source + pivotRange) are named, so
|
|
115
|
+
// a chart never points Excel at a pivot part that is not in the file.
|
|
116
|
+
const pivotSourceNameById = new Map();
|
|
117
|
+
for (const p of pivotTables) {
|
|
118
|
+
const targetSheet = sortedSheets.find((s) => s.sheetId === p.targetPosition?.sheetId);
|
|
119
|
+
if (!targetSheet || !p.source || !p.pivotRange)
|
|
120
|
+
continue;
|
|
121
|
+
const sheetTitle = targetSheet.title ?? `Sheet${targetSheet.sheetId}`;
|
|
122
|
+
const quoted = /^[A-Za-z_][A-Za-z0-9_.]*$/.test(sheetTitle)
|
|
123
|
+
? sheetTitle
|
|
124
|
+
: `'${sheetTitle.replace(/'/g, "''")}'`;
|
|
125
|
+
pivotSourceNameById.set(String(p.pivotId), `[Workbook]${quoted}!${p.name ?? `PivotTable${p.pivotId}`}`);
|
|
126
|
+
}
|
|
127
|
+
const resolvePivotSourceName = (pivotTableId) => pivotSourceNameById.get(String(pivotTableId));
|
|
103
128
|
// Group pivots by the sheet they render on (targetPosition.sheetId).
|
|
104
129
|
const pivotsByTargetSheet = new Map();
|
|
105
130
|
for (const p of pivotTables) {
|
|
@@ -115,7 +140,15 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
115
140
|
// Records pivot caches that need a workbook-level entry (added
|
|
116
141
|
// after the sheet loop closes). Each entry: { cacheIndex, rIdOnWorkbook }
|
|
117
142
|
const workbookPivotCacheRefs = [];
|
|
143
|
+
// Worksheet and chartsheet parts are numbered 1..N within their own folder
|
|
144
|
+
// (`xl/worksheets/sheetN.xml` / `xl/chartsheets/sheetN.xml`), as Excel does;
|
|
145
|
+
// drawings stay keyed by the sheet's position in `sortedSheets`.
|
|
146
|
+
let worksheetCount = 0;
|
|
147
|
+
let chartsheetCount = 0;
|
|
118
148
|
const worksheetEntries = sortedSheets.map((sheet, index) => {
|
|
149
|
+
const isChartsheet = sheet.type === "chartsheet";
|
|
150
|
+
const partIndex = isChartsheet ? ++chartsheetCount : ++worksheetCount;
|
|
151
|
+
const partFolder = isChartsheet ? "chartsheets" : "worksheets";
|
|
119
152
|
const rows = getRowsForSheet(sheetData, sheet.sheetId);
|
|
120
153
|
const sheetRelationships = new RelationshipsBuilder();
|
|
121
154
|
const sheetTables = preparedTables.filter((table) => table.sheetId === sheet.sheetId);
|
|
@@ -165,6 +198,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
165
198
|
offsetY: placement.offsetY,
|
|
166
199
|
width: placement.width,
|
|
167
200
|
height: placement.height,
|
|
201
|
+
overlay: slicer.position?.overlayPosition,
|
|
168
202
|
});
|
|
169
203
|
globalSlicerIndex += 1;
|
|
170
204
|
}
|
|
@@ -233,35 +267,42 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
233
267
|
workbookPivotCacheRefs.push({ cacheIndex });
|
|
234
268
|
globalPivotIndex += 1;
|
|
235
269
|
}
|
|
236
|
-
const result =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
const result = isChartsheet
|
|
271
|
+
? buildChartsheetXml({
|
|
272
|
+
sheet,
|
|
273
|
+
relationships: sheetRelationships,
|
|
274
|
+
drawingContext,
|
|
275
|
+
})
|
|
276
|
+
: buildWorksheetXml({
|
|
277
|
+
sheet,
|
|
278
|
+
rows,
|
|
279
|
+
sharedStrings,
|
|
280
|
+
sourceSharedStrings,
|
|
281
|
+
cellXfs,
|
|
282
|
+
styleBuilder,
|
|
283
|
+
tables: sheetTables,
|
|
284
|
+
relationships: sheetRelationships,
|
|
285
|
+
drawingContext,
|
|
286
|
+
conditionalFormats: sheetConditionalFormats,
|
|
287
|
+
dataValidationsById,
|
|
288
|
+
dataValidations: _dataValidations,
|
|
289
|
+
ignoreFormulaPatterns,
|
|
290
|
+
citations: sheetCitations,
|
|
291
|
+
comments,
|
|
292
|
+
personRegistry,
|
|
293
|
+
slicerRelIds: worksheetSlicerRelIds,
|
|
294
|
+
sheetData,
|
|
295
|
+
sheets: sortedSheets,
|
|
296
|
+
locale,
|
|
297
|
+
protectedRanges,
|
|
298
|
+
formControls: sheetFormControls,
|
|
299
|
+
ctrlProps,
|
|
300
|
+
});
|
|
261
301
|
return {
|
|
262
302
|
index,
|
|
263
|
-
|
|
264
|
-
|
|
303
|
+
isChartsheet,
|
|
304
|
+
path: `xl/${partFolder}/sheet${partIndex}.xml`,
|
|
305
|
+
relationshipsPath: `xl/${partFolder}/_rels/sheet${partIndex}.xml.rels`,
|
|
265
306
|
content: result.xml,
|
|
266
307
|
relationshipsXml: result.relationshipsXml,
|
|
267
308
|
drawingPath: `xl/drawings/drawing${index + 1}.xml`,
|
|
@@ -278,12 +319,25 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
278
319
|
threadedCommentsXml: result.threadedCommentsXml,
|
|
279
320
|
};
|
|
280
321
|
});
|
|
281
|
-
const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.
|
|
322
|
+
const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.map((entry) => ({
|
|
323
|
+
chartsheet: entry.isChartsheet,
|
|
324
|
+
target: entry.path.replace(/^xl\//, ""),
|
|
325
|
+
})), slicerCacheEntries.length, externalLinks.length, pivotCacheEntries.length, !personRegistry.isEmpty());
|
|
282
326
|
const workbookXml = buildWorkbookXml(sortedSheets, sheetRelationshipIds, workbookSheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelationshipIds, use1904, externalLinkRelationshipIds, pivotCacheRelationshipIds);
|
|
283
327
|
const sharedStringsXml = buildSharedStringsXml(sharedStrings);
|
|
284
328
|
const rootRelationshipsXml = buildRootRelationshipsXml();
|
|
285
|
-
const appXml = buildAppXml(workbookSheetNames, properties);
|
|
329
|
+
const appXml = buildAppXml(workbookSheetNames, sortedSheets.map((sheet) => sheet.type === "chartsheet"), properties);
|
|
286
330
|
const coreXml = buildCoreXml(properties);
|
|
331
|
+
// Table parts register dxfs (colour filters, colour sort levels), so they
|
|
332
|
+
// are built before styles.xml is serialized.
|
|
333
|
+
const tablePartFiles = {};
|
|
334
|
+
for (const table of preparedTables) {
|
|
335
|
+
const rows = getRowsForSheet(sheetData, table.sheetId);
|
|
336
|
+
const tableXml = buildTableXml(table, rows, sourceSharedStrings, styleBuilder, customTableStyleNames);
|
|
337
|
+
if (tableXml) {
|
|
338
|
+
tablePartFiles[table.path] = encodeXml(tableXml);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
287
341
|
const stylesXml = styleBuilder.toXml();
|
|
288
342
|
const themeXml = theme ? generateThemeXml(theme) : theme1Xml;
|
|
289
343
|
const files = {
|
|
@@ -317,13 +371,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
317
371
|
if (personsXml) {
|
|
318
372
|
files["xl/persons/person.xml"] = encodeXml(personsXml);
|
|
319
373
|
}
|
|
320
|
-
|
|
321
|
-
const rows = getRowsForSheet(sheetData, table.sheetId);
|
|
322
|
-
const tableXml = buildTableXml(table, rows, sourceSharedStrings, styleBuilder);
|
|
323
|
-
if (tableXml) {
|
|
324
|
-
files[table.path] = encodeXml(tableXml);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
374
|
+
Object.assign(files, tablePartFiles);
|
|
327
375
|
let globalImageIndex = 1;
|
|
328
376
|
let globalChartIndex = 1;
|
|
329
377
|
const drawingSheets = [];
|
|
@@ -356,6 +404,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
356
404
|
charts: chartsForSheet,
|
|
357
405
|
slicers: slicersForSheet,
|
|
358
406
|
sheetNameById,
|
|
407
|
+
resolvePivotSourceName,
|
|
359
408
|
});
|
|
360
409
|
if (!drawingParts)
|
|
361
410
|
continue;
|
|
@@ -425,7 +474,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
425
474
|
.filter((entry) => entry.threadedCommentsXml)
|
|
426
475
|
.map((entry) => entry.sheet.sheetId ?? entry.index + 1);
|
|
427
476
|
const contentTypesXml = buildContentTypesXml({
|
|
428
|
-
|
|
477
|
+
worksheetCount,
|
|
478
|
+
chartsheetCount,
|
|
429
479
|
tableCount: preparedTables.length,
|
|
430
480
|
drawingSheets,
|
|
431
481
|
mediaExtensions: Array.from(imageExtensions),
|
|
@@ -929,6 +979,27 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
929
979
|
threadedCommentsXml,
|
|
930
980
|
};
|
|
931
981
|
};
|
|
982
|
+
/**
|
|
983
|
+
* `CT_Chartsheet` — a sheet tab whose whole body is one chart and that has no
|
|
984
|
+
* cells (#668 C-17). Children in schema order: `sheetPr` (tab colour only),
|
|
985
|
+
* `sheetViews`, `pageMargins`, `drawing`. The chart itself rides the sheet's
|
|
986
|
+
* drawing part as an `absoluteAnchor`, like every other chart.
|
|
987
|
+
*/
|
|
988
|
+
const buildChartsheetXml = ({ sheet, relationships, drawingContext, }) => {
|
|
989
|
+
const tabColor = serializeColorXml(sheet.tabColor ?? undefined);
|
|
990
|
+
const sheetPrXml = tabColor ? `<sheetPr><tabColor ${tabColor}/></sheetPr>` : "";
|
|
991
|
+
const drawingXml = drawingContext.hasDrawing()
|
|
992
|
+
? `<drawing r:id="${drawingContext.relId}"/>`
|
|
993
|
+
: "";
|
|
994
|
+
return {
|
|
995
|
+
xml: `${XML_DECLARATION}<chartsheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<sheetViews><sheetView workbookViewId="0" zoomToFit="1"/></sheetViews>${buildPageMarginsXml(sheet.printSettings)}${drawingXml}</chartsheet>`,
|
|
996
|
+
relationshipsXml: relationships.toXml(),
|
|
997
|
+
images: [],
|
|
998
|
+
commentsXml: undefined,
|
|
999
|
+
vmlDrawingXml: undefined,
|
|
1000
|
+
threadedCommentsXml: undefined,
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
932
1003
|
/**
|
|
933
1004
|
* Build combined extLst XML for worksheet (sparklines + slicers)
|
|
934
1005
|
*/
|
|
@@ -1015,6 +1086,109 @@ const normalizeMergeRanges = (ranges) => {
|
|
|
1015
1086
|
}
|
|
1016
1087
|
return valid;
|
|
1017
1088
|
};
|
|
1089
|
+
/** Escape `"` as `""` inside a formula string literal. */
|
|
1090
|
+
const escapeFormulaText = (value) => value.replace(/"/g, '""');
|
|
1091
|
+
// `xlsxSource` is meant to hold the verbatim OOXML representation, but
|
|
1092
|
+
// writers other than the importer have filled it from Excel's JavaScript-API
|
|
1093
|
+
// vocabulary: a bare `list.source` literal (`On track,Watch,Off track`),
|
|
1094
|
+
// PascalCase operators (`Between`), PascalCase types (`WholeNumber`) and
|
|
1095
|
+
// `=`-prefixed formulas. Excel opens a file carrying any of those in repair
|
|
1096
|
+
// mode and drops the rule, so the exporter normalizes every field before
|
|
1097
|
+
// replaying it, and a type / operator that is not an OOXML token even after
|
|
1098
|
+
// normalization (`undefined` below) is never replayed — the rule falls
|
|
1099
|
+
// through to the condition synthesizer. Twin of `normalize_source_*` in
|
|
1100
|
+
// `rust/crates/rnc-export/src/xlsx/datavalidation.rs` — keep the two in sync
|
|
1101
|
+
// so both engines emit identical XML.
|
|
1102
|
+
const normalizeXlsxSourceType = (type) => {
|
|
1103
|
+
switch (type.trim().toLowerCase()) {
|
|
1104
|
+
case "list":
|
|
1105
|
+
return "list";
|
|
1106
|
+
case "whole":
|
|
1107
|
+
case "wholenumber":
|
|
1108
|
+
return "whole";
|
|
1109
|
+
case "decimal":
|
|
1110
|
+
return "decimal";
|
|
1111
|
+
case "date":
|
|
1112
|
+
return "date";
|
|
1113
|
+
case "time":
|
|
1114
|
+
return "time";
|
|
1115
|
+
case "textlength":
|
|
1116
|
+
return "textLength";
|
|
1117
|
+
case "custom":
|
|
1118
|
+
return "custom";
|
|
1119
|
+
default:
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
const normalizeXlsxSourceOperator = (operator) => {
|
|
1124
|
+
switch (operator.trim().toLowerCase()) {
|
|
1125
|
+
case "between":
|
|
1126
|
+
return "between";
|
|
1127
|
+
case "notbetween":
|
|
1128
|
+
return "notBetween";
|
|
1129
|
+
case "equal":
|
|
1130
|
+
case "equalto":
|
|
1131
|
+
return "equal";
|
|
1132
|
+
case "notequal":
|
|
1133
|
+
case "notequalto":
|
|
1134
|
+
return "notEqual";
|
|
1135
|
+
case "greaterthan":
|
|
1136
|
+
return "greaterThan";
|
|
1137
|
+
case "lessthan":
|
|
1138
|
+
return "lessThan";
|
|
1139
|
+
case "greaterthanorequal":
|
|
1140
|
+
case "greaterthanorequalto":
|
|
1141
|
+
return "greaterThanOrEqual";
|
|
1142
|
+
case "lessthanorequal":
|
|
1143
|
+
case "lessthanorequalto":
|
|
1144
|
+
return "lessThanOrEqual";
|
|
1145
|
+
default:
|
|
1146
|
+
return undefined;
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
// OOXML `<formula1>` text never carries the leading `=` a formula-style
|
|
1150
|
+
// source (`=Sheet1!$A$1:$A$5`) has.
|
|
1151
|
+
const stripLeadingEquals = (formula) => {
|
|
1152
|
+
const trimmed = formula.trim();
|
|
1153
|
+
return trimmed.startsWith("=") ? trimmed.slice(1) : trimmed;
|
|
1154
|
+
};
|
|
1155
|
+
// One identifier token — `[A-Za-z_\\][A-Za-z0-9_.]*` — which is what a
|
|
1156
|
+
// defined name (`StatusList`, `My_List.v2`) or a single A1 cell (`A1`) looks
|
|
1157
|
+
// like; a word list with commas or spaces is not one.
|
|
1158
|
+
const XLSX_IDENTIFIER_TOKEN = /^[A-Za-z_\\][A-Za-z0-9_.]*$/;
|
|
1159
|
+
// A single A1 cell token: 1–3 letters followed by digits.
|
|
1160
|
+
const XLSX_A1_CELL_TOKEN = /^[A-Za-z]{1,3}[0-9]+$/;
|
|
1161
|
+
// A function call — `OFFSET(…)`, `INDIRECT(…)` — which Excel writes without
|
|
1162
|
+
// a leading `=`.
|
|
1163
|
+
const XLSX_FUNCTION_CALL = /^[A-Za-z_\\][A-Za-z0-9_.]*\(.*\)$/;
|
|
1164
|
+
// `true` when a list source reads as a reference / formula rather than as
|
|
1165
|
+
// list text: it carries a reference marker (`!` sheet qualifier, `:` range,
|
|
1166
|
+
// `$` absolute anchor, `[` structured / external ref), is a single A1 cell
|
|
1167
|
+
// or defined-name token, or is a function call. Anything else — comma lists,
|
|
1168
|
+
// text with spaces, digit-only entries — is a bare literal that Excel only
|
|
1169
|
+
// accepts quoted.
|
|
1170
|
+
const looksLikeXlsxListReference = (text) => text.length > 0 &&
|
|
1171
|
+
(/[!:$[]/.test(text) ||
|
|
1172
|
+
XLSX_A1_CELL_TOKEN.test(text) ||
|
|
1173
|
+
XLSX_IDENTIFIER_TOKEN.test(text) ||
|
|
1174
|
+
XLSX_FUNCTION_CALL.test(text));
|
|
1175
|
+
// A `list` source is valid OOXML only as a quoted string literal (`"a,b"`)
|
|
1176
|
+
// or a reference (`$A$1:$A$5`, `Sheet1!$A$1:$A$5`, `StatusList`, `OFFSET(…)`).
|
|
1177
|
+
// Excel's API hands back a literal list as bare comma-separated text and a
|
|
1178
|
+
// reference with a leading `=`, so: quoted → verbatim, `=`-prefixed → the `=`
|
|
1179
|
+
// is stripped and the rest replayed verbatim, reference-like → verbatim,
|
|
1180
|
+
// anything else → a literal that must be quoted (with `"` doubled inside, as
|
|
1181
|
+
// the ONE_OF_LIST branch does).
|
|
1182
|
+
const normalizeXlsxSourceListFormula1 = (formula1) => {
|
|
1183
|
+
const trimmed = formula1.trim();
|
|
1184
|
+
if (trimmed.startsWith('"'))
|
|
1185
|
+
return trimmed;
|
|
1186
|
+
if (trimmed.startsWith("="))
|
|
1187
|
+
return trimmed.slice(1).trim();
|
|
1188
|
+
if (looksLikeXlsxListReference(trimmed))
|
|
1189
|
+
return trimmed;
|
|
1190
|
+
return `"${escapeFormulaText(trimmed)}"`;
|
|
1191
|
+
};
|
|
1018
1192
|
const convertDataValidationRule = (rule) => {
|
|
1019
1193
|
if (!rule)
|
|
1020
1194
|
return undefined;
|
|
@@ -1032,20 +1206,58 @@ const convertDataValidationRule = (rule) => {
|
|
|
1032
1206
|
prompt: inputMessage?.message ?? undefined,
|
|
1033
1207
|
};
|
|
1034
1208
|
// Round-trip path: when the rule was imported from xlsx, replay the
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1209
|
+
// source representation after normalizing it to OOXML vocabulary (see
|
|
1210
|
+
// `normalizeXlsxSource*`). Preserves type/operator/formulas, including
|
|
1211
|
+
// `whole`/`textLength`/`time` types and user-written custom formulas
|
|
1212
|
+
// without `INDIRECT("RC",FALSE)` rewriting. A type / operator that is not
|
|
1213
|
+
// an OOXML token even after normalization (Excel's `Inconsistent`,
|
|
1214
|
+
// `MixedCriteria`) is never written: the rule falls through to the
|
|
1215
|
+
// condition synthesizer.
|
|
1038
1216
|
const src = rule.xlsxSource;
|
|
1217
|
+
let rejectedListSource;
|
|
1039
1218
|
if (src && src.type && typeof src.formula1 === "string" && src.formula1) {
|
|
1219
|
+
const type = normalizeXlsxSourceType(src.type);
|
|
1220
|
+
const operator = src.operator
|
|
1221
|
+
? normalizeXlsxSourceOperator(src.operator)
|
|
1222
|
+
: undefined;
|
|
1223
|
+
const operatorValid = !src.operator || operator !== undefined;
|
|
1224
|
+
if (type !== undefined && operatorValid) {
|
|
1225
|
+
const formula1 = type === "list"
|
|
1226
|
+
? normalizeXlsxSourceListFormula1(src.formula1)
|
|
1227
|
+
: stripLeadingEquals(src.formula1);
|
|
1228
|
+
return {
|
|
1229
|
+
type,
|
|
1230
|
+
operator,
|
|
1231
|
+
formula1,
|
|
1232
|
+
formula2: src.formula2 ? stripLeadingEquals(src.formula2) : undefined,
|
|
1233
|
+
showDropDown: type === "list" && rule.displayStyle === "plain" ? true : undefined,
|
|
1234
|
+
...baseConfig,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (type === "list") {
|
|
1238
|
+
rejectedListSource = normalizeXlsxSourceListFormula1(src.formula1);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const synthesized = convertDataValidationCondition(rule, baseConfig);
|
|
1242
|
+
if (synthesized)
|
|
1243
|
+
return synthesized;
|
|
1244
|
+
// A rejected `list` source with no usable condition is still kept — its
|
|
1245
|
+
// normalized source text (quoted literal or reference) is always valid
|
|
1246
|
+
// OOXML. Anything else rejected has nothing valid to write and is skipped.
|
|
1247
|
+
if (rejectedListSource !== undefined) {
|
|
1040
1248
|
return {
|
|
1041
|
-
type:
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
formula2: src.formula2,
|
|
1045
|
-
showDropDown: src.type === "list" && rule.displayStyle === "plain" ? true : undefined,
|
|
1249
|
+
type: "list",
|
|
1250
|
+
formula1: rejectedListSource,
|
|
1251
|
+
showDropDown: rule.displayStyle === "plain" ? true : undefined,
|
|
1046
1252
|
...baseConfig,
|
|
1047
1253
|
};
|
|
1048
1254
|
}
|
|
1255
|
+
return undefined;
|
|
1256
|
+
};
|
|
1257
|
+
// The condition-based synthesizer: the Excel config for a rule that has no
|
|
1258
|
+
// replayable `xlsxSource` (UI-authored, edited after import, or rejected by
|
|
1259
|
+
// the round-trip guard above).
|
|
1260
|
+
const convertDataValidationCondition = (rule, baseConfig) => {
|
|
1049
1261
|
if (!rule.condition?.type) {
|
|
1050
1262
|
// Prompt-only DV (no validation rule, just an input-message tooltip
|
|
1051
1263
|
// and/or an error alert). Emit it with no type/formula so Excel still
|
|
@@ -1058,7 +1270,6 @@ const convertDataValidationRule = (rule) => {
|
|
|
1058
1270
|
const { condition } = rule;
|
|
1059
1271
|
const values = condition.values ?? [];
|
|
1060
1272
|
const currentCell = 'INDIRECT("RC",FALSE)';
|
|
1061
|
-
const escapeFormulaText = (value) => value.replace(/"/g, '""');
|
|
1062
1273
|
const ensureFormula = (value) => value.startsWith("=") ? value : `=${value}`;
|
|
1063
1274
|
const ifErrorWrap = (formula) => `IFERROR(${formula},TRUE)`;
|
|
1064
1275
|
if (condition.type === "ONE_OF_LIST") {
|
|
@@ -2360,7 +2571,7 @@ const resolveTableColumnTotals = (column, totalsCell) => {
|
|
|
2360
2571
|
const label = getExtendedValueString(getCellUserEnteredValue(totalsCell));
|
|
2361
2572
|
return label && label.trim() !== "" ? { totalsRowLabel: label } : undefined;
|
|
2362
2573
|
};
|
|
2363
|
-
const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
2574
|
+
const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder, customTableStyleNames) => {
|
|
2364
2575
|
const rangeRef = table.ref;
|
|
2365
2576
|
const columnCount = Math.max(table.columns?.length ?? 0, (table.range.endColumnIndex ?? table.range.startColumnIndex) -
|
|
2366
2577
|
table.range.startColumnIndex +
|
|
@@ -2434,17 +2645,71 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
|
2434
2645
|
})
|
|
2435
2646
|
.join("")}</tableColumns>`;
|
|
2436
2647
|
const isBuiltInStyle = table.theme && /^TableStyle(Light|Medium|Dark)\d+$/.test(table.theme);
|
|
2437
|
-
|
|
2648
|
+
// A custom style written to `<tableStyles>` keeps its name here (#668 T-103).
|
|
2649
|
+
const isCustomStyle = !!table.theme && (customTableStyleNames?.has(table.theme) ?? false);
|
|
2650
|
+
const tableStyleName = isBuiltInStyle || isCustomStyle ? table.theme : "TableStyleMedium2";
|
|
2438
2651
|
const tableStyleInfo = `<tableStyleInfo name="${tableStyleName}" showFirstColumn="${table.showFirstColumn ? 1 : 0}" showLastColumn="${table.showLastColumn ? 1 : 0}" showRowStripes="${table.showRowStripes ? 1 : 0}" showColumnStripes="${table.showColumnStripes ? 1 : 0}"/>`;
|
|
2652
|
+
// Excel's per-column "filter button" toggle: `<filterColumn hiddenButton="1">`
|
|
2653
|
+
// (the table-level toggle is the presence of `<autoFilter>` itself).
|
|
2654
|
+
const hiddenButtonColumns = new Set(columnNames
|
|
2655
|
+
.map((_, idx) => idx)
|
|
2656
|
+
.filter((idx) => table.columns?.[idx]?.filterButton === false));
|
|
2439
2657
|
const autoFilter = headerRowCount > 0 && table.filterButton !== false
|
|
2440
2658
|
? (() => {
|
|
2441
|
-
const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder);
|
|
2659
|
+
const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder, hiddenButtonColumns);
|
|
2442
2660
|
return filterColumns
|
|
2443
2661
|
? `<autoFilter ref="${rangeRef}">${filterColumns}</autoFilter>`
|
|
2444
2662
|
: `<autoFilter ref="${rangeRef}"/>`;
|
|
2445
2663
|
})()
|
|
2446
2664
|
: "";
|
|
2447
|
-
|
|
2665
|
+
const sortState = buildTableSortStateXml(table, headerRowCount, totalsRowCount, styleBuilder);
|
|
2666
|
+
return `${XML_DECLARATION}<table xmlns="${WORKSHEET_NS}" id="${table.id}" name="${escapeXmlAttr(table.name)}" displayName="${escapeXmlAttr(table.displayName)}" ref="${rangeRef}" totalsRowCount="${totalsRowCount}" headerRowCount="${headerRowCount}">${autoFilter}${sortState}${tableColumns}${tableStyleInfo}</table>`;
|
|
2667
|
+
};
|
|
2668
|
+
/**
|
|
2669
|
+
* A table's persisted sort levels as `<sortState ref="<body>">` with one
|
|
2670
|
+
* `<sortCondition>` per level (`CT_SortCondition`: `ref` is the level's
|
|
2671
|
+
* column over the body rows, `descending`, `sortBy` + `dxfId` for a colour
|
|
2672
|
+
* level, `sortBy="icon"` + `iconSet`/`iconId`, and Excel's comma-joined
|
|
2673
|
+
* `customList`). Sits between `<autoFilter>` and `<tableColumns>` per
|
|
2674
|
+
* `CT_Table`. The inverse of the parser's `createSortSpecsFromSortState`.
|
|
2675
|
+
*/
|
|
2676
|
+
const buildTableSortStateXml = (table, headerRowCount, totalsRowCount, styleBuilder) => {
|
|
2677
|
+
const sortSpecs = table.sortSpecs ?? [];
|
|
2678
|
+
const bodyStartRow = table.range.startRowIndex + headerRowCount;
|
|
2679
|
+
const bodyEndRow = table.range.endRowIndex - totalsRowCount;
|
|
2680
|
+
if (!sortSpecs.length || bodyEndRow < bodyStartRow)
|
|
2681
|
+
return "";
|
|
2682
|
+
const bodyRef = (startColumnIndex, endColumnIndex) => cellRangeToAddress(undefined, {
|
|
2683
|
+
startRowIndex: bodyStartRow,
|
|
2684
|
+
endRowIndex: bodyEndRow,
|
|
2685
|
+
startColumnIndex,
|
|
2686
|
+
endColumnIndex,
|
|
2687
|
+
});
|
|
2688
|
+
const conditions = sortSpecs.map((spec) => {
|
|
2689
|
+
const columnIndex = table.range.startColumnIndex + spec.dimensionIndex;
|
|
2690
|
+
const attrs = [`ref="${bodyRef(columnIndex, columnIndex)}"`];
|
|
2691
|
+
if (spec.sortOrder === "DESCENDING")
|
|
2692
|
+
attrs.push(`descending="1"`);
|
|
2693
|
+
const sortBy = spec.sortBy ?? "value";
|
|
2694
|
+
if (sortBy === "cellColor" || sortBy === "fontColor") {
|
|
2695
|
+
attrs.push(`sortBy="${sortBy}"`);
|
|
2696
|
+
const dxfId = spec.sortColor && styleBuilder
|
|
2697
|
+
? styleBuilder.registerDifferentialFormat(sortBy === "cellColor"
|
|
2698
|
+
? { backgroundColor: spec.sortColor }
|
|
2699
|
+
: { textFormat: { color: spec.sortColor } })
|
|
2700
|
+
: undefined;
|
|
2701
|
+
if (typeof dxfId === "number")
|
|
2702
|
+
attrs.push(`dxfId="${dxfId}"`);
|
|
2703
|
+
}
|
|
2704
|
+
else if (sortBy === "icon" && spec.sortIcon) {
|
|
2705
|
+
attrs.push(`sortBy="icon"`, `iconSet="${escapeXmlAttr(spec.sortIcon.iconSet)}"`, `iconId="${spec.sortIcon.iconId ?? 0}"`);
|
|
2706
|
+
}
|
|
2707
|
+
if (spec.customList?.length) {
|
|
2708
|
+
attrs.push(`customList="${escapeXmlAttr(spec.customList.join(","))}"`);
|
|
2709
|
+
}
|
|
2710
|
+
return `<sortCondition ${attrs.join(" ")}/>`;
|
|
2711
|
+
});
|
|
2712
|
+
return `<sortState ref="${bodyRef(table.range.startColumnIndex, table.range.endColumnIndex)}">${conditions.join("")}</sortState>`;
|
|
2448
2713
|
};
|
|
2449
2714
|
const getFilterVisibleValues = (spec, range, rows, headerRow = true, totalRow = false) => {
|
|
2450
2715
|
const values = new Set();
|
|
@@ -2615,6 +2880,12 @@ const xmlNumAttr = (name, value) => typeof value === "number" && Number.isFinite
|
|
|
2615
2880
|
* mandatory in practice (Excel always writes it), so the default margins go
|
|
2616
2881
|
* out when the sheet has none of its own.
|
|
2617
2882
|
*/
|
|
2883
|
+
const buildPageMarginsXml = (print) => {
|
|
2884
|
+
const margins = print?.pageMargins;
|
|
2885
|
+
return margins && Object.values(margins).some((v) => typeof v === "number")
|
|
2886
|
+
? `<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}"/>`
|
|
2887
|
+
: DEFAULT_PAGE_MARGINS;
|
|
2888
|
+
};
|
|
2618
2889
|
const buildPrintSettingsXml = (print) => {
|
|
2619
2890
|
const parts = [];
|
|
2620
2891
|
const options = print?.printOptions;
|
|
@@ -2624,11 +2895,7 @@ const buildPrintSettingsXml = (print) => {
|
|
|
2624
2895
|
xmlBoolAttr("gridLines", options?.gridLines);
|
|
2625
2896
|
if (optionAttrs)
|
|
2626
2897
|
parts.push(`<printOptions${optionAttrs}/>`);
|
|
2627
|
-
|
|
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);
|
|
2898
|
+
parts.push(buildPageMarginsXml(print));
|
|
2632
2899
|
const setup = print?.pageSetup;
|
|
2633
2900
|
if (setup) {
|
|
2634
2901
|
// Excel keeps `firstPageNumber` as a dormant value until the user picks
|
|
@@ -2693,13 +2960,27 @@ const buildPrintSettingsXml = (print) => {
|
|
|
2693
2960
|
}
|
|
2694
2961
|
return parts.join("");
|
|
2695
2962
|
};
|
|
2696
|
-
const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
const entries =
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2963
|
+
const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder,
|
|
2964
|
+
/** Table-relative column ids whose header hides its filter button. */
|
|
2965
|
+
hiddenButtonColumns) => {
|
|
2966
|
+
const entries = new Map();
|
|
2967
|
+
for (const spec of filterSpecs ?? []) {
|
|
2968
|
+
const xml = buildFilterColumnXml(spec, range, rows, headerRow, totalRow, styleBuilder);
|
|
2969
|
+
if (xml)
|
|
2970
|
+
entries.set(spec.columnIndex, xml);
|
|
2971
|
+
}
|
|
2972
|
+
// `hiddenButton="1"` rides the column's criteria element when it has one,
|
|
2973
|
+
// else a bare `<filterColumn/>` carries it.
|
|
2974
|
+
hiddenButtonColumns?.forEach((colId) => {
|
|
2975
|
+
const existing = entries.get(colId);
|
|
2976
|
+
entries.set(colId, existing
|
|
2977
|
+
? existing.replace(`<filterColumn colId="${colId}">`, `<filterColumn colId="${colId}" hiddenButton="1">`)
|
|
2978
|
+
: `<filterColumn colId="${colId}" hiddenButton="1"/>`);
|
|
2979
|
+
});
|
|
2980
|
+
return [...entries.entries()]
|
|
2981
|
+
.sort(([a], [b]) => a - b)
|
|
2982
|
+
.map(([, xml]) => xml)
|
|
2983
|
+
.join("");
|
|
2703
2984
|
};
|
|
2704
2985
|
const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
|
|
2705
2986
|
const parts = [];
|
|
@@ -2728,7 +3009,9 @@ const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = fa
|
|
|
2728
3009
|
// above-average …) takes precedence over a value-list and is emitted as
|
|
2729
3010
|
// <customFilters> or <dynamicFilter>. Falls back to the value list when the
|
|
2730
3011
|
// condition has no AutoFilter representation.
|
|
2731
|
-
const conditionXml =
|
|
3012
|
+
const conditionXml = spec.filterCriteria
|
|
3013
|
+
? buildAutoFilterCriteriaXml(spec.filterCriteria)
|
|
3014
|
+
: null;
|
|
2732
3015
|
if (conditionXml) {
|
|
2733
3016
|
parts.push(conditionXml);
|
|
2734
3017
|
}
|
|
@@ -2758,9 +3041,9 @@ const gridRangeToRef = (range) => {
|
|
|
2758
3041
|
return undefined;
|
|
2759
3042
|
}
|
|
2760
3043
|
};
|
|
2761
|
-
const groupEmbedsBySheet = (embeds) => {
|
|
3044
|
+
const groupEmbedsBySheet = (embeds, chartCount) => {
|
|
2762
3045
|
const result = new Map();
|
|
2763
|
-
(embeds ?? []).forEach((embed,
|
|
3046
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2764
3047
|
if (!embed.position?.overlayPosition)
|
|
2765
3048
|
return;
|
|
2766
3049
|
if (embed.spec?.type !== "image")
|
|
@@ -2779,13 +3062,14 @@ const groupEmbedsBySheet = (embeds) => {
|
|
|
2779
3062
|
adjustments: Object.values(adjustments).some((v) => v !== undefined)
|
|
2780
3063
|
? adjustments
|
|
2781
3064
|
: undefined,
|
|
2782
|
-
order,
|
|
3065
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2783
3066
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2784
3067
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2785
3068
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
2786
3069
|
offsetY: overlay.offsetYPixels ?? 0,
|
|
2787
3070
|
width: overlay.widthPixels ?? 120,
|
|
2788
3071
|
height: overlay.heightPixels ?? 120,
|
|
3072
|
+
overlay,
|
|
2789
3073
|
};
|
|
2790
3074
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2791
3075
|
list.push(placement);
|
|
@@ -2793,9 +3077,9 @@ const groupEmbedsBySheet = (embeds) => {
|
|
|
2793
3077
|
});
|
|
2794
3078
|
return result;
|
|
2795
3079
|
};
|
|
2796
|
-
const groupTextBoxesBySheet = (embeds) => {
|
|
3080
|
+
const groupTextBoxesBySheet = (embeds, chartCount) => {
|
|
2797
3081
|
const result = new Map();
|
|
2798
|
-
(embeds ?? []).forEach((embed,
|
|
3082
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2799
3083
|
if (!embed.position?.overlayPosition)
|
|
2800
3084
|
return;
|
|
2801
3085
|
if (embed.spec?.type !== "textbox")
|
|
@@ -2803,7 +3087,7 @@ const groupTextBoxesBySheet = (embeds) => {
|
|
|
2803
3087
|
const overlay = embed.position.overlayPosition;
|
|
2804
3088
|
const placement = {
|
|
2805
3089
|
textBox: embed,
|
|
2806
|
-
order,
|
|
3090
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2807
3091
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2808
3092
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2809
3093
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2818,9 +3102,9 @@ const groupTextBoxesBySheet = (embeds) => {
|
|
|
2818
3102
|
return result;
|
|
2819
3103
|
};
|
|
2820
3104
|
/** Form-control embeds (`spec.type === "formControl"`) by sheet, in host order. */
|
|
2821
|
-
const groupFormControlsBySheet = (embeds) => {
|
|
3105
|
+
const groupFormControlsBySheet = (embeds, chartCount) => {
|
|
2822
3106
|
const result = new Map();
|
|
2823
|
-
(embeds ?? []).forEach((embed,
|
|
3107
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2824
3108
|
if (!embed.position?.overlayPosition)
|
|
2825
3109
|
return;
|
|
2826
3110
|
if (embed.spec?.type !== "formControl")
|
|
@@ -2828,7 +3112,7 @@ const groupFormControlsBySheet = (embeds) => {
|
|
|
2828
3112
|
const overlay = embed.position.overlayPosition;
|
|
2829
3113
|
const placement = {
|
|
2830
3114
|
shape: embed,
|
|
2831
|
-
order,
|
|
3115
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2832
3116
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2833
3117
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2834
3118
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2883,18 +3167,33 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
|
|
|
2883
3167
|
const heightPx = Math.max(5, placement.height ?? 20);
|
|
2884
3168
|
const columnSize = (index) => getColumnWidthPx(sheet, index, true);
|
|
2885
3169
|
const rowSize = (index) => getRowHeightPx(sheet, index, true);
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3170
|
+
// A "Move and size" control keeps its persisted second anchor (#668 C-46)
|
|
3171
|
+
const overlay = placement.shape.position?.overlayPosition;
|
|
3172
|
+
const persistedTo = controlAnchorFlags(spec, overlay).sizeWithCells && overlay?.toCell
|
|
3173
|
+
? overlay.toCell
|
|
3174
|
+
: undefined;
|
|
3175
|
+
const toColumn = persistedTo
|
|
3176
|
+
? {
|
|
3177
|
+
index: Math.max(0, persistedTo.columnIndex - 1),
|
|
3178
|
+
offset: Math.max(0, overlay?.toOffsetXPixels ?? 0),
|
|
3179
|
+
}
|
|
3180
|
+
: resolveAnchorEndpoint({
|
|
3181
|
+
startIndex: fromColumn,
|
|
3182
|
+
startOffset: offsetX,
|
|
3183
|
+
span: widthPx,
|
|
3184
|
+
getSize: columnSize,
|
|
3185
|
+
});
|
|
3186
|
+
const toRow = persistedTo
|
|
3187
|
+
? {
|
|
3188
|
+
index: Math.max(0, persistedTo.rowIndex - 1),
|
|
3189
|
+
offset: Math.max(0, overlay?.toOffsetYPixels ?? 0),
|
|
3190
|
+
}
|
|
3191
|
+
: resolveAnchorEndpoint({
|
|
3192
|
+
startIndex: fromRow,
|
|
3193
|
+
startOffset: offsetY,
|
|
3194
|
+
span: heightPx,
|
|
3195
|
+
getSize: rowSize,
|
|
3196
|
+
});
|
|
2898
3197
|
const partIndex = ctrlProps.parts.length + 1;
|
|
2899
3198
|
const path = `xl/ctrlProps/ctrlProp${partIndex}.xml`;
|
|
2900
3199
|
ctrlProps.parts.push({ path, xml: buildCtrlPropXml(spec) });
|
|
@@ -2916,11 +3215,13 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
|
|
|
2916
3215
|
row: toRow.index,
|
|
2917
3216
|
rowOff: toRow.offset * EMUS_PER_PIXEL,
|
|
2918
3217
|
},
|
|
3218
|
+
overlay,
|
|
2919
3219
|
}));
|
|
2920
3220
|
vmlShapes.push(buildFormControlVmlShape({
|
|
2921
3221
|
spec,
|
|
2922
3222
|
shapeId,
|
|
2923
3223
|
zIndex: zIndexStart + position + 1,
|
|
3224
|
+
overlay,
|
|
2924
3225
|
geometry: {
|
|
2925
3226
|
leftPx: sumSizes(fromColumn, columnSize) + offsetX,
|
|
2926
3227
|
topPx: sumSizes(fromRow, rowSize) + offsetY,
|
|
@@ -2941,9 +3242,9 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
|
|
|
2941
3242
|
});
|
|
2942
3243
|
return { controlsXml: buildControlsXml(controlElements), vmlShapes };
|
|
2943
3244
|
};
|
|
2944
|
-
const groupShapesBySheet = (embeds) => {
|
|
3245
|
+
const groupShapesBySheet = (embeds, chartCount) => {
|
|
2945
3246
|
const result = new Map();
|
|
2946
|
-
(embeds ?? []).forEach((embed,
|
|
3247
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2947
3248
|
if (!embed.position?.overlayPosition)
|
|
2948
3249
|
return;
|
|
2949
3250
|
if (embed.spec?.type !== "shape")
|
|
@@ -2951,7 +3252,7 @@ const groupShapesBySheet = (embeds) => {
|
|
|
2951
3252
|
const overlay = embed.position.overlayPosition;
|
|
2952
3253
|
const placement = {
|
|
2953
3254
|
shape: embed,
|
|
2954
|
-
order,
|
|
3255
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2955
3256
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2956
3257
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2957
3258
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2967,12 +3268,13 @@ const groupShapesBySheet = (embeds) => {
|
|
|
2967
3268
|
};
|
|
2968
3269
|
const groupChartsBySheet = (charts) => {
|
|
2969
3270
|
const result = new Map();
|
|
2970
|
-
|
|
3271
|
+
(charts ?? []).forEach((chart, index) => {
|
|
2971
3272
|
const overlay = chart.position?.overlayPosition;
|
|
2972
3273
|
if (!overlay)
|
|
2973
|
-
|
|
3274
|
+
return;
|
|
2974
3275
|
const placement = {
|
|
2975
3276
|
chart,
|
|
3277
|
+
order: chart.position.zOrder ?? index,
|
|
2976
3278
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2977
3279
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2978
3280
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2983,7 +3285,7 @@ const groupChartsBySheet = (charts) => {
|
|
|
2983
3285
|
const list = result.get(chart.position.sheetId) ?? [];
|
|
2984
3286
|
list.push(placement);
|
|
2985
3287
|
result.set(chart.position.sheetId, list);
|
|
2986
|
-
}
|
|
3288
|
+
});
|
|
2987
3289
|
return result;
|
|
2988
3290
|
};
|
|
2989
3291
|
const groupSlicersBySheet = (slicers) => {
|
|
@@ -3554,7 +3856,7 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
|
|
|
3554
3856
|
rules.push(dataBarXml);
|
|
3555
3857
|
return rules.length ? rules : undefined;
|
|
3556
3858
|
};
|
|
3557
|
-
const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, }) => {
|
|
3859
|
+
const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, resolvePivotSourceName, }) => {
|
|
3558
3860
|
if (!placements.length &&
|
|
3559
3861
|
!textBoxes.length &&
|
|
3560
3862
|
!shapes.length &&
|
|
@@ -3571,6 +3873,12 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3571
3873
|
let frameId = 1;
|
|
3572
3874
|
const chartEntries = [];
|
|
3573
3875
|
const chartExEntries = [];
|
|
3876
|
+
// Charts and embeds (text boxes, shapes, images) are written back-to-front
|
|
3877
|
+
// by their shared z-order — anchor order IS Excel's z-order, so a picture
|
|
3878
|
+
// sent behind a chart must be emitted before it regardless of kind.
|
|
3879
|
+
// Placements without an order (in-cell images) go last, in their own order.
|
|
3880
|
+
const orderedAnchors = [];
|
|
3881
|
+
const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
|
|
3574
3882
|
for (const placement of charts) {
|
|
3575
3883
|
if (isChartExType(placement.chart.spec.chartType)) {
|
|
3576
3884
|
// Treemap / sunburst live in a chartex part with an mc:AlternateContent anchor.
|
|
@@ -3578,7 +3886,16 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3578
3886
|
if (!chartExXml)
|
|
3579
3887
|
continue;
|
|
3580
3888
|
const relId = relationships.add(CHART_EX_REL_TYPE, `../charts/chartEx${chartExIndex}.xml`);
|
|
3581
|
-
|
|
3889
|
+
orderedAnchors.push({
|
|
3890
|
+
order: placement.order,
|
|
3891
|
+
xml: createChartAnchor({
|
|
3892
|
+
sheet,
|
|
3893
|
+
placement,
|
|
3894
|
+
relId,
|
|
3895
|
+
frameId,
|
|
3896
|
+
chartEx: true,
|
|
3897
|
+
}),
|
|
3898
|
+
});
|
|
3582
3899
|
chartExEntries.push({
|
|
3583
3900
|
path: `xl/charts/chartEx${chartExIndex}.xml`,
|
|
3584
3901
|
xml: chartExXml,
|
|
@@ -3587,16 +3904,19 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3587
3904
|
frameId += 1;
|
|
3588
3905
|
continue;
|
|
3589
3906
|
}
|
|
3590
|
-
const chartXml = buildChartXml(placement.chart, sheetNameById);
|
|
3907
|
+
const chartXml = buildChartXml(placement.chart, sheetNameById, resolvePivotSourceName);
|
|
3591
3908
|
if (!chartXml)
|
|
3592
3909
|
continue;
|
|
3593
3910
|
const relId = relationships.add(CHART_REL_TYPE, `../charts/chart${chartIndex}.xml`);
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3911
|
+
orderedAnchors.push({
|
|
3912
|
+
order: placement.order,
|
|
3913
|
+
xml: createChartAnchor({
|
|
3914
|
+
sheet,
|
|
3915
|
+
placement,
|
|
3916
|
+
relId,
|
|
3917
|
+
frameId,
|
|
3918
|
+
}),
|
|
3919
|
+
});
|
|
3600
3920
|
chartEntries.push({
|
|
3601
3921
|
path: `xl/charts/chart${chartIndex}.xml`,
|
|
3602
3922
|
xml: chartXml,
|
|
@@ -3612,14 +3932,8 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3612
3932
|
}));
|
|
3613
3933
|
frameId += 1;
|
|
3614
3934
|
}
|
|
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;
|
|
3621
3935
|
for (const placement of textBoxes) {
|
|
3622
|
-
|
|
3936
|
+
orderedAnchors.push({
|
|
3623
3937
|
order: orderOf(placement.order),
|
|
3624
3938
|
xml: createTextBoxAnchor({
|
|
3625
3939
|
sheet,
|
|
@@ -3630,7 +3944,7 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3630
3944
|
frameId += 1;
|
|
3631
3945
|
}
|
|
3632
3946
|
for (const placement of shapes) {
|
|
3633
|
-
|
|
3947
|
+
orderedAnchors.push({
|
|
3634
3948
|
order: orderOf(placement.order),
|
|
3635
3949
|
xml: createShapeAnchor({
|
|
3636
3950
|
sheet,
|
|
@@ -3654,7 +3968,7 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3654
3968
|
extension: buffer.extension,
|
|
3655
3969
|
});
|
|
3656
3970
|
const relId = relationships.add(IMAGE_REL_TYPE, `../media/image${imageIndex}.${buffer.extension}`);
|
|
3657
|
-
|
|
3971
|
+
orderedAnchors.push({
|
|
3658
3972
|
order: orderOf(placement.order),
|
|
3659
3973
|
xml: createTwoCellAnchor({
|
|
3660
3974
|
sheet,
|
|
@@ -3666,8 +3980,8 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3666
3980
|
imageIndex += 1;
|
|
3667
3981
|
pictureId += 1;
|
|
3668
3982
|
}
|
|
3669
|
-
|
|
3670
|
-
for (const entry of
|
|
3983
|
+
orderedAnchors.sort((a, b) => a.order - b.order);
|
|
3984
|
+
for (const entry of orderedAnchors)
|
|
3671
3985
|
anchors.push(entry.xml);
|
|
3672
3986
|
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>`;
|
|
3673
3987
|
return {
|
|
@@ -3690,25 +4004,21 @@ const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
|
|
|
3690
4004
|
const defaultHeight = placement.kind === "cell" ? getRowHeightPx(sheet, fromRow) : 120;
|
|
3691
4005
|
const widthPx = Math.max(5, placement.width ?? defaultWidth);
|
|
3692
4006
|
const heightPx = Math.max(5, placement.height ?? defaultHeight);
|
|
3693
|
-
const toColumn = resolveAnchorEndpoint({
|
|
3694
|
-
startIndex: fromColumn,
|
|
3695
|
-
startOffset: offsetX,
|
|
3696
|
-
span: widthPx,
|
|
3697
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3698
|
-
});
|
|
3699
|
-
const toRow = resolveAnchorEndpoint({
|
|
3700
|
-
startIndex: fromRow,
|
|
3701
|
-
startOffset: offsetY,
|
|
3702
|
-
span: heightPx,
|
|
3703
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3704
|
-
});
|
|
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>`;
|
|
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>`;
|
|
3707
4007
|
const { blipChildren, srcRectXml } = buildPictureAdjustmentsXml(placement.adjustments);
|
|
3708
4008
|
const blipXml = blipChildren
|
|
3709
4009
|
? `<a:blip r:embed="${relId}">${blipChildren}</a:blip>`
|
|
3710
4010
|
: `<a:blip r:embed="${relId}"/>`;
|
|
3711
|
-
return
|
|
4011
|
+
return buildDrawingAnchorXml({
|
|
4012
|
+
sheet,
|
|
4013
|
+
overlay: placement.overlay,
|
|
4014
|
+
fromRow,
|
|
4015
|
+
fromColumn,
|
|
4016
|
+
offsetX,
|
|
4017
|
+
offsetY,
|
|
4018
|
+
widthPx,
|
|
4019
|
+
heightPx,
|
|
4020
|
+
body: `<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>`,
|
|
4021
|
+
});
|
|
3712
4022
|
};
|
|
3713
4023
|
/** Fraction (0…1) → DrawingML per-mille percent (100000 = 100%). */
|
|
3714
4024
|
const fractionToPct = (value) => Math.round(Math.max(-1, Math.min(1, value)) * 100000);
|
|
@@ -3759,29 +4069,30 @@ const createChartAnchor = ({ placement, relId, frameId, sheet, chartEx = false,
|
|
|
3759
4069
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3760
4070
|
const widthPx = Math.max(5, placement.width ?? 300);
|
|
3761
4071
|
const heightPx = Math.max(5, placement.height ?? 200);
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
startOffset: offsetX,
|
|
3765
|
-
span: widthPx,
|
|
3766
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3767
|
-
});
|
|
3768
|
-
const toRow = resolveAnchorEndpoint({
|
|
3769
|
-
startIndex: fromRow,
|
|
3770
|
-
startOffset: offsetY,
|
|
3771
|
-
span: heightPx,
|
|
3772
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3773
|
-
});
|
|
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>`;
|
|
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({
|
|
4072
|
+
const body = chartEx
|
|
4073
|
+
? buildChartExGraphicFrameXml({
|
|
3778
4074
|
relId,
|
|
3779
4075
|
frameId,
|
|
3780
4076
|
widthEmu: Math.round(widthPx * EMUS_PER_PIXEL),
|
|
3781
4077
|
heightEmu: Math.round(heightPx * EMUS_PER_PIXEL),
|
|
3782
|
-
})
|
|
3783
|
-
|
|
3784
|
-
|
|
4078
|
+
})
|
|
4079
|
+
: `<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>`;
|
|
4080
|
+
const overlay = placement.chart.position?.overlayPosition;
|
|
4081
|
+
return buildDrawingAnchorXml({
|
|
4082
|
+
sheet,
|
|
4083
|
+
// A chartsheet has no cells to anchor to: Excel only reads an
|
|
4084
|
+
// `absoluteAnchor` there, whatever placement the chart carries.
|
|
4085
|
+
overlay: sheet.type === "chartsheet" && overlay
|
|
4086
|
+
? { ...overlay, placement: "absolute" }
|
|
4087
|
+
: overlay,
|
|
4088
|
+
fromRow,
|
|
4089
|
+
fromColumn,
|
|
4090
|
+
offsetX,
|
|
4091
|
+
offsetY,
|
|
4092
|
+
widthPx,
|
|
4093
|
+
heightPx,
|
|
4094
|
+
body,
|
|
4095
|
+
});
|
|
3785
4096
|
};
|
|
3786
4097
|
const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
|
|
3787
4098
|
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
@@ -3790,21 +4101,42 @@ const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3790
4101
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3791
4102
|
const widthPx = Math.max(50, placement.width ?? 200);
|
|
3792
4103
|
const heightPx = Math.max(50, placement.height ?? 200);
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
const
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4104
|
+
// Excel keeps a slicer in a `twoCellAnchor` whatever its placement and
|
|
4105
|
+
// spells the other two modes as `editAs` (#668 C-46); a "Move and size"
|
|
4106
|
+
// slicer keeps its persisted second anchor.
|
|
4107
|
+
const overlay = placement.overlay;
|
|
4108
|
+
const mode = overlay?.placement ?? "moveAndSize";
|
|
4109
|
+
const persistedTo = mode === "moveAndSize" ? overlay?.toCell : undefined;
|
|
4110
|
+
const editAs = mode === "moveOnly"
|
|
4111
|
+
? ' editAs="oneCell"'
|
|
4112
|
+
: mode === "absolute"
|
|
4113
|
+
? ' editAs="absolute"'
|
|
4114
|
+
: "";
|
|
4115
|
+
const toColumn = persistedTo
|
|
4116
|
+
? {
|
|
4117
|
+
index: Math.max(0, persistedTo.columnIndex - 1),
|
|
4118
|
+
offset: Math.max(0, overlay?.toOffsetXPixels ?? 0),
|
|
4119
|
+
}
|
|
4120
|
+
: resolveAnchorEndpoint({
|
|
4121
|
+
startIndex: fromColumn,
|
|
4122
|
+
startOffset: offsetX,
|
|
4123
|
+
span: widthPx,
|
|
4124
|
+
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
4125
|
+
});
|
|
4126
|
+
const toRow = persistedTo
|
|
4127
|
+
? {
|
|
4128
|
+
index: Math.max(0, persistedTo.rowIndex - 1),
|
|
4129
|
+
offset: Math.max(0, overlay?.toOffsetYPixels ?? 0),
|
|
4130
|
+
}
|
|
4131
|
+
: resolveAnchorEndpoint({
|
|
4132
|
+
startIndex: fromRow,
|
|
4133
|
+
startOffset: offsetY,
|
|
4134
|
+
span: heightPx,
|
|
4135
|
+
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
4136
|
+
});
|
|
3805
4137
|
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>`;
|
|
3806
4138
|
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>`;
|
|
3807
|
-
return `<xdr:twoCellAnchor
|
|
4139
|
+
return `<xdr:twoCellAnchor${editAs}>${fromXml}${toXml}<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:sle15="http://schemas.microsoft.com/office/drawing/2012/slicer" Requires="sle15"><xdr:graphicFrame macro=""><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="${escapeXmlAttr(placement.title)}"/><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.microsoft.com/office/drawing/2010/slicer"><sle:slicer xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" name="${escapeXmlAttr(placement.title)}"/></a:graphicData></a:graphic></xdr:graphicFrame></mc:Choice></mc:AlternateContent><xdr:clientData/></xdr:twoCellAnchor>`;
|
|
3808
4140
|
};
|
|
3809
4141
|
const createTextBoxAnchor = ({ placement, frameId, sheet, }) => {
|
|
3810
4142
|
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
@@ -3813,24 +4145,20 @@ const createTextBoxAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3813
4145
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3814
4146
|
const widthPx = Math.max(50, placement.width ?? 200);
|
|
3815
4147
|
const heightPx = Math.max(50, placement.height ?? 60);
|
|
3816
|
-
const toColumn = resolveAnchorEndpoint({
|
|
3817
|
-
startIndex: fromColumn,
|
|
3818
|
-
startOffset: offsetX,
|
|
3819
|
-
span: widthPx,
|
|
3820
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3821
|
-
});
|
|
3822
|
-
const toRow = resolveAnchorEndpoint({
|
|
3823
|
-
startIndex: fromRow,
|
|
3824
|
-
startOffset: offsetY,
|
|
3825
|
-
span: heightPx,
|
|
3826
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3827
|
-
});
|
|
3828
|
-
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>`;
|
|
3829
|
-
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>`;
|
|
3830
4148
|
const spec = placement.textBox.spec;
|
|
3831
4149
|
const textBodyXml = buildTextBodyXml(spec);
|
|
3832
4150
|
const shapePropertiesXml = buildShapePropertiesXml(spec, widthPx, heightPx);
|
|
3833
|
-
return
|
|
4151
|
+
return buildDrawingAnchorXml({
|
|
4152
|
+
sheet,
|
|
4153
|
+
overlay: placement.textBox.position?.overlayPosition,
|
|
4154
|
+
fromRow,
|
|
4155
|
+
fromColumn,
|
|
4156
|
+
offsetX,
|
|
4157
|
+
offsetY,
|
|
4158
|
+
widthPx,
|
|
4159
|
+
heightPx,
|
|
4160
|
+
body: `<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="TextBox ${frameId}"/><xdr:cNvSpPr txBox="1"/></xdr:nvSpPr>${shapePropertiesXml}${textBodyXml}</xdr:sp>`,
|
|
4161
|
+
});
|
|
3834
4162
|
};
|
|
3835
4163
|
/** ColorIndex slots → canonical DrawingML scheme names (inverse of the importer's map). */
|
|
3836
4164
|
const THEME_INDEX_TO_SCHEME = [
|
|
@@ -3948,20 +4276,6 @@ const createShapeAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3948
4276
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3949
4277
|
const widthPx = Math.max(10, placement.width ?? 120);
|
|
3950
4278
|
const heightPx = Math.max(10, placement.height ?? 60);
|
|
3951
|
-
const toColumn = resolveAnchorEndpoint({
|
|
3952
|
-
startIndex: fromColumn,
|
|
3953
|
-
startOffset: offsetX,
|
|
3954
|
-
span: widthPx,
|
|
3955
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3956
|
-
});
|
|
3957
|
-
const toRow = resolveAnchorEndpoint({
|
|
3958
|
-
startIndex: fromRow,
|
|
3959
|
-
startOffset: offsetY,
|
|
3960
|
-
span: heightPx,
|
|
3961
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3962
|
-
});
|
|
3963
|
-
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>`;
|
|
3964
|
-
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>`;
|
|
3965
4279
|
const xfrmAttrs = [
|
|
3966
4280
|
placement.shape.rotation
|
|
3967
4281
|
? ` rot="${Math.round(placement.shape.rotation * 60000)}"`
|
|
@@ -3988,7 +4302,17 @@ const createShapeAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3988
4302
|
? buildTextBodyXml(spec.textBody)
|
|
3989
4303
|
: `<xdr:txBody><a:bodyPr vertOverflow="clip" horzOverflow="clip" rtlCol="0" anchor="t"/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></xdr:txBody>`;
|
|
3990
4304
|
const name = escapeXmlAttr(spec.name ?? `Shape ${frameId}`);
|
|
3991
|
-
return
|
|
4305
|
+
return buildDrawingAnchorXml({
|
|
4306
|
+
sheet,
|
|
4307
|
+
overlay: placement.shape.position?.overlayPosition,
|
|
4308
|
+
fromRow,
|
|
4309
|
+
fromColumn,
|
|
4310
|
+
offsetX,
|
|
4311
|
+
offsetY,
|
|
4312
|
+
widthPx,
|
|
4313
|
+
heightPx,
|
|
4314
|
+
body: `<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="${name}"/><xdr:cNvSpPr/></xdr:nvSpPr>${spPr}${styleXml}${textBodyXml}</xdr:sp>`,
|
|
4315
|
+
});
|
|
3992
4316
|
};
|
|
3993
4317
|
const buildTextWithTabs = (text, rPrXml) => {
|
|
3994
4318
|
const parts = text.split("\t");
|
|
@@ -4239,6 +4563,60 @@ const buildShapePropertiesXml = (spec, widthPx, heightPx) => {
|
|
|
4239
4563
|
}
|
|
4240
4564
|
return `<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>${fillXml}${outlineXml}</xdr:spPr>`;
|
|
4241
4565
|
};
|
|
4566
|
+
/**
|
|
4567
|
+
* Wrap a drawing object's body in the anchor its placement mode calls for
|
|
4568
|
+
* (#668 C-06). "Move and size with cells" (Excel's default) is a
|
|
4569
|
+
* `twoCellAnchor` whose `to` is the object's persisted second anchor — or,
|
|
4570
|
+
* for an object that has none yet, the cell its pixel extent reaches over
|
|
4571
|
+
* this sheet's row / column sizes. "Move but don't size" is a
|
|
4572
|
+
* `oneCellAnchor` (`from` + `ext`); "Don't move or size" is an
|
|
4573
|
+
* `absoluteAnchor` (`pos` + `ext`). The importers read all three back into
|
|
4574
|
+
* the same placement.
|
|
4575
|
+
*/
|
|
4576
|
+
const buildDrawingAnchorXml = ({ sheet, overlay, fromRow, fromColumn, offsetX, offsetY, widthPx, heightPx, body, }) => {
|
|
4577
|
+
const emu = (px) => Math.round(px * EMUS_PER_PIXEL);
|
|
4578
|
+
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${emu(offsetX)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${emu(offsetY)}</xdr:rowOff></xdr:from>`;
|
|
4579
|
+
const extXml = `<xdr:ext cx="${emu(widthPx)}" cy="${emu(heightPx)}"/>`;
|
|
4580
|
+
const placement = overlay?.placement ?? "moveAndSize";
|
|
4581
|
+
if (placement === "moveOnly") {
|
|
4582
|
+
return `<xdr:oneCellAnchor>${fromXml}${extXml}${body}<xdr:clientData/></xdr:oneCellAnchor>`;
|
|
4583
|
+
}
|
|
4584
|
+
if (placement === "absolute") {
|
|
4585
|
+
let x = offsetX;
|
|
4586
|
+
for (let index = 0; index < fromColumn; index++) {
|
|
4587
|
+
x += getColumnWidthPx(sheet, index, true);
|
|
4588
|
+
}
|
|
4589
|
+
let y = offsetY;
|
|
4590
|
+
for (let index = 0; index < fromRow; index++) {
|
|
4591
|
+
y += getRowHeightPx(sheet, index, true);
|
|
4592
|
+
}
|
|
4593
|
+
return `<xdr:absoluteAnchor><xdr:pos x="${emu(x)}" y="${emu(y)}"/>${extXml}${body}<xdr:clientData/></xdr:absoluteAnchor>`;
|
|
4594
|
+
}
|
|
4595
|
+
const toColumn = overlay?.toCell
|
|
4596
|
+
? {
|
|
4597
|
+
index: Math.max(0, overlay.toCell.columnIndex - 1),
|
|
4598
|
+
offset: Math.max(0, overlay.toOffsetXPixels ?? 0),
|
|
4599
|
+
}
|
|
4600
|
+
: resolveAnchorEndpoint({
|
|
4601
|
+
startIndex: fromColumn,
|
|
4602
|
+
startOffset: offsetX,
|
|
4603
|
+
span: widthPx,
|
|
4604
|
+
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
4605
|
+
});
|
|
4606
|
+
const toRow = overlay?.toCell
|
|
4607
|
+
? {
|
|
4608
|
+
index: Math.max(0, overlay.toCell.rowIndex - 1),
|
|
4609
|
+
offset: Math.max(0, overlay.toOffsetYPixels ?? 0),
|
|
4610
|
+
}
|
|
4611
|
+
: resolveAnchorEndpoint({
|
|
4612
|
+
startIndex: fromRow,
|
|
4613
|
+
startOffset: offsetY,
|
|
4614
|
+
span: heightPx,
|
|
4615
|
+
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
4616
|
+
});
|
|
4617
|
+
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${emu(toColumn.offset)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${emu(toRow.offset)}</xdr:rowOff></xdr:to>`;
|
|
4618
|
+
return `<xdr:twoCellAnchor>${fromXml}${toXml}${body}<xdr:clientData/></xdr:twoCellAnchor>`;
|
|
4619
|
+
};
|
|
4242
4620
|
const resolveAnchorEndpoint = ({ startIndex, startOffset, span, getSize, }) => {
|
|
4243
4621
|
let index = startIndex;
|
|
4244
4622
|
let offset = Math.max(0, startOffset);
|
|
@@ -4548,15 +4926,18 @@ const buildWorkbookSlicerCachesXml = (slicerCacheRelIds) => {
|
|
|
4548
4926
|
.join("");
|
|
4549
4927
|
return `<extLst><ext uri="{46BE6895-7355-4a93-B00E-2C351335B9C9}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"><x15:slicerCaches xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">${cacheEntries}</x15:slicerCaches></ext></extLst>`;
|
|
4550
4928
|
};
|
|
4551
|
-
const buildWorkbookRelationships = (
|
|
4929
|
+
const buildWorkbookRelationships = (
|
|
4930
|
+
/** One entry per sheet in `<sheets>` order; `target` is relative to `xl/`. */
|
|
4931
|
+
sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, hasPersons = false) => {
|
|
4552
4932
|
const relationships = [];
|
|
4553
4933
|
const sheetRelIds = [];
|
|
4554
4934
|
const slicerCacheRelIds = [];
|
|
4555
|
-
|
|
4935
|
+
const sheetCount = sheetParts.length;
|
|
4936
|
+
sheetParts.forEach((part, i) => {
|
|
4556
4937
|
const relId = `rId${i + 1}`;
|
|
4557
4938
|
sheetRelIds.push(relId);
|
|
4558
|
-
relationships.push(`<Relationship Id="${relId}" Type="${REL_NS}
|
|
4559
|
-
}
|
|
4939
|
+
relationships.push(`<Relationship Id="${relId}" Type="${REL_NS}/${part.chartsheet ? "chartsheet" : "worksheet"}" Target="${part.target}"/>`);
|
|
4940
|
+
});
|
|
4560
4941
|
const stylesId = `rId${sheetCount + 1}`;
|
|
4561
4942
|
const themeId = `rId${sheetCount + 2}`;
|
|
4562
4943
|
const sharedStringsId = `rId${sheetCount + 3}`;
|
|
@@ -4600,10 +4981,11 @@ const buildWorkbookRelationships = (sheetCount, slicerCacheCount = 0, externalLi
|
|
|
4600
4981
|
slicerCacheRelationshipIds: slicerCacheRelIds,
|
|
4601
4982
|
};
|
|
4602
4983
|
};
|
|
4603
|
-
const buildContentTypesXml = ({
|
|
4604
|
-
const sheetOverrides = Array.from({ length:
|
|
4984
|
+
const buildContentTypesXml = ({ worksheetCount, chartsheetCount = 0, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
|
|
4985
|
+
const sheetOverrides = Array.from({ length: worksheetCount }, (_, index) => {
|
|
4605
4986
|
return `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
|
4606
4987
|
}).join("");
|
|
4988
|
+
const chartsheetOverrides = Array.from({ length: chartsheetCount }, (_, index) => `<Override PartName="/xl/chartsheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"/>`).join("");
|
|
4607
4989
|
const tableOverrides = Array.from({ length: tableCount }, (_, index) => {
|
|
4608
4990
|
return `<Override PartName="/xl/tables/table${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"/>`;
|
|
4609
4991
|
}).join("");
|
|
@@ -4644,13 +5026,25 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
|
|
|
4644
5026
|
? `<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>`
|
|
4645
5027
|
: "";
|
|
4646
5028
|
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>`;
|
|
5029
|
+
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}${chartsheetOverrides}${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>`;
|
|
4648
5030
|
};
|
|
4649
5031
|
const buildRootRelationshipsXml = () => {
|
|
4650
5032
|
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>`;
|
|
4651
5033
|
};
|
|
4652
|
-
const buildAppXml = (sheetNames, properties) => {
|
|
4653
|
-
|
|
5034
|
+
const buildAppXml = (sheetNames, isChartsheet, properties) => {
|
|
5035
|
+
// Excel groups the tabs by kind: `HeadingPairs` counts `Worksheets` then
|
|
5036
|
+
// `Charts` (a pair is only written for a non-empty kind) and `TitlesOfParts`
|
|
5037
|
+
// lists the worksheet names followed by the chartsheet names.
|
|
5038
|
+
const worksheetNames = sheetNames.filter((_, index) => !isChartsheet[index]);
|
|
5039
|
+
const chartsheetNames = sheetNames.filter((_, index) => isChartsheet[index]);
|
|
5040
|
+
const headingPairs = [
|
|
5041
|
+
["Worksheets", worksheetNames.length],
|
|
5042
|
+
["Charts", chartsheetNames.length],
|
|
5043
|
+
].filter(([, count]) => count > 0);
|
|
5044
|
+
const headingPairsXml = headingPairs
|
|
5045
|
+
.map(([kind, count]) => `<vt:variant><vt:lpstr>${kind}</vt:lpstr></vt:variant><vt:variant><vt:i4>${count}</vt:i4></vt:variant>`)
|
|
5046
|
+
.join("");
|
|
5047
|
+
const titles = [...worksheetNames, ...chartsheetNames]
|
|
4654
5048
|
.map((name) => `<vt:lpstr>${escapeXmlText(name)}</vt:lpstr>`)
|
|
4655
5049
|
.join("");
|
|
4656
5050
|
const application = properties?.application ?? CREATOR;
|
|
@@ -4660,7 +5054,7 @@ const buildAppXml = (sheetNames, properties) => {
|
|
|
4660
5054
|
const managerTag = properties?.manager
|
|
4661
5055
|
? `<Manager>${escapeXmlText(properties.manager)}</Manager>`
|
|
4662
5056
|
: "";
|
|
4663
|
-
return `${XML_DECLARATION}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${escapeXmlText(application)}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="2" baseType="variant"
|
|
5057
|
+
return `${XML_DECLARATION}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${escapeXmlText(application)}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="${headingPairs.length * 2}" baseType="variant">${headingPairsXml}</vt:vector></HeadingPairs><TitlesOfParts><vt:vector size="${sheetNames.length}" baseType="lpstr">${titles}</vt:vector></TitlesOfParts>${companyTag}${managerTag}</Properties>`;
|
|
4664
5058
|
};
|
|
4665
5059
|
const buildCoreXml = (properties) => {
|
|
4666
5060
|
const now = new Date().toISOString();
|