xls-codec 4.11.0 → 4.12.0
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 +17 -17
- package/dist/workbook/conditional-format-write.cjs +93 -0
- package/dist/workbook/conditional-format-write.d.cts +5 -0
- package/dist/workbook/conditional-format-write.d.ts +5 -0
- package/dist/workbook/conditional-format-write.js +92 -0
- package/dist/workbook/data-validation-write.cjs +85 -0
- package/dist/workbook/data-validation-write.d.cts +5 -0
- package/dist/workbook/data-validation-write.d.ts +5 -0
- package/dist/workbook/data-validation-write.js +84 -0
- package/dist/workbook/sheet-writer.cjs +4 -0
- package/dist/workbook/sheet-writer.d.cts +3 -1
- package/dist/workbook/sheet-writer.d.ts +3 -1
- package/dist/workbook/sheet-writer.js +4 -0
- package/dist/write.cjs +16 -8
- package/dist/write.js +16 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -30,22 +30,22 @@ Verified primarily by round trip (`src/write.test.ts`, plus a dedicated `test/wo
|
|
|
30
30
|
|
|
31
31
|
### Writer scope
|
|
32
32
|
|
|
33
|
-
What `writeXlsContent`/`writeXls` cover: every `ContentCellValue` kind a real `.xls` can hold (`number`, `percentage`, `currency`, `boolean`, `date`, `time`, `dateTime`, `string`, `error`; an `empty` cell is written as a `Blank` record when it carries formatting and as nothing at all when it does not — see below), merged ranges (`colSpan`/`rowSpan`), row heights and hidden rows, column widths and hidden columns, multiple sheets, explicit and default number formats, a shared string table deduplicated across the whole workbook, every field of a sheet's own [print settings](#print-settings), a cell's own [alignment](#cell-alignment), a cell's own background fill and per-side borders (see [Cell decoration](#cell-decoration)), a same-sheet formula (see [Formula writing](#formula-writing)), and a cell's own comment text and author (see [Cell comments, written](#cell-comments-written)). What it deliberately does not:
|
|
34
|
-
|
|
35
|
-
| Not written | Why
|
|
36
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
-
| `Formula` records for a 3D (cross-sheet or external-workbook) reference, an array-constant literal or CSE array formula, a defined name, or a call to a function outside `Ftab`'s own vocabulary | Same-sheet formulas write (see [Formula writing](#formula-writing)); these four constructs each throw a `BiffWriteError` naming the construct instead, rather than emit a token stream the writer cannot prove round-trips. A 3D reference needs a `SupBook`/`ExternSheet` pair this writer only ever mints today for the two built-in print-settings names (see [Print settings](#print-settings)), not for an arbitrary formula; an array constant/CSE formula needs a `PtgExtraArray`/`Array`-record trailer this writer does not build; a defined name has nowhere to resolve against, the same gap the `Lbl` row below describes; and Excel 2007+ added many worksheet functions BIFF8's own `Ftab` enumeration never named, resolved through a `PtgNameX`/add-in mechanism this writer does not implement.
|
|
38
|
-
| Per-cell font | The reader does not read one back: `ContentSheetCell` has no cell-level font field at all (`ooxml.js`'s xlsx reader makes the identical font-scope choice), so writing a real value here would be unverifiable by round trip. Every `XF` this writer emits still references the same single font.
|
|
39
|
-
| `MulBlank`/`RK`/`MulRk` | Pure compaction optimisations over information a plain `Blank`/`Number`/`LabelSst`/`BoolErr` record already carries losslessly. (`Blank` itself _is_ written, for a formatted empty cell — see the `empty` row below.)
|
|
40
|
-
| An `empty`-kind cell carrying no formatting | Written as nothing at all, which is what round-trips: `content.ts`'s reader drops an unformatted blank cell it reads, and a merged range's empty anchor is independently reconstructed from `MergeCells` alone. A formatted one (a fill, a border, or a non-default alignment) is the opposite case — its formatting exists only in the `XF` a cell record names, so it gets a real `Blank` record ([MS-XLS] 2.4.20) and round-trips with that formatting intact.
|
|
41
|
-
| Images, embedded objects (charts/drawings) | The reader now recovers a sheet's own pictures, charts, and generic drawing shapes (see Read-side gaps below), but writing them back out is its own separate mechanism — a shape's own MS-ODRAW (Escher) container tree, a workbook-wide Blip Store, and (for a chart) a whole nested `BOF`/`EOF` substream, none of which this writer's existing per-record emission has any machinery for — scoped as its own piece of work rather than folded into recovering the read side, the same split this table already draws for `Formula` records. `ContentSheet.images` and `ContentSheet.embeddedObjects` are silently ignored on write.
|
|
42
|
-
| Defined names (`Lbl`) | Read only for the two built-in ones a sheet's print range and repeated header bands live in ([Print settings](#print-settings)); a user-defined name has nowhere to land in `document-schema.js`'s spreadsheet model, so there is no round trip to verify a writer for it against.
|
|
43
|
-
|
|
|
44
|
-
| Conditional formatting
|
|
45
|
-
| A comment's `replies` or `createdAt` | A cell's `text` and `author` write (see the Status list above); legacy BIFF8's `Note`/`Obj`/`Txo` triple has no threading and no per-comment timestamp at all, the same gap `src/workbook/comments.ts`'s own read side already documents, so there is nothing for these two fields to round-trip against.
|
|
46
|
-
| A page size no `iPaperSize` code names | Written as [MS-XLS] 2.4.257's own custom-paper value rather than as a named paper it is not — the dimensions themselves are unwritable, since `Setup` addresses paper only by code. See [Print settings](#print-settings).
|
|
47
|
-
| `Window1`/`Window2`, `CodePage`, `Index`/`DBCell`, the legacy interface records (`InterfaceHdr`, `WriteAccess`, …) | UI and interoperability bookkeeping [MS-XLS]'s own grammar names in the globals/worksheet substreams alongside the content-carrying records above, not data. `Index`/`DBCell` specifically is a pure cell-lookup performance optimisation (see [MS-XLS]'s own "Retrieval of Last-Calculated Cell Values Without Loading Cell Table") that this reader — and Excel's own reader — does not require to find a cell; real, well-established minimal BIFF8 writers (e.g. Python's `xlwt`) omit the same set and produce files Excel opens correctly. The calculation-state records (`CalcCount`, `CalcRefMode`, `CalcIter`, `CalcDelta`, `CalcSaveRecalc`) sat in this row until print settings needed them — see [Print settings](#print-settings) for why the writer emits them now.
|
|
48
|
-
| `Continue`-chain splitting | A record whose data would exceed the 8224-byte single-record ceiling ([MS-XLS] 2.1.4) — an extremely long shared string, an enormous shared string table, or thousands of merged ranges in one sheet — is refused with a thrown `BiffWriteError` rather than silently split across `Continue` records.
|
|
33
|
+
What `writeXlsContent`/`writeXls` cover: every `ContentCellValue` kind a real `.xls` can hold (`number`, `percentage`, `currency`, `boolean`, `date`, `time`, `dateTime`, `string`, `error`; an `empty` cell is written as a `Blank` record when it carries formatting and as nothing at all when it does not — see below), merged ranges (`colSpan`/`rowSpan`), row heights and hidden rows, column widths and hidden columns, multiple sheets, explicit and default number formats, a shared string table deduplicated across the whole workbook, every field of a sheet's own [print settings](#print-settings), a cell's own [alignment](#cell-alignment), a cell's own background fill and per-side borders (see [Cell decoration](#cell-decoration)), a same-sheet formula (see [Formula writing](#formula-writing)), and a cell's own comment text and author (see [Cell comments, written](#cell-comments-written)), a sheet's own data validations (one Dval wrapper plus one Dv record per rule, every schema field through the same `compileFormulaText` formula compiler cell formulas use), and a sheet's own `cellIs` conditional formats (one CondFmt plus one CF record per rule, its style's text colour and solid background encoded as a DXFN through the workbook's own palette). What it deliberately does not:
|
|
34
|
+
|
|
35
|
+
| Not written | Why |
|
|
36
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `Formula` records for a 3D (cross-sheet or external-workbook) reference, an array-constant literal or CSE array formula, a defined name, or a call to a function outside `Ftab`'s own vocabulary | Same-sheet formulas write (see [Formula writing](#formula-writing)); these four constructs each throw a `BiffWriteError` naming the construct instead, rather than emit a token stream the writer cannot prove round-trips. A 3D reference needs a `SupBook`/`ExternSheet` pair this writer only ever mints today for the two built-in print-settings names (see [Print settings](#print-settings)), not for an arbitrary formula; an array constant/CSE formula needs a `PtgExtraArray`/`Array`-record trailer this writer does not build; a defined name has nowhere to resolve against, the same gap the `Lbl` row below describes; and Excel 2007+ added many worksheet functions BIFF8's own `Ftab` enumeration never named, resolved through a `PtgNameX`/add-in mechanism this writer does not implement. |
|
|
38
|
+
| Per-cell font | The reader does not read one back: `ContentSheetCell` has no cell-level font field at all (`ooxml.js`'s xlsx reader makes the identical font-scope choice), so writing a real value here would be unverifiable by round trip. Every `XF` this writer emits still references the same single font. |
|
|
39
|
+
| `MulBlank`/`RK`/`MulRk` | Pure compaction optimisations over information a plain `Blank`/`Number`/`LabelSst`/`BoolErr` record already carries losslessly. (`Blank` itself _is_ written, for a formatted empty cell — see the `empty` row below.) |
|
|
40
|
+
| An `empty`-kind cell carrying no formatting | Written as nothing at all, which is what round-trips: `content.ts`'s reader drops an unformatted blank cell it reads, and a merged range's empty anchor is independently reconstructed from `MergeCells` alone. A formatted one (a fill, a border, or a non-default alignment) is the opposite case — its formatting exists only in the `XF` a cell record names, so it gets a real `Blank` record ([MS-XLS] 2.4.20) and round-trips with that formatting intact. |
|
|
41
|
+
| Images, embedded objects (charts/drawings) | The reader now recovers a sheet's own pictures, charts, and generic drawing shapes (see Read-side gaps below), but writing them back out is its own separate mechanism — a shape's own MS-ODRAW (Escher) container tree, a workbook-wide Blip Store, and (for a chart) a whole nested `BOF`/`EOF` substream, none of which this writer's existing per-record emission has any machinery for — scoped as its own piece of work rather than folded into recovering the read side, the same split this table already draws for `Formula` records. `ContentSheet.images` and `ContentSheet.embeddedObjects` are silently ignored on write. |
|
|
42
|
+
| Defined names (`Lbl`) | Read only for the two built-in ones a sheet's print range and repeated header bands live in ([Print settings](#print-settings)); a user-defined name has nowhere to land in `document-schema.js`'s spreadsheet model, so there is no round trip to verify a writer for it against. |
|
|
43
|
+
| A data validation whose formula text `compileFormulaText` cannot encode | Every other rule shape writes (see the lead paragraph); a formula naming a construct outside the same-sheet `Ptg` vocabulary the cell-formula writer supports throws the identical `BiffWriteError` that cell formula already does, rather than emit a token stream this writer cannot prove round-trips. A comparison-typed rule carrying no operator, or a two-operand operator (`between`/`notBetween`) carrying fewer or more than its two formulas, is a malformed model and throws naming it. |
|
|
44
|
+
| Conditional formatting beyond the base `cellIs` comparison (`CondFmt12`/`CF12`/`CFEx`) | A `cellIs` rule writes as one `CondFmt` plus one `CF` record -- its operator, both formula operands through `compileFormulaText`, and its style's text colour and solid background as a `DXFN` resolved through the workbook's own palette (`workbook/conditional-format-write.ts`, the write-side inverse of `readCondFmtGroup`/`readCf`/`parseDxfStyle`). Every other variant the schema models (colour scale, data bar, icon set, the text family, `top10`, ...) is a CF12-era rule with no base-CF spelling, and CF12 writing is its own mechanism this writer does not build -- such a rule throws a `BiffWriteError` naming its type rather than being silently dropped, the identical refusal the four out-of-scope formula constructs already draw. A `cellIs` rule's `priority`/`stopIfTrue`/`source` have no base-CF field and are not written. |
|
|
45
|
+
| A comment's `replies` or `createdAt` | A cell's `text` and `author` write (see the Status list above); legacy BIFF8's `Note`/`Obj`/`Txo` triple has no threading and no per-comment timestamp at all, the same gap `src/workbook/comments.ts`'s own read side already documents, so there is nothing for these two fields to round-trip against. |
|
|
46
|
+
| A page size no `iPaperSize` code names | Written as [MS-XLS] 2.4.257's own custom-paper value rather than as a named paper it is not — the dimensions themselves are unwritable, since `Setup` addresses paper only by code. See [Print settings](#print-settings). |
|
|
47
|
+
| `Window1`/`Window2`, `CodePage`, `Index`/`DBCell`, the legacy interface records (`InterfaceHdr`, `WriteAccess`, …) | UI and interoperability bookkeeping [MS-XLS]'s own grammar names in the globals/worksheet substreams alongside the content-carrying records above, not data. `Index`/`DBCell` specifically is a pure cell-lookup performance optimisation (see [MS-XLS]'s own "Retrieval of Last-Calculated Cell Values Without Loading Cell Table") that this reader — and Excel's own reader — does not require to find a cell; real, well-established minimal BIFF8 writers (e.g. Python's `xlwt`) omit the same set and produce files Excel opens correctly. The calculation-state records (`CalcCount`, `CalcRefMode`, `CalcIter`, `CalcDelta`, `CalcSaveRecalc`) sat in this row until print settings needed them — see [Print settings](#print-settings) for why the writer emits them now. |
|
|
48
|
+
| `Continue`-chain splitting | A record whose data would exceed the 8224-byte single-record ceiling ([MS-XLS] 2.1.4) — an extremely long shared string, an enormous shared string table, or thousands of merged ranges in one sheet — is refused with a thrown `BiffWriteError` rather than silently split across `Continue` records. |
|
|
49
49
|
|
|
50
50
|
Column widths round-trip to the nearest pixel Excel's own integer-pixel-grid quantization allows (matching the read direction's own "honestly approximate" contract, `units.ts`), never narrower than requested. A `.xls` cell outside BIFF8's own grid (65536 rows, 256 columns) is refused rather than silently wrapped or truncated.
|
|
51
51
|
|
|
@@ -66,7 +66,7 @@ Each deliberate rather than overlooked:
|
|
|
66
66
|
- **Not read at all:** a `CFEx` record that extends a genuine `CF12` record rather than a legacy `CF` (see above). Defined names (`Lbl`) are read only for the two built-in ones a sheet's print range and repeated header bands live in ([Print settings](#print-settings)); a user-defined name has nowhere to land in `document-schema.js`'s spreadsheet model, so it is skipped.
|
|
67
67
|
- **RC4-encrypted and XOR-obfuscated workbooks, decrypted for real (ExaDev/documents.js#1108, #922).** A `FilePass` record ([MS-XLS] 2.4.117) means every record after it is ciphertext under one of three schemes; this reader implements two of them, dispatching on the record's own `wEncryptionType` -- the [MS-OFFCRYPTO] 2.3.6.1 "RC4 encryption header" and 2.3.7's own XOR obfuscation (Method 1) -- `readXlsContent`/`readXls` take an optional `password`, verify it against the header's own fields (RC4's `EncryptedVerifier`/`EncryptedVerifierHash`, decrypted then compared; XOR obfuscation's `key`/`verificationBytes`, plain unencrypted checksums compared directly), then decrypt every subsequent record's data (`src/workbook/encryption.ts`) using the RC4/MD5 and XOR-obfuscation primitives [`archive-codec`](../archive-codec/README.md#xor-obfuscation-xlsdoc) shares with `doc-codec`. XOR obfuscation's own per-record `XorArrayIndex` -- `(streamOffset + recordDataLength) % 16`, `recordDataLength` always the record's own full declared size regardless of how much of a given span is actually being decrypted -- is confirmed against a genuine Excel-generated fixture, not just the published spec text (see `archive-codec`'s own account of why that spec text alone is not trustworthy here). [MS-XLS] 2.2.10's own exclusion list -- `BOF`, `FilePass`, and four shared-workbook revision-tracking records this package otherwise never reads, plus `BoundSheet8`'s own `lbPlyPos` field specifically -- is honoured exactly for both schemes, re-derived independently in this package's own tests rather than trusted by construction. A missing or incorrect password throws rather than returning a garbled document; the newer "RC4 CryptoAPI encryption header" scheme remains explicitly out of scope (ppt-codec's own encryption, tracked separately on [#1116](https://github.com/ExaDev/documents.js/issues/1116)).
|
|
68
68
|
|
|
69
|
-
This package is wired into `documents.js`'s conversion registry (`xlsToPdf`/`pdfToXls`, `convertDocument("xls", ...)`, and every same-variant spreadsheet bridge) — see that package's own README Fidelity table for exactly which pairs route and which don't. Per-cell font is the one remaining read+write scope gap that is genuinely permanent rather than pending (see the Writer scope table below); formula writing
|
|
69
|
+
This package is wired into `documents.js`'s conversion registry (`xlsToPdf`/`pdfToXls`, `convertDocument("xls", ...)`, and every same-variant spreadsheet bridge) — see that package's own README Fidelity table for exactly which pairs route and which don't. Per-cell font is the one remaining read+write scope gap that is genuinely permanent rather than pending (see the Writer scope table below); formula writing, cell comments, data validation writing, and `cellIs` conditional-format writing are covered, each within the boundary the sections and the Writer scope table describe. Images/embedded objects (their MS-ODRAW/Escher container trees and Blip Store) and defined names remain read-only — the remaining [#971](https://github.com/ExaDev/documents.js/issues/971) scope.
|
|
70
70
|
|
|
71
71
|
## Cell decoration
|
|
72
72
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("../biff/record-types.cjs");
|
|
3
|
+
const require_biff_write_errors = require("../biff/write-errors.cjs");
|
|
4
|
+
const require_biff_builder = require("../biff/builder.cjs");
|
|
5
|
+
const require_biff_record_writer = require("../biff/record-writer.cjs");
|
|
6
|
+
const require_biff_ptg_writer = require("../biff/ptg-writer.cjs");
|
|
7
|
+
//#region src/workbook/conditional-format-write.ts
|
|
8
|
+
const CP_BY_OPERATOR = /* @__PURE__ */ new Map([
|
|
9
|
+
["between", 1],
|
|
10
|
+
["notBetween", 2],
|
|
11
|
+
["equal", 3],
|
|
12
|
+
["notEqual", 4],
|
|
13
|
+
["greaterThan", 5],
|
|
14
|
+
["lessThan", 6],
|
|
15
|
+
["greaterThanOrEqual", 7],
|
|
16
|
+
["lessThanOrEqual", 8]
|
|
17
|
+
]);
|
|
18
|
+
const DXFFNTD_LENGTH = 122;
|
|
19
|
+
const DXFFNTD_ICV_FORE_OFFSET = 80;
|
|
20
|
+
function writeDxfn(style, icvOf) {
|
|
21
|
+
const textColor = style?.textColor;
|
|
22
|
+
const background = style?.background;
|
|
23
|
+
if (textColor === void 0 && background === void 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
24
|
+
const flags = new require_biff_builder.RecordBuilder();
|
|
25
|
+
let flagsWord = 0;
|
|
26
|
+
if (textColor !== void 0) flagsWord |= 1 << 26;
|
|
27
|
+
if (background !== void 0) flagsWord |= 1 << 29;
|
|
28
|
+
flags.u32(flagsWord);
|
|
29
|
+
flags.u16(0);
|
|
30
|
+
if (textColor !== void 0) {
|
|
31
|
+
const fontBlock = new Uint8Array(DXFFNTD_LENGTH);
|
|
32
|
+
new DataView(fontBlock.buffer, fontBlock.byteOffset, fontBlock.byteLength).setInt32(DXFFNTD_ICV_FORE_OFFSET, icvOf(textColor), true);
|
|
33
|
+
flags.bytes(fontBlock);
|
|
34
|
+
}
|
|
35
|
+
if (background !== void 0) {
|
|
36
|
+
const patWord = 1024 | icvOf(background) << 16;
|
|
37
|
+
flags.u32(patWord >>> 0);
|
|
38
|
+
}
|
|
39
|
+
return flags.build();
|
|
40
|
+
}
|
|
41
|
+
function writeCfRecord(rule, icvOf) {
|
|
42
|
+
const cp = CP_BY_OPERATOR.get(rule.operator);
|
|
43
|
+
if (cp === void 0) throw new require_biff_write_errors.BiffWriteError(`conditional-format operator "${rule.operator}" has no CF cp value`);
|
|
44
|
+
const rgce1 = require_biff_ptg_writer.compileFormulaText(rule.formula1);
|
|
45
|
+
const rgce2 = rule.formula2 !== void 0 ? require_biff_ptg_writer.compileFormulaText(rule.formula2) : void 0;
|
|
46
|
+
const dxf = writeDxfn(rule.style, icvOf);
|
|
47
|
+
const writer = new require_biff_builder.RecordBuilder().u8(1).u8(cp).u16(rgce1.length).u16(rgce2?.length ?? 0).bytes(dxf).bytes(rgce1);
|
|
48
|
+
if (rgce2 !== void 0) writer.bytes(rgce2);
|
|
49
|
+
return require_biff_record_writer.writeRecord(433, writer.build());
|
|
50
|
+
}
|
|
51
|
+
function boundingBoxOf(rule) {
|
|
52
|
+
let startRow = Number.MAX_SAFE_INTEGER;
|
|
53
|
+
let endRow = 0;
|
|
54
|
+
let startColumn = Number.MAX_SAFE_INTEGER;
|
|
55
|
+
let endColumn = 0;
|
|
56
|
+
for (const range of rule.ranges) {
|
|
57
|
+
startRow = Math.min(startRow, range.startRow);
|
|
58
|
+
endRow = Math.max(endRow, range.endRow);
|
|
59
|
+
startColumn = Math.min(startColumn, range.startColumn);
|
|
60
|
+
endColumn = Math.max(endColumn, range.endColumn);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
startRow,
|
|
64
|
+
endRow,
|
|
65
|
+
startColumn,
|
|
66
|
+
endColumn
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function writeSheetConditionalFormats(sheet, icvOf) {
|
|
70
|
+
const rules = sheet.conditionalFormats ?? [];
|
|
71
|
+
if (rules.length === 0) return [];
|
|
72
|
+
const pieces = [];
|
|
73
|
+
for (const [index, rule] of rules.entries()) {
|
|
74
|
+
if (rule.type !== "cellIs") throw new require_biff_write_errors.BiffWriteError(`a conditional-format rule of type "${rule.type}" has no base BIFF8 CF spelling; this writer states cellIs comparisons only (CF12 writing is out of scope), so the rule cannot be written rather than silently dropped`);
|
|
75
|
+
if (rule.ranges.length === 0) throw new require_biff_write_errors.BiffWriteError("a conditional-format rule carrying no range states nothing; the schema requires at least one");
|
|
76
|
+
for (const range of rule.ranges) if (range.startRow > 65535 || range.endRow > 65535 || range.startColumn > 255 || range.endColumn > 255) throw new require_biff_write_errors.BiffWriteError(`a conditional-format range (rows ${range.startRow}-${range.endRow}, columns ${range.startColumn}-${range.endColumn}) is outside BIFF8's own grid; a .xls workbook cannot address it`);
|
|
77
|
+
const nID = index + 1;
|
|
78
|
+
if (nID > 32767) throw new require_biff_write_errors.BiffWriteError(`this sheet's ${rules.length} conditional-format rules exceed CondFmt's own 15-bit nID field`);
|
|
79
|
+
const box = boundingBoxOf(rule);
|
|
80
|
+
const header = new require_biff_builder.RecordBuilder().u16(1).u16(nID << 1).u16(box.startRow).u16(box.endRow).u16(box.startColumn).u16(box.endColumn).u16(rule.ranges.length);
|
|
81
|
+
for (const range of rule.ranges) {
|
|
82
|
+
header.u16(range.startRow);
|
|
83
|
+
header.u16(range.endRow);
|
|
84
|
+
header.u16(range.startColumn);
|
|
85
|
+
header.u16(range.endColumn);
|
|
86
|
+
}
|
|
87
|
+
pieces.push(require_biff_record_writer.writeRecord(432, header.build()));
|
|
88
|
+
pieces.push(writeCfRecord(rule, icvOf));
|
|
89
|
+
}
|
|
90
|
+
return pieces;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
exports.writeSheetConditionalFormats = writeSheetConditionalFormats;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Color, ContentSheet } from "document-schema.js";
|
|
2
|
+
//#region src/workbook/conditional-format-write.d.ts
|
|
3
|
+
declare function writeSheetConditionalFormats(sheet: ContentSheet, icvOf: (color: Color) => number): Uint8Array<ArrayBuffer>[];
|
|
4
|
+
//#endregion
|
|
5
|
+
export { writeSheetConditionalFormats };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Color, ContentSheet } from "document-schema.js";
|
|
2
|
+
//#region src/workbook/conditional-format-write.d.ts
|
|
3
|
+
declare function writeSheetConditionalFormats(sheet: ContentSheet, icvOf: (color: Color) => number): Uint8Array<ArrayBuffer>[];
|
|
4
|
+
//#endregion
|
|
5
|
+
export { writeSheetConditionalFormats };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import "../biff/record-types.js";
|
|
2
|
+
import { BiffWriteError } from "../biff/write-errors.js";
|
|
3
|
+
import { RecordBuilder } from "../biff/builder.js";
|
|
4
|
+
import { writeRecord } from "../biff/record-writer.js";
|
|
5
|
+
import { compileFormulaText } from "../biff/ptg-writer.js";
|
|
6
|
+
//#region src/workbook/conditional-format-write.ts
|
|
7
|
+
const CP_BY_OPERATOR = /* @__PURE__ */ new Map([
|
|
8
|
+
["between", 1],
|
|
9
|
+
["notBetween", 2],
|
|
10
|
+
["equal", 3],
|
|
11
|
+
["notEqual", 4],
|
|
12
|
+
["greaterThan", 5],
|
|
13
|
+
["lessThan", 6],
|
|
14
|
+
["greaterThanOrEqual", 7],
|
|
15
|
+
["lessThanOrEqual", 8]
|
|
16
|
+
]);
|
|
17
|
+
const DXFFNTD_LENGTH = 122;
|
|
18
|
+
const DXFFNTD_ICV_FORE_OFFSET = 80;
|
|
19
|
+
function writeDxfn(style, icvOf) {
|
|
20
|
+
const textColor = style?.textColor;
|
|
21
|
+
const background = style?.background;
|
|
22
|
+
if (textColor === void 0 && background === void 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
23
|
+
const flags = new RecordBuilder();
|
|
24
|
+
let flagsWord = 0;
|
|
25
|
+
if (textColor !== void 0) flagsWord |= 1 << 26;
|
|
26
|
+
if (background !== void 0) flagsWord |= 1 << 29;
|
|
27
|
+
flags.u32(flagsWord);
|
|
28
|
+
flags.u16(0);
|
|
29
|
+
if (textColor !== void 0) {
|
|
30
|
+
const fontBlock = new Uint8Array(DXFFNTD_LENGTH);
|
|
31
|
+
new DataView(fontBlock.buffer, fontBlock.byteOffset, fontBlock.byteLength).setInt32(DXFFNTD_ICV_FORE_OFFSET, icvOf(textColor), true);
|
|
32
|
+
flags.bytes(fontBlock);
|
|
33
|
+
}
|
|
34
|
+
if (background !== void 0) {
|
|
35
|
+
const patWord = 1024 | icvOf(background) << 16;
|
|
36
|
+
flags.u32(patWord >>> 0);
|
|
37
|
+
}
|
|
38
|
+
return flags.build();
|
|
39
|
+
}
|
|
40
|
+
function writeCfRecord(rule, icvOf) {
|
|
41
|
+
const cp = CP_BY_OPERATOR.get(rule.operator);
|
|
42
|
+
if (cp === void 0) throw new BiffWriteError(`conditional-format operator "${rule.operator}" has no CF cp value`);
|
|
43
|
+
const rgce1 = compileFormulaText(rule.formula1);
|
|
44
|
+
const rgce2 = rule.formula2 !== void 0 ? compileFormulaText(rule.formula2) : void 0;
|
|
45
|
+
const dxf = writeDxfn(rule.style, icvOf);
|
|
46
|
+
const writer = new RecordBuilder().u8(1).u8(cp).u16(rgce1.length).u16(rgce2?.length ?? 0).bytes(dxf).bytes(rgce1);
|
|
47
|
+
if (rgce2 !== void 0) writer.bytes(rgce2);
|
|
48
|
+
return writeRecord(433, writer.build());
|
|
49
|
+
}
|
|
50
|
+
function boundingBoxOf(rule) {
|
|
51
|
+
let startRow = Number.MAX_SAFE_INTEGER;
|
|
52
|
+
let endRow = 0;
|
|
53
|
+
let startColumn = Number.MAX_SAFE_INTEGER;
|
|
54
|
+
let endColumn = 0;
|
|
55
|
+
for (const range of rule.ranges) {
|
|
56
|
+
startRow = Math.min(startRow, range.startRow);
|
|
57
|
+
endRow = Math.max(endRow, range.endRow);
|
|
58
|
+
startColumn = Math.min(startColumn, range.startColumn);
|
|
59
|
+
endColumn = Math.max(endColumn, range.endColumn);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
startRow,
|
|
63
|
+
endRow,
|
|
64
|
+
startColumn,
|
|
65
|
+
endColumn
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function writeSheetConditionalFormats(sheet, icvOf) {
|
|
69
|
+
const rules = sheet.conditionalFormats ?? [];
|
|
70
|
+
if (rules.length === 0) return [];
|
|
71
|
+
const pieces = [];
|
|
72
|
+
for (const [index, rule] of rules.entries()) {
|
|
73
|
+
if (rule.type !== "cellIs") throw new BiffWriteError(`a conditional-format rule of type "${rule.type}" has no base BIFF8 CF spelling; this writer states cellIs comparisons only (CF12 writing is out of scope), so the rule cannot be written rather than silently dropped`);
|
|
74
|
+
if (rule.ranges.length === 0) throw new BiffWriteError("a conditional-format rule carrying no range states nothing; the schema requires at least one");
|
|
75
|
+
for (const range of rule.ranges) if (range.startRow > 65535 || range.endRow > 65535 || range.startColumn > 255 || range.endColumn > 255) throw new BiffWriteError(`a conditional-format range (rows ${range.startRow}-${range.endRow}, columns ${range.startColumn}-${range.endColumn}) is outside BIFF8's own grid; a .xls workbook cannot address it`);
|
|
76
|
+
const nID = index + 1;
|
|
77
|
+
if (nID > 32767) throw new BiffWriteError(`this sheet's ${rules.length} conditional-format rules exceed CondFmt's own 15-bit nID field`);
|
|
78
|
+
const box = boundingBoxOf(rule);
|
|
79
|
+
const header = new RecordBuilder().u16(1).u16(nID << 1).u16(box.startRow).u16(box.endRow).u16(box.startColumn).u16(box.endColumn).u16(rule.ranges.length);
|
|
80
|
+
for (const range of rule.ranges) {
|
|
81
|
+
header.u16(range.startRow);
|
|
82
|
+
header.u16(range.endRow);
|
|
83
|
+
header.u16(range.startColumn);
|
|
84
|
+
header.u16(range.endColumn);
|
|
85
|
+
}
|
|
86
|
+
pieces.push(writeRecord(432, header.build()));
|
|
87
|
+
pieces.push(writeCfRecord(rule, icvOf));
|
|
88
|
+
}
|
|
89
|
+
return pieces;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { writeSheetConditionalFormats };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("../biff/record-types.cjs");
|
|
3
|
+
const require_biff_write_errors = require("../biff/write-errors.cjs");
|
|
4
|
+
const require_biff_builder = require("../biff/builder.cjs");
|
|
5
|
+
const require_biff_record_writer = require("../biff/record-writer.cjs");
|
|
6
|
+
const require_biff_string_writer = require("../biff/string-writer.cjs");
|
|
7
|
+
const require_biff_ptg_writer = require("../biff/ptg-writer.cjs");
|
|
8
|
+
//#region src/workbook/data-validation-write.ts
|
|
9
|
+
const VAL_TYPE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
10
|
+
["whole", 1],
|
|
11
|
+
["decimal", 2],
|
|
12
|
+
["list", 3],
|
|
13
|
+
["date", 4],
|
|
14
|
+
["time", 5],
|
|
15
|
+
["textLength", 6],
|
|
16
|
+
["custom", 7]
|
|
17
|
+
]);
|
|
18
|
+
const TYP_OPERATOR_BY_OPERATOR = /* @__PURE__ */ new Map([
|
|
19
|
+
["between", 0],
|
|
20
|
+
["notBetween", 1],
|
|
21
|
+
["equal", 2],
|
|
22
|
+
["notEqual", 3],
|
|
23
|
+
["greaterThan", 4],
|
|
24
|
+
["lessThan", 5],
|
|
25
|
+
["greaterThanOrEqual", 6],
|
|
26
|
+
["lessThanOrEqual", 7]
|
|
27
|
+
]);
|
|
28
|
+
const ERR_STYLE_CODE = /* @__PURE__ */ new Map([
|
|
29
|
+
["stop", 0],
|
|
30
|
+
["warning", 1],
|
|
31
|
+
["information", 2]
|
|
32
|
+
]);
|
|
33
|
+
function writeDvParsedFormula(text) {
|
|
34
|
+
const rgce = text === void 0 ? /* @__PURE__ */ new Uint8Array(0) : require_biff_ptg_writer.compileFormulaText(text);
|
|
35
|
+
return [new require_biff_builder.RecordBuilder().u16(rgce.length).u16(0).build(), rgce];
|
|
36
|
+
}
|
|
37
|
+
function writeDvRecord(validation) {
|
|
38
|
+
const valType = VAL_TYPE_BY_TYPE.get(validation.type);
|
|
39
|
+
if (valType === void 0) throw new require_biff_write_errors.BiffWriteError(`data validation type "${validation.type}" has no Dv valType value`);
|
|
40
|
+
let flags = valType;
|
|
41
|
+
flags |= (ERR_STYLE_CODE.get(validation.errorStyle ?? "stop") ?? 0) << 4;
|
|
42
|
+
if (validation.allowBlank === true) flags |= 256;
|
|
43
|
+
if (validation.showInputMessage === true) flags |= 1 << 18;
|
|
44
|
+
if (validation.showErrorMessage === true) flags |= 1 << 19;
|
|
45
|
+
if (validation.type !== "list" && validation.type !== "custom") {
|
|
46
|
+
if (validation.operator === void 0) throw new require_biff_write_errors.BiffWriteError(`a ${validation.type} data validation over ${validation.ranges.length} range(s) carries no operator; the schema requires one for every type but 'list' and 'custom'`);
|
|
47
|
+
const typOperator = TYP_OPERATOR_BY_OPERATOR.get(validation.operator);
|
|
48
|
+
if (typOperator === void 0) throw new require_biff_write_errors.BiffWriteError(`data validation operator "${validation.operator}" has no Dv typOperator value`);
|
|
49
|
+
flags |= typOperator << 20;
|
|
50
|
+
}
|
|
51
|
+
const isTwoOperand = validation.operator === "between" || validation.operator === "notBetween";
|
|
52
|
+
if (!isTwoOperand && validation.formula2 !== void 0) throw new require_biff_write_errors.BiffWriteError(`a data validation with operator "${validation.operator}" carries a second formula; only 'between' and 'notBetween' state two operands`);
|
|
53
|
+
if (isTwoOperand && validation.formula2 === void 0) throw new require_biff_write_errors.BiffWriteError(`a data validation with operator "${validation.operator}" carries no second formula; 'between' and 'notBetween' require two operands`);
|
|
54
|
+
if (validation.ranges.length === 0) throw new require_biff_write_errors.BiffWriteError("a data validation carrying no range states nothing; the schema requires at least one");
|
|
55
|
+
const writer = new require_biff_builder.RecordBuilder();
|
|
56
|
+
writer.u32(flags);
|
|
57
|
+
writer.bytes(require_biff_string_writer.writeXLUnicodeString(validation.promptTitle ?? ""));
|
|
58
|
+
writer.bytes(require_biff_string_writer.writeXLUnicodeString(validation.errorTitle ?? ""));
|
|
59
|
+
writer.bytes(require_biff_string_writer.writeXLUnicodeString(validation.prompt ?? ""));
|
|
60
|
+
writer.bytes(require_biff_string_writer.writeXLUnicodeString(validation.error ?? ""));
|
|
61
|
+
const [f1Header, f1Rgce] = writeDvParsedFormula(validation.formula1);
|
|
62
|
+
writer.bytes(f1Header).bytes(f1Rgce);
|
|
63
|
+
const [f2Header, f2Rgce] = writeDvParsedFormula(validation.formula2);
|
|
64
|
+
writer.bytes(f2Header).bytes(f2Rgce);
|
|
65
|
+
writer.u16(validation.ranges.length);
|
|
66
|
+
for (const range of validation.ranges) {
|
|
67
|
+
writer.u16(range.startRow);
|
|
68
|
+
writer.u16(range.endRow);
|
|
69
|
+
writer.u16(range.startColumn);
|
|
70
|
+
writer.u16(range.endColumn);
|
|
71
|
+
}
|
|
72
|
+
return require_biff_record_writer.writeRecord(446, writer.build());
|
|
73
|
+
}
|
|
74
|
+
function writeDvalRecord(ruleCount) {
|
|
75
|
+
const writer = new require_biff_builder.RecordBuilder().u16(0).u32(0).u32(0).u32(4294967295).u32(ruleCount);
|
|
76
|
+
return require_biff_record_writer.writeRecord(434, writer.build());
|
|
77
|
+
}
|
|
78
|
+
function writeSheetDataValidations(sheet) {
|
|
79
|
+
const validations = sheet.dataValidations ?? [];
|
|
80
|
+
if (validations.length === 0) return [];
|
|
81
|
+
for (const validation of validations) for (const range of validation.ranges) if (range.startRow > 65535 || range.endRow > 65535 || range.startColumn > 255 || range.endColumn > 255) throw new require_biff_write_errors.BiffWriteError(`a data validation range (rows ${range.startRow}-${range.endRow}, columns ${range.startColumn}-${range.endColumn}) is outside BIFF8's own grid; a .xls workbook cannot address it`);
|
|
82
|
+
return [writeDvalRecord(validations.length), ...validations.map((validation) => writeDvRecord(validation))];
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
exports.writeSheetDataValidations = writeSheetDataValidations;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import "../biff/record-types.js";
|
|
2
|
+
import { BiffWriteError } from "../biff/write-errors.js";
|
|
3
|
+
import { RecordBuilder } from "../biff/builder.js";
|
|
4
|
+
import { writeRecord } from "../biff/record-writer.js";
|
|
5
|
+
import { writeXLUnicodeString } from "../biff/string-writer.js";
|
|
6
|
+
import { compileFormulaText } from "../biff/ptg-writer.js";
|
|
7
|
+
//#region src/workbook/data-validation-write.ts
|
|
8
|
+
const VAL_TYPE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
9
|
+
["whole", 1],
|
|
10
|
+
["decimal", 2],
|
|
11
|
+
["list", 3],
|
|
12
|
+
["date", 4],
|
|
13
|
+
["time", 5],
|
|
14
|
+
["textLength", 6],
|
|
15
|
+
["custom", 7]
|
|
16
|
+
]);
|
|
17
|
+
const TYP_OPERATOR_BY_OPERATOR = /* @__PURE__ */ new Map([
|
|
18
|
+
["between", 0],
|
|
19
|
+
["notBetween", 1],
|
|
20
|
+
["equal", 2],
|
|
21
|
+
["notEqual", 3],
|
|
22
|
+
["greaterThan", 4],
|
|
23
|
+
["lessThan", 5],
|
|
24
|
+
["greaterThanOrEqual", 6],
|
|
25
|
+
["lessThanOrEqual", 7]
|
|
26
|
+
]);
|
|
27
|
+
const ERR_STYLE_CODE = /* @__PURE__ */ new Map([
|
|
28
|
+
["stop", 0],
|
|
29
|
+
["warning", 1],
|
|
30
|
+
["information", 2]
|
|
31
|
+
]);
|
|
32
|
+
function writeDvParsedFormula(text) {
|
|
33
|
+
const rgce = text === void 0 ? /* @__PURE__ */ new Uint8Array(0) : compileFormulaText(text);
|
|
34
|
+
return [new RecordBuilder().u16(rgce.length).u16(0).build(), rgce];
|
|
35
|
+
}
|
|
36
|
+
function writeDvRecord(validation) {
|
|
37
|
+
const valType = VAL_TYPE_BY_TYPE.get(validation.type);
|
|
38
|
+
if (valType === void 0) throw new BiffWriteError(`data validation type "${validation.type}" has no Dv valType value`);
|
|
39
|
+
let flags = valType;
|
|
40
|
+
flags |= (ERR_STYLE_CODE.get(validation.errorStyle ?? "stop") ?? 0) << 4;
|
|
41
|
+
if (validation.allowBlank === true) flags |= 256;
|
|
42
|
+
if (validation.showInputMessage === true) flags |= 1 << 18;
|
|
43
|
+
if (validation.showErrorMessage === true) flags |= 1 << 19;
|
|
44
|
+
if (validation.type !== "list" && validation.type !== "custom") {
|
|
45
|
+
if (validation.operator === void 0) throw new BiffWriteError(`a ${validation.type} data validation over ${validation.ranges.length} range(s) carries no operator; the schema requires one for every type but 'list' and 'custom'`);
|
|
46
|
+
const typOperator = TYP_OPERATOR_BY_OPERATOR.get(validation.operator);
|
|
47
|
+
if (typOperator === void 0) throw new BiffWriteError(`data validation operator "${validation.operator}" has no Dv typOperator value`);
|
|
48
|
+
flags |= typOperator << 20;
|
|
49
|
+
}
|
|
50
|
+
const isTwoOperand = validation.operator === "between" || validation.operator === "notBetween";
|
|
51
|
+
if (!isTwoOperand && validation.formula2 !== void 0) throw new BiffWriteError(`a data validation with operator "${validation.operator}" carries a second formula; only 'between' and 'notBetween' state two operands`);
|
|
52
|
+
if (isTwoOperand && validation.formula2 === void 0) throw new BiffWriteError(`a data validation with operator "${validation.operator}" carries no second formula; 'between' and 'notBetween' require two operands`);
|
|
53
|
+
if (validation.ranges.length === 0) throw new BiffWriteError("a data validation carrying no range states nothing; the schema requires at least one");
|
|
54
|
+
const writer = new RecordBuilder();
|
|
55
|
+
writer.u32(flags);
|
|
56
|
+
writer.bytes(writeXLUnicodeString(validation.promptTitle ?? ""));
|
|
57
|
+
writer.bytes(writeXLUnicodeString(validation.errorTitle ?? ""));
|
|
58
|
+
writer.bytes(writeXLUnicodeString(validation.prompt ?? ""));
|
|
59
|
+
writer.bytes(writeXLUnicodeString(validation.error ?? ""));
|
|
60
|
+
const [f1Header, f1Rgce] = writeDvParsedFormula(validation.formula1);
|
|
61
|
+
writer.bytes(f1Header).bytes(f1Rgce);
|
|
62
|
+
const [f2Header, f2Rgce] = writeDvParsedFormula(validation.formula2);
|
|
63
|
+
writer.bytes(f2Header).bytes(f2Rgce);
|
|
64
|
+
writer.u16(validation.ranges.length);
|
|
65
|
+
for (const range of validation.ranges) {
|
|
66
|
+
writer.u16(range.startRow);
|
|
67
|
+
writer.u16(range.endRow);
|
|
68
|
+
writer.u16(range.startColumn);
|
|
69
|
+
writer.u16(range.endColumn);
|
|
70
|
+
}
|
|
71
|
+
return writeRecord(446, writer.build());
|
|
72
|
+
}
|
|
73
|
+
function writeDvalRecord(ruleCount) {
|
|
74
|
+
const writer = new RecordBuilder().u16(0).u32(0).u32(0).u32(4294967295).u32(ruleCount);
|
|
75
|
+
return writeRecord(434, writer.build());
|
|
76
|
+
}
|
|
77
|
+
function writeSheetDataValidations(sheet) {
|
|
78
|
+
const validations = sheet.dataValidations ?? [];
|
|
79
|
+
if (validations.length === 0) return [];
|
|
80
|
+
for (const validation of validations) for (const range of validation.ranges) if (range.startRow > 65535 || range.endRow > 65535 || range.startColumn > 255 || range.endColumn > 255) throw new BiffWriteError(`a data validation range (rows ${range.startRow}-${range.endRow}, columns ${range.startColumn}-${range.endColumn}) is outside BIFF8's own grid; a .xls workbook cannot address it`);
|
|
81
|
+
return [writeDvalRecord(validations.length), ...validations.map((validation) => writeDvRecord(validation))];
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { writeSheetDataValidations };
|
|
@@ -12,6 +12,8 @@ const require_biff_string_writer = require("../biff/string-writer.cjs");
|
|
|
12
12
|
const require_workbook_globals_writer = require("./globals-writer.cjs");
|
|
13
13
|
const require_biff_ptg_writer = require("../biff/ptg-writer.cjs");
|
|
14
14
|
const require_written_cells = require("../written-cells.cjs");
|
|
15
|
+
const require_workbook_conditional_format_write = require("./conditional-format-write.cjs");
|
|
16
|
+
const require_workbook_data_validation_write = require("./data-validation-write.cjs");
|
|
15
17
|
const require_workbook_comment_writer = require("./comment-writer.cjs");
|
|
16
18
|
//#region src/workbook/sheet-writer.ts
|
|
17
19
|
/** BIFF8's own 16-bit row index and 8-bit column index ceilings ([MS-XLS] 2.4.221's Rw structure and 2.4.53's Col256U structure): 65536 rows (0-65535), 256 columns (0-255) -- unlike xlsx's much larger grid. A cell outside this range cannot be expressed in BIFF8 at all, so it is refused rather than silently truncated into a wrapped index. */
|
|
@@ -318,6 +320,8 @@ function buildWorksheetSubstream(sheet, ctx) {
|
|
|
318
320
|
if (merges.length > 0) pieces.push(writeMergeCellsRecord(merges));
|
|
319
321
|
const commentedCells = sheet.cells.filter((cell) => cell.comment !== void 0);
|
|
320
322
|
if (commentedCells.length > 0) pieces.push(...require_workbook_comment_writer.writeSheetComments(commentedCells));
|
|
323
|
+
pieces.push(...require_workbook_data_validation_write.writeSheetDataValidations(sheet));
|
|
324
|
+
pieces.push(...require_workbook_conditional_format_write.writeSheetConditionalFormats(sheet, ctx.icvOf));
|
|
321
325
|
pieces.push(require_biff_record_writer.writeRecord(10, /* @__PURE__ */ new Uint8Array(0)));
|
|
322
326
|
return require_biff_record_writer.concatRecords(...pieces);
|
|
323
327
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ContentSheet, ContentSheetCell } from "document-schema.js";
|
|
1
|
+
import { Color, ContentSheet, ContentSheetCell } from "document-schema.js";
|
|
2
2
|
//#region src/workbook/sheet-writer.d.ts
|
|
3
3
|
interface SheetWriteContext {
|
|
4
|
+
/** The icv a colour resolves to through the workbook's own palette plan (write.ts's buildPalettePlan) -- the same colour-table resolution a cell decoration's own fill already draws, offered to the conditional-format writer whose DXFN style colours are palette references too. Every colour this is called with must already have been registered during that plan's own workbook-wide scan. */
|
|
5
|
+
icvOf: (color: Color) => number;
|
|
4
6
|
/** The XF index ([MS-XLS] 2.5.168 IXFCell) a cell's own (number format, alignment, decoration) combination resolves to -- GENERAL_CELL_XF_INDEX for a cell with General formatting, general/bottom alignment, and no background/borders, one of the workbook's other cell XFs otherwise. write.ts's own cell-format interning pass is what assigns and deduplicates these. */
|
|
5
7
|
xfIndexForCell(cell: ContentSheetCell): number;
|
|
6
8
|
/** The shared string table index for a string cell's own text; every string a sheet writes must already be registered in the workbook-wide table before this is called. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ContentSheet, ContentSheetCell } from "document-schema.js";
|
|
1
|
+
import { Color, ContentSheet, ContentSheetCell } from "document-schema.js";
|
|
2
2
|
//#region src/workbook/sheet-writer.d.ts
|
|
3
3
|
interface SheetWriteContext {
|
|
4
|
+
/** The icv a colour resolves to through the workbook's own palette plan (write.ts's buildPalettePlan) -- the same colour-table resolution a cell decoration's own fill already draws, offered to the conditional-format writer whose DXFN style colours are palette references too. Every colour this is called with must already have been registered during that plan's own workbook-wide scan. */
|
|
5
|
+
icvOf: (color: Color) => number;
|
|
4
6
|
/** The XF index ([MS-XLS] 2.5.168 IXFCell) a cell's own (number format, alignment, decoration) combination resolves to -- GENERAL_CELL_XF_INDEX for a cell with General formatting, general/bottom alignment, and no background/borders, one of the workbook's other cell XFs otherwise. write.ts's own cell-format interning pass is what assigns and deduplicates these. */
|
|
5
7
|
xfIndexForCell(cell: ContentSheetCell): number;
|
|
6
8
|
/** The shared string table index for a string cell's own text; every string a sheet writes must already be registered in the workbook-wide table before this is called. */
|
|
@@ -11,6 +11,8 @@ import { writeXLUnicodeString } from "../biff/string-writer.js";
|
|
|
11
11
|
import { GENERAL_CELL_XF_INDEX } from "./globals-writer.js";
|
|
12
12
|
import { compileFormulaText } from "../biff/ptg-writer.js";
|
|
13
13
|
import { cellCarriesFormatting, writesCellRecord } from "../written-cells.js";
|
|
14
|
+
import { writeSheetConditionalFormats } from "./conditional-format-write.js";
|
|
15
|
+
import { writeSheetDataValidations } from "./data-validation-write.js";
|
|
14
16
|
import { writeSheetComments } from "./comment-writer.js";
|
|
15
17
|
//#region src/workbook/sheet-writer.ts
|
|
16
18
|
/** BIFF8's own 16-bit row index and 8-bit column index ceilings ([MS-XLS] 2.4.221's Rw structure and 2.4.53's Col256U structure): 65536 rows (0-65535), 256 columns (0-255) -- unlike xlsx's much larger grid. A cell outside this range cannot be expressed in BIFF8 at all, so it is refused rather than silently truncated into a wrapped index. */
|
|
@@ -317,6 +319,8 @@ function buildWorksheetSubstream(sheet, ctx) {
|
|
|
317
319
|
if (merges.length > 0) pieces.push(writeMergeCellsRecord(merges));
|
|
318
320
|
const commentedCells = sheet.cells.filter((cell) => cell.comment !== void 0);
|
|
319
321
|
if (commentedCells.length > 0) pieces.push(...writeSheetComments(commentedCells));
|
|
322
|
+
pieces.push(...writeSheetDataValidations(sheet));
|
|
323
|
+
pieces.push(...writeSheetConditionalFormats(sheet, ctx.icvOf));
|
|
320
324
|
pieces.push(writeRecord(10, /* @__PURE__ */ new Uint8Array(0)));
|
|
321
325
|
return concatRecords(...pieces);
|
|
322
326
|
}
|
package/dist/write.cjs
CHANGED
|
@@ -99,13 +99,20 @@ function buildPalettePlan(sheets) {
|
|
|
99
99
|
record(fill.foregroundColor);
|
|
100
100
|
record(fill.backgroundColor);
|
|
101
101
|
};
|
|
102
|
-
for (const sheet of sheets)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
for (const sheet of sheets) {
|
|
103
|
+
for (const cell of sheet.cells) {
|
|
104
|
+
if (!require_written_cells.writesCellRecord(cell)) continue;
|
|
105
|
+
recordFill(cell.background);
|
|
106
|
+
record(cell.borders?.left?.color);
|
|
107
|
+
record(cell.borders?.right?.color);
|
|
108
|
+
record(cell.borders?.top?.color);
|
|
109
|
+
record(cell.borders?.bottom?.color);
|
|
110
|
+
}
|
|
111
|
+
for (const rule of sheet.conditionalFormats ?? []) {
|
|
112
|
+
if (rule.type !== "cellIs") continue;
|
|
113
|
+
record(rule.style?.textColor);
|
|
114
|
+
record(rule.style?.background);
|
|
115
|
+
}
|
|
109
116
|
}
|
|
110
117
|
const missing = (hex) => {
|
|
111
118
|
throw new require_biff_write_errors.BiffWriteError(`internal error: colour ${hex} was not registered during the workbook-wide palette scan`);
|
|
@@ -303,7 +310,8 @@ function buildWorkbookStream(content) {
|
|
|
303
310
|
const globals = require_workbook_globals_writer.buildWorkbookGlobals(globalsPlan);
|
|
304
311
|
const sheetContext = {
|
|
305
312
|
xfIndexForCell: cellXfPlan.xfIndexForCell,
|
|
306
|
-
sstIndexFor: (text) => sstPlan.indexOf(text)
|
|
313
|
+
sstIndexFor: (text) => sstPlan.indexOf(text),
|
|
314
|
+
icvOf: palettePlan.icvOf
|
|
307
315
|
};
|
|
308
316
|
const sheetStreams = content.sheets.map((sheet) => require_workbook_sheet_writer.buildWorksheetSubstream(sheet, sheetContext));
|
|
309
317
|
const sheetOffsets = [];
|
package/dist/write.js
CHANGED
|
@@ -98,13 +98,20 @@ function buildPalettePlan(sheets) {
|
|
|
98
98
|
record(fill.foregroundColor);
|
|
99
99
|
record(fill.backgroundColor);
|
|
100
100
|
};
|
|
101
|
-
for (const sheet of sheets)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
for (const sheet of sheets) {
|
|
102
|
+
for (const cell of sheet.cells) {
|
|
103
|
+
if (!writesCellRecord(cell)) continue;
|
|
104
|
+
recordFill(cell.background);
|
|
105
|
+
record(cell.borders?.left?.color);
|
|
106
|
+
record(cell.borders?.right?.color);
|
|
107
|
+
record(cell.borders?.top?.color);
|
|
108
|
+
record(cell.borders?.bottom?.color);
|
|
109
|
+
}
|
|
110
|
+
for (const rule of sheet.conditionalFormats ?? []) {
|
|
111
|
+
if (rule.type !== "cellIs") continue;
|
|
112
|
+
record(rule.style?.textColor);
|
|
113
|
+
record(rule.style?.background);
|
|
114
|
+
}
|
|
108
115
|
}
|
|
109
116
|
const missing = (hex) => {
|
|
110
117
|
throw new BiffWriteError(`internal error: colour ${hex} was not registered during the workbook-wide palette scan`);
|
|
@@ -302,7 +309,8 @@ function buildWorkbookStream(content) {
|
|
|
302
309
|
const globals = buildWorkbookGlobals(globalsPlan);
|
|
303
310
|
const sheetContext = {
|
|
304
311
|
xfIndexForCell: cellXfPlan.xfIndexForCell,
|
|
305
|
-
sstIndexFor: (text) => sstPlan.indexOf(text)
|
|
312
|
+
sstIndexFor: (text) => sstPlan.indexOf(text),
|
|
313
|
+
icvOf: palettePlan.icvOf
|
|
306
314
|
};
|
|
307
315
|
const sheetStreams = content.sheets.map((sheet) => buildWorksheetSubstream(sheet, sheetContext));
|
|
308
316
|
const sheetOffsets = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xls-codec",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0",
|
|
4
4
|
"description": "Hand-written reader for the legacy Excel Binary File Format (.xls, BIFF8) as specified by [MS-XLS], mapping a workbook's record stream onto the shared document-schema.js spreadsheet model - the .xls codec for the documents.js family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
},
|
|
71
71
|
"packageManager": "pnpm@11.6.0",
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"archive-codec": "1.10.
|
|
74
|
-
"document-schema.js": "7.5.
|
|
73
|
+
"archive-codec": "1.10.1",
|
|
74
|
+
"document-schema.js": "7.5.1",
|
|
75
75
|
"excel-number-format": "1.1.1"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|