xls-codec 1.0.2 → 2.0.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 +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/README.md
CHANGED
|
@@ -2,35 +2,135 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ExaDev/documents.js/tree/main/packages/xls-codec) [](https://www.npmjs.com/package/xls-codec) [](https://www.npmjs.com/package/xls-codec) [](https://github.com/ExaDev/documents.js/actions)
|
|
4
4
|
|
|
5
|
-
> 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 same `document-schema.js` spreadsheet model `ooxml.js`'s xlsx support and `odf.js`'s ods support target. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
|
|
5
|
+
> Hand-written reader and writer for the legacy Excel Binary File Format (`.xls`, BIFF8) as specified by [MS-XLS], mapping a workbook's record stream onto the same `document-schema.js` spreadsheet model `ooxml.js`'s xlsx support and `odf.js`'s ods support target. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
|
|
6
6
|
|
|
7
|
-
A `.xls` file is not one format but two nested ones. The outer container is an [MS-CFB] compound file — the same "filesystem in a file" that carries `.doc` and `.ppt` — holding a stream named `Workbook`. Inside that stream is BIFF8: a flat sequence of records, each a two-byte type, a two-byte size, and that many bytes of data, organised into substreams delimited by `BOF`/`EOF`. This package leaves the outer layer to [`archive-codec`](../archive-codec/README.md)'s bounded CFB reader and implements the inner one, from the record framing up to a `ContentDocument
|
|
7
|
+
A `.xls` file is not one format but two nested ones. The outer container is an [MS-CFB] compound file — the same "filesystem in a file" that carries `.doc` and `.ppt` — holding a stream named `Workbook`. Inside that stream is BIFF8: a flat sequence of records, each a two-byte type, a two-byte size, and that many bytes of data, organised into substreams delimited by `BOF`/`EOF`. This package leaves the outer layer to [`archive-codec`](../archive-codec/README.md)'s bounded CFB reader and writer and implements the inner one, from the record framing up to a `ContentDocument` and back.
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
|
|
11
|
-
Under active development, **read
|
|
11
|
+
Under active development, with real, tested **read and write** support. Built and shipped:
|
|
12
12
|
|
|
13
|
-
- **Record framing** (`src/biff/records.ts`) — the three-component record structure of [MS-XLS] 2.1.4, with the 8224-byte data ceiling enforced and every malformed stream thrown on rather than silently truncated.
|
|
14
|
-
- **Continuation-aware cursor and strings** (`src/biff/cursor.ts`, `src/biff/strings.ts`) — `Continue` records ([MS-XLS] 2.4.58) joined per the rules of the record being continued, including the case a naive reader gets wrong: an `XLUnicodeRichExtendedString` ([MS-XLS] 2.5.293) resuming after a boundary re-states its own `fHighByte` flag, which may differ from the flag the string started with. All three string shapes (`XLUnicodeString`, `ShortXLUnicodeString`, `XLUnicodeRichExtendedString`) are read.
|
|
15
|
-
- **Workbook globals
|
|
16
|
-
- **Worksheet substreams
|
|
17
|
-
- **Number-format classification and date serials** (`
|
|
18
|
-
- **
|
|
13
|
+
- **Record framing** (`src/biff/records.ts`, `src/biff/record-writer.ts`) — the three-component record structure of [MS-XLS] 2.1.4 in both directions, with the 8224-byte data ceiling enforced and every malformed or oversized stream thrown on rather than silently truncated or split into a `Continue` chain the writer does not implement.
|
|
14
|
+
- **Continuation-aware cursor and strings** (`src/biff/cursor.ts`, `src/biff/strings.ts`, `src/biff/string-writer.ts`) — `Continue` records ([MS-XLS] 2.4.58) joined per the rules of the record being continued on read, including the case a naive reader gets wrong: an `XLUnicodeRichExtendedString` ([MS-XLS] 2.5.293) resuming after a boundary re-states its own `fHighByte` flag, which may differ from the flag the string started with. All three string shapes (`XLUnicodeString`, `ShortXLUnicodeString`, `XLUnicodeRichExtendedString`) are read and written, compressed (one byte per UTF-16 code unit) whenever every character allows it and uncompressed otherwise.
|
|
15
|
+
- **Workbook globals**, read (`src/workbook/globals.ts`) and write (`src/workbook/globals-writer.ts`) — `BoundSheet8` (sheet names, tab order, hidden state, type, and substream offsets), `SST` with its `Continue` chain on read, `Format` (custom number-format codes), `Font`, `XF`'s fixed prefix plus its trailing `CellXF`/`StyleXF` fill/border payload in both directions (`src/biff/xf-colors.ts`'s shared bit-layout packing/unpacking; see [Cell decoration](#cell-decoration)), `Palette` in both directions, the fifteen mandatory built-in `Style` records, and `Date1904`.
|
|
16
|
+
- **Worksheet substreams**, read (`src/workbook/sheet.ts`) and write (`src/workbook/sheet-writer.ts`) — `Dimensions`, `Row` (height and hidden state), `ColInfo` (width and hidden state), `MergeCells`, the print-settings record group (see [Print settings](#print-settings)), and the cell-value family: `Number`, `BoolErr`, `LabelSst`, and `Blank` on write (`MulBlank`, `RK`, `MulRk`, `Label`, and `Formula` with its `String` result record are read-only — see below).
|
|
17
|
+
- **Number-format classification and date serials** ([`excel-number-format`](../excel-number-format/README.md), `src/serial.ts`) — what turns a bare number into the schema's own `percentage`/`currency`/`date`/`time`/`dateTime` value kinds and back, honouring the workbook's own epoch flag (the writer always emits the 1900 system) and refusing the 1900 system's phantom leap day in both directions. The classification itself is a dependency, not local code: this package shares it with `ooxml.js`'s xlsx support, since it is the identical mini-language in both formats (ExaDev/documents.js#848). A cell's own `numberFormatCode` is preserved verbatim on write when present; absent, it resolves to a representative built-in code for its value kind (`General` for a plain number/string/boolean/error, `0%` for a percentage, a bare `$` format for a currency with no code, `mm-dd-yy`/`h:mm:ss`/`m/d/yy h:mm` for date/time/dateTime), and the workbook-wide `Format`/`XF` table is deduplicated across every sheet so two cells sharing one code share one entry.
|
|
18
|
+
- **Formula text recovery** (`src/biff/ptg.ts`, `src/biff/ptg-functions.ts`) — a Formula record's compiled `rgce` token stream ([MS-XLS] 2.5.198's `Ptg` vocabulary) read and rebuilt into the infix text a spreadsheet application would show: literal operands (`PtgInt`/`PtgNum`/`PtgStr`/`PtgBool`/`PtgErr`/`PtgMissArg`), cell and range references including their 3D (cross-sheet) forms (`PtgRef`/`PtgArea`/`PtgRef3d`/`PtgArea3d`, `$`-qualified per their own relative/absolute flags, a 3D reference's sheet name resolved through `EXTERNSHEET` and a self-referencing `SupBook` — `src/workbook/globals.ts`'s own `sheetRanges`), every arithmetic/comparison/unary/percent operator and explicit parentheses, and function calls through both `PtgFunc` (fixed arity, resolved from a curated table of [MS-XLS]'s own Ftab grammar) and `PtgFuncVar` (variable arity, its own on-disk `cparams`) — see "Formula expressions" under Read-side gaps below for the exact boundary of what this does not resolve.
|
|
19
|
+
- **Schema mapping** — `readXlsContent`/`readXls` (`src/content.ts`) as before, now also populating `ContentSheetCell.formula` wherever the Ptg reader above resolves it; `writeXlsContent`/`writeXls` (`src/write.ts`) the counterpart, taking a `ContentDocument`/`DocumentTree` of `kind: 'spreadsheet'` and producing genuine `.xls` bytes: a real BIFF8 `Workbook` stream (globals substream, one worksheet substream per sheet, `BoundSheet8.lbPlyPos` patched to each sheet's real byte offset once every substream's length is known) wrapped in a real [MS-CFB] compound file via `archive-codec`'s `writeCompoundFile`.
|
|
20
|
+
- **Document metadata** — `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` read from a `"\x05SummaryInformation"` stream when one is present, and written back to one whenever the input's metadata carries anything that stream can hold (see [Metadata](#metadata)).
|
|
21
|
+
- **Cell decoration** — a cell's background fill (solid pattern only) and per-side borders, read from and written to `XF`'s trailing `CellXF` payload plus the workbook's own `Palette` record, in both directions and verified against real LibreOffice-produced BIFF8, not just this package's own round trip (see [Cell decoration](#cell-decoration)).
|
|
22
|
+
- **Print settings** — every field of `ContentSheetPrintSettings`: page size and orientation, all four margins, gridline and row/column-header printing, page order, print scale or fit-to-page counts, manual page breaks, the print range, and the repeated header rows and columns — read from and written to the nine worksheet-substream records and the two built-in defined names that carry them, in both directions and verified against real LibreOffice-produced BIFF8 (see [Print settings](#print-settings)).
|
|
23
|
+
- **Cell alignment** — a cell's own horizontal (`left`/`center`/`right`/`justify`) and vertical (`top`/`middle`) alignment, read from and written to `XF`'s trailing `CellXF`/`StyleXF` payload's own leading word, in both directions and verified against real LibreOffice-produced BIFF8 (see [Cell alignment](#cell-alignment)).
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
Verified primarily by round trip (`src/write.test.ts`, plus a dedicated `test/workers/write.test.ts` proving the whole write path inside a real `workerd` isolate, not just Node): build a `ContentDocument`, write it, read it back through this package's own independently-pinned reader, and check the result. Every record's own byte layout is additionally cited to its [MS-XLS] section in the writer's source, matching the reader's own convention.
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
### Writer scope
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
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), and — see [Cell decoration](#cell-decoration) — a cell's own solid background fill and per-side borders, deduplicated across the workbook the same way number formats are. What it deliberately does not:
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
+
| Not written | Why |
|
|
32
|
+
| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `Formula` records | The read side now recovers a formula's expression for the cells it resolves (see Read-side gaps below), but writing one back out is a materially larger task — encoding text back into `Ptg` tokens, choosing built-in function indices, and (for anything the reader could not resolve in the first place) having no formula to write at all — scoped as its own separate piece of work rather than folded into recovering the read side. A `ContentSheetCell.formula` is silently ignored on write; only the cell's own typed `value` is written. |
|
|
34
|
+
| 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. |
|
|
35
|
+
| `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.) |
|
|
36
|
+
| 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. |
|
|
37
|
+
| Images, embedded objects, comments (`Note`/`Txo`), data validation, conditional formatting, defined names (`Lbl`) | Not read either (see below); there is no round trip to verify a writer for them against. |
|
|
38
|
+
| 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). |
|
|
39
|
+
| `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. |
|
|
40
|
+
| `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. |
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
### Read-side gaps
|
|
45
|
+
|
|
46
|
+
Each deliberate rather than overlooked:
|
|
47
|
+
|
|
48
|
+
- **Formula expressions, mostly recovered.** A `Formula` record's compiled `Ptg` token stream (`src/biff/ptg.ts`) is walked and rebuilt into real formula text — literal operands, cell/range references (`$`-qualified, including 3D cross-sheet references resolved through `EXTERNSHEET` and a self-referencing `SupBook`), every arithmetic/comparison/unary/percent operator, explicit parentheses, and both fixed- and variable-arity function calls, resolved by name against [MS-XLS]'s own built-in function table (`src/biff/ptg-functions.ts`, covering the whole published table — [MS-XLS] 2.5.198.17 — cited to that table's own `iftab` index; PtgFunc's fixed argument count is a curated subset of it, since PtgFunc's own token carries no count and only a function [MS-XLS]'s grammar states a fixed, non-optional arity for is resolved through it, empirically confirmed against real LibreOffice-written BIFF8 rather than assumed from the grammar alone). Three constructs remain genuinely unresolved, each leaving `ContentSheetCell.formula` absent for that cell specifically rather than fabricating text: a **shared formula** (`PtgExp`, whose real expression lives in a separate `ShrFmla` record this reader does not yet join), an **array formula** (`PtgArray`, whose literal values live in a separate `PtgExtraArray` trailer this reader does not yet parse), and a **genuinely external workbook** reference (a `SupBook` naming another file, a DDE/OLE data source, or an add-in, rather than this same workbook) — each is real, meaningfully separate work, not an oversight. A defined name (`PtgName`/`PtgNameX`) and a natural-language "Elf" reference are likewise not resolved, for the same reason `Lbl` (defined names) is not read at all yet (see below).
|
|
49
|
+
- **Cell decoration and alignment resolved for real; font still not.** `XF`'s trailing `CellXF` payload's fill pattern/colour and per-side border style/colour are read and resolved through the `Palette` record (or the default colour table when one is absent) — see [Cell decoration](#cell-decoration) for the full mapping, the fill-pattern-beyond-solid judgment call, and how this was verified against real LibreOffice-produced BIFF8. The same payload's leading word is resolved too — see [Cell alignment](#cell-alignment) for the full `alc`/`alcV` mapping. `Font` records are not read at all: `ContentSheetCell` has no cell-level font field, and `ooxml.js`'s xlsx reader makes the identical scope choice.
|
|
50
|
+
- **Print settings, resolved for real.** Every field of `ContentSheetPrintSettings` is read from the records that carry it, with Excel's own "Normal" preset standing in per field for whatever the file leaves unstated — see [Print settings](#print-settings) for the record map, the two of BIFF8's own conditional rules that decide how to read `Setup`, and the three things that genuinely do not come through.
|
|
51
|
+
- **Not read at all:** charts, drawings and images, cell comments (`Note`/`Txo`), data validation, and conditional formatting. 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.
|
|
31
52
|
- **Encrypted workbooks** are refused rather than mis-read: a `FilePass` record means every record after it is ciphertext.
|
|
32
53
|
|
|
33
|
-
This package is
|
|
54
|
+
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. Remaining read+write scope gaps (formula writing, shared/array/external-reference formulas, and per-cell font) are tracked on [#815](https://github.com/ExaDev/documents.js/issues/815).
|
|
55
|
+
|
|
56
|
+
## Cell decoration
|
|
57
|
+
|
|
58
|
+
A cell's own solid background fill and per-side borders are read from and written to `XF`'s trailing `CellXF`/`StyleXF` payload ([MS-XLS] 2.4.353) and the workbook's own `Palette` record ([MS-XLS] 2.4.188), verified both by round trip and against a real, independent BIFF8 implementation — [ExaDev/documents.js#815](https://github.com/ExaDev/documents.js/issues/815) as a scoped chunk of that issue's own broader tracking, not a claim of closing it outright. `src/biff/xf-colors.ts` is the one place the payload's border/fill bit layout is packed or unpacked, shared by `workbook/globals.ts`'s read side and `biff/xf-writer.ts`'s write side, so the two directions cannot silently disagree on what a given byte means.
|
|
59
|
+
|
|
60
|
+
**Colour resolution.** A fill or border colour is a 7-bit `icv` index into BIFF8's own colour table ([MS-XLS] "Icv"): 0-7 name eight fixed built-in colours (this package's writer never emits one of these, per the spec's own "SHOULD NOT be ≤ 0x07"; the reader still resolves them, for a real third-party file that does), 8-63 index into either the workbook's own `Palette` record when one is present or a fixed 56-entry default table when it is not. The writer scans every distinct decoration colour a workbook's cells use before writing anything: when every one already matches the default table exactly, no `Palette` record is written at all, keeping an undecorated-adjacent file as minimal as it always was; the moment even one colour falls outside that table, a real 56-entry `Palette` record is minted, with every distinct colour the workbook actually uses (not only the non-default ones) assigned its own dedicated slot, so the whole table is self-consistent rather than a mix of "the file's own entries" and "the implicit default".
|
|
61
|
+
|
|
62
|
+
**Fill patterns beyond solid — a deliberate judgment call, not an oversight.** [MS-XLS]'s own `FillPattern` enumeration names nineteen patterns: no fill, solid, four grey shades, and stripe/crosshatch families. `ContentSheetCell.background` models exactly one flat colour, so only `FLSSOLID` maps onto it — using the pattern's own foreground colour (`icvFore`), which [MS-XLS] itself documents as the only colour a solid fill actually renders ("If this value is 1, then only `icvFore` is rendered"). Every other pattern reads as no background at all. Approximating a 50% grey crosshatch or a diagonal stripe as its own foreground colour would misreport what the cell actually shows, so this reader leaves it unmapped rather than guessing; this package's own writer never emits anything but `FLSNULL`/`FLSSOLID` either, so a round trip never needs to invent a lossy pattern encoding on the way back out.
|
|
63
|
+
|
|
64
|
+
**Borders.** Each of a cell's four sides carries its own [MS-XLS] `BorderStyle` line-style token and colour, mapped onto `ContentBorder`'s `widthPt`/`style` pair the same way `ooxml.js`'s own `typed/xlsx/styles.ts` maps xlsx's border tokens: four named weights — hair/thin/medium/thick, at 0.5/0.75/1.5/2.25pt, derived from Excel's own documented 96-DPI rendering and held once in [`document-schema.js`](../document-schema.js/README.md)'s own `border-weight` module, which this package and `ooxml.js` both import rather than each keeping a copy of it — crossed with a pattern (solid/dashed/dotted/double); the dash-family tokens (`dashDot`, `dashDotDot`, and their medium/slant variants) collapse to `'dashed'`, the closest `ContentStrokeStyle` member, exactly as `ooxml.js`'s equivalent table does for xlsx's own dash tokens. Diagonal borders (`dgDiag`/`grbitDiag`) are out of scope — `ContentCellBordersSchema` has no diagonal member — and are always read as absent, always written as none.
|
|
65
|
+
|
|
66
|
+
**Verified against a real, independent BIFF8 implementation, not just this package's own reader/writer pair.** A `.xls` built directly by LibreOffice (`soffice --headless --convert-to xls`, from a hand-authored `.fods` declaring real `fo:background-color`/`fo:border*` cell styles) is read correctly by this package's own reader — every fill and border colour matched exactly against what LibreOffice's own subsequent re-export of the same bytes independently confirms them to be, and every border's named weight and pattern resolved to the same [MS-XLS] `BorderStyle` token LibreOffice wrote. The named weight is where the two implementations stop agreeing on a number, and necessarily so: BIFF8 stores a name (`thin`, `medium`, `thick`), not a width, so LibreOffice's re-export states its own rendered widths (0.74/1.76/2.49pt) for the same tokens this package renders at 0.75/1.5/2.25pt. Both are honest readings of the same bytes. And a `.xls` this package writes — including one whose colours force a real `Palette` record, proving that path specifically — opens in LibreOffice with the correct fill and borders, confirmed by converting it back through `soffice --headless --convert-to fods` and inspecting the cell styles LibreOffice itself recovers from it.
|
|
67
|
+
|
|
68
|
+
The decorated-blank case was checked the same way, in both directions and against the same implementation: a `.fods` whose one styled cell has a fill and four borders but no value converts to a `.xls` in which LibreOffice writes a real `Blank` record, and this reader recovers that cell's fill colour and all four border colours exactly; a `.xls` this package writes for the equivalent `empty` cell converts back to a `.fods` in which LibreOffice recovers a valueless cell carrying the same `fo:background-color` and `fo:border` colours — at its own 0.74pt rendering of the `thin` weight, per the note above.
|
|
69
|
+
|
|
70
|
+
**Decoration on a cell with no value.** A cell can be empty and still have something to show, and BIFF8 says so with a `Blank` record ([MS-XLS] 2.4.20) — a cell header naming an `XF` and nothing else, written precisely because that `XF` carries a fill or a border. Both directions honour it: a `Blank` or `MulBlank` whose `XF` resolves to real decoration is read as an `empty`-kind cell carrying that decoration rather than dropped, and an `empty` cell carrying a background or a border is written back as a `Blank` record pointing at an `XF` encoding it. An empty cell with no decoration is still written as nothing at all and still read as absent, which is what keeps `ContentSheet`'s cell array sparse; "decoration this reader can express" is the same test the value-cell path applies, so a fill pattern beyond solid or an unresolvable colour leaves a blank cell dropped exactly as before.
|
|
71
|
+
|
|
72
|
+
A per-cell font remains out of scope in both directions — see [Read-side gaps](#read-side-gaps) and [Writer scope](#writer-scope) above. The payload's leading word, once similarly out of scope, is now [Cell alignment](#cell-alignment) below.
|
|
73
|
+
|
|
74
|
+
## Cell alignment
|
|
75
|
+
|
|
76
|
+
A cell's own horizontal and vertical alignment are read from and written to the same `XF` trailing `CellXF`/`StyleXF` payload [Cell decoration](#cell-decoration) covers ([MS-XLS] 2.4.353), but a different pair of fields within it: `alc` and `alcV`, the leading word's own bits 0-2 and 4-6 ([MS-XLS] [`HorizAlign`](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/75e17a8f-9cd4-4b37-927e-4b0a54ef9266) and [`VertAlign`](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/1e9eb7d3-0cd8-42a3-af1e-f523105a5e93)) — verified both by round trip and against a real, independent BIFF8 implementation, the identical bar [Cell decoration](#cell-decoration) and [Print settings](#print-settings) are held to. `src/biff/xf-colors.ts` is the one place `alc`/`alcV` are packed or unpacked in either direction (`resolveHorizontalAlignment`/`horizAlignTokenFor` and `resolveVerticalAlignment`/`vertAlignTokenFor`), so the two directions cannot silently disagree on what a given bit pattern means.
|
|
77
|
+
|
|
78
|
+
**Only the members `ContentSheetCell.alignment`/`verticalAlignment` can express survive, matching `ooxml.js`'s own xlsx alignment policy exactly.** `HorizAlign` names eight members; only `ALCLEFT`/`ALCCTR`/`ALCRIGHT`/`ALCJUST` have an `Alignment` counterpart (`left`/`center`/`right`/`justify`). `ALCGEN` — general alignment — is the identical semantics to `alignment` being absent (the schema's own "numeric right, text left" value-kind default already means the same thing), so it round-trips to and from `undefined` rather than a literal member that would override the default it means to request. `ALCFILL`/`ALCCONTCTR`/`ALCDIST` are real members with no schema counterpart at all and are left unread, the same policy `odf.js`'s own alignment reader applies to a construct its schema has no member for. `VertAlign` names five members; only `ALCVTOP`/`ALCVCTR` have a `verticalAlignment` counterpart (`top`/`middle`). `ALCVBOT` is the schema's own documented default for an absent `verticalAlignment` ("no value-kind default to fall back to, so its own absence means `'bottom'` outright"), so it round-trips to and from `undefined` the same way `ALCGEN` does; `ALCVJUST`/`ALCVDIST` are left unread for the identical reason `ALCFILL`/`ALCCONTCTR`/`ALCDIST` are.
|
|
79
|
+
|
|
80
|
+
**Interned into the cell-XF table alongside decoration, not a separate table.** `write.ts`'s own `buildCellXfPlan` already deduplicated cells sharing an identical (number format, decoration) pair into one `XF` record; the interning key is now a (number format, alignment, vertical alignment, decoration) tuple, so two cells sharing all four still share one record and a cell differing in only its alignment still mints its own. A cell with General formatting, no alignment, and no decoration still resolves to the workbook's own implicit `GENERAL_CELL_XF_INDEX` with no new `XF` record at all, exactly as before alignment was modelled.
|
|
81
|
+
|
|
82
|
+
**Formatting on a cell with no value, widened.** [Cell decoration](#cell-decoration)'s own `Blank`-record rule (a decorated empty cell is worth a record; an undecorated one is written as nothing at all) now triggers on alignment too: `written-cells.ts`'s `cellCarriesFormatting` (renamed from `cellCarriesDecoration` when this was added) treats a non-default `alignment` or `verticalAlignment` the same way it already treats a background or a border. An empty cell stating only `alignment: 'center'` therefore gets a real `Blank` record and round-trips with that alignment intact, rather than being silently dropped as if it had nothing to say.
|
|
83
|
+
|
|
84
|
+
**Verified against a real, independent BIFF8 implementation, not just this package's own reader/writer pair.** A `.xls` this package writes — one cell per `Alignment` member, one per `verticalAlignment` member, and one combining a non-default horizontal and vertical alignment on the same cell — opens in LibreOffice 26.2.5.2 with every alignment intact, confirmed by converting it back through `soffice --headless --convert-to fods` and inspecting the cell styles LibreOffice itself recovers: each `left`/`center`/`right`/`justify` cell reads back as LibreOffice's own `fo:text-align="start"/"center"/"end"/"justify"`, each `top`/`middle` cell as `style:vertical-align="top"/"middle"`, and the combined cell as both at once on the identical style. Going the other way, a `.xls` built directly by LibreOffice (`soffice --headless --convert-to xls`, from a hand-authored `.fods` declaring the same real `fo:text-align`/`style:vertical-align` cell styles) is read correctly by this package's own reader, every cell recovering the exact alignment LibreOffice's own style declared.
|
|
85
|
+
|
|
86
|
+
## Print settings
|
|
87
|
+
|
|
88
|
+
Every field of `document-schema.js`'s own `ContentSheetPrintSettings` is read from and written to the BIFF8 records that carry it, verified both by round trip and against a real, independent BIFF8 implementation — [ExaDev/documents.js#815](https://github.com/ExaDev/documents.js/issues/815) as a scoped chunk of that issue's own broader tracking, not a claim of closing it outright. The two directions had to land together: before this, the reader returned Excel's fixed "Normal" preset for every sheet regardless of what the file said, so there was nothing to verify a writer's output against.
|
|
89
|
+
|
|
90
|
+
**One sheet's print settings live in two substreams, not one.** The page setup is in the sheet's own substream, as the optional records of [MS-XLS] 2.1.7.20.6's own `GLOBALS` and `PAGESETUP` productions. The print range and the repeated header bands are not there at all: BIFF8 keeps them in the workbook globals substream, as ordinary defined names ([MS-XLS] 2.4.150's `Lbl` record) carrying a built-in name index rather than a user-typed name, scoped to one sheet through the record's own `itab`. `src/workbook/print-names.ts` is both directions of exactly those two names; `src/biff/print-setup.ts` is the one place the `Setup` record's own bit layout and paper-size code table are packed or unpacked, shared by the read and write sides so the two cannot silently disagree.
|
|
91
|
+
|
|
92
|
+
| Schema field | Where BIFF8 keeps it |
|
|
93
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| `pageSize` | `Setup`'s `iPaperSize` code ([MS-XLS] 2.4.257), transposed to landscape by its own `fPortrait`/`fNoOrient` flags |
|
|
95
|
+
| `margins` | `LeftMargin`/`RightMargin`/`TopMargin`/`BottomMargin` ([MS-XLS] 2.4.151, 2.4.219, 2.4.328, 2.4.27), each a single `Xnum` of inches |
|
|
96
|
+
| `gridlines` | `PrintGrid`'s `fPrintGrid` ([MS-XLS] 2.4.202) |
|
|
97
|
+
| `headers` | `PrintRowCol`'s `printRwCol` ([MS-XLS] 2.4.203) — the row/column header chrome, not a repeated print band |
|
|
98
|
+
| `pageOrder` | `Setup`'s `fLeftToRight` |
|
|
99
|
+
| `scalePercent` / `fitToPages` | `Setup`'s `iScale` or its `iFitWidth`/`iFitHeight` pair, selected by `WsBool`'s own `fFitToPage` bit ([MS-XLS] 2.4.351) |
|
|
100
|
+
| `manualBreaks` | `HorizontalPageBreaks`/`VerticalPageBreaks` ([MS-XLS] 2.4.142, 2.4.343) |
|
|
101
|
+
| `printRange` | The built-in `Print_Area` defined name (`Lbl` with built-in index `0x06`), whose `rgce` is a `PtgArea3d` naming the range |
|
|
102
|
+
| `repeatRows` / `repeatColumns` | The built-in `Print_Titles` name (index `0x07`), whose `rgce` is a `PtgMemFunc` wrapping one or two `PtgArea3d` tokens joined by `PtgUnion` |
|
|
103
|
+
|
|
104
|
+
**Every record behind these is optional, and every field falls back independently.** A sheet whose page setup was never touched carries no `Setup` and no margin records at all, so something has to stand in — Excel's own "Normal" preset (top/bottom 0.75in, left/right 0.7in, Letter paper, gridlines and headers not printed, pages down-then-over), the same constants `ooxml.js` falls back to for an xlsx carrying no `pageMargins` element, so the same untouched sheet reads identically from either format. The fallback is per field rather than wholesale: a sheet declaring a left margin and nothing else keeps its real left margin and takes the preset for the other three.
|
|
105
|
+
|
|
106
|
+
**Two of BIFF8's own conditional rules are honoured rather than flattened.** A `Setup` record whose `fNoPls` bit is set declares its own paper size and scale undefined ([MS-XLS] 2.4.257: "whether the `iPaperSize`, `iScale`, `iRes`, `iVRes`, `iCopies`, `fNoOrient`, and `fPortrait` data are undefined and ignored"), so neither is read from it — the page size falls back to the preset and no `scalePercent` is reported, rather than a paper code the file itself disowns being resolved into a confident page size. And `WsBool`'s `fFitToPage` decides which of `Setup`'s two mutually exclusive scaling fields is live: real producers write both regardless of which is active (confirmed against LibreOffice-written BIFF8, which carries `iScale=100` alongside a real fit-to-page pair, and a real `iScale` alongside `iFitWidth=iFitHeight=1`), so reading both would report a scale and a page count that contradict each other.
|
|
107
|
+
|
|
108
|
+
**A repeated band's axis is its shape, not a field.** BIFF8 has no flag saying which axis a `Print_Titles` band repeats along: a repeated row band is written as an area spanning every column of the sheet (`$1:$2`, columns 0-255) and a repeated column band as one spanning every row (`$A:$A`, rows 0-65535). Both directions use that shape as the discriminant, and an area spanning both axes at once names the whole sheet — which is neither — so it is left unclassified rather than assigned to whichever branch happened to be tested first.
|
|
109
|
+
|
|
110
|
+
**Three things do not come through, each for a reason in the format rather than an oversight:**
|
|
111
|
+
|
|
112
|
+
- **A page size no `iPaperSize` code names loses its dimensions.** Unlike xlsx's own `pageSetup` element, which can state an explicit `paperWidth`/`paperHeight` pair, `Setup` addresses paper only by code; its escape hatch for a size outside the table is a printer-defined custom size carried in a separate `Pls` record ([MS-XLS] 2.4.199), a printer driver's opaque `DEVMODE` blob rather than a pair of dimensions any reader could recover a size from. So the writer emits `iPaperSize` 0 — that section's own "custom printer paper sizes" — which is true, where substituting Letter would not be; the reading application then falls back to its own default paper (this package to the preset, LibreOffice to its locale's), and every other print setting on the sheet still comes through. Refusing the file outright was the first thing tried and is the wrong trade: a spreadsheet converted from a slide deck or a drawing carries that source's own canvas as its page size, which is almost never a named paper, and failing the conversion would lose the cells too. The code table this package does map is the office paper sizes of [MS-XLS]'s own 118-entry enumeration — the Letter/Legal/Tabloid/Executive/Statement family, A2 through A6, B4/B5 in both the JIS and ISO spellings, Folio and Quarto — each derived from the inches or millimetres that table itself states rather than from pre-converted points, and each matched within half a point so a page size picking up conversion drift on its way between codecs still resolves.
|
|
113
|
+
- **A "fit to as many pages as necessary" axis has no schema spelling.** `Setup` documents `iFitWidth`/`iFitHeight` of 0 as "use as many pages as necessary to print the columns/rows in the sheet", and `ContentSheetPrintSettings.fitToPages` requires both counts to be positive. A fit-to-page sheet with an auto axis therefore reports no `fitToPages` at all rather than a fabricated 1, which would claim the sheet is pinned to a single page along an axis the file left free.
|
|
114
|
+
- **An explicit 100% scale reads back as no declared scale.** `Setup`'s `iScale` is a mandatory field of a mandatory record with no spelling for "this sheet declares no scale", so an untouched sheet still states 100 — and `ContentSheetPrintSettings` already means exactly that by carrying no `scalePercent` at all. The two spellings print identically, so the reader collapses them onto the absent one rather than putting a field carrying no actionable information on every sheet of every workbook it reads. A scale that is genuinely anything else is reported exactly as the file states it.
|
|
115
|
+
|
|
116
|
+
Two further deliberate narrowings, both because the schema models less than BIFF8 states: a `Print_Area` naming several disjoint areas (legal in BIFF8, and what Excel writes for a multi-area print selection) yields only the first, since `printRange` models one rectangle and merging several into their bounding box would claim cells print that do not; and a BIFF8 page break carries an extent along the perpendicular axis, which `manualBreaks` — an index with no extent — cannot express, so a partial break is carried as a full one and two breaks on the same row collapse into one.
|
|
117
|
+
|
|
118
|
+
**Verified against a real, independent BIFF8 implementation, not just this package's own reader/writer pair.** Three `.xls` files built directly by LibreOffice (`soffice --headless --convert-to xls`, from hand-authored `.fods` files declaring a real page layout, print range, header rows and columns, and manual breaks) read back through this package with every field matching what was authored: A4 landscape, four distinct margins, gridlines and headers on or off, `overThenDown` page order, an 80% scale and a 2x3 fit-to-page pair, a row break at row 10, a column break at column 3, a `B2:D6` print range, two repeated header rows, and one repeated header column. Going the other way, a `.xls` this package writes for the same content opens in LibreOffice with every one of those fields intact, confirmed by converting it back through `soffice --headless --convert-to fods` and comparing LibreOffice's own re-export of our file against its re-export of its own, attribute by attribute. A fourth file, written with a page size no paper code names, opens in LibreOffice with its own default paper substituted and every other print setting and cell intact — the custom-paper behaviour above, checked rather than assumed. The `Print_Area` record this writer emits is byte-for-byte the one LibreOffice writes for the same range (`src/workbook/print-names.test.ts` asserts exactly that against the real bytes); the `Print_Titles` record differs by one byte, a trailing `PtgParen` display token this writer has no reason to emit.
|
|
119
|
+
|
|
120
|
+
**Why this writer now emits the calculation-state records.** [MS-XLS] 2.1.7.20.6's `GLOBALS` production makes `CalcCount`, `CalcRefMode`, `CalcIter`, `CalcDelta` and `CalcSaveRecalc` mandatory ahead of `PrintRowCol`, and this writer previously omitted them along with the rest of BIFF8's UI and interoperability bookkeeping. That turned out to matter: LibreOffice's own importer silently discards whichever page-settings record comes first in a worksheet substream, so with `PrintRowCol` in that slot, a `.xls` this package wrote with row and column headers enabled opened in LibreOffice with them off — while every other print setting in the same file came through correctly. Moving any other record into that slot fixes it, and the records the grammar already required there are the honest way to do it. Confirmed by writing the same workbook with and without them and re-reading each through `soffice --convert-to fods`.
|
|
121
|
+
|
|
122
|
+
## Metadata
|
|
123
|
+
|
|
124
|
+
A `.xls`'s title, author, and dates do not live in any BIFF8 record at all — they live in a `"\x05SummaryInformation"` stream, a genuinely different format ([MS-OLEPS] Property Set Streams, [MS-OSHARED] 2.3.3.2.2's own naming of the specific properties Office uses) that happens to sit beside `Workbook` in the same [MS-CFB] compound file. `readXlsContent` reads that stream when present (`archive-codec`'s `readSummaryInformation`, since the property-set format itself is zero document-format knowledge, exactly as the [MS-CFB] container it sits inside is) and maps it onto `document-schema.js`'s `LayoutMetadata` (`archive-codec`'s own `summaryInformationToLayoutMetadata` — the mapping is format-agnostic, so it lives there rather than being copied in this package, alongside `doc-codec`'s and `ppt-codec`'s identical need for it); `writeXlsContent` does the inverse (`src/metadata.ts`'s `layoutMetadataToSummaryInformation`, which validates `createdIso`/`modifiedIso` as real dates and throws a `BiffWriteError` naming the offending field before delegating to `archive-codec`'s own mapping), including a `"\x05SummaryInformation"` stream in its `writeCompoundFile` call only when the input's metadata actually carries something that stream can hold — an input whose metadata is `{}`, or carries only fields the mapping below has no destination for, produces no stream at all, matching what an absent-metadata read already returns.
|
|
125
|
+
|
|
126
|
+
The mapping is not 1:1, and each gap is permanent rather than a remaining TODO:
|
|
127
|
+
|
|
128
|
+
| Direction | Fields covered | Gap |
|
|
129
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| SummaryInformation → LayoutMetadata | `title`, `subject`, `author`, `keywords`, `createdIso`, `lastSavedIso` → `modifiedIso` | `comments` and `lastPrintedIso` have no LayoutMetadata field to land in — no other codec in the family has a "last printed" or free-text "comments" concept, so these are read from the stream but never reach a `ContentDocument`. |
|
|
131
|
+
| LayoutMetadata → SummaryInformation | the same six fields, in reverse | `creator`, `producer`, and `language` have no SummaryInformation equivalent: `producer` is a PDF-only concept in this schema, and `creator`/`language` are not among the fields the stream this package writes covers. |
|
|
132
|
+
|
|
133
|
+
Only the fixed SummaryInformation property set is read or written — the sibling `"\x05DocumentSummaryInformation"` stream (company, manager, and custom user-defined properties, [MS-OLEPS]'s two-property-set spelling) is not attempted at all, an explicit scope boundary `archive-codec`'s own `oleps` support shares.
|
|
34
134
|
|
|
35
135
|
## Getting started
|
|
36
136
|
|
|
@@ -65,10 +165,10 @@ if (isXlsFile(bytes)) {
|
|
|
65
165
|
The record layer is exported in its own right, for a caller inspecting a workbook rather than converting it:
|
|
66
166
|
|
|
67
167
|
```ts
|
|
68
|
-
import { readRecords,
|
|
168
|
+
import { readRecords, readWorkbookStreams } from "xls-codec";
|
|
69
169
|
|
|
70
|
-
const
|
|
71
|
-
for (const rec of readRecords(
|
|
170
|
+
const { workbook } = readWorkbookStreams(bytes); // the raw BIFF8 record stream out of the compound file, plus the optional "\x05SummaryInformation" stream beside it
|
|
171
|
+
for (const rec of readRecords(workbook)) {
|
|
72
172
|
console.log(rec.type.toString(16), rec.data.length);
|
|
73
173
|
}
|
|
74
174
|
```
|
|
@@ -85,9 +185,13 @@ Layered bottom-up, each layer testable against hand-built byte sequences taken f
|
|
|
85
185
|
- **`src/biff/records.ts`** — the record framing, and nothing above it. Deliberately does not merge `Continue` records: whether a continuation's bytes simply append or re-state a flag byte first is decided by the record being continued, so the blocks are reported as written.
|
|
86
186
|
- **`src/biff/cursor.ts`** — a field cursor over one record's blocks that reads across a continuation boundary transparently while keeping the boundary observable, which is exactly what the string reader needs.
|
|
87
187
|
- **`src/biff/strings.ts`**, **`src/biff/rk.ts`**, **`src/biff/errors.ts`** — the shared value encodings: the three string shapes, the `RkNumber` packed-numeric encoding, and the `BErr` error-value vocabulary.
|
|
88
|
-
- **`src/
|
|
89
|
-
- **`src/
|
|
188
|
+
- **`src/biff/ptg.ts`**, **`src/biff/ptg-functions.ts`** — the `Ptg` compiled-formula token stream, walked as a postfix expression and rebuilt into infix formula text (an operand stack tagged with each entry's own operator precedence, so a child is parenthesised only when its precedence genuinely requires it), and the built-in worksheet-function name/fixed-arity table `PtgFunc`/`PtgFuncVar` resolve against.
|
|
189
|
+
- **`src/workbook/globals.ts`**, **`src/workbook/sheet.ts`** — the two substream readers, each walking the record sequence its ABNF in [MS-XLS] 2.1.7.20.3 / 2.1.7.20.5 defines; `globals.ts` also resolves a 3D reference's own `ixti` to a sheet range through `EXTERNSHEET` and a self-referencing `SupBook`, which `sheet.ts` threads into `ptg.ts` for a Formula record's own 3D references, and reads a `Palette` record and each `XF`'s trailing fill/border payload for [Cell decoration](#cell-decoration). Both readers contribute to [Print settings](#print-settings), which BIFF8 splits between them: `sheet.ts` reads the page-setup record group, `globals.ts` the two built-in defined names carrying the print range and the repeated header bands.
|
|
190
|
+
- **`src/biff/xf-colors.ts`** — the `Icv` colour table (both the eight fixed colours and the 56-entry default palette), the `BorderStyle`/`FillPattern` vocabularies, and the `CellXF`/`StyleXF` trailing payload's own border/fill bit-layout packing and unpacking, shared by `globals.ts`'s read side and `biff/xf-writer.ts`'s write side — see [Cell decoration](#cell-decoration).
|
|
191
|
+
- **`src/biff/print-setup.ts`**, **`src/workbook/print-names.ts`** — the two halves of [Print settings](#print-settings). The first is the `Setup` record's own flag bit layout and `iPaperSize` code table, packed and unpacked in one place exactly as `xf-colors.ts` does for the `XF` payload; the second is both directions of the built-in `Print_Area`/`Print_Titles` defined names, which live in the globals substream rather than the sheet's own and so are read by `globals.ts` and written by `globals-writer.ts`.
|
|
192
|
+
- **[`excel-number-format`](../excel-number-format/README.md)**, **`src/serial.ts`** — number-format classification and date-serial conversion, the two pieces of xlsx semantics BIFF8 shares because ECMA-376 inherited them from BIFF. The classifier itself is a dependency shared with `ooxml.js`, not a module in this package (ExaDev/documents.js#848) — `classifyNumberFormat` and `BUILTIN_NUMBER_FORMATS` still ride this package's own barrel (`export * from "excel-number-format"` in `src/index.ts`), so `import { classifyNumberFormat } from "xls-codec"` is unchanged.
|
|
90
193
|
- **`src/content.ts`** — the mapping onto `document-schema.js`.
|
|
194
|
+
- **`src/metadata.ts`** — wraps `archive-codec`'s own `SummaryInformationProperties` <-> `LayoutMetadata` mapping with this package's `createdIso`/`modifiedIso` date validation, throwing `BiffWriteError` for a malformed one rather than letting an opaque `RangeError` escape the FILETIME conversion (see [Metadata](#metadata)).
|
|
91
195
|
|
|
92
196
|
### Deliberately not depended on
|
|
93
197
|
|
|
@@ -121,6 +225,7 @@ Conventional Commits, enforced workspace-wide by commitlint through a root `comm
|
|
|
121
225
|
- [MS-CFB]: [Compound File Binary File Format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b) — the container, read through `archive-codec`.
|
|
122
226
|
- [archive-codec](../archive-codec/README.md) — the bounded CFB reader this package selects the `Workbook` stream through.
|
|
123
227
|
- [document-schema.js](../document-schema.js/README.md) — the `ContentDocument`/`ContentSheet`/`ContentSheetCell` vocabulary this package maps onto, and the `assembleTree` transform behind `readXls`.
|
|
228
|
+
- [excel-number-format](../excel-number-format/README.md) — the number-format classifier this package depends on, shared with `ooxml.js`'s xlsx support.
|
|
124
229
|
- [ooxml.js](../ooxml.js/README.md) — the sibling reading `.xlsx`, BIFF8's successor, onto the same schema.
|
|
125
230
|
|
|
126
231
|
## License
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_biff_record_types = require("./record-types.cjs");
|
|
3
|
+
const require_biff_builder = require("./builder.cjs");
|
|
4
|
+
//#region src/biff/bof-writer.ts
|
|
5
|
+
/** rupBuild ([MS-XLS] 2.4.21): an arbitrary build identifier: no permitted-values table constrains it, so any 16-bit value is spec-legal. */
|
|
6
|
+
const RUP_BUILD = 3515;
|
|
7
|
+
/** rupYear: [MS-XLS] 2.4.21 requires 0x07CC or 0x07CD. */
|
|
8
|
+
const RUP_YEAR = 1996;
|
|
9
|
+
/** The doc-flags dword: bit 0 (fWin) and bit 3 (fWinAny) set, both of which [MS-XLS] 2.4.21 fixes at MUST-be-1; every other bit -- fRisc, fBeta, fMacAny, fBetaAny, the unused/reserved runs, fOOM, fGlJmp, fFontLimit, verXLHigh, unused3 -- left at its own spec-permitted default of 0. */
|
|
10
|
+
const DOC_FLAGS = 9;
|
|
11
|
+
/** The version-tracking dword: verLowestBiff = 6 (offset 0, [MS-XLS] 2.4.21: "the value MUST be 6"), verLastXLSaved = 0 (offset 8, a value the field's own table permits), reserved2 = 0. */
|
|
12
|
+
const VERSION_TRACKING = 6;
|
|
13
|
+
/** Builds a BOF record's 16-byte data for the given substream document type (BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, ...). */
|
|
14
|
+
function writeBofData(documentType) {
|
|
15
|
+
return new require_biff_builder.RecordBuilder().u16(require_biff_record_types.BIFF8_VERSION).u16(documentType).u16(RUP_BUILD).u16(RUP_YEAR).u32(DOC_FLAGS).u32(VERSION_TRACKING).build();
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
exports.writeBofData = writeBofData;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
//#region src/biff/bof-writer.d.ts
|
|
2
|
+
/** Builds a BOF record's 16-byte data for the given substream document type (BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, ...). */
|
|
3
|
+
declare function writeBofData(documentType: number): Uint8Array<ArrayBuffer>;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { writeBofData };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
//#region src/biff/bof-writer.d.ts
|
|
2
|
+
/** Builds a BOF record's 16-byte data for the given substream document type (BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, ...). */
|
|
3
|
+
declare function writeBofData(documentType: number): Uint8Array<ArrayBuffer>;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { writeBofData };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BIFF8_VERSION } from "./record-types.js";
|
|
2
|
+
import { RecordBuilder } from "./builder.js";
|
|
3
|
+
//#region src/biff/bof-writer.ts
|
|
4
|
+
/** rupBuild ([MS-XLS] 2.4.21): an arbitrary build identifier: no permitted-values table constrains it, so any 16-bit value is spec-legal. */
|
|
5
|
+
const RUP_BUILD = 3515;
|
|
6
|
+
/** rupYear: [MS-XLS] 2.4.21 requires 0x07CC or 0x07CD. */
|
|
7
|
+
const RUP_YEAR = 1996;
|
|
8
|
+
/** The doc-flags dword: bit 0 (fWin) and bit 3 (fWinAny) set, both of which [MS-XLS] 2.4.21 fixes at MUST-be-1; every other bit -- fRisc, fBeta, fMacAny, fBetaAny, the unused/reserved runs, fOOM, fGlJmp, fFontLimit, verXLHigh, unused3 -- left at its own spec-permitted default of 0. */
|
|
9
|
+
const DOC_FLAGS = 9;
|
|
10
|
+
/** The version-tracking dword: verLowestBiff = 6 (offset 0, [MS-XLS] 2.4.21: "the value MUST be 6"), verLastXLSaved = 0 (offset 8, a value the field's own table permits), reserved2 = 0. */
|
|
11
|
+
const VERSION_TRACKING = 6;
|
|
12
|
+
/** Builds a BOF record's 16-byte data for the given substream document type (BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, ...). */
|
|
13
|
+
function writeBofData(documentType) {
|
|
14
|
+
return new RecordBuilder().u16(BIFF8_VERSION).u16(documentType).u16(RUP_BUILD).u16(RUP_YEAR).u32(DOC_FLAGS).u32(VERSION_TRACKING).build();
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { writeBofData };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/biff/builder.ts
|
|
3
|
+
var RecordBuilder = class {
|
|
4
|
+
parts = [];
|
|
5
|
+
pushBytes(...bytes) {
|
|
6
|
+
this.parts.push(new Uint8Array(bytes));
|
|
7
|
+
return this;
|
|
8
|
+
}
|
|
9
|
+
u8(value) {
|
|
10
|
+
return this.pushBytes(value & 255);
|
|
11
|
+
}
|
|
12
|
+
u16(value) {
|
|
13
|
+
const bits = value & 65535;
|
|
14
|
+
return this.pushBytes(bits & 255, bits >>> 8 & 255);
|
|
15
|
+
}
|
|
16
|
+
/** Composed with multiplication rather than `<< 24`, matching biff/cursor.ts's own u32 reader: a shift would produce a signed result for any value with the top bit set. */
|
|
17
|
+
u32(value) {
|
|
18
|
+
const bits = value >>> 0;
|
|
19
|
+
return this.pushBytes(bits & 255, bits >>> 8 & 255, bits >>> 16 & 255, bits >>> 24 & 255);
|
|
20
|
+
}
|
|
21
|
+
i32(value) {
|
|
22
|
+
return this.u32(value >>> 0);
|
|
23
|
+
}
|
|
24
|
+
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
25
|
+
f64(value) {
|
|
26
|
+
const buffer = /* @__PURE__ */ new ArrayBuffer(8);
|
|
27
|
+
new DataView(buffer).setFloat64(0, value, true);
|
|
28
|
+
this.parts.push(new Uint8Array(buffer));
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
/** Appends raw bytes -- for a field already encoded elsewhere (a packed bitfield word, a string's own byte run). */
|
|
32
|
+
bytes(data) {
|
|
33
|
+
this.parts.push(data);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
build() {
|
|
37
|
+
const total = this.parts.reduce((sum, part) => sum + part.length, 0);
|
|
38
|
+
const out = new Uint8Array(total);
|
|
39
|
+
let offset = 0;
|
|
40
|
+
for (const part of this.parts) {
|
|
41
|
+
out.set(part, offset);
|
|
42
|
+
offset += part.length;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
exports.RecordBuilder = RecordBuilder;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/biff/builder.d.ts
|
|
2
|
+
declare class RecordBuilder {
|
|
3
|
+
private readonly parts;
|
|
4
|
+
private pushBytes;
|
|
5
|
+
u8(value: number): this;
|
|
6
|
+
u16(value: number): this;
|
|
7
|
+
/** Composed with multiplication rather than `<< 24`, matching biff/cursor.ts's own u32 reader: a shift would produce a signed result for any value with the top bit set. */
|
|
8
|
+
u32(value: number): this;
|
|
9
|
+
i32(value: number): this;
|
|
10
|
+
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
11
|
+
f64(value: number): this;
|
|
12
|
+
/** Appends raw bytes -- for a field already encoded elsewhere (a packed bitfield word, a string's own byte run). */
|
|
13
|
+
bytes(data: Uint8Array<ArrayBuffer>): this;
|
|
14
|
+
build(): Uint8Array<ArrayBuffer>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { RecordBuilder };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/biff/builder.d.ts
|
|
2
|
+
declare class RecordBuilder {
|
|
3
|
+
private readonly parts;
|
|
4
|
+
private pushBytes;
|
|
5
|
+
u8(value: number): this;
|
|
6
|
+
u16(value: number): this;
|
|
7
|
+
/** Composed with multiplication rather than `<< 24`, matching biff/cursor.ts's own u32 reader: a shift would produce a signed result for any value with the top bit set. */
|
|
8
|
+
u32(value: number): this;
|
|
9
|
+
i32(value: number): this;
|
|
10
|
+
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
11
|
+
f64(value: number): this;
|
|
12
|
+
/** Appends raw bytes -- for a field already encoded elsewhere (a packed bitfield word, a string's own byte run). */
|
|
13
|
+
bytes(data: Uint8Array<ArrayBuffer>): this;
|
|
14
|
+
build(): Uint8Array<ArrayBuffer>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { RecordBuilder };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/biff/builder.ts
|
|
2
|
+
var RecordBuilder = class {
|
|
3
|
+
parts = [];
|
|
4
|
+
pushBytes(...bytes) {
|
|
5
|
+
this.parts.push(new Uint8Array(bytes));
|
|
6
|
+
return this;
|
|
7
|
+
}
|
|
8
|
+
u8(value) {
|
|
9
|
+
return this.pushBytes(value & 255);
|
|
10
|
+
}
|
|
11
|
+
u16(value) {
|
|
12
|
+
const bits = value & 65535;
|
|
13
|
+
return this.pushBytes(bits & 255, bits >>> 8 & 255);
|
|
14
|
+
}
|
|
15
|
+
/** Composed with multiplication rather than `<< 24`, matching biff/cursor.ts's own u32 reader: a shift would produce a signed result for any value with the top bit set. */
|
|
16
|
+
u32(value) {
|
|
17
|
+
const bits = value >>> 0;
|
|
18
|
+
return this.pushBytes(bits & 255, bits >>> 8 & 255, bits >>> 16 & 255, bits >>> 24 & 255);
|
|
19
|
+
}
|
|
20
|
+
i32(value) {
|
|
21
|
+
return this.u32(value >>> 0);
|
|
22
|
+
}
|
|
23
|
+
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
24
|
+
f64(value) {
|
|
25
|
+
const buffer = /* @__PURE__ */ new ArrayBuffer(8);
|
|
26
|
+
new DataView(buffer).setFloat64(0, value, true);
|
|
27
|
+
this.parts.push(new Uint8Array(buffer));
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
/** Appends raw bytes -- for a field already encoded elsewhere (a packed bitfield word, a string's own byte run). */
|
|
31
|
+
bytes(data) {
|
|
32
|
+
this.parts.push(data);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
build() {
|
|
36
|
+
const total = this.parts.reduce((sum, part) => sum + part.length, 0);
|
|
37
|
+
const out = new Uint8Array(total);
|
|
38
|
+
let offset = 0;
|
|
39
|
+
for (const part of this.parts) {
|
|
40
|
+
out.set(part, offset);
|
|
41
|
+
offset += part.length;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { RecordBuilder };
|
package/dist/biff/cursor.cjs
CHANGED
|
@@ -53,6 +53,10 @@ var BlockCursor = class {
|
|
|
53
53
|
i32() {
|
|
54
54
|
return this.u32() | 0;
|
|
55
55
|
}
|
|
56
|
+
/** A signed 16-bit integer, sign-extended by shifting the raw value out of and back into the low 16 bits -- what XTI's itabFirst/itabLast ([MS-XLS] 2.5.344) and a handful of other structures carry. */
|
|
57
|
+
i16() {
|
|
58
|
+
return this.u16() << 16 >> 16;
|
|
59
|
+
}
|
|
56
60
|
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
57
61
|
f64() {
|
|
58
62
|
const raw = this.take(8);
|
package/dist/biff/cursor.d.cts
CHANGED
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
declare class BlockCursor {
|
|
3
|
-
private readonly blocks;
|
|
4
|
-
private blockIndex;
|
|
5
|
-
private offset;
|
|
6
|
-
constructor(blocks: readonly Uint8Array<ArrayBuffer>[]);
|
|
7
|
-
/** Advances past any exhausted or empty blocks, so the cursor always rests either on a readable byte or past the end of the last block. A Continue carrying no data is legal and must not read as the end of the record. */
|
|
8
|
-
private settle;
|
|
9
|
-
private nextByte;
|
|
10
|
-
/** Whether any unread byte remains, in this block or a later one. */
|
|
11
|
-
hasMore(): boolean;
|
|
12
|
-
/** How many unread bytes remain in the block the cursor currently rests in -- the distance to the next continuation boundary, which a string's character run must not cross without consuming a new flag byte. */
|
|
13
|
-
remainingInBlock(): number;
|
|
14
|
-
/** The index of the block the cursor currently rests in, so a caller can detect that a read crossed into a new one. */
|
|
15
|
-
blockPosition(): number;
|
|
16
|
-
u8(): number;
|
|
17
|
-
u16(): number;
|
|
18
|
-
u32(): number;
|
|
19
|
-
i32(): number;
|
|
20
|
-
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
21
|
-
f64(): number;
|
|
22
|
-
/** The next `count` bytes, copied out. Spans block boundaries. */
|
|
23
|
-
take(count: number): Uint8Array<ArrayBuffer>;
|
|
24
|
-
/** Advances `count` bytes without materialising them -- for a field this package reads past rather than reads. */
|
|
25
|
-
skip(count: number): void;
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
1
|
+
import { t as BlockCursor } from "../cursor-VMtw9uVP.cjs";
|
|
28
2
|
export { BlockCursor };
|
package/dist/biff/cursor.d.ts
CHANGED
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
declare class BlockCursor {
|
|
3
|
-
private readonly blocks;
|
|
4
|
-
private blockIndex;
|
|
5
|
-
private offset;
|
|
6
|
-
constructor(blocks: readonly Uint8Array<ArrayBuffer>[]);
|
|
7
|
-
/** Advances past any exhausted or empty blocks, so the cursor always rests either on a readable byte or past the end of the last block. A Continue carrying no data is legal and must not read as the end of the record. */
|
|
8
|
-
private settle;
|
|
9
|
-
private nextByte;
|
|
10
|
-
/** Whether any unread byte remains, in this block or a later one. */
|
|
11
|
-
hasMore(): boolean;
|
|
12
|
-
/** How many unread bytes remain in the block the cursor currently rests in -- the distance to the next continuation boundary, which a string's character run must not cross without consuming a new flag byte. */
|
|
13
|
-
remainingInBlock(): number;
|
|
14
|
-
/** The index of the block the cursor currently rests in, so a caller can detect that a read crossed into a new one. */
|
|
15
|
-
blockPosition(): number;
|
|
16
|
-
u8(): number;
|
|
17
|
-
u16(): number;
|
|
18
|
-
u32(): number;
|
|
19
|
-
i32(): number;
|
|
20
|
-
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
21
|
-
f64(): number;
|
|
22
|
-
/** The next `count` bytes, copied out. Spans block boundaries. */
|
|
23
|
-
take(count: number): Uint8Array<ArrayBuffer>;
|
|
24
|
-
/** Advances `count` bytes without materialising them -- for a field this package reads past rather than reads. */
|
|
25
|
-
skip(count: number): void;
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
1
|
+
import { t as BlockCursor } from "../cursor-VMtw9uVP.js";
|
|
28
2
|
export { BlockCursor };
|
package/dist/biff/cursor.js
CHANGED
|
@@ -52,6 +52,10 @@ var BlockCursor = class {
|
|
|
52
52
|
i32() {
|
|
53
53
|
return this.u32() | 0;
|
|
54
54
|
}
|
|
55
|
+
/** A signed 16-bit integer, sign-extended by shifting the raw value out of and back into the low 16 bits -- what XTI's itabFirst/itabLast ([MS-XLS] 2.5.344) and a handful of other structures carry. */
|
|
56
|
+
i16() {
|
|
57
|
+
return this.u16() << 16 >> 16;
|
|
58
|
+
}
|
|
55
59
|
/** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
|
|
56
60
|
f64() {
|
|
57
61
|
const raw = this.take(8);
|
package/dist/biff/errors.cjs
CHANGED
|
@@ -14,5 +14,12 @@ const ERROR_TEXT = /* @__PURE__ */ new Map([
|
|
|
14
14
|
function errorTextOf(code) {
|
|
15
15
|
return ERROR_TEXT.get(code);
|
|
16
16
|
}
|
|
17
|
+
/** The write direction's own lookup, built once from ERROR_TEXT rather than as a second hand-maintained table, so the two directions cannot drift apart. */
|
|
18
|
+
const ERROR_CODE = new Map(Array.from(ERROR_TEXT, ([code, text]) => [text, code]));
|
|
19
|
+
/** The BIFF8 error code for a cell's displayed error text, or undefined when the text is not one of the eight [MS-XLS] defines -- which the writer refuses to guess a code for rather than silently substituting a different error. */
|
|
20
|
+
function errorCodeOf(text) {
|
|
21
|
+
return ERROR_CODE.get(text);
|
|
22
|
+
}
|
|
17
23
|
//#endregion
|
|
24
|
+
exports.errorCodeOf = errorCodeOf;
|
|
18
25
|
exports.errorTextOf = errorTextOf;
|
package/dist/biff/errors.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/biff/errors.d.ts
|
|
2
2
|
/** The displayed spelling of a BIFF8 error code, or undefined for a code [MS-XLS] does not define -- which the caller degrades rather than guessing a spelling for. */
|
|
3
3
|
declare function errorTextOf(code: number): string | undefined;
|
|
4
|
+
/** The BIFF8 error code for a cell's displayed error text, or undefined when the text is not one of the eight [MS-XLS] defines -- which the writer refuses to guess a code for rather than silently substituting a different error. */
|
|
5
|
+
declare function errorCodeOf(text: string): number | undefined;
|
|
4
6
|
//#endregion
|
|
5
|
-
export { errorTextOf };
|
|
7
|
+
export { errorCodeOf, errorTextOf };
|
package/dist/biff/errors.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/biff/errors.d.ts
|
|
2
2
|
/** The displayed spelling of a BIFF8 error code, or undefined for a code [MS-XLS] does not define -- which the caller degrades rather than guessing a spelling for. */
|
|
3
3
|
declare function errorTextOf(code: number): string | undefined;
|
|
4
|
+
/** The BIFF8 error code for a cell's displayed error text, or undefined when the text is not one of the eight [MS-XLS] defines -- which the writer refuses to guess a code for rather than silently substituting a different error. */
|
|
5
|
+
declare function errorCodeOf(text: string): number | undefined;
|
|
4
6
|
//#endregion
|
|
5
|
-
export { errorTextOf };
|
|
7
|
+
export { errorCodeOf, errorTextOf };
|