@shbernal/ts-xlsx 1.1.0 → 1.2.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 +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.js +0 -131
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.js +0 -60
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.js +0 -45
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -48
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.js +0 -66
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.js +0 -40
- package/dist/core/theme.js +0 -85
- package/dist/core/value.js +0 -94
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.js +0 -380
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.js +7 -506
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.js +0 -6
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +0 -18
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +8 -9
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -223,6 +223,27 @@ shapes are deliberately different (synchronous `Uint8Array` I/O instead of async
|
|
|
223
223
|
`Buffer`/stream methods, `readXlsx`/`writeXlsx` free functions instead of
|
|
224
224
|
`workbook.xlsx.*`), because the goal was the *right* API, not the familiar one.
|
|
225
225
|
|
|
226
|
+
## Hit a bug? There is a skill for that
|
|
227
|
+
|
|
228
|
+
Most code that uses this library is written by an agent, and an agent that hits a library
|
|
229
|
+
defect will usually route around it silently — so the defect is never reported and never
|
|
230
|
+
fixed. `ts-xlsx-upstream` is a skill that turns that moment into a filed issue with a
|
|
231
|
+
minimal reproduction, which is what the maintainers convert into a permanent regression
|
|
232
|
+
test. It ships inside the package, so it is already on disk:
|
|
233
|
+
|
|
234
|
+
```shell
|
|
235
|
+
npx skills add ./node_modules/@shbernal/ts-xlsx # offline, matches your installed version
|
|
236
|
+
npx skills add shbernal/ts-xlsx # or straight from the repo
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
It covers triage (is this bug ours or your file's?), reducing a failure to a script that
|
|
240
|
+
builds its own input, and — because spreadsheets carry real data — never uploading a
|
|
241
|
+
workbook to a public tracker. Once the reproduction stands on its own, it files without
|
|
242
|
+
interrupting you, and tells you the issue number afterwards.
|
|
243
|
+
|
|
244
|
+
You do not need it to report something: <https://github.com/shbernal/ts-xlsx/issues> is
|
|
245
|
+
open, and errors the library knows are its own fault print that link themselves.
|
|
246
|
+
|
|
226
247
|
## Design principles
|
|
227
248
|
|
|
228
249
|
This project is optimized to be built and maintained largely by autonomous agents, with
|
package/dist/core/address.js
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
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
1
|
export const MAX_COLUMN = 16384;
|
|
15
|
-
/** Excel's row bound: 1 through 1048576. The other axis of {@link MAX_COLUMN}. */
|
|
16
2
|
export const MAX_ROW = 1048576;
|
|
17
3
|
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
4
|
const SHEET_PREFIX = /^(?:(?:'((?:[^']|'')*)')|([^'!]+))!(.*)$/;
|
|
21
|
-
/** Convert a 1-based column number to its letters (`1 → "A"`, `27 → "AA"`). */
|
|
22
5
|
export function numberToColumn(n) {
|
|
23
6
|
if (!Number.isInteger(n) || n < 1 || n > MAX_COLUMN) {
|
|
24
7
|
throw new RangeError(`column ${n} is out of bounds — Excel supports 1..${MAX_COLUMN}`);
|
|
@@ -32,7 +15,6 @@ export function numberToColumn(n) {
|
|
|
32
15
|
}
|
|
33
16
|
return letters;
|
|
34
17
|
}
|
|
35
|
-
/** Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`). */
|
|
36
18
|
export function columnToNumber(letters) {
|
|
37
19
|
if (letters.length === 0 || letters.length > 3) {
|
|
38
20
|
throw new RangeError(`invalid column letters: "${letters}"`);
|
|
@@ -50,9 +32,6 @@ export function columnToNumber(letters) {
|
|
|
50
32
|
}
|
|
51
33
|
return n;
|
|
52
34
|
}
|
|
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
35
|
function makeCellAddress(col, row) {
|
|
57
36
|
return {
|
|
58
37
|
address: `${col !== undefined ? numberToColumn(col) : ''}${row !== undefined ? row : ''}`,
|
|
@@ -60,12 +39,6 @@ function makeCellAddress(col, row) {
|
|
|
60
39
|
row,
|
|
61
40
|
};
|
|
62
41
|
}
|
|
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
42
|
export function decodeAddress(reference) {
|
|
70
43
|
const match = SINGLE_REF.exec(reference);
|
|
71
44
|
if (!match) {
|
|
@@ -80,11 +53,6 @@ export function decodeAddress(reference) {
|
|
|
80
53
|
const row = digits.length > 0 ? Number.parseInt(digits, 10) : undefined;
|
|
81
54
|
return { address: `${letters}${digits}`, col, row };
|
|
82
55
|
}
|
|
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
56
|
export function decodeRange(reference) {
|
|
89
57
|
const prefix = SHEET_PREFIX.exec(reference);
|
|
90
58
|
let sheetName;
|
|
@@ -116,7 +84,6 @@ export function decodeRange(reference) {
|
|
|
116
84
|
dimensions: `${tl.address}:${br.address}`,
|
|
117
85
|
};
|
|
118
86
|
}
|
|
119
|
-
/** Encode a 1-based `col`/`row` pair into its canonical A1 address (`"B2"`). */
|
|
120
87
|
export function encodeAddress(col, row) {
|
|
121
88
|
if (!Number.isInteger(row) || row < 1) {
|
|
122
89
|
throw new RangeError(`row ${row} is out of bounds — rows start at 1`);
|
package/dist/core/autofilter.js
CHANGED
|
@@ -8,18 +8,9 @@ const CUSTOM_FILTER_OPERATORS = new Set([
|
|
|
8
8
|
'greaterThan',
|
|
9
9
|
'greaterThanOrEqual',
|
|
10
10
|
]);
|
|
11
|
-
/** Narrow a raw `operator` attribute to a known {@link CustomFilterOperator}. */
|
|
12
11
|
export function isCustomFilterOperator(value) {
|
|
13
12
|
return CUSTOM_FILTER_OPERATORS.has(value);
|
|
14
13
|
}
|
|
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
14
|
export function canonicalizeAutoFilter(input) {
|
|
24
15
|
const ref = typeof input === 'string' ? input : input.ref;
|
|
25
16
|
const { top, left, bottom, right, dimensions } = decodeRange(ref);
|
package/dist/core/cell.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
// A single cell: a typed value at a fixed 1-based `(row, col)` position.
|
|
2
|
-
//
|
|
3
|
-
// Position is immutable and numeric — the corpus locks `col`/`row` as 1-based
|
|
4
|
-
// *numbers* (legacy shipped a type declaration calling them strings, which broke
|
|
5
|
-
// strict consumers). The value is the only mutable state here; assigning it routes
|
|
6
|
-
// through the value model so the cell's `type` is always consistent with what it holds.
|
|
7
1
|
import { encodeAddress } from './address.js';
|
|
8
2
|
import { NAMED_STYLE_ID } from './internal.js';
|
|
9
3
|
import { assignStyleFacets, } from './style.js';
|
|
10
4
|
import { cellValueToText, coerceCellValue, detectValueType, } from './value.js';
|
|
11
|
-
/**
|
|
12
|
-
* A single cell owns its value and every style facet outright. Each facet below — fill, number format,
|
|
13
|
-
* font, border, alignment, protection, quote-prefix, and note — is held in the cell's own field and
|
|
14
|
-
* *replaced* (never mutated in place) by its setter, so a facet set on one cell never aliases or bleeds
|
|
15
|
-
* onto its row, column, or sheet siblings. Each facet's own doc covers only what is specific to it.
|
|
16
|
-
*/
|
|
17
5
|
export class Cell {
|
|
18
|
-
/** 1-based row index. */
|
|
19
6
|
row;
|
|
20
|
-
/** 1-based column index. */
|
|
21
7
|
col;
|
|
22
8
|
#value = null;
|
|
23
9
|
#fill;
|
|
@@ -39,54 +25,21 @@ export class Cell {
|
|
|
39
25
|
this.row = row;
|
|
40
26
|
this.col = col;
|
|
41
27
|
}
|
|
42
|
-
/** Canonical A1 address of this cell (`"B3"`). */
|
|
43
28
|
get address() {
|
|
44
29
|
return encodeAddress(this.col, this.row);
|
|
45
30
|
}
|
|
46
|
-
/** The cell's value; `null` when empty. Assigning `undefined` clears it. */
|
|
47
31
|
get value() {
|
|
48
32
|
return this.#value;
|
|
49
33
|
}
|
|
50
34
|
set value(value) {
|
|
51
35
|
this.#value = coerceCellValue(value);
|
|
52
36
|
}
|
|
53
|
-
/** The observable {@link ValueType} of the current value. */
|
|
54
37
|
get type() {
|
|
55
38
|
return detectValueType(this.#value);
|
|
56
39
|
}
|
|
57
|
-
/**
|
|
58
|
-
* The cell's value as plain text ({@link cellValueToText}), `""` when it is empty — so a reader
|
|
59
|
-
* that only wants strings never has to narrow the value union itself.
|
|
60
|
-
*
|
|
61
|
-
* Read-only, because text is a *rendering* of the value and not a second place to store one:
|
|
62
|
-
* writing `"3"` here could only mean the string `"3"`, which is exactly `value = '3'` and reads
|
|
63
|
-
* nothing like it. The number format is not applied either — the style is not the cell's value,
|
|
64
|
-
* so a currency cell's text carries no currency sign.
|
|
65
|
-
*/
|
|
66
40
|
get text() {
|
|
67
41
|
return cellValueToText(this.#value);
|
|
68
42
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
|
|
71
|
-
* changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
|
|
72
|
-
* cell's typeface, size and colour throughout and bolds the first run.
|
|
73
|
-
*
|
|
74
|
-
* This exists because a run's format element does **not** inherit anything. A `<rPr>` is a
|
|
75
|
-
* *complete* character format, and any facet it omits falls back to the workbook default font
|
|
76
|
-
* ({@link Workbook.defaultFont}) — not to the cell's. Verified against Excel: a cell set to
|
|
77
|
-
* Courier New 16 whose first run carries only `<b/>` renders that run in the workbook default face
|
|
78
|
-
* at the default size, bold, while the rest of the cell renders Courier New 16. So a run authored
|
|
79
|
-
* as `{bold: true}` beside a styled cell silently loses the face, which is the format's rule rather
|
|
80
|
-
* than a bug — and the reason this is a helper rather than a change to how runs are written.
|
|
81
|
-
*
|
|
82
|
-
* Composition is per facet: a facet the run names wins, one it omits comes from the cell. Assigning
|
|
83
|
-
* `value` directly stays the bare path, for a caller who wants a run that deliberately falls back
|
|
84
|
-
* to the workbook default.
|
|
85
|
-
*
|
|
86
|
-
* A cell that names no font of its own needs no composition — an omitted facet already falls back
|
|
87
|
-
* to the workbook default, which is exactly what such a cell renders in — so the runs pass through
|
|
88
|
-
* unchanged.
|
|
89
|
-
*/
|
|
90
43
|
setRichText(runs) {
|
|
91
44
|
const base = this.#font;
|
|
92
45
|
this.#value =
|
|
@@ -94,15 +47,6 @@ export class Cell {
|
|
|
94
47
|
? { richText: [...runs] }
|
|
95
48
|
: { richText: runs.map((run) => ({ ...run, font: { ...base, ...run.font } })) };
|
|
96
49
|
}
|
|
97
|
-
/**
|
|
98
|
-
* The cell's full style — fill, number format, font, border, alignment, and protection — as one
|
|
99
|
-
* {@link CellStyle}, for restyling a cell wholesale without importing {@link applyCellStyle}
|
|
100
|
-
* separately (mirrors {@link Worksheet.model}'s getter/setter pair for the whole sheet). The
|
|
101
|
-
* getter carries only the facets this cell has set (the same shape {@link cellToModel} emits);
|
|
102
|
-
* the setter lays each facet `style` carries onto this cell — like every per-facet setter, it
|
|
103
|
-
* replaces that facet outright but leaves a facet `style` omits untouched, so `cell.style = {...}`
|
|
104
|
-
* composes with prior per-facet sets rather than clearing them wholesale.
|
|
105
|
-
*/
|
|
106
50
|
get style() {
|
|
107
51
|
const style = {};
|
|
108
52
|
assignStyleFacets(style, this);
|
|
@@ -111,107 +55,54 @@ export class Cell {
|
|
|
111
55
|
set style(style) {
|
|
112
56
|
applyCellStyle(this, style);
|
|
113
57
|
}
|
|
114
|
-
/** The cell's background fill, or `undefined` when it has none. */
|
|
115
58
|
get fill() {
|
|
116
59
|
return this.#fill;
|
|
117
60
|
}
|
|
118
61
|
set fill(fill) {
|
|
119
62
|
this.#fill = fill;
|
|
120
63
|
}
|
|
121
|
-
/**
|
|
122
|
-
* The cell's number-format code (`"0.00%"`, a custom accounting format, …), or
|
|
123
|
-
* `undefined` for the General format. Stored verbatim: the invariant form Excel
|
|
124
|
-
* persists — `.` decimal, `,` grouping, `/` date separator — is neither localized
|
|
125
|
-
* nor rewritten, so the code round-trips character-for-character. A cell that also carries
|
|
126
|
-
* a column-level format keeps both, so overriding one facet never drops the other.
|
|
127
|
-
*/
|
|
128
64
|
get numFmt() {
|
|
129
65
|
return this.#numFmt;
|
|
130
66
|
}
|
|
131
67
|
set numFmt(numFmt) {
|
|
132
68
|
this.#numFmt = numFmt;
|
|
133
69
|
}
|
|
134
|
-
/**
|
|
135
|
-
* The cell's font — bold/italic/underline, size, colour, typeface — as a partial set
|
|
136
|
-
* of the facets that differ from the default (only the facets actually set are carried,
|
|
137
|
-
* exactly as OOXML stores them). `undefined` means the cell uses the workbook default font.
|
|
138
|
-
*/
|
|
139
70
|
get font() {
|
|
140
71
|
return this.#font;
|
|
141
72
|
}
|
|
142
73
|
set font(font) {
|
|
143
74
|
this.#font = font;
|
|
144
75
|
}
|
|
145
|
-
/**
|
|
146
|
-
* The cell's border — the line style and colour of each side — or `undefined` when the
|
|
147
|
-
* cell has none. An absent edge within a border means that side is unbordered, so reading
|
|
148
|
-
* a cell never fabricates a border it does not have.
|
|
149
|
-
*/
|
|
150
76
|
get border() {
|
|
151
77
|
return this.#border;
|
|
152
78
|
}
|
|
153
79
|
set border(border) {
|
|
154
80
|
this.#border = border;
|
|
155
81
|
}
|
|
156
|
-
/**
|
|
157
|
-
* The cell's alignment — how its content sits within the cell, plus the wrap/shrink flags —
|
|
158
|
-
* or `undefined` when it uses the defaults. The boolean flags are off unless explicitly set,
|
|
159
|
-
* so a cell that never enabled wrapping never reads back wrapped.
|
|
160
|
-
*/
|
|
161
82
|
get alignment() {
|
|
162
83
|
return this.#alignment;
|
|
163
84
|
}
|
|
164
85
|
set alignment(alignment) {
|
|
165
86
|
this.#alignment = alignment;
|
|
166
87
|
}
|
|
167
|
-
/**
|
|
168
|
-
* The cell's protection — its locked/hidden flags, enforced only once the sheet is protected —
|
|
169
|
-
* or `undefined` when the cell carries neither. `locked` defaults to on in OOXML, so a cell
|
|
170
|
-
* that never touched protection is implicitly locked and reads back as `undefined`, not as
|
|
171
|
-
* `{locked: true}`; the flag only becomes explicit when a cell is unlocked.
|
|
172
|
-
*/
|
|
173
88
|
get protection() {
|
|
174
89
|
return this.#protection;
|
|
175
90
|
}
|
|
176
91
|
set protection(protection) {
|
|
177
92
|
this.#protection = protection;
|
|
178
93
|
}
|
|
179
|
-
/**
|
|
180
|
-
* The quote-prefix flag: when set, a spreadsheet stores the cell's content as literal text even
|
|
181
|
-
* when it looks like a formula or number, and shows a leading apostrophe in the formula bar without
|
|
182
|
-
* that apostrophe being part of the stored value. `undefined` (or `false`) when unset. It is a
|
|
183
|
-
* cell-format flag — an attribute on the cell's `xf` record — so it composes independently of the
|
|
184
|
-
* value.
|
|
185
|
-
*/
|
|
186
94
|
get quotePrefix() {
|
|
187
95
|
return this.#quotePrefix;
|
|
188
96
|
}
|
|
189
97
|
set quotePrefix(quotePrefix) {
|
|
190
98
|
this.#quotePrefix = quotePrefix;
|
|
191
99
|
}
|
|
192
|
-
/**
|
|
193
|
-
* The index of the named cell style this cell links to (its OOXML `xfId`), or `undefined` when the
|
|
194
|
-
* cell references no named style beyond the default. The cell inherits any facet its own direct
|
|
195
|
-
* format leaves unset from that named style; the reader resolves the effective look onto the cell's
|
|
196
|
-
* own facets, and this link is preserved so a round-trip keeps the cell tied to its named style
|
|
197
|
-
* rather than flattening it away.
|
|
198
|
-
*
|
|
199
|
-
* Codec-only: an index is meaningful only against the workbook's `namedStyles` table as the reader
|
|
200
|
-
* built it, and a value naming no entry leaves the cell pointing at nothing. Authors set the facets
|
|
201
|
-
* they want directly.
|
|
202
|
-
*/
|
|
203
100
|
get [NAMED_STYLE_ID]() {
|
|
204
101
|
return this.#namedStyleId;
|
|
205
102
|
}
|
|
206
103
|
set [NAMED_STYLE_ID](namedStyleId) {
|
|
207
104
|
this.#namedStyleId = namedStyleId;
|
|
208
105
|
}
|
|
209
|
-
/**
|
|
210
|
-
* The cell's note (comment) as plain text, or `undefined` when it carries none. A note is
|
|
211
|
-
* metadata anchored to the cell, independent of its value: a cell can hold a note while empty,
|
|
212
|
-
* and clearing the value leaves the note intact. A structural edit that shifts the cell carries the
|
|
213
|
-
* note along to its new position.
|
|
214
|
-
*/
|
|
215
106
|
get note() {
|
|
216
107
|
return this.#note;
|
|
217
108
|
}
|
|
@@ -219,36 +110,14 @@ export class Cell {
|
|
|
219
110
|
this.#note = note;
|
|
220
111
|
}
|
|
221
112
|
}
|
|
222
|
-
// Lay each present style facet of `style` onto `cell`, leaving facets it omits untouched. A {@link Cell}
|
|
223
|
-
// exposes every facet as a setter of its declared type, so it *is* a mutable {@link CellStyle} target and
|
|
224
|
-
// the shared {@link assignStyleFacets} loop drives it — the facet list lives only on {@link CellStyle}, and
|
|
225
|
-
// no apply path can forget a facet without a compile error there. Facet objects are assigned by reference,
|
|
226
|
-
// safe under the copy-on-write style model (setters replace, never mutate in place). This is the named
|
|
227
|
-
// entry point for the many paths that style a cell — a table column's format, a resolved read xf, a model
|
|
228
|
-
// assignment — so their call sites read as intent, not as a raw record copy.
|
|
229
113
|
export function applyCellStyle(cell, style) {
|
|
230
114
|
assignStyleFacets(cell, style);
|
|
231
115
|
}
|
|
232
|
-
// Copy a cell's value and every style facet onto a target cell. The source is a {@link CellModel},
|
|
233
|
-
// which a live {@link Cell} structurally satisfies, so this one primitive serves both directions that
|
|
234
|
-
// load content into a cell: a structural edit shifting a cell to fresh coordinates (`Cell` fixes its
|
|
235
|
-
// `(row, col)` at construction, so the shifted cell is a new cell carrying the original's content) and
|
|
236
|
-
// assigning a {@link WorksheetModel} onto a sheet. Position is never copied — the target keeps its own
|
|
237
|
-
// `(row, col)`. The style facets go through {@link applyCellStyle} (targets are always fresh cells, so
|
|
238
|
-
// its skip-if-absent is equivalent to a full copy here). Paired with {@link cellToModel} (the read
|
|
239
|
-
// direction); a facet cellToModel emits but applyCellStyle omits (or the reverse) would silently drop
|
|
240
|
-
// on a model round-trip — the historical merge-loss failure the CellStyle tuple now guards by type.
|
|
241
116
|
export function copyCellContent(source, target) {
|
|
242
117
|
target.value = source.value;
|
|
243
118
|
applyCellStyle(target, source);
|
|
244
119
|
target.note = source.note;
|
|
245
120
|
}
|
|
246
|
-
// Snapshot a cell's position and content as a {@link CellModel} — the read direction paired with
|
|
247
|
-
// {@link copyCellContent}'s write. The style facets flow through the same {@link assignStyleFacets} loop
|
|
248
|
-
// as every other copy (a {@link Cell} is structurally a {@link CellStyle} source), so this direction emits
|
|
249
|
-
// exactly the facets the apply direction consumes: a `dst.model = src.model` round-trip carries every one,
|
|
250
|
-
// and a facet added to {@link CellStyle} propagates here without a hand edit. Facets the cell does not
|
|
251
|
-
// carry are left off the model rather than pinned to `undefined`; no consumer distinguishes the two.
|
|
252
121
|
export function cellToModel(cell) {
|
|
253
122
|
const model = {
|
|
254
123
|
row: cell.row,
|
|
@@ -1,28 +1,4 @@
|
|
|
1
|
-
// Turning an OOXML colour *reference* into a concrete colour.
|
|
2
|
-
//
|
|
3
|
-
// A `<color>` in a spreadsheet states its value in one of three ways, and two of them are references
|
|
4
|
-
// that mean nothing on their own: `indexed="n"` points into a 64-entry legacy palette carried over
|
|
5
|
-
// from the BIFF era, and `theme="n"` points into the workbook theme's colour scheme. Either may
|
|
6
|
-
// additionally carry a `tint`, which lightens or darkens whatever it resolved to.
|
|
7
|
-
//
|
|
8
|
-
// Resolution is deliberately a *derived* view. Nothing here writes back into the model: a `Color`
|
|
9
|
-
// keeps the encoding its file used, so a round-trip re-emits `theme="4" tint="0.4"` rather than
|
|
10
|
-
// rewriting every cell as a literal ARGB — which would bloat the styles table, break the link to the
|
|
11
|
-
// theme (recolouring the workbook would stop working), and change what the file means.
|
|
12
1
|
import { DEFAULT_THEME_COLOR_SCHEME, THEME_COLOR_SLOTS } from './theme.js';
|
|
13
|
-
/**
|
|
14
|
-
* The built-in indexed colour palette (ECMA-376 §18.8.27), by index. Entries 0–7 duplicate 8–15 —
|
|
15
|
-
* redundancy the spec preserves for backwards compatibility with the legacy formats this palette came
|
|
16
|
-
* from — and the table is only 64 long: indices 64 and 65 are the *system* foreground and background,
|
|
17
|
-
* which name whatever the operating system's window colours are and therefore have no fixed value at
|
|
18
|
-
* all (see {@link SYSTEM_INDEXED_COLORS}).
|
|
19
|
-
*
|
|
20
|
-
* The spec writes each entry with a leading `00`. That byte is not an alpha channel — a palette of
|
|
21
|
-
* fully transparent colours would be absurd — it is an artefact of the 32-bit colour records these
|
|
22
|
-
* values were lifted from, which is why {@link resolveColor} returns them fully opaque.
|
|
23
|
-
*/
|
|
24
|
-
// biome-ignore format: laid out eight per row, matching how the spec tabulates the palette —
|
|
25
|
-
// the 0-7 / 8-15 duplication and the 16-entry banding are legible here and invisible one-per-line.
|
|
26
2
|
export const DEFAULT_INDEXED_COLORS = [
|
|
27
3
|
'00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', '0000FFFF',
|
|
28
4
|
'00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', '0000FFFF',
|
|
@@ -33,26 +9,7 @@ export const DEFAULT_INDEXED_COLORS = [
|
|
|
33
9
|
'003366FF', '0033CCCC', '0099CC00', '00FFCC00', '00FF9900', '00FF6600', '00666699', '00969696',
|
|
34
10
|
'00003366', '00339966', '00003300', '00333300', '00993300', '00993366', '00333399', '00333333',
|
|
35
11
|
];
|
|
36
|
-
/**
|
|
37
|
-
* The two indices that are not colours: 64 is the system foreground and 65 the system background.
|
|
38
|
-
* They resolve to whatever the viewing system's window colours are, so this library reports them
|
|
39
|
-
* unresolved rather than inventing black and white — a caller that wants to paint them must decide
|
|
40
|
-
* for itself what "automatic" means in its context. `indexed="64"` in particular is extremely common:
|
|
41
|
-
* it is the placeholder every solid fill Excel writes carries as its background colour.
|
|
42
|
-
*/
|
|
43
12
|
export const SYSTEM_INDEXED_COLORS = new Set([64, 65]);
|
|
44
|
-
/**
|
|
45
|
-
* Resolve a colour reference to a concrete 8-hex ARGB string, or `undefined` when it cannot be
|
|
46
|
-
* resolved — an `auto` colour, a system indexed colour, a theme slot the workbook's scheme does not
|
|
47
|
-
* declare, or an index past the end of a custom palette.
|
|
48
|
-
*
|
|
49
|
-
* Precedence follows what the encodings mean: an explicit `argb` is already concrete and wins; then
|
|
50
|
-
* `theme`, then `indexed`. A `tint` applies to whatever the base resolved to.
|
|
51
|
-
*
|
|
52
|
-
* Alpha: a resolved `theme`/`indexed` colour comes back fully opaque, because neither the theme scheme
|
|
53
|
-
* nor the palette carries a meaningful alpha (see {@link DEFAULT_INDEXED_COLORS}). An explicit `argb`
|
|
54
|
-
* keeps the alpha the file stated.
|
|
55
|
-
*/
|
|
56
13
|
export function resolveColor(color, context = {}) {
|
|
57
14
|
const base = resolveBase(color, context);
|
|
58
15
|
if (base === undefined)
|
|
@@ -86,25 +43,11 @@ function resolveBase(color, context) {
|
|
|
86
43
|
}
|
|
87
44
|
return undefined;
|
|
88
45
|
}
|
|
89
|
-
// Accept the shapes a colour value legitimately arrives in — 6-hex RGB, 8-hex ARGB, either with a
|
|
90
|
-
// leading '#' — and reject anything else rather than returning a half-parsed value. This is a *read*
|
|
91
|
-
// path over foreign data, so a malformed entry resolves to nothing; the writer's own normaliser
|
|
92
|
-
// throws, because there the malformed value is a caller's bug.
|
|
93
46
|
function normalizeArgb(value) {
|
|
94
47
|
const hex = value.startsWith('#') ? value.slice(1) : value;
|
|
95
48
|
const argb = hex.length === 6 ? `FF${hex}` : hex;
|
|
96
49
|
return /^[0-9a-fA-F]{8}$/.test(argb) ? argb.toUpperCase() : undefined;
|
|
97
50
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Apply a `tint` to a concrete ARGB colour: convert to HLS, shift the luminance, convert back.
|
|
100
|
-
*
|
|
101
|
-
* The shift is ECMA-376's (§18.3.1.15): `-1` darkens to black, `+1` lightens to white, `0` is a
|
|
102
|
-
* no-op. The spec states the formula but not the rounding either side of the RGB↔HLS conversion, so
|
|
103
|
-
* an implementation cannot be checked against the prose alone. This one lands within 2/255 per
|
|
104
|
-
* channel of what Excel Desktop itself renders, measured over three hues × twelve tints and recorded
|
|
105
|
-
* in `test/corpus/fixtures/excel-oracle/theme-color-tint-luminance.json`. Closing that last gap would
|
|
106
|
-
* mean reproducing one Excel build's fixed-point HLS rounding, and the difference is not visible.
|
|
107
|
-
*/
|
|
108
51
|
export function applyTint(argb, tint) {
|
|
109
52
|
const alpha = argb.slice(0, 2);
|
|
110
53
|
const r = Number.parseInt(argb.slice(2, 4), 16) / 255;
|
|
@@ -126,9 +69,6 @@ export function applyTint(argb, tint) {
|
|
|
126
69
|
hue = (r - g) / delta + 4;
|
|
127
70
|
hue /= 6;
|
|
128
71
|
}
|
|
129
|
-
// Lum' = Lum * (1 + tint) when darkening; Lum' = Lum * (1 - tint) + tint when lightening. The
|
|
130
|
-
// spec writes the second as `Lum * (1 - tint) + (HLSMAX - HLSMAX * (1 - tint))`, which is the same
|
|
131
|
-
// thing once the HLSMAX scale is divided out.
|
|
132
72
|
const shifted = tint < 0 ? lightness * (1 + tint) : lightness * (1 - tint) + tint;
|
|
133
73
|
return alpha + hlsToRgbHex(hue, saturation, clamp01(shifted));
|
|
134
74
|
}
|
package/dist/core/column.js
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
// A handle on one column of a worksheet: its formatting and its cells, reached by column index.
|
|
2
|
-
//
|
|
3
|
-
// The other axis of `Row`, and the same contract — a live view over the worksheet's stores, position
|
|
4
|
-
// fixed at construction, formatting created on write rather than on read. See `core/row.ts` for why
|
|
5
|
-
// it is a handle rather than a record.
|
|
6
|
-
//
|
|
7
|
-
// A column carries more than a row does: besides the geometry (width, visibility, outline) it holds
|
|
8
|
-
// the six `CellStyle` facets as *defaults* for its cells, which is why the mirror below is twice the
|
|
9
|
-
// length of the row's.
|
|
10
1
|
import { encodeAddress, numberToColumn } from './address.js';
|
|
11
2
|
import { INTERNAL } from './internal.js';
|
|
12
3
|
export class Column {
|
|
13
4
|
#sheet;
|
|
14
|
-
/** 1-based column index. Fixed for this handle's lifetime. */
|
|
15
5
|
index;
|
|
16
|
-
/** @throws {RangeError} if the index is not a positive integer. */
|
|
17
6
|
constructor(sheet, index) {
|
|
18
7
|
if (!Number.isInteger(index) || index < 1) {
|
|
19
8
|
throw new RangeError(`column ${index} is out of bounds — columns start at 1`);
|
|
@@ -21,132 +10,84 @@ export class Column {
|
|
|
21
10
|
this.#sheet = sheet;
|
|
22
11
|
this.index = index;
|
|
23
12
|
}
|
|
24
|
-
/** The column's letters (`"B"`) — the spreadsheet-facing name for {@link index}. */
|
|
25
13
|
get letter() {
|
|
26
14
|
return numberToColumn(this.index);
|
|
27
15
|
}
|
|
28
|
-
/**
|
|
29
|
-
* The column's format record if it has one, else `undefined` — a read that never fabricates, so a
|
|
30
|
-
* serializer can ask every column it visits whether there are attributes to emit without giving
|
|
31
|
-
* each one an empty record. Read-only on purpose: {@link width} and its siblings are how a column
|
|
32
|
-
* is formatted, and they create the record on first write.
|
|
33
|
-
*/
|
|
34
16
|
get properties() {
|
|
35
17
|
return this.#sheet[INTERNAL].columnPropertiesOf(this.index);
|
|
36
18
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Stable key naming this column so a keyed-object row (see {@link Worksheet.addRow}) can place a
|
|
39
|
-
* value under it by name rather than position. In-memory only — never serialized to OOXML.
|
|
40
|
-
*/
|
|
41
19
|
get key() {
|
|
42
20
|
return this.#read('key');
|
|
43
21
|
}
|
|
44
22
|
set key(key) {
|
|
45
23
|
this.#write('key', key);
|
|
46
24
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Column width in character units — digits of the workbook default font's maximum digit width,
|
|
49
|
-
* so what one unit measures moves with that font. `undefined` leaves the sheet default in force.
|
|
50
|
-
*
|
|
51
|
-
* Not bounded here, for the same reason {@link Row.height} is not, and for a stronger one:
|
|
52
|
-
* {@link MAX_COLUMN_WIDTH} bounds what Excel accepts being set, not what a file may hold. Excel
|
|
53
|
-
* reads a wider column back unchanged and re-saves it verbatim, so a bound here would refuse a
|
|
54
|
-
* width Excel itself preserves.
|
|
55
|
-
*/
|
|
56
25
|
get width() {
|
|
57
26
|
return this.#read('width');
|
|
58
27
|
}
|
|
59
28
|
set width(width) {
|
|
60
29
|
this.#write('width', width);
|
|
61
30
|
}
|
|
62
|
-
/** Whether the column is hidden. */
|
|
63
31
|
get hidden() {
|
|
64
32
|
return this.#read('hidden');
|
|
65
33
|
}
|
|
66
34
|
set hidden(hidden) {
|
|
67
35
|
this.#write('hidden', hidden);
|
|
68
36
|
}
|
|
69
|
-
/** Outline (grouping) depth; 0 or `undefined` means ungrouped. */
|
|
70
37
|
get outlineLevel() {
|
|
71
38
|
return this.#read('outlineLevel');
|
|
72
39
|
}
|
|
73
40
|
set outlineLevel(outlineLevel) {
|
|
74
41
|
this.#write('outlineLevel', outlineLevel);
|
|
75
42
|
}
|
|
76
|
-
/** Whether this column is the collapsed summary of an outline group. */
|
|
77
43
|
get collapsed() {
|
|
78
44
|
return this.#read('collapsed');
|
|
79
45
|
}
|
|
80
46
|
set collapsed(collapsed) {
|
|
81
47
|
this.#write('collapsed', collapsed);
|
|
82
48
|
}
|
|
83
|
-
/** Default fill for the column's cells that set none of their own. */
|
|
84
49
|
get fill() {
|
|
85
50
|
return this.#read('fill');
|
|
86
51
|
}
|
|
87
52
|
set fill(fill) {
|
|
88
53
|
this.#write('fill', fill);
|
|
89
54
|
}
|
|
90
|
-
/** Default number format for the column's cells that set none of their own. */
|
|
91
55
|
get numFmt() {
|
|
92
56
|
return this.#read('numFmt');
|
|
93
57
|
}
|
|
94
58
|
set numFmt(numFmt) {
|
|
95
59
|
this.#write('numFmt', numFmt);
|
|
96
60
|
}
|
|
97
|
-
/** Default font for the column's cells that set none of their own. */
|
|
98
61
|
get font() {
|
|
99
62
|
return this.#read('font');
|
|
100
63
|
}
|
|
101
64
|
set font(font) {
|
|
102
65
|
this.#write('font', font);
|
|
103
66
|
}
|
|
104
|
-
/** Default border for the column's cells that set none of their own. */
|
|
105
67
|
get border() {
|
|
106
68
|
return this.#read('border');
|
|
107
69
|
}
|
|
108
70
|
set border(border) {
|
|
109
71
|
this.#write('border', border);
|
|
110
72
|
}
|
|
111
|
-
/** Default alignment for the column's cells that set none of their own. */
|
|
112
73
|
get alignment() {
|
|
113
74
|
return this.#read('alignment');
|
|
114
75
|
}
|
|
115
76
|
set alignment(alignment) {
|
|
116
77
|
this.#write('alignment', alignment);
|
|
117
78
|
}
|
|
118
|
-
/** Default protection flags for the column's cells that set none of their own. */
|
|
119
79
|
get protection() {
|
|
120
80
|
return this.#read('protection');
|
|
121
81
|
}
|
|
122
82
|
set protection(protection) {
|
|
123
83
|
this.#write('protection', protection);
|
|
124
84
|
}
|
|
125
|
-
/**
|
|
126
|
-
* The cell at a 1-based row number in this column, creating it on first access. Resolves through
|
|
127
|
-
* merges exactly as {@link Worksheet.getCell} does.
|
|
128
|
-
*
|
|
129
|
-
* @throws {RangeError} if the row is not a positive integer.
|
|
130
|
-
*/
|
|
131
85
|
getCell(row) {
|
|
132
86
|
return this.#sheet.getCell(encodeAddress(this.index, row));
|
|
133
87
|
}
|
|
134
|
-
/**
|
|
135
|
-
* The column's materialised cells in ascending row order. Sparse: a row that never wrote to this
|
|
136
|
-
* column has no cell here.
|
|
137
|
-
*/
|
|
138
88
|
get cells() {
|
|
139
89
|
return this.#sheet[INTERNAL].columnCells(this.index);
|
|
140
90
|
}
|
|
141
|
-
/**
|
|
142
|
-
* The column's values by position, index 0 being row 1. Sparse in the same way {@link cells} is:
|
|
143
|
-
* a row with no cell in this column is a hole, which is what distinguishes "never written" from a
|
|
144
|
-
* cell holding `null`.
|
|
145
|
-
*
|
|
146
|
-
* Assigning places each value it names and leaves every other row untouched, mirroring
|
|
147
|
-
* {@link Row.values} — a hole or an explicit `undefined` skips that row, and a shorter array does
|
|
148
|
-
* not clear the tail.
|
|
149
|
-
*/
|
|
150
91
|
get values() {
|
|
151
92
|
const values = [];
|
|
152
93
|
for (const cell of this.cells)
|
|
@@ -162,7 +103,6 @@ export class Column {
|
|
|
162
103
|
#read(key) {
|
|
163
104
|
return this.#sheet[INTERNAL].columnPropertiesOf(this.index)?.[key];
|
|
164
105
|
}
|
|
165
|
-
// `undefined` clears rather than stores — see the note on `Row`'s counterpart.
|
|
166
106
|
#write(key, value) {
|
|
167
107
|
if (value === undefined) {
|
|
168
108
|
const properties = this.#sheet[INTERNAL].columnPropertiesOf(this.index);
|