@shbernal/ts-xlsx 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +5 -1
  16. package/dist/core/column.js +34 -35
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +9 -3
  27. package/dist/core/data-validation.js +21 -18
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +14 -10
  39. package/dist/core/image.js +9 -9
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
@@ -1,13 +1,15 @@
1
1
  import { type CustomUiDocument } from '../customui/index.ts';
2
2
  import { type VbaLibraryReference, type VbaProject, type VbaProjectSignature } from '../vba/index.ts';
3
3
  import { type Person } from './comment-thread.ts';
4
- import { type WorkbookImage, type WorksheetImages } from './image.ts';
4
+ import type { DateEpoch } from './date.ts';
5
+ import type { WorkbookImage, WorksheetImages } from './image.ts';
5
6
  import { INTERNAL } from './internal.ts';
6
7
  import type { PreservedPart, PreservedRootReference } from './preserved.ts';
7
- import type { Color, Font, NamedCellStyle, TableStyleTable } from './style.ts';
8
+ import type { Color, Font } from './style.ts';
8
9
  import { type TableStyle } from './table-style.ts';
9
10
  import { type ThemeColorScheme, type ThemeFontScheme, type ThemeOverrides } from './theme.ts';
10
11
  import type { WorkbookProtection } from './workbook-protection.ts';
12
+ import { type NamedCellStyle, type TableStyleTable } from './workbook-styles.ts';
11
13
  import { type DeclaredThemeSchemes } from './workbook-theme.ts';
12
14
  import { type Visibility, Worksheet, type WorksheetState } from './worksheet.ts';
13
15
  /**
@@ -155,6 +157,25 @@ export declare class Workbook {
155
157
  * workbook whose cached results are authoritative stays unmarked.
156
158
  */
157
159
  fullCalcOnLoad: boolean;
160
+ /**
161
+ * Which date system this workbook's serials count in: `1900` (the Windows default) or `1904` (the
162
+ * `date1904` flag of `<workbookPr>`, Excel for Macintosh's original). It governs every conversion
163
+ * between a `Date` and the number a cell actually stores, in both directions, so setting it after
164
+ * cells hold dates changes what those cells mean rather than converting them.
165
+ *
166
+ * Read from the file and written back, because dropping it is not a cosmetic loss: the serials stay
167
+ * as they were and the consumer re-reads them under the other system, so the workbook silently
168
+ * changes meaning by four years and a day.
169
+ */
170
+ dateEpoch: DateEpoch;
171
+ /**
172
+ * The workbook's VBA identity (`<workbookPr codeName>`), the name a macro means by `ThisWorkbook`.
173
+ * Undefined for a workbook with no VBA project, which is what Excel writes for one.
174
+ *
175
+ * Preserved rather than modeled: nothing here reads it, but a `.xlsm` whose code name is dropped on
176
+ * a round trip has had the binding between its macros and its document cut.
177
+ */
178
+ codeName?: string;
158
179
  /**
159
180
  * Workbook-level structure/window protection: the OOXML `<workbookProtection>` element. Absent by
160
181
  * default (an unprotected workbook). Set it to lock the workbook shell, or leave it as read from a
@@ -1,11 +1,12 @@
1
1
  import { isCustomUiRelType, parseCustomUi } from '../customui/index.js';
2
- import { AuthoringError } from '../errors.js';
2
+ import { AuthoringError, quoted } from '../errors.js';
3
3
  import { addVbaReference, removeVbaModule, } from '../vba/index.js';
4
4
  import { commentThreadGuid } from './comment-thread.js';
5
- import { findRegisteredImage, normalizeImageExtension, } from './image.js';
6
5
  import { INTERNAL } from './internal.js';
6
+ import { INVALID_SHEET_NAME_CHARS, MAX_SHEET_NAME_LENGTH } from './limits.js';
7
7
  import { checkTableStyle } from './table-style.js';
8
8
  import { OFFICE_BODY_FACE, THEME_COLOR_SLOTS, } from './theme.js';
9
+ import { WorkbookMedia } from './workbook-media.js';
9
10
  import { WorkbookStyleTables } from './workbook-styles.js';
10
11
  import { WorkbookTheme } from './workbook-theme.js';
11
12
  import { WorkbookVbaProject } from './workbook-vba.js';
@@ -17,16 +18,16 @@ export const DEFAULT_WORKBOOK_VIEW = {
17
18
  height: 12220,
18
19
  activeTab: 0,
19
20
  };
20
- const MAX_SHEET_NAME_LENGTH = 31;
21
- const INVALID_SHEET_NAME_CHARS = /[*?:\\/[\]]/;
22
21
  export class Workbook {
23
22
  properties = {};
24
23
  view = { ...DEFAULT_WORKBOOK_VIEW };
25
24
  fullCalcOnLoad = false;
25
+ dateEpoch = 1900;
26
+ codeName;
26
27
  protection = undefined;
27
28
  #worksheets = [];
28
29
  #nextSheetId = 1;
29
- #media = [];
30
+ #media = new WorkbookMedia();
30
31
  #definedNames = [];
31
32
  #styles = new WorkbookStyleTables();
32
33
  #theme = new WorkbookTheme(() => this.#styles.indexedPalette());
@@ -37,7 +38,7 @@ export class Workbook {
37
38
  }
38
39
  get activeTabIndex() {
39
40
  const { activeTab } = this.view;
40
- return Number.isInteger(activeTab) && activeTab > 0 && activeTab < this.#worksheets.length
41
+ return Number.isInteger(activeTab) && activeTab >= 0 && activeTab < this.#worksheets.length
41
42
  ? activeTab
42
43
  : 0;
43
44
  }
@@ -180,31 +181,22 @@ export class Workbook {
180
181
  return this.#persons.get(id);
181
182
  }
182
183
  addImage(options) {
183
- this.#media.push({
184
- extension: normalizeImageExtension(options.extension, options.buffer),
185
- data: options.buffer,
186
- });
187
- return this.#media.length - 1;
184
+ return this.#media.register(options.extension, options.buffer);
188
185
  }
189
186
  get media() {
190
- return this.#media;
187
+ return this.#media.all;
191
188
  }
192
189
  getImage(id) {
193
- return this.#media[id];
190
+ return this.#media.get(id);
194
191
  }
195
192
  exportImages(sheet) {
196
- const resolve = (id) => {
197
- const image = this.#media[id];
198
- if (image === undefined) {
199
- throw new AuthoringError(`worksheet "${sheet.name}" shows image id ${id}, which is not registered on this ` +
200
- "workbook: a sheet's images can only be exported by the workbook that holds them");
201
- }
202
- return image;
203
- };
204
193
  const background = sheet.backgroundImageId;
205
194
  return {
206
- anchored: sheet.images.map(({ imageId, anchor }) => ({ image: resolve(imageId), anchor })),
207
- background: background === undefined ? undefined : resolve(background),
195
+ anchored: sheet.images.map(({ imageId, anchor }) => ({
196
+ image: this.#media.require(imageId, sheet.name),
197
+ anchor,
198
+ })),
199
+ background: background === undefined ? undefined : this.#media.require(background, sheet.name),
208
200
  };
209
201
  }
210
202
  importImages(sheet, images) {
@@ -212,20 +204,12 @@ export class Workbook {
212
204
  sheet.removeImage(id);
213
205
  sheet.removeBackgroundImage();
214
206
  for (const { image, anchor } of images.anchored) {
215
- sheet.addImageAnchor(this.#registerImage(image), anchor);
207
+ sheet.addImageAnchor(this.#media.registerExisting(image), anchor);
216
208
  }
217
209
  if (images.background !== undefined) {
218
- sheet.addBackgroundImage(this.#registerImage(images.background));
210
+ sheet.addBackgroundImage(this.#media.registerExisting(images.background));
219
211
  }
220
212
  }
221
- #registerImage(image) {
222
- const candidate = {
223
- extension: normalizeImageExtension(image.extension, image.data),
224
- data: image.data,
225
- };
226
- return (findRegisteredImage(this.#media, candidate) ??
227
- this.addImage({ buffer: candidate.data, extension: candidate.extension }));
228
- }
229
213
  get definedNames() {
230
214
  return this.#definedNames;
231
215
  }
@@ -234,7 +218,7 @@ export class Workbook {
234
218
  throw new AuthoringError('a defined name cannot be empty');
235
219
  }
236
220
  if (definedName.scope !== undefined && this.getWorksheet(definedName.scope) === undefined) {
237
- throw new AuthoringError(`defined name "${definedName.name}" is scoped to unknown worksheet "${definedName.scope}"`);
221
+ throw new AuthoringError(`defined name ${quoted(definedName.name)} is scoped to unknown worksheet ${quoted(definedName.scope)}`);
238
222
  }
239
223
  this.#definedNames.push(definedName);
240
224
  }
@@ -255,11 +239,11 @@ export class Workbook {
255
239
  const sheet = this.getWorksheet(nameOrId);
256
240
  if (sheet !== undefined)
257
241
  return sheet;
258
- const wanted = typeof nameOrId === 'number' ? `id ${nameOrId}` : JSON.stringify(nameOrId);
242
+ const wanted = typeof nameOrId === 'number' ? `id ${nameOrId}` : quoted(nameOrId);
259
243
  if (this.#worksheets.length === 0) {
260
244
  throw new AuthoringError(`no worksheet ${wanted}: this workbook has no worksheets`);
261
245
  }
262
- const have = this.#worksheets.map((sheet) => JSON.stringify(sheet.name)).join(', ');
246
+ const have = this.#worksheets.map((sheet) => quoted(sheet.name)).join(', ');
263
247
  throw new AuthoringError(`no worksheet ${wanted}; this workbook has ${have}`);
264
248
  }
265
249
  #assertValidSheetName(name) {
@@ -267,16 +251,16 @@ export class Workbook {
267
251
  throw new AuthoringError('worksheet name cannot be empty');
268
252
  }
269
253
  if (name.length > MAX_SHEET_NAME_LENGTH) {
270
- throw new AuthoringError(`worksheet name "${name}" exceeds the ${MAX_SHEET_NAME_LENGTH}-character limit`);
254
+ throw new AuthoringError(`worksheet name ${quoted(name)} exceeds the ${MAX_SHEET_NAME_LENGTH}-character limit`);
271
255
  }
272
256
  if (INVALID_SHEET_NAME_CHARS.test(name)) {
273
- throw new AuthoringError(`worksheet name "${name}" contains a character Excel forbids (* ? : \\ / [ ])`);
257
+ throw new AuthoringError(`worksheet name ${quoted(name)} contains a character Excel forbids (* ? : \\ / [ ])`);
274
258
  }
275
259
  if (name.startsWith("'") || name.endsWith("'")) {
276
- throw new AuthoringError(`worksheet name "${name}" cannot start or end with an apostrophe`);
260
+ throw new AuthoringError(`worksheet name ${quoted(name)} cannot start or end with an apostrophe`);
277
261
  }
278
262
  if (this.getWorksheet(name) !== undefined) {
279
- throw new AuthoringError(`a worksheet named "${name}" already exists (names are case-insensitive)`);
263
+ throw new AuthoringError(`a worksheet named ${quoted(name)} already exists (names are case-insensitive)`);
280
264
  }
281
265
  }
282
266
  [INTERNAL] = {
@@ -1,4 +1,5 @@
1
1
  import { type CommentThread } from './comment-thread.ts';
2
+ import { type AxisSplice } from './grid-shift.ts';
2
3
  export declare class WorksheetComments {
3
4
  #private;
4
5
  constructor(sheetName: () => string);
@@ -12,6 +13,6 @@ export declare class WorksheetComments {
12
13
  * the cell, so a thread left behind would put a note on one cell and its conversation on another,
13
14
  * a pairing the writer emits and Excel refuses.
14
15
  */
15
- shift(axis: 'row' | 'col', start: number, count: number, delta: number): void;
16
+ shift(splice: AxisSplice): void;
16
17
  restore(threads: readonly CommentThread[]): void;
17
18
  }
@@ -1,10 +1,12 @@
1
+ import { quoted } from '../errors.js';
1
2
  import { decodeCellRef, encodeAddress } from './address.js';
2
3
  import { commentThreadGuid, commentThreadOffset } from './comment-thread.js';
3
4
  import { replaceContents } from './containers.js';
4
- import { isDeletedSpan, shiftIndex } from './grid-shift.js';
5
+ import { shiftPoint } from './grid-shift.js';
5
6
  export class WorksheetComments {
6
7
  #sheetName;
7
8
  #threads = [];
9
+ #takenIds = new Set();
8
10
  constructor(sheetName) {
9
11
  this.#sheetName = sheetName;
10
12
  }
@@ -12,14 +14,14 @@ export class WorksheetComments {
12
14
  return this.#threads;
13
15
  }
14
16
  add(thread) {
15
- const taken = new Set(this.#threads.flatMap((held) => held.comments.map((comment) => comment.id)));
17
+ const claimed = new Set();
16
18
  const comments = thread.comments.map((comment) => {
17
19
  const id = commentThreadGuid(comment.id, 'a comment id');
18
- if (taken.has(id)) {
19
- throw new SyntaxError(`a comment id must be unique within a sheet, but "${id}" is already used on "${this.#sheetName()}": ` +
20
+ if (this.#takenIds.has(id) || claimed.has(id)) {
21
+ throw new SyntaxError(`a comment id must be unique within a sheet, but ${quoted(id)} is already used on ${quoted(this.#sheetName())}: ` +
20
22
  'a reply and the legacy fallback comment both find their thread by it');
21
23
  }
22
- taken.add(id);
24
+ claimed.add(id);
23
25
  return {
24
26
  ...comment,
25
27
  id,
@@ -38,30 +40,35 @@ export class WorksheetComments {
38
40
  };
39
41
  });
40
42
  this.#threads.push({ ...thread, ref: anchorRef(thread.ref), comments });
43
+ for (const id of claimed)
44
+ this.#takenIds.add(id);
41
45
  }
42
46
  at(reference) {
43
47
  const anchor = anchorRef(reference);
44
48
  return this.#threads.find((thread) => thread.ref === anchor);
45
49
  }
46
- shift(axis, start, count, delta) {
50
+ shift(splice) {
47
51
  const survivors = [];
48
52
  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))
53
+ const anchor = decodeCellRef(thread.ref);
54
+ const moved = shiftPoint(anchor, splice);
55
+ if (moved === undefined)
52
56
  continue;
53
- const moved = shiftIndex(line, start, count, delta, axis);
54
- if (moved === line) {
57
+ if (moved.col === anchor.col && moved.row === anchor.row) {
55
58
  survivors.push(thread);
56
59
  continue;
57
60
  }
58
- const ref = axis === 'row' ? encodeAddress(col, moved) : encodeAddress(moved, row);
59
- survivors.push({ ...thread, ref });
61
+ survivors.push({ ...thread, ref: encodeAddress(moved.col, moved.row) });
60
62
  }
61
63
  replaceContents(this.#threads, survivors);
62
64
  }
63
65
  restore(threads) {
64
66
  replaceContents(this.#threads, threads);
67
+ this.#takenIds.clear();
68
+ for (const thread of threads) {
69
+ for (const comment of thread.comments)
70
+ this.#takenIds.add(comment.id);
71
+ }
65
72
  }
66
73
  }
67
74
  function anchorRef(reference) {
@@ -0,0 +1,44 @@
1
+ import type { MergeRect } from './merge.ts';
2
+ /** What removing a declared range did, which is two answers because a range and a rectangle are not
3
+ * the same thing: an unbounded whole-row/column merge is declared but has no rectangle, so dropping
4
+ * it leaves every geometry derived from the rectangles (the used extent, the index) still valid. */
5
+ export interface MergeRemoval {
6
+ /** Whether a merge with this exact range string was declared. */
7
+ readonly existed: boolean;
8
+ /** Whether a rectangle went with it, so anything derived from the rectangles is now stale. */
9
+ readonly rectsChanged: boolean;
10
+ }
11
+ export declare class WorksheetMerges {
12
+ #private;
13
+ /** The declared ranges, in the order they were added. Live, not a copy. */
14
+ get ranges(): readonly string[];
15
+ /**
16
+ * The bounded regions as rectangles. Live, and its *identity* is stable for the sheet's lifetime:
17
+ * {@link UsedExtent} holds this array by reference, so every rewrite here goes through
18
+ * `replaceContents` rather than reassignment.
19
+ */
20
+ get rects(): readonly MergeRect[];
21
+ /**
22
+ * Declare a merged range, returning the rectangle it covers, or `undefined` for an unbounded
23
+ * whole-row/column range, which is declared and overlap-checks against nothing.
24
+ *
25
+ * @throws {AuthoringError} if the range overlaps an already-merged region. Excel forbids
26
+ * overlapping merges and writes such geometry as a file it then offers to repair.
27
+ */
28
+ add(range: string): MergeRect | undefined;
29
+ /** Drop a declared range and, with it, the rectangle it covers. The inverse of {@link add}. */
30
+ remove(range: string): MergeRemoval;
31
+ /**
32
+ * Resolve a position to the top-left of the region covering it, or to itself when none does. This
33
+ * is what makes a write through a covered address land on the region's master.
34
+ */
35
+ masterOf(row: number, col: number): {
36
+ row: number;
37
+ col: number;
38
+ };
39
+ /** Replace every declared range and rectangle at once: what a structural splice re-anchors to. */
40
+ replaceAll(ranges: readonly string[], rects: readonly MergeRect[]): void;
41
+ /** Report that something outside has rewritten the grid under these regions. */
42
+ invalidate(): void;
43
+ clear(): void;
44
+ }
@@ -0,0 +1,59 @@
1
+ import { AuthoringError, quoted } from '../errors.js';
2
+ import { boundedRect, decodeRange } from './address.js';
3
+ import { replaceContents } from './containers.js';
4
+ import { MergeIndex } from './merge-index.js';
5
+ export class WorksheetMerges {
6
+ #ranges = [];
7
+ #rects = [];
8
+ #index = new MergeIndex(this.#rects);
9
+ get ranges() {
10
+ return this.#ranges;
11
+ }
12
+ get rects() {
13
+ return this.#rects;
14
+ }
15
+ add(range) {
16
+ const rect = boundedRect(decodeRange(range));
17
+ if (rect !== undefined) {
18
+ if (this.#index.overlapping(rect) !== undefined) {
19
+ throw new AuthoringError(`merged range ${quoted(range)} overlaps an existing merged region`);
20
+ }
21
+ this.#rects.push(rect);
22
+ this.#index.note(rect);
23
+ }
24
+ this.#ranges.push(range);
25
+ return rect;
26
+ }
27
+ remove(range) {
28
+ const index = this.#ranges.indexOf(range);
29
+ if (index === -1)
30
+ return { existed: false, rectsChanged: false };
31
+ this.#ranges.splice(index, 1);
32
+ const rect = boundedRect(decodeRange(range));
33
+ if (rect === undefined)
34
+ return { existed: true, rectsChanged: false };
35
+ const { top, left, bottom, right } = rect;
36
+ const at = this.#rects.findIndex((r) => r.top === top && r.left === left && r.bottom === bottom && r.right === right);
37
+ if (at === -1)
38
+ return { existed: true, rectsChanged: false };
39
+ this.#rects.splice(at, 1);
40
+ this.#index.invalidate();
41
+ return { existed: true, rectsChanged: true };
42
+ }
43
+ masterOf(row, col) {
44
+ return this.#index.masterOf(row, col);
45
+ }
46
+ replaceAll(ranges, rects) {
47
+ replaceContents(this.#ranges, ranges);
48
+ replaceContents(this.#rects, rects);
49
+ this.#index.invalidate();
50
+ }
51
+ invalidate() {
52
+ this.#index.invalidate();
53
+ }
54
+ clear() {
55
+ this.#ranges.length = 0;
56
+ this.#rects.length = 0;
57
+ this.#index.invalidate();
58
+ }
59
+ }
@@ -3,7 +3,15 @@ import type { Worksheet, WorksheetModel } from './worksheet.ts';
3
3
  /** One field of a {@link WorksheetModel}, with both directions of its round-trip declared together. */
4
4
  interface ModelFacet<K extends keyof WorksheetModel = keyof WorksheetModel> {
5
5
  readonly key: K;
6
- /** Produce the field's value, copied deeply enough that mutating it cannot reach back into the sheet. */
6
+ /**
7
+ * Produce the field's value, in a form a caller cannot mutate the sheet through.
8
+ *
9
+ * Copying is one way to get there and the type is the other. `tabColor`, `autoFilter` and
10
+ * `protection` are handed back by reference precisely because their types are readonly all the way
11
+ * down, so there is nothing to defend against and a clone would only be one more shape to keep in
12
+ * step with its declaration. Every other field is a mutable record or array and is copied, which is
13
+ * where the spreads below come from.
14
+ */
7
15
  readonly read: (sheet: Worksheet) => WorksheetModel[K];
8
16
  /**
9
17
  * Apply the field to a sheet whose content has already been reset. Takes the whole model rather
@@ -6,6 +6,21 @@ import { INTERNAL } from './internal.js';
6
6
  function facet(key, read, write) {
7
7
  return { key, read, write: (sheet, model) => write(sheet, model[key]) };
8
8
  }
9
+ function recordFacet(key) {
10
+ return {
11
+ key,
12
+ read: (sheet) => ({ ...sheet[key] }),
13
+ write: (sheet, model) => overwrite(sheet[key], model[key]),
14
+ };
15
+ }
16
+ function recordsFacet(key) {
17
+ const copy = (breaks) => breaks.map((brk) => ({ ...brk }));
18
+ return {
19
+ key,
20
+ read: (sheet) => copy(sheet[key]),
21
+ write: (sheet, model) => replaceContents(sheet[key], copy(model[key])),
22
+ };
23
+ }
9
24
  export const WORKSHEET_MODEL_FACETS = [
10
25
  facet('state', (sheet) => sheet.state, (sheet, value) => {
11
26
  sheet.state = value;
@@ -13,15 +28,15 @@ export const WORKSHEET_MODEL_FACETS = [
13
28
  facet('tabColor', (sheet) => sheet.tabColor, (sheet, value) => {
14
29
  sheet.tabColor = value;
15
30
  }),
16
- facet('properties', (sheet) => ({ ...sheet.properties }), (sheet, value) => overwrite(sheet.properties, value)),
17
- facet('outline', (sheet) => ({ ...sheet.outline }), (sheet, value) => overwrite(sheet.outline, value)),
18
- facet('view', (sheet) => ({ ...sheet.view }), (sheet, value) => overwrite(sheet.view, value)),
19
- facet('pageSetup', (sheet) => ({ ...sheet.pageSetup }), (sheet, value) => overwrite(sheet.pageSetup, value)),
20
- facet('printOptions', (sheet) => ({ ...sheet.printOptions }), (sheet, value) => overwrite(sheet.printOptions, value)),
21
- facet('pageMargins', (sheet) => ({ ...sheet.pageMargins }), (sheet, value) => overwrite(sheet.pageMargins, value)),
22
- facet('headerFooter', (sheet) => ({ ...sheet.headerFooter }), (sheet, value) => overwrite(sheet.headerFooter, value)),
23
- facet('rowBreaks', (sheet) => sheet.rowBreaks.map((brk) => ({ ...brk })), (sheet, value) => replaceContents(sheet.rowBreaks, value.map((brk) => ({ ...brk })))),
24
- facet('columnBreaks', (sheet) => sheet.columnBreaks.map((brk) => ({ ...brk })), (sheet, value) => replaceContents(sheet.columnBreaks, value.map((brk) => ({ ...brk })))),
31
+ recordFacet('properties'),
32
+ recordFacet('outline'),
33
+ recordFacet('view'),
34
+ recordFacet('pageSetup'),
35
+ recordFacet('printOptions'),
36
+ recordFacet('pageMargins'),
37
+ recordFacet('headerFooter'),
38
+ recordsFacet('rowBreaks'),
39
+ recordsFacet('columnBreaks'),
25
40
  facet('columns', (sheet) => [...sheet.columns()].map(({ index, properties }) => ({ index, properties: { ...properties } })), (sheet, value) => {
26
41
  for (const { index, properties } of value)
27
42
  Object.assign(sheet.getColumn(index), properties);
@@ -12,7 +12,7 @@ import type { PreservedWorksheetReference } from './preserved.ts';
12
12
  import { type SheetProtection, type SheetProtectionOptions } from './protection.ts';
13
13
  import { Range } from './range.ts';
14
14
  import { Row } from './row.ts';
15
- import type { CellStyle, Color, Fill } from './style.ts';
15
+ import type { CellContent, CellStyle, Color, Fill } from './style.ts';
16
16
  import { Table, type TableOptions } from './table.ts';
17
17
  import type { CellValue } from './value.ts';
18
18
  /**
@@ -24,7 +24,7 @@ import type { CellValue } from './value.ts';
24
24
  */
25
25
  export type Visibility = 'visible' | 'hidden' | 'veryHidden';
26
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;
27
+ export declare const isVisibility: (value: string) => value is Visibility;
28
28
  export interface WorksheetState {
29
29
  /** Sheet visibility, as Excel models it. Defaults to `visible`. */
30
30
  readonly state: Visibility;
@@ -107,8 +107,13 @@ export interface RowProperties {
107
107
  /** Background fill applied to the row's cells that carry no fill of their own. */
108
108
  fill?: Fill;
109
109
  }
110
- /** One materialised cell in a {@link WorksheetModel}: its position, value, and per-cell style facets. */
111
- export interface CellModel extends CellStyle {
110
+ /**
111
+ * One materialised cell in a {@link WorksheetModel}: its position, value, note, and every facet of its
112
+ * formatting. Extends {@link CellContent} rather than {@link CellStyle} so the quote-prefix flag and
113
+ * the named-style link travel with a model round-trip: they are written and read back like any other
114
+ * facet, and leaving them off the tuple is what made a `dst.model = src.model` drop them.
115
+ */
116
+ export interface CellModel extends CellContent {
112
117
  readonly row: number;
113
118
  readonly col: number;
114
119
  value: CellValue;
@@ -177,6 +182,17 @@ export declare class Worksheet {
177
182
  * never fabricates one.
178
183
  */
179
184
  tabColor: Color | undefined;
185
+ /**
186
+ * The sheet's VBA identity (`<sheetPr codeName>`), the name a macro means by `Sheet1`. Undefined
187
+ * for a sheet in a workbook with no VBA project, which is what Excel writes for one.
188
+ *
189
+ * Preserved rather than modeled: nothing here reads it, but a `.xlsm` whose sheet code names are
190
+ * dropped on a round trip has had the binding between its macros and its sheets cut. Deliberately
191
+ * absent from {@link WorksheetModel}, which is the copy shape: a code name identifies *this* sheet
192
+ * to the workbook's VBA project, so copying it onto a second sheet would give the project two
193
+ * sheets answering to one name.
194
+ */
195
+ codeName?: string;
180
196
  /** Sheet-level format defaults. Mutate in place: `sheet.properties.defaultRowHeight = 20`. */
181
197
  readonly properties: WorksheetProperties;
182
198
  /**
@@ -267,6 +283,7 @@ export declare class Worksheet {
267
283
  *
268
284
  * @throws {SyntaxError} if the reference is unparseable, names another worksheet, or leaves an
269
285
  * axis unbounded.
286
+ * @throws {AuthoringError} if the numeric form is called with fewer than four corners.
270
287
  * @throws {RangeError} if a numeric corner is not a positive integer within the sheet's bounds.
271
288
  */
272
289
  getRange(reference: string): Range;
@@ -276,14 +293,21 @@ export declare class Worksheet {
276
293
  * or 0 for an empty sheet. Spans gaps: a value in row 5 makes this 5 even if rows 2–4
277
294
  * are empty. This is the used-range extent, not a populated-row tally (see
278
295
  * {@link actualRowCount}).
296
+ *
297
+ * *Carrying anything* is deliberately wide: a value, a cell's own style facet, quote-prefix flag
298
+ * or named-style link, a note, a row height or outline level, or a merge reaching down. Styling a
299
+ * band of empty rows is how a template is laid out, so those rows are used and {@link addRow}
300
+ * appends past them. A cell merely materialised by {@link getCell} and left untouched carries
301
+ * nothing, so reading a far address never grows the sheet.
279
302
  */
280
303
  get rowCount(): number;
281
304
  /** The number of rows that hold at least one non-empty cell, ignoring gaps and formatting-only rows. */
282
305
  get actualRowCount(): number;
283
306
  /**
284
- * The 1-based index of the last column carrying anything (a non-empty cell or its own format
307
+ * The 1-based index of the last column carrying anything (a used cell or the column's own format
285
308
  * properties), or 0 for an empty sheet. The used-range width, mirroring {@link rowCount} for the
286
- * other axis: a value in column E makes this 5 even if columns B–D are empty.
309
+ * other axis, down to what *carrying anything* means: a value in column E makes this 5 even if
310
+ * columns B–D are empty, and a column holding nothing but a styled empty cell is still used.
287
311
  */
288
312
  get columnCount(): number;
289
313
  /**
@@ -505,6 +529,9 @@ export declare class Worksheet {
505
529
  * shift with the rows they cover.
506
530
  *
507
531
  * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
532
+ * @throws {RangeError} if an inserted row would land past the last row of the grid. The sheet is
533
+ * left untouched, so this is a refused edit rather than half of one: a region pushed off the edge
534
+ * clamps and absorbs the loss, but content pushed off it is what Excel refuses outright.
508
535
  */
509
536
  spliceRows(start: number, count: number, ...inserts: RowInput[]): void;
510
537
  /**
@@ -548,9 +575,13 @@ export declare class Worksheet {
548
575
  /**
549
576
  * Copy the row at the 1-based `start`, `options.count` times (default 1). With `options.insert`
550
577
  * (the default) the copies are inserted directly after the source, shifting the rows below, and
551
- * any merged range there, down by `count`; otherwise the copies overwrite the rows immediately
552
- * below without shifting. Each copy is a faithful duplicate of the source's values and per-cell
553
- * styles, and carries no merge of its own, so a range can be merged onto a duplicated row afterwards.
578
+ * any merged range there, down by `count`; otherwise the copies *replace* the rows immediately
579
+ * below without shifting. A destination row is not overlaid but wholly re-made, so a cell it held
580
+ * in a column the source leaves empty is dropped, exactly as it would be with a shifting insert.
581
+ *
582
+ * Each copy is a faithful duplicate of the source: its cell values, its per-cell styles, and its
583
+ * row properties (height, hidden, outline level, row fill). It carries no merge of its own, so a
584
+ * range can be merged onto a duplicated row afterwards.
554
585
  *
555
586
  * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
556
587
  */
@@ -566,6 +597,10 @@ export declare class Worksheet {
566
597
  * indexed by row (index 0 → row 1); an empty array inserts a blank column.
567
598
  *
568
599
  * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
600
+ * @throws {RangeError} if an inserted column would land past the last column, or one of its values
601
+ * past the last row. The sheet is left untouched, so this is a refused edit rather than half of
602
+ * one: a region pushed off the edge clamps and absorbs the loss, but content pushed off it is
603
+ * what Excel refuses outright, and {@link addColumn} refuses the same argument identically.
569
604
  */
570
605
  spliceColumns(start: number, count: number, ...inserts: CellValue[][]): void;
571
606
  /**
@@ -573,7 +608,8 @@ export declare class Worksheet {
573
608
  * by one. `values` is an array of values indexed by row (index 0 → row 1), like
574
609
  * {@link addColumn}. Shorthand for {@link spliceColumns}`(pos, 0, values)`.
575
610
  *
576
- * @throws {RangeError} if `pos` is not a positive integer.
611
+ * @throws {RangeError} if `pos` is not a positive integer, or if the column would land past the
612
+ * last column or one of its values past the last row; see {@link spliceColumns}.
577
613
  */
578
614
  insertColumn(pos: number, values: CellValue[]): void;
579
615
  /**
@@ -682,18 +718,42 @@ export interface WorksheetInternals {
682
718
  */
683
719
  cellAt(row: number, col: number): Cell;
684
720
  /**
685
- * The store behind a {@link Row} or {@link Column} handle. These six exist because the handles are
721
+ * The cell materialised at a position, or `undefined` when nothing has been written there.
722
+ *
723
+ * Merge-blind and non-materialising, which is what separates it from `Worksheet.getCell`: that
724
+ * resolves a covered address to its merge master, so `hasCell(row, col)` followed by a `getCell` of
725
+ * the same position can hand back a *different* cell. A caller walking a rectangle got the master
726
+ * once per covered position and never saw the covered cells at all, which made a `clearStyle` over a
727
+ * merge clear the master repeatedly and the covered cells not once. A caller that wants merge
728
+ * resolution keeps `getCell`; a caller enumerating what is actually stored wants this.
729
+ */
730
+ peekCell(row: number, col: number): Cell | undefined;
731
+ /**
732
+ * {@link cellAt} with merge resolution: the cell a write to this position lands on, materialised.
733
+ *
734
+ * What {@link Worksheet.getCell} does, minus the address. `getCell` is the public spelling and takes
735
+ * an A1 reference, so a caller already holding a row and a column had to encode one and have it
736
+ * decoded straight back. That round-trip costs about three times a positional read, and a
737
+ * {@link Range} write performs one per cell: styling a 100x100 block spent 10,000 encodes and 10,000
738
+ * regex decodes on positions it already had.
739
+ */
740
+ masterAt(row: number, col: number): Cell;
741
+ /**
742
+ * The store behind a {@link Row} or {@link Column} handle. These exist because the handles are
686
743
  * views rather than records: they hold a sheet and a position, and every read and write goes
687
744
  * through here to the one authoritative map. `…PropertiesOf` never fabricates, so reading a row
688
745
  * cannot extend the used range; `ensure…` is what a write calls, so the record appears exactly
689
- * when a value is set.
746
+ * when a value is set; `drop…` is what clearing the last field calls, so the record disappears
747
+ * exactly when the line stops being formatted, which is what keeps the used range honest.
690
748
  */
691
749
  rowPropertiesOf(number: number): RowProperties | undefined;
692
750
  ensureRowProperties(number: number): RowProperties;
751
+ dropRowProperties(number: number): void;
693
752
  /** The row's materialised cells in ascending column order. */
694
753
  rowCells(number: number): Cell[];
695
754
  columnPropertiesOf(index: number): ColumnProperties | undefined;
696
755
  ensureColumnProperties(index: number): ColumnProperties;
756
+ dropColumnProperties(index: number): void;
697
757
  /** The column's materialised cells in ascending row order. */
698
758
  columnCells(index: number): Cell[];
699
759
  }