@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.
Files changed (127) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.js +0 -131
  5. package/dist/core/color-resolution.js +0 -60
  6. package/dist/core/column.js +0 -60
  7. package/dist/core/comment-thread.js +0 -45
  8. package/dist/core/conditional-formatting-overlay.js +0 -11
  9. package/dist/core/conditional-formatting.js +0 -10
  10. package/dist/core/containers.js +0 -15
  11. package/dist/core/data-validation-overlay.js +0 -25
  12. package/dist/core/data-validation.js +0 -7
  13. package/dist/core/date.js +0 -39
  14. package/dist/core/formula.js +0 -117
  15. package/dist/core/grid-edits.js +0 -38
  16. package/dist/core/image.js +0 -25
  17. package/dist/core/internal.js +0 -27
  18. package/dist/core/limits.js +0 -45
  19. package/dist/core/merge.d.ts +18 -0
  20. package/dist/core/merge.js +22 -7
  21. package/dist/core/modern-functions.js +0 -17
  22. package/dist/core/page-setup.js +0 -5
  23. package/dist/core/pivot-table.d.ts +1 -1
  24. package/dist/core/pivot-table.js +0 -48
  25. package/dist/core/preserved.js +0 -4
  26. package/dist/core/protection.js +0 -26
  27. package/dist/core/range.js +0 -112
  28. package/dist/core/row-input.d.ts +24 -0
  29. package/dist/core/row-input.js +29 -0
  30. package/dist/core/row.js +0 -66
  31. package/dist/core/style.js +0 -26
  32. package/dist/core/table-style.js +0 -28
  33. package/dist/core/table.js +0 -97
  34. package/dist/core/text-metrics.js +0 -40
  35. package/dist/core/theme.js +0 -85
  36. package/dist/core/value.js +0 -94
  37. package/dist/core/workbook-protection.js +0 -13
  38. package/dist/core/workbook.js +0 -380
  39. package/dist/core/worksheet-model.js +2 -23
  40. package/dist/core/worksheet.js +7 -506
  41. package/dist/customui/errors.js +0 -10
  42. package/dist/customui/index.js +0 -5
  43. package/dist/customui/ribbon.js +0 -43
  44. package/dist/entries/core.js +0 -6
  45. package/dist/entries/csv.js +0 -4
  46. package/dist/entries/customui.js +0 -2
  47. package/dist/entries/errors.js +0 -13
  48. package/dist/entries/vba.js +0 -5
  49. package/dist/entries/xlsb.js +0 -6
  50. package/dist/entries/xlsx.js +0 -13
  51. package/dist/errors.d.ts +14 -0
  52. package/dist/errors.js +5 -56
  53. package/dist/index.js +0 -12
  54. package/dist/io/csv/read.js +0 -18
  55. package/dist/io/csv/write.js +0 -18
  56. package/dist/io/opc/errors.js +0 -30
  57. package/dist/io/opc/inflate.js +0 -35
  58. package/dist/io/opc/namespaces.js +0 -15
  59. package/dist/io/opc/part-paths.js +0 -11
  60. package/dist/io/opc/read-opc.js +0 -31
  61. package/dist/io/opc/read-options.js +0 -6
  62. package/dist/io/opc/rels.js +0 -13
  63. package/dist/io/opc/sniff-format.js +0 -50
  64. package/dist/io/style/xf-style.js +0 -31
  65. package/dist/io/xlsb/errors.js +0 -10
  66. package/dist/io/xlsb/formula.js +1 -99
  67. package/dist/io/xlsb/primitives.js +1 -83
  68. package/dist/io/xlsb/ptg-functions.js +0 -34
  69. package/dist/io/xlsb/read-shared-strings.js +0 -7
  70. package/dist/io/xlsb/read-styles.js +3 -66
  71. package/dist/io/xlsb/read-worksheet.js +4 -87
  72. package/dist/io/xlsb/read.js +2 -60
  73. package/dist/io/xlsb/record-stream.js +0 -28
  74. package/dist/io/xlsb/record-types.js +0 -19
  75. package/dist/io/xlsx/cell-accumulator.js +0 -32
  76. package/dist/io/xlsx/cell-value.js +0 -29
  77. package/dist/io/xlsx/color-xml.d.ts +6 -0
  78. package/dist/io/xlsx/color-xml.js +42 -0
  79. package/dist/io/xlsx/comments.js +0 -84
  80. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  81. package/dist/io/xlsx/conditional-formatting.js +1 -97
  82. package/dist/io/xlsx/data-validation.js +0 -56
  83. package/dist/io/xlsx/edit-vba.js +0 -54
  84. package/dist/io/xlsx/errors.js +0 -12
  85. package/dist/io/xlsx/hyperlinks.js +0 -40
  86. package/dist/io/xlsx/images.js +0 -32
  87. package/dist/io/xlsx/namespaces.js +0 -42
  88. package/dist/io/xlsx/package-plan.js +0 -47
  89. package/dist/io/xlsx/pivot.js +0 -22
  90. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  91. package/dist/io/xlsx/read-rows.js +1 -83
  92. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  93. package/dist/io/xlsx/read-styles.d.ts +25 -1
  94. package/dist/io/xlsx/read-styles.js +61 -108
  95. package/dist/io/xlsx/read-worksheet.js +1 -88
  96. package/dist/io/xlsx/read.js +3 -211
  97. package/dist/io/xlsx/relationships.js +0 -11
  98. package/dist/io/xlsx/rich-runs.js +0 -16
  99. package/dist/io/xlsx/rich-text.js +0 -12
  100. package/dist/io/xlsx/shared-formulas.js +0 -7
  101. package/dist/io/xlsx/shared-strings.js +0 -21
  102. package/dist/io/xlsx/sheet-properties.js +1 -64
  103. package/dist/io/xlsx/styles.d.ts +1 -29
  104. package/dist/io/xlsx/styles.js +2 -366
  105. package/dist/io/xlsx/tables.js +5 -52
  106. package/dist/io/xlsx/threaded-comments.js +0 -103
  107. package/dist/io/xlsx/workbook-xml.js +0 -94
  108. package/dist/io/xlsx/worksheet-xml.js +0 -174
  109. package/dist/io/xlsx/write-stream.js +0 -137
  110. package/dist/io/xlsx/write.js +1 -134
  111. package/dist/io/xlsx/x14-ext.js +0 -8
  112. package/dist/vba/cfb-writer.js +12 -59
  113. package/dist/vba/cfb.js +2 -32
  114. package/dist/vba/codepage.js +1 -23
  115. package/dist/vba/errors.js +0 -15
  116. package/dist/vba/index.js +0 -10
  117. package/dist/vba/ms-ovba.js +3 -46
  118. package/dist/vba/project-editor.js +4 -89
  119. package/dist/vba/project.js +2 -34
  120. package/dist/vba/vba-encoding.js +1 -17
  121. package/dist/xml/errors.js +0 -10
  122. package/dist/xml/xml-read.js +3 -82
  123. package/dist/xml/xml.js +0 -39
  124. package/package.json +8 -9
  125. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  126. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  127. /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
@@ -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`);
@@ -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
  }
@@ -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);