@shbernal/ts-xlsx 3.0.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 (213) 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 +4 -3
  7. package/dist/core/autofilter.js +31 -30
  8. package/dist/core/axis-handle.d.ts +2 -0
  9. package/dist/core/axis-handle.js +5 -2
  10. package/dist/core/cell.d.ts +4 -3
  11. package/dist/core/cell.js +4 -6
  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 +1 -0
  16. package/dist/core/column.js +3 -0
  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 +9 -1
  21. package/dist/core/conditional-formatting.js +34 -18
  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 +6 -0
  27. package/dist/core/data-validation.js +15 -4
  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 +13 -9
  39. package/dist/core/image.js +7 -5
  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.d.ts +5 -4
  44. package/dist/core/merge.js +18 -19
  45. package/dist/core/pivot-table.js +63 -45
  46. package/dist/core/protection.js +2 -11
  47. package/dist/core/range.js +34 -30
  48. package/dist/core/row-input.js +2 -2
  49. package/dist/core/row.d.ts +1 -0
  50. package/dist/core/row.js +3 -0
  51. package/dist/core/style.d.ts +85 -70
  52. package/dist/core/style.js +92 -28
  53. package/dist/core/table-style.d.ts +1 -1
  54. package/dist/core/table-style.js +4 -4
  55. package/dist/core/table.d.ts +27 -9
  56. package/dist/core/table.js +51 -34
  57. package/dist/core/theme.js +6 -5
  58. package/dist/core/value.d.ts +9 -0
  59. package/dist/core/value.js +36 -34
  60. package/dist/core/workbook-media.d.ts +34 -0
  61. package/dist/core/workbook-media.js +44 -0
  62. package/dist/core/workbook-styles.d.ts +67 -1
  63. package/dist/core/workbook-vba.js +3 -2
  64. package/dist/core/workbook.d.ts +23 -2
  65. package/dist/core/workbook.js +24 -40
  66. package/dist/core/worksheet-comments.d.ts +2 -1
  67. package/dist/core/worksheet-comments.js +20 -13
  68. package/dist/core/worksheet-merges.d.ts +44 -0
  69. package/dist/core/worksheet-merges.js +59 -0
  70. package/dist/core/worksheet-model.d.ts +9 -1
  71. package/dist/core/worksheet-model.js +24 -9
  72. package/dist/core/worksheet.d.ts +55 -6
  73. package/dist/core/worksheet.js +36 -44
  74. package/dist/customui/ribbon.js +2 -1
  75. package/dist/entries/core.d.ts +18 -13
  76. package/dist/entries/core.js +12 -6
  77. package/dist/entries/xlsx.d.ts +1 -1
  78. package/dist/errors.d.ts +64 -0
  79. package/dist/errors.js +22 -0
  80. package/dist/hex.d.ts +8 -0
  81. package/dist/hex.js +3 -0
  82. package/dist/io/csv/delimiter.d.ts +9 -0
  83. package/dist/io/csv/delimiter.js +6 -0
  84. package/dist/io/csv/read.js +11 -4
  85. package/dist/io/csv/write.d.ts +9 -2
  86. package/dist/io/csv/write.js +9 -38
  87. package/dist/io/opc/inflate.js +14 -0
  88. package/dist/io/opc/namespaces.d.ts +16 -0
  89. package/dist/io/opc/namespaces.js +4 -0
  90. package/dist/io/opc/part-paths.d.ts +9 -0
  91. package/dist/io/opc/part-paths.js +1 -1
  92. package/dist/io/opc/read-opc.d.ts +54 -14
  93. package/dist/io/opc/read-opc.js +42 -20
  94. package/dist/io/opc/read-options.d.ts +5 -1
  95. package/dist/io/opc/sniff-format.d.ts +3 -3
  96. package/dist/io/opc/sniff-format.js +2 -2
  97. package/dist/io/style/xf-style.d.ts +23 -1
  98. package/dist/io/style/xf-style.js +8 -0
  99. package/dist/io/xlsb/formula.d.ts +2 -2
  100. package/dist/io/xlsb/formula.js +49 -34
  101. package/dist/io/xlsb/primitives.js +8 -14
  102. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  103. package/dist/io/xlsb/ptg-functions.js +385 -596
  104. package/dist/io/xlsb/read-styles.js +45 -81
  105. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  106. package/dist/io/xlsb/read-worksheet.js +73 -89
  107. package/dist/io/xlsb/read.d.ts +6 -3
  108. package/dist/io/xlsb/read.js +44 -23
  109. package/dist/io/xlsb/record-stream.d.ts +30 -0
  110. package/dist/io/xlsb/record-stream.js +20 -0
  111. package/dist/io/xlsb/record-types.d.ts +1 -5
  112. package/dist/io/xlsb/record-types.js +1 -5
  113. package/dist/io/xlsx/cell-accumulator.d.ts +8 -1
  114. package/dist/io/xlsx/cell-accumulator.js +32 -9
  115. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  116. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  117. package/dist/io/xlsx/cell-value.d.ts +5 -3
  118. package/dist/io/xlsx/cell-value.js +13 -15
  119. package/dist/io/xlsx/color-xml.js +3 -2
  120. package/dist/io/xlsx/column-budget.d.ts +26 -0
  121. package/dist/io/xlsx/column-budget.js +22 -0
  122. package/dist/io/xlsx/comments.d.ts +6 -2
  123. package/dist/io/xlsx/comments.js +36 -32
  124. package/dist/io/xlsx/conditional-formatting.d.ts +6 -3
  125. package/dist/io/xlsx/conditional-formatting.js +30 -26
  126. package/dist/io/xlsx/data-validation.js +29 -27
  127. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  128. package/dist/io/xlsx/edit-vba.js +14 -21
  129. package/dist/io/xlsx/font-xml.d.ts +7 -0
  130. package/dist/io/xlsx/font-xml.js +104 -0
  131. package/dist/io/xlsx/hyperlinks.d.ts +37 -5
  132. package/dist/io/xlsx/hyperlinks.js +22 -13
  133. package/dist/io/xlsx/images.js +11 -13
  134. package/dist/io/xlsx/namespaces.d.ts +16 -0
  135. package/dist/io/xlsx/namespaces.js +7 -0
  136. package/dist/io/xlsx/package-plan.d.ts +14 -2
  137. package/dist/io/xlsx/package-plan.js +39 -11
  138. package/dist/io/xlsx/pivot.js +8 -7
  139. package/dist/io/xlsx/read-parts.d.ts +59 -0
  140. package/dist/io/xlsx/read-parts.js +252 -0
  141. package/dist/io/xlsx/read-pivot.js +2 -1
  142. package/dist/io/xlsx/read-repair.d.ts +26 -0
  143. package/dist/io/xlsx/read-repair.js +39 -0
  144. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +6 -3
  145. package/dist/io/xlsx/read-rows.d.ts +18 -5
  146. package/dist/io/xlsx/read-rows.js +45 -31
  147. package/dist/io/xlsx/read-shared-strings.js +3 -2
  148. package/dist/io/xlsx/read-styles.d.ts +1 -6
  149. package/dist/io/xlsx/read-styles.js +6 -71
  150. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  151. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  152. package/dist/io/xlsx/read-worksheet.d.ts +2 -1
  153. package/dist/io/xlsx/read-worksheet.js +35 -164
  154. package/dist/io/xlsx/read.d.ts +5 -12
  155. package/dist/io/xlsx/read.js +73 -406
  156. package/dist/io/xlsx/rich-text.js +1 -1
  157. package/dist/io/xlsx/row-position.d.ts +18 -0
  158. package/dist/io/xlsx/row-position.js +11 -0
  159. package/dist/io/xlsx/row-xml.d.ts +128 -0
  160. package/dist/io/xlsx/row-xml.js +203 -0
  161. package/dist/io/xlsx/shared-formulas.js +2 -2
  162. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  163. package/dist/io/xlsx/sheet-properties.js +145 -9
  164. package/dist/io/xlsx/style-elements.d.ts +43 -0
  165. package/dist/io/xlsx/style-elements.js +156 -0
  166. package/dist/io/xlsx/styles.d.ts +12 -3
  167. package/dist/io/xlsx/styles.js +5 -207
  168. package/dist/io/xlsx/tables.js +10 -14
  169. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  170. package/dist/io/xlsx/theme-xml.js +107 -69
  171. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  172. package/dist/io/xlsx/threaded-comments.js +2 -1
  173. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  174. package/dist/io/xlsx/workbook-xml.js +60 -34
  175. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  176. package/dist/io/xlsx/worksheet-xml.js +48 -229
  177. package/dist/io/xlsx/write-stream.d.ts +65 -16
  178. package/dist/io/xlsx/write-stream.js +50 -20
  179. package/dist/io/xlsx/write.d.ts +1 -2
  180. package/dist/io/xlsx/write.js +114 -74
  181. package/dist/rel-type.d.ts +16 -0
  182. package/dist/rel-type.js +6 -0
  183. package/dist/token-set.d.ts +13 -0
  184. package/dist/token-set.js +4 -1
  185. package/dist/vba/bytes.d.ts +14 -5
  186. package/dist/vba/bytes.js +16 -6
  187. package/dist/vba/cfb-format.d.ts +8 -0
  188. package/dist/vba/cfb-format.js +1 -0
  189. package/dist/vba/cfb-writer.d.ts +9 -0
  190. package/dist/vba/cfb-writer.js +12 -9
  191. package/dist/vba/cfb.d.ts +13 -1
  192. package/dist/vba/cfb.js +73 -51
  193. package/dist/vba/codepage.js +2 -1
  194. package/dist/vba/errors.d.ts +7 -0
  195. package/dist/vba/ms-ovba.js +51 -16
  196. package/dist/vba/project-editor.js +35 -40
  197. package/dist/vba/project.d.ts +11 -1
  198. package/dist/vba/project.js +27 -8
  199. package/dist/vba/vba-encoding.js +4 -5
  200. package/dist/xml/xml-attrs.d.ts +37 -0
  201. package/dist/xml/xml-attrs.js +30 -0
  202. package/dist/xml/xml-chars.d.ts +40 -0
  203. package/dist/xml/xml-chars.js +12 -0
  204. package/dist/xml/xml-namespaces.d.ts +31 -0
  205. package/dist/xml/xml-namespaces.js +50 -0
  206. package/dist/xml/xml-read.d.ts +72 -10
  207. package/dist/xml/xml-read.js +94 -18
  208. package/dist/xml/xml-scan.d.ts +15 -37
  209. package/dist/xml/xml-scan.js +12 -40
  210. package/dist/xml/xml.d.ts +28 -27
  211. package/dist/xml/xml.js +24 -24
  212. package/package.json +12 -7
  213. /package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +0 -0
@@ -1,5 +1,71 @@
1
- import type { NamedCellStyle, TableStyleTable } from './style.ts';
1
+ import type { CellStyle } from './style.ts';
2
2
  import type { TableStyle } from './table-style.ts';
3
+ /**
4
+ * A named cell style: the OOXML `cellStyleXfs`/`cellStyles` layer. A spreadsheet applies a built-in
5
+ * or custom style (e.g. "Normal", "Accent1") whose visual facets live in this shared, named layer
6
+ * rather than on each cell's direct format; a cell links to it and inherits any facet the direct
7
+ * format leaves unset. The facets are a cell's own (see {@link CellStyle}); `name` is the style's
8
+ * display name and `builtinId` its Excel gallery index when it is a built-in style.
9
+ */
10
+ export type NamedCellStyle = Readonly<CellStyle> & {
11
+ readonly name?: string;
12
+ readonly builtinId?: number;
13
+ };
14
+ /**
15
+ * A differential style (OOXML CT_Dxf): formatting laid *over* whatever a cell already carries. Only
16
+ * the facets present override; the rest of the cell's own style shows through. It carries the subset
17
+ * of the cell-style facets (see {@link CellStyle}) a `<dxf>` can express: font, number format, fill,
18
+ * and border.
19
+ *
20
+ * Differential styles live in one workbook-level table (`<dxfs>`) that several features index into:
21
+ * a conditional-formatting rule's highlight format, and a table style's per-element formatting
22
+ * (`<tableStyleElement dxfId="…">`). They are interned and shared, so two features asking for the
23
+ * same formatting land on one entry.
24
+ *
25
+ * Not every facet reaches every consumer. As a **table style element**, Excel applies only the font,
26
+ * fill, and border: its own object model exposes `Font`, `Interior`, and `Borders` on a table style
27
+ * element and nothing for a number format, so a `numFmt` set here is carried faithfully through a
28
+ * round-trip but has no visible effect. The type is left whole rather than split, because the same
29
+ * value is legitimately reused across both consumers and narrowing it would only move the surprise.
30
+ */
31
+ export type DifferentialStyle = Pick<CellStyle, 'font' | 'numFmt' | 'fill' | 'border'>;
32
+ /**
33
+ * The `<tableStyles>` block of a styles part: the custom table/pivot style definitions a file
34
+ * declares, and the two gallery names it nominates as the default for a new table and a new pivot.
35
+ *
36
+ * Each entry of {@link styles} is one `<tableStyle>…</tableStyle>` fragment kept verbatim, for the
37
+ * same reason a `<dxf>` is: a `tableStyleElement`'s `dxfId` indexes the differential-style table,
38
+ * which the writer re-emits **at its original indices**, so the references stay valid without
39
+ * reparsing anything. That index-stability is load-bearing: renumbering the dxf table would
40
+ * silently re-point every preserved table style at a different format.
41
+ *
42
+ * The two default names are ordinary strings, not fragments: they are re-escaped on write, so they
43
+ * are held decoded.
44
+ */
45
+ export interface TableStyleTable {
46
+ readonly styles: readonly string[];
47
+ readonly defaultTableStyle?: string | undefined;
48
+ readonly defaultPivotStyle?: string | undefined;
49
+ /**
50
+ * The namespace prefixes the verbatim {@link styles} fragments use, mapped to their URI and to
51
+ * whether the source marked the prefix ignorable (`mc:Ignorable`).
52
+ *
53
+ * Carrying a fragment verbatim carries its *prefixes* too. Excel stamps a revision id
54
+ * (`xr9:uid="{…}"`) on every `<tableStyle>` it writes, so a fragment re-emitted under a
55
+ * `<styleSheet>` that declares only the default namespace is not namespace-well-formed, and no
56
+ * consumer can parse the part at all, which is a far louder failure than the dropped table style
57
+ * this preservation exists to prevent. The writer re-declares each prefix on `<styleSheet>` and
58
+ * re-states the ignorable ones, exactly as the source did.
59
+ */
60
+ readonly namespaces?: readonly TableStyleNamespace[];
61
+ }
62
+ /** One namespace declaration a preserved `<tableStyle>` fragment depends on. */
63
+ export interface TableStyleNamespace {
64
+ readonly prefix: string;
65
+ readonly uri: string;
66
+ /** Whether the source listed this prefix in the stylesheet's `mc:Ignorable`. */
67
+ readonly ignorable: boolean;
68
+ }
3
69
  /**
4
70
  * The preserved style tables of a workbook: the `<dxfs>` fragments, the named cell styles, the two
5
71
  * colour lists, and the table-style definitions a file declared plus the ones a caller authored.
@@ -1,3 +1,4 @@
1
+ import { isRelType } from '../rel-type.js';
1
2
  import { addVbaReference, parseVbaProject, removeVbaModule, VBA_PROJECT_CONTENT_TYPE, VBA_PROJECT_PART_PATH, VBA_PROJECT_REL_TYPE, VbaAuthorError, vbaProjectSignatureKind, } from '../vba/index.js';
2
3
  import { replaceContents } from './containers.js';
3
4
  export class WorkbookVbaProject {
@@ -21,7 +22,7 @@ export class WorkbookVbaProject {
21
22
  set bytes(bytes) {
22
23
  if (bytes !== undefined)
23
24
  parseVbaProject(bytes);
24
- replaceContents(this.#references, this.#references.filter((r) => !r.relType.endsWith('/vbaProject')));
25
+ replaceContents(this.#references, this.#references.filter((r) => !isRelType(r.relType, 'vbaProject')));
25
26
  if (bytes !== undefined) {
26
27
  this.#references.push({
27
28
  relType: VBA_PROJECT_REL_TYPE,
@@ -70,7 +71,7 @@ export class WorkbookVbaProject {
70
71
  this.bytes = addVbaReference(bytes, ref);
71
72
  }
72
73
  #ref() {
73
- return this.#references.find((r) => r.relType.endsWith('/vbaProject'));
74
+ return this.#references.find((r) => isRelType(r.relType, 'vbaProject'));
74
75
  }
75
76
  #entry() {
76
77
  const ref = this.#ref();
@@ -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);