xls-codec 1.0.2 → 2.0.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/README.md +128 -23
- package/dist/biff/bof-writer.cjs +18 -0
- package/dist/biff/bof-writer.d.cts +5 -0
- package/dist/biff/bof-writer.d.ts +5 -0
- package/dist/biff/bof-writer.js +17 -0
- package/dist/biff/builder.cjs +48 -0
- package/dist/biff/builder.d.cts +17 -0
- package/dist/biff/builder.d.ts +17 -0
- package/dist/biff/builder.js +47 -0
- package/dist/biff/cursor.cjs +4 -0
- package/dist/biff/cursor.d.cts +1 -27
- package/dist/biff/cursor.d.ts +1 -27
- package/dist/biff/cursor.js +4 -0
- package/dist/biff/errors.cjs +7 -0
- package/dist/biff/errors.d.cts +3 -1
- package/dist/biff/errors.d.ts +3 -1
- package/dist/biff/errors.js +7 -1
- package/dist/biff/print-setup.cjs +108 -0
- package/dist/biff/print-setup.d.cts +2 -0
- package/dist/biff/print-setup.d.ts +2 -0
- package/dist/biff/print-setup.js +103 -0
- package/dist/biff/ptg-functions.cjs +383 -0
- package/dist/biff/ptg-functions.d.cts +7 -0
- package/dist/biff/ptg-functions.d.ts +7 -0
- package/dist/biff/ptg-functions.js +381 -0
- package/dist/biff/ptg.cjs +319 -0
- package/dist/biff/ptg.d.cts +2 -0
- package/dist/biff/ptg.d.ts +2 -0
- package/dist/biff/ptg.js +318 -0
- package/dist/biff/record-types.cjs +60 -0
- package/dist/biff/record-types.d.cts +41 -1
- package/dist/biff/record-types.d.ts +41 -1
- package/dist/biff/record-types.js +41 -1
- package/dist/biff/record-writer.cjs +29 -0
- package/dist/biff/record-writer.d.cts +7 -0
- package/dist/biff/record-writer.d.ts +7 -0
- package/dist/biff/record-writer.js +27 -0
- package/dist/biff/string-writer.cjs +58 -0
- package/dist/biff/string-writer.d.cts +13 -0
- package/dist/biff/string-writer.d.ts +13 -0
- package/dist/biff/string-writer.js +55 -0
- package/dist/biff/strings.d.cts +1 -1
- package/dist/biff/strings.d.ts +1 -1
- package/dist/biff/substreams.d.cts +1 -20
- package/dist/biff/substreams.d.ts +1 -20
- package/dist/biff/write-errors.cjs +10 -0
- package/dist/biff/write-errors.d.cts +6 -0
- package/dist/biff/write-errors.d.ts +6 -0
- package/dist/biff/write-errors.js +9 -0
- package/dist/biff/xf-colors.cjs +374 -0
- package/dist/biff/xf-colors.d.cts +2 -0
- package/dist/biff/xf-colors.d.ts +2 -0
- package/dist/biff/xf-colors.js +339 -0
- package/dist/biff/xf-writer.cjs +96 -0
- package/dist/biff/xf-writer.d.cts +29 -0
- package/dist/biff/xf-writer.d.ts +29 -0
- package/dist/biff/xf-writer.js +90 -0
- package/dist/container.cjs +13 -4
- package/dist/container.d.cts +10 -3
- package/dist/container.d.ts +10 -3
- package/dist/container.js +12 -4
- package/dist/content.cjs +123 -18
- package/dist/content.js +123 -18
- package/dist/cursor-VMtw9uVP.d.cts +30 -0
- package/dist/cursor-VMtw9uVP.d.ts +30 -0
- package/dist/index.cjs +71 -5
- package/dist/index.d.cts +20 -10
- package/dist/index.d.ts +20 -10
- package/dist/index.js +17 -7
- package/dist/metadata.cjs +14 -0
- package/dist/metadata.d.cts +6 -0
- package/dist/metadata.d.ts +6 -0
- package/dist/metadata.js +13 -0
- package/dist/print-names-D-njuzVw.d.cts +41 -0
- package/dist/print-names-DUlpVE00.d.ts +41 -0
- package/dist/print-setup-B_ihDvm5.d.cts +57 -0
- package/dist/print-setup-B_ihDvm5.d.ts +57 -0
- package/dist/ptg-B2K8t3js.d.cts +21 -0
- package/dist/ptg-B2K8t3js.d.ts +21 -0
- package/dist/serial.cjs +41 -0
- package/dist/serial.d.cts +7 -1
- package/dist/serial.d.ts +7 -1
- package/dist/serial.js +39 -1
- package/dist/substreams-D7dQiJbp.d.ts +21 -0
- package/dist/substreams-Ddtvn_Vr.d.cts +21 -0
- package/dist/units.cjs +37 -0
- package/dist/units.d.cts +15 -1
- package/dist/units.d.ts +15 -1
- package/dist/units.js +33 -1
- package/dist/workbook/globals-writer.cjs +122 -0
- package/dist/workbook/globals-writer.d.cts +39 -0
- package/dist/workbook/globals-writer.d.ts +39 -0
- package/dist/workbook/globals-writer.js +120 -0
- package/dist/workbook/globals.cjs +72 -8
- package/dist/workbook/globals.d.cts +26 -2
- package/dist/workbook/globals.d.ts +26 -2
- package/dist/workbook/globals.js +71 -7
- package/dist/workbook/print-names.cjs +241 -0
- package/dist/workbook/print-names.d.cts +2 -0
- package/dist/workbook/print-names.d.ts +2 -0
- package/dist/workbook/print-names.js +238 -0
- package/dist/workbook/sheet-writer.cjs +277 -0
- package/dist/workbook/sheet-writer.d.cts +12 -0
- package/dist/workbook/sheet-writer.d.ts +12 -0
- package/dist/workbook/sheet-writer.js +276 -0
- package/dist/workbook/sheet.cjs +125 -17
- package/dist/workbook/sheet.d.cts +38 -4
- package/dist/workbook/sheet.d.ts +38 -4
- package/dist/workbook/sheet.js +125 -17
- package/dist/write.cjs +311 -0
- package/dist/write.d.cts +13 -0
- package/dist/write.d.ts +13 -0
- package/dist/write.js +309 -0
- package/dist/written-cells.cjs +26 -0
- package/dist/written-cells.d.cts +16 -0
- package/dist/written-cells.d.ts +16 -0
- package/dist/written-cells.js +24 -0
- package/dist/xf-colors-CehHZtBy.d.cts +91 -0
- package/dist/xf-colors-CpykR3B9.d.ts +91 -0
- package/package.json +4 -3
- package/dist/number-format.cjs +0 -298
- package/dist/number-format.d.cts +0 -32
- package/dist/number-format.d.ts +0 -32
- package/dist/number-format.js +0 -296
package/dist/write.cjs
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_container = require("./container.cjs");
|
|
3
|
+
const require_biff_xf_colors = require("./biff/xf-colors.cjs");
|
|
4
|
+
const require_biff_write_errors = require("./biff/write-errors.cjs");
|
|
5
|
+
const require_workbook_print_names = require("./workbook/print-names.cjs");
|
|
6
|
+
const require_metadata = require("./metadata.cjs");
|
|
7
|
+
const require_workbook_globals_writer = require("./workbook/globals-writer.cjs");
|
|
8
|
+
const require_written_cells = require("./written-cells.cjs");
|
|
9
|
+
const require_workbook_sheet_writer = require("./workbook/sheet-writer.cjs");
|
|
10
|
+
let archive_codec = require("archive-codec");
|
|
11
|
+
let document_schema_js = require("document-schema.js");
|
|
12
|
+
let excel_number_format = require("excel-number-format");
|
|
13
|
+
//#region src/write.ts
|
|
14
|
+
const WORKBOOK_STREAM_NAME = "Workbook";
|
|
15
|
+
/** [MS-XLS] 2.4.126: a Format record's own ifmt is constrained to the ranges 5-8, 23-26, 41-44, 63-66, and 164-382 for a custom (non-built-in) code; 164 is the first identifier every real producer actually uses for a custom code, and this writer follows suit, incrementing sequentially and refusing to exceed the range's own ceiling. */
|
|
16
|
+
const FIRST_CUSTOM_FORMAT_ID = 164;
|
|
17
|
+
const LAST_CUSTOM_FORMAT_ID = 382;
|
|
18
|
+
const BUILTIN_FORMAT_PERCENTAGE = 9;
|
|
19
|
+
const BUILTIN_FORMAT_CURRENCY = 44;
|
|
20
|
+
const BUILTIN_FORMAT_DATE = 14;
|
|
21
|
+
const BUILTIN_FORMAT_TIME = 21;
|
|
22
|
+
const BUILTIN_FORMAT_DATE_TIME = 22;
|
|
23
|
+
const GENERAL_FORMAT_ID = 0;
|
|
24
|
+
function builtinCode(id) {
|
|
25
|
+
const code = excel_number_format.BUILTIN_NUMBER_FORMATS.get(id);
|
|
26
|
+
if (code === void 0) throw new require_biff_write_errors.BiffWriteError(`internal error: BUILTIN_NUMBER_FORMATS has no entry for id ${id}`);
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
/** The default format identifier a value kind resolves to when its own cell carries no numberFormatCode -- chosen so excel-number-format's classifyNumberFormat, run against the resulting code on the way back in, reclassifies to the identical kind. A plain number/string/boolean/error needs no distinguishing code at all: General classifies as 'number', which is exactly the fallback content.ts's own resolveValue already takes for a numeric cell with no format. */
|
|
30
|
+
function defaultFormatIdForKind(kind) {
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case "percentage": return BUILTIN_FORMAT_PERCENTAGE;
|
|
33
|
+
case "currency": return BUILTIN_FORMAT_CURRENCY;
|
|
34
|
+
case "date": return BUILTIN_FORMAT_DATE;
|
|
35
|
+
case "time": return BUILTIN_FORMAT_TIME;
|
|
36
|
+
case "dateTime": return BUILTIN_FORMAT_DATE_TIME;
|
|
37
|
+
case "number":
|
|
38
|
+
case "string":
|
|
39
|
+
case "boolean":
|
|
40
|
+
case "error":
|
|
41
|
+
case "empty": return GENERAL_FORMAT_ID;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The number-format code a cell resolves through -- its own explicit numberFormatCode, or the built-in code for its value kind's own default identifier. This is called identically during the workbook-wide format scan and later per cell, so the two can never resolve a cell to different codes. */
|
|
45
|
+
function formatCodeForCell(cell) {
|
|
46
|
+
return cell.numberFormatCode ?? builtinCode(defaultFormatIdForKind(cell.value.kind));
|
|
47
|
+
}
|
|
48
|
+
/** Scans every sheet's cells once, assigning each distinct number-format code a formatId: reusing a built-in id for a code matching one of excel-number-format's own BUILTIN_NUMBER_FORMATS strings exactly, minting a new custom id from FIRST_CUSTOM_FORMAT_ID otherwise. Cell XF index assignment is a separate, later pass (buildCellXfPlan below) -- a formatId alone no longer determines a cell's XF index once decoration exists, since two cells sharing a format but differing in background/borders need two distinct XFs. */
|
|
49
|
+
function buildFormatPlan(sheets) {
|
|
50
|
+
const codeToFormatId = /* @__PURE__ */ new Map();
|
|
51
|
+
const builtinIdByCode = new Map(Array.from(excel_number_format.BUILTIN_NUMBER_FORMATS, ([id, code]) => [code, id]));
|
|
52
|
+
const customFormats = [];
|
|
53
|
+
let nextCustomId = FIRST_CUSTOM_FORMAT_ID;
|
|
54
|
+
const resolve = (code) => {
|
|
55
|
+
const existing = codeToFormatId.get(code);
|
|
56
|
+
if (existing !== void 0) return existing;
|
|
57
|
+
const builtinId = builtinIdByCode.get(code);
|
|
58
|
+
let formatId;
|
|
59
|
+
if (builtinId !== void 0) formatId = builtinId;
|
|
60
|
+
else {
|
|
61
|
+
if (nextCustomId > LAST_CUSTOM_FORMAT_ID) throw new require_biff_write_errors.BiffWriteError(`workbook needs more than 219 distinct custom number formats, more than [MS-XLS] 2.4.126's own ${FIRST_CUSTOM_FORMAT_ID}-${LAST_CUSTOM_FORMAT_ID} custom-identifier range allows`);
|
|
62
|
+
formatId = nextCustomId;
|
|
63
|
+
nextCustomId += 1;
|
|
64
|
+
customFormats.push({
|
|
65
|
+
id: formatId,
|
|
66
|
+
code
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
codeToFormatId.set(code, formatId);
|
|
70
|
+
return formatId;
|
|
71
|
+
};
|
|
72
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
73
|
+
if (!require_written_cells.writesCellRecord(cell)) continue;
|
|
74
|
+
resolve(formatCodeForCell(cell));
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
customFormats,
|
|
78
|
+
formatIdOf: (code) => {
|
|
79
|
+
const id = codeToFormatId.get(code);
|
|
80
|
+
if (id === void 0) throw new require_biff_write_errors.BiffWriteError(`internal error: number-format code ${JSON.stringify(code)} was not registered during the workbook-wide format scan`);
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Scans every sheet's cells once for the distinct fill/border colours the workbook actually uses (background, and each present border side's own colour), then decides whether they all already have a home in the fixed default table (no Palette record needed) or whether at least one genuinely custom colour forces a real one -- in which case every distinct colour, not just the non-default ones, is allocated its own dedicated slot, so the whole 56-entry table is self-consistent and every reference resolves through it rather than a mix of "the file's own table" and "the implicit default". */
|
|
86
|
+
function buildPalettePlan(sheets) {
|
|
87
|
+
const colorByHex = /* @__PURE__ */ new Map();
|
|
88
|
+
const record = (color) => {
|
|
89
|
+
if (color === void 0) return;
|
|
90
|
+
const hex = (0, document_schema_js.colorToRgbHex)(color);
|
|
91
|
+
if (!colorByHex.has(hex)) colorByHex.set(hex, color);
|
|
92
|
+
};
|
|
93
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
94
|
+
if (!require_written_cells.writesCellRecord(cell)) continue;
|
|
95
|
+
record(cell.background);
|
|
96
|
+
record(cell.borders?.left?.color);
|
|
97
|
+
record(cell.borders?.right?.color);
|
|
98
|
+
record(cell.borders?.top?.color);
|
|
99
|
+
record(cell.borders?.bottom?.color);
|
|
100
|
+
}
|
|
101
|
+
const missing = (hex) => {
|
|
102
|
+
throw new require_biff_write_errors.BiffWriteError(`internal error: colour ${hex} was not registered during the workbook-wide palette scan`);
|
|
103
|
+
};
|
|
104
|
+
if (colorByHex.size === 0) return {
|
|
105
|
+
paletteColors: void 0,
|
|
106
|
+
icvOf: (color) => missing((0, document_schema_js.colorToRgbHex)(color))
|
|
107
|
+
};
|
|
108
|
+
const defaultIcvByHex = /* @__PURE__ */ new Map();
|
|
109
|
+
let needsCustomPalette = false;
|
|
110
|
+
for (const hex of colorByHex.keys()) {
|
|
111
|
+
const icv = require_biff_xf_colors.DEFAULT_PALETTE_HEX_TO_ICV.get(hex);
|
|
112
|
+
if (icv === void 0) {
|
|
113
|
+
needsCustomPalette = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
defaultIcvByHex.set(hex, icv);
|
|
117
|
+
}
|
|
118
|
+
if (!needsCustomPalette) return {
|
|
119
|
+
paletteColors: void 0,
|
|
120
|
+
icvOf: (color) => defaultIcvByHex.get((0, document_schema_js.colorToRgbHex)(color)) ?? missing((0, document_schema_js.colorToRgbHex)(color))
|
|
121
|
+
};
|
|
122
|
+
if (colorByHex.size > 56) throw new require_biff_write_errors.BiffWriteError(`workbook needs ${colorByHex.size} distinct decoration colours, more than the 56 entries [MS-XLS] 2.4.188's own Palette record can hold`);
|
|
123
|
+
const icvByHex = /* @__PURE__ */ new Map();
|
|
124
|
+
const paletteColors = [];
|
|
125
|
+
let nextIcv = 8;
|
|
126
|
+
for (const [hex, color] of colorByHex) {
|
|
127
|
+
icvByHex.set(hex, nextIcv);
|
|
128
|
+
paletteColors.push(color);
|
|
129
|
+
nextIcv += 1;
|
|
130
|
+
}
|
|
131
|
+
while (paletteColors.length < 56) paletteColors.push({
|
|
132
|
+
r: 0,
|
|
133
|
+
g: 0,
|
|
134
|
+
b: 0
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
paletteColors,
|
|
138
|
+
icvOf: (color) => icvByHex.get((0, document_schema_js.colorToRgbHex)(color)) ?? missing((0, document_schema_js.colorToRgbHex)(color))
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const UNDECORATED_EDGE = {
|
|
142
|
+
style: 0,
|
|
143
|
+
icv: 0
|
|
144
|
+
};
|
|
145
|
+
function resolveWriteEdge(border, icvOf) {
|
|
146
|
+
if (border === void 0) return UNDECORATED_EDGE;
|
|
147
|
+
return {
|
|
148
|
+
style: require_biff_xf_colors.borderStyleTokenFor(border),
|
|
149
|
+
icv: icvOf(border.color)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** A cell's own decoration, resolved into the raw XfDecorationFields the CellXF payload packs -- undefined for a cell with neither a background nor any border, so it shares the workbook's plain undecorated XF exactly as it did before decoration existed. The "has decoration at all" question is written-cells.ts's, since the writer's own record-emission predicate turns on the identical answer. */
|
|
153
|
+
function resolveDecorationForCell(cell, icvOf) {
|
|
154
|
+
if (!require_written_cells.cellCarriesFormatting(cell)) return;
|
|
155
|
+
return {
|
|
156
|
+
fillPattern: cell.background === void 0 ? 0 : 1,
|
|
157
|
+
fillForegroundIcv: cell.background === void 0 ? 64 : icvOf(cell.background),
|
|
158
|
+
left: resolveWriteEdge(cell.borders?.left, icvOf),
|
|
159
|
+
right: resolveWriteEdge(cell.borders?.right, icvOf),
|
|
160
|
+
top: resolveWriteEdge(cell.borders?.top, icvOf),
|
|
161
|
+
bottom: resolveWriteEdge(cell.borders?.bottom, icvOf)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** A deterministic signature for one cell XF's own (formatId, alignment, verticalAlignment, decoration) tuple, so two cells sharing all four share one XF record -- the interning key buildCellXfPlan below dedupes on, mirroring how CellFormatTable in ooxml.js's typed/xlsx/styles.ts dedupes an <xf> on (number format, decoration) together rather than on format alone, widened here by the cell's own alignment. */
|
|
165
|
+
function signatureOfCellXf(formatId, alignment, verticalAlignment, decoration) {
|
|
166
|
+
let signature = `f${formatId}|a${alignment ?? ""}|v${verticalAlignment ?? ""}`;
|
|
167
|
+
if (decoration === void 0) return signature;
|
|
168
|
+
signature += `|p${decoration.fillPattern}:${decoration.fillForegroundIcv}|l${decoration.left.style}:${decoration.left.icv}|r${decoration.right.style}:${decoration.right.icv}|t${decoration.top.style}:${decoration.top.icv}|b${decoration.bottom.style}:${decoration.bottom.icv}`;
|
|
169
|
+
return signature;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Scans every sheet's cells once, interning each distinct (number format, decoration) combination into its own cell XF index -- a cell with General formatting and no decoration resolves to the workbook's own implicit GENERAL_CELL_XF_INDEX with no new XF record at all, exactly as before; every other combination mints one XF record the first time it is seen and is reused by every later cell sharing it.
|
|
173
|
+
*
|
|
174
|
+
* The returned xfIndexForCell only ever LOOKS UP -- it cannot mint an entry, and refuses a signature this scan never saw. buildWorkbookGlobals is handed cellXfEntries before any sheet's records are built, so an entry minted later than this scan would be one no XF record was written for, and the cell record naming its index would point past the end of the workbook's XF table. Nothing about the resulting bytes says so: a reader resolves that index to whatever XF happens to sit there, or to none, and the cell's format is silently wrong either way. Refusing the lookup is the only place that divergence can still be caught.
|
|
175
|
+
*/
|
|
176
|
+
function buildCellXfPlan(sheets, formatPlan, palettePlan) {
|
|
177
|
+
const cellXfEntries = [];
|
|
178
|
+
const xfIndexBySignature = /* @__PURE__ */ new Map([[signatureOfCellXf(GENERAL_FORMAT_ID, void 0, void 0, void 0), require_workbook_globals_writer.GENERAL_CELL_XF_INDEX]]);
|
|
179
|
+
let nextXfIndex = require_workbook_globals_writer.GENERAL_CELL_XF_INDEX + 1;
|
|
180
|
+
const signatureOf = (cell) => signatureOfCellXf(formatPlan.formatIdOf(formatCodeForCell(cell)), cell.alignment, cell.verticalAlignment, resolveDecorationForCell(cell, palettePlan.icvOf));
|
|
181
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
182
|
+
if (!require_written_cells.writesCellRecord(cell)) continue;
|
|
183
|
+
const formatId = formatPlan.formatIdOf(formatCodeForCell(cell));
|
|
184
|
+
const decoration = resolveDecorationForCell(cell, palettePlan.icvOf);
|
|
185
|
+
const signature = signatureOfCellXf(formatId, cell.alignment, cell.verticalAlignment, decoration);
|
|
186
|
+
if (xfIndexBySignature.has(signature)) continue;
|
|
187
|
+
xfIndexBySignature.set(signature, nextXfIndex);
|
|
188
|
+
nextXfIndex += 1;
|
|
189
|
+
cellXfEntries.push({
|
|
190
|
+
formatId,
|
|
191
|
+
alignment: cell.alignment,
|
|
192
|
+
verticalAlignment: cell.verticalAlignment,
|
|
193
|
+
decoration
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
cellXfEntries,
|
|
198
|
+
xfIndexForCell: (cell) => {
|
|
199
|
+
const signature = signatureOf(cell);
|
|
200
|
+
const index = xfIndexBySignature.get(signature);
|
|
201
|
+
if (index === void 0) throw new require_biff_write_errors.BiffWriteError(`internal error: the cell at row ${cell.row}, column ${cell.column} resolves to cell-XF signature ${JSON.stringify(signature)}, which the workbook-wide cell-format scan never saw -- the writer's own "does this cell get a record" predicate and its XF-interning pass disagree about this cell`);
|
|
202
|
+
return index;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Scans every sheet's string-kind cells once, in sheet then cell order, assigning each distinct value the shared string table index every LabelSst cell referencing it uses. */
|
|
207
|
+
function buildSstPlan(sheets) {
|
|
208
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
209
|
+
const strings = [];
|
|
210
|
+
let totalCount = 0;
|
|
211
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
212
|
+
if (cell.value.kind !== "string") continue;
|
|
213
|
+
totalCount += 1;
|
|
214
|
+
if (!indexOf.has(cell.value.value)) {
|
|
215
|
+
indexOf.set(cell.value.value, strings.length);
|
|
216
|
+
strings.push(cell.value.value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
strings,
|
|
221
|
+
totalCount,
|
|
222
|
+
indexOf: (text) => {
|
|
223
|
+
const index = indexOf.get(text);
|
|
224
|
+
if (index === void 0) throw new require_biff_write_errors.BiffWriteError(`internal error: string ${JSON.stringify(text)} was not registered during the workbook-wide shared-string scan`);
|
|
225
|
+
return index;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The built-in Print_Area/Print_Titles defined names every sheet's own print settings need, across the whole workbook.
|
|
231
|
+
*
|
|
232
|
+
* Workbook-wide rather than per-sheet because that is where BIFF8 puts them: an Lbl lives in the globals substream and names its sheet through its own itab, so a sheet's print RANGE is written nowhere near the sheet's own records. The ixti each name's PtgArea3d refers to is the sheet's own index, matching the one-XTI-per-sheet ExternSheet record globals-writer.ts writes alongside them.
|
|
233
|
+
*/
|
|
234
|
+
function buildPrintNamePlan(sheets) {
|
|
235
|
+
return sheets.flatMap((sheet, sheetIndex) => require_workbook_print_names.printNameEntriesFor(sheetIndex, sheetIndex, sheet.printSettings));
|
|
236
|
+
}
|
|
237
|
+
/** Patches a BoundSheet8's own lbPlyPos field in place: a 4-byte little-endian integer at a byte offset globals-writer.ts already reported, once the real value -- where that sheet's own substream landed in the finished workbook stream -- is known. */
|
|
238
|
+
function patchBoundSheetOffsets(globalsBytes, offsets, values) {
|
|
239
|
+
const view = new DataView(globalsBytes.buffer, globalsBytes.byteOffset, globalsBytes.byteLength);
|
|
240
|
+
offsets.forEach((offset, index) => {
|
|
241
|
+
view.setUint32(offset, values[index] ?? 0, true);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function concatBytes(parts) {
|
|
245
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
246
|
+
const out = new Uint8Array(total);
|
|
247
|
+
let offset = 0;
|
|
248
|
+
for (const part of parts) {
|
|
249
|
+
out.set(part, offset);
|
|
250
|
+
offset += part.length;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/** Builds the [MS-XLS] Workbook stream: the globals substream followed by one worksheet substream per sheet, with every BoundSheet8's own lbPlyPos patched to the real byte offset its sheet's substream landed at. */
|
|
255
|
+
function buildWorkbookStream(content) {
|
|
256
|
+
if (content.sheets.length === 0) throw new require_biff_write_errors.BiffWriteError("a .xls workbook must contain at least one sheet ([MS-XLS] 2.1.7.20.3's own BUNDLESHEET production requires 1*BoundSheet8), but the document being written has none");
|
|
257
|
+
const formatPlan = buildFormatPlan(content.sheets);
|
|
258
|
+
const sstPlan = buildSstPlan(content.sheets);
|
|
259
|
+
const palettePlan = buildPalettePlan(content.sheets);
|
|
260
|
+
const cellXfPlan = buildCellXfPlan(content.sheets, formatPlan, palettePlan);
|
|
261
|
+
const globalsPlan = {
|
|
262
|
+
sheetNames: content.sheets.map((sheet) => sheet.name),
|
|
263
|
+
customFormats: formatPlan.customFormats,
|
|
264
|
+
cellXfEntries: cellXfPlan.cellXfEntries,
|
|
265
|
+
sharedStrings: sstPlan.strings,
|
|
266
|
+
sharedStringTotalCount: sstPlan.totalCount,
|
|
267
|
+
paletteColors: palettePlan.paletteColors,
|
|
268
|
+
printNames: buildPrintNamePlan(content.sheets)
|
|
269
|
+
};
|
|
270
|
+
const globals = require_workbook_globals_writer.buildWorkbookGlobals(globalsPlan);
|
|
271
|
+
const sheetContext = {
|
|
272
|
+
xfIndexForCell: cellXfPlan.xfIndexForCell,
|
|
273
|
+
sstIndexFor: (text) => sstPlan.indexOf(text)
|
|
274
|
+
};
|
|
275
|
+
const sheetStreams = content.sheets.map((sheet) => require_workbook_sheet_writer.buildWorksheetSubstream(sheet, sheetContext));
|
|
276
|
+
const sheetOffsets = [];
|
|
277
|
+
let offset = globals.bytes.length;
|
|
278
|
+
for (const stream of sheetStreams) {
|
|
279
|
+
sheetOffsets.push(offset);
|
|
280
|
+
offset += stream.length;
|
|
281
|
+
}
|
|
282
|
+
const globalsBytes = globals.bytes.slice();
|
|
283
|
+
patchBoundSheetOffsets(globalsBytes, globals.lbPlyPosOffsets, sheetOffsets);
|
|
284
|
+
return concatBytes([globalsBytes, ...sheetStreams]);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Writes a spreadsheet ContentDocument to real .xls bytes: a BIFF8 Workbook stream wrapped in an [MS-CFB] compound file.
|
|
288
|
+
*
|
|
289
|
+
* The counterpart of content.ts's readXlsContent. See this package's README for the writer's full scope.
|
|
290
|
+
*/
|
|
291
|
+
function writeXlsContent(content) {
|
|
292
|
+
const stream = buildWorkbookStream(content);
|
|
293
|
+
const streams = [{
|
|
294
|
+
path: WORKBOOK_STREAM_NAME,
|
|
295
|
+
bytes: stream
|
|
296
|
+
}];
|
|
297
|
+
if ((0, archive_codec.hasSummaryInformationFields)(content.metadata)) streams.push({
|
|
298
|
+
path: require_container.SUMMARY_INFORMATION_STREAM,
|
|
299
|
+
bytes: (0, archive_codec.writeSummaryInformationStream)(require_metadata.layoutMetadataToSummaryInformation(content.metadata))
|
|
300
|
+
});
|
|
301
|
+
return (0, archive_codec.writeCompoundFile)(streams);
|
|
302
|
+
}
|
|
303
|
+
/** Writes a DocumentTree of kind 'spreadsheet' to real .xls bytes, flattening it to a ContentDocument first -- the counterpart of content.ts's readXls. */
|
|
304
|
+
function writeXls(tree) {
|
|
305
|
+
const content = (0, document_schema_js.flattenTree)(tree);
|
|
306
|
+
if (content.kind !== "spreadsheet") throw new require_biff_write_errors.BiffWriteError(`writeXls was given a DocumentTree of kind '${content.kind}', but a .xls workbook can only be written from a 'spreadsheet' document`);
|
|
307
|
+
return writeXlsContent(content);
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
exports.writeXls = writeXls;
|
|
311
|
+
exports.writeXlsContent = writeXlsContent;
|
package/dist/write.d.cts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { XlsContentDocument } from "./content.cjs";
|
|
2
|
+
import { DocumentTree } from "document-schema.js";
|
|
3
|
+
//#region src/write.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Writes a spreadsheet ContentDocument to real .xls bytes: a BIFF8 Workbook stream wrapped in an [MS-CFB] compound file.
|
|
6
|
+
*
|
|
7
|
+
* The counterpart of content.ts's readXlsContent. See this package's README for the writer's full scope.
|
|
8
|
+
*/
|
|
9
|
+
declare function writeXlsContent(content: XlsContentDocument): Uint8Array<ArrayBuffer>;
|
|
10
|
+
/** Writes a DocumentTree of kind 'spreadsheet' to real .xls bytes, flattening it to a ContentDocument first -- the counterpart of content.ts's readXls. */
|
|
11
|
+
declare function writeXls(tree: DocumentTree): Uint8Array<ArrayBuffer>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { writeXls, writeXlsContent };
|
package/dist/write.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { XlsContentDocument } from "./content.js";
|
|
2
|
+
import { DocumentTree } from "document-schema.js";
|
|
3
|
+
//#region src/write.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Writes a spreadsheet ContentDocument to real .xls bytes: a BIFF8 Workbook stream wrapped in an [MS-CFB] compound file.
|
|
6
|
+
*
|
|
7
|
+
* The counterpart of content.ts's readXlsContent. See this package's README for the writer's full scope.
|
|
8
|
+
*/
|
|
9
|
+
declare function writeXlsContent(content: XlsContentDocument): Uint8Array<ArrayBuffer>;
|
|
10
|
+
/** Writes a DocumentTree of kind 'spreadsheet' to real .xls bytes, flattening it to a ContentDocument first -- the counterpart of content.ts's readXls. */
|
|
11
|
+
declare function writeXls(tree: DocumentTree): Uint8Array<ArrayBuffer>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { writeXls, writeXlsContent };
|
package/dist/write.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { SUMMARY_INFORMATION_STREAM } from "./container.js";
|
|
2
|
+
import { DEFAULT_PALETTE_HEX_TO_ICV, borderStyleTokenFor } from "./biff/xf-colors.js";
|
|
3
|
+
import { BiffWriteError } from "./biff/write-errors.js";
|
|
4
|
+
import { printNameEntriesFor } from "./workbook/print-names.js";
|
|
5
|
+
import { layoutMetadataToSummaryInformation as layoutMetadataToSummaryInformation$1 } from "./metadata.js";
|
|
6
|
+
import { GENERAL_CELL_XF_INDEX, buildWorkbookGlobals } from "./workbook/globals-writer.js";
|
|
7
|
+
import { cellCarriesFormatting, writesCellRecord } from "./written-cells.js";
|
|
8
|
+
import { buildWorksheetSubstream } from "./workbook/sheet-writer.js";
|
|
9
|
+
import { hasSummaryInformationFields, writeCompoundFile, writeSummaryInformationStream } from "archive-codec";
|
|
10
|
+
import { colorToRgbHex, flattenTree } from "document-schema.js";
|
|
11
|
+
import { BUILTIN_NUMBER_FORMATS } from "excel-number-format";
|
|
12
|
+
//#region src/write.ts
|
|
13
|
+
const WORKBOOK_STREAM_NAME = "Workbook";
|
|
14
|
+
/** [MS-XLS] 2.4.126: a Format record's own ifmt is constrained to the ranges 5-8, 23-26, 41-44, 63-66, and 164-382 for a custom (non-built-in) code; 164 is the first identifier every real producer actually uses for a custom code, and this writer follows suit, incrementing sequentially and refusing to exceed the range's own ceiling. */
|
|
15
|
+
const FIRST_CUSTOM_FORMAT_ID = 164;
|
|
16
|
+
const LAST_CUSTOM_FORMAT_ID = 382;
|
|
17
|
+
const BUILTIN_FORMAT_PERCENTAGE = 9;
|
|
18
|
+
const BUILTIN_FORMAT_CURRENCY = 44;
|
|
19
|
+
const BUILTIN_FORMAT_DATE = 14;
|
|
20
|
+
const BUILTIN_FORMAT_TIME = 21;
|
|
21
|
+
const BUILTIN_FORMAT_DATE_TIME = 22;
|
|
22
|
+
const GENERAL_FORMAT_ID = 0;
|
|
23
|
+
function builtinCode(id) {
|
|
24
|
+
const code = BUILTIN_NUMBER_FORMATS.get(id);
|
|
25
|
+
if (code === void 0) throw new BiffWriteError(`internal error: BUILTIN_NUMBER_FORMATS has no entry for id ${id}`);
|
|
26
|
+
return code;
|
|
27
|
+
}
|
|
28
|
+
/** The default format identifier a value kind resolves to when its own cell carries no numberFormatCode -- chosen so excel-number-format's classifyNumberFormat, run against the resulting code on the way back in, reclassifies to the identical kind. A plain number/string/boolean/error needs no distinguishing code at all: General classifies as 'number', which is exactly the fallback content.ts's own resolveValue already takes for a numeric cell with no format. */
|
|
29
|
+
function defaultFormatIdForKind(kind) {
|
|
30
|
+
switch (kind) {
|
|
31
|
+
case "percentage": return BUILTIN_FORMAT_PERCENTAGE;
|
|
32
|
+
case "currency": return BUILTIN_FORMAT_CURRENCY;
|
|
33
|
+
case "date": return BUILTIN_FORMAT_DATE;
|
|
34
|
+
case "time": return BUILTIN_FORMAT_TIME;
|
|
35
|
+
case "dateTime": return BUILTIN_FORMAT_DATE_TIME;
|
|
36
|
+
case "number":
|
|
37
|
+
case "string":
|
|
38
|
+
case "boolean":
|
|
39
|
+
case "error":
|
|
40
|
+
case "empty": return GENERAL_FORMAT_ID;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The number-format code a cell resolves through -- its own explicit numberFormatCode, or the built-in code for its value kind's own default identifier. This is called identically during the workbook-wide format scan and later per cell, so the two can never resolve a cell to different codes. */
|
|
44
|
+
function formatCodeForCell(cell) {
|
|
45
|
+
return cell.numberFormatCode ?? builtinCode(defaultFormatIdForKind(cell.value.kind));
|
|
46
|
+
}
|
|
47
|
+
/** Scans every sheet's cells once, assigning each distinct number-format code a formatId: reusing a built-in id for a code matching one of excel-number-format's own BUILTIN_NUMBER_FORMATS strings exactly, minting a new custom id from FIRST_CUSTOM_FORMAT_ID otherwise. Cell XF index assignment is a separate, later pass (buildCellXfPlan below) -- a formatId alone no longer determines a cell's XF index once decoration exists, since two cells sharing a format but differing in background/borders need two distinct XFs. */
|
|
48
|
+
function buildFormatPlan(sheets) {
|
|
49
|
+
const codeToFormatId = /* @__PURE__ */ new Map();
|
|
50
|
+
const builtinIdByCode = new Map(Array.from(BUILTIN_NUMBER_FORMATS, ([id, code]) => [code, id]));
|
|
51
|
+
const customFormats = [];
|
|
52
|
+
let nextCustomId = FIRST_CUSTOM_FORMAT_ID;
|
|
53
|
+
const resolve = (code) => {
|
|
54
|
+
const existing = codeToFormatId.get(code);
|
|
55
|
+
if (existing !== void 0) return existing;
|
|
56
|
+
const builtinId = builtinIdByCode.get(code);
|
|
57
|
+
let formatId;
|
|
58
|
+
if (builtinId !== void 0) formatId = builtinId;
|
|
59
|
+
else {
|
|
60
|
+
if (nextCustomId > LAST_CUSTOM_FORMAT_ID) throw new BiffWriteError(`workbook needs more than 219 distinct custom number formats, more than [MS-XLS] 2.4.126's own ${FIRST_CUSTOM_FORMAT_ID}-${LAST_CUSTOM_FORMAT_ID} custom-identifier range allows`);
|
|
61
|
+
formatId = nextCustomId;
|
|
62
|
+
nextCustomId += 1;
|
|
63
|
+
customFormats.push({
|
|
64
|
+
id: formatId,
|
|
65
|
+
code
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
codeToFormatId.set(code, formatId);
|
|
69
|
+
return formatId;
|
|
70
|
+
};
|
|
71
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
72
|
+
if (!writesCellRecord(cell)) continue;
|
|
73
|
+
resolve(formatCodeForCell(cell));
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
customFormats,
|
|
77
|
+
formatIdOf: (code) => {
|
|
78
|
+
const id = codeToFormatId.get(code);
|
|
79
|
+
if (id === void 0) throw new BiffWriteError(`internal error: number-format code ${JSON.stringify(code)} was not registered during the workbook-wide format scan`);
|
|
80
|
+
return id;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Scans every sheet's cells once for the distinct fill/border colours the workbook actually uses (background, and each present border side's own colour), then decides whether they all already have a home in the fixed default table (no Palette record needed) or whether at least one genuinely custom colour forces a real one -- in which case every distinct colour, not just the non-default ones, is allocated its own dedicated slot, so the whole 56-entry table is self-consistent and every reference resolves through it rather than a mix of "the file's own table" and "the implicit default". */
|
|
85
|
+
function buildPalettePlan(sheets) {
|
|
86
|
+
const colorByHex = /* @__PURE__ */ new Map();
|
|
87
|
+
const record = (color) => {
|
|
88
|
+
if (color === void 0) return;
|
|
89
|
+
const hex = colorToRgbHex(color);
|
|
90
|
+
if (!colorByHex.has(hex)) colorByHex.set(hex, color);
|
|
91
|
+
};
|
|
92
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
93
|
+
if (!writesCellRecord(cell)) continue;
|
|
94
|
+
record(cell.background);
|
|
95
|
+
record(cell.borders?.left?.color);
|
|
96
|
+
record(cell.borders?.right?.color);
|
|
97
|
+
record(cell.borders?.top?.color);
|
|
98
|
+
record(cell.borders?.bottom?.color);
|
|
99
|
+
}
|
|
100
|
+
const missing = (hex) => {
|
|
101
|
+
throw new BiffWriteError(`internal error: colour ${hex} was not registered during the workbook-wide palette scan`);
|
|
102
|
+
};
|
|
103
|
+
if (colorByHex.size === 0) return {
|
|
104
|
+
paletteColors: void 0,
|
|
105
|
+
icvOf: (color) => missing(colorToRgbHex(color))
|
|
106
|
+
};
|
|
107
|
+
const defaultIcvByHex = /* @__PURE__ */ new Map();
|
|
108
|
+
let needsCustomPalette = false;
|
|
109
|
+
for (const hex of colorByHex.keys()) {
|
|
110
|
+
const icv = DEFAULT_PALETTE_HEX_TO_ICV.get(hex);
|
|
111
|
+
if (icv === void 0) {
|
|
112
|
+
needsCustomPalette = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
defaultIcvByHex.set(hex, icv);
|
|
116
|
+
}
|
|
117
|
+
if (!needsCustomPalette) return {
|
|
118
|
+
paletteColors: void 0,
|
|
119
|
+
icvOf: (color) => defaultIcvByHex.get(colorToRgbHex(color)) ?? missing(colorToRgbHex(color))
|
|
120
|
+
};
|
|
121
|
+
if (colorByHex.size > 56) throw new BiffWriteError(`workbook needs ${colorByHex.size} distinct decoration colours, more than the 56 entries [MS-XLS] 2.4.188's own Palette record can hold`);
|
|
122
|
+
const icvByHex = /* @__PURE__ */ new Map();
|
|
123
|
+
const paletteColors = [];
|
|
124
|
+
let nextIcv = 8;
|
|
125
|
+
for (const [hex, color] of colorByHex) {
|
|
126
|
+
icvByHex.set(hex, nextIcv);
|
|
127
|
+
paletteColors.push(color);
|
|
128
|
+
nextIcv += 1;
|
|
129
|
+
}
|
|
130
|
+
while (paletteColors.length < 56) paletteColors.push({
|
|
131
|
+
r: 0,
|
|
132
|
+
g: 0,
|
|
133
|
+
b: 0
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
paletteColors,
|
|
137
|
+
icvOf: (color) => icvByHex.get(colorToRgbHex(color)) ?? missing(colorToRgbHex(color))
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const UNDECORATED_EDGE = {
|
|
141
|
+
style: 0,
|
|
142
|
+
icv: 0
|
|
143
|
+
};
|
|
144
|
+
function resolveWriteEdge(border, icvOf) {
|
|
145
|
+
if (border === void 0) return UNDECORATED_EDGE;
|
|
146
|
+
return {
|
|
147
|
+
style: borderStyleTokenFor(border),
|
|
148
|
+
icv: icvOf(border.color)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** A cell's own decoration, resolved into the raw XfDecorationFields the CellXF payload packs -- undefined for a cell with neither a background nor any border, so it shares the workbook's plain undecorated XF exactly as it did before decoration existed. The "has decoration at all" question is written-cells.ts's, since the writer's own record-emission predicate turns on the identical answer. */
|
|
152
|
+
function resolveDecorationForCell(cell, icvOf) {
|
|
153
|
+
if (!cellCarriesFormatting(cell)) return;
|
|
154
|
+
return {
|
|
155
|
+
fillPattern: cell.background === void 0 ? 0 : 1,
|
|
156
|
+
fillForegroundIcv: cell.background === void 0 ? 64 : icvOf(cell.background),
|
|
157
|
+
left: resolveWriteEdge(cell.borders?.left, icvOf),
|
|
158
|
+
right: resolveWriteEdge(cell.borders?.right, icvOf),
|
|
159
|
+
top: resolveWriteEdge(cell.borders?.top, icvOf),
|
|
160
|
+
bottom: resolveWriteEdge(cell.borders?.bottom, icvOf)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** A deterministic signature for one cell XF's own (formatId, alignment, verticalAlignment, decoration) tuple, so two cells sharing all four share one XF record -- the interning key buildCellXfPlan below dedupes on, mirroring how CellFormatTable in ooxml.js's typed/xlsx/styles.ts dedupes an <xf> on (number format, decoration) together rather than on format alone, widened here by the cell's own alignment. */
|
|
164
|
+
function signatureOfCellXf(formatId, alignment, verticalAlignment, decoration) {
|
|
165
|
+
let signature = `f${formatId}|a${alignment ?? ""}|v${verticalAlignment ?? ""}`;
|
|
166
|
+
if (decoration === void 0) return signature;
|
|
167
|
+
signature += `|p${decoration.fillPattern}:${decoration.fillForegroundIcv}|l${decoration.left.style}:${decoration.left.icv}|r${decoration.right.style}:${decoration.right.icv}|t${decoration.top.style}:${decoration.top.icv}|b${decoration.bottom.style}:${decoration.bottom.icv}`;
|
|
168
|
+
return signature;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Scans every sheet's cells once, interning each distinct (number format, decoration) combination into its own cell XF index -- a cell with General formatting and no decoration resolves to the workbook's own implicit GENERAL_CELL_XF_INDEX with no new XF record at all, exactly as before; every other combination mints one XF record the first time it is seen and is reused by every later cell sharing it.
|
|
172
|
+
*
|
|
173
|
+
* The returned xfIndexForCell only ever LOOKS UP -- it cannot mint an entry, and refuses a signature this scan never saw. buildWorkbookGlobals is handed cellXfEntries before any sheet's records are built, so an entry minted later than this scan would be one no XF record was written for, and the cell record naming its index would point past the end of the workbook's XF table. Nothing about the resulting bytes says so: a reader resolves that index to whatever XF happens to sit there, or to none, and the cell's format is silently wrong either way. Refusing the lookup is the only place that divergence can still be caught.
|
|
174
|
+
*/
|
|
175
|
+
function buildCellXfPlan(sheets, formatPlan, palettePlan) {
|
|
176
|
+
const cellXfEntries = [];
|
|
177
|
+
const xfIndexBySignature = /* @__PURE__ */ new Map([[signatureOfCellXf(GENERAL_FORMAT_ID, void 0, void 0, void 0), GENERAL_CELL_XF_INDEX]]);
|
|
178
|
+
let nextXfIndex = GENERAL_CELL_XF_INDEX + 1;
|
|
179
|
+
const signatureOf = (cell) => signatureOfCellXf(formatPlan.formatIdOf(formatCodeForCell(cell)), cell.alignment, cell.verticalAlignment, resolveDecorationForCell(cell, palettePlan.icvOf));
|
|
180
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
181
|
+
if (!writesCellRecord(cell)) continue;
|
|
182
|
+
const formatId = formatPlan.formatIdOf(formatCodeForCell(cell));
|
|
183
|
+
const decoration = resolveDecorationForCell(cell, palettePlan.icvOf);
|
|
184
|
+
const signature = signatureOfCellXf(formatId, cell.alignment, cell.verticalAlignment, decoration);
|
|
185
|
+
if (xfIndexBySignature.has(signature)) continue;
|
|
186
|
+
xfIndexBySignature.set(signature, nextXfIndex);
|
|
187
|
+
nextXfIndex += 1;
|
|
188
|
+
cellXfEntries.push({
|
|
189
|
+
formatId,
|
|
190
|
+
alignment: cell.alignment,
|
|
191
|
+
verticalAlignment: cell.verticalAlignment,
|
|
192
|
+
decoration
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
cellXfEntries,
|
|
197
|
+
xfIndexForCell: (cell) => {
|
|
198
|
+
const signature = signatureOf(cell);
|
|
199
|
+
const index = xfIndexBySignature.get(signature);
|
|
200
|
+
if (index === void 0) throw new BiffWriteError(`internal error: the cell at row ${cell.row}, column ${cell.column} resolves to cell-XF signature ${JSON.stringify(signature)}, which the workbook-wide cell-format scan never saw -- the writer's own "does this cell get a record" predicate and its XF-interning pass disagree about this cell`);
|
|
201
|
+
return index;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Scans every sheet's string-kind cells once, in sheet then cell order, assigning each distinct value the shared string table index every LabelSst cell referencing it uses. */
|
|
206
|
+
function buildSstPlan(sheets) {
|
|
207
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
208
|
+
const strings = [];
|
|
209
|
+
let totalCount = 0;
|
|
210
|
+
for (const sheet of sheets) for (const cell of sheet.cells) {
|
|
211
|
+
if (cell.value.kind !== "string") continue;
|
|
212
|
+
totalCount += 1;
|
|
213
|
+
if (!indexOf.has(cell.value.value)) {
|
|
214
|
+
indexOf.set(cell.value.value, strings.length);
|
|
215
|
+
strings.push(cell.value.value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
strings,
|
|
220
|
+
totalCount,
|
|
221
|
+
indexOf: (text) => {
|
|
222
|
+
const index = indexOf.get(text);
|
|
223
|
+
if (index === void 0) throw new BiffWriteError(`internal error: string ${JSON.stringify(text)} was not registered during the workbook-wide shared-string scan`);
|
|
224
|
+
return index;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The built-in Print_Area/Print_Titles defined names every sheet's own print settings need, across the whole workbook.
|
|
230
|
+
*
|
|
231
|
+
* Workbook-wide rather than per-sheet because that is where BIFF8 puts them: an Lbl lives in the globals substream and names its sheet through its own itab, so a sheet's print RANGE is written nowhere near the sheet's own records. The ixti each name's PtgArea3d refers to is the sheet's own index, matching the one-XTI-per-sheet ExternSheet record globals-writer.ts writes alongside them.
|
|
232
|
+
*/
|
|
233
|
+
function buildPrintNamePlan(sheets) {
|
|
234
|
+
return sheets.flatMap((sheet, sheetIndex) => printNameEntriesFor(sheetIndex, sheetIndex, sheet.printSettings));
|
|
235
|
+
}
|
|
236
|
+
/** Patches a BoundSheet8's own lbPlyPos field in place: a 4-byte little-endian integer at a byte offset globals-writer.ts already reported, once the real value -- where that sheet's own substream landed in the finished workbook stream -- is known. */
|
|
237
|
+
function patchBoundSheetOffsets(globalsBytes, offsets, values) {
|
|
238
|
+
const view = new DataView(globalsBytes.buffer, globalsBytes.byteOffset, globalsBytes.byteLength);
|
|
239
|
+
offsets.forEach((offset, index) => {
|
|
240
|
+
view.setUint32(offset, values[index] ?? 0, true);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function concatBytes(parts) {
|
|
244
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
245
|
+
const out = new Uint8Array(total);
|
|
246
|
+
let offset = 0;
|
|
247
|
+
for (const part of parts) {
|
|
248
|
+
out.set(part, offset);
|
|
249
|
+
offset += part.length;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
/** Builds the [MS-XLS] Workbook stream: the globals substream followed by one worksheet substream per sheet, with every BoundSheet8's own lbPlyPos patched to the real byte offset its sheet's substream landed at. */
|
|
254
|
+
function buildWorkbookStream(content) {
|
|
255
|
+
if (content.sheets.length === 0) throw new BiffWriteError("a .xls workbook must contain at least one sheet ([MS-XLS] 2.1.7.20.3's own BUNDLESHEET production requires 1*BoundSheet8), but the document being written has none");
|
|
256
|
+
const formatPlan = buildFormatPlan(content.sheets);
|
|
257
|
+
const sstPlan = buildSstPlan(content.sheets);
|
|
258
|
+
const palettePlan = buildPalettePlan(content.sheets);
|
|
259
|
+
const cellXfPlan = buildCellXfPlan(content.sheets, formatPlan, palettePlan);
|
|
260
|
+
const globalsPlan = {
|
|
261
|
+
sheetNames: content.sheets.map((sheet) => sheet.name),
|
|
262
|
+
customFormats: formatPlan.customFormats,
|
|
263
|
+
cellXfEntries: cellXfPlan.cellXfEntries,
|
|
264
|
+
sharedStrings: sstPlan.strings,
|
|
265
|
+
sharedStringTotalCount: sstPlan.totalCount,
|
|
266
|
+
paletteColors: palettePlan.paletteColors,
|
|
267
|
+
printNames: buildPrintNamePlan(content.sheets)
|
|
268
|
+
};
|
|
269
|
+
const globals = buildWorkbookGlobals(globalsPlan);
|
|
270
|
+
const sheetContext = {
|
|
271
|
+
xfIndexForCell: cellXfPlan.xfIndexForCell,
|
|
272
|
+
sstIndexFor: (text) => sstPlan.indexOf(text)
|
|
273
|
+
};
|
|
274
|
+
const sheetStreams = content.sheets.map((sheet) => buildWorksheetSubstream(sheet, sheetContext));
|
|
275
|
+
const sheetOffsets = [];
|
|
276
|
+
let offset = globals.bytes.length;
|
|
277
|
+
for (const stream of sheetStreams) {
|
|
278
|
+
sheetOffsets.push(offset);
|
|
279
|
+
offset += stream.length;
|
|
280
|
+
}
|
|
281
|
+
const globalsBytes = globals.bytes.slice();
|
|
282
|
+
patchBoundSheetOffsets(globalsBytes, globals.lbPlyPosOffsets, sheetOffsets);
|
|
283
|
+
return concatBytes([globalsBytes, ...sheetStreams]);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Writes a spreadsheet ContentDocument to real .xls bytes: a BIFF8 Workbook stream wrapped in an [MS-CFB] compound file.
|
|
287
|
+
*
|
|
288
|
+
* The counterpart of content.ts's readXlsContent. See this package's README for the writer's full scope.
|
|
289
|
+
*/
|
|
290
|
+
function writeXlsContent(content) {
|
|
291
|
+
const stream = buildWorkbookStream(content);
|
|
292
|
+
const streams = [{
|
|
293
|
+
path: WORKBOOK_STREAM_NAME,
|
|
294
|
+
bytes: stream
|
|
295
|
+
}];
|
|
296
|
+
if (hasSummaryInformationFields(content.metadata)) streams.push({
|
|
297
|
+
path: SUMMARY_INFORMATION_STREAM,
|
|
298
|
+
bytes: writeSummaryInformationStream(layoutMetadataToSummaryInformation$1(content.metadata))
|
|
299
|
+
});
|
|
300
|
+
return writeCompoundFile(streams);
|
|
301
|
+
}
|
|
302
|
+
/** Writes a DocumentTree of kind 'spreadsheet' to real .xls bytes, flattening it to a ContentDocument first -- the counterpart of content.ts's readXls. */
|
|
303
|
+
function writeXls(tree) {
|
|
304
|
+
const content = flattenTree(tree);
|
|
305
|
+
if (content.kind !== "spreadsheet") throw new BiffWriteError(`writeXls was given a DocumentTree of kind '${content.kind}', but a .xls workbook can only be written from a 'spreadsheet' document`);
|
|
306
|
+
return writeXlsContent(content);
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
export { writeXls, writeXlsContent };
|