@shbernal/ts-xlsx 1.0.0

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