@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
@@ -1,10 +1,3 @@
1
- // A worksheet: a sparsely-populated grid of cells addressed by A1 reference.
2
- //
3
- // Storage is sparse by construction — a spreadsheet is mostly empty, so cells
4
- // materialise on first access and only occupied positions cost memory. Column and
5
- // row metadata (widths, heights, visibility, outline grouping) are stored apart from
6
- // the cell grid, because a column or row can carry formatting while holding no cells.
7
- // Merges and views layer on in later slices.
8
1
  import { AuthoringError } from '../errors.js';
9
2
  import { decodeAddress, decodeRange, encodeAddress } from './address.js';
10
3
  import { canonicalizeAutoFilter } from './autofilter.js';
@@ -17,116 +10,44 @@ import { DataValidationOverlay } from './data-validation-overlay.js';
17
10
  import { GridEdits } from './grid-edits.js';
18
11
  import { PX_TO_EMU, resolveAnchorPoint, } from './image.js';
19
12
  import { INTERNAL } from './internal.js';
20
- import { rectsOverlap } from './merge.js';
13
+ import { clearCoveredValues, masterOf, rectsOverlap } from './merge.js';
21
14
  import { PivotTable } from './pivot-table.js';
22
15
  import { deriveCredential, } from './protection.js';
23
16
  import { Range, rangeFrom } from './range.js';
24
17
  import { Row } from './row.js';
18
+ import { buildRowCells, rowPlacements } from './row-input.js';
25
19
  import { Table, TOTALS_ROW_SUBTOTAL_CODE } from './table.js';
26
20
  import { WORKSHEET_MODEL_FACETS } from './worksheet-model.js';
27
21
  export class Worksheet {
28
22
  name;
29
- /** 1-based workbook-assigned id, stable for the sheet's lifetime. */
30
23
  id;
31
24
  state;
32
- /**
33
- * Colour of the sheet's tab, as an ARGB/theme {@link Color}. `undefined` leaves the tab its
34
- * default colour; the writer emits no `<tabColor>` for an uncoloured sheet, so a round-trip
35
- * never fabricates one.
36
- */
37
25
  tabColor;
38
- /** Sheet-level format defaults. Mutate in place: `sheet.properties.defaultRowHeight = 20`. */
39
26
  properties = {};
40
- /**
41
- * Outline summary-position flags. Mutate in place: `sheet.outline.summaryBelow = false`. Empty
42
- * means unset — the writer emits no `<outlinePr>` and a round-trip never fabricates one.
43
- */
44
27
  outline = {};
45
- /**
46
- * The sheet's frozen-pane view. Empty (a normal view) emits no `<pane>`. Use {@link freeze} and
47
- * {@link unfreeze} for the common cases, or mutate in place for finer control.
48
- */
49
28
  view = {};
50
- /**
51
- * Print-scaling and orientation. Mutate in place: `sheet.pageSetup.fitToPage = true`. Empty means
52
- * unset — the writer emits neither `<pageSetUpPr>` nor `<pageSetup>` and a round-trip never
53
- * fabricates them.
54
- */
55
29
  pageSetup = {};
56
- /**
57
- * Print-toggle flags (`<printOptions>`): centring, and whether headings/gridlines print. Mutate in
58
- * place: `sheet.printOptions.gridLines = true`. Empty means unset — the writer emits no element and
59
- * a round-trip never fabricates one.
60
- */
61
30
  printOptions = {};
62
- /** Print margins. Mutate in place: `sheet.pageMargins.left = 0.5`. Empty means unset. */
63
31
  pageMargins = {};
64
- /** Page header/footer text. Mutate in place: `sheet.headerFooter.oddHeader = '&C&"..."'`. */
65
32
  headerFooter = {};
66
- /**
67
- * Manual horizontal page breaks (`<rowBreaks>`): each break's `id` is a row the print layout splits
68
- * before. Mutate in place: `sheet.rowBreaks.push({id: 3})`. Empty means no manual row breaks and the
69
- * writer emits no `<rowBreaks>` element.
70
- */
71
33
  rowBreaks = [];
72
- /**
73
- * Manual vertical page breaks (`<colBreaks>`): each break's `id` is a column the print layout splits
74
- * before. Mutate in place: `sheet.columnBreaks.push({id: 3})`. Empty means no manual column breaks and
75
- * the writer emits no `<colBreaks>` element.
76
- */
77
34
  columnBreaks = [];
78
- // Row-major sparse storage: row index → (column index → cell). Keeping rows as the
79
- // outer key makes whole-row iteration cheap and mirrors how OOXML serializes
80
- // (`<row>` wrapping `<c>`).
81
35
  #rows = new Map();
82
- // Column and row metadata live apart from the grid so an empty-but-formatted line
83
- // (a hidden column, a tall header row with no data yet) costs no phantom cells.
84
36
  #columns = new Map();
85
37
  #rowProperties = new Map();
86
- // Tables, merged ranges, and anchored images are sheet-level overlays on the grid, not cell storage.
87
38
  #tables = [];
88
39
  #pivotTables = [];
89
- // Pivot tables reconstructed from a loaded package (see io/xlsx/pivot-read.ts) — a read-only,
90
- // inspection-only view distinct from #pivotTables. A loaded pivot round-trips by byte-preservation
91
- // (#preservedReferences), which stays its sole emission authority; this collection is never emitted,
92
- // so exposing it cannot double-emit. Empty for a sheet authored from scratch.
93
40
  #loadedPivotTables = [];
94
- // The sheet's threaded conversations (see io/xlsx/threaded-comments.ts) — unlike #loadedPivotTables, the
95
- // authority the writer serialises from: both the sheet's threadedComment part and the legacy fallback
96
- // comment that binds each cell to its conversation are derived from this list. Empty for a sheet with no
97
- // threaded comments.
98
41
  #commentThreads = [];
99
42
  #merges = [];
100
43
  #images = [];
101
- // A sheet background is a single workbook image tiled behind the grid — distinct from an anchored
102
- // drawing (it has no anchor and rides its own worksheet relationship, not a drawing part).
103
44
  #backgroundImageId;
104
- // Worksheet-level references to package content the model does not interpret (a vector-shape
105
- // drawing, a header/footer image), captured verbatim on read so a round-trip re-emits them rather
106
- // than dropping them. Empty for a sheet authored from scratch.
107
45
  #preservedReferences = [];
108
- // Decoded rectangles parallel to #merges, kept so that addressing a covered cell can
109
- // resolve to its region's master without re-parsing the range string on every access, and
110
- // so that a new merge can be checked for overlap against the existing ones. Only fully-bounded
111
- // merges (a real cell block) get a rect; an unbounded whole-row/column merge is still declared
112
- // but participates in neither slave resolution nor overlap checking.
113
46
  #mergeRects = [];
114
- // Data validations and conditional formattings are sheet-level overlays keyed by range, each owning
115
- // its own storage/cloning/lookup — see DataValidationOverlay and ConditionalFormattingOverlay.
116
47
  #dataValidations = new DataValidationOverlay();
117
48
  #conditionalFormattings = new ConditionalFormattingOverlay();
118
- // Sheet-level protection is a single overlay switch, absent until `protect` is called.
119
49
  #protection;
120
- // The sheet's autofilter (range plus any per-column criteria), absent until one is set. A single
121
- // sheet-level overlay, distinct from a table's own autofilter; stored canonically so the
122
- // `<autoFilter>` element and the derived `_FilterDatabase` defined name always agree.
123
50
  #autoFilter;
124
- // Structural-edit machinery (row/column splices), sharing this sheet's storage by reference. The
125
- // public spliceRows/spliceColumns/duplicateRow build the cells an insert introduces, then delegate
126
- // the shift arithmetic here. Wired in the constructor body, not a field initializer, so it never
127
- // depends on this field's declaration position relative to the storage fields above — a class field
128
- // initializer only sees an earlier-declared field's initial value, so if this stayed a field
129
- // initializer, reordering the class body could silently hand GridEdits a still-undefined map.
130
51
  #edits;
131
52
  constructor(name, id, state = 'visible') {
132
53
  this.name = name;
@@ -142,50 +63,20 @@ export class Worksheet {
142
63
  images: this.#images,
143
64
  });
144
65
  }
145
- /**
146
- * Get the cell at an A1 reference, creating it on first access. The reference must
147
- * name both a column and a row (`"B3"`); a whole-row or whole-column reference is
148
- * not a cell and is rejected.
149
- *
150
- * Addressing a cell covered by a merged region resolves to that region's master
151
- * (top-left) cell, mirroring how a spreadsheet treats the merge as one cell: a value
152
- * or style written through a covered address lands on the master, and reading a
153
- * covered address returns the master's. Only the master ever holds an independent
154
- * value, so the serialized sheet stays well-formed (no stray value on a covered cell).
155
- *
156
- * @throws {SyntaxError} if the reference does not resolve to a single cell.
157
- */
158
66
  getCell(reference) {
159
67
  const { col, row } = decodeAddress(reference);
160
68
  if (col === undefined || row === undefined) {
161
69
  throw new SyntaxError(`"${reference}" is not a single-cell reference — it omits a column or row`);
162
70
  }
163
- const master = this.#masterOf(row, col);
71
+ const master = masterOf(this.#mergeRects, row, col);
164
72
  return this.#cellAt(master.row, master.col);
165
73
  }
166
- /** Whether a cell has been materialised at the given 1-based position. */
167
74
  hasCell(row, col) {
168
75
  return this.#rows.get(row)?.has(col) ?? false;
169
76
  }
170
- /**
171
- * A handle on a 1-based column: its formatting, its cells, and its values. Cheap and stateless —
172
- * it creates neither cells nor a format record, so asking about a column costs nothing and does
173
- * not extend the used range. Writing through it (`getColumn(2).width = 12`) is what materialises
174
- * the record.
175
- *
176
- * @throws {RangeError} if the index is not a positive integer.
177
- */
178
77
  getColumn(index) {
179
78
  return new Column(this, index);
180
79
  }
181
- /**
182
- * A handle on a 1-based row: its formatting, its cells, and its values. Cheap and stateless — it
183
- * creates neither cells nor a format record, so asking about a row costs nothing and does not
184
- * extend the used range. Writing through it (`getRow(3).height = 20`) is what materialises the
185
- * record.
186
- *
187
- * @throws {RangeError} if the number is not a positive integer.
188
- */
189
80
  getRow(number) {
190
81
  return new Row(this, number);
191
82
  }
@@ -194,12 +85,6 @@ export class Worksheet {
194
85
  return rangeFrom(this, referenceOrTop);
195
86
  return new Range(this, referenceOrTop, left ?? 0, bottom ?? 0, right ?? 0);
196
87
  }
197
- /**
198
- * The 1-based index of the last row carrying anything — data or its own formatting —
199
- * or 0 for an empty sheet. Spans gaps: a value in row 5 makes this 5 even if rows 2–4
200
- * are empty. This is the used-range extent, not a populated-row tally (see
201
- * {@link actualRowCount}).
202
- */
203
88
  get rowCount() {
204
89
  let last = 0;
205
90
  for (const [number, cols] of this.#rows) {
@@ -210,15 +95,12 @@ export class Worksheet {
210
95
  if (number > last)
211
96
  last = number;
212
97
  }
213
- // A merged region occupies its whole rectangle even where the covered cells are empty, so a merge
214
- // extending past the last populated row still belongs to the used range.
215
98
  for (const rect of this.#mergeRects) {
216
99
  if (rect.bottom > last)
217
100
  last = rect.bottom;
218
101
  }
219
102
  return last;
220
103
  }
221
- /** The number of rows that hold at least one non-empty cell, ignoring gaps and formatting-only rows. */
222
104
  get actualRowCount() {
223
105
  let count = 0;
224
106
  for (const cols of this.#rows.values()) {
@@ -227,9 +109,6 @@ export class Worksheet {
227
109
  }
228
110
  return count;
229
111
  }
230
- // Whether any cell materialised in a row holds a value — the used-range test {@link rowCount} and
231
- // {@link actualRowCount} share. Short-circuits on the first non-empty cell rather than allocating the
232
- // row's values into a throwaway array to scan them.
233
112
  #rowHasContent(cols) {
234
113
  for (const cell of cols.values()) {
235
114
  if (cell.value !== null)
@@ -237,11 +116,6 @@ export class Worksheet {
237
116
  }
238
117
  return false;
239
118
  }
240
- /**
241
- * The 1-based index of the last column carrying anything — a non-empty cell or its own format
242
- * properties — or 0 for an empty sheet. The used-range width, mirroring {@link rowCount} for the
243
- * other axis: a value in column E makes this 5 even if columns B–D are empty.
244
- */
245
119
  get columnCount() {
246
120
  let last = 0;
247
121
  for (const cols of this.#rows.values()) {
@@ -260,27 +134,6 @@ export class Worksheet {
260
134
  }
261
135
  return last;
262
136
  }
263
- /**
264
- * The sheet's used range as one handle — `A1` through the last row and column that carry
265
- * anything — or `undefined` when there is no rectangle to name.
266
- *
267
- * This is {@link rowCount} and {@link columnCount} said once, so a caller stops reassembling
268
- * `A1:${numberToColumn(sheet.columnCount)}${sheet.rowCount}` by hand. That is what an
269
- * {@link autoFilter} covering the whole sheet wants — `sheet.autoFilter = sheet.usedRange.address`
270
- * — and Excel writes exactly that ref for a filter it applies itself. A header-only ref filters
271
- * nothing, which is the bug this exists to make hard to write.
272
- *
273
- * It inherits both counts' definition of *used*, so it spans gaps (a value in `E5` and nothing
274
- * else still gives `A1:E5`) and includes a line carrying only its own formatting — a set column
275
- * width, an outline level, a merge reaching past the last value. `undefined` therefore means
276
- * strictly "no rectangle": an empty sheet, or one carrying only row formatting and no columns at
277
- * all (or the reverse), where an axis has no extent to bound the other against.
278
- *
279
- * Not the same thing as the `<dimension>` a written package records. That is the *tight* box —
280
- * top-left at the first used cell, formatting-only rows excluded — because Excel writes it to
281
- * describe where the data is, not what the grid spans. This handle is anchored at `A1`, because
282
- * a caller asking for the used range means the block to read, style or filter.
283
- */
284
137
  get usedRange() {
285
138
  const bottom = this.rowCount;
286
139
  const right = this.columnCount;
@@ -288,54 +141,25 @@ export class Worksheet {
288
141
  return undefined;
289
142
  return new Range(this, 1, 1, bottom, right);
290
143
  }
291
- /** The columns carrying format properties, as handles, in ascending index order. */
292
144
  *columns() {
293
145
  for (const index of [...this.#columns.keys()].sort((a, b) => a - b)) {
294
146
  yield new Column(this, index);
295
147
  }
296
148
  }
297
- /**
298
- * The rows to serialise, as handles, in ascending row order: the union of rows holding cells and
299
- * rows holding only metadata (a hidden or grouped row need carry no data). Mirrors how OOXML
300
- * serialises (`<row>` wrapping `<c>`) and is the writer's row surface.
301
- *
302
- * A handle yields its cells only when asked, so a pass that reads nothing but row attributes
303
- * never assembles a cell array it will not look at.
304
- */
305
149
  *rows() {
306
150
  const numbers = new Set([...this.#rows.keys(), ...this.#rowProperties.keys()]);
307
151
  for (const number of [...numbers].sort((a, b) => a - b)) {
308
152
  yield new Row(this, number);
309
153
  }
310
154
  }
311
- /**
312
- * Define a table over a range of this sheet. The table's shape invariants (a legal
313
- * name, at least one column, at least one row) are enforced here; conflicts with the
314
- * rest of the sheet (e.g. an overlapping merge) are the writer's concern.
315
- *
316
- * @throws {AuthoringError} if the name, columns, or geometry are invalid.
317
- */
318
155
  addTable(options) {
319
156
  const table = new Table(options, (row, col, value, style) => {
320
157
  const cell = this.#cellAt(row, col);
321
158
  cell.value = value;
322
159
  if (style !== undefined)
323
160
  applyCellStyle(cell, style);
324
- },
325
- // Insert one empty grid row at `row`; the splice re-pins this table (growing its data rows) and
326
- // shifts the totals row and everything below down by one.
327
- (row) => this.spliceRows(row, 0, []));
161
+ }, (row) => this.spliceRows(row, 0, []));
328
162
  this.#tables.push(table);
329
- // A table's declared range includes its header row, and Excel treats the column metadata and
330
- // the cells under it as one fact: a header row that is empty in the grid is corruption, and
331
- // Excel repairs the file on open — discarding the column names entirely. The caller already
332
- // named the columns once in the table definition, so materialising them here is what makes the
333
- // obvious API call produce a file that opens.
334
- //
335
- // Only *empty* header cells are filled. Reading a workbook re-registers each table through this
336
- // method after the sheet's cells are loaded, and those cells are authoritative: they may carry
337
- // rich text, a style, or text that drifted from the column name, none of which a re-declaration
338
- // may clobber. An empty cell has no such content to lose.
339
163
  if (table.headerRow) {
340
164
  const { top, left } = table.region;
341
165
  table.columns.forEach((column, index) => {
@@ -345,19 +169,6 @@ export class Worksheet {
345
169
  this.#cellAt(top, col).value = column.name;
346
170
  });
347
171
  }
348
- // Materialize the totals row Excel renders on open, so our files show it immediately rather than a
349
- // blank strip until the user interacts. A labelled column writes its label string; an aggregate
350
- // column writes the `SUBTOTAL(code, Table[Column])` formula Excel would compute. Unlike the header
351
- // row, this is a UX-parity nicety, not a validity fix — Excel opens a declared-but-empty totals row
352
- // without repair — but matching its on-open rendering is the point.
353
- //
354
- // Same round-trip guard as the header row: only *empty* cells are filled. Reading a file
355
- // re-registers the table after its cells are loaded, so a materialized totals cell — ours, Excel's,
356
- // or a hand-set override — is authoritative and must survive untouched, keeping the round-trip
357
- // idempotent. The formula carries no cached result; Excel computes an uncached formula cell on open,
358
- // so the row shows real values without the library pretending to be a calc engine. A `custom` column
359
- // writes its stored `totalsRowFormula` verbatim; a `none` column (or a `custom` with no stored
360
- // formula) has nothing to write (see {@link TOTALS_ROW_SUBTOTAL_CODE}) and stays blank.
361
172
  if (table.totalsRow) {
362
173
  const { left, bottom } = table.region;
363
174
  table.columns.forEach((column, index) => {
@@ -377,8 +188,6 @@ export class Worksheet {
377
188
  }
378
189
  else if (column.totalsRowFunction === 'custom' &&
379
190
  column.totalsRowFormula !== undefined) {
380
- // A `custom` total is the column's own stored formula, not a SUBTOTAL. Excel stores it
381
- // without a leading `=`, which is the formula string a cell value expects.
382
191
  this.#cellAt(bottom, col).value = { formula: column.totalsRowFormula };
383
192
  }
384
193
  }
@@ -386,67 +195,25 @@ export class Worksheet {
386
195
  }
387
196
  return table;
388
197
  }
389
- /** The tables defined on this sheet, in definition order. */
390
198
  get tables() {
391
199
  return this.#tables;
392
200
  }
393
- /** The table with the given name (case-sensitive, the identifier Excel uses), or `undefined`.
394
- * A table read back from a file is fully hydrated — its rows can be read and appended to. */
395
201
  getTable(name) {
396
202
  return this.#tables.find((table) => table.name === name);
397
203
  }
398
- /**
399
- * Add a pivot table to this (destination) sheet, summarising a source sheet's data. The source is
400
- * read once, now, so the pivot is a snapshot: later edits to the source do not change it. The
401
- * supported shape (one summed value field, at least one row and column field) is enforced here.
402
- *
403
- * @throws {AuthoringError} if the metric, fields, or source shape are unsupported.
404
- */
405
204
  addPivotTable(options) {
406
205
  const pivot = new PivotTable(options);
407
206
  this.#pivotTables.push(pivot);
408
207
  return pivot;
409
208
  }
410
- /** The pivot tables hosted on this sheet, in definition order. */
411
209
  get pivotTables() {
412
210
  return this.#pivotTables;
413
211
  }
414
- /**
415
- * Pivot tables reconstructed from a loaded package, in the order the reader found them — a
416
- * read-only inspection view (source range, field roles, value field, aggregation). A pivot
417
- * authored on this sheet via {@link addPivotTable} does not appear here; a pivot loaded from a
418
- * file does not appear in {@link pivotTables}. The loaded pivots re-emit verbatim through
419
- * byte-preservation, so this collection is never itself serialised.
420
- */
421
212
  get loadedPivotTables() {
422
213
  return this.#loadedPivotTables;
423
214
  }
424
- /**
425
- * Anchor a threaded conversation to a cell — Excel's modern review comment: an opening message, its
426
- * replies, and whether the discussion was marked resolved. Distinct from a cell's legacy note
427
- * ({@link Cell.note}), and mutually exclusive with one: Excel refuses to put both on one cell, and a
428
- * cell carrying both is written back as the conversation alone.
429
- *
430
- * Every message supplies its own {@link Comment.id} and {@link Comment.date}, and names its author by
431
- * {@link Comment.personId} into the workbook registry ({@link Workbook.addPerson}) — the writer has no
432
- * clock and no id generator, so nothing here is invented and the same workbook always serialises to the
433
- * same bytes. Every id is normalised to the brace-wrapped upper-case GUID form the format requires, so a
434
- * `crypto.randomUUID()` is accepted as-is.
435
- *
436
- * Message ids must be unique **within this sheet**, because that is the scope in which they mean
437
- * anything: a reply names its thread by the head's id inside the sheet's own part, and the legacy
438
- * fallback comment binds its cell by the same id inside the sheet's own comments part. Two sheets reusing
439
- * one id is therefore harmless and is not rejected — Excel's ids happen to be globally unique, but
440
- * nothing resolves across a part boundary.
441
- *
442
- * @throws {SyntaxError} if the anchor does not resolve to a single cell, if any id is not a GUID, if a
443
- * message id is already used on this sheet, or if a mention's span is not a whole number the wire can
444
- * express.
445
- */
446
215
  addCommentThread(thread) {
447
216
  const taken = new Set(this.#commentThreads.flatMap((held) => held.comments.map((comment) => comment.id)));
448
- // Every message is validated before any of it is stored, so a rejection leaves the sheet untouched
449
- // rather than half-carrying a conversation whose remaining messages were refused.
450
217
  const comments = thread.comments.map((comment) => {
451
218
  const id = commentThreadGuid(comment.id, 'a comment id');
452
219
  if (taken.has(id)) {
@@ -473,17 +240,9 @@ export class Worksheet {
473
240
  });
474
241
  this.#commentThreads.push({ ...thread, ref: this.#anchorRef(thread.ref), comments });
475
242
  }
476
- /**
477
- * The threaded conversations on this sheet — Excel's modern review comments (author, timestamp,
478
- * replies, resolved state, `@mentions`). Empty for a sheet with none. Distinct from a cell's legacy note
479
- * ({@link Cell.note}).
480
- */
481
243
  get commentThreads() {
482
244
  return this.#commentThreads;
483
245
  }
484
- // The canonical A1 form of a conversation's anchor. A thread hangs off one cell, and both the writer's
485
- // fallback comment and {@link commentThreadAt} compare anchors as plain strings, so `$B$2` and `B2` must
486
- // not be two anchors.
487
246
  #anchorRef(reference) {
488
247
  const { col, row } = decodeAddress(reference);
489
248
  if (col === undefined || row === undefined) {
@@ -491,20 +250,11 @@ export class Worksheet {
491
250
  }
492
251
  return encodeAddress(col, row);
493
252
  }
494
- /**
495
- * The conversation anchored to a cell, or `undefined` when that cell carries none. The reference is
496
- * canonicalized, so an absolute `"$B$2"` finds the same thread as `"B2"`; it names the *anchor* cell,
497
- * so a cell merely covered by the anchor's merged region is not a match.
498
- *
499
- * @throws {SyntaxError} if the reference does not resolve to a single cell.
500
- */
501
253
  commentThreadAt(reference) {
502
254
  const anchor = this.#anchorRef(reference);
503
255
  return this.#commentThreads.find((thread) => thread.ref === anchor);
504
256
  }
505
257
  addImage(imageId, anchor) {
506
- // Bind the pure anchor geometry to this sheet's per-column/row sizes; a size a column or row does
507
- // not set defers to the sheet default, then (inside resolveAnchorPoint) to Excel's own default.
508
258
  const columnWidth = (col) => this.#columns.get(col + 1)?.width ?? this.properties.defaultColWidth;
509
259
  const rowHeight = (row) => this.#rowProperties.get(row + 1)?.height ?? this.properties.defaultRowHeight;
510
260
  if ('ext' in anchor) {
@@ -521,53 +271,28 @@ export class Worksheet {
521
271
  const twoCell = anchor.editAs !== undefined ? { from, to, editAs: anchor.editAs } : { from, to };
522
272
  this.#images.push({ imageId, anchor: twoCell });
523
273
  }
524
- /**
525
- * Anchor an image with a pre-built model anchor in the model's own units (EMUs). This is the
526
- * low-level primitive {@link addImage} builds on and the reader uses to re-pin an image parsed from
527
- * a drawing part without a lossy pixel round-trip.
528
- */
529
274
  addImageAnchor(imageId, anchor) {
530
275
  this.#images.push({ imageId, anchor });
531
276
  }
532
- /** Drop every anchor of the given workbook image from this sheet. The image stays registered on the
533
- * workbook — another sheet may still show it — so only this sheet's anchors are removed; the writer
534
- * then omits any media no sheet anchors any longer. */
535
277
  removeImage(imageId) {
536
278
  const kept = this.#images.filter((image) => image.imageId !== imageId);
537
279
  replaceContents(this.#images, kept);
538
280
  }
539
- /** The images anchored to this sheet, in the order they were added. */
540
281
  get images() {
541
282
  return this.#images;
542
283
  }
543
- /** Set this sheet's background image to a workbook image (the id {@link Workbook.addImage} returned).
544
- * The picture tiles behind the whole grid; it is not anchored to any cell. Passing a new id replaces
545
- * the previous background. */
546
284
  addBackgroundImage(imageId) {
547
285
  this.#backgroundImageId = imageId;
548
286
  }
549
- /** Remove this sheet's background image, if any. The image stays registered on the workbook. */
550
287
  removeBackgroundImage() {
551
288
  this.#backgroundImageId = undefined;
552
289
  }
553
- /** The workbook image id set as this sheet's background, or `undefined` when it has none. */
554
290
  get backgroundImageId() {
555
291
  return this.#backgroundImageId;
556
292
  }
557
- /** The worksheet-level references to unmodeled package content preserved for round-tripping. */
558
293
  get preservedReferences() {
559
294
  return this.#preservedReferences;
560
295
  }
561
- /**
562
- * Merge a range of cells (`"A1:B2"`). A range that overlaps an already-merged region is
563
- * rejected — Excel forbids overlapping merges and writes such geometry as a corrupt file.
564
- * Whole-row/column ranges (`"A:A"`) are unbounded, carry no rectangle, and are not overlap-checked.
565
- *
566
- * Any value already sitting in a covered non-anchor cell is discarded, keeping only the top-left
567
- * anchor's — exactly how Excel collapses a range on merge. Leaving it would emit a populated `<c>`
568
- * under the `<mergeCell>` ref, the geometry Excel opens with a repair prompt. Covered-cell styles
569
- * survive (a border spanning the merge is legal), so only the conflicting value is cleared.
570
- */
571
296
  mergeCells(range) {
572
297
  const { top, left, bottom, right } = decodeRange(range);
573
298
  if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
@@ -577,55 +302,19 @@ export class Worksheet {
577
302
  throw new AuthoringError(`merged range "${range}" overlaps an existing merged region`);
578
303
  }
579
304
  this.#mergeRects.push(rect);
580
- this.#clearCoveredValues(rect);
305
+ clearCoveredValues(this.#rows, rect);
581
306
  }
582
307
  this.#merges.push(range);
583
308
  }
584
- // Drop any value already sitting in a merge's covered non-anchor cells, keeping only the top-left
585
- // anchor — the collapse Excel performs on merge. A leftover covered value would serialise as a
586
- // populated `<c>` under the range's `<mergeCell>` ref, the geometry that trips Excel's repair
587
- // prompt. Styles are untouched: a border spanning the merged region rides the covered cells.
588
- #clearCoveredValues(rect) {
589
- for (let row = rect.top; row <= rect.bottom; row++) {
590
- const cols = this.#rows.get(row);
591
- if (cols === undefined)
592
- continue;
593
- for (let col = rect.left; col <= rect.right; col++) {
594
- if (row === rect.top && col === rect.left)
595
- continue;
596
- const covered = cols.get(col);
597
- if (covered !== undefined)
598
- covered.value = null;
599
- }
600
- }
601
- }
602
- /** The merged ranges on this sheet, in the order they were added. */
603
309
  get merges() {
604
310
  return this.#merges;
605
311
  }
606
- /**
607
- * The sheet's autofilter — its range plus any per-column criteria — or `undefined` when the sheet
608
- * carries none. Setting one turns on the header-row filter dropdowns Excel draws over the range;
609
- * the writer emits both the sheet's `<autoFilter>` element and the hidden `_FilterDatabase` defined
610
- * name Excel derives from it. Setting `undefined` clears the filter.
611
- *
612
- * A bare range string is the ergonomic common case — `sheet.autoFilter = 'A1:C10'` for dropdowns
613
- * with no active criteria; pass an {@link AutoFilter} object to narrow columns. Either way the
614
- * value is normalised on assignment (range to canonical `A1:C10` form) and the getter returns the
615
- * structured object. The range must be a bounded rectangle — a whole-row/column reference is not a
616
- * filterable region and is rejected.
617
- */
618
312
  get autoFilter() {
619
313
  return this.#autoFilter;
620
314
  }
621
315
  set autoFilter(filter) {
622
316
  this.#autoFilter = filter === undefined ? undefined : canonicalizeAutoFilter(filter);
623
317
  }
624
- /**
625
- * Remove a merged range previously added with {@link mergeCells}, returning whether a merge with
626
- * that exact range string existed. The covering rectangle is dropped alongside it, so a cell the
627
- * merge had masked addresses independently again. The inverse of {@link mergeCells}.
628
- */
629
318
  unmergeCells(range) {
630
319
  const index = this.#merges.indexOf(range);
631
320
  if (index === -1)
@@ -639,58 +328,24 @@ export class Worksheet {
639
328
  }
640
329
  return true;
641
330
  }
642
- /**
643
- * Attach a data validation to a target range (`"B2:B20"`, a whole column `"B2:B1048576"`, or a
644
- * space-separated `sqref` of several ranges). The rule is stored once against the range, not copied
645
- * per covered cell, so a whole-column dropdown stays a single entry. A cell inside the range reports
646
- * the rule through {@link dataValidationAt}.
647
- *
648
- * Pass `{extended: true}` to mark a rule that belongs in the 2009 extension form
649
- * (`<x14:dataValidation>`) — the carrier Excel uses for a list source on another sheet and other
650
- * shapes the standard element cannot express. The reader sets it for a rule found in that form so a
651
- * round-trip writes it back there instead of silently corrupting the cross-sheet reference.
652
- */
653
331
  addDataValidation(sqref, rule, options = {}) {
654
332
  this.#dataValidations.add(sqref, rule, options);
655
333
  }
656
- /** The data validations on this sheet, each bound to its target range, in insertion order. */
657
334
  get dataValidations() {
658
335
  return this.#dataValidations.entries;
659
336
  }
660
- /**
661
- * Attach a conditional formatting to a target range. `formatting.ref` is an OOXML `sqref` — one
662
- * range (`"A1:A10"`), a whole column, or several space-separated areas (`"A1:C1 A3:C3"`) sharing one
663
- * rule set. The block is stored once against the range, defensively copied so the getter never hands
664
- * back a reference into the caller's object.
665
- */
666
337
  addConditionalFormatting(formatting) {
667
338
  this.#conditionalFormattings.add(formatting);
668
339
  }
669
- /** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
670
340
  get conditionalFormattings() {
671
341
  return this.#conditionalFormattings.entries;
672
342
  }
673
- /**
674
- * The validation covering a cell, or `undefined` when none does. The first added rule whose range
675
- * contains the cell wins, mirroring how a spreadsheet resolves overlapping validations.
676
- */
677
343
  dataValidationAt(reference) {
678
344
  const { col, row } = decodeAddress(reference);
679
345
  if (col === undefined || row === undefined)
680
346
  return undefined;
681
347
  return this.#dataValidations.at(col, row);
682
348
  }
683
- /**
684
- * Remove `count` rows starting at the 1-based `start`, then insert the given rows in their place.
685
- * Rows below the edit shift by `inserts.length - count`: a delete pulls the tail up, an insert
686
- * pushes it down, and doing both at once is a replace. Each inserted row takes either
687
- * {@link RowInput} shape — a positional array from column A, or a key-addressed object — exactly
688
- * like {@link addRow}. A `count` larger than the rows present simply clears the tail — it never
689
- * silently becomes a no-op. Cells carry their full style to the shifted position, and merged ranges
690
- * shift with the rows they cover.
691
- *
692
- * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
693
- */
694
349
  spliceRows(start, count, ...inserts) {
695
350
  if (!Number.isInteger(start) || start < 1) {
696
351
  throw new RangeError(`splice start ${start} is out of bounds — rows start at 1`);
@@ -698,87 +353,26 @@ export class Worksheet {
698
353
  if (!Number.isInteger(count) || count < 0) {
699
354
  throw new RangeError(`splice count ${count} is invalid — it must be a non-negative integer`);
700
355
  }
701
- const inserted = inserts.map((values, i) => this.#buildRowCells(start + i, values));
356
+ const inserted = inserts.map((values, i) => buildRowCells(start + i, values, this.#columns));
702
357
  this.#edits.spliceRows(start, count, inserted);
703
358
  }
704
- /**
705
- * Insert one row of `values` at the 1-based `pos`, shifting the rows at and below it down by one.
706
- * `values` takes either {@link RowInput} shape (positional array or keyed object), like
707
- * {@link addRow}. Shorthand for {@link spliceRows}`(pos, 0, values)`.
708
- *
709
- * @throws {RangeError} if `pos` is not a positive integer.
710
- */
711
359
  insertRow(pos, values) {
712
360
  this.spliceRows(pos, 0, values);
713
361
  }
714
- /**
715
- * Append a row of `values` after the last used row, returning the cells it materialised.
716
- * The append point is {@link rowCount}` + 1`, so the row lands below every row that holds
717
- * data or its own formatting — never overwriting existing content, unlike {@link insertRow},
718
- * which shifts and needs a position. Unlike {@link spliceRows}, appending shifts nothing, so
719
- * it never disturbs merges or the rows above.
720
- *
721
- * A row takes either shape: a positional array whose values map to columns from A — a hole in a
722
- * sparse array (`['a', , 'c']`) leaves that column untouched — or a keyed object whose values
723
- * land under the columns carrying the matching {@link ColumnProperties.key}.
724
- */
725
362
  addRow(values) {
726
363
  return this.addRows([values])[0] ?? [];
727
364
  }
728
- /**
729
- * Append several rows after the last used row in one call, returning the cells materialised
730
- * for each. The rows stack in order — the first lands at {@link rowCount}` + 1`, the next
731
- * directly below it — so a later row never collides with an earlier one even when both are
732
- * value-less. Each row is an array or a keyed object independently, so a mixed batch is fine.
733
- * The bulk form of {@link addRow}.
734
- */
735
365
  addRows(rows) {
736
366
  let number = this.rowCount;
737
367
  return rows.map((values) => {
738
368
  number += 1;
739
- return this.#rowPlacements(values).map(([col, value]) => {
369
+ return rowPlacements(values, this.#columns).map(([col, value]) => {
740
370
  const cell = this.#cellAt(number, col);
741
371
  cell.value = value;
742
372
  return cell;
743
373
  });
744
374
  });
745
375
  }
746
- // Resolve a RowInput to the (1-based column, value) placements it names, the one interpretation of
747
- // row shape that both appending (into the live grid) and splicing (into a detached row) share. A
748
- // positional array maps each value to its column from A, skipping a hole or an explicit `undefined`
749
- // so that column is left untouched; a keyed object maps each value under the column carrying the
750
- // matching key. Array.isArray, not `instanceof Array`: a row built in another realm (a vm context,
751
- // a browser iframe) is still an array but fails the identity check, and would then be walked as a
752
- // keyed object — placing nothing.
753
- #rowPlacements(values) {
754
- if (Array.isArray(values)) {
755
- const placements = [];
756
- values.forEach((value, index) => {
757
- if (value !== undefined)
758
- placements.push([index + 1, value]);
759
- });
760
- return placements;
761
- }
762
- return Object.entries(values).map(([key, value]) => [this.#columnIndexByKey(key), value]);
763
- }
764
- // Build the detached cell row an insert introduces: a fresh cell per placement, positioned at
765
- // `number`, keyed by column. The grid-edit machinery then splices this map into place.
766
- #buildRowCells(number, values) {
767
- const row = new Map();
768
- for (const [col, value] of this.#rowPlacements(values)) {
769
- const cell = new Cell(number, col);
770
- cell.value = value;
771
- row.set(col, cell);
772
- }
773
- return row;
774
- }
775
- /**
776
- * Freeze the top `ySplit` rows and left `xSplit` columns in place; the rest of the sheet scrolls
777
- * beneath them. `freeze(1)` pins a header row; `freeze(0, 1)` pins the first column. Passing both
778
- * zero clears the freeze (equivalent to {@link unfreeze}).
779
- *
780
- * @throws {RangeError} if either split is a negative or non-integer count.
781
- */
782
376
  freeze(ySplit = 1, xSplit = 0) {
783
377
  if (!Number.isInteger(ySplit) || ySplit < 0 || !Number.isInteger(xSplit) || xSplit < 0) {
784
378
  throw new RangeError(`freeze splits must be non-negative integers; got ySplit=${ySplit}, xSplit=${xSplit}`);
@@ -792,30 +386,12 @@ export class Worksheet {
792
386
  this.view.ySplit = ySplit;
793
387
  this.view.topLeftCell = encodeAddress(xSplit + 1, ySplit + 1);
794
388
  }
795
- /** Clear any frozen split, returning the sheet to a normal (fully scrolling) view. */
796
389
  unfreeze() {
797
390
  this.view.state = 'normal';
798
391
  delete this.view.xSplit;
799
392
  delete this.view.ySplit;
800
393
  delete this.view.topLeftCell;
801
394
  }
802
- /** The 1-based index of the column carrying `key` (see {@link ColumnProperties.key}). */
803
- #columnIndexByKey(key) {
804
- for (const [index, properties] of this.#columns) {
805
- if (properties.key === key)
806
- return index;
807
- }
808
- throw new AuthoringError(`no column is keyed ${JSON.stringify(key)} — set getColumn(n).key first`);
809
- }
810
- /**
811
- * Copy the row at the 1-based `start`, `options.count` times (default 1). With `options.insert`
812
- * (the default) the copies are inserted directly after the source, shifting the rows below — and
813
- * any merged range there — down by `count`; otherwise the copies overwrite the rows immediately
814
- * below without shifting. Each copy is a faithful duplicate of the source's values and per-cell
815
- * styles, and carries no merge of its own, so a range can be merged onto a duplicated row afterwards.
816
- *
817
- * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
818
- */
819
395
  duplicateRow(start, options = {}) {
820
396
  const { count = 1, insert = true } = options;
821
397
  if (!Number.isInteger(start) || start < 1) {
@@ -845,15 +421,6 @@ export class Worksheet {
845
421
  this.#rows.set(start + i, snapshot(start + i));
846
422
  }
847
423
  }
848
- /**
849
- * Remove `count` columns starting at the 1-based `start`, then insert the given columns in their
850
- * place — the column analog of {@link spliceRows}. Columns to the right shift by
851
- * `inserts.length - count`, keeping their values and styles, and a merged range lying wholly to
852
- * the right of the edit re-anchors to its new columns. Each inserted column is an array of values
853
- * indexed by row (index 0 → row 1); an empty array inserts a blank column.
854
- *
855
- * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
856
- */
857
424
  spliceColumns(start, count, ...inserts) {
858
425
  if (!Number.isInteger(start) || start < 1) {
859
426
  throw new RangeError(`splice start ${start} is out of bounds — columns start at 1`);
@@ -863,35 +430,12 @@ export class Worksheet {
863
430
  }
864
431
  this.#edits.spliceColumns(start, count, inserts);
865
432
  }
866
- /**
867
- * Insert one column of `values` at the 1-based `pos`, shifting the columns at and right of it over
868
- * by one. `values` is an array of values indexed by row (index 0 → row 1), like
869
- * {@link addColumn}. Shorthand for {@link spliceColumns}`(pos, 0, values)`.
870
- *
871
- * @throws {RangeError} if `pos` is not a positive integer.
872
- */
873
433
  insertColumn(pos, values) {
874
434
  this.spliceColumns(pos, 0, values);
875
435
  }
876
- /**
877
- * Append a column of `values` after the last used column, returning the cells it materialised.
878
- * The append point is {@link columnCount}` + 1`, so the column lands right of every column that
879
- * holds data or its own formatting — never overwriting existing content, unlike {@link insertColumn},
880
- * which shifts and needs a position. Unlike {@link spliceColumns}, appending shifts nothing, so it
881
- * never disturbs merges or the columns to its left.
882
- *
883
- * `values` is an array indexed by row (index 0 → row 1); a hole or an explicit `undefined` leaves
884
- * that row untouched, mirroring {@link addRow}'s positional-array shape.
885
- */
886
436
  addColumn(values) {
887
437
  return this.addColumns([values])[0] ?? [];
888
438
  }
889
- /**
890
- * Append several columns after the last used column in one call, returning the cells materialised
891
- * for each. The columns stack in order — the first lands at {@link columnCount}` + 1`, the next
892
- * directly right of it — so a later column never collides with an earlier one even when both are
893
- * value-less. The bulk form of {@link addColumn}.
894
- */
895
439
  addColumns(columns) {
896
440
  let index = this.columnCount;
897
441
  return columns.map((values) => {
@@ -907,26 +451,12 @@ export class Worksheet {
907
451
  return cells;
908
452
  });
909
453
  }
910
- /**
911
- * A snapshot of this sheet's value and overlay content (see {@link WorksheetModel}). Reading it and
912
- * assigning it onto another sheet — `dst.model = src.model` — reproduces the source: merges, cells
913
- * and their styles, column/row metadata, tables, the autofilter, protection, and the page setup all
914
- * survive, because the getter emits and the setter consumes exactly the same fields. Identity
915
- * (`name`, `id`) is not part of the model and is never touched by assignment; nor are attached parts
916
- * that carry workbook-level identity (images, pivots, byte-preserved charts/drawings) — see
917
- * {@link WorksheetModel} for that boundary.
918
- */
919
454
  get model() {
920
455
  const model = {};
921
456
  for (const facet of WORKSHEET_MODEL_FACETS)
922
457
  model[facet.key] = facet.read(this);
923
- // TypeScript cannot follow an object built key by key; what makes this sound is that the
924
- // registry is proved exhaustive over `keyof WorksheetModel` — see the type assertion beside it.
925
458
  return model;
926
459
  }
927
- // Empty every collection the model round-trips, so a subsequent replay leaves no residue from
928
- // whatever the sheet held before. Images, pivots, and byte-preserved parts carry workbook-level
929
- // identity, are excluded from the model, and so are deliberately left untouched here.
930
460
  #resetContent() {
931
461
  this.#rows.clear();
932
462
  this.#columns.clear();
@@ -937,23 +467,11 @@ export class Worksheet {
937
467
  this.#conditionalFormattings.clear();
938
468
  this.#tables.length = 0;
939
469
  }
940
- // Assigning a model replaces this sheet's content wholesale — the sheet becomes the model, with no
941
- // residue from whatever it held before. The registry's declaration order is the application order,
942
- // and it is load-bearing: see WORKSHEET_MODEL_FACETS.
943
470
  set model(model) {
944
471
  this.#resetContent();
945
472
  for (const facet of WORKSHEET_MODEL_FACETS)
946
473
  facet.write(this, model);
947
474
  }
948
- /**
949
- * Protect the sheet, making the per-cell `locked`/`hidden` flags enforceable. Without a
950
- * password the protection is a soft lock any consumer can lift; with one, the password is
951
- * salted and hashed on the spot (the plaintext is never retained) so lifting the protection
952
- * requires re-supplying it. `options` names which operations stay available to a user while
953
- * the sheet is protected; anything unspecified falls to Excel's default for that operation.
954
- *
955
- * Re-protecting replaces any prior protection; {@link unprotect} clears it.
956
- */
957
475
  protect(password, options = {}) {
958
476
  const { spinCount, ...flags } = options;
959
477
  const protection = {
@@ -964,25 +482,12 @@ export class Worksheet {
964
482
  }
965
483
  this.#protection = protection;
966
484
  }
967
- /** Remove any protection previously set by {@link protect}. */
968
485
  unprotect() {
969
486
  this.#protection = undefined;
970
487
  }
971
- /** The sheet's protection, or `undefined` if the sheet is unprotected. */
972
488
  get protection() {
973
489
  return this.#protection;
974
490
  }
975
- // Resolve a position to the master (top-left) of the merged region covering it, or to
976
- // itself when no region does. First covering region wins; overlaps are rejected in
977
- // `mergeCells`, so at most one region ever applies.
978
- #masterOf(row, col) {
979
- for (const rect of this.#mergeRects) {
980
- if (row >= rect.top && row <= rect.bottom && col >= rect.left && col <= rect.right) {
981
- return { row: rect.top, col: rect.left };
982
- }
983
- }
984
- return { row, col };
985
- }
986
491
  #cellAt(row, col) {
987
492
  let cols = this.#rows.get(row);
988
493
  if (cols === undefined) {
@@ -996,10 +501,6 @@ export class Worksheet {
996
501
  }
997
502
  return cell;
998
503
  }
999
- /**
1000
- * The codec's channel into this sheet — see `core/internal.ts` for why these are not public
1001
- * methods. Declared last so every private field it closes over is already in scope.
1002
- */
1003
504
  [INTERNAL] = {
1004
505
  evictRow: (number) => {
1005
506
  this.#rows.delete(number);