@shbernal/ts-xlsx 2.0.0 → 2.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 (106) 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 +11 -0
  7. package/dist/core/autofilter.js +35 -10
  8. package/dist/core/color-resolution.d.ts +1 -1
  9. package/dist/core/color-resolution.js +7 -8
  10. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  11. package/dist/core/conditional-formatting-overlay.js +11 -0
  12. package/dist/core/conditional-formatting.d.ts +45 -5
  13. package/dist/core/conditional-formatting.js +88 -0
  14. package/dist/core/data-validation-overlay.d.ts +11 -0
  15. package/dist/core/data-validation-overlay.js +21 -2
  16. package/dist/core/data-validation.d.ts +9 -2
  17. package/dist/core/data-validation.js +34 -0
  18. package/dist/core/grid-edits.d.ts +16 -0
  19. package/dist/core/grid-edits.js +26 -8
  20. package/dist/core/grid-shift.d.ts +19 -0
  21. package/dist/core/grid-shift.js +8 -0
  22. package/dist/core/image.d.ts +2 -0
  23. package/dist/core/image.js +4 -0
  24. package/dist/core/merge.d.ts +12 -1
  25. package/dist/core/merge.js +36 -2
  26. package/dist/core/page-setup.d.ts +16 -2
  27. package/dist/core/page-setup.js +8 -1
  28. package/dist/core/pivot-table.d.ts +2 -0
  29. package/dist/core/pivot-table.js +25 -16
  30. package/dist/core/protection.d.ts +3 -0
  31. package/dist/core/protection.js +20 -14
  32. package/dist/core/row-input.d.ts +7 -0
  33. package/dist/core/row-input.js +10 -8
  34. package/dist/core/style.d.ts +27 -0
  35. package/dist/core/style.js +82 -67
  36. package/dist/core/table.d.ts +25 -11
  37. package/dist/core/table.js +67 -31
  38. package/dist/core/theme.d.ts +3 -1
  39. package/dist/core/theme.js +4 -0
  40. package/dist/core/workbook.d.ts +2 -2
  41. package/dist/core/worksheet-comments.d.ts +8 -0
  42. package/dist/core/worksheet-comments.js +18 -0
  43. package/dist/core/worksheet.d.ts +15 -2
  44. package/dist/core/worksheet.js +28 -48
  45. package/dist/customui/ribbon.js +24 -23
  46. package/dist/entries/core.d.ts +3 -3
  47. package/dist/entries/csv.d.ts +1 -1
  48. package/dist/entries/node-unavailable.d.ts +15 -0
  49. package/dist/entries/node-unavailable.js +20 -0
  50. package/dist/entries/node.d.ts +1 -0
  51. package/dist/entries/node.js +1 -0
  52. package/dist/entries/xlsx.d.ts +0 -1
  53. package/dist/entries/xlsx.js +0 -1
  54. package/dist/io/csv/read.js +1 -1
  55. package/dist/io/csv/write.d.ts +12 -2
  56. package/dist/io/csv/write.js +17 -2
  57. package/dist/io/opc/inflate.js +2 -12
  58. package/dist/io/opc/rels.d.ts +11 -0
  59. package/dist/io/opc/rels.js +3 -2
  60. package/dist/io/style/xf-style.d.ts +7 -2
  61. package/dist/io/style/xf-style.js +6 -1
  62. package/dist/io/xlsx/cell-accumulator.d.ts +1 -3
  63. package/dist/io/xlsx/cell-accumulator.js +20 -58
  64. package/dist/io/xlsx/cell-value.d.ts +1 -1
  65. package/dist/io/xlsx/cell-value.js +7 -4
  66. package/dist/io/xlsx/color-xml.d.ts +1 -1
  67. package/dist/io/xlsx/color-xml.js +7 -6
  68. package/dist/io/xlsx/comments.js +4 -2
  69. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  70. package/dist/io/xlsx/conditional-formatting.js +60 -45
  71. package/dist/io/xlsx/data-validation.d.ts +1 -1
  72. package/dist/io/xlsx/data-validation.js +35 -33
  73. package/dist/io/xlsx/hyperlinks.js +6 -14
  74. package/dist/io/xlsx/images.js +19 -22
  75. package/dist/io/xlsx/read-pivot.js +2 -8
  76. package/dist/io/xlsx/read-shared-strings.js +8 -47
  77. package/dist/io/xlsx/read-styles.js +2 -1
  78. package/dist/io/xlsx/read-worksheet.js +14 -16
  79. package/dist/io/xlsx/read.d.ts +1 -1
  80. package/dist/io/xlsx/read.js +44 -74
  81. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  82. package/dist/io/xlsx/rich-runs.js +94 -30
  83. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  84. package/dist/io/xlsx/sheet-properties.js +13 -9
  85. package/dist/io/xlsx/styles.d.ts +3 -15
  86. package/dist/io/xlsx/styles.js +63 -88
  87. package/dist/io/xlsx/tables.js +13 -13
  88. package/dist/io/xlsx/theme-xml.js +7 -4
  89. package/dist/io/xlsx/threaded-comments.js +19 -19
  90. package/dist/io/xlsx/workbook-xml.js +14 -8
  91. package/dist/io/xlsx/worksheet-xml.js +34 -37
  92. package/dist/io/xlsx/write-stream.d.ts +8 -1
  93. package/dist/io/xlsx/write-stream.js +10 -11
  94. package/dist/sha512.d.ts +2 -0
  95. package/dist/sha512.js +146 -0
  96. package/dist/vba/bytes.d.ts +1 -2
  97. package/dist/vba/bytes.js +1 -12
  98. package/dist/vba/dir-records.d.ts +53 -0
  99. package/dist/vba/dir-records.js +28 -0
  100. package/dist/vba/project-editor.js +5 -34
  101. package/dist/vba/project.js +2 -18
  102. package/dist/xml/xml-read.d.ts +60 -0
  103. package/dist/xml/xml-read.js +47 -0
  104. package/dist/xml/xml.d.ts +43 -1
  105. package/dist/xml/xml.js +17 -5
  106. package/package.json +21 -4
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);
@@ -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,15 +1,16 @@
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
- ]);
2
+ import { decodeRange, encodeAddress } from './address.js';
3
+ import { isDeletedSpan, shiftIndex } from './grid-shift.js';
4
+ const CUSTOM_FILTER_OPERATORS = {
5
+ equal: true,
6
+ notEqual: true,
7
+ lessThan: true,
8
+ lessThanOrEqual: true,
9
+ greaterThan: true,
10
+ greaterThanOrEqual: true,
11
+ };
11
12
  export function isCustomFilterOperator(value) {
12
- return CUSTOM_FILTER_OPERATORS.has(value);
13
+ return Object.hasOwn(CUSTOM_FILTER_OPERATORS, value);
13
14
  }
14
15
  export function canonicalizeAutoFilter(input) {
15
16
  const ref = typeof input === 'string' ? input : input.ref;
@@ -37,3 +38,27 @@ function canonicalizeColumn(column, width) {
37
38
  }
38
39
  return column;
39
40
  }
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) {
44
+ return filter;
45
+ }
46
+ const [lo, hi] = axis === 'row' ? [top, bottom] : [left, right];
47
+ if (isDeletedSpan(lo, hi, start, count))
48
+ 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)}`;
53
+ return { ref, columns: filter.columns };
54
+ }
55
+ const ref = `${encodeAddress(movedLo, top)}:${encodeAddress(movedHi, bottom)}`;
56
+ const columns = [];
57
+ for (const column of filter.columns) {
58
+ const absolute = left + column.colId;
59
+ if (isDeletedSpan(absolute, absolute, start, count))
60
+ continue;
61
+ columns.push({ ...column, colId: shiftIndex(absolute, start, count, delta, axis) - movedLo });
62
+ }
63
+ return { ref, columns };
64
+ }
@@ -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;
@@ -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
  }
@@ -1,26 +1,66 @@
1
1
  import type { Color, DifferentialStyle } from './style.ts';
2
+ /** How a {@link CfValueObject} reads its `value`: `ST_CfvoType` verbatim. */
3
+ export type CfValueObjectType = 'num' | 'percent' | 'max' | 'min' | 'percentile' | 'formula';
4
+ /** Narrow a raw `<cfvo type>` token to a known {@link CfValueObjectType}. */
5
+ export declare function isCfValueObjectType(value: string): value is CfValueObjectType;
2
6
  /**
3
7
  * One anchor of a colour-scale, data-bar, or icon-set scale: a "conditional format value object".
4
8
  * `type` names how `value` is read: a literal `num`, a `percent`/`percentile` of the range, a
5
9
  * `formula`, or the range's own `min`/`max` (which carry no value).
6
10
  */
7
11
  export interface CfValueObject {
8
- type: 'num' | 'percent' | 'max' | 'min' | 'percentile' | 'formula';
12
+ type: CfValueObjectType;
9
13
  value?: number | string;
10
14
  }
15
+ /**
16
+ * What a rule tests, as `<cfRule type>` carries it: `ST_CfType` verbatim.
17
+ *
18
+ * Closed, and stated in full rather than left as `string`, even though the library models only some
19
+ * of these in depth. Depth of modelling and legality are different questions: a `timePeriod` rule
20
+ * whose operands the library never inspects still round-trips, while a token outside this list is one
21
+ * Excel refuses to open, so it is refused on the way in and on the way out alike.
22
+ */
23
+ export type ConditionalFormattingType = 'expression' | 'cellIs' | 'colorScale' | 'dataBar' | 'iconSet' | 'top10' | 'uniqueValues' | 'duplicateValues' | 'containsText' | 'notContainsText' | 'beginsWith' | 'endsWith' | 'containsBlanks' | 'notContainsBlanks' | 'containsErrors' | 'notContainsErrors' | 'timePeriod' | 'aboveAverage';
24
+ /** Narrow a raw `<cfRule type>` token to a known {@link ConditionalFormattingType}. */
25
+ export declare function isConditionalFormattingType(value: string): value is ConditionalFormattingType;
26
+ /**
27
+ * How a `cellIs` or text rule compares, as `ST_ConditionalFormattingOperator` enumerates it.
28
+ *
29
+ * Overlaps {@link import('./data-validation.ts').DataValidationOperator} in four members and diverges
30
+ * in the rest: this one has the text comparisons a validation has no use for, and spells "does not
31
+ * contain" as `notContains` where nothing else in the format does.
32
+ */
33
+ export type ConditionalFormattingOperator = 'lessThan' | 'lessThanOrEqual' | 'equal' | 'notEqual' | 'greaterThanOrEqual' | 'greaterThan' | 'between' | 'notBetween' | 'containsText' | 'notContains' | 'beginsWith' | 'endsWith';
34
+ /** Narrow a raw `<cfRule operator>` token to a known {@link ConditionalFormattingOperator}. */
35
+ export declare function isConditionalFormattingOperator(value: string): value is ConditionalFormattingOperator;
36
+ /** The window a `timePeriod` rule matches against, relative to the day the sheet is recalculated. */
37
+ export type CfTimePeriod = 'today' | 'yesterday' | 'tomorrow' | 'last7Days' | 'thisMonth' | 'lastMonth' | 'nextMonth' | 'thisWeek' | 'lastWeek' | 'nextWeek';
38
+ /** Narrow a raw `<cfRule timePeriod>` token to a known {@link CfTimePeriod}. */
39
+ export declare function isCfTimePeriod(value: string): value is CfTimePeriod;
40
+ /**
41
+ * The named icon family an `iconSet` rule draws from, as `ST_IconSetType` enumerates it. The leading
42
+ * digit is the number of icons, which is also how many {@link CfValueObject} anchors the rule needs.
43
+ *
44
+ * The 2009 extension adds three more families (`3Stars`, `3Triangles`, `5Boxes`) under its own
45
+ * namespace. They are absent here because the classic `<iconSet>` element this list types cannot
46
+ * carry them; a file using one states it in the extension, which the library round-trips verbatim.
47
+ */
48
+ export type IconSetType = '3Arrows' | '3ArrowsGray' | '3Flags' | '3TrafficLights1' | '3TrafficLights2' | '3Signs' | '3Symbols' | '3Symbols2' | '4Arrows' | '4ArrowsGray' | '4RedToBlack' | '4Rating' | '4TrafficLights' | '5Arrows' | '5ArrowsGray' | '5Rating' | '5Quarters';
49
+ /** Narrow a raw `<iconSet iconSet>` token to a known {@link IconSetType}. */
50
+ export declare function isIconSetType(value: string): value is IconSetType;
11
51
  /**
12
52
  * A single conditional-formatting rule. `type` is the OOXML cfRule type; the remaining fields carry
13
53
  * the operands that type needs and are absent otherwise. A rule the library does not model in depth
14
54
  * still preserves `type`, `priority`, `operator`, `formulae`, and `dxfId` across a round-trip.
15
55
  */
16
56
  export interface ConditionalFormattingRule {
17
- type: string;
57
+ type: ConditionalFormattingType;
18
58
  /** Evaluation precedence; lower wins. Excel requires one, so the writer supplies it when absent. */
19
59
  priority?: number;
20
60
  /** Halt evaluation of lower-priority rules on any cell this rule matches. */
21
61
  stopIfTrue?: boolean;
22
62
  /** cellIs / text comparison operator (`greaterThan`, `between`, `beginsWith`, …). */
23
- operator?: string;
63
+ operator?: ConditionalFormattingOperator;
24
64
  /** Formula operands: cellIs bounds, an expression predicate, a containsText target formula, … */
25
65
  formulae?: (string | number)[];
26
66
  /** The literal a containsText / beginsWith / endsWith rule searches for. */
@@ -42,7 +82,7 @@ export interface ConditionalFormattingRule {
42
82
  /** A dataBar's axis colour (the zero line between positive and negative bars). An x14 property. */
43
83
  axisColor?: Color;
44
84
  /** An iconSet's named icon family (e.g. `3TrafficLights1`). */
45
- iconSet?: string;
85
+ iconSet?: IconSetType;
46
86
  /** top10 rank cutoff. */
47
87
  rank?: number;
48
88
  /** top10: the rank is a percentage rather than a count. */
@@ -56,7 +96,7 @@ export interface ConditionalFormattingRule {
56
96
  /** aboveAverage: match beyond this many standard deviations. */
57
97
  stdDev?: number;
58
98
  /** timePeriod window (`today`, `lastWeek`, …). */
59
- timePeriod?: string;
99
+ timePeriod?: CfTimePeriod;
60
100
  }
61
101
  /** A set of rules bound to the range(s) they cover. `ref` is an OOXML `sqref`: one or more
62
102
  * space-separated areas (`"A1:C1 A3:C3 A5:C5"`), the shape Excel writes when one rule is applied to
@@ -1,3 +1,91 @@
1
+ const CF_VALUE_OBJECT_TYPES = {
2
+ num: true,
3
+ percent: true,
4
+ max: true,
5
+ min: true,
6
+ percentile: true,
7
+ formula: true,
8
+ };
9
+ export function isCfValueObjectType(value) {
10
+ return Object.hasOwn(CF_VALUE_OBJECT_TYPES, value);
11
+ }
12
+ const CONDITIONAL_FORMATTING_TYPES = {
13
+ expression: true,
14
+ cellIs: true,
15
+ colorScale: true,
16
+ dataBar: true,
17
+ iconSet: true,
18
+ top10: true,
19
+ uniqueValues: true,
20
+ duplicateValues: true,
21
+ containsText: true,
22
+ notContainsText: true,
23
+ beginsWith: true,
24
+ endsWith: true,
25
+ containsBlanks: true,
26
+ notContainsBlanks: true,
27
+ containsErrors: true,
28
+ notContainsErrors: true,
29
+ timePeriod: true,
30
+ aboveAverage: true,
31
+ };
32
+ export function isConditionalFormattingType(value) {
33
+ return Object.hasOwn(CONDITIONAL_FORMATTING_TYPES, value);
34
+ }
35
+ const CONDITIONAL_FORMATTING_OPERATORS = {
36
+ lessThan: true,
37
+ lessThanOrEqual: true,
38
+ equal: true,
39
+ notEqual: true,
40
+ greaterThanOrEqual: true,
41
+ greaterThan: true,
42
+ between: true,
43
+ notBetween: true,
44
+ containsText: true,
45
+ notContains: true,
46
+ beginsWith: true,
47
+ endsWith: true,
48
+ };
49
+ export function isConditionalFormattingOperator(value) {
50
+ return Object.hasOwn(CONDITIONAL_FORMATTING_OPERATORS, value);
51
+ }
52
+ const CF_TIME_PERIODS = {
53
+ today: true,
54
+ yesterday: true,
55
+ tomorrow: true,
56
+ last7Days: true,
57
+ thisMonth: true,
58
+ lastMonth: true,
59
+ nextMonth: true,
60
+ thisWeek: true,
61
+ lastWeek: true,
62
+ nextWeek: true,
63
+ };
64
+ export function isCfTimePeriod(value) {
65
+ return Object.hasOwn(CF_TIME_PERIODS, value);
66
+ }
67
+ const ICON_SET_TYPES = {
68
+ '3Arrows': true,
69
+ '3ArrowsGray': true,
70
+ '3Flags': true,
71
+ '3TrafficLights1': true,
72
+ '3TrafficLights2': true,
73
+ '3Signs': true,
74
+ '3Symbols': true,
75
+ '3Symbols2': true,
76
+ '4Arrows': true,
77
+ '4ArrowsGray': true,
78
+ '4RedToBlack': true,
79
+ '4Rating': true,
80
+ '4TrafficLights': true,
81
+ '5Arrows': true,
82
+ '5ArrowsGray': true,
83
+ '5Rating': true,
84
+ '5Quarters': true,
85
+ };
86
+ export function isIconSetType(value) {
87
+ return Object.hasOwn(ICON_SET_TYPES, value);
88
+ }
1
89
  export function cloneConditionalFormatting(cf) {
2
90
  return { ref: cf.ref, rules: cf.rules.map(cloneRule) };
3
91
  }
@@ -11,6 +11,11 @@ export declare class DataValidationOverlay {
11
11
  * (`<x14:dataValidation>`), the carrier Excel uses for a list source on another sheet and other
12
12
  * shapes the standard element cannot express. The reader sets it for a rule found in that form so a
13
13
  * round-trip writes it back there instead of silently corrupting the cross-sheet reference.
14
+ *
15
+ * @throws {AuthoringError} when `sqref` names no area at all. A rule attached to nothing covers no
16
+ * cell and is written back as the same unreadable text, so it is a mistake worth surfacing at the
17
+ * call. The reader does not reach this: it drops such an entry at its own boundary, where a foreign
18
+ * file's malformed attribute is supposed to be dropped.
14
19
  */
15
20
  add(sqref: string, rule: DataValidation, options?: {
16
21
  extended?: boolean;
@@ -23,6 +28,12 @@ export declare class DataValidationOverlay {
23
28
  * validations.
24
29
  */
25
30
  at(col: number, row: number): DataValidation | undefined;
31
+ /**
32
+ * Re-anchor every rule through a row or column splice, so a dropdown stays on the cells it was
33
+ * attached to rather than on whatever moved into their place. A rule whose every target area fell
34
+ * inside a deleted span is dropped with them.
35
+ */
36
+ shift(axis: 'row' | 'col', start: number, count: number, delta: number): void;
26
37
  /** Drop every validation, leaving the overlay empty. */
27
38
  clear(): void;
28
39
  }
@@ -1,15 +1,21 @@
1
+ import { AuthoringError } from '../errors.js';
2
+ import { replaceContents } from './containers.js';
1
3
  import { cloneDataValidation, } from './data-validation.js';
2
- import { decodeSqrefRects } from './merge.js';
4
+ import { decodeSqrefRects, shiftSqref } from './merge.js';
3
5
  export class DataValidationOverlay {
4
6
  #entries = [];
5
7
  #rects = [];
6
8
  add(sqref, rule, options = {}) {
9
+ const rects = decodeSqrefRects(sqref);
10
+ if (rects.length === 0) {
11
+ throw new AuthoringError(`data validation range "${sqref}" names no cells`);
12
+ }
7
13
  const stored = cloneDataValidation(rule);
8
14
  const entry = { sqref, rule: stored };
9
15
  if (options.extended)
10
16
  entry.extended = true;
11
17
  this.#entries.push(entry);
12
- this.#rects.push({ rects: decodeSqrefRects(sqref), rule: stored });
18
+ this.#rects.push({ rects, rule: stored });
13
19
  }
14
20
  get entries() {
15
21
  return this.#entries;
@@ -24,6 +30,19 @@ export class DataValidationOverlay {
24
30
  }
25
31
  return undefined;
26
32
  }
33
+ shift(axis, start, count, delta) {
34
+ const entries = [];
35
+ const rects = [];
36
+ for (const entry of this.#entries) {
37
+ const sqref = shiftSqref(entry.sqref, axis, start, count, delta);
38
+ if (sqref === undefined)
39
+ continue;
40
+ entries.push({ ...entry, sqref });
41
+ rects.push({ rects: decodeSqrefRects(sqref), rule: entry.rule });
42
+ }
43
+ replaceContents(this.#entries, entries);
44
+ replaceContents(this.#rects, rects);
45
+ }
27
46
  clear() {
28
47
  this.#entries.length = 0;
29
48
  this.#rects.length = 0;
@@ -1,11 +1,18 @@
1
1
  /** The kind of constraint a validation enforces. `list` is a dropdown; `custom` is an arbitrary
2
- * boolean formula; the rest bound a typed value (`whole`/`decimal`/`date`/`time`/`textLength`). */
3
- export type DataValidationType = 'list' | 'whole' | 'decimal' | 'date' | 'time' | 'textLength' | 'custom';
2
+ * boolean formula; `none` constrains nothing and exists only to carry the rule's messages; the rest
3
+ * bound a typed value (`whole`/`decimal`/`date`/`time`/`textLength`). */
4
+ export type DataValidationType = 'none' | 'list' | 'whole' | 'decimal' | 'date' | 'time' | 'textLength' | 'custom';
5
+ /** Narrow a raw `<dataValidation type>` token to a known {@link DataValidationType}. */
6
+ export declare function isDataValidationType(value: string): value is DataValidationType;
4
7
  /** How a typed validation compares its operand(s). Absent on a `list`/`custom` rule; defaults to
5
8
  * `between` on a typed rule (the value Excel omits from the XML). */
6
9
  export type DataValidationOperator = 'between' | 'notBetween' | 'equal' | 'notEqual' | 'greaterThan' | 'lessThan' | 'greaterThanOrEqual' | 'lessThanOrEqual';
10
+ /** Narrow a raw `<dataValidation operator>` token to a known {@link DataValidationOperator}. */
11
+ export declare function isDataValidationOperator(value: string): value is DataValidationOperator;
7
12
  /** How Excel reacts to input that fails the rule. */
8
13
  export type DataValidationErrorStyle = 'stop' | 'warning' | 'information';
14
+ /** Narrow a raw `<dataValidation errorStyle>` token to a known {@link DataValidationErrorStyle}. */
15
+ export declare function isDataValidationErrorStyle(value: string): value is DataValidationErrorStyle;
9
16
  /** One validation rule. `formulae` holds the operand(s), `formula1` then optional `formula2`: a
10
17
  * numeric literal is stored as a number, while a cell reference, defined name, or list source keeps
11
18
  * its verbatim string. */