@shbernal/ts-xlsx 2.1.0 → 3.1.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 (225) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +5 -1
  16. package/dist/core/column.js +34 -35
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +9 -3
  27. package/dist/core/data-validation.js +21 -18
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +14 -10
  39. package/dist/core/image.js +9 -9
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
package/README.md CHANGED
@@ -149,12 +149,15 @@ const writer = new WorkbookStreamWriter();
149
149
  const out = writer.addWorksheet('Big');
150
150
  for (let i = 1; i <= 1_000_000; i++) out.addRow([i, i * i]).commit();
151
151
  out.commit();
152
- const packaged: Uint8Array = await writer.commit(); // also delivered via writer.stream
152
+ const packaged = await writer.commit(); // Uint8Array here; also delivered via writer.stream
153
153
  ```
154
154
 
155
155
  The streaming writer is asynchronous where the buffered path is synchronous: `commit()`
156
156
  resolves to the package bytes and simultaneously pipes them through `writer.stream` (a Node
157
157
  `Readable`), so `writer.stream.pipe(res)` streams a workbook straight to an HTTP response.
158
+ Hand the writer its own sink instead (`{stream}` or `{filename}`) and never touch
159
+ `writer.stream`, and `commit()` resolves with `undefined`: the archive goes to the sink and is
160
+ never assembled as one object, which is what passing a sink was for.
158
161
 
159
162
  ## Writing without blocking the event loop
160
163
 
package/dist/bytes.d.ts CHANGED
@@ -22,3 +22,10 @@ export declare function concat(chunks: readonly Uint8Array[], size?: number): Ui
22
22
  * hash, so the loop below is not on any path where its cost is measurable.
23
23
  */
24
24
  export declare function toBase64(bytes: Uint8Array): string;
25
+ /** A string's UTF-16LE bytes, code unit by code unit, with lone surrogates carried through. */
26
+ export declare function utf16leBytes(text: string): Uint8Array;
27
+ /**
28
+ * Decode UTF-16LE code units. A trailing odd byte is dropped: these fields are length-prefixed by
29
+ * their producer, and half a code unit carries nothing to decode.
30
+ */
31
+ export declare function decodeUtf16le(bytes: Uint8Array): string;
package/dist/bytes.js CHANGED
@@ -32,3 +32,25 @@ export function toBase64(bytes) {
32
32
  }
33
33
  return out;
34
34
  }
35
+ export function utf16leBytes(text) {
36
+ const bytes = new Uint8Array(text.length * 2);
37
+ for (let i = 0; i < text.length; i++) {
38
+ const unit = text.charCodeAt(i);
39
+ bytes[i * 2] = unit & 0xff;
40
+ bytes[i * 2 + 1] = unit >>> 8;
41
+ }
42
+ return bytes;
43
+ }
44
+ const CHARS_PER_BATCH = 4096;
45
+ export function decodeUtf16le(bytes) {
46
+ let text = '';
47
+ let batch = [];
48
+ for (let i = 0; i + 1 < bytes.length; i += 2) {
49
+ batch.push(bytes[i] | (bytes[i + 1] << 8));
50
+ if (batch.length === CHARS_PER_BATCH) {
51
+ text += String.fromCharCode(...batch);
52
+ batch = [];
53
+ }
54
+ }
55
+ return batch.length > 0 ? text + String.fromCharCode(...batch) : text;
56
+ }
@@ -2,6 +2,22 @@
2
2
  export declare const MAX_COLUMN = 16384;
3
3
  /** Excel's row bound: 1 through 1048576. The other axis of {@link MAX_COLUMN}. */
4
4
  export declare const MAX_ROW = 1048576;
5
+ /**
6
+ * The same two bounds, zero-based: the last valid *index* on each axis.
7
+ *
8
+ * BIFF12 counts from zero throughout, so its reader and its formula decoder both need these, and both
9
+ * were deriving them privately. Two derivations of one fact is one fewer than three, but it is still
10
+ * the shape where a change to a limit reaches some of its consequences. The subtraction belongs beside
11
+ * the number it subtracts from.
12
+ */
13
+ export declare const MAX_COLUMN_INDEX: number;
14
+ export declare const MAX_ROW_INDEX: number;
15
+ /**
16
+ * Refuse a coordinate that names no position on the grid.
17
+ *
18
+ * @throws {RangeError} unless `value` is an integer within the axis's bounds.
19
+ */
20
+ export declare function assertAxisInBounds(axis: 'row' | 'column', value: number): void;
5
21
  /** @throws {RangeError} unless `n` is an integer in `1..MAX_COLUMN`. */
6
22
  export declare function assertColumnInBounds(n: number): void;
7
23
  /** @throws {RangeError} unless `n` is an integer in `1..MAX_ROW`. */
@@ -56,7 +72,19 @@ export interface RangeAddress {
56
72
  *
57
73
  * @throws {RangeError} unless `n` is an integer in `1..MAX_COLUMN`. */
58
74
  export declare function numberToColumn(n: number): string;
59
- /** Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`). */
75
+ /**
76
+ * Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`), or `undefined` when they name
77
+ * no column that can exist: not letters at all, or past `XFD` (`"ZZZ"` is well-formed and is column
78
+ * 18278, which the grid does not have). The tolerant half of the pair the module header describes,
79
+ * for a caller reading a file rather than a caller's own argument.
80
+ */
81
+ export declare function tryColumnToNumber(letters: string): number | undefined;
82
+ /**
83
+ * Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`).
84
+ *
85
+ * @throws {RangeError} if the letters are malformed or name a column past `XFD`. The two are
86
+ * separate messages because a caller fixes them differently.
87
+ */
60
88
  export declare function columnToNumber(letters: string): number;
61
89
  /**
62
90
  * One corner of a range, as its reference text. An axis the corner omits contributes nothing, so a
@@ -67,6 +95,17 @@ export declare function columnToNumber(letters: string): number;
67
95
  * The unbounded counterpart to {@link encodeAddress}, which names one cell and so requires both.
68
96
  */
69
97
  export declare function encodeCornerRef(col: number | undefined, row: number | undefined): string;
98
+ /**
99
+ * A bounded rectangle as its canonical `tl:br` A1 range, `"B2:D5"`. A one-cell rectangle still reads
100
+ * as `"B2:B2"`, which is the form Excel writes and the form every consumer of these refs parses.
101
+ *
102
+ * The rendering half of {@link boundedRect}, and it belongs beside it for the same reason: ten sites
103
+ * spelled `` `${encodeAddress(left, top)}:${encodeAddress(right, bottom)}` `` by hand, and the risk in
104
+ * that expression is not its length but its argument order. `encodeAddress` takes column first and a
105
+ * rectangle names its rows first, so the two orders are transposed with respect to each other at every
106
+ * one of those sites.
107
+ */
108
+ export declare function encodeRect(rect: GridRect): string;
70
109
  /**
71
110
  * Decode a single cell/row/column reference into `{address, col, row}`. Anchoring
72
111
  * `$` signs are accepted and dropped; an absent axis is `undefined`.
@@ -98,6 +137,19 @@ export declare function decodeCellRef(reference: string): CellPosition;
98
137
  * there is nothing here".
99
138
  */
100
139
  export declare function tryDecodeCellRef(reference: string): CellPosition | undefined;
140
+ /**
141
+ * Narrow a decoded range to a {@link GridRect}, or `undefined` when either axis is unbounded.
142
+ *
143
+ * A whole-column reference (`A:A`) and a whole-row one (`1:1`) are legal range references whose
144
+ * corners are partly absent, so every caller that needs a real rectangle has to test all four. Doing
145
+ * that in seven places is the other half of the convention `GridRect` already exists to state once:
146
+ * the shape is declared here, and so is the one narrowing that produces it.
147
+ *
148
+ * A caller wanting to say something *different* about each axis (a `Range` refuses `A:A` and `1:1`
149
+ * with two distinct messages) still tests them separately, and one that only cares about the axis it
150
+ * is splicing still tests only that one. Neither is this question.
151
+ */
152
+ export declare function boundedRect(range: RangeAddress): GridRect | undefined;
101
153
  /**
102
154
  * {@link decodeRange} for a reference that came out of a file: `undefined` for anything that does
103
155
  * not name a region that can exist. The sibling of {@link tryDecodeCellRef} on the other arity: a
@@ -114,5 +166,11 @@ export declare function tryDecodeRange(reference: string): RangeAddress | undefi
114
166
  * corners coincide.
115
167
  */
116
168
  export declare function decodeRange(reference: string): RangeAddress;
117
- /** Encode a 1-based `col`/`row` pair into its canonical A1 address (`"B2"`). */
169
+ /**
170
+ * Encode a 1-based `col`/`row` pair into its canonical A1 address (`"B2"`).
171
+ *
172
+ * Both axes go through the shared guard. The column already did, through `numberToColumn`; the row
173
+ * checked only its lower bound in a message of its own, so `encodeAddress(1, 1048577)` produced an
174
+ * address naming a row Excel has no reference for while `encodeAddress(16385, 1)` refused.
175
+ */
118
176
  export declare function encodeAddress(col: number, row: number): string;
@@ -1,14 +1,20 @@
1
+ import { quoted } from '../errors.js';
1
2
  export const MAX_COLUMN = 16384;
2
3
  export const MAX_ROW = 1048576;
3
- export function assertColumnInBounds(n) {
4
- if (!Number.isInteger(n) || n < 1 || n > MAX_COLUMN) {
5
- throw new RangeError(`column ${n} is out of bounds: Excel supports 1..${MAX_COLUMN}`);
4
+ export const MAX_COLUMN_INDEX = MAX_COLUMN - 1;
5
+ export const MAX_ROW_INDEX = MAX_ROW - 1;
6
+ const AXIS_BOUND = { row: MAX_ROW, column: MAX_COLUMN };
7
+ export function assertAxisInBounds(axis, value) {
8
+ const bound = AXIS_BOUND[axis];
9
+ if (!Number.isInteger(value) || value < 1 || value > bound) {
10
+ throw new RangeError(`${axis} ${value} is out of bounds: Excel supports 1..${bound}`);
6
11
  }
7
12
  }
13
+ export function assertColumnInBounds(n) {
14
+ assertAxisInBounds('column', n);
15
+ }
8
16
  export function assertRowInBounds(n) {
9
- if (!Number.isInteger(n) || n < 1 || n > MAX_ROW) {
10
- throw new RangeError(`row ${n} is out of bounds: Excel supports 1..${MAX_ROW}`);
11
- }
17
+ assertAxisInBounds('row', n);
12
18
  }
13
19
  export function rectsOverlap(a, b) {
14
20
  return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
@@ -26,38 +32,43 @@ export function numberToColumn(n) {
26
32
  }
27
33
  return letters;
28
34
  }
29
- export function columnToNumber(letters) {
30
- if (letters.length === 0 || letters.length > 3) {
31
- throw new RangeError(`invalid column letters: "${letters}"`);
32
- }
35
+ const COLUMN_LETTERS = /^[A-Z]{1,3}$/;
36
+ export function tryColumnToNumber(letters) {
37
+ if (!COLUMN_LETTERS.test(letters))
38
+ return undefined;
33
39
  let n = 0;
34
40
  for (let i = 0; i < letters.length; i++) {
35
- const code = letters.charCodeAt(i);
36
- if (code < 65 || code > 90) {
37
- throw new RangeError(`invalid column letters: "${letters}"`);
38
- }
39
- n = n * 26 + (code - 64);
41
+ n = n * 26 + (letters.charCodeAt(i) - 64);
40
42
  }
41
- if (n > MAX_COLUMN) {
42
- throw new RangeError(`column "${letters}" is out of bounds: Excel supports up to ${MAX_COLUMN} (XFD)`);
43
+ return n > MAX_COLUMN ? undefined : n;
44
+ }
45
+ export function columnToNumber(letters) {
46
+ const n = tryColumnToNumber(letters);
47
+ if (n !== undefined)
48
+ return n;
49
+ if (!COLUMN_LETTERS.test(letters)) {
50
+ throw new RangeError(`invalid column letters: ${quoted(letters)}`);
43
51
  }
44
- return n;
52
+ throw new RangeError(`column ${quoted(letters)} is out of bounds: Excel supports up to ${MAX_COLUMN} (XFD)`);
45
53
  }
46
54
  export function encodeCornerRef(col, row) {
47
55
  return `${col !== undefined ? numberToColumn(col) : ''}${row !== undefined ? row : ''}`;
48
56
  }
57
+ export function encodeRect(rect) {
58
+ return `${encodeAddress(rect.left, rect.top)}:${encodeAddress(rect.right, rect.bottom)}`;
59
+ }
49
60
  function makeCellAddress(col, row) {
50
61
  return { address: encodeCornerRef(col, row), col, row };
51
62
  }
52
63
  export function decodeAddress(reference) {
53
64
  const match = SINGLE_REF.exec(reference);
54
65
  if (!match) {
55
- throw new SyntaxError(`invalid cell reference: "${reference}"`);
66
+ throw new SyntaxError(`invalid cell reference: ${quoted(reference)}`);
56
67
  }
57
68
  const letters = match[1] ?? '';
58
69
  const digits = match[2] ?? '';
59
70
  if (letters.length === 0 && digits.length === 0) {
60
- throw new SyntaxError(`invalid cell reference: "${reference}"`);
71
+ throw new SyntaxError(`invalid cell reference: ${quoted(reference)}`);
61
72
  }
62
73
  const col = letters.length > 0 ? columnToNumber(letters) : undefined;
63
74
  const row = digits.length > 0 ? Number.parseInt(digits, 10) : undefined;
@@ -66,7 +77,7 @@ export function decodeAddress(reference) {
66
77
  export function decodeCellRef(reference) {
67
78
  const { col, row } = decodeAddress(reference);
68
79
  if (col === undefined || row === undefined) {
69
- throw new SyntaxError(`"${reference}" is not a single-cell reference: it omits a column or row`);
80
+ throw new SyntaxError(`${quoted(reference)} is not a single-cell reference: it omits a column or row`);
70
81
  }
71
82
  return { col, row };
72
83
  }
@@ -83,6 +94,13 @@ export function tryDecodeCellRef(reference) {
83
94
  }
84
95
  return rowCanExist(position.row) ? position : undefined;
85
96
  }
97
+ export function boundedRect(range) {
98
+ const { top, left, bottom, right } = range;
99
+ if (top === undefined || left === undefined || bottom === undefined || right === undefined) {
100
+ return undefined;
101
+ }
102
+ return { top, left, bottom, right };
103
+ }
86
104
  export function tryDecodeRange(reference) {
87
105
  let range;
88
106
  try {
@@ -125,8 +143,6 @@ export function decodeRange(reference) {
125
143
  };
126
144
  }
127
145
  export function encodeAddress(col, row) {
128
- if (!Number.isInteger(row) || row < 1) {
129
- throw new RangeError(`row ${row} is out of bounds: rows start at 1`);
130
- }
146
+ assertRowInBounds(row);
131
147
  return `${numberToColumn(col)}${row}`;
132
148
  }
@@ -1,3 +1,4 @@
1
+ import { type AxisSplice } from './grid-shift.ts';
1
2
  /**
2
3
  * A worksheet's autofilter: the filtered region plus any per-column criteria narrowing it. A bare
3
4
  * range (no columns) is just the header-row dropdowns Excel draws; adding {@link FilterColumn}s
@@ -43,7 +44,7 @@ export interface CustomFilterPredicate {
43
44
  }
44
45
  export type CustomFilterOperator = 'equal' | 'notEqual' | 'lessThan' | 'lessThanOrEqual' | 'greaterThan' | 'greaterThanOrEqual';
45
46
  /** Narrow a raw `operator` attribute to a known {@link CustomFilterOperator}. */
46
- export declare function isCustomFilterOperator(value: string): value is CustomFilterOperator;
47
+ export declare const isCustomFilterOperator: (value: string) => value is CustomFilterOperator;
47
48
  /**
48
49
  * Validate and normalise a settable autofilter into its canonical stored form. A bare range string
49
50
  * is the common case (dropdowns, no criteria); an {@link AutoFilter} object carries per-column
@@ -54,8 +55,8 @@ export declare function isCustomFilterOperator(value: string): value is CustomFi
54
55
  */
55
56
  export declare function canonicalizeAutoFilter(input: string | AutoFilter): AutoFilter;
56
57
  /**
57
- * Re-anchor a filter through a splice of `count` lines at `start` on `axis`, or drop it (`undefined`)
58
- * when the splice deleted every line it covered.
58
+ * Re-anchor a filter through a splice, or drop it (`undefined`) when the splice deleted every line
59
+ * it covered.
59
60
  *
60
61
  * A row splice only moves the range. A column splice moves its left edge too, and a criterion is
61
62
  * addressed by its offset from that edge rather than by an absolute column, so every offset is
@@ -63,4 +64,4 @@ export declare function canonicalizeAutoFilter(input: string | AutoFilter): Auto
63
64
  * Left alone, those offsets would keep their old numbers and silently re-point each filter at a
64
65
  * neighbouring column.
65
66
  */
66
- export declare function shiftAutoFilter(filter: AutoFilter, axis: 'row' | 'col', start: number, count: number, delta: number): AutoFilter | undefined;
67
+ export declare function shiftAutoFilter(filter: AutoFilter, splice: AxisSplice): AutoFilter | undefined;
@@ -1,28 +1,27 @@
1
- import { AuthoringError } from '../errors.js';
2
- import { decodeRange, encodeAddress } from './address.js';
3
- import { isDeletedSpan, shiftIndex } from './grid-shift.js';
4
- const CUSTOM_FILTER_OPERATORS = {
1
+ import { AuthoringError, quoted } from '../errors.js';
2
+ import { tokenSet } from '../token-set.js';
3
+ import { boundedRect, decodeRange, encodeRect } from './address.js';
4
+ import { isDeletedSpan, shiftIndex, shiftRect } from './grid-shift.js';
5
+ export const isCustomFilterOperator = tokenSet({
5
6
  equal: true,
6
7
  notEqual: true,
7
8
  lessThan: true,
8
9
  lessThanOrEqual: true,
9
10
  greaterThan: true,
10
11
  greaterThanOrEqual: true,
11
- };
12
- export function isCustomFilterOperator(value) {
13
- return Object.hasOwn(CUSTOM_FILTER_OPERATORS, value);
14
- }
12
+ });
15
13
  export function canonicalizeAutoFilter(input) {
16
14
  const ref = typeof input === 'string' ? input : input.ref;
17
- const { top, left, bottom, right, dimensions } = decodeRange(ref);
18
- if (top === undefined || left === undefined || bottom === undefined || right === undefined) {
19
- throw new AuthoringError(`autofilter range "${ref}" must be a bounded rectangle`);
15
+ const decoded = decodeRange(ref);
16
+ const rect = boundedRect(decoded);
17
+ if (rect === undefined) {
18
+ throw new AuthoringError(`autofilter range ${quoted(ref)} must be a bounded rectangle`);
20
19
  }
21
20
  if (typeof input === 'string')
22
- return { ref: dimensions, columns: [] };
23
- const width = right - left + 1;
21
+ return { ref: decoded.dimensions, columns: [] };
22
+ const width = rect.right - rect.left + 1;
24
23
  return {
25
- ref: dimensions,
24
+ ref: decoded.dimensions,
26
25
  columns: input.columns.map((column) => canonicalizeColumn(column, width)),
27
26
  };
28
27
  }
@@ -30,35 +29,35 @@ function canonicalizeColumn(column, width) {
30
29
  if (!Number.isInteger(column.colId) || column.colId < 0 || column.colId >= width) {
31
30
  throw new AuthoringError(`autofilter colId ${column.colId} is outside the filter range`);
32
31
  }
33
- if (column.criteria.kind === 'custom') {
34
- const count = column.criteria.predicates.length;
35
- if (count < 1 || count > 2) {
36
- throw new AuthoringError(`a custom filter needs one or two predicates, got ${count}`);
37
- }
32
+ const criteria = column.criteria;
33
+ if (criteria.kind === 'values') {
34
+ return { colId: column.colId, criteria: { ...criteria, values: [...criteria.values] } };
35
+ }
36
+ const count = criteria.predicates.length;
37
+ if (count < 1 || count > 2) {
38
+ throw new AuthoringError(`a custom filter needs one or two predicates, got ${count}`);
38
39
  }
39
- return column;
40
+ return {
41
+ colId: column.colId,
42
+ criteria: { ...criteria, predicates: criteria.predicates.map((predicate) => ({ ...predicate })) },
43
+ };
40
44
  }
41
- export function shiftAutoFilter(filter, axis, start, count, delta) {
42
- const { top, left, bottom, right } = decodeRange(filter.ref);
43
- if (top === undefined || left === undefined || bottom === undefined || right === undefined) {
45
+ export function shiftAutoFilter(filter, splice) {
46
+ const rect = boundedRect(decodeRange(filter.ref));
47
+ if (rect === undefined)
44
48
  return filter;
45
- }
46
- const [lo, hi] = axis === 'row' ? [top, bottom] : [left, right];
47
- if (isDeletedSpan(lo, hi, start, count))
49
+ const moved = shiftRect(rect, splice);
50
+ if (moved === undefined)
48
51
  return undefined;
49
- const movedLo = shiftIndex(lo, start, count, delta, axis);
50
- const movedHi = shiftIndex(hi, start, count, delta, axis);
51
- if (axis === 'row') {
52
- const ref = `${encodeAddress(left, movedLo)}:${encodeAddress(right, movedHi)}`;
52
+ const ref = encodeRect(moved);
53
+ if (splice.axis === 'row')
53
54
  return { ref, columns: filter.columns };
54
- }
55
- const ref = `${encodeAddress(movedLo, top)}:${encodeAddress(movedHi, bottom)}`;
56
55
  const columns = [];
57
56
  for (const column of filter.columns) {
58
- const absolute = left + column.colId;
59
- if (isDeletedSpan(absolute, absolute, start, count))
57
+ const absolute = rect.left + column.colId;
58
+ if (isDeletedSpan(absolute, absolute, splice))
60
59
  continue;
61
- columns.push({ ...column, colId: shiftIndex(absolute, start, count, delta, axis) - movedLo });
60
+ columns.push({ ...column, colId: shiftIndex(absolute, splice) - moved.left });
62
61
  }
63
62
  return { ref, columns };
64
63
  }
@@ -0,0 +1,10 @@
1
+ export declare abstract class AxisHandle<P extends object> {
2
+ /** This position's format record, or `undefined` when it has none. Never fabricates one. */
3
+ protected abstract propertiesOf(): P | undefined;
4
+ /** This position's format record, created if it does not exist yet. */
5
+ protected abstract ensureProperties(): P;
6
+ /** Remove this position's format record entirely, so nothing is left declaring the line. */
7
+ protected abstract dropProperties(): void;
8
+ protected read<K extends keyof P>(key: K): P[K] | undefined;
9
+ protected write<K extends keyof P>(key: K, value: P[K]): void;
10
+ }
@@ -0,0 +1,17 @@
1
+ export class AxisHandle {
2
+ read(key) {
3
+ return this.propertiesOf()?.[key];
4
+ }
5
+ write(key, value) {
6
+ if (value === undefined) {
7
+ const properties = this.propertiesOf();
8
+ if (properties === undefined)
9
+ return;
10
+ delete properties[key];
11
+ if (Object.keys(properties).length === 0)
12
+ this.dropProperties();
13
+ return;
14
+ }
15
+ this.ensureProperties()[key] = value;
16
+ }
17
+ }
@@ -138,6 +138,31 @@ export declare class Cell {
138
138
  get note(): string | undefined;
139
139
  set note(note: string | undefined);
140
140
  }
141
+ /**
142
+ * Whether a cell carries formatting of its own: any {@link CellContent} facet, which is the six shared
143
+ * style facets plus the quote-prefix flag and the link to a named style. Driven by
144
+ * {@link CELL_CONTENT_FACETS}, so a facet added to the tuple reaches every "is this cell blank"
145
+ * decision without anyone remembering to widen a literal.
146
+ *
147
+ * Row- and column-inherited formatting is not the cell's own and does not count. Neither does a
148
+ * note: it lives in the comments part, not the cell's `<c>` element, so a writer deciding whether an
149
+ * empty cell needs serialising asks exactly this. A caller asking whether the cell is *used* wants
150
+ * {@link cellCarriesContent}, which is this plus the value and the note.
151
+ */
152
+ export declare function cellHasOwnStyle(cell: Cell): boolean;
153
+ /**
154
+ * Whether a cell carries anything at all: a value, formatting of its own, or a note. This is the
155
+ * used-range test, the "carrying anything (data or its own formatting)" that `rowCount` and
156
+ * `columnCount` promise, and it is deliberately wider than "holds a value": someone who pre-formats
157
+ * an empty band has
158
+ * laid claim to those cells, and an append that treats them as free ground writes over the layout
159
+ * they just built; someone who notes an empty cell has put content in it by the note's own
160
+ * definition.
161
+ *
162
+ * A cell merely materialised by `getCell` and then left alone carries nothing, which is what keeps
163
+ * reading a far address from growing the sheet.
164
+ */
165
+ export declare function cellCarriesContent(cell: Cell): boolean;
141
166
  export declare function applyCellStyle(cell: Cell, style: Readonly<CellStyle>): void;
142
167
  export declare function copyCellContent(source: CellModel, target: Cell): void;
143
168
  export declare function cellToModel(cell: Cell): CellModel;
package/dist/core/cell.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assertColumnInBounds, assertRowInBounds, encodeAddress } from './address.js';
2
2
  import { NAMED_STYLE_ID } from './internal.js';
3
- import { assignStyleFacets, } from './style.js';
3
+ import { assignContentFacets, assignStyleFacets, CELL_CONTENT_FACETS, } from './style.js';
4
4
  import { cellValueToText, coerceCellValue, detectValueType, } from './value.js';
5
5
  export class Cell {
6
6
  row;
@@ -106,12 +106,18 @@ export class Cell {
106
106
  this.#note = note;
107
107
  }
108
108
  }
109
+ export function cellHasOwnStyle(cell) {
110
+ return CELL_CONTENT_FACETS.some((facet) => cell[facet] !== undefined);
111
+ }
112
+ export function cellCarriesContent(cell) {
113
+ return cell.value !== null || cell.note !== undefined || cellHasOwnStyle(cell);
114
+ }
109
115
  export function applyCellStyle(cell, style) {
110
116
  assignStyleFacets(cell, style);
111
117
  }
112
118
  export function copyCellContent(source, target) {
113
119
  target.value = source.value;
114
- applyCellStyle(target, source);
120
+ assignContentFacets(target, source);
115
121
  target.note = source.note;
116
122
  }
117
123
  export function cellToModel(cell) {
@@ -121,6 +127,6 @@ export function cellToModel(cell) {
121
127
  value: cell.value,
122
128
  note: cell.note,
123
129
  };
124
- assignStyleFacets(model, cell);
130
+ assignContentFacets(model, cell);
125
131
  return model;
126
132
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * How far a copy of one field goes.
3
+ *
4
+ * - `'value'`: assign it. Correct for a scalar, and for anything genuinely immutable.
5
+ * - `'record'`: `{...v}`. A flat object whose own fields are scalars.
6
+ * - `'values'`: `[...v]`. An array of scalars.
7
+ * - `'records'`: `v.map(x => ({...x}))`. An array of flat objects.
8
+ * - a function: the field's own clone, for a nested type that has a plan of its own.
9
+ */
10
+ export type CloneStrategy<V> = 'value' | 'record' | 'values' | 'records' | ((value: V) => V);
11
+ /**
12
+ * One strategy per field of `T`. The `Record` is what makes a missing field a compile error.
13
+ *
14
+ * A field's strategy sees the field's type without `undefined`, because {@link cloneWith} skips an
15
+ * absent field rather than handing it over. Under `exactOptionalPropertyTypes` a facet declared
16
+ * `font?: Font | undefined` keeps that `undefined` through `Required`, so without the exclusion a
17
+ * nested type's own clone function would not satisfy its own field's strategy.
18
+ */
19
+ export type ClonePlan<T> = {
20
+ readonly [K in keyof Required<T>]-?: CloneStrategy<Exclude<Required<T>[K], undefined>>;
21
+ };
22
+ /**
23
+ * Copy `source` according to `plan`, leaving a field absent when the source leaves it absent.
24
+ *
25
+ * Absent rather than `undefined`: every one of these types declares its optional fields under
26
+ * `exactOptionalPropertyTypes`, where a present-but-undefined key is a different shape from a missing
27
+ * one, and a round-trip that fabricated the first would report formatting a cell does not have.
28
+ */
29
+ export declare function cloneWith<T extends object>(source: T, plan: ClonePlan<T>): T;
@@ -0,0 +1,24 @@
1
+ export function cloneWith(source, plan) {
2
+ const out = {};
3
+ for (const key of Object.keys(plan)) {
4
+ const value = source[key];
5
+ if (value === undefined)
6
+ continue;
7
+ out[key] = applyStrategy(value, plan[key]);
8
+ }
9
+ return out;
10
+ }
11
+ function applyStrategy(value, strategy) {
12
+ if (typeof strategy === 'function')
13
+ return strategy(value);
14
+ switch (strategy) {
15
+ case 'value':
16
+ return value;
17
+ case 'record':
18
+ return { ...value };
19
+ case 'values':
20
+ return [...value];
21
+ case 'records':
22
+ return value.map((entry) => ({ ...entry }));
23
+ }
24
+ }
@@ -1,3 +1,4 @@
1
+ import { hex } from '../hex.js';
1
2
  import { parseArgb } from './style.js';
2
3
  import { DEFAULT_THEME_COLOR_SCHEME, THEME_COLOR_SLOTS } from './theme.js';
3
4
  export const DEFAULT_INDEXED_COLORS = [
@@ -102,8 +103,5 @@ function hueToChannel(p, q, offset) {
102
103
  return p;
103
104
  }
104
105
  function channelHex(value) {
105
- return Math.round(clamp01(value) * 255)
106
- .toString(16)
107
- .toUpperCase()
108
- .padStart(2, '0');
106
+ return hex(Math.round(clamp01(value) * 255), 2);
109
107
  }
@@ -1,9 +1,10 @@
1
+ import { AxisHandle } from './axis-handle.ts';
1
2
  import type { Cell } from './cell.ts';
2
3
  import { type AssertNever } from './internal.ts';
3
4
  import type { Alignment, Border, Fill, Font, Protection } from './style.ts';
4
5
  import type { CellValue } from './value.ts';
5
6
  import type { ColumnProperties, Worksheet } from './worksheet.ts';
6
- export declare class Column {
7
+ export declare class Column extends AxisHandle<ColumnProperties> {
7
8
  #private;
8
9
  /** 1-based column index. Fixed for this handle's lifetime. */
9
10
  readonly index: number;
@@ -18,6 +19,9 @@ export declare class Column {
18
19
  * is formatted, and they create the record on first write.
19
20
  */
20
21
  get properties(): Readonly<ColumnProperties> | undefined;
22
+ protected propertiesOf(): ColumnProperties | undefined;
23
+ protected ensureProperties(): ColumnProperties;
24
+ protected dropProperties(): void;
21
25
  /**
22
26
  * Stable key naming this column so a keyed-object row (see {@link Worksheet.addRow}) can place a
23
27
  * value under it by name rather than position. In-memory only: never serialized to OOXML.