@rowsncolumns/toolkit 24.4.2 → 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,6 +3,7 @@ import { CellXfsRegistry, deriveEffectiveNumberFormatFromFormula, } from "@rowsn
|
|
|
3
3
|
import { cellRangeToAddress, cellToAddress, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, escapeCharacters, isNil, sortSheetsByIndex, addressToSelection, } from "@rowsncolumns/utils";
|
|
4
4
|
import { defaultSpreadsheetTheme } from "../../excel-parser/theme-default";
|
|
5
5
|
import { imageUrlToBase64 } from "../../helpers";
|
|
6
|
+
import { buildOdsPrintTokens, DEFAULT_PAGE_MARGINS_IN, inchesToOdsCm, isHeaderFooterEmpty, odsTableCentering, paperSizeToCm, splitHeaderFooterZones, tokenizeHeaderFooterZone, } from "../../print-settings";
|
|
6
7
|
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
7
8
|
import { buildOdsSparklineGroupsXml } from "./sparkline-builder";
|
|
8
9
|
const BLANK_LABEL = "(Blanks)";
|
|
@@ -80,6 +81,24 @@ const outlineGroupOpenTag = (span, tag) => {
|
|
|
80
81
|
};
|
|
81
82
|
const XML_DECLARATION = `<?xml version="1.0" encoding="UTF-8"?>`;
|
|
82
83
|
const ODS_MIMETYPE = "application/vnd.oasis.opendocument.spreadsheet";
|
|
84
|
+
/**
|
|
85
|
+
* Emit the wrappers that end at `rowIndex`: the header-rows band (when this
|
|
86
|
+
* is its last row) and any outline groups. A group may not close inside
|
|
87
|
+
* `<table:table-header-rows>`, so an open band is closed first — the caller
|
|
88
|
+
* re-opens it on the next band row. Returns the band's open state.
|
|
89
|
+
*/
|
|
90
|
+
const closeRowWrappers = (chunks, rowIndex, closesAt, headerRowsCloseAt, headerRowsOpen) => {
|
|
91
|
+
let open = headerRowsOpen;
|
|
92
|
+
const closes = closesAt.get(rowIndex) ?? [];
|
|
93
|
+
if (open && (rowIndex === headerRowsCloseAt || closes.length)) {
|
|
94
|
+
chunks.push("</table:table-header-rows>");
|
|
95
|
+
open = false;
|
|
96
|
+
}
|
|
97
|
+
for (const _ of closes) {
|
|
98
|
+
chunks.push("</table:table-row-group>");
|
|
99
|
+
}
|
|
100
|
+
return open;
|
|
101
|
+
};
|
|
83
102
|
const CREATOR = "Rows n Columns";
|
|
84
103
|
const PIXELS_PER_INCH = 96;
|
|
85
104
|
const CM_PER_INCH = 2.54;
|
|
@@ -127,14 +146,18 @@ class OdsStyleBuilder {
|
|
|
127
146
|
usedStyleNames = new Set();
|
|
128
147
|
rowStyles = new Map();
|
|
129
148
|
columnStyles = new Map();
|
|
149
|
+
rtlTableStyleName;
|
|
130
150
|
textStyles = new Map();
|
|
131
151
|
numberStyles = new Map();
|
|
132
152
|
styleXmlChunks = [];
|
|
133
153
|
numberXmlChunks = [];
|
|
134
154
|
// NEW: separate storage for conditional format styles (go to styles.xml)
|
|
135
155
|
conditionalStyleXmlChunks = [];
|
|
156
|
+
pageLayoutXmlChunks = [];
|
|
157
|
+
masterPageXmlChunks = [];
|
|
136
158
|
styleIndex = 1;
|
|
137
159
|
numberIndex = 1;
|
|
160
|
+
pageLayoutIndex = 0;
|
|
138
161
|
constructor(theme) {
|
|
139
162
|
this.theme = theme;
|
|
140
163
|
}
|
|
@@ -147,32 +170,73 @@ class OdsStyleBuilder {
|
|
|
147
170
|
return existing;
|
|
148
171
|
return this.registerCellWithName(format, key);
|
|
149
172
|
}
|
|
150
|
-
registerRow(heightPx, hidden) {
|
|
151
|
-
if (!heightPx && !hidden)
|
|
173
|
+
registerRow(heightPx, hidden, breakBefore = false) {
|
|
174
|
+
if (!heightPx && !hidden && !breakBefore)
|
|
152
175
|
return undefined;
|
|
153
|
-
const key = stableStringify({ heightPx, hidden });
|
|
176
|
+
const key = stableStringify({ heightPx, hidden, breakBefore });
|
|
154
177
|
const existing = this.rowStyles.get(key);
|
|
155
178
|
if (existing)
|
|
156
179
|
return existing;
|
|
157
180
|
const styleName = `ro${this.styleIndex++}`;
|
|
158
|
-
const styleXml = buildRowStyleXml(styleName, heightPx, hidden);
|
|
181
|
+
const styleXml = buildRowStyleXml(styleName, heightPx, hidden, breakBefore);
|
|
159
182
|
this.rowStyles.set(key, styleName);
|
|
160
183
|
this.styleXmlChunks.push(styleXml);
|
|
161
184
|
return styleName;
|
|
162
185
|
}
|
|
163
|
-
registerColumn(widthPx, hidden) {
|
|
164
|
-
if (!widthPx && !hidden)
|
|
186
|
+
registerColumn(widthPx, hidden, breakBefore = false) {
|
|
187
|
+
if (!widthPx && !hidden && !breakBefore)
|
|
165
188
|
return undefined;
|
|
166
|
-
const key = stableStringify({ widthPx, hidden });
|
|
189
|
+
const key = stableStringify({ widthPx, hidden, breakBefore });
|
|
167
190
|
const existing = this.columnStyles.get(key);
|
|
168
191
|
if (existing)
|
|
169
192
|
return existing;
|
|
170
193
|
const styleName = `co${this.styleIndex++}`;
|
|
171
|
-
const styleXml = buildColumnStyleXml(styleName, widthPx, hidden);
|
|
194
|
+
const styleXml = buildColumnStyleXml(styleName, widthPx, hidden, breakBefore);
|
|
172
195
|
this.columnStyles.set(key, styleName);
|
|
173
196
|
this.styleXmlChunks.push(styleXml);
|
|
174
197
|
return styleName;
|
|
175
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* A sheet's print settings → a `table` automatic style (content.xml)
|
|
201
|
+
* pointing at a master page whose page layout + header/footer live in
|
|
202
|
+
* styles.xml. Returns the table style name, or undefined when the sheet
|
|
203
|
+
* has no page-level print settings to carry.
|
|
204
|
+
*/
|
|
205
|
+
registerTable(print, rightToLeft) {
|
|
206
|
+
const pageLayoutXml = buildOdsPageLayoutXml(print);
|
|
207
|
+
const headerFooter = print?.headerFooter;
|
|
208
|
+
const hasPrintSettings = !!pageLayoutXml || !isHeaderFooterEmpty(headerFooter);
|
|
209
|
+
// Right-to-left is the table style's `style:writing-mode="rl-tb"`, the
|
|
210
|
+
// ODF counterpart of `<sheetView rightToLeft="1">`. A sheet with only
|
|
211
|
+
// that flag shares one RTL style on the default master page.
|
|
212
|
+
const writingMode = rightToLeft ? "rl-tb" : "lr-tb";
|
|
213
|
+
if (!hasPrintSettings) {
|
|
214
|
+
if (!rightToLeft)
|
|
215
|
+
return undefined;
|
|
216
|
+
if (this.rtlTableStyleName)
|
|
217
|
+
return this.rtlTableStyleName;
|
|
218
|
+
const styleName = `ta${this.styleIndex++}`;
|
|
219
|
+
this.rtlTableStyleName = styleName;
|
|
220
|
+
this.styleXmlChunks.push(`<style:style style:name="${styleName}" style:family="table" style:master-page-name="Default"><style:table-properties table:display="true" style:writing-mode="${writingMode}"/></style:style>`);
|
|
221
|
+
return styleName;
|
|
222
|
+
}
|
|
223
|
+
const index = ++this.pageLayoutIndex;
|
|
224
|
+
const layoutName = `Mpm${index}`;
|
|
225
|
+
const masterName = `Mp${index}`;
|
|
226
|
+
const tableStyleName = `ta${this.styleIndex++}`;
|
|
227
|
+
this.pageLayoutXmlChunks.push(`<style:page-layout style:name="${layoutName}">${pageLayoutXml ?? "<style:page-layout-properties/>"}</style:page-layout>`);
|
|
228
|
+
this.masterPageXmlChunks.push(`<style:master-page style:name="${masterName}" style:page-layout-name="${layoutName}">${buildOdsHeaderFooterXml("style:header", headerFooter?.oddHeader)}${buildOdsHeaderFooterXml("style:footer", headerFooter?.oddFooter)}</style:master-page>`);
|
|
229
|
+
this.styleXmlChunks.push(`<style:style style:name="${tableStyleName}" style:family="table" style:master-page-name="${masterName}"><style:table-properties table:display="true" style:writing-mode="${writingMode}"/></style:style>`);
|
|
230
|
+
return tableStyleName;
|
|
231
|
+
}
|
|
232
|
+
/** `<style:page-layout>` entries for styles.xml `<office:automatic-styles>`. */
|
|
233
|
+
toPageLayoutStylesXml() {
|
|
234
|
+
return this.pageLayoutXmlChunks.join("");
|
|
235
|
+
}
|
|
236
|
+
/** `<style:master-page>` entries for styles.xml `<office:master-styles>`. */
|
|
237
|
+
toMasterPagesXml() {
|
|
238
|
+
return this.masterPageXmlChunks.join("");
|
|
239
|
+
}
|
|
176
240
|
registerText(format) {
|
|
177
241
|
if (!format || Object.keys(format).length === 0)
|
|
178
242
|
return undefined;
|
|
@@ -353,7 +417,7 @@ export const createODSFile = async ({ conditionalFormats: _conditionalFormats, d
|
|
|
353
417
|
automaticStylesXml: styleBuilder.toAutomaticStylesXml(),
|
|
354
418
|
calculationSettingsXml,
|
|
355
419
|
});
|
|
356
|
-
const stylesXml = buildStylesXml(styleBuilder.toConditionalStylesXml());
|
|
420
|
+
const stylesXml = buildStylesXml(styleBuilder.toConditionalStylesXml(), styleBuilder.toPageLayoutStylesXml(), styleBuilder.toMasterPagesXml());
|
|
357
421
|
const metaXml = buildMetaXml();
|
|
358
422
|
const settingsXml = buildSettingsXml(sortedSheets, _theme ?? defaultSpreadsheetTheme);
|
|
359
423
|
const chartObjects = sheetXmlEntries.flatMap((entry) => entry.chartObjects);
|
|
@@ -393,7 +457,18 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
393
457
|
maxColumn: 0,
|
|
394
458
|
};
|
|
395
459
|
const rowMetadata = sheet.rowMetadata ?? [];
|
|
396
|
-
const
|
|
460
|
+
const print = sheet.printSettings;
|
|
461
|
+
// Manual breaks ride on the row AFTER the break (`fo:break-before`), and
|
|
462
|
+
// print-title rows need a real `<table:table-row>` to wrap — so both
|
|
463
|
+
// extend the emitted row range past the data.
|
|
464
|
+
const rowBreakSet = new Set((print?.rowBreaks ?? []).filter((n) => Number.isInteger(n) && n >= 1));
|
|
465
|
+
const titleRows = print?.printTitles?.rowStart != null && print.printTitles.rowEnd != null
|
|
466
|
+
? {
|
|
467
|
+
start: Math.max(1, print.printTitles.rowStart),
|
|
468
|
+
end: Math.max(print.printTitles.rowStart, print.printTitles.rowEnd),
|
|
469
|
+
}
|
|
470
|
+
: undefined;
|
|
471
|
+
const maxRows = Math.max(rows.length, rowMetadata.length, rowBreakSet.size ? Math.max(...rowBreakSet) + 2 : 0, titleRows ? titleRows.end + 1 : 0);
|
|
397
472
|
const rowXmlEntries = [];
|
|
398
473
|
const mergeState = buildMergeState(sheet.merges ?? []);
|
|
399
474
|
// Build annotations map from citations
|
|
@@ -431,6 +506,7 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
431
506
|
rowNumber: rowIndex,
|
|
432
507
|
row,
|
|
433
508
|
rowMetadata: metadata,
|
|
509
|
+
breakBefore: rowBreakSet.has(rowIndex - 1),
|
|
434
510
|
sourceSharedStrings,
|
|
435
511
|
cellXfs,
|
|
436
512
|
styleBuilder,
|
|
@@ -478,19 +554,41 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
478
554
|
const rowSpans = findOutlineGroupSpans(rowMetadata, maxRowIndex, summaryBelow);
|
|
479
555
|
const { opensAt: rowOpensAt, closesAt: rowClosesAt } = buildOutlineGroupIndex(rowSpans);
|
|
480
556
|
const rowXmlChunks = [];
|
|
557
|
+
// Print titles: `<table:table-header-rows>` wraps the repeated band. ODF
|
|
558
|
+
// only allows plain rows inside it, so — like LibreOffice's writer — the
|
|
559
|
+
// wrapper is closed before any `<table:table-row-group>` opens or closes
|
|
560
|
+
// inside the band and re-opened on the next band row; the importer unions
|
|
561
|
+
// the segments back into one band.
|
|
562
|
+
const headerRowsOpenAt = titleRows?.start;
|
|
563
|
+
const headerRowsCloseAt = titleRows?.end;
|
|
564
|
+
let headerRowsOpen = false;
|
|
481
565
|
for (let index = 0; index < rowXmlEntries.length; index++) {
|
|
482
566
|
const rowIndex = index + 1;
|
|
483
|
-
|
|
567
|
+
const opens = rowOpensAt.get(rowIndex) ?? [];
|
|
568
|
+
if (opens.length && headerRowsOpen) {
|
|
569
|
+
rowXmlChunks.push("</table:table-header-rows>");
|
|
570
|
+
headerRowsOpen = false;
|
|
571
|
+
}
|
|
572
|
+
for (const span of opens) {
|
|
484
573
|
rowXmlChunks.push(outlineGroupOpenTag(span, "table:table-row-group"));
|
|
485
574
|
}
|
|
575
|
+
if (!headerRowsOpen &&
|
|
576
|
+
headerRowsOpenAt != null &&
|
|
577
|
+
headerRowsCloseAt != null &&
|
|
578
|
+
rowIndex >= headerRowsOpenAt &&
|
|
579
|
+
rowIndex <= headerRowsCloseAt) {
|
|
580
|
+
rowXmlChunks.push("<table:table-header-rows>");
|
|
581
|
+
headerRowsOpen = true;
|
|
582
|
+
}
|
|
486
583
|
const entry = rowXmlEntries[index];
|
|
487
584
|
if (entry) {
|
|
488
585
|
rowXmlChunks.push(entry.xml);
|
|
489
586
|
}
|
|
490
587
|
else {
|
|
491
588
|
// Aggregate consecutive empty rows into `number-rows-repeated`, but
|
|
492
|
-
// never cross a group boundary or include a row that
|
|
493
|
-
// opens/closes at — that would put a row inside the wrong
|
|
589
|
+
// never cross a group / header-rows boundary or include a row that
|
|
590
|
+
// any group opens/closes at — that would put a row inside the wrong
|
|
591
|
+
// wrapper.
|
|
494
592
|
let repeat = 1;
|
|
495
593
|
while (index + repeat < rowXmlEntries.length &&
|
|
496
594
|
!rowXmlEntries[index + repeat]) {
|
|
@@ -498,6 +596,10 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
498
596
|
if (rowOpensAt.has(nextRowIndex) || rowClosesAt.has(nextRowIndex - 1)) {
|
|
499
597
|
break;
|
|
500
598
|
}
|
|
599
|
+
if (nextRowIndex === headerRowsOpenAt ||
|
|
600
|
+
nextRowIndex - 1 === headerRowsCloseAt) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
501
603
|
repeat += 1;
|
|
502
604
|
}
|
|
503
605
|
rowXmlChunks.push(repeat > 1
|
|
@@ -506,15 +608,11 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
506
608
|
// The break condition keeps closes from landing on any row except
|
|
507
609
|
// the last in the repetition, so we only need to emit them once.
|
|
508
610
|
const lastInRepeat = rowIndex + repeat - 1;
|
|
509
|
-
|
|
510
|
-
rowXmlChunks.push(`</table:table-row-group>`);
|
|
511
|
-
}
|
|
611
|
+
headerRowsOpen = closeRowWrappers(rowXmlChunks, lastInRepeat, rowClosesAt, headerRowsCloseAt, headerRowsOpen);
|
|
512
612
|
index += repeat - 1;
|
|
513
613
|
continue;
|
|
514
614
|
}
|
|
515
|
-
|
|
516
|
-
rowXmlChunks.push(`</table:table-row-group>`);
|
|
517
|
-
}
|
|
615
|
+
headerRowsOpen = closeRowWrappers(rowXmlChunks, rowIndex, rowClosesAt, headerRowsCloseAt, headerRowsOpen);
|
|
518
616
|
}
|
|
519
617
|
const embedFrames = embeds.length
|
|
520
618
|
? await buildEmbedFramesXml({
|
|
@@ -536,10 +634,28 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
536
634
|
const shapesXml = embedFrames.length || chartFramesResult.frames.length
|
|
537
635
|
? `<table:shapes>${[...embedFrames, ...chartFramesResult.frames].join("")}</table:shapes>`
|
|
538
636
|
: "";
|
|
539
|
-
const columnsXml = buildColumnsXml(sheet, styleBuilder, bounds);
|
|
637
|
+
const columnsXml = buildColumnsXml(sheet, styleBuilder, bounds, print);
|
|
540
638
|
// Build sparkline XML from SPARKLINE formulas
|
|
541
639
|
const sparklineXml = buildOdsSparklineGroupsXml(sheet, sheetName, rowDataMap) ?? "";
|
|
542
|
-
|
|
640
|
+
// ODF has no allow-edit `<protectedRange>` equivalent, so only the sheet-level
|
|
641
|
+
// switch is written; protected ranges degrade to unprotected on ODS export.
|
|
642
|
+
const protectedAttr = sheet.sheetProtection?.sheet
|
|
643
|
+
? ' table:protected="true"'
|
|
644
|
+
: "";
|
|
645
|
+
// Page setup / margins / header-footer ride on the table style's master
|
|
646
|
+
// page (right-to-left is the same style's writing mode); the print area
|
|
647
|
+
// is `table:print-ranges` (space-separated).
|
|
648
|
+
const tableStyleName = styleBuilder.registerTable(print, sheet.rightToLeft ?? undefined);
|
|
649
|
+
const tableStyleAttr = tableStyleName
|
|
650
|
+
? ` table:style-name="${tableStyleName}"`
|
|
651
|
+
: "";
|
|
652
|
+
const printRanges = (print?.printArea ?? [])
|
|
653
|
+
.map((range) => buildOdsRangeAddress(range, sheetName))
|
|
654
|
+
.filter((address) => Boolean(address));
|
|
655
|
+
const printRangesAttr = printRanges.length
|
|
656
|
+
? ` table:print-ranges="${escapeXml(printRanges.join(" "))}"`
|
|
657
|
+
: "";
|
|
658
|
+
const sheetXml = `<table:table table:name="${escapeXml(sheetName)}"${tableStyleAttr}${protectedAttr}${printRangesAttr}>${shapesXml}${columnsXml}${rowXmlChunks.join("")}${conditionalFormattingXml}${sparklineXml}</table:table>`;
|
|
543
659
|
return {
|
|
544
660
|
xml: sheetXml,
|
|
545
661
|
databaseRanges: buildDatabaseRangeEntries(tables, sheetName, sheet, rows),
|
|
@@ -547,7 +663,7 @@ const buildWorksheetXml = async ({ sheet, sheetName, rows, sourceSharedStrings,
|
|
|
547
663
|
nextChartObjectIndex: chartFramesResult.nextIndex,
|
|
548
664
|
};
|
|
549
665
|
};
|
|
550
|
-
const buildWorksheetRow = async ({ rowNumber, row, rowMetadata, sourceSharedStrings, cellXfs, styleBuilder, mergeState, dataValidationsById, mediaEntries, sheet, namedRangeNames, tables, ignoreFormulaPatterns, annotationsMap, sheetData, sheets, locale, }) => {
|
|
666
|
+
const buildWorksheetRow = async ({ rowNumber, row, rowMetadata, breakBefore = false, sourceSharedStrings, cellXfs, styleBuilder, mergeState, dataValidationsById, mediaEntries, sheet, namedRangeNames, tables, ignoreFormulaPatterns, annotationsMap, sheetData, sheets, locale, }) => {
|
|
551
667
|
const values = row?.values ?? [];
|
|
552
668
|
const cellXmlChunks = [];
|
|
553
669
|
let minColumn = Number.POSITIVE_INFINITY;
|
|
@@ -557,7 +673,7 @@ const buildWorksheetRow = async ({ rowNumber, row, rowMetadata, sourceSharedStri
|
|
|
557
673
|
const rowSize = rowMetadata?.size && rowMetadata.size > 0 ? rowMetadata.size : undefined;
|
|
558
674
|
const hasExplicitHeight = Boolean(rowSize && Math.abs(rowSize - defaultRowHeightPx) > 0.01);
|
|
559
675
|
const hasCustomHeight = Boolean(rowSize && rowMetadata?.resizedByUser);
|
|
560
|
-
const rowStyle = styleBuilder.registerRow(rowSize && (hasExplicitHeight || hasCustomHeight) ? rowSize : undefined, hasHiddenFlag);
|
|
676
|
+
const rowStyle = styleBuilder.registerRow(rowSize && (hasExplicitHeight || hasCustomHeight) ? rowSize : undefined, hasHiddenFlag, breakBefore);
|
|
561
677
|
const maxColumnIndex = Math.max(values.length - 1, mergeState.maxColumnByRow.get(rowNumber) ?? 0);
|
|
562
678
|
for (let columnIndex = 1; columnIndex <= maxColumnIndex; columnIndex++) {
|
|
563
679
|
if (mergeState.coveredCells.has(`${rowNumber}:${columnIndex}`)) {
|
|
@@ -594,7 +710,7 @@ const buildWorksheetRow = async ({ rowNumber, row, rowMetadata, sourceSharedStri
|
|
|
594
710
|
maxColumn = Math.max(maxColumn, columnIndex);
|
|
595
711
|
}
|
|
596
712
|
const hasCells = cellXmlChunks.length > 0;
|
|
597
|
-
const needsMetadata = hasHiddenFlag || hasExplicitHeight || hasCustomHeight;
|
|
713
|
+
const needsMetadata = hasHiddenFlag || hasExplicitHeight || hasCustomHeight || breakBefore;
|
|
598
714
|
if (!hasCells && !needsMetadata) {
|
|
599
715
|
return null;
|
|
600
716
|
}
|
|
@@ -962,8 +1078,110 @@ const buildCalculationSettingsXml = (iterativeCalculationOptions) => {
|
|
|
962
1078
|
const buildContentXml = ({ sheetsXml, databaseRangesXml, namedRangesXml, contentValidationsXml, automaticStylesXml, calculationSettingsXml, }) => {
|
|
963
1079
|
return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:style="${ODS_NS.style}" xmlns:text="${ODS_NS.text}" xmlns:table="${ODS_NS.table}" xmlns:draw="${ODS_NS.draw}" xmlns:fo="${ODS_NS.fo}" xmlns:xlink="${ODS_NS.xlink}" xmlns:svg="${ODS_NS.svg}" xmlns:number="${ODS_NS.number}" xmlns:of="${ODS_NS.of}" xmlns:calcext="${ODS_NS.calcext}" xmlns:dc="${ODS_NS.dc}" office:version="1.2"><office:scripts/><office:automatic-styles>${automaticStylesXml}</office:automatic-styles><office:body><office:spreadsheet>${calculationSettingsXml}${contentValidationsXml}${sheetsXml}${namedRangesXml}${databaseRangesXml}</office:spreadsheet></office:body></office:document-content>`;
|
|
964
1080
|
};
|
|
965
|
-
const buildStylesXml = (conditionalStylesXml = "") => {
|
|
966
|
-
|
|
1081
|
+
const buildStylesXml = (conditionalStylesXml = "", pageLayoutsXml = "", masterPagesXml = "") => {
|
|
1082
|
+
const automaticStyles = pageLayoutsXml
|
|
1083
|
+
? `<office:automatic-styles>${pageLayoutsXml}</office:automatic-styles>`
|
|
1084
|
+
: "<office:automatic-styles/>";
|
|
1085
|
+
const masterStyles = masterPagesXml
|
|
1086
|
+
? `<office:master-styles>${masterPagesXml}</office:master-styles>`
|
|
1087
|
+
: "<office:master-styles/>";
|
|
1088
|
+
return `${XML_DECLARATION}<office:document-styles xmlns:office="${ODS_NS.office}" xmlns:style="${ODS_NS.style}" xmlns:text="${ODS_NS.text}" xmlns:table="${ODS_NS.table}" xmlns:draw="${ODS_NS.draw}" xmlns:fo="${ODS_NS.fo}" xmlns:xlink="${ODS_NS.xlink}" xmlns:svg="${ODS_NS.svg}" xmlns:number="${ODS_NS.number}" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" office:version="1.2"><office:styles><style:style style:name="Default" style:family="table-cell"><style:paragraph-properties fo:margin-left="0cm" fo:margin-right="0cm"/></style:style>${conditionalStylesXml}</office:styles>${automaticStyles}${masterStyles}</office:document-styles>`;
|
|
1089
|
+
};
|
|
1090
|
+
/**
|
|
1091
|
+
* `<style:page-layout-properties>` (+ header/footer styles) for a sheet's
|
|
1092
|
+
* page setup, margins and print options. ODF keeps the header INSIDE the
|
|
1093
|
+
* top margin, so Excel's `header` (edge → header) becomes `fo:margin-top`
|
|
1094
|
+
* and `top - header` the header's `fo:min-height`; footer likewise.
|
|
1095
|
+
*/
|
|
1096
|
+
const buildOdsPageLayoutXml = (print) => {
|
|
1097
|
+
const setup = print?.pageSetup;
|
|
1098
|
+
const margins = print?.pageMargins;
|
|
1099
|
+
const options = print?.printOptions;
|
|
1100
|
+
if (!setup && !margins && !options)
|
|
1101
|
+
return undefined;
|
|
1102
|
+
const attrs = [];
|
|
1103
|
+
const landscape = setup?.orientation === "landscape";
|
|
1104
|
+
const paper = paperSizeToCm(setup?.paperSize);
|
|
1105
|
+
if (paper) {
|
|
1106
|
+
const width = landscape ? paper.height : paper.width;
|
|
1107
|
+
const height = landscape ? paper.width : paper.height;
|
|
1108
|
+
attrs.push(`fo:page-width="${width.toFixed(3)}cm"`, `fo:page-height="${height.toFixed(3)}cm"`);
|
|
1109
|
+
}
|
|
1110
|
+
if (setup?.orientation) {
|
|
1111
|
+
attrs.push(`style:print-orientation="${setup.orientation}"`);
|
|
1112
|
+
}
|
|
1113
|
+
const left = margins?.left ?? DEFAULT_PAGE_MARGINS_IN.left;
|
|
1114
|
+
const right = margins?.right ?? DEFAULT_PAGE_MARGINS_IN.right;
|
|
1115
|
+
const top = margins?.top ?? DEFAULT_PAGE_MARGINS_IN.top;
|
|
1116
|
+
const bottom = margins?.bottom ?? DEFAULT_PAGE_MARGINS_IN.bottom;
|
|
1117
|
+
const header = Math.min(margins?.header ?? DEFAULT_PAGE_MARGINS_IN.header, top);
|
|
1118
|
+
const footer = Math.min(margins?.footer ?? DEFAULT_PAGE_MARGINS_IN.footer, bottom);
|
|
1119
|
+
attrs.push(`fo:margin-top="${inchesToOdsCm(header)}"`, `fo:margin-bottom="${inchesToOdsCm(footer)}"`, `fo:margin-left="${inchesToOdsCm(left)}"`, `fo:margin-right="${inchesToOdsCm(right)}"`);
|
|
1120
|
+
if (setup?.fitToPage) {
|
|
1121
|
+
// LibreOffice's per-axis fit (`loext:scale-to-X/Y`); 0 = automatic.
|
|
1122
|
+
attrs.push(`loext:scale-to-X="${setup.fitToWidth ?? 1}"`, `loext:scale-to-Y="${setup.fitToHeight ?? 1}"`);
|
|
1123
|
+
}
|
|
1124
|
+
else if (typeof setup?.scale === "number" && setup.scale > 0) {
|
|
1125
|
+
attrs.push(`style:scale-to="${setup.scale}%"`);
|
|
1126
|
+
}
|
|
1127
|
+
const centering = odsTableCentering(options);
|
|
1128
|
+
if (centering)
|
|
1129
|
+
attrs.push(`style:table-centering="${centering}"`);
|
|
1130
|
+
attrs.push(`style:print="${buildOdsPrintTokens(options)}"`);
|
|
1131
|
+
if (setup?.pageOrder === "overThenDown") {
|
|
1132
|
+
attrs.push(`style:print-page-order="ltr"`);
|
|
1133
|
+
}
|
|
1134
|
+
if (typeof setup?.firstPageNumber === "number" &&
|
|
1135
|
+
setup.useFirstPageNumber !== false) {
|
|
1136
|
+
attrs.push(`style:first-page-number="${setup.firstPageNumber}"`);
|
|
1137
|
+
}
|
|
1138
|
+
const headerStyle = `<style:header-style><style:header-footer-properties fo:min-height="${inchesToOdsCm(Math.max(0, top - header))}" fo:margin-bottom="0cm"/></style:header-style>`;
|
|
1139
|
+
const footerStyle = `<style:footer-style><style:header-footer-properties fo:min-height="${inchesToOdsCm(Math.max(0, bottom - footer))}" fo:margin-top="0cm"/></style:footer-style>`;
|
|
1140
|
+
return `<style:page-layout-properties ${attrs.join(" ")}/>${headerStyle}${footerStyle}`;
|
|
1141
|
+
};
|
|
1142
|
+
const odsFieldXml = (token) => {
|
|
1143
|
+
if (token.type === "text") {
|
|
1144
|
+
return token.text
|
|
1145
|
+
.split("\n")
|
|
1146
|
+
.map((line) => escapeXml(line))
|
|
1147
|
+
.join("<text:line-break/>");
|
|
1148
|
+
}
|
|
1149
|
+
switch (token.field) {
|
|
1150
|
+
case "page":
|
|
1151
|
+
return "<text:page-number>1</text:page-number>";
|
|
1152
|
+
case "pages":
|
|
1153
|
+
return "<text:page-count>1</text:page-count>";
|
|
1154
|
+
case "date":
|
|
1155
|
+
return "<text:date/>";
|
|
1156
|
+
case "time":
|
|
1157
|
+
return "<text:time/>";
|
|
1158
|
+
case "sheet":
|
|
1159
|
+
return "<text:sheet-name/>";
|
|
1160
|
+
case "file":
|
|
1161
|
+
return '<text:file-name text:display="name"/>';
|
|
1162
|
+
case "path":
|
|
1163
|
+
return '<text:file-name text:display="path"/>';
|
|
1164
|
+
default:
|
|
1165
|
+
return "";
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
const odsHeaderZoneXml = (code) => code
|
|
1169
|
+
? `<text:p>${tokenizeHeaderFooterZone(code).map(odsFieldXml).join("")}</text:p>`
|
|
1170
|
+
: "";
|
|
1171
|
+
/**
|
|
1172
|
+
* Excel header/footer code → `<style:header>` / `<style:footer>` with
|
|
1173
|
+
* `&L` / `&C` / `&R` zones as ODF regions and field macros as `<text:*>`
|
|
1174
|
+
* fields. Formatting codes have no ODF counterpart and are dropped; an
|
|
1175
|
+
* absent code yields a hidden (`style:display="false"`) element.
|
|
1176
|
+
*/
|
|
1177
|
+
const buildOdsHeaderFooterXml = (tag, code) => {
|
|
1178
|
+
if (!code)
|
|
1179
|
+
return `<${tag} style:display="false"/>`;
|
|
1180
|
+
const zones = splitHeaderFooterZones(code);
|
|
1181
|
+
if (!zones.left && !zones.right) {
|
|
1182
|
+
return `<${tag}>${odsHeaderZoneXml(zones.center)}</${tag}>`;
|
|
1183
|
+
}
|
|
1184
|
+
return `<${tag}><style:region-left>${odsHeaderZoneXml(zones.left)}</style:region-left><style:region-center>${odsHeaderZoneXml(zones.center)}</style:region-center><style:region-right>${odsHeaderZoneXml(zones.right)}</style:region-right></${tag}>`;
|
|
967
1185
|
};
|
|
968
1186
|
const buildMetaXml = () => {
|
|
969
1187
|
const now = new Date().toISOString();
|
|
@@ -1024,29 +1242,57 @@ const buildFreezeCell = (rows, columns) => {
|
|
|
1024
1242
|
const row = Math.max(1, rows + 1);
|
|
1025
1243
|
return `${col}${row}`;
|
|
1026
1244
|
};
|
|
1027
|
-
const buildColumnsXml = (sheet, styleBuilder, bounds) => {
|
|
1245
|
+
const buildColumnsXml = (sheet, styleBuilder, bounds, print) => {
|
|
1028
1246
|
const metadata = sheet.columnMetadata ?? [];
|
|
1029
|
-
const
|
|
1247
|
+
const columnBreakSet = new Set((print?.columnBreaks ?? []).filter((n) => Number.isInteger(n) && n >= 1));
|
|
1248
|
+
const titles = print?.printTitles;
|
|
1249
|
+
const titleColumns = titles?.columnStart != null && titles.columnEnd != null
|
|
1250
|
+
? {
|
|
1251
|
+
start: Math.max(1, titles.columnStart),
|
|
1252
|
+
end: Math.max(titles.columnStart, titles.columnEnd),
|
|
1253
|
+
}
|
|
1254
|
+
: undefined;
|
|
1255
|
+
const maxColumns = Math.max(metadata.length - 1, bounds.maxColumn || 0, columnBreakSet.size ? Math.max(...columnBreakSet) + 1 : 0, titleColumns?.end ?? 0);
|
|
1030
1256
|
if (!maxColumns)
|
|
1031
1257
|
return "";
|
|
1032
1258
|
const summaryRight = sheet.outlinePr?.summaryRight !== false;
|
|
1033
1259
|
const spans = findOutlineGroupSpans(metadata, maxColumns, summaryRight);
|
|
1034
1260
|
const { opensAt, closesAt } = buildOutlineGroupIndex(spans);
|
|
1035
1261
|
const parts = [];
|
|
1262
|
+
// Same rule as the row band: `<table:table-header-columns>` holds plain
|
|
1263
|
+
// columns only, so it closes around a column-group edge and re-opens.
|
|
1264
|
+
let headerColumnsOpen = false;
|
|
1036
1265
|
for (let index = 1; index <= maxColumns; index++) {
|
|
1037
|
-
|
|
1266
|
+
const opens = opensAt.get(index) ?? [];
|
|
1267
|
+
if (opens.length && headerColumnsOpen) {
|
|
1268
|
+
parts.push("</table:table-header-columns>");
|
|
1269
|
+
headerColumnsOpen = false;
|
|
1270
|
+
}
|
|
1271
|
+
for (const span of opens) {
|
|
1038
1272
|
parts.push(outlineGroupOpenTag(span, "table:table-column-group"));
|
|
1039
1273
|
}
|
|
1274
|
+
if (!headerColumnsOpen &&
|
|
1275
|
+
titleColumns &&
|
|
1276
|
+
index >= titleColumns.start &&
|
|
1277
|
+
index <= titleColumns.end) {
|
|
1278
|
+
parts.push("<table:table-header-columns>");
|
|
1279
|
+
headerColumnsOpen = true;
|
|
1280
|
+
}
|
|
1040
1281
|
const props = metadata[index];
|
|
1041
1282
|
const widthPx = getColumnWidthPx(sheet, index);
|
|
1042
1283
|
const hidden = Boolean(props?.hiddenByUser || props?.hiddenByFilter);
|
|
1043
|
-
const styleName = styleBuilder.registerColumn(widthPx, hidden);
|
|
1284
|
+
const styleName = styleBuilder.registerColumn(widthPx, hidden, columnBreakSet.has(index - 1));
|
|
1044
1285
|
const attrs = [];
|
|
1045
1286
|
if (styleName) {
|
|
1046
1287
|
attrs.push(`table:style-name="${styleName}"`);
|
|
1047
1288
|
}
|
|
1048
1289
|
parts.push(`<table:table-column${attrs.length ? " " + attrs.join(" ") : ""}/>`);
|
|
1049
|
-
|
|
1290
|
+
const closes = closesAt.get(index) ?? [];
|
|
1291
|
+
if (headerColumnsOpen && (index === titleColumns?.end || closes.length)) {
|
|
1292
|
+
parts.push("</table:table-header-columns>");
|
|
1293
|
+
headerColumnsOpen = false;
|
|
1294
|
+
}
|
|
1295
|
+
for (const _ of closes) {
|
|
1050
1296
|
parts.push(`</table:table-column-group>`);
|
|
1051
1297
|
}
|
|
1052
1298
|
}
|
|
@@ -1208,7 +1454,7 @@ const buildRichTextParagraph = (text, runs, styleBuilder) => {
|
|
|
1208
1454
|
}
|
|
1209
1455
|
return `<text:p>${parts.join("")}</text:p>`;
|
|
1210
1456
|
};
|
|
1211
|
-
const buildRowStyleXml = (name, heightPx, hidden) => {
|
|
1457
|
+
const buildRowStyleXml = (name, heightPx, hidden, breakBefore = false) => {
|
|
1212
1458
|
const props = [];
|
|
1213
1459
|
if (heightPx) {
|
|
1214
1460
|
props.push(`style:row-height="${pxToCm(heightPx).toFixed(2)}cm"`);
|
|
@@ -1216,9 +1462,12 @@ const buildRowStyleXml = (name, heightPx, hidden) => {
|
|
|
1216
1462
|
if (hidden) {
|
|
1217
1463
|
props.push(`style:visibility="collapse"`);
|
|
1218
1464
|
}
|
|
1465
|
+
if (breakBefore) {
|
|
1466
|
+
props.push(`fo:break-before="page"`);
|
|
1467
|
+
}
|
|
1219
1468
|
return `<style:style style:name="${name}" style:family="table-row"><style:table-row-properties${props.length ? " " + props.join(" ") : ""}/></style:style>`;
|
|
1220
1469
|
};
|
|
1221
|
-
const buildColumnStyleXml = (name, widthPx, hidden) => {
|
|
1470
|
+
const buildColumnStyleXml = (name, widthPx, hidden, breakBefore = false) => {
|
|
1222
1471
|
const props = [];
|
|
1223
1472
|
if (widthPx) {
|
|
1224
1473
|
props.push(`style:column-width="${pxToCm(widthPx).toFixed(2)}cm"`);
|
|
@@ -1226,6 +1475,9 @@ const buildColumnStyleXml = (name, widthPx, hidden) => {
|
|
|
1226
1475
|
if (hidden) {
|
|
1227
1476
|
props.push(`style:visibility="collapse"`);
|
|
1228
1477
|
}
|
|
1478
|
+
if (breakBefore) {
|
|
1479
|
+
props.push(`fo:break-before="page"`);
|
|
1480
|
+
}
|
|
1229
1481
|
return `<style:style style:name="${name}" style:family="table-column"><style:table-column-properties${props.length ? " " + props.join(" ") : ""}/></style:style>`;
|
|
1230
1482
|
};
|
|
1231
1483
|
const buildNumberStyleXml = (name, format) => {
|
|
@@ -2727,7 +2979,26 @@ const buildChartObjectXml = (chart, sheetNameById) => {
|
|
|
2727
2979
|
scatter: "chart:scatter",
|
|
2728
2980
|
area: "chart:area",
|
|
2729
2981
|
};
|
|
2730
|
-
|
|
2982
|
+
// 3-D / surface classes degrade to a 2-D `chartType` with the authored
|
|
2983
|
+
// class kept as `sourceChartType` (BAR_3D, LINE_3D, PIE_3D, AREA_3D,
|
|
2984
|
+
// SURFACE, SURFACE_3D). ODF expresses the same thing as
|
|
2985
|
+
// `chart:three-dimensional="true"` on the plot-area style (plus the
|
|
2986
|
+
// `chart:surface` class for surfaces), so the flag round-trips.
|
|
2987
|
+
const sourceChartType = String(spec.sourceChartType ?? "").toUpperCase();
|
|
2988
|
+
const isSurface = sourceChartType === "SURFACE" || sourceChartType === "SURFACE_3D";
|
|
2989
|
+
// A plain SURFACE is Excel's contour (top-down) view, so only SURFACE_3D
|
|
2990
|
+
// carries the three-dimensional flag.
|
|
2991
|
+
const threeDimensional = sourceChartType === "SURFACE_3D" ||
|
|
2992
|
+
sourceChartType === "BAR_3D" ||
|
|
2993
|
+
sourceChartType === "LINE_3D" ||
|
|
2994
|
+
sourceChartType === "PIE_3D" ||
|
|
2995
|
+
sourceChartType === "AREA_3D";
|
|
2996
|
+
const chartClass = isSurface
|
|
2997
|
+
? "chart:surface"
|
|
2998
|
+
: (typeMap[spec.chartType] ?? "chart:bar");
|
|
2999
|
+
const plotStyleProps = threeDimensional
|
|
3000
|
+
? `<style:chart-properties chart:three-dimensional="true"/>`
|
|
3001
|
+
: "";
|
|
2731
3002
|
const title = spec.title
|
|
2732
3003
|
? `<chart:title><text:p>${escapeXml(String(spec.title))}</text:p></chart:title>`
|
|
2733
3004
|
: "";
|
|
@@ -2810,7 +3081,7 @@ const buildChartObjectXml = (chart, sheetNameById) => {
|
|
|
2810
3081
|
const legendXml = chartClass === "chart:circle"
|
|
2811
3082
|
? ""
|
|
2812
3083
|
: `<chart:legend chart:legend-position="end" chart:legend-align="center" chart:style-name="legend1"/>`;
|
|
2813
|
-
return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:chart="${ODS_NS.chart}" xmlns:table="${ODS_NS.table}" xmlns:text="${ODS_NS.text}" xmlns:style="${ODS_NS.style}" xmlns:draw="${ODS_NS.draw}" xmlns:svg="${ODS_NS.svg}" xmlns:fo="${ODS_NS.fo}" office:version="1.2"><office:automatic-styles>${gradientDefsXml}<style:style style:name="ch1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff"/></style:style><style:style style:name="plot1" style:family="chart"
|
|
3084
|
+
return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:chart="${ODS_NS.chart}" xmlns:table="${ODS_NS.table}" xmlns:text="${ODS_NS.text}" xmlns:style="${ODS_NS.style}" xmlns:draw="${ODS_NS.draw}" xmlns:svg="${ODS_NS.svg}" xmlns:fo="${ODS_NS.fo}" office:version="1.2"><office:automatic-styles>${gradientDefsXml}<style:style style:name="ch1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff"/></style:style><style:style style:name="plot1" style:family="chart">${plotStyleProps}<style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="wall1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3" svg:stroke-width="0.01cm"/></style:style><style:style style:name="floor1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="legend1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="10pt"/></style:style><style:style style:name="axisx" style:family="chart"><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style><style:style style:name="axisy" style:family="chart"><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style>${seriesStyles}</office:automatic-styles><office:body><office:chart><chart:chart chart:class="${chartClass}" chart:style-name="ch1">${title}${legendXml}<chart:plot-area chart:style-name="plot1"${plotAreaAttrs}>${axisXml}${seriesXml}${wallFloor}</chart:plot-area></chart:chart></office:chart></office:body></office:document-content>`;
|
|
2814
3085
|
};
|
|
2815
3086
|
const buildChartRangeAddress = (range, sheetNameById) => {
|
|
2816
3087
|
const sheetName = sheetNameById.get(range.sheetId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-builder.d.ts","sourceRoot":"","sources":["../../../../exporter/xlsx/chart-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,2BAA2B,CAAC;AAovCnC,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KACjC,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"chart-builder.d.ts","sourceRoot":"","sources":["../../../../exporter/xlsx/chart-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,2BAA2B,CAAC;AAovCnC,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KACjC,MAAM,GAAG,SAgoBX,CAAC"}
|