@shbernal/ts-xlsx 1.0.3 → 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 (136) 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.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /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);
@@ -22,6 +22,16 @@ export declare class Cell {
22
22
  set value(value: CellValue | undefined);
23
23
  /** The observable {@link ValueType} of the current value. */
24
24
  get type(): ValueType;
25
+ /**
26
+ * The cell's value as plain text ({@link cellValueToText}), `""` when it is empty — so a reader
27
+ * that only wants strings never has to narrow the value union itself.
28
+ *
29
+ * Read-only, because text is a *rendering* of the value and not a second place to store one:
30
+ * writing `"3"` here could only mean the string `"3"`, which is exactly `value = '3'` and reads
31
+ * nothing like it. The number format is not applied either — the style is not the cell's value,
32
+ * so a currency cell's text carries no currency sign.
33
+ */
34
+ get text(): string;
25
35
  /**
26
36
  * Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
27
37
  * changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
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
- import { 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
- */
4
+ import { cellValueToText, coerceCellValue, detectValueType, } from './value.js';
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,42 +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
- * Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
59
- * changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
60
- * cell's typeface, size and colour throughout and bolds the first run.
61
- *
62
- * This exists because a run's format element does **not** inherit anything. A `<rPr>` is a
63
- * *complete* character format, and any facet it omits falls back to the workbook default font
64
- * ({@link Workbook.defaultFont}) — not to the cell's. Verified against Excel: a cell set to
65
- * Courier New 16 whose first run carries only `<b/>` renders that run in the workbook default face
66
- * at the default size, bold, while the rest of the cell renders Courier New 16. So a run authored
67
- * as `{bold: true}` beside a styled cell silently loses the face, which is the format's rule rather
68
- * than a bug — and the reason this is a helper rather than a change to how runs are written.
69
- *
70
- * Composition is per facet: a facet the run names wins, one it omits comes from the cell. Assigning
71
- * `value` directly stays the bare path, for a caller who wants a run that deliberately falls back
72
- * to the workbook default.
73
- *
74
- * A cell that names no font of its own needs no composition — an omitted facet already falls back
75
- * to the workbook default, which is exactly what such a cell renders in — so the runs pass through
76
- * unchanged.
77
- */
40
+ get text() {
41
+ return cellValueToText(this.#value);
42
+ }
78
43
  setRichText(runs) {
79
44
  const base = this.#font;
80
45
  this.#value =
@@ -82,15 +47,6 @@ export class Cell {
82
47
  ? { richText: [...runs] }
83
48
  : { richText: runs.map((run) => ({ ...run, font: { ...base, ...run.font } })) };
84
49
  }
85
- /**
86
- * The cell's full style — fill, number format, font, border, alignment, and protection — as one
87
- * {@link CellStyle}, for restyling a cell wholesale without importing {@link applyCellStyle}
88
- * separately (mirrors {@link Worksheet.model}'s getter/setter pair for the whole sheet). The
89
- * getter carries only the facets this cell has set (the same shape {@link cellToModel} emits);
90
- * the setter lays each facet `style` carries onto this cell — like every per-facet setter, it
91
- * replaces that facet outright but leaves a facet `style` omits untouched, so `cell.style = {...}`
92
- * composes with prior per-facet sets rather than clearing them wholesale.
93
- */
94
50
  get style() {
95
51
  const style = {};
96
52
  assignStyleFacets(style, this);
@@ -99,107 +55,54 @@ export class Cell {
99
55
  set style(style) {
100
56
  applyCellStyle(this, style);
101
57
  }
102
- /** The cell's background fill, or `undefined` when it has none. */
103
58
  get fill() {
104
59
  return this.#fill;
105
60
  }
106
61
  set fill(fill) {
107
62
  this.#fill = fill;
108
63
  }
109
- /**
110
- * The cell's number-format code (`"0.00%"`, a custom accounting format, …), or
111
- * `undefined` for the General format. Stored verbatim: the invariant form Excel
112
- * persists — `.` decimal, `,` grouping, `/` date separator — is neither localized
113
- * nor rewritten, so the code round-trips character-for-character. A cell that also carries
114
- * a column-level format keeps both, so overriding one facet never drops the other.
115
- */
116
64
  get numFmt() {
117
65
  return this.#numFmt;
118
66
  }
119
67
  set numFmt(numFmt) {
120
68
  this.#numFmt = numFmt;
121
69
  }
122
- /**
123
- * The cell's font — bold/italic/underline, size, colour, typeface — as a partial set
124
- * of the facets that differ from the default (only the facets actually set are carried,
125
- * exactly as OOXML stores them). `undefined` means the cell uses the workbook default font.
126
- */
127
70
  get font() {
128
71
  return this.#font;
129
72
  }
130
73
  set font(font) {
131
74
  this.#font = font;
132
75
  }
133
- /**
134
- * The cell's border — the line style and colour of each side — or `undefined` when the
135
- * cell has none. An absent edge within a border means that side is unbordered, so reading
136
- * a cell never fabricates a border it does not have.
137
- */
138
76
  get border() {
139
77
  return this.#border;
140
78
  }
141
79
  set border(border) {
142
80
  this.#border = border;
143
81
  }
144
- /**
145
- * The cell's alignment — how its content sits within the cell, plus the wrap/shrink flags —
146
- * or `undefined` when it uses the defaults. The boolean flags are off unless explicitly set,
147
- * so a cell that never enabled wrapping never reads back wrapped.
148
- */
149
82
  get alignment() {
150
83
  return this.#alignment;
151
84
  }
152
85
  set alignment(alignment) {
153
86
  this.#alignment = alignment;
154
87
  }
155
- /**
156
- * The cell's protection — its locked/hidden flags, enforced only once the sheet is protected —
157
- * or `undefined` when the cell carries neither. `locked` defaults to on in OOXML, so a cell
158
- * that never touched protection is implicitly locked and reads back as `undefined`, not as
159
- * `{locked: true}`; the flag only becomes explicit when a cell is unlocked.
160
- */
161
88
  get protection() {
162
89
  return this.#protection;
163
90
  }
164
91
  set protection(protection) {
165
92
  this.#protection = protection;
166
93
  }
167
- /**
168
- * The quote-prefix flag: when set, a spreadsheet stores the cell's content as literal text even
169
- * when it looks like a formula or number, and shows a leading apostrophe in the formula bar without
170
- * that apostrophe being part of the stored value. `undefined` (or `false`) when unset. It is a
171
- * cell-format flag — an attribute on the cell's `xf` record — so it composes independently of the
172
- * value.
173
- */
174
94
  get quotePrefix() {
175
95
  return this.#quotePrefix;
176
96
  }
177
97
  set quotePrefix(quotePrefix) {
178
98
  this.#quotePrefix = quotePrefix;
179
99
  }
180
- /**
181
- * The index of the named cell style this cell links to (its OOXML `xfId`), or `undefined` when the
182
- * cell references no named style beyond the default. The cell inherits any facet its own direct
183
- * format leaves unset from that named style; the reader resolves the effective look onto the cell's
184
- * own facets, and this link is preserved so a round-trip keeps the cell tied to its named style
185
- * rather than flattening it away.
186
- *
187
- * Codec-only: an index is meaningful only against the workbook's `namedStyles` table as the reader
188
- * built it, and a value naming no entry leaves the cell pointing at nothing. Authors set the facets
189
- * they want directly.
190
- */
191
100
  get [NAMED_STYLE_ID]() {
192
101
  return this.#namedStyleId;
193
102
  }
194
103
  set [NAMED_STYLE_ID](namedStyleId) {
195
104
  this.#namedStyleId = namedStyleId;
196
105
  }
197
- /**
198
- * The cell's note (comment) as plain text, or `undefined` when it carries none. A note is
199
- * metadata anchored to the cell, independent of its value: a cell can hold a note while empty,
200
- * and clearing the value leaves the note intact. A structural edit that shifts the cell carries the
201
- * note along to its new position.
202
- */
203
106
  get note() {
204
107
  return this.#note;
205
108
  }
@@ -207,36 +110,14 @@ export class Cell {
207
110
  this.#note = note;
208
111
  }
209
112
  }
210
- // Lay each present style facet of `style` onto `cell`, leaving facets it omits untouched. A {@link Cell}
211
- // exposes every facet as a setter of its declared type, so it *is* a mutable {@link CellStyle} target and
212
- // the shared {@link assignStyleFacets} loop drives it — the facet list lives only on {@link CellStyle}, and
213
- // no apply path can forget a facet without a compile error there. Facet objects are assigned by reference,
214
- // safe under the copy-on-write style model (setters replace, never mutate in place). This is the named
215
- // entry point for the many paths that style a cell — a table column's format, a resolved read xf, a model
216
- // assignment — so their call sites read as intent, not as a raw record copy.
217
113
  export function applyCellStyle(cell, style) {
218
114
  assignStyleFacets(cell, style);
219
115
  }
220
- // Copy a cell's value and every style facet onto a target cell. The source is a {@link CellModel},
221
- // which a live {@link Cell} structurally satisfies, so this one primitive serves both directions that
222
- // load content into a cell: a structural edit shifting a cell to fresh coordinates (`Cell` fixes its
223
- // `(row, col)` at construction, so the shifted cell is a new cell carrying the original's content) and
224
- // assigning a {@link WorksheetModel} onto a sheet. Position is never copied — the target keeps its own
225
- // `(row, col)`. The style facets go through {@link applyCellStyle} (targets are always fresh cells, so
226
- // its skip-if-absent is equivalent to a full copy here). Paired with {@link cellToModel} (the read
227
- // direction); a facet cellToModel emits but applyCellStyle omits (or the reverse) would silently drop
228
- // on a model round-trip — the historical merge-loss failure the CellStyle tuple now guards by type.
229
116
  export function copyCellContent(source, target) {
230
117
  target.value = source.value;
231
118
  applyCellStyle(target, source);
232
119
  target.note = source.note;
233
120
  }
234
- // Snapshot a cell's position and content as a {@link CellModel} — the read direction paired with
235
- // {@link copyCellContent}'s write. The style facets flow through the same {@link assignStyleFacets} loop
236
- // as every other copy (a {@link Cell} is structurally a {@link CellStyle} source), so this direction emits
237
- // exactly the facets the apply direction consumes: a `dst.model = src.model` round-trip carries every one,
238
- // and a facet added to {@link CellStyle} propagates here without a hand edit. Facets the cell does not
239
- // carry are left off the model rather than pinned to `undefined`; no consumer distinguishes the two.
240
121
  export function cellToModel(cell) {
241
122
  const model = {
242
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
  }
@@ -23,7 +23,15 @@ export declare class Column {
23
23
  */
24
24
  get key(): string | undefined;
25
25
  set key(key: string | undefined);
26
- /** Column width in character units; `undefined` leaves the sheet default in force. */
26
+ /**
27
+ * Column width in character units — digits of the workbook default font's maximum digit width,
28
+ * so what one unit measures moves with that font. `undefined` leaves the sheet default in force.
29
+ *
30
+ * Not bounded here, for the same reason {@link Row.height} is not, and for a stronger one:
31
+ * {@link MAX_COLUMN_WIDTH} bounds what Excel accepts being set, not what a file may hold. Excel
32
+ * reads a wider column back unchanged and re-saves it verbatim, so a bound here would refuse a
33
+ * width Excel itself preserves.
34
+ */
27
35
  get width(): number | undefined;
28
36
  set width(width: number | undefined);
29
37
  /** Whether the column is hidden. */
@@ -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,124 +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
- /** Column width in character units; `undefined` leaves the sheet default in force. */
48
25
  get width() {
49
26
  return this.#read('width');
50
27
  }
51
28
  set width(width) {
52
29
  this.#write('width', width);
53
30
  }
54
- /** Whether the column is hidden. */
55
31
  get hidden() {
56
32
  return this.#read('hidden');
57
33
  }
58
34
  set hidden(hidden) {
59
35
  this.#write('hidden', hidden);
60
36
  }
61
- /** Outline (grouping) depth; 0 or `undefined` means ungrouped. */
62
37
  get outlineLevel() {
63
38
  return this.#read('outlineLevel');
64
39
  }
65
40
  set outlineLevel(outlineLevel) {
66
41
  this.#write('outlineLevel', outlineLevel);
67
42
  }
68
- /** Whether this column is the collapsed summary of an outline group. */
69
43
  get collapsed() {
70
44
  return this.#read('collapsed');
71
45
  }
72
46
  set collapsed(collapsed) {
73
47
  this.#write('collapsed', collapsed);
74
48
  }
75
- /** Default fill for the column's cells that set none of their own. */
76
49
  get fill() {
77
50
  return this.#read('fill');
78
51
  }
79
52
  set fill(fill) {
80
53
  this.#write('fill', fill);
81
54
  }
82
- /** Default number format for the column's cells that set none of their own. */
83
55
  get numFmt() {
84
56
  return this.#read('numFmt');
85
57
  }
86
58
  set numFmt(numFmt) {
87
59
  this.#write('numFmt', numFmt);
88
60
  }
89
- /** Default font for the column's cells that set none of their own. */
90
61
  get font() {
91
62
  return this.#read('font');
92
63
  }
93
64
  set font(font) {
94
65
  this.#write('font', font);
95
66
  }
96
- /** Default border for the column's cells that set none of their own. */
97
67
  get border() {
98
68
  return this.#read('border');
99
69
  }
100
70
  set border(border) {
101
71
  this.#write('border', border);
102
72
  }
103
- /** Default alignment for the column's cells that set none of their own. */
104
73
  get alignment() {
105
74
  return this.#read('alignment');
106
75
  }
107
76
  set alignment(alignment) {
108
77
  this.#write('alignment', alignment);
109
78
  }
110
- /** Default protection flags for the column's cells that set none of their own. */
111
79
  get protection() {
112
80
  return this.#read('protection');
113
81
  }
114
82
  set protection(protection) {
115
83
  this.#write('protection', protection);
116
84
  }
117
- /**
118
- * The cell at a 1-based row number in this column, creating it on first access. Resolves through
119
- * merges exactly as {@link Worksheet.getCell} does.
120
- *
121
- * @throws {RangeError} if the row is not a positive integer.
122
- */
123
85
  getCell(row) {
124
86
  return this.#sheet.getCell(encodeAddress(this.index, row));
125
87
  }
126
- /**
127
- * The column's materialised cells in ascending row order. Sparse: a row that never wrote to this
128
- * column has no cell here.
129
- */
130
88
  get cells() {
131
89
  return this.#sheet[INTERNAL].columnCells(this.index);
132
90
  }
133
- /**
134
- * The column's values by position, index 0 being row 1. Sparse in the same way {@link cells} is:
135
- * a row with no cell in this column is a hole, which is what distinguishes "never written" from a
136
- * cell holding `null`.
137
- *
138
- * Assigning places each value it names and leaves every other row untouched, mirroring
139
- * {@link Row.values} — a hole or an explicit `undefined` skips that row, and a shorter array does
140
- * not clear the tail.
141
- */
142
91
  get values() {
143
92
  const values = [];
144
93
  for (const cell of this.cells)
@@ -154,7 +103,6 @@ export class Column {
154
103
  #read(key) {
155
104
  return this.#sheet[INTERNAL].columnPropertiesOf(this.index)?.[key];
156
105
  }
157
- // `undefined` clears rather than stores — see the note on `Row`'s counterpart.
158
106
  #write(key, value) {
159
107
  if (value === undefined) {
160
108
  const properties = this.#sheet[INTERNAL].columnPropertiesOf(this.index);