@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
@@ -1,83 +1,93 @@
1
- const NAMED_UNDERLINE_STYLES = new Set([
2
- 'none',
3
- 'single',
4
- 'double',
5
- 'singleAccounting',
6
- 'doubleAccounting',
7
- ]);
1
+ const NAMED_UNDERLINE_STYLES = {
2
+ none: true,
3
+ single: true,
4
+ double: true,
5
+ singleAccounting: true,
6
+ doubleAccounting: true,
7
+ };
8
8
  export function isNamedUnderlineStyle(value) {
9
- return NAMED_UNDERLINE_STYLES.has(value);
9
+ return Object.hasOwn(NAMED_UNDERLINE_STYLES, value);
10
+ }
11
+ export function parseArgb(value) {
12
+ const hex = value.startsWith('#') ? value.slice(1) : value;
13
+ const argb = hex.length === 6 ? `FF${hex}` : hex;
14
+ return /^[0-9a-fA-F]{8}$/.test(argb) ? argb : undefined;
10
15
  }
11
- const FILL_PATTERN_TYPES = new Set([
12
- 'none',
13
- 'solid',
14
- 'gray125',
15
- 'darkGray',
16
- 'mediumGray',
17
- 'lightGray',
18
- 'gray0625',
19
- 'darkHorizontal',
20
- 'darkVertical',
21
- 'darkDown',
22
- 'darkUp',
23
- 'darkGrid',
24
- 'darkTrellis',
25
- 'lightHorizontal',
26
- 'lightVertical',
27
- 'lightDown',
28
- 'lightUp',
29
- 'lightGrid',
30
- 'lightTrellis',
31
- ]);
16
+ const FILL_PATTERN_TYPES = {
17
+ none: true,
18
+ solid: true,
19
+ gray125: true,
20
+ darkGray: true,
21
+ mediumGray: true,
22
+ lightGray: true,
23
+ gray0625: true,
24
+ darkHorizontal: true,
25
+ darkVertical: true,
26
+ darkDown: true,
27
+ darkUp: true,
28
+ darkGrid: true,
29
+ darkTrellis: true,
30
+ lightHorizontal: true,
31
+ lightVertical: true,
32
+ lightDown: true,
33
+ lightUp: true,
34
+ lightGrid: true,
35
+ lightTrellis: true,
36
+ };
32
37
  export function isFillPatternType(value) {
33
- return FILL_PATTERN_TYPES.has(value);
38
+ return Object.hasOwn(FILL_PATTERN_TYPES, value);
34
39
  }
35
- const BORDER_STYLES = new Set([
36
- 'thin',
37
- 'medium',
38
- 'thick',
39
- 'dashed',
40
- 'dotted',
41
- 'double',
42
- 'hair',
43
- 'mediumDashed',
44
- 'dashDot',
45
- 'mediumDashDot',
46
- 'dashDotDot',
47
- 'mediumDashDotDot',
48
- 'slantDashDot',
49
- ]);
40
+ const BORDER_STYLES = {
41
+ thin: true,
42
+ medium: true,
43
+ thick: true,
44
+ dashed: true,
45
+ dotted: true,
46
+ double: true,
47
+ hair: true,
48
+ mediumDashed: true,
49
+ dashDot: true,
50
+ mediumDashDot: true,
51
+ dashDotDot: true,
52
+ mediumDashDotDot: true,
53
+ slantDashDot: true,
54
+ };
50
55
  export function isBorderStyle(value) {
51
- return BORDER_STYLES.has(value);
56
+ return Object.hasOwn(BORDER_STYLES, value);
52
57
  }
58
+ const FONT_VERTICAL_ALIGNMENTS = {
59
+ superscript: true,
60
+ subscript: true,
61
+ };
53
62
  export function isFontVerticalAlignment(value) {
54
- return value === 'superscript' || value === 'subscript';
63
+ return Object.hasOwn(FONT_VERTICAL_ALIGNMENTS, value);
55
64
  }
65
+ const FONT_SCHEMES = { minor: true, major: true, none: true };
56
66
  export function isFontScheme(value) {
57
- return value === 'minor' || value === 'major' || value === 'none';
67
+ return Object.hasOwn(FONT_SCHEMES, value);
58
68
  }
59
- const HORIZONTAL_ALIGNMENTS = new Set([
60
- 'general',
61
- 'left',
62
- 'center',
63
- 'right',
64
- 'fill',
65
- 'justify',
66
- 'centerContinuous',
67
- 'distributed',
68
- ]);
69
+ const HORIZONTAL_ALIGNMENTS = {
70
+ general: true,
71
+ left: true,
72
+ center: true,
73
+ right: true,
74
+ fill: true,
75
+ justify: true,
76
+ centerContinuous: true,
77
+ distributed: true,
78
+ };
69
79
  export function isHorizontalAlignment(value) {
70
- return HORIZONTAL_ALIGNMENTS.has(value);
80
+ return Object.hasOwn(HORIZONTAL_ALIGNMENTS, value);
71
81
  }
72
- const VERTICAL_ALIGNMENTS = new Set([
73
- 'top',
74
- 'center',
75
- 'bottom',
76
- 'justify',
77
- 'distributed',
78
- ]);
82
+ const VERTICAL_ALIGNMENTS = {
83
+ top: true,
84
+ center: true,
85
+ bottom: true,
86
+ justify: true,
87
+ distributed: true,
88
+ };
79
89
  export function isVerticalAlignment(value) {
80
- return VERTICAL_ALIGNMENTS.has(value);
90
+ return Object.hasOwn(VERTICAL_ALIGNMENTS, value);
81
91
  }
82
92
  const CELL_STYLE_FACET_KEYS = {
83
93
  fill: true,
@@ -92,6 +102,11 @@ export function assignStyleFacets(target, source) {
92
102
  for (const facet of CELL_STYLE_FACETS)
93
103
  copyFacet(target, source, facet);
94
104
  }
105
+ export function pickStyleFacets(source) {
106
+ const facets = {};
107
+ assignStyleFacets(facets, source);
108
+ return facets;
109
+ }
95
110
  function copyFacet(target, source, key) {
96
111
  const value = source[key];
97
112
  if (value !== undefined)
@@ -5,16 +5,30 @@ import type { CellValue } from './value.ts';
5
5
  * bakes into the cells rather than storing as table metadata. Every facet ({@link CellStyle}) is
6
6
  * optional; only the ones set are applied, leaving the rest of each cell's style untouched. */
7
7
  export type TableColumnStyle = Readonly<CellStyle>;
8
- /** Writes a value into the owning worksheet's grid at a 1-based row/column, applying the column's
9
- * style (if any) to the cell: the hook a {@link Table} uses to materialise the cells of a row
10
- * appended through {@link Table.addRow}. A worksheet supplies it when it registers the table; a table
11
- * built standalone has none and cannot write cell values. */
12
- export type TableCellWriter = (row: number, col: number, value: CellValue, style?: TableColumnStyle) => void;
13
- /** Inserts one empty row into the owning worksheet's grid at a 1-based `row`, shifting that row and
14
- * everything below it down by one: the hook a {@link Table} with a totals row uses to open a slot
15
- * for an appended data row above the totals. Relocating the totals row lives in the grid, so a
16
- * standalone table has no inserter and cannot append past a totals row. */
17
- export type TableRowInserter = (row: number) => void;
8
+ /**
9
+ * The channel a registered table holds into its owning worksheet's grid. A worksheet supplies it
10
+ * when it registers the table; a table built standalone (a unit test, a bare model) has none, so it
11
+ * can be inspected but cannot materialise or append cells, and appending throws rather than
12
+ * silently dropping the values.
13
+ *
14
+ * All three coordinates are 1-based.
15
+ */
16
+ export interface TableGrid {
17
+ /**
18
+ * Whether the cell at this position already holds a value. The materialiser's round-trip guard
19
+ * asks this and nothing else: it must not create the cell, because asking whether a table's frame
20
+ * is already filled would otherwise fill the grid with the empty cells it was asking about.
21
+ */
22
+ holdsValue(row: number, col: number): boolean;
23
+ /** Write a value, applying the column's style (if any) to the cell. */
24
+ writeCell(row: number, col: number, value: CellValue, style?: TableColumnStyle): void;
25
+ /**
26
+ * Insert one empty row at `row`, shifting that row and everything below it down by one: how a
27
+ * table with a totals row opens a slot for an appended data row above the totals. Relocating the
28
+ * totals row lives in the grid, which is why this is the grid's job and not the table's.
29
+ */
30
+ insertRow(row: number): void;
31
+ }
18
32
  /**
19
33
  * A table's visual style (`<tableStyleInfo>`): the named style to apply plus the banding/highlight
20
34
  * toggles. Every field is a tri-state so a round-trip stays faithful: a value present in the source
@@ -127,7 +141,7 @@ export declare class Table {
127
141
  readonly totalsRowShown: boolean | undefined;
128
142
  readonly autoFilter: boolean;
129
143
  readonly style: TableStyleInfo | undefined;
130
- constructor(options: TableOptions, writeCell?: TableCellWriter, insertRow?: TableRowInserter);
144
+ constructor(options: TableOptions, grid?: TableGrid);
131
145
  get columnCount(): number;
132
146
  /** The number of data rows (excludes the header and totals rows). Always defined: a table loaded
133
147
  * from a file derives it from the stored range, so reading the height never throws. */
@@ -1,5 +1,6 @@
1
1
  import { AuthoringError } from '../errors.js';
2
2
  import { decodeCellRef, encodeAddress } from './address.js';
3
+ import { isDeletedSpan, shiftIndex } from './grid-shift.js';
3
4
  function cloneStyleInfo(style) {
4
5
  const clone = {};
5
6
  if (style.name !== undefined && style.name !== 'None')
@@ -24,20 +25,20 @@ export const TOTALS_ROW_SUBTOTAL_CODE = {
24
25
  sum: 109,
25
26
  var: 110,
26
27
  };
27
- const TOTALS_ROW_FUNCTIONS = new Set([
28
- 'average',
29
- 'countNums',
30
- 'count',
31
- 'max',
32
- 'min',
33
- 'stdDev',
34
- 'sum',
35
- 'var',
36
- 'custom',
37
- 'none',
38
- ]);
28
+ const TOTALS_ROW_FUNCTIONS = {
29
+ average: true,
30
+ countNums: true,
31
+ count: true,
32
+ max: true,
33
+ min: true,
34
+ stdDev: true,
35
+ sum: true,
36
+ var: true,
37
+ custom: true,
38
+ none: true,
39
+ };
39
40
  export function isTotalsRowFunction(value) {
40
- return TOTALS_ROW_FUNCTIONS.has(value);
41
+ return Object.hasOwn(TOTALS_ROW_FUNCTIONS, value);
41
42
  }
42
43
  const IDENTIFIER = /^[\p{L}\\_][\p{L}\p{N}._]*$/u;
43
44
  function disambiguateColumnNames(columns) {
@@ -71,9 +72,8 @@ export class Table {
71
72
  #anchorCol;
72
73
  #anchorRow;
73
74
  #dataRowCount;
74
- #writeCell;
75
- #insertRow;
76
- constructor(options, writeCell, insertRow) {
75
+ #grid;
76
+ constructor(options, grid) {
77
77
  validateTableName(options.name);
78
78
  if (options.columns.length === 0) {
79
79
  throw new AuthoringError(`table "${options.name}" must declare at least one column`);
@@ -102,11 +102,12 @@ export class Table {
102
102
  this.#anchorCol = col;
103
103
  this.#anchorRow = row;
104
104
  this.#dataRowCount = options.rowCount;
105
- this.#writeCell = writeCell;
106
- this.#insertRow = insertRow;
105
+ this.#grid = grid;
107
106
  if (this.#rowSpan < 1) {
108
107
  throw new AuthoringError(`table "${this.name}" has no rows: it needs a header row or at least one data row`);
109
108
  }
109
+ if (grid !== undefined)
110
+ this.#materializeFrame(grid);
110
111
  }
111
112
  get columnCount() {
112
113
  return this.columns.length;
@@ -119,32 +120,67 @@ export class Table {
119
120
  throw new RangeError(`row has ${values.length} values but table "${this.name}" has ${this.columnCount} columns`);
120
121
  }
121
122
  const target = this.#anchorRow + (this.headerRow ? 1 : 0) + this.#dataRowCount;
123
+ const grid = this.#grid;
122
124
  if (this.totalsRow) {
123
- if (this.#insertRow === undefined) {
125
+ if (grid === undefined) {
124
126
  throw new AuthoringError(`table "${this.name}" is not attached to a worksheet: cannot relocate its totals row to append a data row`);
125
127
  }
126
- this.#insertRow(target);
127
- values.forEach((value, index) => {
128
- this.#writeCell?.(target, this.#anchorCol + index, value, this.columns[index]?.style);
129
- });
128
+ grid.insertRow(target);
129
+ this.#writeRow(grid, target, values);
130
130
  return;
131
131
  }
132
132
  if (values.length > 0) {
133
- if (this.#writeCell === undefined) {
133
+ if (grid === undefined) {
134
134
  throw new AuthoringError(`table "${this.name}" is not attached to a worksheet: cannot write appended row values`);
135
135
  }
136
- values.forEach((value, index) => {
137
- this.#writeCell?.(target, this.#anchorCol + index, value, this.columns[index]?.style);
138
- });
136
+ this.#writeRow(grid, target, values);
139
137
  }
140
138
  this.#dataRowCount += 1;
141
139
  }
140
+ #writeRow(grid, row, values) {
141
+ values.forEach((value, index) => {
142
+ grid.writeCell(row, this.#anchorCol + index, value, this.columns[index]?.style);
143
+ });
144
+ }
145
+ #materializeFrame(grid) {
146
+ if (this.headerRow) {
147
+ const { top, left } = this.region;
148
+ this.columns.forEach((column, index) => {
149
+ const col = left + index;
150
+ if (grid.holdsValue(top, col))
151
+ return;
152
+ grid.writeCell(top, col, column.name);
153
+ });
154
+ }
155
+ if (this.totalsRow) {
156
+ const { left, bottom } = this.region;
157
+ this.columns.forEach((column, index) => {
158
+ const col = left + index;
159
+ if (grid.holdsValue(bottom, col))
160
+ return;
161
+ if (column.totalsRowLabel !== undefined) {
162
+ grid.writeCell(bottom, col, column.totalsRowLabel);
163
+ return;
164
+ }
165
+ if (column.totalsRowFunction === undefined)
166
+ return;
167
+ const code = TOTALS_ROW_SUBTOTAL_CODE[column.totalsRowFunction];
168
+ if (code !== undefined) {
169
+ grid.writeCell(bottom, col, {
170
+ formula: `SUBTOTAL(${code},${this.name}[${column.name}])`,
171
+ });
172
+ }
173
+ else if (column.totalsRowFunction === 'custom' && column.totalsRowFormula !== undefined) {
174
+ grid.writeCell(bottom, col, { formula: column.totalsRowFormula });
175
+ }
176
+ });
177
+ }
178
+ }
142
179
  shiftRows(start, count, delta) {
143
- if (this.#anchorRow >= start && this.#bottom < start + count)
180
+ if (isDeletedSpan(this.#anchorRow, this.#bottom, start, count))
144
181
  return false;
145
- const shift = (v) => (v < start ? v : v >= start + count ? v + delta : start);
146
- const top = shift(this.#anchorRow);
147
- const bottom = shift(this.#bottom);
182
+ const top = shiftIndex(this.#anchorRow, start, count, delta, 'row');
183
+ const bottom = shiftIndex(this.#bottom, start, count, delta, 'row');
148
184
  const span = bottom - top + 1;
149
185
  const fixedRows = (this.headerRow ? 1 : 0) + (this.totalsRow ? 1 : 0);
150
186
  const dataRows = span - fixedRows;
@@ -15,6 +15,8 @@
15
15
  export declare const THEME_COLOR_SLOTS: readonly ['lt1', 'dk1', 'lt2', 'dk2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6', 'hlink', 'folHlink'];
16
16
  /** One slot of a theme's colour scheme. */
17
17
  export type ThemeColorSlot = (typeof THEME_COLOR_SLOTS)[number];
18
+ /** Narrow a raw `<a:clrScheme>` child name to a known {@link ThemeColorSlot}. */
19
+ export declare function isThemeColorSlot(value: string): value is ThemeColorSlot;
18
20
  /**
19
21
  * A theme's colour scheme: each slot's colour as a 6-hex `RRGGBB` string. Partial because a foreign
20
22
  * theme is free to omit a slot (or express one in a colour model this reader does not decode), and an
@@ -22,7 +24,7 @@ export type ThemeColorSlot = (typeof THEME_COLOR_SLOTS)[number];
22
24
  */
23
25
  export type ThemeColorScheme = Readonly<Partial<Record<ThemeColorSlot, string>>>;
24
26
  /** The Office default colour scheme, matching the theme part the writer emits for a workbook with none. */
25
- export declare const DEFAULT_THEME_COLOR_SCHEME: ThemeColorScheme;
27
+ export declare const DEFAULT_THEME_COLOR_SCHEME: Readonly<Record<ThemeColorSlot, string>>;
26
28
  /**
27
29
  * The two typefaces a theme nominates: the `major` face headings use and the `minor` face body text
28
30
  * uses. A cell's font reaches them by `scheme="major"`/`scheme="minor"` instead of naming a typeface,
@@ -12,6 +12,10 @@ export const THEME_COLOR_SLOTS = [
12
12
  'hlink',
13
13
  'folHlink',
14
14
  ];
15
+ const THEME_COLOR_SLOT_NAMES = new Set(THEME_COLOR_SLOTS);
16
+ export function isThemeColorSlot(value) {
17
+ return THEME_COLOR_SLOT_NAMES.has(value);
18
+ }
15
19
  export const DEFAULT_THEME_COLOR_SCHEME = {
16
20
  lt1: 'FFFFFF',
17
21
  dk1: '000000',
@@ -9,7 +9,7 @@ import { type TableStyle } from './table-style.ts';
9
9
  import { type ThemeColorScheme, type ThemeFontScheme, type ThemeOverrides } from './theme.ts';
10
10
  import type { WorkbookProtection } from './workbook-protection.ts';
11
11
  import { type DeclaredThemeSchemes } from './workbook-theme.ts';
12
- import { Worksheet, type WorksheetState } from './worksheet.ts';
12
+ import { type Visibility, Worksheet, type WorksheetState } from './worksheet.ts';
13
13
  /**
14
14
  * A workbook-level reference to package content the model does not model: a pivot cache
15
15
  * (`pivotCacheDefinition`) or a slicer cache (`slicerCache`), preserved verbatim across a round-trip
@@ -70,7 +70,7 @@ export interface WorkbookView {
70
70
  /** 0-based index into {@link Workbook.worksheets} of the sheet selected on open. */
71
71
  activeTab: number;
72
72
  /** Window visibility; omit for a normally visible window. */
73
- visibility?: 'visible' | 'hidden' | 'veryHidden';
73
+ visibility?: Visibility;
74
74
  /** Whether the document window opens minimised; omit for a restored window. */
75
75
  minimized?: boolean;
76
76
  }
@@ -5,5 +5,13 @@ export declare class WorksheetComments {
5
5
  get threads(): readonly CommentThread[];
6
6
  add(thread: CommentThread): void;
7
7
  at(reference: string): CommentThread | undefined;
8
+ /**
9
+ * Re-anchor every conversation through a row or column splice. A thread hangs off exactly one cell,
10
+ * so it moves as a point, not a rectangle, and a thread whose cell the splice deleted is dropped
11
+ * along with its messages. Both halves matter: a cell's legacy note is cell state and travels with
12
+ * the cell, so a thread left behind would put a note on one cell and its conversation on another,
13
+ * a pairing the writer emits and Excel refuses.
14
+ */
15
+ shift(axis: 'row' | 'col', start: number, count: number, delta: number): void;
8
16
  restore(threads: readonly CommentThread[]): void;
9
17
  }
@@ -1,6 +1,7 @@
1
1
  import { decodeCellRef, encodeAddress } from './address.js';
2
2
  import { commentThreadGuid, commentThreadOffset } from './comment-thread.js';
3
3
  import { replaceContents } from './containers.js';
4
+ import { isDeletedSpan, shiftIndex } from './grid-shift.js';
4
5
  export class WorksheetComments {
5
6
  #sheetName;
6
7
  #threads = [];
@@ -42,6 +43,23 @@ export class WorksheetComments {
42
43
  const anchor = anchorRef(reference);
43
44
  return this.#threads.find((thread) => thread.ref === anchor);
44
45
  }
46
+ shift(axis, start, count, delta) {
47
+ const survivors = [];
48
+ for (const thread of this.#threads) {
49
+ const { col, row } = decodeCellRef(thread.ref);
50
+ const line = axis === 'row' ? row : col;
51
+ if (isDeletedSpan(line, line, start, count))
52
+ continue;
53
+ const moved = shiftIndex(line, start, count, delta, axis);
54
+ if (moved === line) {
55
+ survivors.push(thread);
56
+ continue;
57
+ }
58
+ const ref = axis === 'row' ? encodeAddress(col, moved) : encodeAddress(moved, row);
59
+ survivors.push({ ...thread, ref });
60
+ }
61
+ replaceContents(this.#threads, survivors);
62
+ }
45
63
  restore(threads) {
46
64
  replaceContents(this.#threads, threads);
47
65
  }
@@ -15,9 +15,19 @@ import { Row } from './row.ts';
15
15
  import type { CellStyle, Color, Fill } from './style.ts';
16
16
  import { Table, type TableOptions } from './table.ts';
17
17
  import type { CellValue } from './value.ts';
18
+ /**
19
+ * Whether a thing Excel can hide is showing: a sheet's tab, or the document window itself.
20
+ *
21
+ * One type for two schema enumerations. `ST_SheetState` and `ST_Visibility` are declared separately
22
+ * in ECMA-376 and carry the same three tokens with the same meanings, and `veryHidden` means the same
23
+ * thing in both: hidden, and not offered in the unhide list.
24
+ */
25
+ export type Visibility = 'visible' | 'hidden' | 'veryHidden';
26
+ /** Narrow a raw `<sheet state>` or `<workbookView visibility>` token to a known {@link Visibility}. */
27
+ export declare function isVisibility(value: string): value is Visibility;
18
28
  export interface WorksheetState {
19
29
  /** Sheet visibility, as Excel models it. Defaults to `visible`. */
20
- readonly state: 'visible' | 'hidden' | 'veryHidden';
30
+ readonly state: Visibility;
21
31
  }
22
32
  /** Format defaults applied to every row/column that carries no explicit override. */
23
33
  export interface WorksheetProperties {
@@ -574,7 +584,10 @@ export declare class Worksheet {
574
584
  * never disturbs merges or the columns to its left.
575
585
  *
576
586
  * `values` is an array indexed by row (index 0 → row 1); a hole or an explicit `undefined` leaves
577
- * that row untouched, mirroring {@link addRow}'s positional-array shape.
587
+ * that row untouched, mirroring {@link addRow}'s positional-array shape. That is the only shape a
588
+ * column takes: the other {@link RowInput} form addresses columns by their
589
+ * {@link ColumnProperties.key}, and a column's values are indexed by *row*, which carries no key,
590
+ * so there is nothing on this axis for a keyed object to name.
578
591
  */
579
592
  addColumn(values: CellValue[]): Cell[];
580
593
  /**
@@ -11,12 +11,16 @@ import { clearCoveredValues, masterOf } from './merge.js';
11
11
  import { PivotTable } from './pivot-table.js';
12
12
  import { deriveCredential, } from './protection.js';
13
13
  import { Range, rangeFrom } from './range.js';
14
- import { buildRowCells, rowPlacements } from './row-input.js';
14
+ import { buildRowCells, positionalPlacements, rowPlacements } from './row-input.js';
15
15
  import { Row } from './row.js';
16
- import { Table, TOTALS_ROW_SUBTOTAL_CODE } from './table.js';
16
+ import { Table } from './table.js';
17
17
  import { WorksheetComments } from './worksheet-comments.js';
18
18
  import { WORKSHEET_MODEL_FACETS } from './worksheet-model.js';
19
19
  import { WorksheetPictures } from './worksheet-pictures.js';
20
+ const VISIBILITIES = { visible: true, hidden: true, veryHidden: true };
21
+ export function isVisibility(value) {
22
+ return Object.hasOwn(VISIBILITIES, value);
23
+ }
20
24
  export class Worksheet {
21
25
  name;
22
26
  id;
@@ -62,6 +66,15 @@ export class Worksheet {
62
66
  mergeRects: this.#mergeRects,
63
67
  tables: this.#tables,
64
68
  images: this.#images.anchors,
69
+ dataValidations: this.#dataValidations,
70
+ conditionalFormattings: this.#conditionalFormattings,
71
+ comments: this.#comments,
72
+ autoFilter: {
73
+ get: () => this.#autoFilter,
74
+ set: (next) => {
75
+ this.#autoFilter = next;
76
+ },
77
+ },
65
78
  });
66
79
  }
67
80
  getCell(reference) {
@@ -151,46 +164,17 @@ export class Worksheet {
151
164
  }
152
165
  }
153
166
  addTable(options) {
154
- const table = new Table(options, (row, col, value, style) => {
155
- const cell = this.#cellAt(row, col);
156
- cell.value = value;
157
- if (style !== undefined)
158
- applyCellStyle(cell, style);
159
- }, (row) => this.spliceRows(row, 0, []));
167
+ const table = new Table(options, {
168
+ holdsValue: (row, col) => this.hasCell(row, col) && this.#cellAt(row, col).value != null,
169
+ writeCell: (row, col, value, style) => {
170
+ const cell = this.#cellAt(row, col);
171
+ cell.value = value;
172
+ if (style !== undefined)
173
+ applyCellStyle(cell, style);
174
+ },
175
+ insertRow: (row) => this.spliceRows(row, 0, []),
176
+ });
160
177
  this.#tables.push(table);
161
- if (table.headerRow) {
162
- const { top, left } = table.region;
163
- table.columns.forEach((column, index) => {
164
- const col = left + index;
165
- if (this.hasCell(top, col) && this.#cellAt(top, col).value != null)
166
- return;
167
- this.#cellAt(top, col).value = column.name;
168
- });
169
- }
170
- if (table.totalsRow) {
171
- const { left, bottom } = table.region;
172
- table.columns.forEach((column, index) => {
173
- const col = left + index;
174
- if (this.hasCell(bottom, col) && this.#cellAt(bottom, col).value != null)
175
- return;
176
- if (column.totalsRowLabel !== undefined) {
177
- this.#cellAt(bottom, col).value = column.totalsRowLabel;
178
- return;
179
- }
180
- if (column.totalsRowFunction !== undefined) {
181
- const code = TOTALS_ROW_SUBTOTAL_CODE[column.totalsRowFunction];
182
- if (code !== undefined) {
183
- this.#cellAt(bottom, col).value = {
184
- formula: `SUBTOTAL(${code},${table.name}[${column.name}])`,
185
- };
186
- }
187
- else if (column.totalsRowFunction === 'custom' &&
188
- column.totalsRowFormula !== undefined) {
189
- this.#cellAt(bottom, col).value = { formula: column.totalsRowFormula };
190
- }
191
- }
192
- });
193
- }
194
178
  return table;
195
179
  }
196
180
  get tables() {
@@ -373,15 +357,11 @@ export class Worksheet {
373
357
  let index = this.columnCount;
374
358
  return columns.map((values) => {
375
359
  index += 1;
376
- const cells = [];
377
- values.forEach((value, i) => {
378
- if (value === undefined)
379
- return;
380
- const cell = this.#cellAt(i + 1, index);
360
+ return positionalPlacements(values).map(([row, value]) => {
361
+ const cell = this.#cellAt(row, index);
381
362
  cell.value = value;
382
- cells.push(cell);
363
+ return cell;
383
364
  });
384
- return cells;
385
365
  });
386
366
  }
387
367
  get model() {