@shbernal/ts-xlsx 2.0.0 → 3.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 (147) hide show
  1. package/README.md +20 -10
  2. package/dist/bytes.d.ts +24 -0
  3. package/dist/bytes.js +34 -0
  4. package/dist/core/address.d.ts +26 -4
  5. package/dist/core/address.js +21 -9
  6. package/dist/core/autofilter.d.ts +12 -1
  7. package/dist/core/autofilter.js +35 -12
  8. package/dist/core/axis-handle.d.ts +8 -0
  9. package/dist/core/axis-handle.js +14 -0
  10. package/dist/core/cell.d.ts +24 -0
  11. package/dist/core/cell.js +9 -1
  12. package/dist/core/color-resolution.d.ts +1 -1
  13. package/dist/core/color-resolution.js +7 -8
  14. package/dist/core/column.d.ts +4 -1
  15. package/dist/core/column.js +31 -35
  16. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  17. package/dist/core/conditional-formatting-overlay.js +11 -0
  18. package/dist/core/conditional-formatting.d.ts +45 -5
  19. package/dist/core/conditional-formatting.js +74 -0
  20. package/dist/core/data-validation-overlay.d.ts +11 -0
  21. package/dist/core/data-validation-overlay.js +21 -2
  22. package/dist/core/data-validation.d.ts +9 -2
  23. package/dist/core/data-validation.js +26 -0
  24. package/dist/core/grid-edits.d.ts +16 -0
  25. package/dist/core/grid-edits.js +26 -8
  26. package/dist/core/grid-shift.d.ts +19 -0
  27. package/dist/core/grid-shift.js +8 -0
  28. package/dist/core/image.d.ts +2 -0
  29. package/dist/core/image.js +2 -0
  30. package/dist/core/merge-index.d.ts +31 -0
  31. package/dist/core/merge-index.js +60 -0
  32. package/dist/core/merge.d.ts +12 -11
  33. package/dist/core/merge.js +36 -10
  34. package/dist/core/page-setup.d.ts +86 -2
  35. package/dist/core/page-setup.js +34 -1
  36. package/dist/core/pivot-table.d.ts +2 -0
  37. package/dist/core/pivot-table.js +23 -18
  38. package/dist/core/protection.d.ts +3 -0
  39. package/dist/core/protection.js +20 -14
  40. package/dist/core/row-input.d.ts +7 -0
  41. package/dist/core/row-input.js +10 -8
  42. package/dist/core/row.d.ts +4 -1
  43. package/dist/core/row.js +19 -23
  44. package/dist/core/style.d.ts +78 -7
  45. package/dist/core/style.js +90 -80
  46. package/dist/core/table.d.ts +26 -12
  47. package/dist/core/table.js +67 -33
  48. package/dist/core/theme.d.ts +3 -1
  49. package/dist/core/theme.js +4 -0
  50. package/dist/core/used-extent.d.ts +44 -0
  51. package/dist/core/used-extent.js +102 -0
  52. package/dist/core/workbook.d.ts +2 -2
  53. package/dist/core/worksheet-comments.d.ts +8 -0
  54. package/dist/core/worksheet-comments.js +18 -0
  55. package/dist/core/worksheet.d.ts +31 -7
  56. package/dist/core/worksheet.js +72 -86
  57. package/dist/customui/ribbon.js +23 -24
  58. package/dist/entries/core.d.ts +3 -3
  59. package/dist/entries/csv.d.ts +1 -1
  60. package/dist/entries/node-unavailable.d.ts +15 -0
  61. package/dist/entries/node-unavailable.js +20 -0
  62. package/dist/entries/node.d.ts +1 -0
  63. package/dist/entries/node.js +1 -0
  64. package/dist/entries/xlsx.d.ts +0 -1
  65. package/dist/entries/xlsx.js +0 -1
  66. package/dist/io/csv/read.js +1 -1
  67. package/dist/io/csv/write.d.ts +12 -2
  68. package/dist/io/csv/write.js +17 -2
  69. package/dist/io/opc/inflate.js +3 -13
  70. package/dist/io/opc/part-paths.d.ts +1 -0
  71. package/dist/io/opc/part-paths.js +15 -0
  72. package/dist/io/opc/read-opc.d.ts +22 -3
  73. package/dist/io/opc/read-opc.js +11 -39
  74. package/dist/io/opc/rels.d.ts +12 -6
  75. package/dist/io/opc/rels.js +3 -5
  76. package/dist/io/style/xf-style.d.ts +33 -4
  77. package/dist/io/style/xf-style.js +12 -1
  78. package/dist/io/xlsb/read-styles.js +6 -3
  79. package/dist/io/xlsb/read.d.ts +1 -1
  80. package/dist/io/xlsb/read.js +5 -8
  81. package/dist/io/xlsx/cell-accumulator.d.ts +2 -4
  82. package/dist/io/xlsx/cell-accumulator.js +20 -58
  83. package/dist/io/xlsx/cell-value.d.ts +1 -1
  84. package/dist/io/xlsx/cell-value.js +7 -4
  85. package/dist/io/xlsx/color-xml.d.ts +1 -1
  86. package/dist/io/xlsx/color-xml.js +8 -7
  87. package/dist/io/xlsx/comments.js +6 -3
  88. package/dist/io/xlsx/conditional-formatting.d.ts +4 -3
  89. package/dist/io/xlsx/conditional-formatting.js +81 -65
  90. package/dist/io/xlsx/data-validation.d.ts +10 -8
  91. package/dist/io/xlsx/data-validation.js +44 -41
  92. package/dist/io/xlsx/edit-vba.js +4 -11
  93. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  94. package/dist/io/xlsx/hyperlinks.js +26 -31
  95. package/dist/io/xlsx/images.js +33 -35
  96. package/dist/io/xlsx/package-plan.d.ts +9 -1
  97. package/dist/io/xlsx/package-plan.js +29 -16
  98. package/dist/io/xlsx/part-names.d.ts +41 -0
  99. package/dist/io/xlsx/part-names.js +46 -0
  100. package/dist/io/xlsx/read-pivot.js +4 -9
  101. package/dist/io/xlsx/read-rows.js +12 -13
  102. package/dist/io/xlsx/read-shared-strings.js +9 -47
  103. package/dist/io/xlsx/read-styles.d.ts +1 -1
  104. package/dist/io/xlsx/read-styles.js +65 -43
  105. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  106. package/dist/io/xlsx/read-worksheet.js +48 -63
  107. package/dist/io/xlsx/read.d.ts +3 -4
  108. package/dist/io/xlsx/read.js +124 -146
  109. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  110. package/dist/io/xlsx/rich-runs.js +95 -30
  111. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  112. package/dist/io/xlsx/sheet-properties.js +23 -26
  113. package/dist/io/xlsx/styles.d.ts +3 -15
  114. package/dist/io/xlsx/styles.js +83 -105
  115. package/dist/io/xlsx/tables.js +14 -13
  116. package/dist/io/xlsx/theme-xml.js +8 -5
  117. package/dist/io/xlsx/threaded-comments.js +20 -19
  118. package/dist/io/xlsx/workbook-xml.js +43 -36
  119. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  120. package/dist/io/xlsx/worksheet-xml.js +47 -52
  121. package/dist/io/xlsx/write-stream.d.ts +14 -1
  122. package/dist/io/xlsx/write-stream.js +40 -33
  123. package/dist/io/xlsx/write.d.ts +5 -0
  124. package/dist/io/xlsx/write.js +155 -131
  125. package/dist/sha512.d.ts +2 -0
  126. package/dist/sha512.js +146 -0
  127. package/dist/token-set.d.ts +15 -0
  128. package/dist/token-set.js +4 -0
  129. package/dist/vba/bytes.d.ts +1 -2
  130. package/dist/vba/bytes.js +1 -12
  131. package/dist/vba/cfb-format.d.ts +24 -0
  132. package/dist/vba/cfb-format.js +12 -0
  133. package/dist/vba/cfb-writer.js +17 -17
  134. package/dist/vba/cfb.js +1 -7
  135. package/dist/vba/dir-records.d.ts +53 -0
  136. package/dist/vba/dir-records.js +28 -0
  137. package/dist/vba/ms-ovba.js +32 -11
  138. package/dist/vba/project-editor.js +5 -34
  139. package/dist/vba/project.js +15 -31
  140. package/dist/vba/vba-encoding.js +1 -1
  141. package/dist/xml/xml-read.d.ts +117 -72
  142. package/dist/xml/xml-read.js +123 -169
  143. package/dist/xml/xml-scan.d.ts +100 -0
  144. package/dist/xml/xml-scan.js +208 -0
  145. package/dist/xml/xml.d.ts +44 -2
  146. package/dist/xml/xml.js +17 -5
  147. package/package.json +22 -6
package/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
  A TypeScript-first library for reading and writing spreadsheet documents
10
10
  (`.xlsx` / OOXML, and CSV) — synchronous, `Uint8Array`-native, and dependency-lean.
11
11
 
12
+ **[Documentation, guide and a browser playground](https://shbernal.github.io/ts-xlsx/)**
13
+
12
14
  > **Status — 1.0.0, the first release.**
13
15
  > `ts-xlsx` began as a hard fork of [ExcelJS](https://github.com/exceljs/exceljs)
14
16
  > and has been rebuilt from the ground up into a modern, strict-TypeScript library.
@@ -130,10 +132,12 @@ import {
130
132
  readXlsx, writeXlsx, // buffered .xlsx (Uint8Array ⇄ Workbook)
131
133
  readSheetRows, // stream one sheet's rows, bounded memory
132
134
  readWorkbookStream, // stream every sheet, rows one at a time
133
- WorkbookStreamWriter, // write a workbook incrementally, bounded memory
134
135
  readCsv, writeCsv, // CSV as Uint8Array
135
136
  writeCsvText, // CSV as a string
136
137
  } from '@shbernal/ts-xlsx';
138
+ // The streaming writer opens files and pipes Node streams, so it is the one Node-only face
139
+ // and lives behind its own subpath — which is what keeps everything above browser-safe:
140
+ import {WorkbookStreamWriter} from '@shbernal/ts-xlsx/node';
137
141
 
138
142
  // Bounded-memory extraction — the whole workbook is never materialized:
139
143
  for (const row of readSheetRows(bytes, {sheet: 'People'})) {
@@ -189,18 +193,24 @@ on — a Lambda with no bundler, a service that only classifies failures:
189
193
 
190
194
  | Import from | You get | It loads |
191
195
  | --- | --- | --- |
192
- | `@shbernal/ts-xlsx` | everything | 902 KB |
193
- | `@shbernal/ts-xlsx/core` | `Workbook`, `Worksheet`, `Cell`, styles, values, addresses | 332 KB |
194
- | `@shbernal/ts-xlsx/xlsx` | `readXlsx`, `writeXlsx`/`writeXlsxAsync`, the streaming pair, VBA part edits | 887 KB |
195
- | `@shbernal/ts-xlsx/xlsb` | `readXlsb` | 469 KB |
196
- | `@shbernal/ts-xlsx/csv` | `readCsv`, `writeCsv`, `writeCsvText` | 341 KB |
197
- | `@shbernal/ts-xlsx/vba` | `parseVbaProject`, `addVbaReference`, `removeVbaModule` | 73 KB |
198
- | `@shbernal/ts-xlsx/customui` | `parseCustomUi` and the ribbon types | 26 KB |
199
- | `@shbernal/ts-xlsx/errors` | every error class the library throws | 12 KB |
196
+ | `@shbernal/ts-xlsx` | everything except the streaming writer | 501 KB |
197
+ | `@shbernal/ts-xlsx/core` | `Workbook`, `Worksheet`, `Cell`, styles, values, addresses | 185 KB |
198
+ | `@shbernal/ts-xlsx/xlsx` | `readXlsx`, `writeXlsx`/`writeXlsxAsync`, the streaming reader, VBA part edits | 490 KB |
199
+ | `@shbernal/ts-xlsx/xlsb` | `readXlsb` | 256 KB |
200
+ | `@shbernal/ts-xlsx/csv` | `readCsv`, `writeCsv`, `writeCsvText` | 191 KB |
201
+ | `@shbernal/ts-xlsx/node` | `WorkbookStreamWriter` and the rest of the streaming writer | 358 KB |
202
+ | `@shbernal/ts-xlsx/vba` | `parseVbaProject`, `addVbaReference`, `removeVbaModule` | 46 KB |
203
+ | `@shbernal/ts-xlsx/customui` | `parseCustomUi` and the ribbon types | 15 KB |
204
+ | `@shbernal/ts-xlsx/errors` | every error class the library throws | 3 KB |
205
+
206
+ `/node` is the only one the root specifier does not carry, and the reason is its imports
207
+ rather than its size: it reaches `node:fs` and `node:stream`, and every other entry reaches
208
+ no Node built-in at all, which is what makes them bundle for a browser with nothing to
209
+ configure ([ADR-0040](docs/decisions/0040-the-browser-boundary-is-an-entry-point.md)).
200
210
 
201
211
  Every error class lives in `/errors` and nowhere else, because a container-level failure
202
212
  belongs to no single codec — `readXlsx` and `readXlsb` both raise `UnsupportedFormatError`.
203
- Catching and classifying therefore costs 12 KB, not a parser.
213
+ Catching and classifying therefore costs 3 KB, not a parser.
204
214
 
205
215
  `/xlsx` is barely cheaper than the whole package, and that is honest rather than a defect:
206
216
  `readXlsx` sniffs the bytes and hands a binary package to the BIFF12 reader, so the `.xlsb`
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Join byte chunks into one buffer.
3
+ *
4
+ * Both parameters exist to avoid work the caller has already done. `size` is the total length when
5
+ * the caller knows it, as a reader draining a length-prefixed stream does, which skips a pass over
6
+ * the chunks. A lone chunk is handed straight back rather than copied, which is the common case on
7
+ * the inflate path, where a part that fits in one chunk would otherwise be duplicated in memory the
8
+ * moment it is read.
9
+ *
10
+ * The returned buffer therefore aliases the caller's chunk when there is exactly one. Every caller
11
+ * here is a reader assembling bytes it then only reads, so this is sound; a caller that means to
12
+ * mutate the result must copy it.
13
+ */
14
+ export declare function concat(chunks: readonly Uint8Array[], size?: number): Uint8Array;
15
+ /**
16
+ * Standard base64 of `bytes`, padded with `=`.
17
+ *
18
+ * Spelled out rather than delegated because both platform routes are unavailable here: `Buffer` is
19
+ * a Node global, which is exactly what a browser-safe module may not reach for, and `btoa` is
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.
23
+ */
24
+ export declare function toBase64(bytes: Uint8Array): string;
package/dist/bytes.js ADDED
@@ -0,0 +1,34 @@
1
+ export function concat(chunks, size) {
2
+ const first = chunks[0];
3
+ if (chunks.length === 1 && first !== undefined)
4
+ return first;
5
+ let total = size;
6
+ if (total === undefined) {
7
+ total = 0;
8
+ for (const chunk of chunks)
9
+ total += chunk.length;
10
+ }
11
+ const out = new Uint8Array(total);
12
+ let offset = 0;
13
+ for (const chunk of chunks) {
14
+ out.set(chunk, offset);
15
+ offset += chunk.length;
16
+ }
17
+ return out;
18
+ }
19
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
20
+ export function toBase64(bytes) {
21
+ let out = '';
22
+ for (let i = 0; i < bytes.length; i += 3) {
23
+ const a = bytes[i] ?? 0;
24
+ const b = bytes[i + 1] ?? 0;
25
+ const c = bytes[i + 2] ?? 0;
26
+ const triple = (a << 16) | (b << 8) | c;
27
+ const remaining = bytes.length - i;
28
+ out += BASE64_ALPHABET.charAt((triple >>> 18) & 63);
29
+ out += BASE64_ALPHABET.charAt((triple >>> 12) & 63);
30
+ out += remaining > 1 ? BASE64_ALPHABET.charAt((triple >>> 6) & 63) : '=';
31
+ out += remaining > 2 ? BASE64_ALPHABET.charAt(triple & 63) : '=';
32
+ }
33
+ return out;
34
+ }
@@ -52,10 +52,21 @@ export interface RangeAddress {
52
52
  /** Canonical `tl:br` form: `"A1:B2"`, `"1:1"` (rows), `"A:A"` (columns). */
53
53
  readonly dimensions: string;
54
54
  }
55
- /** Convert a 1-based column number to its letters (`1 → "A"`, `27 → "AA"`). */
55
+ /** Convert a 1-based column number to its letters (`1 → "A"`, `27 → "AA"`).
56
+ *
57
+ * @throws {RangeError} unless `n` is an integer in `1..MAX_COLUMN`. */
56
58
  export declare function numberToColumn(n: number): string;
57
59
  /** Convert column letters to a 1-based number (`"A" → 1`, `"AA" → 27`). */
58
60
  export declare function columnToNumber(letters: string): number;
61
+ /**
62
+ * One corner of a range, as its reference text. An axis the corner omits contributes nothing, so a
63
+ * whole-row corner spells `6` and a whole-column one spells `B`, rather than acquiring the other
64
+ * axis from a sentinel. That is the honesty rule this module's header states, and letting an absent
65
+ * axis decay into text is exactly how `"$undefined$1"` and `"NaN:NaN"` used to reach a file.
66
+ *
67
+ * The unbounded counterpart to {@link encodeAddress}, which names one cell and so requires both.
68
+ */
69
+ export declare function encodeCornerRef(col: number | undefined, row: number | undefined): string;
59
70
  /**
60
71
  * Decode a single cell/row/column reference into `{address, col, row}`. Anchoring
61
72
  * `$` signs are accepted and dropped; an absent axis is `undefined`.
@@ -81,11 +92,22 @@ export interface CellPosition {
81
92
  export declare function decodeCellRef(reference: string): CellPosition;
82
93
  /**
83
94
  * {@link decodeCellRef} for a reference that came out of a file rather than out of a caller:
84
- * `undefined` for anything that does not name one cell, whether it is a range, a bare row or
85
- * column, or outright garbage. A foreign producer writes all four, and none of them is worth
86
- * throwing over when the reading code's answer is simply "then there is nothing here".
95
+ * `undefined` for anything that does not name one cell that can exist: a range, a bare row or
96
+ * column, a position off the grid (`A0`, `XFE1`), or outright garbage. A foreign producer writes
97
+ * all of them, and none is worth throwing over when the reading code's answer is simply "then
98
+ * there is nothing here".
87
99
  */
88
100
  export declare function tryDecodeCellRef(reference: string): CellPosition | undefined;
101
+ /**
102
+ * {@link decodeRange} for a reference that came out of a file: `undefined` for anything that does
103
+ * not name a region that can exist. The sibling of {@link tryDecodeCellRef} on the other arity: a
104
+ * `ref` or one area of a `sqref` is as likely to be malformed as a cell's `r`, and the reader's
105
+ * answer to both is the same one.
106
+ *
107
+ * An axis neither endpoint mentions stays `undefined`, exactly as in `decodeRange`: a whole-column
108
+ * range is unbounded, not unreadable, and a caller that needs a bounded rectangle says so itself.
109
+ */
110
+ export declare function tryDecodeRange(reference: string): RangeAddress | undefined;
89
111
  /**
90
112
  * Decode a range reference (`A1:B2`, `$1:$1`, `Sheet1!$A:$A`) into its corners and
91
113
  * canonical dimensions. A single reference collapses to a degenerate range whose
@@ -16,9 +16,7 @@ export function rectsOverlap(a, b) {
16
16
  const SINGLE_REF = /^\$?([A-Z]*)\$?(\d*)$/;
17
17
  const SHEET_PREFIX = /^(?:(?:'((?:[^']|'')*)')|([^'!]+))!(.*)$/;
18
18
  export function numberToColumn(n) {
19
- if (!Number.isInteger(n) || n < 1 || n > MAX_COLUMN) {
20
- throw new RangeError(`column ${n} is out of bounds: Excel supports 1..${MAX_COLUMN}`);
21
- }
19
+ assertColumnInBounds(n);
22
20
  let letters = '';
23
21
  let remaining = n;
24
22
  while (remaining > 0) {
@@ -45,12 +43,11 @@ export function columnToNumber(letters) {
45
43
  }
46
44
  return n;
47
45
  }
46
+ export function encodeCornerRef(col, row) {
47
+ return `${col !== undefined ? numberToColumn(col) : ''}${row !== undefined ? row : ''}`;
48
+ }
48
49
  function makeCellAddress(col, row) {
49
- return {
50
- address: `${col !== undefined ? numberToColumn(col) : ''}${row !== undefined ? row : ''}`,
51
- col,
52
- row,
53
- };
50
+ return { address: encodeCornerRef(col, row), col, row };
54
51
  }
55
52
  export function decodeAddress(reference) {
56
53
  const match = SINGLE_REF.exec(reference);
@@ -73,13 +70,28 @@ export function decodeCellRef(reference) {
73
70
  }
74
71
  return { col, row };
75
72
  }
73
+ function rowCanExist(row) {
74
+ return row === undefined || (Number.isInteger(row) && row >= 1 && row <= MAX_ROW);
75
+ }
76
76
  export function tryDecodeCellRef(reference) {
77
+ let position;
78
+ try {
79
+ position = decodeCellRef(reference);
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ return rowCanExist(position.row) ? position : undefined;
85
+ }
86
+ export function tryDecodeRange(reference) {
87
+ let range;
77
88
  try {
78
- return decodeCellRef(reference);
89
+ range = decodeRange(reference);
79
90
  }
80
91
  catch {
81
92
  return undefined;
82
93
  }
94
+ return rowCanExist(range.top) && rowCanExist(range.bottom) ? range : undefined;
83
95
  }
84
96
  export function decodeRange(reference) {
85
97
  const prefix = SHEET_PREFIX.exec(reference);
@@ -43,7 +43,7 @@ export interface CustomFilterPredicate {
43
43
  }
44
44
  export type CustomFilterOperator = 'equal' | 'notEqual' | 'lessThan' | 'lessThanOrEqual' | 'greaterThan' | 'greaterThanOrEqual';
45
45
  /** Narrow a raw `operator` attribute to a known {@link CustomFilterOperator}. */
46
- export declare function isCustomFilterOperator(value: string): value is CustomFilterOperator;
46
+ export declare const isCustomFilterOperator: (value: string) => value is CustomFilterOperator;
47
47
  /**
48
48
  * Validate and normalise a settable autofilter into its canonical stored form. A bare range string
49
49
  * is the common case (dropdowns, no criteria); an {@link AutoFilter} object carries per-column
@@ -53,3 +53,14 @@ export declare function isCustomFilterOperator(value: string): value is CustomFi
53
53
  * here, so load never trips these guards.)
54
54
  */
55
55
  export declare function canonicalizeAutoFilter(input: string | AutoFilter): AutoFilter;
56
+ /**
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.
59
+ *
60
+ * A row splice only moves the range. A column splice moves its left edge too, and a criterion is
61
+ * addressed by its offset from that edge rather than by an absolute column, so every offset is
62
+ * re-measured against the new edge and a criterion whose column was deleted goes with the column.
63
+ * Left alone, those offsets would keep their old numbers and silently re-point each filter at a
64
+ * neighbouring column.
65
+ */
66
+ export declare function shiftAutoFilter(filter: AutoFilter, axis: 'row' | 'col', start: number, count: number, delta: number): AutoFilter | undefined;
@@ -1,16 +1,15 @@
1
1
  import { AuthoringError } from '../errors.js';
2
- import { decodeRange } from './address.js';
3
- const CUSTOM_FILTER_OPERATORS = new Set([
4
- 'equal',
5
- 'notEqual',
6
- 'lessThan',
7
- 'lessThanOrEqual',
8
- 'greaterThan',
9
- 'greaterThanOrEqual',
10
- ]);
11
- export function isCustomFilterOperator(value) {
12
- return CUSTOM_FILTER_OPERATORS.has(value);
13
- }
2
+ import { tokenSet } from '../token-set.js';
3
+ import { decodeRange, encodeAddress } from './address.js';
4
+ import { isDeletedSpan, shiftIndex } from './grid-shift.js';
5
+ export const isCustomFilterOperator = tokenSet({
6
+ equal: true,
7
+ notEqual: true,
8
+ lessThan: true,
9
+ lessThanOrEqual: true,
10
+ greaterThan: true,
11
+ greaterThanOrEqual: true,
12
+ });
14
13
  export function canonicalizeAutoFilter(input) {
15
14
  const ref = typeof input === 'string' ? input : input.ref;
16
15
  const { top, left, bottom, right, dimensions } = decodeRange(ref);
@@ -37,3 +36,27 @@ function canonicalizeColumn(column, width) {
37
36
  }
38
37
  return column;
39
38
  }
39
+ export function shiftAutoFilter(filter, axis, start, count, delta) {
40
+ const { top, left, bottom, right } = decodeRange(filter.ref);
41
+ if (top === undefined || left === undefined || bottom === undefined || right === undefined) {
42
+ return filter;
43
+ }
44
+ const [lo, hi] = axis === 'row' ? [top, bottom] : [left, right];
45
+ if (isDeletedSpan(lo, hi, start, count))
46
+ return undefined;
47
+ const movedLo = shiftIndex(lo, start, count, delta, axis);
48
+ const movedHi = shiftIndex(hi, start, count, delta, axis);
49
+ if (axis === 'row') {
50
+ const ref = `${encodeAddress(left, movedLo)}:${encodeAddress(right, movedHi)}`;
51
+ return { ref, columns: filter.columns };
52
+ }
53
+ const ref = `${encodeAddress(movedLo, top)}:${encodeAddress(movedHi, bottom)}`;
54
+ const columns = [];
55
+ for (const column of filter.columns) {
56
+ const absolute = left + column.colId;
57
+ if (isDeletedSpan(absolute, absolute, start, count))
58
+ continue;
59
+ columns.push({ ...column, colId: shiftIndex(absolute, start, count, delta, axis) - movedLo });
60
+ }
61
+ return { ref, columns };
62
+ }
@@ -0,0 +1,8 @@
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
+ protected read<K extends keyof P>(key: K): P[K] | undefined;
7
+ protected write<K extends keyof P>(key: K, value: P[K]): void;
8
+ }
@@ -0,0 +1,14 @@
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
+ delete properties[key];
10
+ return;
11
+ }
12
+ this.ensureProperties()[key] = value;
13
+ }
14
+ }
@@ -138,6 +138,30 @@ 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: one of the {@link CellStyle} facets, the quote-prefix
143
+ * flag, or a link to a named style. Driven by {@link CELL_STYLE_FACETS}, so a facet added to the
144
+ * tuple reaches every "is this cell blank" decision without anyone remembering to widen a literal.
145
+ *
146
+ * Row- and column-inherited formatting is not the cell's own and does not count. Neither does a
147
+ * note: it lives in the comments part, not the cell's `<c>` element, so a writer deciding whether an
148
+ * empty cell needs serialising asks exactly this. A caller asking whether the cell is *used* wants
149
+ * {@link cellCarriesContent}, which is this plus the value and the note.
150
+ */
151
+ export declare function cellHasOwnStyle(cell: Cell): boolean;
152
+ /**
153
+ * Whether a cell carries anything at all: a value, formatting of its own, or a note. This is the
154
+ * used-range test, the "carrying anything (data or its own formatting)" that `rowCount` and
155
+ * `columnCount` promise, and it is deliberately wider than "holds a value": someone who pre-formats
156
+ * an empty band has
157
+ * laid claim to those cells, and an append that treats them as free ground writes over the layout
158
+ * they just built; someone who notes an empty cell has put content in it by the note's own
159
+ * definition.
160
+ *
161
+ * A cell merely materialised by `getCell` and then left alone carries nothing, which is what keeps
162
+ * reading a far address from growing the sheet.
163
+ */
164
+ export declare function cellCarriesContent(cell: Cell): boolean;
141
165
  export declare function applyCellStyle(cell: Cell, style: Readonly<CellStyle>): void;
142
166
  export declare function copyCellContent(source: CellModel, target: Cell): void;
143
167
  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 { assignStyleFacets, CELL_STYLE_FACETS, } from './style.js';
4
4
  import { cellValueToText, coerceCellValue, detectValueType, } from './value.js';
5
5
  export class Cell {
6
6
  row;
@@ -106,6 +106,14 @@ export class Cell {
106
106
  this.#note = note;
107
107
  }
108
108
  }
109
+ export function cellHasOwnStyle(cell) {
110
+ return (CELL_STYLE_FACETS.some((facet) => cell[facet] !== undefined) ||
111
+ cell.quotePrefix === true ||
112
+ cell[NAMED_STYLE_ID] !== undefined);
113
+ }
114
+ export function cellCarriesContent(cell) {
115
+ return cell.value !== null || cell.note !== undefined || cellHasOwnStyle(cell);
116
+ }
109
117
  export function applyCellStyle(cell, style) {
110
118
  assignStyleFacets(cell, style);
111
119
  }
@@ -1,4 +1,4 @@
1
- import type { Color } from './style.ts';
1
+ import { type Color } from './style.ts';
2
2
  import { type ThemeColorScheme } from './theme.ts';
3
3
  /**
4
4
  * The built-in indexed colour palette (ECMA-376 §18.8.27), by index. Entries 0–7 duplicate 8–15, a
@@ -1,3 +1,4 @@
1
+ import { parseArgb } from './style.js';
1
2
  import { DEFAULT_THEME_COLOR_SCHEME, THEME_COLOR_SLOTS } from './theme.js';
2
3
  export const DEFAULT_INDEXED_COLORS = [
3
4
  '00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', '0000FFFF',
@@ -19,9 +20,12 @@ export function resolveColor(color, context = {}) {
19
20
  return base;
20
21
  return applyTint(base, tint);
21
22
  }
23
+ function resolveArgb(value) {
24
+ return parseArgb(value)?.toUpperCase();
25
+ }
22
26
  function resolveBase(color, context) {
23
27
  if (color.argb !== undefined)
24
- return normalizeArgb(color.argb);
28
+ return resolveArgb(color.argb);
25
29
  if (color.theme !== undefined) {
26
30
  const slot = THEME_COLOR_SLOTS[color.theme];
27
31
  if (slot === undefined)
@@ -38,16 +42,11 @@ function resolveBase(color, context) {
38
42
  : DEFAULT_INDEXED_COLORS[color.indexed];
39
43
  if (entry === undefined)
40
44
  return undefined;
41
- const normalized = normalizeArgb(entry);
42
- return normalized === undefined ? undefined : `FF${normalized.slice(2)}`;
45
+ const parsed = resolveArgb(entry);
46
+ return parsed === undefined ? undefined : `FF${parsed.slice(2)}`;
43
47
  }
44
48
  return undefined;
45
49
  }
46
- function normalizeArgb(value) {
47
- const hex = value.startsWith('#') ? value.slice(1) : value;
48
- const argb = hex.length === 6 ? `FF${hex}` : hex;
49
- return /^[0-9a-fA-F]{8}$/.test(argb) ? argb.toUpperCase() : undefined;
50
- }
51
50
  export function applyTint(argb, tint) {
52
51
  const alpha = argb.slice(0, 2);
53
52
  const r = Number.parseInt(argb.slice(2, 4), 16) / 255;
@@ -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,8 @@ 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;
21
24
  /**
22
25
  * Stable key naming this column so a keyed-object row (see {@link Worksheet.addRow}) can place a
23
26
  * value under it by name rather than position. In-memory only: never serialized to OOXML.
@@ -1,9 +1,11 @@
1
1
  import { assertColumnInBounds, encodeAddress, numberToColumn } from './address.js';
2
+ import { AxisHandle } from './axis-handle.js';
2
3
  import { INTERNAL } from './internal.js';
3
- export class Column {
4
+ export class Column extends AxisHandle {
4
5
  #sheet;
5
6
  index;
6
7
  constructor(sheet, index) {
8
+ super();
7
9
  assertColumnInBounds(index);
8
10
  this.#sheet = sheet;
9
11
  this.index = index;
@@ -12,73 +14,79 @@ export class Column {
12
14
  return numberToColumn(this.index);
13
15
  }
14
16
  get properties() {
17
+ return this.propertiesOf();
18
+ }
19
+ propertiesOf() {
15
20
  return this.#sheet[INTERNAL].columnPropertiesOf(this.index);
16
21
  }
22
+ ensureProperties() {
23
+ return this.#sheet[INTERNAL].ensureColumnProperties(this.index);
24
+ }
17
25
  get key() {
18
- return this.#read('key');
26
+ return this.read('key');
19
27
  }
20
28
  set key(key) {
21
- this.#write('key', key);
29
+ this.write('key', key);
22
30
  }
23
31
  get width() {
24
- return this.#read('width');
32
+ return this.read('width');
25
33
  }
26
34
  set width(width) {
27
- this.#write('width', width);
35
+ this.write('width', width);
28
36
  }
29
37
  get hidden() {
30
- return this.#read('hidden');
38
+ return this.read('hidden');
31
39
  }
32
40
  set hidden(hidden) {
33
- this.#write('hidden', hidden);
41
+ this.write('hidden', hidden);
34
42
  }
35
43
  get outlineLevel() {
36
- return this.#read('outlineLevel');
44
+ return this.read('outlineLevel');
37
45
  }
38
46
  set outlineLevel(outlineLevel) {
39
- this.#write('outlineLevel', outlineLevel);
47
+ this.write('outlineLevel', outlineLevel);
40
48
  }
41
49
  get collapsed() {
42
- return this.#read('collapsed');
50
+ return this.read('collapsed');
43
51
  }
44
52
  set collapsed(collapsed) {
45
- this.#write('collapsed', collapsed);
53
+ this.write('collapsed', collapsed);
46
54
  }
47
55
  get fill() {
48
- return this.#read('fill');
56
+ return this.read('fill');
49
57
  }
50
58
  set fill(fill) {
51
- this.#write('fill', fill);
59
+ this.write('fill', fill);
52
60
  }
53
61
  get numFmt() {
54
- return this.#read('numFmt');
62
+ return this.read('numFmt');
55
63
  }
56
64
  set numFmt(numFmt) {
57
- this.#write('numFmt', numFmt);
65
+ this.write('numFmt', numFmt);
58
66
  }
59
67
  get font() {
60
- return this.#read('font');
68
+ return this.read('font');
61
69
  }
62
70
  set font(font) {
63
- this.#write('font', font);
71
+ this.write('font', font);
64
72
  }
65
73
  get border() {
66
- return this.#read('border');
74
+ return this.read('border');
67
75
  }
68
76
  set border(border) {
69
- this.#write('border', border);
77
+ this.write('border', border);
70
78
  }
71
79
  get alignment() {
72
- return this.#read('alignment');
80
+ return this.read('alignment');
73
81
  }
74
82
  set alignment(alignment) {
75
- this.#write('alignment', alignment);
83
+ this.write('alignment', alignment);
76
84
  }
77
85
  get protection() {
78
- return this.#read('protection');
86
+ return this.read('protection');
79
87
  }
80
88
  set protection(protection) {
81
- this.#write('protection', protection);
89
+ this.write('protection', protection);
82
90
  }
83
91
  getCell(row) {
84
92
  return this.#sheet.getCell(encodeAddress(this.index, row));
@@ -98,16 +106,4 @@ export class Column {
98
106
  this.getCell(index + 1).value = value;
99
107
  });
100
108
  }
101
- #read(key) {
102
- return this.#sheet[INTERNAL].columnPropertiesOf(this.index)?.[key];
103
- }
104
- #write(key, value) {
105
- if (value === undefined) {
106
- const properties = this.#sheet[INTERNAL].columnPropertiesOf(this.index);
107
- if (properties !== undefined)
108
- delete properties[key];
109
- return;
110
- }
111
- this.#sheet[INTERNAL].ensureColumnProperties(this.index)[key] = value;
112
- }
113
109
  }
@@ -10,6 +10,11 @@ export declare class ConditionalFormattingOverlay {
10
10
  add(formatting: ConditionalFormatting): void;
11
11
  /** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
12
12
  get entries(): readonly ConditionalFormatting[];
13
+ /**
14
+ * Re-anchor every rule set through a row or column splice, so a highlight keeps covering the cells
15
+ * it was written for. A rule set whose every target area fell inside a deleted span goes with them.
16
+ */
17
+ shift(axis: 'row' | 'col', start: number, count: number, delta: number): void;
13
18
  /** Drop every conditional formatting, leaving the overlay empty. */
14
19
  clear(): void;
15
20
  }
@@ -1,4 +1,6 @@
1
1
  import { cloneConditionalFormatting } from './conditional-formatting.js';
2
+ import { replaceContents } from './containers.js';
3
+ import { shiftSqref } from './merge.js';
2
4
  export class ConditionalFormattingOverlay {
3
5
  #entries = [];
4
6
  add(formatting) {
@@ -7,6 +9,15 @@ export class ConditionalFormattingOverlay {
7
9
  get entries() {
8
10
  return this.#entries;
9
11
  }
12
+ shift(axis, start, count, delta) {
13
+ const entries = [];
14
+ for (const entry of this.#entries) {
15
+ const ref = shiftSqref(entry.ref, axis, start, count, delta);
16
+ if (ref !== undefined)
17
+ entries.push({ ...entry, ref });
18
+ }
19
+ replaceContents(this.#entries, entries);
20
+ }
10
21
  clear() {
11
22
  this.#entries.length = 0;
12
23
  }