@shbernal/ts-xlsx 1.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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014-2019 Guyon Roche (original ExcelJS work)
|
|
4
|
+
Copyright (c) 2026 shbernal (ts-xlsx, an independent hard fork)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# ts-xlsx
|
|
2
|
+
|
|
3
|
+
A TypeScript-first library for reading and writing spreadsheet documents
|
|
4
|
+
(`.xlsx` / OOXML, and CSV) — synchronous, `Uint8Array`-native, and dependency-lean.
|
|
5
|
+
|
|
6
|
+
> **Status — 1.0.0, the first release.**
|
|
7
|
+
> `ts-xlsx` began as a hard fork of [ExcelJS](https://github.com/exceljs/exceljs)
|
|
8
|
+
> and has been rebuilt from the ground up into a modern, strict-TypeScript library.
|
|
9
|
+
> **It carries no backwards-compatibility guarantee with ExcelJS** — the API below is
|
|
10
|
+
> its own, not a drop-in. From 1.0.0 onward it follows
|
|
11
|
+
> [SemVer](https://semver.org/) against *itself*; see the [changelog](CHANGELOG.md).
|
|
12
|
+
> See [`CLAUDE.md`](CLAUDE.md) for the goals and [`docs/architecture.md`](docs/architecture.md)
|
|
13
|
+
> for the design, and [migrating from ExcelJS](docs/migrating-from-exceljs.md) if you are coming across.
|
|
14
|
+
|
|
15
|
+
## Why it exists
|
|
16
|
+
|
|
17
|
+
Upstream ExcelJS is effectively unmaintained — no release since 2023, a backlog of
|
|
18
|
+
hundreds of issues and PRs — while still serving tens of millions of downloads a month.
|
|
19
|
+
`ts-xlsx` extracts the accumulated value from that backlog (as a permanent regression
|
|
20
|
+
corpus) and discards the debt: no untyped surfaces, no callback APIs, a single small
|
|
21
|
+
runtime dependency, and every behavior pinned by a test. This is not a compatibility
|
|
22
|
+
shim. It is a different, better library that happens to share ancestry.
|
|
23
|
+
|
|
24
|
+
What that buys you today:
|
|
25
|
+
|
|
26
|
+
- **Strict, precise types are the contract.** `strict` + `noUncheckedIndexedAccess` +
|
|
27
|
+
`exactOptionalPropertyTypes`. The published `.d.ts` *is* the documentation, and the
|
|
28
|
+
[API reference](docs/api/README.md) is generated straight from it.
|
|
29
|
+
- **Synchronous, buffer-native I/O.** `readXlsx`/`writeXlsx` take and return a
|
|
30
|
+
`Uint8Array` — no streams to await for the buffered path, no Node `Buffer` assumption,
|
|
31
|
+
so the same code runs in Node and the browser.
|
|
32
|
+
- **Streaming when you need it.** Bounded-memory row streaming both ways — read a large
|
|
33
|
+
workbook without materializing it, or write one incrementally to a Node stream so peak
|
|
34
|
+
memory stays flat.
|
|
35
|
+
- **One runtime dependency** ([`fflate`](https://github.com/101arrowz/fflate) for zip),
|
|
36
|
+
a hand-written SAX reader with bounded allocation on every parser path, and a
|
|
37
|
+
build-free, strict-typed source tree.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```shell
|
|
42
|
+
npm install @shbernal/ts-xlsx
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Node ≥ 24 (or any modern browser bundler). ESM only.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
The buffered path is synchronous. `writeXlsx` returns the file bytes; `readXlsx` takes them back.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import {Workbook, writeXlsx, readXlsx} from '@shbernal/ts-xlsx';
|
|
53
|
+
|
|
54
|
+
// --- write ---
|
|
55
|
+
const wb = new Workbook();
|
|
56
|
+
const sheet = wb.addWorksheet('People');
|
|
57
|
+
|
|
58
|
+
sheet.getCell('A1').value = 'Name';
|
|
59
|
+
sheet.getCell('B1').value = 'Joined';
|
|
60
|
+
sheet.addRow(['Ada', new Date('2026-01-01')]);
|
|
61
|
+
sheet.addRow(['Grace', new Date('2026-02-14')]);
|
|
62
|
+
sheet.getCell('C1').value = {formula: 'COUNTA(A:A)', result: 3};
|
|
63
|
+
|
|
64
|
+
const bytes: Uint8Array = writeXlsx(wb);
|
|
65
|
+
|
|
66
|
+
// --- read it back ---
|
|
67
|
+
const reopened = readXlsx(bytes);
|
|
68
|
+
const people = reopened.getWorksheet('People');
|
|
69
|
+
console.log(people?.getCell('A2').value); // 'Ada'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Persisting to disk is your platform's job, not the library's — `writeXlsx` hands you the
|
|
73
|
+
bytes:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {writeFileSync, readFileSync} from 'node:fs';
|
|
77
|
+
|
|
78
|
+
writeFileSync('people.xlsx', writeXlsx(wb));
|
|
79
|
+
const wb2 = readXlsx(readFileSync('people.xlsx')); // a Buffer is a Uint8Array
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Core model
|
|
83
|
+
|
|
84
|
+
- **`Workbook`** — the document. `addWorksheet(name)`, `getWorksheet(nameOrId)`,
|
|
85
|
+
`worksheets`, defined names, images, and workbook-level properties.
|
|
86
|
+
- **`Worksheet`** — a sheet. `getCell('B3')`, `addRow(values)`, `addTable(...)`,
|
|
87
|
+
`mergeCells('A1:B2')`, `getRow(n)` / `getColumn(n)`, `rows()` / `columns()`, page setup,
|
|
88
|
+
and print options.
|
|
89
|
+
- **`Row`** / **`Column`** — one line of the grid. Formatting is flat (`row.height = 20`,
|
|
90
|
+
`column.width = 12`, `column.key = 'name'`), and cells are reachable from it:
|
|
91
|
+
`row.getCell('B')`, `row.cells`, `row.values`.
|
|
92
|
+
- **`Cell`** — one cell. `cell.value` is the whole story: a `number`, `string`,
|
|
93
|
+
`boolean`, `Date`, `null` (empty), a formula (`{formula, result}`), rich text, a
|
|
94
|
+
hyperlink, or an error — all precisely typed as [`CellValue`](docs/api/cell-values.md).
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
sheet.getRow(1).height = 20;
|
|
98
|
+
sheet.getRow(1).values = ['Name', 'Joined'];
|
|
99
|
+
sheet.getColumn(1).width = 24;
|
|
100
|
+
sheet.getRow(2).getCell('B').value = new Date('2026-01-01');
|
|
101
|
+
|
|
102
|
+
for (const row of sheet.rows()) {
|
|
103
|
+
console.log(row.number, row.values);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`Row` and `Column` are *handles*, not snapshots: they read and write straight through to the
|
|
108
|
+
sheet, so two handles on the same line always agree, and reading one creates nothing — asking
|
|
109
|
+
about row 500 costs nothing and does not extend the used range. Position is fixed, exactly as a
|
|
110
|
+
`Cell`'s is: after a splice, `getRow(3)` still means row 3, now holding whatever moved there.
|
|
111
|
+
|
|
112
|
+
Addresses are honest: an axis a reference doesn't mention is `undefined`, never a
|
|
113
|
+
sentinel — see [`decodeAddress`](docs/api/addresses-ranges.md).
|
|
114
|
+
|
|
115
|
+
Charts, vector shapes, slicers, and legacy form controls are **round-trip-only**: a
|
|
116
|
+
workbook that has them keeps them byte-faithfully through a load/edit/save, but there is
|
|
117
|
+
no API to author a new one — see [`docs/api/preserved.md`](docs/api/preserved.md) and
|
|
118
|
+
[ADR-0014](docs/decisions/0014-charts-shapes-slicers-are-round-trip-only-for-1-0.md).
|
|
119
|
+
|
|
120
|
+
## Reading, writing, streaming, CSV
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import {
|
|
124
|
+
readXlsx, writeXlsx, // buffered .xlsx (Uint8Array ⇄ Workbook)
|
|
125
|
+
readSheetRows, // stream one sheet's rows, bounded memory
|
|
126
|
+
readWorkbookStream, // stream every sheet, rows one at a time
|
|
127
|
+
WorkbookStreamWriter, // write a workbook incrementally, bounded memory
|
|
128
|
+
readCsv, writeCsv, // CSV as Uint8Array
|
|
129
|
+
writeCsvText, // CSV as a string
|
|
130
|
+
} from '@shbernal/ts-xlsx';
|
|
131
|
+
|
|
132
|
+
// Bounded-memory extraction — the whole workbook is never materialized:
|
|
133
|
+
for (const row of readSheetRows(bytes, {sheet: 'People'})) {
|
|
134
|
+
console.log(row.number, row.cells.map((c) => c.value));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Bounded-memory generation — commit each row to serialize and free it as you go:
|
|
138
|
+
const writer = new WorkbookStreamWriter();
|
|
139
|
+
const out = writer.addWorksheet('Big');
|
|
140
|
+
for (let i = 1; i <= 1_000_000; i++) out.addRow([i, i * i]).commit();
|
|
141
|
+
out.commit();
|
|
142
|
+
const packaged: Uint8Array = await writer.commit(); // also delivered via writer.stream
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The streaming writer is asynchronous where the buffered path is synchronous: `commit()`
|
|
146
|
+
resolves to the package bytes and simultaneously pipes them through `writer.stream` (a Node
|
|
147
|
+
`Readable`), so `writer.stream.pipe(res)` streams a workbook straight to an HTTP response.
|
|
148
|
+
|
|
149
|
+
## Writing without blocking the event loop
|
|
150
|
+
|
|
151
|
+
`writeXlsx` spends the whole cost of DEFLATE on the calling thread — seconds, for a large
|
|
152
|
+
workbook, during which a server answers nothing. `writeXlsxAsync` produces the same package
|
|
153
|
+
with the compression handed to worker threads:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import {writeXlsxAsync} from '@shbernal/ts-xlsx';
|
|
157
|
+
|
|
158
|
+
const bytes = await writeXlsxAsync(wb); // same package, event loop stays live
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Measured on a ~42 MB part map: one large sheet takes the same wall-clock either way, but the
|
|
162
|
+
longest event-loop stall drops from the entire write to ~17 ms; a twenty-sheet workbook also
|
|
163
|
+
finishes about 2.4× sooner, because its parts deflate in parallel. Expect responsiveness
|
|
164
|
+
always and speed only when there is more than one substantial part.
|
|
165
|
+
|
|
166
|
+
There is deliberately **no `readXlsxAsync`**. Reading is dominated by XML parsing and model
|
|
167
|
+
building, which no worker can take off the calling thread, so it would advertise a
|
|
168
|
+
non-blocking read and then block for most of its duration — and the reader's zip-bomb ceiling
|
|
169
|
+
is enforced by counting output between synchronous input slices, a guarantee that weakens the
|
|
170
|
+
moment inflation moves to a worker. If you need a non-blocking read, run the whole read in a
|
|
171
|
+
worker. See [ADR-0024](docs/decisions/0024-async-is-one-writer-not-a-mirrored-pair.md).
|
|
172
|
+
|
|
173
|
+
The reader decodes untrusted input defensively — entities are decoded but never
|
|
174
|
+
expanded, and inflation is bounded by a running output counter rather than any declared
|
|
175
|
+
size, so a malformed or hostile package can't exhaust memory.
|
|
176
|
+
|
|
177
|
+
## Entry points
|
|
178
|
+
|
|
179
|
+
The bare package name gives you everything, and with a bundler that is the right default:
|
|
180
|
+
`"sideEffects": false` is declared, so anything you don't reference is dropped. The subpaths
|
|
181
|
+
are for when you'd rather the module graph itself said which half of the library you depend
|
|
182
|
+
on — a Lambda with no bundler, a service that only classifies failures:
|
|
183
|
+
|
|
184
|
+
| Import from | You get | It loads |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| `@shbernal/ts-xlsx` | everything | 902 KB |
|
|
187
|
+
| `@shbernal/ts-xlsx/core` | `Workbook`, `Worksheet`, `Cell`, styles, values, addresses | 332 KB |
|
|
188
|
+
| `@shbernal/ts-xlsx/xlsx` | `readXlsx`, `writeXlsx`/`writeXlsxAsync`, the streaming pair, VBA part edits | 887 KB |
|
|
189
|
+
| `@shbernal/ts-xlsx/xlsb` | `readXlsb` | 469 KB |
|
|
190
|
+
| `@shbernal/ts-xlsx/csv` | `readCsv`, `writeCsv`, `writeCsvText` | 341 KB |
|
|
191
|
+
| `@shbernal/ts-xlsx/vba` | `parseVbaProject`, `addVbaReference`, `removeVbaModule` | 73 KB |
|
|
192
|
+
| `@shbernal/ts-xlsx/customui` | `parseCustomUi` and the ribbon types | 26 KB |
|
|
193
|
+
| `@shbernal/ts-xlsx/errors` | every error class the library throws | 12 KB |
|
|
194
|
+
|
|
195
|
+
Every error class lives in `/errors` and nowhere else, because a container-level failure
|
|
196
|
+
belongs to no single codec — `readXlsx` and `readXlsb` both raise `UnsupportedFormatError`.
|
|
197
|
+
Catching and classifying therefore costs 12 KB, not a parser.
|
|
198
|
+
|
|
199
|
+
`/xlsx` is barely cheaper than the whole package, and that is honest rather than a defect:
|
|
200
|
+
`readXlsx` sniffs the bytes and hands a binary package to the BIFF12 reader, so the `.xlsb`
|
|
201
|
+
codec is not optional on that path. The numbers above are the static-import closure of each
|
|
202
|
+
entry, measured by `pnpm run size` — a bundler can only shrink them further.
|
|
203
|
+
|
|
204
|
+
## API reference
|
|
205
|
+
|
|
206
|
+
The full reference is generated from the public types — it cannot drift from what the
|
|
207
|
+
compiler accepts — and lives in **[`docs/api/`](docs/api/README.md)**. Regenerate it with:
|
|
208
|
+
|
|
209
|
+
```shell
|
|
210
|
+
pnpm run docs
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Coming from ExcelJS?
|
|
214
|
+
|
|
215
|
+
Read **[migrating from ExcelJS](docs/migrating-from-exceljs.md)**. The short version: the
|
|
216
|
+
shapes are deliberately different (synchronous `Uint8Array` I/O instead of async
|
|
217
|
+
`Buffer`/stream methods, `readXlsx`/`writeXlsx` free functions instead of
|
|
218
|
+
`workbook.xlsx.*`), because the goal was the *right* API, not the familiar one.
|
|
219
|
+
|
|
220
|
+
## Design principles
|
|
221
|
+
|
|
222
|
+
This project is optimized to be built and maintained largely by autonomous agents, with
|
|
223
|
+
a machine-checkable safety net as the primary guarantor of correctness. The rules that
|
|
224
|
+
govern every change are in [`CLAUDE.md`](CLAUDE.md); the design and working agreements are
|
|
225
|
+
in [`docs/architecture.md`](docs/architecture.md); notable decisions are recorded under
|
|
226
|
+
[`docs/decisions/`](docs/decisions/).
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
[MIT](LICENSE) — inherited from ExcelJS and retained.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Excel's column bounds: `A` (1) through `XFD` (16384). */
|
|
2
|
+
export declare const MAX_COLUMN = 16384;
|
|
3
|
+
/** Excel's row bound: 1 through 1048576. The other axis of {@link MAX_COLUMN}. */
|
|
4
|
+
export declare const MAX_ROW = 1048576;
|
|
5
|
+
/** A decoded single-cell reference. An axis the reference omits is `undefined`. */
|
|
6
|
+
export interface CellAddress {
|
|
7
|
+
/** Canonical A1 form with `$` anchors stripped — e.g. `"B2"`, `"1"`, `"A"`. */
|
|
8
|
+
readonly address: string;
|
|
9
|
+
/** 1-based column, or `undefined` for a row-only reference (`$1`). */
|
|
10
|
+
readonly col: number | undefined;
|
|
11
|
+
/** 1-based row, or `undefined` for a column-only reference (`$A`). */
|
|
12
|
+
readonly row: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A decoded range reference. Corners are the min/max of the endpoints per axis;
|
|
16
|
+
* an axis neither endpoint mentions (a whole-row or whole-column range) is
|
|
17
|
+
* `undefined` on every corner and simply absent from `dimensions`.
|
|
18
|
+
*/
|
|
19
|
+
export interface RangeAddress {
|
|
20
|
+
readonly top: number | undefined;
|
|
21
|
+
readonly left: number | undefined;
|
|
22
|
+
readonly bottom: number | undefined;
|
|
23
|
+
readonly right: number | undefined;
|
|
24
|
+
/** The originating sheet, present only when the reference carried one. */
|
|
25
|
+
readonly sheetName?: string;
|
|
26
|
+
readonly tl: CellAddress;
|
|
27
|
+
readonly br: CellAddress;
|
|
28
|
+
/** Canonical `tl:br` form — `"A1:B2"`, `"1:1"` (rows), `"A:A"` (columns). */
|
|
29
|
+
readonly dimensions: string;
|
|
30
|
+
}
|
|
31
|
+
/** Convert a 1-based column number to its letters (`1 → "A"`, `27 → "AA"`). */
|
|
32
|
+
export declare function numberToColumn(n: number): string;
|
|
33
|
+
/** Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`). */
|
|
34
|
+
export declare function columnToNumber(letters: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* Decode a single cell/row/column reference into `{address, col, row}`. Anchoring
|
|
37
|
+
* `$` signs are accepted and dropped; an absent axis is `undefined`.
|
|
38
|
+
*
|
|
39
|
+
* @throws {SyntaxError} if the reference mentions neither a column nor a row.
|
|
40
|
+
*/
|
|
41
|
+
export declare function decodeAddress(reference: string): CellAddress;
|
|
42
|
+
/**
|
|
43
|
+
* Decode a range reference (`A1:B2`, `$1:$1`, `Sheet1!$A:$A`) into its corners and
|
|
44
|
+
* canonical dimensions. A single reference collapses to a degenerate range whose
|
|
45
|
+
* corners coincide.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decodeRange(reference: string): RangeAddress;
|
|
48
|
+
/** Encode a 1-based `col`/`row` pair into its canonical A1 address (`"B2"`). */
|
|
49
|
+
export declare function encodeAddress(col: number, row: number): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Address decoding — the foundational spreadsheet primitive.
|
|
2
|
+
//
|
|
3
|
+
// Every higher layer (cells, ranges, defined names, formulas) is ultimately
|
|
4
|
+
// addressed by a `col`/`row` pair, so this module is the first thing the rewrite
|
|
5
|
+
// builds and the bedrock everything else stands on. It is pure, allocation-bounded,
|
|
6
|
+
// and has no I/O or platform dependency.
|
|
7
|
+
//
|
|
8
|
+
// The honesty rule that drives the shape here: an axis that a reference does not
|
|
9
|
+
// mention is `undefined`, never a sentinel. A whole-row reference (`$1`) has no
|
|
10
|
+
// column; a whole-column reference (`$A:$A`) has no row. Legacy code let those
|
|
11
|
+
// absent axes decay into `NaN`/`"undefined"` and leak into serialized addresses
|
|
12
|
+
// (`"$undefined$1"`, `"NaN:NaN"`) — the exact defect the corpus locks against.
|
|
13
|
+
/** Excel's column bounds: `A` (1) through `XFD` (16384). */
|
|
14
|
+
export const MAX_COLUMN = 16384;
|
|
15
|
+
/** Excel's row bound: 1 through 1048576. The other axis of {@link MAX_COLUMN}. */
|
|
16
|
+
export const MAX_ROW = 1048576;
|
|
17
|
+
const SINGLE_REF = /^\$?([A-Z]*)\$?(\d*)$/;
|
|
18
|
+
// A leading `Sheet!` prefix: quoted (`'a''b'!`, doubled apostrophe escapes one) or
|
|
19
|
+
// bare (`Sheet1!`). Group 1 = quoted body, group 2 = bare name, group 3 = the rest.
|
|
20
|
+
const SHEET_PREFIX = /^(?:(?:'((?:[^']|'')*)')|([^'!]+))!(.*)$/;
|
|
21
|
+
/** Convert a 1-based column number to its letters (`1 → "A"`, `27 → "AA"`). */
|
|
22
|
+
export function numberToColumn(n) {
|
|
23
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_COLUMN) {
|
|
24
|
+
throw new RangeError(`column ${n} is out of bounds — Excel supports 1..${MAX_COLUMN}`);
|
|
25
|
+
}
|
|
26
|
+
let letters = '';
|
|
27
|
+
let remaining = n;
|
|
28
|
+
while (remaining > 0) {
|
|
29
|
+
const digit = (remaining - 1) % 26;
|
|
30
|
+
letters = String.fromCharCode(65 + digit) + letters;
|
|
31
|
+
remaining = Math.floor((remaining - 1) / 26);
|
|
32
|
+
}
|
|
33
|
+
return letters;
|
|
34
|
+
}
|
|
35
|
+
/** Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`). */
|
|
36
|
+
export function columnToNumber(letters) {
|
|
37
|
+
if (letters.length === 0 || letters.length > 3) {
|
|
38
|
+
throw new RangeError(`invalid column letters: "${letters}"`);
|
|
39
|
+
}
|
|
40
|
+
let n = 0;
|
|
41
|
+
for (let i = 0; i < letters.length; i++) {
|
|
42
|
+
const code = letters.charCodeAt(i);
|
|
43
|
+
if (code < 65 || code > 90) {
|
|
44
|
+
throw new RangeError(`invalid column letters: "${letters}"`);
|
|
45
|
+
}
|
|
46
|
+
n = n * 26 + (code - 64);
|
|
47
|
+
}
|
|
48
|
+
if (n > MAX_COLUMN) {
|
|
49
|
+
throw new RangeError(`column "${letters}" is out of bounds — Excel supports up to ${MAX_COLUMN} (XFD)`);
|
|
50
|
+
}
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
/** Build a {@link CellAddress} corner straight from optional numeric axes — the address string is
|
|
54
|
+
* assembled from the parts we already hold, so no encode-then-decode round-trip is needed. An axis the
|
|
55
|
+
* corner omits stays `undefined`; both absent yields the empty address (`""`). */
|
|
56
|
+
function makeCellAddress(col, row) {
|
|
57
|
+
return {
|
|
58
|
+
address: `${col !== undefined ? numberToColumn(col) : ''}${row !== undefined ? row : ''}`,
|
|
59
|
+
col,
|
|
60
|
+
row,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decode a single cell/row/column reference into `{address, col, row}`. Anchoring
|
|
65
|
+
* `$` signs are accepted and dropped; an absent axis is `undefined`.
|
|
66
|
+
*
|
|
67
|
+
* @throws {SyntaxError} if the reference mentions neither a column nor a row.
|
|
68
|
+
*/
|
|
69
|
+
export function decodeAddress(reference) {
|
|
70
|
+
const match = SINGLE_REF.exec(reference);
|
|
71
|
+
if (!match) {
|
|
72
|
+
throw new SyntaxError(`invalid cell reference: "${reference}"`);
|
|
73
|
+
}
|
|
74
|
+
const letters = match[1] ?? '';
|
|
75
|
+
const digits = match[2] ?? '';
|
|
76
|
+
if (letters.length === 0 && digits.length === 0) {
|
|
77
|
+
throw new SyntaxError(`invalid cell reference: "${reference}"`);
|
|
78
|
+
}
|
|
79
|
+
const col = letters.length > 0 ? columnToNumber(letters) : undefined;
|
|
80
|
+
const row = digits.length > 0 ? Number.parseInt(digits, 10) : undefined;
|
|
81
|
+
return { address: `${letters}${digits}`, col, row };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decode a range reference (`A1:B2`, `$1:$1`, `Sheet1!$A:$A`) into its corners and
|
|
85
|
+
* canonical dimensions. A single reference collapses to a degenerate range whose
|
|
86
|
+
* corners coincide.
|
|
87
|
+
*/
|
|
88
|
+
export function decodeRange(reference) {
|
|
89
|
+
const prefix = SHEET_PREFIX.exec(reference);
|
|
90
|
+
let sheetName;
|
|
91
|
+
let body = reference;
|
|
92
|
+
if (prefix) {
|
|
93
|
+
const quoted = prefix[1];
|
|
94
|
+
sheetName = quoted !== undefined ? quoted.replace(/''/g, "'") : prefix[2];
|
|
95
|
+
body = prefix[3] ?? '';
|
|
96
|
+
}
|
|
97
|
+
const parts = body.split(':');
|
|
98
|
+
const start = decodeAddress(parts[0] ?? '');
|
|
99
|
+
const end = parts.length > 1 ? decodeAddress(parts[1] ?? '') : start;
|
|
100
|
+
const cols = [start.col, end.col].filter((v) => v !== undefined);
|
|
101
|
+
const rows = [start.row, end.row].filter((v) => v !== undefined);
|
|
102
|
+
const left = cols.length > 0 ? Math.min(...cols) : undefined;
|
|
103
|
+
const right = cols.length > 0 ? Math.max(...cols) : undefined;
|
|
104
|
+
const top = rows.length > 0 ? Math.min(...rows) : undefined;
|
|
105
|
+
const bottom = rows.length > 0 ? Math.max(...rows) : undefined;
|
|
106
|
+
const tl = makeCellAddress(left, top);
|
|
107
|
+
const br = makeCellAddress(right, bottom);
|
|
108
|
+
return {
|
|
109
|
+
top,
|
|
110
|
+
left,
|
|
111
|
+
bottom,
|
|
112
|
+
right,
|
|
113
|
+
...(sheetName !== undefined ? { sheetName } : {}),
|
|
114
|
+
tl,
|
|
115
|
+
br,
|
|
116
|
+
dimensions: `${tl.address}:${br.address}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Encode a 1-based `col`/`row` pair into its canonical A1 address (`"B2"`). */
|
|
120
|
+
export function encodeAddress(col, row) {
|
|
121
|
+
if (!Number.isInteger(row) || row < 1) {
|
|
122
|
+
throw new RangeError(`row ${row} is out of bounds — rows start at 1`);
|
|
123
|
+
}
|
|
124
|
+
return `${numberToColumn(col)}${row}`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A worksheet's autofilter: the filtered region plus any per-column criteria narrowing it. A bare
|
|
3
|
+
* range (no columns) is just the header-row dropdowns Excel draws; adding {@link FilterColumn}s
|
|
4
|
+
* records the criteria a column is actively filtered by.
|
|
5
|
+
*/
|
|
6
|
+
export interface AutoFilter {
|
|
7
|
+
/** The filtered region in canonical `A1:C10` form; its top row is the header the dropdowns sit on. */
|
|
8
|
+
readonly ref: string;
|
|
9
|
+
/** The columns actively narrowed, each addressed by its offset from the range's left edge. Empty
|
|
10
|
+
* when the filter only draws dropdowns without hiding any row. */
|
|
11
|
+
readonly columns: readonly FilterColumn[];
|
|
12
|
+
}
|
|
13
|
+
/** One filtered column, addressed by its 0-based offset (`colId`) from the filter range's left edge. */
|
|
14
|
+
export interface FilterColumn {
|
|
15
|
+
readonly colId: number;
|
|
16
|
+
readonly criteria: FilterCriteria;
|
|
17
|
+
}
|
|
18
|
+
/** The two criteria kinds this library models: a discrete value set, or operator predicates. */
|
|
19
|
+
export type FilterCriteria = ValuesFilter | CustomFilter;
|
|
20
|
+
/**
|
|
21
|
+
* A column narrowed to a discrete set of allowed values — the checkbox list in Excel's dropdown.
|
|
22
|
+
* A row survives when its cell in this column matches one of {@link values} (or is blank, when
|
|
23
|
+
* {@link blank} is set).
|
|
24
|
+
*/
|
|
25
|
+
export interface ValuesFilter {
|
|
26
|
+
readonly kind: 'values';
|
|
27
|
+
readonly values: readonly string[];
|
|
28
|
+
readonly blank: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A column narrowed to one or two operator predicates (`> 6`, `<> "draft"`). Two predicates are
|
|
32
|
+
* AND-combined when {@link and} is set, else OR-combined; Excel permits at most two.
|
|
33
|
+
*/
|
|
34
|
+
export interface CustomFilter {
|
|
35
|
+
readonly kind: 'custom';
|
|
36
|
+
readonly and: boolean;
|
|
37
|
+
readonly predicates: readonly CustomFilterPredicate[];
|
|
38
|
+
}
|
|
39
|
+
export interface CustomFilterPredicate {
|
|
40
|
+
readonly operator: CustomFilterOperator;
|
|
41
|
+
/** The comparison operand, kept as its raw string form (a number, or wildcard text like `a*`). */
|
|
42
|
+
readonly val: string;
|
|
43
|
+
}
|
|
44
|
+
export type CustomFilterOperator = 'equal' | 'notEqual' | 'lessThan' | 'lessThanOrEqual' | 'greaterThan' | 'greaterThanOrEqual';
|
|
45
|
+
/** Narrow a raw `operator` attribute to a known {@link CustomFilterOperator}. */
|
|
46
|
+
export declare function isCustomFilterOperator(value: string): value is CustomFilterOperator;
|
|
47
|
+
/**
|
|
48
|
+
* Validate and normalise a settable autofilter into its canonical stored form. A bare range string
|
|
49
|
+
* is the common case (dropdowns, no criteria); an {@link AutoFilter} object carries per-column
|
|
50
|
+
* criteria too. Throws when the range is not a bounded rectangle, or a column's `colId` falls
|
|
51
|
+
* outside it, or a custom filter does not carry one or two predicates — a filter authored wrong is
|
|
52
|
+
* a bug to surface, not to silently repair. (The reader sanitises hostile input before it reaches
|
|
53
|
+
* here, so load never trips these guards.)
|
|
54
|
+
*/
|
|
55
|
+
export declare function canonicalizeAutoFilter(input: string | AutoFilter): AutoFilter;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AuthoringError } from "../errors.js";
|
|
2
|
+
import { decodeRange } from "./address.js";
|
|
3
|
+
const CUSTOM_FILTER_OPERATORS = new Set([
|
|
4
|
+
'equal',
|
|
5
|
+
'notEqual',
|
|
6
|
+
'lessThan',
|
|
7
|
+
'lessThanOrEqual',
|
|
8
|
+
'greaterThan',
|
|
9
|
+
'greaterThanOrEqual',
|
|
10
|
+
]);
|
|
11
|
+
/** Narrow a raw `operator` attribute to a known {@link CustomFilterOperator}. */
|
|
12
|
+
export function isCustomFilterOperator(value) {
|
|
13
|
+
return CUSTOM_FILTER_OPERATORS.has(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate and normalise a settable autofilter into its canonical stored form. A bare range string
|
|
17
|
+
* is the common case (dropdowns, no criteria); an {@link AutoFilter} object carries per-column
|
|
18
|
+
* criteria too. Throws when the range is not a bounded rectangle, or a column's `colId` falls
|
|
19
|
+
* outside it, or a custom filter does not carry one or two predicates — a filter authored wrong is
|
|
20
|
+
* a bug to surface, not to silently repair. (The reader sanitises hostile input before it reaches
|
|
21
|
+
* here, so load never trips these guards.)
|
|
22
|
+
*/
|
|
23
|
+
export function canonicalizeAutoFilter(input) {
|
|
24
|
+
const ref = typeof input === 'string' ? input : input.ref;
|
|
25
|
+
const { top, left, bottom, right, dimensions } = decodeRange(ref);
|
|
26
|
+
if (top === undefined || left === undefined || bottom === undefined || right === undefined) {
|
|
27
|
+
throw new AuthoringError(`autofilter range "${ref}" must be a bounded rectangle`);
|
|
28
|
+
}
|
|
29
|
+
if (typeof input === 'string')
|
|
30
|
+
return { ref: dimensions, columns: [] };
|
|
31
|
+
const width = right - left + 1;
|
|
32
|
+
return {
|
|
33
|
+
ref: dimensions,
|
|
34
|
+
columns: input.columns.map((column) => canonicalizeColumn(column, width)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function canonicalizeColumn(column, width) {
|
|
38
|
+
if (!Number.isInteger(column.colId) || column.colId < 0 || column.colId >= width) {
|
|
39
|
+
throw new AuthoringError(`autofilter colId ${column.colId} is outside the filter range`);
|
|
40
|
+
}
|
|
41
|
+
if (column.criteria.kind === 'custom') {
|
|
42
|
+
const count = column.criteria.predicates.length;
|
|
43
|
+
if (count < 1 || count > 2) {
|
|
44
|
+
throw new AuthoringError(`a custom filter needs one or two predicates, got ${count}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return column;
|
|
48
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { NAMED_STYLE_ID } from './internal.ts';
|
|
2
|
+
import { type Alignment, type Border, type CellStyle, type Fill, type Font, type Protection } from './style.ts';
|
|
3
|
+
import { type CellValue, type RichTextRun, type ValueType } from './value.ts';
|
|
4
|
+
import type { CellModel } from './worksheet.ts';
|
|
5
|
+
/**
|
|
6
|
+
* A single cell owns its value and every style facet outright. Each facet below — fill, number format,
|
|
7
|
+
* font, border, alignment, protection, quote-prefix, and note — is held in the cell's own field and
|
|
8
|
+
* *replaced* (never mutated in place) by its setter, so a facet set on one cell never aliases or bleeds
|
|
9
|
+
* onto its row, column, or sheet siblings. Each facet's own doc covers only what is specific to it.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Cell {
|
|
12
|
+
#private;
|
|
13
|
+
/** 1-based row index. */
|
|
14
|
+
readonly row: number;
|
|
15
|
+
/** 1-based column index. */
|
|
16
|
+
readonly col: number;
|
|
17
|
+
constructor(row: number, col: number);
|
|
18
|
+
/** Canonical A1 address of this cell (`"B3"`). */
|
|
19
|
+
get address(): string;
|
|
20
|
+
/** The cell's value; `null` when empty. Assigning `undefined` clears it. */
|
|
21
|
+
get value(): CellValue;
|
|
22
|
+
set value(value: CellValue | undefined);
|
|
23
|
+
/** The observable {@link ValueType} of the current value. */
|
|
24
|
+
get type(): ValueType;
|
|
25
|
+
/**
|
|
26
|
+
* Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
|
|
27
|
+
* changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
|
|
28
|
+
* cell's typeface, size and colour throughout and bolds the first run.
|
|
29
|
+
*
|
|
30
|
+
* This exists because a run's format element does **not** inherit anything. A `<rPr>` is a
|
|
31
|
+
* *complete* character format, and any facet it omits falls back to the workbook default font
|
|
32
|
+
* ({@link Workbook.defaultFont}) — not to the cell's. Verified against Excel: a cell set to
|
|
33
|
+
* Courier New 16 whose first run carries only `<b/>` renders that run in the workbook default face
|
|
34
|
+
* at the default size, bold, while the rest of the cell renders Courier New 16. So a run authored
|
|
35
|
+
* as `{bold: true}` beside a styled cell silently loses the face, which is the format's rule rather
|
|
36
|
+
* than a bug — and the reason this is a helper rather than a change to how runs are written.
|
|
37
|
+
*
|
|
38
|
+
* Composition is per facet: a facet the run names wins, one it omits comes from the cell. Assigning
|
|
39
|
+
* `value` directly stays the bare path, for a caller who wants a run that deliberately falls back
|
|
40
|
+
* to the workbook default.
|
|
41
|
+
*
|
|
42
|
+
* A cell that names no font of its own needs no composition — an omitted facet already falls back
|
|
43
|
+
* to the workbook default, which is exactly what such a cell renders in — so the runs pass through
|
|
44
|
+
* unchanged.
|
|
45
|
+
*/
|
|
46
|
+
setRichText(runs: readonly RichTextRun[]): void;
|
|
47
|
+
/**
|
|
48
|
+
* The cell's full style — fill, number format, font, border, alignment, and protection — as one
|
|
49
|
+
* {@link CellStyle}, for restyling a cell wholesale without importing {@link applyCellStyle}
|
|
50
|
+
* separately (mirrors {@link Worksheet.model}'s getter/setter pair for the whole sheet). The
|
|
51
|
+
* getter carries only the facets this cell has set (the same shape {@link cellToModel} emits);
|
|
52
|
+
* the setter lays each facet `style` carries onto this cell — like every per-facet setter, it
|
|
53
|
+
* replaces that facet outright but leaves a facet `style` omits untouched, so `cell.style = {...}`
|
|
54
|
+
* composes with prior per-facet sets rather than clearing them wholesale.
|
|
55
|
+
*/
|
|
56
|
+
get style(): CellStyle;
|
|
57
|
+
set style(style: Readonly<CellStyle>);
|
|
58
|
+
/** The cell's background fill, or `undefined` when it has none. */
|
|
59
|
+
get fill(): Fill | undefined;
|
|
60
|
+
set fill(fill: Fill | undefined);
|
|
61
|
+
/**
|
|
62
|
+
* The cell's number-format code (`"0.00%"`, a custom accounting format, …), or
|
|
63
|
+
* `undefined` for the General format. Stored verbatim: the invariant form Excel
|
|
64
|
+
* persists — `.` decimal, `,` grouping, `/` date separator — is neither localized
|
|
65
|
+
* nor rewritten, so the code round-trips character-for-character. A cell that also carries
|
|
66
|
+
* a column-level format keeps both, so overriding one facet never drops the other.
|
|
67
|
+
*/
|
|
68
|
+
get numFmt(): string | undefined;
|
|
69
|
+
set numFmt(numFmt: string | undefined);
|
|
70
|
+
/**
|
|
71
|
+
* The cell's font — bold/italic/underline, size, colour, typeface — as a partial set
|
|
72
|
+
* of the facets that differ from the default (only the facets actually set are carried,
|
|
73
|
+
* exactly as OOXML stores them). `undefined` means the cell uses the workbook default font.
|
|
74
|
+
*/
|
|
75
|
+
get font(): Font | undefined;
|
|
76
|
+
set font(font: Font | undefined);
|
|
77
|
+
/**
|
|
78
|
+
* The cell's border — the line style and colour of each side — or `undefined` when the
|
|
79
|
+
* cell has none. An absent edge within a border means that side is unbordered, so reading
|
|
80
|
+
* a cell never fabricates a border it does not have.
|
|
81
|
+
*/
|
|
82
|
+
get border(): Border | undefined;
|
|
83
|
+
set border(border: Border | undefined);
|
|
84
|
+
/**
|
|
85
|
+
* The cell's alignment — how its content sits within the cell, plus the wrap/shrink flags —
|
|
86
|
+
* or `undefined` when it uses the defaults. The boolean flags are off unless explicitly set,
|
|
87
|
+
* so a cell that never enabled wrapping never reads back wrapped.
|
|
88
|
+
*/
|
|
89
|
+
get alignment(): Alignment | undefined;
|
|
90
|
+
set alignment(alignment: Alignment | undefined);
|
|
91
|
+
/**
|
|
92
|
+
* The cell's protection — its locked/hidden flags, enforced only once the sheet is protected —
|
|
93
|
+
* or `undefined` when the cell carries neither. `locked` defaults to on in OOXML, so a cell
|
|
94
|
+
* that never touched protection is implicitly locked and reads back as `undefined`, not as
|
|
95
|
+
* `{locked: true}`; the flag only becomes explicit when a cell is unlocked.
|
|
96
|
+
*/
|
|
97
|
+
get protection(): Protection | undefined;
|
|
98
|
+
set protection(protection: Protection | undefined);
|
|
99
|
+
/**
|
|
100
|
+
* The quote-prefix flag: when set, a spreadsheet stores the cell's content as literal text even
|
|
101
|
+
* when it looks like a formula or number, and shows a leading apostrophe in the formula bar without
|
|
102
|
+
* that apostrophe being part of the stored value. `undefined` (or `false`) when unset. It is a
|
|
103
|
+
* cell-format flag — an attribute on the cell's `xf` record — so it composes independently of the
|
|
104
|
+
* value.
|
|
105
|
+
*/
|
|
106
|
+
get quotePrefix(): boolean | undefined;
|
|
107
|
+
set quotePrefix(quotePrefix: boolean | undefined);
|
|
108
|
+
/**
|
|
109
|
+
* The index of the named cell style this cell links to (its OOXML `xfId`), or `undefined` when the
|
|
110
|
+
* cell references no named style beyond the default. The cell inherits any facet its own direct
|
|
111
|
+
* format leaves unset from that named style; the reader resolves the effective look onto the cell's
|
|
112
|
+
* own facets, and this link is preserved so a round-trip keeps the cell tied to its named style
|
|
113
|
+
* rather than flattening it away.
|
|
114
|
+
*
|
|
115
|
+
* Codec-only: an index is meaningful only against the workbook's `namedStyles` table as the reader
|
|
116
|
+
* built it, and a value naming no entry leaves the cell pointing at nothing. Authors set the facets
|
|
117
|
+
* they want directly.
|
|
118
|
+
*/
|
|
119
|
+
get [NAMED_STYLE_ID](): number | undefined;
|
|
120
|
+
set [NAMED_STYLE_ID](namedStyleId: number | undefined);
|
|
121
|
+
/**
|
|
122
|
+
* The cell's note (comment) as plain text, or `undefined` when it carries none. A note is
|
|
123
|
+
* metadata anchored to the cell, independent of its value: a cell can hold a note while empty,
|
|
124
|
+
* and clearing the value leaves the note intact. A structural edit that shifts the cell carries the
|
|
125
|
+
* note along to its new position.
|
|
126
|
+
*/
|
|
127
|
+
get note(): string | undefined;
|
|
128
|
+
set note(note: string | undefined);
|
|
129
|
+
}
|
|
130
|
+
export declare function applyCellStyle(cell: Cell, style: Readonly<CellStyle>): void;
|
|
131
|
+
export declare function copyCellContent(source: CellModel, target: Cell): void;
|
|
132
|
+
export declare function cellToModel(cell: Cell): CellModel;
|