@shbernal/ts-xlsx 3.1.0 → 3.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 (214) hide show
  1. package/README.md +30 -23
  2. package/dist/bytes.d.ts +3 -2
  3. package/dist/core/address.d.ts +36 -0
  4. package/dist/core/address.js +47 -9
  5. package/dist/core/array-formula-ranges.d.ts +33 -0
  6. package/dist/core/array-formula-ranges.js +62 -0
  7. package/dist/core/color-resolution.js +2 -2
  8. package/dist/core/column.js +3 -2
  9. package/dist/core/comment-thread.d.ts +7 -3
  10. package/dist/core/conditional-formatting-overlay.d.ts +9 -0
  11. package/dist/core/conditional-formatting-overlay.js +28 -1
  12. package/dist/core/conditional-formatting.d.ts +98 -10
  13. package/dist/core/conditional-formatting.js +42 -1
  14. package/dist/core/data-validation-overlay.d.ts +5 -0
  15. package/dist/core/data-validation-overlay.js +15 -0
  16. package/dist/core/data-validation.d.ts +16 -0
  17. package/dist/core/data-validation.js +15 -0
  18. package/dist/core/date-format.js +14 -4
  19. package/dist/core/date.js +1 -1
  20. package/dist/core/formula-references.d.ts +26 -0
  21. package/dist/core/formula-references.js +173 -0
  22. package/dist/core/formula-scan.d.ts +2 -0
  23. package/dist/core/formula-scan.js +58 -0
  24. package/dist/core/formula.d.ts +51 -18
  25. package/dist/core/formula.js +77 -86
  26. package/dist/core/function-values.d.ts +4 -0
  27. package/dist/core/function-values.js +58 -0
  28. package/dist/core/future-functions.d.ts +3 -0
  29. package/dist/core/{modern-functions.js → future-functions.js} +119 -88
  30. package/dist/core/grid-edits.d.ts +28 -0
  31. package/dist/core/grid-edits.js +193 -25
  32. package/dist/core/hyperlink.d.ts +39 -0
  33. package/dist/core/hyperlink.js +88 -0
  34. package/dist/core/image.d.ts +36 -5
  35. package/dist/core/image.js +9 -0
  36. package/dist/core/internal.d.ts +3 -1
  37. package/dist/core/limits.d.ts +5 -3
  38. package/dist/core/merge.d.ts +13 -0
  39. package/dist/core/merge.js +15 -1
  40. package/dist/core/page-setup.d.ts +26 -11
  41. package/dist/core/pivot-table.d.ts +49 -10
  42. package/dist/core/pivot-table.js +55 -21
  43. package/dist/core/protection.d.ts +66 -4
  44. package/dist/core/protection.js +4 -0
  45. package/dist/core/row.js +3 -2
  46. package/dist/core/structured-reference.d.ts +7 -0
  47. package/dist/core/structured-reference.js +7 -0
  48. package/dist/core/table.d.ts +38 -8
  49. package/dist/core/table.js +37 -7
  50. package/dist/core/theme.d.ts +2 -0
  51. package/dist/core/theme.js +4 -1
  52. package/dist/core/value.d.ts +51 -28
  53. package/dist/core/value.js +15 -12
  54. package/dist/core/workbook-styles.d.ts +17 -1
  55. package/dist/core/workbook-styles.js +4 -0
  56. package/dist/core/workbook-theme.js +5 -3
  57. package/dist/core/workbook.d.ts +20 -3
  58. package/dist/core/workbook.js +26 -5
  59. package/dist/core/worksheet-merges.d.ts +9 -4
  60. package/dist/core/worksheet-merges.js +17 -11
  61. package/dist/core/worksheet-model.d.ts +1 -1
  62. package/dist/core/worksheet-model.js +4 -0
  63. package/dist/core/worksheet-pictures.d.ts +3 -3
  64. package/dist/core/worksheet-pictures.js +7 -6
  65. package/dist/core/worksheet.d.ts +110 -21
  66. package/dist/core/worksheet.js +174 -49
  67. package/dist/entries/core.d.ts +7 -5
  68. package/dist/entries/core.js +4 -3
  69. package/dist/entries/errors.d.ts +1 -0
  70. package/dist/entries/errors.js +1 -0
  71. package/dist/errors.js +1 -1
  72. package/dist/io/cell-metadata/metadata.d.ts +42 -0
  73. package/dist/io/cell-metadata/metadata.js +35 -0
  74. package/dist/io/cell-metadata/rich-values.d.ts +11 -0
  75. package/dist/io/cell-metadata/rich-values.js +80 -0
  76. package/dist/io/csv/delimiter.d.ts +14 -2
  77. package/dist/io/csv/delimiter.js +12 -0
  78. package/dist/io/csv/errors.d.ts +14 -0
  79. package/dist/io/csv/errors.js +5 -0
  80. package/dist/io/csv/read.d.ts +6 -1
  81. package/dist/io/csv/read.js +46 -24
  82. package/dist/io/csv/write.d.ts +10 -3
  83. package/dist/io/csv/write.js +16 -7
  84. package/dist/io/opc/part-paths.js +4 -4
  85. package/dist/io/opc/read-opc.d.ts +4 -0
  86. package/dist/io/opc/read-opc.js +10 -4
  87. package/dist/io/opc/strict-relationships.d.ts +2 -0
  88. package/dist/io/opc/strict-relationships.js +12 -0
  89. package/dist/io/read-policy/column-budget.d.ts +26 -0
  90. package/dist/io/{xlsx → read-policy}/column-budget.js +4 -8
  91. package/dist/io/read-policy/read-repair.d.ts +74 -0
  92. package/dist/io/read-policy/read-repair.js +119 -0
  93. package/dist/io/style/cell-style-resolution.d.ts +25 -0
  94. package/dist/io/style/cell-style-resolution.js +30 -0
  95. package/dist/io/xlsb/formula.js +4 -2
  96. package/dist/io/xlsb/ptg-functions.js +110 -0
  97. package/dist/io/xlsb/read-metadata.d.ts +7 -0
  98. package/dist/io/xlsb/read-metadata.js +69 -0
  99. package/dist/io/xlsb/read-worksheet.d.ts +6 -0
  100. package/dist/io/xlsb/read-worksheet.js +95 -37
  101. package/dist/io/xlsb/read.js +26 -8
  102. package/dist/io/xlsb/record-types.d.ts +13 -0
  103. package/dist/io/xlsb/record-types.js +13 -0
  104. package/dist/io/xlsb/sheet-protection.d.ts +20 -0
  105. package/dist/io/xlsb/sheet-protection.js +60 -0
  106. package/dist/io/xlsx/cell-accumulator.d.ts +20 -12
  107. package/dist/io/xlsx/cell-accumulator.js +49 -19
  108. package/dist/io/xlsx/cell-metadata.d.ts +39 -0
  109. package/dist/io/xlsx/cell-metadata.js +162 -0
  110. package/dist/io/xlsx/cell-value.d.ts +16 -4
  111. package/dist/io/xlsx/cell-value.js +38 -21
  112. package/dist/io/xlsx/color-xml.js +1 -1
  113. package/dist/io/xlsx/column-span.d.ts +11 -0
  114. package/dist/io/xlsx/column-span.js +9 -0
  115. package/dist/io/xlsx/comments.d.ts +3 -28
  116. package/dist/io/xlsx/comments.js +3 -65
  117. package/dist/io/xlsx/conditional-formatting.d.ts +42 -25
  118. package/dist/io/xlsx/conditional-formatting.js +424 -129
  119. package/dist/io/xlsx/data-validation.d.ts +12 -7
  120. package/dist/io/xlsx/data-validation.js +41 -29
  121. package/dist/io/xlsx/edit-vba.js +43 -36
  122. package/dist/io/xlsx/hyperlinks.d.ts +16 -50
  123. package/dist/io/xlsx/hyperlinks.js +8 -37
  124. package/dist/io/xlsx/images.d.ts +49 -16
  125. package/dist/io/xlsx/images.js +145 -44
  126. package/dist/io/xlsx/namespaces.d.ts +8 -0
  127. package/dist/io/xlsx/namespaces.js +4 -0
  128. package/dist/io/xlsx/package-plan.d.ts +48 -15
  129. package/dist/io/xlsx/package-plan.js +46 -9
  130. package/dist/io/xlsx/part-names.d.ts +6 -8
  131. package/dist/io/xlsx/part-names.js +3 -7
  132. package/dist/io/xlsx/pivot.d.ts +5 -5
  133. package/dist/io/xlsx/pivot.js +23 -22
  134. package/dist/io/xlsx/preserved-splices.d.ts +6 -0
  135. package/dist/io/xlsx/preserved-splices.js +74 -0
  136. package/dist/io/xlsx/read-comments.d.ts +29 -0
  137. package/dist/io/xlsx/read-comments.js +75 -0
  138. package/dist/io/xlsx/read-parts.d.ts +2 -2
  139. package/dist/io/xlsx/read-parts.js +44 -37
  140. package/dist/io/xlsx/read-pivot.js +9 -3
  141. package/dist/io/xlsx/read-rich-runs.d.ts +8 -5
  142. package/dist/io/xlsx/read-rich-runs.js +18 -1
  143. package/dist/io/xlsx/read-rows.d.ts +18 -7
  144. package/dist/io/xlsx/read-rows.js +47 -37
  145. package/dist/io/xlsx/read-styles.d.ts +0 -25
  146. package/dist/io/xlsx/read-styles.js +39 -34
  147. package/dist/io/xlsx/read-workbook-xml.d.ts +6 -0
  148. package/dist/io/xlsx/read-workbook-xml.js +9 -4
  149. package/dist/io/xlsx/read-worksheet.d.ts +8 -3
  150. package/dist/io/xlsx/read-worksheet.js +45 -23
  151. package/dist/io/xlsx/read.d.ts +0 -4
  152. package/dist/io/xlsx/read.js +17 -21
  153. package/dist/io/xlsx/relationships.d.ts +3 -0
  154. package/dist/io/xlsx/relationships.js +4 -0
  155. package/dist/io/xlsx/row-position.d.ts +3 -1
  156. package/dist/io/xlsx/row-xml.d.ts +18 -7
  157. package/dist/io/xlsx/row-xml.js +54 -35
  158. package/dist/io/xlsx/sheet-properties.js +68 -42
  159. package/dist/io/xlsx/strict-parts.d.ts +7 -0
  160. package/dist/io/xlsx/strict-parts.js +171 -0
  161. package/dist/io/xlsx/style-elements.d.ts +0 -2
  162. package/dist/io/xlsx/style-elements.js +9 -29
  163. package/dist/io/xlsx/styles.d.ts +5 -0
  164. package/dist/io/xlsx/styles.js +11 -5
  165. package/dist/io/xlsx/tables.d.ts +10 -2
  166. package/dist/io/xlsx/tables.js +49 -42
  167. package/dist/io/xlsx/theme-xml.d.ts +6 -0
  168. package/dist/io/xlsx/theme-xml.js +16 -19
  169. package/dist/io/xlsx/threaded-comments.d.ts +6 -1
  170. package/dist/io/xlsx/threaded-comments.js +14 -7
  171. package/dist/io/xlsx/workbook-xml.d.ts +13 -24
  172. package/dist/io/xlsx/workbook-xml.js +15 -33
  173. package/dist/io/xlsx/worksheet-xml.d.ts +5 -2
  174. package/dist/io/xlsx/worksheet-xml.js +37 -69
  175. package/dist/io/xlsx/write-stream.d.ts +28 -6
  176. package/dist/io/xlsx/write-stream.js +42 -17
  177. package/dist/io/xlsx/write.d.ts +11 -4
  178. package/dist/io/xlsx/write.js +153 -90
  179. package/dist/rel-type.d.ts +9 -0
  180. package/dist/rel-type.js +3 -0
  181. package/dist/vba/bytes.d.ts +2 -0
  182. package/dist/vba/bytes.js +11 -0
  183. package/dist/vba/cfb-format.d.ts +47 -0
  184. package/dist/vba/cfb-format.js +34 -0
  185. package/dist/vba/cfb-writer.d.ts +1 -2
  186. package/dist/vba/cfb-writer.js +33 -36
  187. package/dist/vba/cfb.d.ts +9 -4
  188. package/dist/vba/cfb.js +55 -43
  189. package/dist/vba/dir-records.d.ts +6 -0
  190. package/dist/vba/dir-records.js +10 -2
  191. package/dist/vba/errors.d.ts +4 -4
  192. package/dist/vba/index.d.ts +0 -2
  193. package/dist/vba/index.js +0 -2
  194. package/dist/vba/ms-ovba.d.ts +6 -2
  195. package/dist/vba/ms-ovba.js +15 -6
  196. package/dist/vba/project-editor.js +102 -87
  197. package/dist/vba/project.d.ts +4 -0
  198. package/dist/vba/project.js +12 -6
  199. package/dist/vba/vba-encoding.d.ts +4 -10
  200. package/dist/vba/vba-encoding.js +2 -15
  201. package/dist/xml/xml-read.d.ts +29 -1
  202. package/dist/xml/xml-read.js +56 -0
  203. package/dist/xml/xml-scan.d.ts +13 -0
  204. package/dist/xml/xml-scan.js +58 -7
  205. package/dist/xml/xml.d.ts +33 -1
  206. package/dist/xml/xml.js +22 -0
  207. package/package.json +5 -4
  208. package/skills/ts-xlsx-upstream/SKILL.md +21 -13
  209. package/dist/core/modern-functions.d.ts +0 -1
  210. package/dist/io/xlsx/cell-style-resolution.d.ts +0 -25
  211. package/dist/io/xlsx/cell-style-resolution.js +0 -29
  212. package/dist/io/xlsx/column-budget.d.ts +0 -26
  213. package/dist/io/xlsx/read-repair.d.ts +0 -26
  214. package/dist/io/xlsx/read-repair.js +0 -39
package/README.md CHANGED
@@ -16,7 +16,7 @@ A TypeScript-first library for reading and writing spreadsheet documents
16
16
  > and has been rebuilt from the ground up into a modern, strict-TypeScript library.
17
17
  > **It carries no backwards-compatibility guarantee with ExcelJS** — the API below is
18
18
  > its own, not a drop-in. From 1.0.0 onward it follows
19
- > [SemVer](https://semver.org/) against *itself*; see the [changelog](CHANGELOG.md).
19
+ > [SemVer](https://semver.org/) against _itself_; see the [changelog](CHANGELOG.md).
20
20
  > See [`CLAUDE.md`](CLAUDE.md) for the goals and [`docs/architecture.md`](docs/architecture.md)
21
21
  > for the design, and [migrating from ExcelJS](docs/migrating-from-exceljs.md) if you are coming across.
22
22
 
@@ -32,7 +32,7 @@ shim. It is a different, better library that happens to share ancestry.
32
32
  What that buys you today:
33
33
 
34
34
  - **Strict, precise types are the contract.** `strict` + `noUncheckedIndexedAccess` +
35
- `exactOptionalPropertyTypes`. The published `.d.ts` *is* the documentation, and the
35
+ `exactOptionalPropertyTypes`. The published `.d.ts` _is_ the documentation, and the
36
36
  [API reference](docs/api/README.md) is generated straight from it.
37
37
  - **Synchronous, buffer-native I/O.** `readXlsx`/`writeXlsx` take and return a
38
38
  `Uint8Array` — no streams to await for the buffered path, no Node `Buffer` assumption,
@@ -98,8 +98,10 @@ const wb2 = readXlsx(readFileSync('people.xlsx')); // a Buffer is a Uint8Array
98
98
  `column.width = 12`, `column.key = 'name'`), and cells are reachable from it:
99
99
  `row.getCell('B')`, `row.cells`, `row.values`.
100
100
  - **`Cell`** — one cell. `cell.value` is the whole story: a `number`, `string`,
101
- `boolean`, `Date`, `null` (empty), a formula (`{formula, result}`), rich text, a
102
- hyperlink, or an error — all precisely typed as [`CellValue`](docs/api/cell-values.md).
101
+ `boolean`, `Date`, `null` (empty), a formula (`{formula, result}`), rich text, or an
102
+ error — all precisely typed as [`CellValue`](docs/api/cell-values.md). A hyperlink is not
103
+ a value: it sits on the sheet, over any cell, through
104
+ `sheet.addHyperlink({ref: 'B2', target: 'https://example.com'})`.
103
105
 
104
106
  ```ts
105
107
  sheet.getRow(1).height = 20;
@@ -112,7 +114,7 @@ for (const row of sheet.rows()) {
112
114
  }
113
115
  ```
114
116
 
115
- `Row` and `Column` are *handles*, not snapshots: they read and write straight through to the
117
+ `Row` and `Column` are _handles_, not snapshots: they read and write straight through to the
116
118
  sheet, so two handles on the same line always agree, and reading one creates nothing — asking
117
119
  about row 500 costs nothing and does not extend the used range. Position is fixed, exactly as a
118
120
  `Cell`'s is: after a splice, `getRow(3)` still means row 3, now holding whatever moved there.
@@ -129,11 +131,13 @@ no API to author a new one — see [`docs/api/preserved.md`](docs/api/preserved.
129
131
 
130
132
  ```ts
131
133
  import {
132
- readXlsx, writeXlsx, // buffered .xlsx (Uint8Array ⇄ Workbook)
133
- readSheetRows, // stream one sheet's rows, bounded memory
134
- readWorkbookStream, // stream every sheet, rows one at a time
135
- readCsv, writeCsv, // CSV as Uint8Array
136
- writeCsvText, // CSV as a string
134
+ readXlsx,
135
+ writeXlsx, // buffered .xlsx (Uint8Array Workbook)
136
+ readSheetRows, // stream one sheet's rows, bounded memory
137
+ readWorkbookStream, // stream every sheet, rows one at a time
138
+ readCsv,
139
+ writeCsv, // CSV as Uint8Array
140
+ writeCsvText, // CSV as a string
137
141
  } from '@shbernal/ts-xlsx';
138
142
  // The streaming writer opens files and pipes Node streams, so it is the one Node-only face
139
143
  // and lives behind its own subpath — which is what keeps everything above browser-safe:
@@ -141,7 +145,10 @@ import {WorkbookStreamWriter} from '@shbernal/ts-xlsx/node';
141
145
 
142
146
  // Bounded-memory extraction — the whole workbook is never materialized:
143
147
  for (const row of readSheetRows(bytes, {sheet: 'People'})) {
144
- console.log(row.number, row.cells.map((c) => c.value));
148
+ console.log(
149
+ row.number,
150
+ row.cells.map((c) => c.value),
151
+ );
145
152
  }
146
153
 
147
154
  // Bounded-memory generation — commit each row to serialize and free it as you go:
@@ -194,17 +201,17 @@ The bare package name gives you everything, and with a bundler that is the right
194
201
  are for when you'd rather the module graph itself said which half of the library you depend
195
202
  on — a Lambda with no bundler, a service that only classifies failures:
196
203
 
197
- | Import from | You get | It loads |
198
- | --- | --- | --- |
199
- | `@shbernal/ts-xlsx` | everything except the streaming writer | 501 KB |
200
- | `@shbernal/ts-xlsx/core` | `Workbook`, `Worksheet`, `Cell`, styles, values, addresses | 185 KB |
201
- | `@shbernal/ts-xlsx/xlsx` | `readXlsx`, `writeXlsx`/`writeXlsxAsync`, the streaming reader, VBA part edits | 490 KB |
202
- | `@shbernal/ts-xlsx/xlsb` | `readXlsb` | 256 KB |
203
- | `@shbernal/ts-xlsx/csv` | `readCsv`, `writeCsv`, `writeCsvText` | 191 KB |
204
- | `@shbernal/ts-xlsx/node` | `WorkbookStreamWriter` and the rest of the streaming writer | 358 KB |
205
- | `@shbernal/ts-xlsx/vba` | `parseVbaProject`, `addVbaReference`, `removeVbaModule` | 46 KB |
206
- | `@shbernal/ts-xlsx/customui` | `parseCustomUi` and the ribbon types | 15 KB |
207
- | `@shbernal/ts-xlsx/errors` | every error class the library throws | 3 KB |
204
+ | Import from | You get | It loads |
205
+ | ---------------------------- | ------------------------------------------------------------------------------ | -------- |
206
+ | `@shbernal/ts-xlsx` | everything except the streaming writer | 501 KB |
207
+ | `@shbernal/ts-xlsx/core` | `Workbook`, `Worksheet`, `Cell`, styles, values, addresses | 185 KB |
208
+ | `@shbernal/ts-xlsx/xlsx` | `readXlsx`, `writeXlsx`/`writeXlsxAsync`, the streaming reader, VBA part edits | 490 KB |
209
+ | `@shbernal/ts-xlsx/xlsb` | `readXlsb` | 256 KB |
210
+ | `@shbernal/ts-xlsx/csv` | `readCsv`, `writeCsv`, `writeCsvText` | 191 KB |
211
+ | `@shbernal/ts-xlsx/node` | `WorkbookStreamWriter` and the rest of the streaming writer | 358 KB |
212
+ | `@shbernal/ts-xlsx/vba` | `parseVbaProject`, `addVbaReference`, `removeVbaModule` | 46 KB |
213
+ | `@shbernal/ts-xlsx/customui` | `parseCustomUi` and the ribbon types | 15 KB |
214
+ | `@shbernal/ts-xlsx/errors` | every error class the library throws | 3 KB |
208
215
 
209
216
  `/node` is the only one the root specifier does not carry, and the reason is its imports
210
217
  rather than its size: it reaches `node:fs` and `node:stream`, and every other entry reaches
@@ -234,7 +241,7 @@ pnpm run docs
234
241
  Read **[migrating from ExcelJS](docs/migrating-from-exceljs.md)**. The short version: the
235
242
  shapes are deliberately different (synchronous `Uint8Array` I/O instead of async
236
243
  `Buffer`/stream methods, `readXlsx`/`writeXlsx` free functions instead of
237
- `workbook.xlsx.*`), because the goal was the *right* API, not the familiar one.
244
+ `workbook.xlsx.*`), because the goal was the _right_ API, not the familiar one.
238
245
 
239
246
  ## Hit a bug? There is a skill for that
240
247
 
package/dist/bytes.d.ts CHANGED
@@ -18,8 +18,9 @@ export declare function concat(chunks: readonly Uint8Array[], size?: number): Ui
18
18
  * Spelled out rather than delegated because both platform routes are unavailable here: `Buffer` is
19
19
  * a Node global, which is exactly what a browser-safe module may not reach for, and `btoa` is
20
20
  * declared deprecated in Node's types (so the `no-deprecated` lint rule rejects it) and takes a
21
- * binary string rather than bytes anyway. The one caller encodes a 16-byte salt and a 64-byte
22
- * hash, so the loop below is not on any path where its cost is measurable.
21
+ * binary string rather than bytes anyway. The callers encode a 16-byte salt and a 64-byte hash, a
22
+ * sheet password's when one is set and a binary sheet's credential when one is read, so the loop below
23
+ * is not on any path where its cost is measurable.
23
24
  */
24
25
  export declare function toBase64(bytes: Uint8Array): string;
25
26
  /** A string's UTF-16LE bytes, code unit by code unit, with lone surrogates carried through. */
@@ -106,11 +106,18 @@ export declare function encodeCornerRef(col: number | undefined, row: number | u
106
106
  * one of those sites.
107
107
  */
108
108
  export declare function encodeRect(rect: GridRect): string;
109
+ /**
110
+ * A bounded rectangle as a formula's `ref` spells it: {@link encodeRect}'s `"B1:B3"`, except that one
111
+ * cell is its bare address, `"B1"`. Excel writes an array formula over a single cell that way, and a
112
+ * range that moves keeps the spelling it would be saved with.
113
+ */
114
+ export declare function encodeRange(rect: GridRect): string;
109
115
  /**
110
116
  * Decode a single cell/row/column reference into `{address, col, row}`. Anchoring
111
117
  * `$` signs are accepted and dropped; an absent axis is `undefined`.
112
118
  *
113
119
  * @throws {SyntaxError} if the reference mentions neither a column nor a row.
120
+ * @throws {RangeError} if it names a column past `XFD` or a row outside `1..1048576`.
114
121
  */
115
122
  export declare function decodeAddress(reference: string): CellAddress;
116
123
  /**
@@ -127,6 +134,7 @@ export interface CellPosition {
127
134
  * Decode a reference that must name a single cell. Anchoring `$` signs are accepted and dropped.
128
135
  *
129
136
  * @throws {SyntaxError} if the reference is unparseable, or parses but omits an axis (`"A"`, `"1"`).
137
+ * @throws {RangeError} if it names a position off the grid (`"A0"`, `"XFE1"`).
130
138
  */
131
139
  export declare function decodeCellRef(reference: string): CellPosition;
132
140
  /**
@@ -160,10 +168,20 @@ export declare function boundedRect(range: RangeAddress): GridRect | undefined;
160
168
  * range is unbounded, not unreadable, and a caller that needs a bounded rectangle says so itself.
161
169
  */
162
170
  export declare function tryDecodeRange(reference: string): RangeAddress | undefined;
171
+ /**
172
+ * The rectangle `reference` names when its top-left cell is (`col`, `row`), or `undefined` for a
173
+ * reference that does not parse, is unbounded, names a sheet, or starts at another cell. That is the
174
+ * shape of the range an array formula fills: Excel states the formula on the cell the range starts at,
175
+ * so a `ref` read from a file and one handed to the writer are both held to it.
176
+ */
177
+ export declare function tryDecodeAnchoredRange(reference: string, col: number, row: number): GridRect | undefined;
163
178
  /**
164
179
  * Decode a range reference (`A1:B2`, `$1:$1`, `Sheet1!$A:$A`) into its corners and
165
180
  * canonical dimensions. A single reference collapses to a degenerate range whose
166
181
  * corners coincide.
182
+ *
183
+ * @throws {SyntaxError} if an endpoint is unparseable.
184
+ * @throws {RangeError} if an endpoint names a column past `XFD` or a row outside `1..1048576`.
167
185
  */
168
186
  export declare function decodeRange(reference: string): RangeAddress;
169
187
  /**
@@ -174,3 +192,21 @@ export declare function decodeRange(reference: string): RangeAddress;
174
192
  * address naming a row Excel has no reference for while `encodeAddress(16385, 1)` refused.
175
193
  */
176
194
  export declare function encodeAddress(col: number, row: number): string;
195
+ /**
196
+ * Does Excel read this name as a reference, where it expects a name? Such a name must be quoted as a
197
+ * sheet prefix (`'R1C1'!A1`), and cannot name a table at all.
198
+ *
199
+ * Four shapes, case-insensitive, all found by asking Excel (16.0 build 20326) rather than read off a
200
+ * specification, which states the rule as "not the same as a cell reference, and not `C` or `R`" and
201
+ * leaves the edges to the implementation:
202
+ *
203
+ * - an A1 cell **on the grid**: `T1`, `a01`, `XFD1048576`, but not `XFE1` or `A0`;
204
+ * - a whole R1C1 reference, each number optional and on the grid: `R`, `C`, `RC`, `R1C`, `R1C1`;
205
+ * - an R1C1 row or column number followed by a letter or underscore: `R1X`, `C1X`, `R1C1_`, but
206
+ * not `R1.5`, nor `RCX`, which carries no number;
207
+ * - `TRUE` or `FALSE`.
208
+ *
209
+ * Every name tried both ways was quoted in a formula exactly when a table by that name made Excel
210
+ * offer to repair the package, so the two rules are one.
211
+ */
212
+ export declare function nameReadsAsReference(name: string): boolean;
@@ -57,6 +57,11 @@ export function encodeCornerRef(col, row) {
57
57
  export function encodeRect(rect) {
58
58
  return `${encodeAddress(rect.left, rect.top)}:${encodeAddress(rect.right, rect.bottom)}`;
59
59
  }
60
+ export function encodeRange(rect) {
61
+ return rect.top === rect.bottom && rect.left === rect.right
62
+ ? encodeAddress(rect.left, rect.top)
63
+ : encodeRect(rect);
64
+ }
60
65
  function makeCellAddress(col, row) {
61
66
  return { address: encodeCornerRef(col, row), col, row };
62
67
  }
@@ -72,6 +77,8 @@ export function decodeAddress(reference) {
72
77
  }
73
78
  const col = letters.length > 0 ? columnToNumber(letters) : undefined;
74
79
  const row = digits.length > 0 ? Number.parseInt(digits, 10) : undefined;
80
+ if (row !== undefined)
81
+ assertRowInBounds(row);
75
82
  return { address: `${letters}${digits}`, col, row };
76
83
  }
77
84
  export function decodeCellRef(reference) {
@@ -81,18 +88,13 @@ export function decodeCellRef(reference) {
81
88
  }
82
89
  return { col, row };
83
90
  }
84
- function rowCanExist(row) {
85
- return row === undefined || (Number.isInteger(row) && row >= 1 && row <= MAX_ROW);
86
- }
87
91
  export function tryDecodeCellRef(reference) {
88
- let position;
89
92
  try {
90
- position = decodeCellRef(reference);
93
+ return decodeCellRef(reference);
91
94
  }
92
95
  catch {
93
96
  return undefined;
94
97
  }
95
- return rowCanExist(position.row) ? position : undefined;
96
98
  }
97
99
  export function boundedRect(range) {
98
100
  const { top, left, bottom, right } = range;
@@ -102,14 +104,19 @@ export function boundedRect(range) {
102
104
  return { top, left, bottom, right };
103
105
  }
104
106
  export function tryDecodeRange(reference) {
105
- let range;
106
107
  try {
107
- range = decodeRange(reference);
108
+ return decodeRange(reference);
108
109
  }
109
110
  catch {
110
111
  return undefined;
111
112
  }
112
- return rowCanExist(range.top) && rowCanExist(range.bottom) ? range : undefined;
113
+ }
114
+ export function tryDecodeAnchoredRange(reference, col, row) {
115
+ const range = tryDecodeRange(reference);
116
+ if (range === undefined || range.sheetName !== undefined)
117
+ return undefined;
118
+ const rect = boundedRect(range);
119
+ return rect !== undefined && rect.left === col && rect.top === row ? rect : undefined;
113
120
  }
114
121
  export function decodeRange(reference) {
115
122
  const prefix = SHEET_PREFIX.exec(reference);
@@ -146,3 +153,34 @@ export function encodeAddress(col, row) {
146
153
  assertRowInBounds(row);
147
154
  return `${numberToColumn(col)}${row}`;
148
155
  }
156
+ function isLineNumber(digits, bound) {
157
+ const n = Number(digits);
158
+ return n >= 1 && n <= bound;
159
+ }
160
+ const A1_NAME = /^([A-Za-z]{1,3})(\d+)$/;
161
+ const R1C1_NAME = /^(?:R(\d*)C(\d*)|R(\d*)|C(\d*))$/i;
162
+ const R1C1_PREFIX = /^(?:R(\d+)|C(\d+))[A-Za-z_]/i;
163
+ export function nameReadsAsReference(name) {
164
+ const a1 = A1_NAME.exec(name);
165
+ if (a1 !== null) {
166
+ const [, letters = '', digits = ''] = a1;
167
+ if (tryColumnToNumber(letters.toUpperCase()) !== undefined && isLineNumber(digits, MAX_ROW)) {
168
+ return true;
169
+ }
170
+ }
171
+ const r1c1 = R1C1_NAME.exec(name);
172
+ if (r1c1 !== null) {
173
+ const [, row, column, rowOnly, columnOnly] = r1c1;
174
+ const on = (digits, bound) => digits === undefined || digits === '' || isLineNumber(digits, bound);
175
+ if (on(row ?? rowOnly, MAX_ROW) && on(column ?? columnOnly, MAX_COLUMN))
176
+ return true;
177
+ }
178
+ const prefix = R1C1_PREFIX.exec(name);
179
+ if (prefix !== null) {
180
+ const [, row, column] = prefix;
181
+ if (row !== undefined ? isLineNumber(row, MAX_ROW) : isLineNumber(column ?? '', MAX_COLUMN)) {
182
+ return true;
183
+ }
184
+ }
185
+ return /^(?:TRUE|FALSE)$/i.test(name);
186
+ }
@@ -0,0 +1,33 @@
1
+ import { type CellValue } from './value.ts';
2
+ /**
3
+ * Where a cell holding `value` places a formula, or `undefined` when the value is no formula. A shared
4
+ * formula's clone and a data table's cell count: each is a formula Excel will not find inside another
5
+ * array formula's range.
6
+ */
7
+ export declare function formulaPlacement(address: string, col: number, row: number, value: CellValue): FormulaPlacement | undefined;
8
+ /** A cell holding a formula of any kind, with the range it fills when it is an array formula. */
9
+ export interface FormulaPlacement {
10
+ readonly address: string;
11
+ readonly col: number;
12
+ readonly row: number;
13
+ /** The array formula's `ref`, as its value states it; absent for any other formula. */
14
+ readonly arrayRef?: string | undefined;
15
+ }
16
+ /** An array formula whose range holds another formula's cell, or shares a cell with a range kept. */
17
+ export interface ArrayRangeConflict {
18
+ readonly array: FormulaPlacement;
19
+ /** The address of the other formula in the range, or of the array formula whose range it overlaps. */
20
+ readonly other: string;
21
+ readonly kind: 'formula-inside' | 'ranges-overlap';
22
+ }
23
+ /**
24
+ * The array formulas that cannot stand, deciding in reading order: an array formula conflicts when
25
+ * another formula's cell lies in its range, or when its range shares a cell with the range of an
26
+ * array formula decided before it that did not conflict. The array formulas left over hold ranges
27
+ * that share no cell with each other and hold no formula but their own, so reading each conflict as
28
+ * its plain formula leaves a sheet the writer accepts.
29
+ *
30
+ * A `ref` that names no bounded range starting at its own cell fills no cells here; the reader never
31
+ * produces one and the writer refuses it on its own terms.
32
+ */
33
+ export declare function arrayRangeConflicts(placements: Iterable<FormulaPlacement>): ArrayRangeConflict[];
@@ -0,0 +1,62 @@
1
+ import { tryDecodeAnchoredRange } from './address.js';
2
+ import { MergeIndex } from './merge-index.js';
3
+ import { isArrayFormulaValue, isDataTableFormulaValue, isFormulaValue, isSharedFormulaValue, } from './value.js';
4
+ export function formulaPlacement(address, col, row, value) {
5
+ if (isArrayFormulaValue(value))
6
+ return { address, col, row, arrayRef: value.ref };
7
+ if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
8
+ return { address, col, row };
9
+ }
10
+ return undefined;
11
+ }
12
+ export function arrayRangeConflicts(placements) {
13
+ const all = [...placements];
14
+ const cellRects = [];
15
+ const placementOfCell = new Map();
16
+ const arrays = [];
17
+ for (const placement of all) {
18
+ const cell = {
19
+ top: placement.row,
20
+ left: placement.col,
21
+ bottom: placement.row,
22
+ right: placement.col,
23
+ };
24
+ cellRects.push(cell);
25
+ placementOfCell.set(cell, placement);
26
+ const range = placement.arrayRef === undefined
27
+ ? undefined
28
+ : tryDecodeAnchoredRange(placement.arrayRef, placement.col, placement.row);
29
+ if (range !== undefined)
30
+ arrays.push({ placement, range });
31
+ }
32
+ arrays.sort((a, b) => a.placement.row - b.placement.row || a.placement.col - b.placement.col);
33
+ const cells = new MergeIndex(cellRects);
34
+ const keptRanges = [];
35
+ const kept = new MergeIndex(keptRanges);
36
+ const placementOfRange = new Map();
37
+ const conflicts = [];
38
+ for (const { placement, range } of arrays) {
39
+ const inside = formulaInside(range, cells);
40
+ if (inside !== undefined) {
41
+ const other = placementOfCell.get(inside)?.address ?? '';
42
+ conflicts.push({ array: placement, other, kind: 'formula-inside' });
43
+ continue;
44
+ }
45
+ const overlapped = kept.overlapping(range);
46
+ if (overlapped !== undefined) {
47
+ const other = placementOfRange.get(overlapped)?.address ?? '';
48
+ conflicts.push({ array: placement, other, kind: 'ranges-overlap' });
49
+ continue;
50
+ }
51
+ keptRanges.push(range);
52
+ placementOfRange.set(range, placement);
53
+ kept.note(range);
54
+ }
55
+ return conflicts;
56
+ }
57
+ function formulaInside(range, cells) {
58
+ const below = { ...range, top: range.top + 1 };
59
+ const beside = { ...range, bottom: range.top, left: range.left + 1 };
60
+ return ((below.top <= below.bottom ? cells.overlapping(below) : undefined) ??
61
+ (beside.left <= beside.right ? cells.overlapping(beside) : undefined));
62
+ }
@@ -1,6 +1,6 @@
1
1
  import { hex } from '../hex.js';
2
2
  import { parseArgb } from './style.js';
3
- import { DEFAULT_THEME_COLOR_SCHEME, THEME_COLOR_SLOTS } from './theme.js';
3
+ import { DEFAULT_THEME_COLOR_SCHEME, isThemeRgb, THEME_COLOR_SLOTS, } from './theme.js';
4
4
  export const DEFAULT_INDEXED_COLORS = [
5
5
  '00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', '0000FFFF',
6
6
  '00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', '0000FFFF',
@@ -32,7 +32,7 @@ function resolveBase(color, context) {
32
32
  if (slot === undefined)
33
33
  return undefined;
34
34
  const value = (context.theme ?? DEFAULT_THEME_COLOR_SCHEME)[slot];
35
- return value === undefined ? undefined : `FF${value.toUpperCase()}`;
35
+ return value !== undefined && isThemeRgb(value) ? `FF${value.toUpperCase()}` : undefined;
36
36
  }
37
37
  if (color.indexed !== undefined) {
38
38
  if (SYSTEM_INDEXED_COLORS.has(color.indexed))
@@ -1,4 +1,4 @@
1
- import { assertColumnInBounds, encodeAddress, numberToColumn } from './address.js';
1
+ import { assertColumnInBounds, assertRowInBounds, numberToColumn } from './address.js';
2
2
  import { AxisHandle } from './axis-handle.js';
3
3
  import { INTERNAL } from './internal.js';
4
4
  export class Column extends AxisHandle {
@@ -92,7 +92,8 @@ export class Column extends AxisHandle {
92
92
  this.write('protection', protection);
93
93
  }
94
94
  getCell(row) {
95
- return this.#sheet.getCell(encodeAddress(this.index, row));
95
+ assertRowInBounds(row);
96
+ return this.#sheet[INTERNAL].masterAt(row, this.index);
96
97
  }
97
98
  get cells() {
98
99
  return this.#sheet[INTERNAL].columnCells(this.index);
@@ -55,11 +55,15 @@ export interface Comment {
55
55
  /** Brace-wrapped GUID identifying this message, preserved verbatim from the file. */
56
56
  readonly id: string;
57
57
  /**
58
- * Who wrote it, resolved through the workbook registry. Absent when the file recorded no author or
59
- * named an id the registry does not hold; {@link personId} distinguishes those two cases.
58
+ * Who wrote it, resolved through the workbook registry. Absent when the file recorded no author, or
59
+ * named an id the registry does not hold, which Excel treats the same way.
60
60
  */
61
61
  readonly author?: Person;
62
- /** The author's {@link Person.id} exactly as written; absent when the file recorded no author. */
62
+ /**
63
+ * The author's {@link Person.id}, which must be registered ({@link Workbook.addPerson}) by the time
64
+ * the workbook is written. Absent for a message with no recorded author, which is written under the
65
+ * null GUID Excel uses for one and shown by Excel as "Author".
66
+ */
63
67
  readonly personId?: string;
64
68
  /**
65
69
  * When it was written, verbatim. Excel writes local wall-clock with fractional seconds and no
@@ -7,6 +7,10 @@ export declare class ConditionalFormattingOverlay {
7
7
  * range (`"A1:A10"`), a whole column, or several space-separated areas (`"A1:C1 A3:C3"`) sharing one
8
8
  * rule set. The block is stored once against the range, defensively copied so the getter never hands
9
9
  * back a reference into the caller's object.
10
+ *
11
+ * @throws {AuthoringError} when `formatting.ref` names no area at all. A rule set attached to
12
+ * nothing formats no cell and is written back as the same unreadable text, so it is a mistake worth
13
+ * surfacing at the call. The reader does not reach this: it drops such a block at its own boundary.
10
14
  */
11
15
  add(formatting: ConditionalFormatting): void;
12
16
  /** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
@@ -16,6 +20,11 @@ export declare class ConditionalFormattingOverlay {
16
20
  * it was written for. A rule set whose every target area fell inside a deleted span goes with them.
17
21
  */
18
22
  shift(splice: AxisSplice): void;
23
+ /**
24
+ * Rewrite every rule's formulae, and the value of each scale anchor whose type is `formula`, through
25
+ * `rewrite`, keeping a rule set nothing in which changed as the same object.
26
+ */
27
+ mapFormulas(rewrite: (formula: string) => string): void;
19
28
  /** Drop every conditional formatting, leaving the overlay empty. */
20
29
  clear(): void;
21
30
  }
@@ -1,9 +1,13 @@
1
+ import { AuthoringError, quoted } from '../errors.js';
1
2
  import { cloneConditionalFormatting } from './conditional-formatting.js';
2
3
  import { replaceContents } from './containers.js';
3
- import { shiftSqref } from './merge.js';
4
+ import { decodeSqrefRects, shiftSqref } from './merge.js';
4
5
  export class ConditionalFormattingOverlay {
5
6
  #entries = [];
6
7
  add(formatting) {
8
+ if (decodeSqrefRects(formatting.ref).length === 0) {
9
+ throw new AuthoringError(`conditional formatting range ${quoted(formatting.ref)} names no cells`);
10
+ }
7
11
  this.#entries.push(cloneConditionalFormatting(formatting));
8
12
  }
9
13
  get entries() {
@@ -18,6 +22,29 @@ export class ConditionalFormattingOverlay {
18
22
  }
19
23
  replaceContents(this.#entries, entries);
20
24
  }
25
+ mapFormulas(rewrite) {
26
+ for (const [index, entry] of this.#entries.entries()) {
27
+ let changed = false;
28
+ const rules = entry.rules.map((rule) => {
29
+ const formulae = rule.formulae?.map((operand) => typeof operand === 'string' ? rewrite(operand) : operand);
30
+ const cfvo = rule.cfvo?.map((anchor) => anchor.type === 'formula' && typeof anchor.value === 'string'
31
+ ? { ...anchor, value: rewrite(anchor.value) }
32
+ : anchor);
33
+ const formulaeMoved = formulae?.some((operand, i) => operand !== rule.formulae?.[i]) ?? false;
34
+ const cfvoMoved = cfvo?.some((anchor, i) => anchor.value !== rule.cfvo?.[i]?.value) ?? false;
35
+ if (!formulaeMoved && !cfvoMoved)
36
+ return rule;
37
+ changed = true;
38
+ return {
39
+ ...rule,
40
+ ...(formulae === undefined ? {} : { formulae }),
41
+ ...(cfvo === undefined ? {} : { cfvo }),
42
+ };
43
+ });
44
+ if (changed)
45
+ this.#entries[index] = { ...entry, rules };
46
+ }
47
+ }
21
48
  clear() {
22
49
  this.#entries.length = 0;
23
50
  }