@rowsncolumns/toolkit 24.4.1 → 24.4.3
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 +10 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- 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 +11 -3
- package/dist/cjs/excel-parser/plugins/xls/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xls/index.js +55 -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 +21 -9
- 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/index.d.ts +26 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +567 -234
- 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.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/styles.js +12 -2
- package/dist/cjs/excel-parser/registry.d.ts.map +1 -1
- package/dist/cjs/excel-parser/registry.js +2 -0
- package/dist/cjs/excel-parser/types.d.ts +7 -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 +4 -0
- package/dist/cjs/excel-parser/worker.js +3 -1
- 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 +210 -6
- 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 +678 -222
- 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 +10 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- 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 +11 -3
- package/dist/esm/excel-parser/plugins/xls/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xls/index.js +37 -5
- 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 +20 -6
- 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/index.d.ts +26 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +362 -26
- 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.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/styles.js +12 -2
- package/dist/esm/excel-parser/registry.d.ts.map +1 -1
- package/dist/esm/excel-parser/registry.js +2 -0
- package/dist/esm/excel-parser/types.d.ts +7 -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 +3 -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 +306 -35
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +161 -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 +449 -47
- 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
|
@@ -3,16 +3,18 @@ import { zipSync } from "fflate";
|
|
|
3
3
|
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
4
4
|
import { buildAutoFilterConditionXml } from "../../autofilter-condition";
|
|
5
5
|
import { applyPowerQueryParts } from "./power-query-builder";
|
|
6
|
+
import { encodeImplicitIntersection } from "../../helpers/implicit-intersection";
|
|
6
7
|
import { qualifyFutureFunctions } from "./future-functions";
|
|
7
8
|
import { PERSON_CONTENT_TYPE, PERSON_REL_TYPE, PersonRegistry, THREADED_COMMENT_REL_TYPE, THREADED_COMMENTS_CONTENT_TYPE, ThreadedCommentsCollector, } from "./threaded-comments-builder";
|
|
8
9
|
import { CFBEncryptor } from "../../excel-parser/cfb-encryptor";
|
|
9
10
|
import { columnPixelToChars, imageUrlToBase64 } from "../../helpers";
|
|
11
|
+
import { buildPrintAreaFormula, buildPrintTitlesFormula, isBuiltinPrintName, isHeaderFooterEmpty, PRINT_AREA_NAME, PRINT_TITLES_NAME, } from "../../print-settings";
|
|
10
12
|
import { generateThemeXml } from "../../helpers/theme-xml-generator";
|
|
11
13
|
import theme1Xml from "../../theme1";
|
|
12
14
|
import { tokenize as defaultTokenizer } from "@rowsncolumns/calculator";
|
|
13
15
|
import { CellXfsRegistry, deriveEffectiveNumberFormatFromFormula, } from "@rowsncolumns/spreadsheet-state";
|
|
14
16
|
import { buildChartXml } from "./chart-builder";
|
|
15
|
-
import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, } from "./pivot-builder";
|
|
17
|
+
import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, calculatedFieldReferences, } from "./pivot-builder";
|
|
16
18
|
import { buildSlicerCacheXml, buildSlicerXml } from "./slicer-builder";
|
|
17
19
|
import { buildSparklineGroupsXml } from "./sparkline-builder";
|
|
18
20
|
import { StyleBuilder } from "./style-builder";
|
|
@@ -21,7 +23,7 @@ const isStyleReference = (value) => !!value && typeof value === "object" && "sid
|
|
|
21
23
|
const DEFAULT_COLUMN_WIDTH_PX = 100;
|
|
22
24
|
const DEFAULT_ROW_HEIGHT_PX = 21;
|
|
23
25
|
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, }) => {
|
|
26
|
+
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
27
|
const sortedSheets = sortSheetsByIndex(sheets, true);
|
|
26
28
|
const sharedStrings = new SharedStrings();
|
|
27
29
|
// Workbook-scoped author registry shared across every sheet's threaded
|
|
@@ -183,12 +185,26 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
183
185
|
fieldIndex.set(name, fieldNameList.length);
|
|
184
186
|
fieldNameList.push(name);
|
|
185
187
|
};
|
|
188
|
+
const calculatedNames = new Set((pivot.calculatedFields ?? []).map((c) => c.name));
|
|
186
189
|
for (const r of pivot.rows ?? [])
|
|
187
190
|
registerField(r.field);
|
|
188
191
|
for (const c of pivot.columns ?? [])
|
|
189
192
|
registerField(c.field);
|
|
190
|
-
for (const v of pivot.values ?? [])
|
|
191
|
-
|
|
193
|
+
for (const v of pivot.values ?? []) {
|
|
194
|
+
if (!calculatedNames.has(v.field))
|
|
195
|
+
registerField(v.field);
|
|
196
|
+
}
|
|
197
|
+
// Source fields a calculated field reads must exist in the cache
|
|
198
|
+
// for Excel to evaluate the formula; the calculated fields
|
|
199
|
+
// themselves come last, as Excel orders them.
|
|
200
|
+
for (const c of pivot.calculatedFields ?? []) {
|
|
201
|
+
for (const ref of calculatedFieldReferences(c.formula)) {
|
|
202
|
+
if (!calculatedNames.has(ref))
|
|
203
|
+
registerField(ref);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const c of pivot.calculatedFields ?? [])
|
|
207
|
+
registerField(c.name);
|
|
192
208
|
const cacheXml = buildPivotCacheDefinitionXml(pivot, sourceSheetName, fieldNameList);
|
|
193
209
|
const tableXml = buildPivotTableXml(pivot, cacheIndex, fieldIndex);
|
|
194
210
|
if (!cacheXml || !tableXml)
|
|
@@ -233,6 +249,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
233
249
|
sheetData,
|
|
234
250
|
sheets: sortedSheets,
|
|
235
251
|
locale,
|
|
252
|
+
protectedRanges,
|
|
236
253
|
});
|
|
237
254
|
return {
|
|
238
255
|
index,
|
|
@@ -643,7 +660,7 @@ const getRowsForSheet = (sheetData, sheetId) => {
|
|
|
643
660
|
sheetData[Number(sheetId)] ??
|
|
644
661
|
[]);
|
|
645
662
|
};
|
|
646
|
-
const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, tables, relationships, drawingContext, conditionalFormats, dataValidationsById, dataValidations, ignoreFormulaPatterns, citations, comments, personRegistry, slicerRelIds, sheetData, sheets, locale, }) => {
|
|
663
|
+
const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, tables, relationships, drawingContext, conditionalFormats, dataValidationsById, dataValidations, ignoreFormulaPatterns, citations, comments, personRegistry, slicerRelIds, sheetData, sheets, locale, protectedRanges, }) => {
|
|
647
664
|
const tableNameMap = buildTableNameMap(tables);
|
|
648
665
|
const bounds = {
|
|
649
666
|
minRow: Number.POSITIVE_INFINITY,
|
|
@@ -813,7 +830,8 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
813
830
|
legacyDrawingXml = `<legacyDrawing r:id="${legacyDrawingRelId}"/>`;
|
|
814
831
|
}
|
|
815
832
|
const sheetDataXml = `<sheetData>${rowXmlChunks.join("")}</sheetData>`;
|
|
816
|
-
const
|
|
833
|
+
const protectionXml = buildSheetProtectionXml(sheet, protectedRanges);
|
|
834
|
+
const printSettingsXml = buildPrintSettingsXml(sheet.printSettings);
|
|
817
835
|
// Build row data map for sparkline detection
|
|
818
836
|
const rowDataMap = {};
|
|
819
837
|
for (let rowIndex = 1; rowIndex < maxRows; rowIndex++) {
|
|
@@ -847,7 +865,7 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
847
865
|
explicitDefaultColumnWidthPx > 0) {
|
|
848
866
|
sheetFormatPrAttrs.push(`defaultColWidth="${columnPixelToChars(explicitDefaultColumnWidthPx).toFixed(2)}"`);
|
|
849
867
|
}
|
|
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}${
|
|
868
|
+
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}${tablePartsXml}${extLstXml}</worksheet>`;
|
|
851
869
|
return {
|
|
852
870
|
xml: worksheetXml,
|
|
853
871
|
relationshipsXml: relationships.toXml(),
|
|
@@ -1814,7 +1832,9 @@ function rewriteStructuredReferenceFormula(formula, tableNameMap, tables, rowNum
|
|
|
1814
1832
|
}
|
|
1815
1833
|
function sanitizeFormulaText(formula) {
|
|
1816
1834
|
const normalized = sanitizeXmlText(formula);
|
|
1817
|
-
|
|
1835
|
+
// `@x` is persisted as `_xlfn.SINGLE(x)`; encoding first lets the
|
|
1836
|
+
// future-function pass add the prefix.
|
|
1837
|
+
return qualifyFutureFunctions(encodeImplicitIntersection(normalized.replace(/\r\n|\r|\n/g, " ")));
|
|
1818
1838
|
}
|
|
1819
1839
|
function escapeXmlBase(value) {
|
|
1820
1840
|
return sanitizeXmlText(value)
|
|
@@ -1976,6 +1996,11 @@ const buildSheetViewsXml = (sheet) => {
|
|
|
1976
1996
|
zoomScale !== 100) {
|
|
1977
1997
|
attrs.push(`zoomScale="${zoomScale}"`);
|
|
1978
1998
|
}
|
|
1999
|
+
// `rightToLeft` — only an explicit true is written (Excel's default is
|
|
2000
|
+
// LTR and the attribute is omitted), mirroring the Rust exporter.
|
|
2001
|
+
if (sheet.rightToLeft) {
|
|
2002
|
+
attrs.push(`rightToLeft="1"`);
|
|
2003
|
+
}
|
|
1979
2004
|
const panes = [];
|
|
1980
2005
|
const frozenRows = sheet.frozenRowCount ?? 0;
|
|
1981
2006
|
const frozenColumns = sheet.frozenColumnCount ?? 0;
|
|
@@ -2230,6 +2255,57 @@ const sanitizeTableName = (name) => {
|
|
|
2230
2255
|
}
|
|
2231
2256
|
return cleaned;
|
|
2232
2257
|
};
|
|
2258
|
+
/**
|
|
2259
|
+
* `SUBTOTAL()` function numbers (the 101+ "ignore hidden rows" family Excel
|
|
2260
|
+
* writes into totals-row cells) → `totalsRowFunction`.
|
|
2261
|
+
*/
|
|
2262
|
+
const SUBTOTAL_TOTALS_ROW_FUNCTIONS = {
|
|
2263
|
+
"101": "average",
|
|
2264
|
+
"102": "countNums",
|
|
2265
|
+
"103": "count",
|
|
2266
|
+
"104": "max",
|
|
2267
|
+
"105": "min",
|
|
2268
|
+
"107": "stdDev",
|
|
2269
|
+
"109": "sum",
|
|
2270
|
+
"110": "var",
|
|
2271
|
+
};
|
|
2272
|
+
/**
|
|
2273
|
+
* Totals-row metadata for one `<tableColumn>`. Explicit column metadata
|
|
2274
|
+
* (imported from Excel, or set by the host) wins — an explicit `none` means
|
|
2275
|
+
* "no totals function" and is not second-guessed from the cell (the Rust
|
|
2276
|
+
* exporter's `totals_for_column` agrees). Otherwise it is derived from the
|
|
2277
|
+
* totals-row cell so tables whose totals row was added in-app (a "Total"
|
|
2278
|
+
* label + `=SUBTOTAL(109,[Col])` per numeric column) open in Excel with the
|
|
2279
|
+
* matching function selected in the totals dropdown. A formula that is not
|
|
2280
|
+
* a plain `SUBTOTAL(n, ref)` is left to the cell.
|
|
2281
|
+
*/
|
|
2282
|
+
const resolveTableColumnTotals = (column, totalsCell) => {
|
|
2283
|
+
if (column?.totalsRowLabel) {
|
|
2284
|
+
return { totalsRowLabel: column.totalsRowLabel };
|
|
2285
|
+
}
|
|
2286
|
+
if (column?.totalsRowFunction) {
|
|
2287
|
+
if (column.totalsRowFunction === "none") {
|
|
2288
|
+
return undefined;
|
|
2289
|
+
}
|
|
2290
|
+
if (column.totalsRowFunction === "custom" && !column.totalsRowFormula) {
|
|
2291
|
+
return undefined;
|
|
2292
|
+
}
|
|
2293
|
+
return {
|
|
2294
|
+
totalsRowFunction: column.totalsRowFunction,
|
|
2295
|
+
totalsRowFormula: column.totalsRowFormula ?? undefined,
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
if (!totalsCell)
|
|
2299
|
+
return undefined;
|
|
2300
|
+
const formula = getExtendedValueFormula(getCellUserEnteredValue(totalsCell));
|
|
2301
|
+
if (formula) {
|
|
2302
|
+
const match = /^=?\s*SUBTOTAL\(\s*(\d{3})\s*,[^,]*\)\s*$/i.exec(formula);
|
|
2303
|
+
const fn = match ? SUBTOTAL_TOTALS_ROW_FUNCTIONS[match[1]] : undefined;
|
|
2304
|
+
return fn ? { totalsRowFunction: fn } : undefined;
|
|
2305
|
+
}
|
|
2306
|
+
const label = getExtendedValueString(getCellUserEnteredValue(totalsCell));
|
|
2307
|
+
return label && label.trim() !== "" ? { totalsRowLabel: label } : undefined;
|
|
2308
|
+
};
|
|
2233
2309
|
const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
2234
2310
|
const rangeRef = table.ref;
|
|
2235
2311
|
const columnCount = Math.max(table.columns?.length ?? 0, (table.range.endColumnIndex ?? table.range.startColumnIndex) -
|
|
@@ -2275,6 +2351,7 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
|
2275
2351
|
// Fallback to table.columns name or generic name
|
|
2276
2352
|
return table.columns?.[index]?.name ?? `Column${index + 1}`;
|
|
2277
2353
|
});
|
|
2354
|
+
const totalsRowIndex = table.range.endRowIndex;
|
|
2278
2355
|
const tableColumns = `<tableColumns count="${columnCount}">${columnNames
|
|
2279
2356
|
.map((name, idx) => {
|
|
2280
2357
|
const columnFormula = table.columns?.[idx]?.formula;
|
|
@@ -2284,7 +2361,22 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
|
2284
2361
|
const formulaXml = normalizedFormula
|
|
2285
2362
|
? `<calculatedColumnFormula>${escapeXmlText(stripFormula(normalizedFormula))}</calculatedColumnFormula>`
|
|
2286
2363
|
: "";
|
|
2287
|
-
|
|
2364
|
+
// Explicit per-column totals metadata exports whether or not the totals
|
|
2365
|
+
// row is shown: Excel keeps it on a table whose totals row is toggled
|
|
2366
|
+
// off so re-enabling restores each column's choice. Deriving it from
|
|
2367
|
+
// the last row only applies when that row IS the totals row.
|
|
2368
|
+
const totals = resolveTableColumnTotals(table.columns?.[idx], totalsRowCount > 0
|
|
2369
|
+
? rows[totalsRowIndex]?.values?.[table.range.startColumnIndex + idx]
|
|
2370
|
+
: undefined);
|
|
2371
|
+
const totalsAttrs = totals?.totalsRowLabel
|
|
2372
|
+
? ` totalsRowLabel="${escapeXmlAttr(totals.totalsRowLabel)}"`
|
|
2373
|
+
: totals?.totalsRowFunction
|
|
2374
|
+
? ` totalsRowFunction="${totals.totalsRowFunction}"`
|
|
2375
|
+
: "";
|
|
2376
|
+
const totalsFormulaXml = totals?.totalsRowFunction === "custom" && totals.totalsRowFormula
|
|
2377
|
+
? `<totalsRowFormula>${escapeXmlText(stripFormula(normalizeCalculatedColumnFormula(totals.totalsRowFormula, table)))}</totalsRowFormula>`
|
|
2378
|
+
: "";
|
|
2379
|
+
return `<tableColumn id="${idx + 1}" name="${escapeXmlAttr(name)}"${totalsAttrs}>${formulaXml}${totalsFormulaXml}</tableColumn>`;
|
|
2288
2380
|
})
|
|
2289
2381
|
.join("")}</tableColumns>`;
|
|
2290
2382
|
const isBuiltInStyle = table.theme && /^TableStyle(Light|Medium|Dark)\d+$/.test(table.theme);
|
|
@@ -2332,6 +2424,106 @@ const buildWorksheetAutoFilterXml = (sheet, rows, styleBuilder) => {
|
|
|
2332
2424
|
}
|
|
2333
2425
|
return `<autoFilter ref="${ref}"/>`;
|
|
2334
2426
|
};
|
|
2427
|
+
/** `CT_SheetProtection` boolean attributes in Excel's attribute order. */
|
|
2428
|
+
const SHEET_PROTECTION_FLAG_ATTRS = [
|
|
2429
|
+
"sheet",
|
|
2430
|
+
"objects",
|
|
2431
|
+
"scenarios",
|
|
2432
|
+
"formatCells",
|
|
2433
|
+
"formatColumns",
|
|
2434
|
+
"formatRows",
|
|
2435
|
+
"insertColumns",
|
|
2436
|
+
"insertRows",
|
|
2437
|
+
"insertHyperlinks",
|
|
2438
|
+
"deleteColumns",
|
|
2439
|
+
"deleteRows",
|
|
2440
|
+
"selectLockedCells",
|
|
2441
|
+
"sort",
|
|
2442
|
+
"autoFilter",
|
|
2443
|
+
"pivotTables",
|
|
2444
|
+
"selectUnlockedCells",
|
|
2445
|
+
];
|
|
2446
|
+
/**
|
|
2447
|
+
* `<sheetProtection …/>` from `Sheet.sheetProtection`: the password hash first
|
|
2448
|
+
* (legacy `password` or the agile quartet, verbatim), then only the action
|
|
2449
|
+
* flags the model carries (`true` → `"1"`, `false` → `"0"`, absent → omitted so
|
|
2450
|
+
* Excel's schema default applies). Mirrors `rnc-export`'s `sheet_protection_xml`.
|
|
2451
|
+
*/
|
|
2452
|
+
const buildSheetProtectionElementXml = (protection) => {
|
|
2453
|
+
const attrs = [];
|
|
2454
|
+
if (protection.algorithmName) {
|
|
2455
|
+
attrs.push(`algorithmName="${escapeXmlAttr(protection.algorithmName)}"`);
|
|
2456
|
+
}
|
|
2457
|
+
if (protection.hashValue) {
|
|
2458
|
+
attrs.push(`hashValue="${escapeXmlAttr(protection.hashValue)}"`);
|
|
2459
|
+
}
|
|
2460
|
+
if (protection.saltValue) {
|
|
2461
|
+
attrs.push(`saltValue="${escapeXmlAttr(protection.saltValue)}"`);
|
|
2462
|
+
}
|
|
2463
|
+
if (protection.spinCount != null) {
|
|
2464
|
+
attrs.push(`spinCount="${protection.spinCount}"`);
|
|
2465
|
+
}
|
|
2466
|
+
if (protection.password) {
|
|
2467
|
+
attrs.push(`password="${escapeXmlAttr(protection.password)}"`);
|
|
2468
|
+
}
|
|
2469
|
+
for (const flag of SHEET_PROTECTION_FLAG_ATTRS) {
|
|
2470
|
+
const value = protection[flag];
|
|
2471
|
+
if (typeof value === "boolean") {
|
|
2472
|
+
attrs.push(`${flag}="${value ? 1 : 0}"`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return attrs.length ? `<sheetProtection ${attrs.join(" ")}/>` : "";
|
|
2476
|
+
};
|
|
2477
|
+
/**
|
|
2478
|
+
* The sheet's protection block — `<sheetProtection>` followed by
|
|
2479
|
+
* `<protectedRanges>` — or `""` when the sheet is unprotected and has no
|
|
2480
|
+
* allow-edit ranges. Ranges missing a bound (a sheet-wide `protect-sheet`
|
|
2481
|
+
* entry only carries `sheetId`) have no `sqref` and are skipped; the `name`
|
|
2482
|
+
* is the range's id, synthesized as `Range{n}` when absent (schema requires it).
|
|
2483
|
+
*/
|
|
2484
|
+
const buildSheetProtectionXml = (sheet, protectedRanges) => {
|
|
2485
|
+
const protectionXml = sheet.sheetProtection
|
|
2486
|
+
? buildSheetProtectionElementXml(sheet.sheetProtection)
|
|
2487
|
+
: "";
|
|
2488
|
+
const rangeItems = [];
|
|
2489
|
+
for (const protectedRange of protectedRanges ?? []) {
|
|
2490
|
+
const range = protectedRange.range;
|
|
2491
|
+
if (range.sheetId !== sheet.sheetId)
|
|
2492
|
+
continue;
|
|
2493
|
+
if (range.startRowIndex == null ||
|
|
2494
|
+
range.endRowIndex == null ||
|
|
2495
|
+
range.startColumnIndex == null ||
|
|
2496
|
+
range.endColumnIndex == null) {
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
// Excel writes a single cell as `D10`, a block as `A2:C5`.
|
|
2500
|
+
const isSingleCell = range.startRowIndex === range.endRowIndex &&
|
|
2501
|
+
range.startColumnIndex === range.endColumnIndex;
|
|
2502
|
+
const sqref = isSingleCell
|
|
2503
|
+
? cellToAddress({
|
|
2504
|
+
rowIndex: range.startRowIndex,
|
|
2505
|
+
columnIndex: range.startColumnIndex,
|
|
2506
|
+
})
|
|
2507
|
+
: cellRangeToAddress(undefined, {
|
|
2508
|
+
startRowIndex: range.startRowIndex,
|
|
2509
|
+
endRowIndex: range.endRowIndex,
|
|
2510
|
+
startColumnIndex: range.startColumnIndex,
|
|
2511
|
+
endColumnIndex: range.endColumnIndex,
|
|
2512
|
+
});
|
|
2513
|
+
if (!sqref)
|
|
2514
|
+
continue;
|
|
2515
|
+
const id = protectedRange.protectedRangeId;
|
|
2516
|
+
const name = id != null && String(id) !== "" ? String(id) : `Range${rangeItems.length + 1}`;
|
|
2517
|
+
const securityDescriptor = protectedRange.securityDescriptor
|
|
2518
|
+
? ` securityDescriptor="${escapeXmlAttr(protectedRange.securityDescriptor)}"`
|
|
2519
|
+
: "";
|
|
2520
|
+
rangeItems.push(`<protectedRange sqref="${escapeXmlAttr(sqref)}" name="${escapeXmlAttr(name)}"${securityDescriptor}/>`);
|
|
2521
|
+
}
|
|
2522
|
+
const rangesXml = rangeItems.length
|
|
2523
|
+
? `<protectedRanges>${rangeItems.join("")}</protectedRanges>`
|
|
2524
|
+
: "";
|
|
2525
|
+
return `${protectionXml}${rangesXml}`;
|
|
2526
|
+
};
|
|
2335
2527
|
const buildSheetPropertiesXml = (sheet) => {
|
|
2336
2528
|
const tabColor = serializeColorXml(sheet.tabColor ?? undefined);
|
|
2337
2529
|
// <outlinePr> mirrors the sheet's `outlinePr` setting; both attrs
|
|
@@ -2348,11 +2540,105 @@ const buildSheetPropertiesXml = (sheet) => {
|
|
|
2348
2540
|
const outlineXml = outlineAttrs.length
|
|
2349
2541
|
? `<outlinePr ${outlineAttrs.join(" ")}/>`
|
|
2350
2542
|
: "";
|
|
2351
|
-
|
|
2543
|
+
// `<pageSetUpPr fitToPage="1">` is what makes Excel honour
|
|
2544
|
+
// `<pageSetup fitToWidth/fitToHeight>` instead of `scale`.
|
|
2545
|
+
const pageSetUpPrXml = sheet.printSettings?.pageSetup?.fitToPage
|
|
2546
|
+
? `<pageSetUpPr fitToPage="1"/>`
|
|
2547
|
+
: "";
|
|
2548
|
+
if (!tabColor && !outlineXml && !pageSetUpPrXml)
|
|
2352
2549
|
return "";
|
|
2353
|
-
const inner = `${tabColor ? `<tabColor ${tabColor}/>` : ""}${outlineXml}`;
|
|
2550
|
+
const inner = `${tabColor ? `<tabColor ${tabColor}/>` : ""}${outlineXml}${pageSetUpPrXml}`;
|
|
2354
2551
|
return `<sheetPr>${inner}</sheetPr>`;
|
|
2355
2552
|
};
|
|
2553
|
+
const xmlBoolAttr = (name, value) => value == null ? "" : ` ${name}="${value ? "1" : "0"}"`;
|
|
2554
|
+
const xmlNumAttr = (name, value) => typeof value === "number" && Number.isFinite(value)
|
|
2555
|
+
? ` ${name}="${value}"`
|
|
2556
|
+
: "";
|
|
2557
|
+
/**
|
|
2558
|
+
* The `CT_Worksheet` print tail — `<printOptions>`, `<pageMargins>`,
|
|
2559
|
+
* `<pageSetup>`, `<headerFooter>`, `<rowBreaks>`, `<colBreaks>` — in schema
|
|
2560
|
+
* order (after `<hyperlinks>`, before `<drawing>`). `<pageMargins>` is
|
|
2561
|
+
* mandatory in practice (Excel always writes it), so the default margins go
|
|
2562
|
+
* out when the sheet has none of its own.
|
|
2563
|
+
*/
|
|
2564
|
+
const buildPrintSettingsXml = (print) => {
|
|
2565
|
+
const parts = [];
|
|
2566
|
+
const options = print?.printOptions;
|
|
2567
|
+
const optionAttrs = xmlBoolAttr("horizontalCentered", options?.horizontalCentered) +
|
|
2568
|
+
xmlBoolAttr("verticalCentered", options?.verticalCentered) +
|
|
2569
|
+
xmlBoolAttr("headings", options?.headings) +
|
|
2570
|
+
xmlBoolAttr("gridLines", options?.gridLines);
|
|
2571
|
+
if (optionAttrs)
|
|
2572
|
+
parts.push(`<printOptions${optionAttrs}/>`);
|
|
2573
|
+
const margins = print?.pageMargins;
|
|
2574
|
+
parts.push(margins &&
|
|
2575
|
+
Object.values(margins).some((v) => typeof v === "number")
|
|
2576
|
+
? `<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}"/>`
|
|
2577
|
+
: DEFAULT_PAGE_MARGINS);
|
|
2578
|
+
const setup = print?.pageSetup;
|
|
2579
|
+
if (setup) {
|
|
2580
|
+
// Excel keeps `firstPageNumber` as a dormant value until the user picks
|
|
2581
|
+
// a first page number (`useFirstPageNumber="1"`); a bare number means
|
|
2582
|
+
// "Auto", so the flag is written only when the model has it on.
|
|
2583
|
+
const useFirst = setup.useFirstPageNumber === true ? true : undefined;
|
|
2584
|
+
const setupAttrs = xmlNumAttr("paperSize", setup.paperSize) +
|
|
2585
|
+
xmlNumAttr("scale", setup.scale) +
|
|
2586
|
+
xmlNumAttr("firstPageNumber", setup.firstPageNumber) +
|
|
2587
|
+
xmlNumAttr("fitToWidth", setup.fitToWidth) +
|
|
2588
|
+
xmlNumAttr("fitToHeight", setup.fitToHeight) +
|
|
2589
|
+
(setup.pageOrder ? ` pageOrder="${setup.pageOrder}"` : "") +
|
|
2590
|
+
(setup.orientation ? ` orientation="${setup.orientation}"` : "") +
|
|
2591
|
+
xmlBoolAttr("blackAndWhite", setup.blackAndWhite) +
|
|
2592
|
+
xmlBoolAttr("draft", setup.draft) +
|
|
2593
|
+
xmlBoolAttr("useFirstPageNumber", useFirst);
|
|
2594
|
+
if (setupAttrs)
|
|
2595
|
+
parts.push(`<pageSetup${setupAttrs}/>`);
|
|
2596
|
+
}
|
|
2597
|
+
const hf = print?.headerFooter;
|
|
2598
|
+
if (hf && !isHeaderFooterEmpty(hf)) {
|
|
2599
|
+
const hfAttrs = xmlBoolAttr("differentOddEven", hf.differentOddEven) +
|
|
2600
|
+
xmlBoolAttr("differentFirst", hf.differentFirst) +
|
|
2601
|
+
xmlBoolAttr("scaleWithDoc", hf.scaleWithDoc) +
|
|
2602
|
+
xmlBoolAttr("alignWithMargins", hf.alignWithMargins);
|
|
2603
|
+
const children = [
|
|
2604
|
+
"oddHeader",
|
|
2605
|
+
"oddFooter",
|
|
2606
|
+
"evenHeader",
|
|
2607
|
+
"evenFooter",
|
|
2608
|
+
"firstHeader",
|
|
2609
|
+
"firstFooter",
|
|
2610
|
+
]
|
|
2611
|
+
.map((key) => {
|
|
2612
|
+
const text = hf[key];
|
|
2613
|
+
return typeof text === "string" && text
|
|
2614
|
+
? `<${key}>${escapeXmlText(text)}</${key}>`
|
|
2615
|
+
: "";
|
|
2616
|
+
})
|
|
2617
|
+
.join("");
|
|
2618
|
+
parts.push(children
|
|
2619
|
+
? `<headerFooter${hfAttrs}>${children}</headerFooter>`
|
|
2620
|
+
: `<headerFooter${hfAttrs}/>`);
|
|
2621
|
+
}
|
|
2622
|
+
const breaks = (ids, max) => {
|
|
2623
|
+
const valid = Array.from(new Set((ids ?? []).filter((id) => Number.isInteger(id) && id > 0))).sort((a, b) => a - b);
|
|
2624
|
+
if (!valid.length)
|
|
2625
|
+
return "";
|
|
2626
|
+
return valid
|
|
2627
|
+
.map((id) => `<brk id="${id}" max="${max}" man="1"/>`)
|
|
2628
|
+
.join("");
|
|
2629
|
+
};
|
|
2630
|
+
const rowBrks = breaks(print?.rowBreaks, 16383);
|
|
2631
|
+
if (rowBrks) {
|
|
2632
|
+
const count = rowBrks.split("<brk ").length - 1;
|
|
2633
|
+
parts.push(`<rowBreaks count="${count}" manualBreakCount="${count}">${rowBrks}</rowBreaks>`);
|
|
2634
|
+
}
|
|
2635
|
+
const colBrks = breaks(print?.columnBreaks, 1048575);
|
|
2636
|
+
if (colBrks) {
|
|
2637
|
+
const count = colBrks.split("<brk ").length - 1;
|
|
2638
|
+
parts.push(`<colBreaks count="${count}" manualBreakCount="${count}">${colBrks}</colBreaks>`);
|
|
2639
|
+
}
|
|
2640
|
+
return parts.join("");
|
|
2641
|
+
};
|
|
2356
2642
|
const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
|
|
2357
2643
|
if (!filterSpecs?.length)
|
|
2358
2644
|
return "";
|
|
@@ -2420,15 +2706,26 @@ const gridRangeToRef = (range) => {
|
|
|
2420
2706
|
};
|
|
2421
2707
|
const groupEmbedsBySheet = (embeds) => {
|
|
2422
2708
|
const result = new Map();
|
|
2423
|
-
|
|
2709
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2424
2710
|
if (!embed.position?.overlayPosition)
|
|
2425
|
-
|
|
2711
|
+
return;
|
|
2426
2712
|
if (embed.spec?.type !== "image")
|
|
2427
|
-
|
|
2713
|
+
return;
|
|
2428
2714
|
const overlay = embed.position.overlayPosition;
|
|
2715
|
+
const imageSpec = embed.spec;
|
|
2716
|
+
const adjustments = {
|
|
2717
|
+
crop: imageSpec.crop ?? undefined,
|
|
2718
|
+
brightness: imageSpec.brightness ?? undefined,
|
|
2719
|
+
contrast: imageSpec.contrast ?? undefined,
|
|
2720
|
+
transparency: imageSpec.transparency ?? undefined,
|
|
2721
|
+
};
|
|
2429
2722
|
const placement = {
|
|
2430
2723
|
kind: "embed",
|
|
2431
|
-
imageUrl:
|
|
2724
|
+
imageUrl: imageSpec.imageUrl,
|
|
2725
|
+
adjustments: Object.values(adjustments).some((v) => v !== undefined)
|
|
2726
|
+
? adjustments
|
|
2727
|
+
: undefined,
|
|
2728
|
+
order,
|
|
2432
2729
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2433
2730
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2434
2731
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2439,19 +2736,20 @@ const groupEmbedsBySheet = (embeds) => {
|
|
|
2439
2736
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2440
2737
|
list.push(placement);
|
|
2441
2738
|
result.set(embed.position.sheetId, list);
|
|
2442
|
-
}
|
|
2739
|
+
});
|
|
2443
2740
|
return result;
|
|
2444
2741
|
};
|
|
2445
2742
|
const groupTextBoxesBySheet = (embeds) => {
|
|
2446
2743
|
const result = new Map();
|
|
2447
|
-
|
|
2744
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2448
2745
|
if (!embed.position?.overlayPosition)
|
|
2449
|
-
|
|
2746
|
+
return;
|
|
2450
2747
|
if (embed.spec?.type !== "textbox")
|
|
2451
|
-
|
|
2748
|
+
return;
|
|
2452
2749
|
const overlay = embed.position.overlayPosition;
|
|
2453
2750
|
const placement = {
|
|
2454
2751
|
textBox: embed,
|
|
2752
|
+
order,
|
|
2455
2753
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2456
2754
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2457
2755
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2462,19 +2760,20 @@ const groupTextBoxesBySheet = (embeds) => {
|
|
|
2462
2760
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2463
2761
|
list.push(placement);
|
|
2464
2762
|
result.set(embed.position.sheetId, list);
|
|
2465
|
-
}
|
|
2763
|
+
});
|
|
2466
2764
|
return result;
|
|
2467
2765
|
};
|
|
2468
2766
|
const groupShapesBySheet = (embeds) => {
|
|
2469
2767
|
const result = new Map();
|
|
2470
|
-
|
|
2768
|
+
(embeds ?? []).forEach((embed, order) => {
|
|
2471
2769
|
if (!embed.position?.overlayPosition)
|
|
2472
|
-
|
|
2770
|
+
return;
|
|
2473
2771
|
if (embed.spec?.type !== "shape")
|
|
2474
|
-
|
|
2772
|
+
return;
|
|
2475
2773
|
const overlay = embed.position.overlayPosition;
|
|
2476
2774
|
const placement = {
|
|
2477
2775
|
shape: embed,
|
|
2776
|
+
order,
|
|
2478
2777
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2479
2778
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2480
2779
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2485,7 +2784,7 @@ const groupShapesBySheet = (embeds) => {
|
|
|
2485
2784
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2486
2785
|
list.push(placement);
|
|
2487
2786
|
result.set(embed.position.sheetId, list);
|
|
2488
|
-
}
|
|
2787
|
+
});
|
|
2489
2788
|
return result;
|
|
2490
2789
|
};
|
|
2491
2790
|
const groupChartsBySheet = (charts) => {
|
|
@@ -3118,20 +3417,32 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3118
3417
|
}));
|
|
3119
3418
|
frameId += 1;
|
|
3120
3419
|
}
|
|
3420
|
+
// Embeds (text boxes, shapes, images) keep the host's `embeds` order in the
|
|
3421
|
+
// drawing part — anchor order IS Excel's z-order, so an image drawn over a
|
|
3422
|
+
// shape must be emitted after it regardless of kind. Placements without an
|
|
3423
|
+
// order (in-cell images) go last, in their own order.
|
|
3424
|
+
const embedAnchors = [];
|
|
3425
|
+
const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
|
|
3121
3426
|
for (const placement of textBoxes) {
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3427
|
+
embedAnchors.push({
|
|
3428
|
+
order: orderOf(placement.order),
|
|
3429
|
+
xml: createTextBoxAnchor({
|
|
3430
|
+
sheet,
|
|
3431
|
+
placement,
|
|
3432
|
+
frameId,
|
|
3433
|
+
}),
|
|
3434
|
+
});
|
|
3127
3435
|
frameId += 1;
|
|
3128
3436
|
}
|
|
3129
3437
|
for (const placement of shapes) {
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3438
|
+
embedAnchors.push({
|
|
3439
|
+
order: orderOf(placement.order),
|
|
3440
|
+
xml: createShapeAnchor({
|
|
3441
|
+
sheet,
|
|
3442
|
+
placement,
|
|
3443
|
+
frameId,
|
|
3444
|
+
}),
|
|
3445
|
+
});
|
|
3135
3446
|
frameId += 1;
|
|
3136
3447
|
}
|
|
3137
3448
|
for (const placement of placements) {
|
|
@@ -3148,15 +3459,21 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3148
3459
|
extension: buffer.extension,
|
|
3149
3460
|
});
|
|
3150
3461
|
const relId = relationships.add(IMAGE_REL_TYPE, `../media/image${imageIndex}.${buffer.extension}`);
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3462
|
+
embedAnchors.push({
|
|
3463
|
+
order: orderOf(placement.order),
|
|
3464
|
+
xml: createTwoCellAnchor({
|
|
3465
|
+
sheet,
|
|
3466
|
+
placement,
|
|
3467
|
+
relId,
|
|
3468
|
+
pictureId,
|
|
3469
|
+
}),
|
|
3470
|
+
});
|
|
3157
3471
|
imageIndex += 1;
|
|
3158
3472
|
pictureId += 1;
|
|
3159
3473
|
}
|
|
3474
|
+
embedAnchors.sort((a, b) => a.order - b.order);
|
|
3475
|
+
for (const entry of embedAnchors)
|
|
3476
|
+
anchors.push(entry.xml);
|
|
3160
3477
|
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
3478
|
return {
|
|
3162
3479
|
xml: drawingXml,
|
|
@@ -3190,7 +3507,53 @@ const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
|
|
|
3190
3507
|
});
|
|
3191
3508
|
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
3509
|
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
|
-
|
|
3510
|
+
const { blipChildren, srcRectXml } = buildPictureAdjustmentsXml(placement.adjustments);
|
|
3511
|
+
const blipXml = blipChildren
|
|
3512
|
+
? `<a:blip r:embed="${relId}">${blipChildren}</a:blip>`
|
|
3513
|
+
: `<a:blip r:embed="${relId}"/>`;
|
|
3514
|
+
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>`;
|
|
3515
|
+
};
|
|
3516
|
+
/** Fraction (0…1) → DrawingML per-mille percent (100000 = 100%). */
|
|
3517
|
+
const fractionToPct = (value) => Math.round(Math.max(-1, Math.min(1, value)) * 100000);
|
|
3518
|
+
/**
|
|
3519
|
+
* `<a:blip>` children (`<a:alphaModFix>`, `<a:lum>`) and the `<a:srcRect>`
|
|
3520
|
+
* sibling for an image's picture-format adjustments; both empty when the
|
|
3521
|
+
* spec carries none.
|
|
3522
|
+
*/
|
|
3523
|
+
const buildPictureAdjustmentsXml = (adjustments) => {
|
|
3524
|
+
if (!adjustments)
|
|
3525
|
+
return { blipChildren: "", srcRectXml: "" };
|
|
3526
|
+
let blipChildren = "";
|
|
3527
|
+
const { transparency, brightness, contrast, crop } = adjustments;
|
|
3528
|
+
if (typeof transparency === "number" && transparency > 0) {
|
|
3529
|
+
blipChildren += `<a:alphaModFix amt="${fractionToPct(1 - Math.min(1, transparency))}"/>`;
|
|
3530
|
+
}
|
|
3531
|
+
const hasBrightness = typeof brightness === "number" && brightness !== 0;
|
|
3532
|
+
const hasContrast = typeof contrast === "number" && contrast !== 0;
|
|
3533
|
+
if (hasBrightness || hasContrast) {
|
|
3534
|
+
blipChildren += `<a:lum${hasBrightness ? ` bright="${fractionToPct(brightness)}"` : ""}${hasContrast ? ` contrast="${fractionToPct(contrast)}"` : ""}/>`;
|
|
3535
|
+
}
|
|
3536
|
+
let srcRectXml = "";
|
|
3537
|
+
if (crop) {
|
|
3538
|
+
const edges = ["l", "t", "r", "b"]
|
|
3539
|
+
.map((edge) => {
|
|
3540
|
+
const value = {
|
|
3541
|
+
l: crop.left,
|
|
3542
|
+
t: crop.top,
|
|
3543
|
+
r: crop.right,
|
|
3544
|
+
b: crop.bottom,
|
|
3545
|
+
}[edge];
|
|
3546
|
+
// Negative edges are Excel's "out-crop" (padding around the
|
|
3547
|
+
// bitmap) and round-trip like positive ones.
|
|
3548
|
+
return typeof value === "number" && Number.isFinite(value) && value !== 0
|
|
3549
|
+
? ` ${edge}="${fractionToPct(value)}"`
|
|
3550
|
+
: "";
|
|
3551
|
+
})
|
|
3552
|
+
.join("");
|
|
3553
|
+
if (edges)
|
|
3554
|
+
srcRectXml = `<a:srcRect${edges}/>`;
|
|
3555
|
+
}
|
|
3556
|
+
return { blipChildren, srcRectXml };
|
|
3194
3557
|
};
|
|
3195
3558
|
const createChartAnchor = ({ placement, relId, frameId, sheet, }) => {
|
|
3196
3559
|
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
@@ -3857,7 +4220,10 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
|
|
|
3857
4220
|
sheets.forEach((sheet, index) => {
|
|
3858
4221
|
sheetNameById.set(sheet.sheetId ?? index + 1, sheetNames[index]);
|
|
3859
4222
|
});
|
|
3860
|
-
const definedNamesXml = buildDefinedNamesXml(namedRanges, sheetNameById)
|
|
4223
|
+
const definedNamesXml = buildDefinedNamesXml(namedRanges, sheetNameById, sheets.map((sheet, index) => ({
|
|
4224
|
+
sheetName: sheetNames[index] ?? `Sheet${index + 1}`,
|
|
4225
|
+
printSettings: sheet.printSettings,
|
|
4226
|
+
})));
|
|
3861
4227
|
const calcPrXml = buildCalcPrXml(iterativeCalculationOptions);
|
|
3862
4228
|
const slicerCachesXml = buildWorkbookSlicerCachesXml(slicerCacheRelIds);
|
|
3863
4229
|
// Phase D2 — <pivotCaches> element. Each <pivotCache> pairs a
|
|
@@ -3886,12 +4252,48 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
|
|
|
3886
4252
|
: `<workbookPr/>`;
|
|
3887
4253
|
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
4254
|
};
|
|
3889
|
-
const buildDefinedNamesXml = (namedRanges, sheetNameById) => {
|
|
3890
|
-
if (!namedRanges?.length)
|
|
3891
|
-
return "";
|
|
4255
|
+
const buildDefinedNamesXml = (namedRanges, sheetNameById, printScopes = []) => {
|
|
3892
4256
|
const entries = [];
|
|
3893
|
-
|
|
3894
|
-
|
|
4257
|
+
// The builtin print names are sheet-scoped (`localSheetId` = position in
|
|
4258
|
+
// `<sheets>`); they come from `sheet.printSettings`, never from
|
|
4259
|
+
// `namedRanges` (a global `_xlnm.Print_Area` makes Excel repair the file).
|
|
4260
|
+
// Documents imported before `printSettings` existed still carry the print
|
|
4261
|
+
// area as a `_xlnm.Print_Area` named range on the sheet; it is converted
|
|
4262
|
+
// to the sheet-scoped name here unless the sheet already has a print area.
|
|
4263
|
+
const sheetIndexByName = new Map(printScopes.map(({ sheetName }, index) => [sheetName, index]));
|
|
4264
|
+
const legacyPrintAreas = new Map();
|
|
4265
|
+
for (const namedRange of namedRanges ?? []) {
|
|
4266
|
+
if (namedRange?.name !== PRINT_AREA_NAME || !namedRange.range?.sheetId) {
|
|
4267
|
+
continue;
|
|
4268
|
+
}
|
|
4269
|
+
const sheetName = sheetNameById.get(namedRange.range.sheetId);
|
|
4270
|
+
const index = sheetName ? sheetIndexByName.get(sheetName) : undefined;
|
|
4271
|
+
if (index == null)
|
|
4272
|
+
continue;
|
|
4273
|
+
legacyPrintAreas.set(index, [
|
|
4274
|
+
...(legacyPrintAreas.get(index) ?? []),
|
|
4275
|
+
namedRange.range,
|
|
4276
|
+
]);
|
|
4277
|
+
}
|
|
4278
|
+
printScopes.forEach(({ sheetName, printSettings }, index) => {
|
|
4279
|
+
const printArea = printSettings?.printArea?.length
|
|
4280
|
+
? printSettings.printArea
|
|
4281
|
+
: legacyPrintAreas.get(index);
|
|
4282
|
+
const area = printArea?.length
|
|
4283
|
+
? buildPrintAreaFormula(sheetName, printArea)
|
|
4284
|
+
: undefined;
|
|
4285
|
+
if (area) {
|
|
4286
|
+
entries.push(`<definedName name="${PRINT_AREA_NAME}" localSheetId="${index}">${escapeXmlText(area)}</definedName>`);
|
|
4287
|
+
}
|
|
4288
|
+
const titles = printSettings?.printTitles
|
|
4289
|
+
? buildPrintTitlesFormula(sheetName, printSettings.printTitles)
|
|
4290
|
+
: undefined;
|
|
4291
|
+
if (titles) {
|
|
4292
|
+
entries.push(`<definedName name="${PRINT_TITLES_NAME}" localSheetId="${index}">${escapeXmlText(titles)}</definedName>`);
|
|
4293
|
+
}
|
|
4294
|
+
});
|
|
4295
|
+
for (const namedRange of namedRanges ?? []) {
|
|
4296
|
+
if (!namedRange?.name || isBuiltinPrintName(namedRange.name))
|
|
3895
4297
|
continue;
|
|
3896
4298
|
const hiddenAttr = namedRange.hidden ? ` hidden="1"` : "";
|
|
3897
4299
|
if (namedRange.range?.sheetId) {
|