@shbernal/ts-xlsx 1.0.3 → 1.2.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 (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,9 +1,3 @@
1
- // The workbook: the top of the model and the entry point of the public API.
2
- //
3
- // It owns its worksheets and the document-level properties. Sheet identity follows
4
- // Excel's rules — names are unique case-insensitively, bounded in length, and free
5
- // of the characters Excel forbids — so an invalid book cannot be constructed in the
6
- // first place, rather than failing only at write time.
7
1
  import { isCustomUiRelType, parseCustomUi } from '../customui/index.js';
8
2
  import { AuthoringError } from '../errors.js';
9
3
  import { addVbaReference, parseVbaProject, removeVbaModule, VBA_PROJECT_CONTENT_TYPE, VBA_PROJECT_PART_PATH, VBA_PROJECT_REL_TYPE, VbaAuthorError, vbaProjectSignatureKind, } from '../vba/index.js';
@@ -15,15 +9,6 @@ import { INTERNAL } from './internal.js';
15
9
  import { checkTableStyle } from './table-style.js';
16
10
  import { applyThemeOverrides, DEFAULT_THEME_COLOR_SCHEME, DEFAULT_THEME_FONTS, DEFAULT_THEME_XML, OFFICE_BODY_FACE, parseThemeColorScheme, parseThemeFontScheme, THEME_COLOR_SLOTS, } from './theme.js';
17
11
  import { Worksheet } from './worksheet.js';
18
- /**
19
- * The window geometry a workbook starts from — the values desktop Excel writes for its own default
20
- * window.
21
- *
22
- * A default is emitted rather than the element left out because Excel writes `<bookViews>` into every
23
- * file it saves and consumers lay panes out against that rect. With no view at all the frozen-pane
24
- * split is computed against an uninitialised window, and the frozen region can stay unpainted until
25
- * some later event forces a relayout.
26
- */
27
12
  export const DEFAULT_WORKBOOK_VIEW = {
28
13
  x: -110,
29
14
  y: -110,
@@ -32,120 +17,42 @@ export const DEFAULT_WORKBOOK_VIEW = {
32
17
  activeTab: 0,
33
18
  };
34
19
  const MAX_SHEET_NAME_LENGTH = 31;
35
- // Excel rejects these in a sheet name, plus a leading/trailing apostrophe.
36
20
  const INVALID_SHEET_NAME_CHARS = /[*?:\\/[\]]/;
37
21
  export class Workbook {
38
22
  properties = {};
39
- /**
40
- * The workbook's window state — position, size, and the selected sheet. Always present (see
41
- * {@link DEFAULT_WORKBOOK_VIEW} for why it is defaulted rather than left unset) and always written.
42
- * Reading a file replaces it with that file's saved geometry, so a round-trip restores the window
43
- * the author left rather than stamping ours over it.
44
- */
45
23
  view = { ...DEFAULT_WORKBOOK_VIEW };
46
- /**
47
- * Ask consuming spreadsheet apps to recalculate every formula when the file is opened, rather than
48
- * trusting the cached results stored with each formula cell. Set this when the producer cannot
49
- * compute formula results itself — the OOXML `fullCalcOnLoad` flag. Off by default, so a workbook
50
- * whose cached results are authoritative stays unmarked.
51
- */
52
24
  fullCalcOnLoad = false;
53
- /**
54
- * Workbook-level structure/window protection — the OOXML `<workbookProtection>` element. Absent by
55
- * default (an unprotected workbook). Set it to lock the workbook shell, or leave it as read from a
56
- * file so a protected workbook stays locked across a passthrough save rather than being silently
57
- * unlocked. Distinct from a worksheet's own `protect()`, which guards a single sheet's cells.
58
- */
59
25
  protection = undefined;
60
26
  #worksheets = [];
61
27
  #nextSheetId = 1;
62
- // Media is shared workbook-wide: a worksheet anchors an image by its registry index, so one
63
- // picture used on several sheets is stored once.
64
28
  #media = [];
65
29
  #definedNames = [];
66
- // Differential styles (`<dxfs>`) are a workbook-level table in styles.xml that conditional
67
- // formatting references by index. The library models the classic scale rules directly but preserves
68
- // the dxf table as opaque XML fragments, so a rule that references a dxfId (a highlight fill, a
69
- // custom number format) keeps a valid target across a read/write cycle instead of dangling.
70
30
  #dxfs = [];
71
- // Named cell styles (`cellStyleXfs`/`cellStyles` in styles.xml) — the shared, named formatting layer
72
- // a cell links to by index. Preserved so a cell whose fill/font/… lives only in a named style keeps
73
- // that style, and the link, across a round-trip. Empty when a file declares nothing beyond the
74
- // default Normal style, in which case the writer emits just that default.
75
31
  #namedStyles = [];
76
- // A custom indexed-color palette (`<colors><indexedColors>` in styles.xml) read from a file, each
77
- // entry a verbatim `<rgbColor rgb="…"/>` fragment. Preserved so an `indexed="…"` colour reference
78
- // keeps its intended RGB across a round-trip instead of resolving to a different default-palette
79
- // entry. Empty for a workbook that never overrode the palette.
80
32
  #indexedColors = [];
81
- // The most-recently-used colour swatches (`<colors><mruColors>` in styles.xml), each a verbatim
82
- // `<color rgb="…"/>` fragment. The author's own working set of colours; dropping it on a re-write
83
- // quietly resets a habit. Empty for a workbook that never picked a custom colour.
84
33
  #mruColors = [];
85
- // The custom table-style definitions (`<tableStyles>` in styles.xml), each `<tableStyle>` kept
86
- // verbatim, plus the gallery names the file nominates as the default for a new table and pivot. A
87
- // table's `tableStyleInfo/@name` can name one of these definitions, so dropping the block leaves
88
- // that reference dangling and the table renders unstyled.
89
34
  #tableStyles = { styles: [] };
90
- // The theme part read from a file, kept verbatim with the closure of parts it reaches. The writer
91
- // emits its own default theme for a workbook that has none, so without this a branded theme would be
92
- // overwritten by that default and every `theme="n"` colour in the file would silently re-render.
93
- // Undefined for a workbook authored from scratch, or read from a package declaring no theme.
94
35
  #theme;
95
- // Workbook-level references to package content the model does not interpret (pivot caches, slicer
96
- // caches), captured verbatim on read so a round-trip re-emits them rather than dropping the pivots
97
- // and slicers they back. Empty for a workbook authored from scratch.
98
36
  #preservedReferences = [];
99
- // The threaded-comment identity registry (`xl/persons/person.xml`), the workbook-level table every
100
- // message resolves its author through and every @mention its target. Keyed by person id — see
101
- // `restorePersons` for why nothing else will do. Empty for a workbook with no threaded comments.
102
37
  #persons = new Map();
103
- /** The worksheets in insertion order. */
104
38
  get worksheets() {
105
39
  return this.#worksheets;
106
40
  }
107
- /**
108
- * The 0-based index of the active sheet: {@link WorkbookView.activeTab} resolved against the sheets
109
- * that actually exist. Exactly one sheet is always active — an out-of-range tab (a caller's stale
110
- * index, or a file whose sheet was removed after the view was saved) falls back to the first sheet
111
- * rather than to none, because a package where no sheet is selected gives the consumer no view to
112
- * initialise on open.
113
- */
114
41
  get activeTabIndex() {
115
42
  const { activeTab } = this.view;
116
43
  return Number.isInteger(activeTab) && activeTab > 0 && activeTab < this.#worksheets.length
117
44
  ? activeTab
118
45
  : 0;
119
46
  }
120
- /** The workbook-level preserved references, in the order they were read. */
121
47
  get preservedReferences() {
122
48
  return this.#preservedReferences;
123
49
  }
124
- // Package-root references to unmodeled content wired from `_rels/.rels` (the ribbon customUI parts,
125
- // custom document properties, a thumbnail), captured verbatim on read so a round-trip re-declares
126
- // them in the regenerated root rels rather than dropping them. Empty for a workbook authored from
127
- // scratch.
128
50
  #preservedRootReferences = [];
129
- /** The package-root preserved references, in the order they were read. */
130
51
  get preservedRootReferences() {
131
52
  return this.#preservedRootReferences;
132
53
  }
133
- // Lazily-parsed customUI ribbon view. `#customUiParsed` distinguishes "not yet parsed" from a genuine
134
- // "no ribbon parts" (an empty array), so a ribbon-free workbook is not re-scanned on every access.
135
54
  #customUiParsed = false;
136
55
  #customUI = [];
137
- /**
138
- * The ribbon customisations decoded from this workbook's `customUI` parts — `customUI.xml` (Office
139
- * 2007) and/or `customUI14.xml` (Office 2010+), in the order their root relationships were read. Each
140
- * {@link CustomUiDocument} is tagged with its dialect and exposes the parsed `<ribbon>` tree. Empty
141
- * for a workbook that customises no ribbon.
142
- *
143
- * This is a **read-only view** over parts the writer already round-trips verbatim — mutating the
144
- * returned objects changes nothing on write; the original `customUI` XML is re-emitted byte-for-byte
145
- * regardless. Parsed lazily on first access and memoised.
146
- *
147
- * @throws {CustomUiParseError} if a `customUI` part is present but its XML is malformed.
148
- */
149
56
  get customUI() {
150
57
  if (!this.#customUiParsed) {
151
58
  this.#customUI = this.#preservedRootReferences
@@ -158,18 +65,8 @@ export class Workbook {
158
65
  }
159
66
  return this.#customUI;
160
67
  }
161
- // Lazily-decoded macro source. `#vbaParsed` distinguishes "not yet decoded" from a genuine "no
162
- // macros" (`undefined`) result, so a macro-free workbook is not re-probed on every access.
163
68
  #vbaParsed = false;
164
69
  #vbaProject = undefined;
165
- /**
166
- * The VBA project decoded from this workbook's preserved `vbaProject.bin`, or `undefined` for a
167
- * workbook with no macros. This is a **read-only view** over the bytes the writer already round-trips
168
- * verbatim — mutating the returned object changes nothing on write; the original macro blob is
169
- * re-emitted byte-for-byte regardless. Parsed lazily on first access and memoised.
170
- *
171
- * @throws {VbaParseError} if a macro project is present but its `vbaProject.bin` is malformed.
172
- */
173
70
  get vbaProject() {
174
71
  if (!this.#vbaParsed) {
175
72
  const bytes = this.#vbaProjectEntry()?.bytes;
@@ -178,34 +75,12 @@ export class Workbook {
178
75
  }
179
76
  return this.#vbaProject;
180
77
  }
181
- /**
182
- * The raw `vbaProject.bin` bytes attached to this workbook — the exact macro blob the writer will
183
- * embed — or `undefined` for a workbook with no macros. The getter returns a defensive copy, so
184
- * mutating it changes nothing on write.
185
- *
186
- * Assigning bytes attaches (or replaces) the macro project: the written package becomes
187
- * macro-enabled and re-embeds these bytes verbatim. The bytes must be a well-formed VBA container
188
- * (a CFB holding a `dir` stream); a malformed blob is rejected with {@link VbaParseError} rather
189
- * than written out to produce a package Excel would flag for repair. This is the attach-blob path:
190
- * copy a project between workbooks with `dst.vbaProjectBytes = src.vbaProjectBytes`, or import a
191
- * `.bin` produced by another tool. Assigning `undefined` removes the project, reverting the workbook
192
- * to a plain (non-macro) package.
193
- *
194
- * Replacing or removing the project also drops any digital signature the previous blob carried — a
195
- * signature over the old bytes cannot validate new ones — so the result never advertises a broken
196
- * signature.
197
- */
198
78
  get vbaProjectBytes() {
199
79
  return this.#vbaProjectEntry()?.bytes.slice();
200
80
  }
201
81
  set vbaProjectBytes(bytes) {
202
- // Validate before touching any state: a malformed blob must fail closed and leave the existing
203
- // project intact, never half-remove it. Only past this point do we mutate.
204
82
  if (bytes !== undefined)
205
83
  parseVbaProject(bytes);
206
- // Drop any existing project; its whole closure goes, taking a now-stale signature part with it. A
207
- // fresh reference then mirrors exactly what the reader captures for a macro workbook, so the writer
208
- // emits a byte-identical macro-enabled package with no writer changes.
209
84
  replaceContents(this.#preservedReferences, this.#preservedReferences.filter((r) => !r.relType.endsWith('/vbaProject')));
210
85
  if (bytes !== undefined) {
211
86
  this.#preservedReferences.push({
@@ -224,37 +99,12 @@ export class Workbook {
224
99
  this.#vbaParsed = false;
225
100
  this.#vbaProject = undefined;
226
101
  }
227
- /**
228
- * Whether this workbook's VBA project carries a digital signature — `true` if any signature part is
229
- * attached, `false` for an unsigned project or a workbook with no macros.
230
- *
231
- * This reflects the **presence** of a signature blob, not its cryptographic validity: the library
232
- * neither parses the PKCS#7/CMS structure nor validates the certificate chain or signer. A `true`
233
- * here means "a signature is attached," never "this signature is valid." Replacing or editing the
234
- * project drops its signatures (a signature over the old bytes cannot validate new ones), so this
235
- * reads `false` again after {@link vbaProjectBytes}, {@link removeVbaModule}, or
236
- * {@link addVbaReference} mutates the project. See {@link vbaProjectSignatures} for the raw bytes and
237
- * which generation(s) are present.
238
- */
239
102
  get vbaProjectSigned() {
240
103
  return this.#vbaSignatures().length > 0;
241
104
  }
242
- /**
243
- * The digital signatures attached to this workbook's VBA project, in the order their relationships
244
- * are wired off `vbaProject.bin` — up to three generations (legacy, agile, V3) can coexist over the
245
- * same project bytes. Empty for an unsigned project or a workbook with no macros.
246
- *
247
- * Each entry's `bytes` are the raw signature blob passed through verbatim; the library does not parse
248
- * or verify them (see {@link vbaProjectSigned} on presence-vs-validity). Hand a blob to an external
249
- * verifier if you need cryptographic validation — that is deliberately out of this library's scope.
250
- */
251
105
  get vbaProjectSignatures() {
252
106
  return this.#vbaSignatures();
253
107
  }
254
- // Walk the VBA project's preserved closure for its signature parts — each reached by a signature
255
- // relationship off `vbaProject.bin`. Computed on each access rather than memoised: the closure is
256
- // small and already in memory, and recomputing sidesteps a cache that a signature-dropping mutation
257
- // (`vbaProjectBytes` replace, module remove, reference add) would otherwise have to invalidate.
258
108
  #vbaSignatures() {
259
109
  const ref = this.#vbaProjectRef();
260
110
  const entry = ref?.parts.find((p) => p.path === ref.entryPath);
@@ -271,19 +121,6 @@ export class Workbook {
271
121
  }
272
122
  return signatures;
273
123
  }
274
- /**
275
- * Remove a standard module from this workbook's existing macro project, in place — a structural splice
276
- * that leaves every remaining module's compiled p-code untouched (see {@link removeVbaModule}).
277
- * Replacing the project also drops a stale signature, as {@link vbaProjectBytes} does.
278
- *
279
- * Only `procedural` and `class` modules can be removed this way — see {@link removeVbaModule} for why.
280
- * To author or edit module *source* (which needs real compiled p-code), use the offline
281
- * `tools/vba-compiler`, then attach its output via {@link vbaProjectBytes}.
282
- *
283
- * @throws {VbaAuthorError} if the workbook has no macro project, or `name` is not in the project,
284
- * or names a `document`/`designer` module.
285
- * @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
286
- */
287
124
  removeVbaModule(name) {
288
125
  const bytes = this.vbaProjectBytes;
289
126
  if (bytes === undefined) {
@@ -291,16 +128,6 @@ export class Workbook {
291
128
  }
292
129
  this.vbaProjectBytes = removeVbaModule(bytes, name);
293
130
  }
294
- /**
295
- * Add a registered (COM type-library) reference to this workbook's existing macro project, in place.
296
- * Every existing module, reference, and host-info record rides through unchanged (see
297
- * {@link addVbaReference}). Replacing the project also drops a stale signature, as
298
- * {@link vbaProjectBytes} does.
299
- *
300
- * @throws {VbaAuthorError} if the workbook has no macro project, or any field of `ref` is invalid
301
- * (see {@link VbaLibraryReference}).
302
- * @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
303
- */
304
131
  addVbaReference(ref) {
305
132
  const bytes = this.vbaProjectBytes;
306
133
  if (bytes === undefined) {
@@ -315,162 +142,60 @@ export class Workbook {
315
142
  const ref = this.#vbaProjectRef();
316
143
  return ref?.parts.find((p) => p.path === ref.entryPath);
317
144
  }
318
- /** The preserved differential-style (`<dxfs>`) fragments, in index order. */
319
145
  get differentialStyles() {
320
146
  return this.#dxfs;
321
147
  }
322
- /** The preserved custom indexed-color palette, in index order; empty when the default palette rules. */
323
148
  get indexedColors() {
324
149
  return this.#indexedColors;
325
150
  }
326
- /** The preserved most-recently-used colour swatches, in order; empty when the file declared none. */
327
151
  get mruColors() {
328
152
  return this.#mruColors;
329
153
  }
330
- /** The preserved `<tableStyles>` block; `styles` is empty when the file declared no custom style. */
331
154
  get tableStyles() {
332
155
  return this.#tableStyles;
333
156
  }
334
- // Table styles authored on this workbook, keyed by name so registering the same name twice replaces
335
- // rather than duplicates — two definitions sharing a name leave a table's reference ambiguous.
336
157
  #customTableStyles = new Map();
337
- /**
338
- * Register a custom table style — a named look a table applies to itself by putting that name in
339
- * {@link TableStyleInfo.name}, exactly as it would name one of Excel's built-in gallery styles.
340
- *
341
- * ```ts
342
- * workbook.addTableStyle({
343
- * name: 'Harbour',
344
- * elements: {
345
- * wholeTable: {border: {top: {style: 'thin'}, bottom: {style: 'thin'}}},
346
- * headerRow: {font: {bold: true, color: {argb: 'FFFFFFFF'}},
347
- * fill: {type: 'pattern', pattern: 'solid', bgColor: {argb: 'FFBB2649'}}},
348
- * firstRowStripe: {fill: {type: 'pattern', pattern: 'solid', bgColor: {argb: 'FFF6E7EB'}}},
349
- * },
350
- * });
351
- * sheet.addTable({name: 'Cargo', ref: 'A1:B3', columns, style: {name: 'Harbour'}});
352
- * ```
353
- *
354
- * Each element's formatting is interned into the workbook's shared differential-style table, so two
355
- * elements — or a conditional-formatting rule — that paint the same way share one entry.
356
- *
357
- * Registering a name a source file already defined **overrides** that definition rather than adding
358
- * a second one beside it.
359
- *
360
- * @throws {AuthoringError} if the name is empty, or an element carries a `size` outside the four stripe
361
- * types, or a `size` is not a positive integer — see {@link checkTableStyle} for why those are
362
- * refused here rather than silently dropped.
363
- */
364
158
  addTableStyle(style) {
365
159
  checkTableStyle(style);
366
160
  this.#customTableStyles.set(style.name, style);
367
161
  }
368
- /** The table styles authored on this workbook, in registration order. */
369
162
  get customTableStyles() {
370
163
  return [...this.#customTableStyles.values()];
371
164
  }
372
- /** The preserved theme part, or undefined when the workbook rides the library's default theme. */
373
165
  get themePart() {
374
166
  return this.#theme;
375
167
  }
376
- // The theme's colour scheme, decoded from the preserved part (and merged with any authored
377
- // overrides) on first use. Cached because resolving a colour is a per-cell operation and the part is
378
- // otherwise held as bytes; invalidated whenever the theme is replaced or authored.
379
168
  #themeColors;
380
- // Colour slots and typefaces the caller authored, merged over whatever the workbook already had.
381
169
  #authoredTheme = {
382
170
  colors: {},
383
171
  fonts: {},
384
172
  };
385
- /**
386
- * Author the workbook's theme: any subset of the twelve colour-scheme slots, and either of the two
387
- * typefaces. Merges into what the workbook already has, so branding one accent leaves the other
388
- * eleven slots alone, and calling it twice accumulates.
389
- *
390
- * This is the workbook-wide palette. A cell that names a colour as `theme="4"` — which is what Excel
391
- * writes whenever a user picks from the theme row of the colour picker — follows `accent1` here, so
392
- * one call restyles every such cell, chart and table style at once. Colours are `RRGGBB`; a leading
393
- * `#` and an 8-hex ARGB are both accepted and reduced, and anything else throws rather than writing
394
- * a value Excel silently renders as flat black.
395
- *
396
- * What it does **not** touch: the theme's format scheme — the gradient, line and effect styles that
397
- * give a theme its texture. Those ride through from the source theme (or the library's default)
398
- * untouched, because nobody hand-authors gradient stops from a spreadsheet API and regenerating them
399
- * would replace a designer's work with the Office default. For the same reason a slot left
400
- * unauthored keeps the source's own encoding, including the `<a:sysClr>` form Excel uses for
401
- * `dk1`/`lt1` so they follow the viewer's window colours.
402
- *
403
- * @throws {AuthoringError} if a colour is not 6 or 8 hexadecimal digits.
404
- */
405
173
  setTheme(overrides) {
406
- // Validated eagerly, by running the generation the writer will later run: a colour rejected at
407
- // write time would surface far from the call that supplied it.
408
174
  applyThemeOverrides(this.#baseThemeXml(), overrides);
409
175
  Object.assign(this.#authoredTheme.colors, overrides.colors ?? {});
410
176
  this.#authoredTheme.fonts = { ...this.#authoredTheme.fonts, ...(overrides.fonts ?? {}) };
411
177
  this.#themeColors = undefined;
412
178
  }
413
- /**
414
- * The colour scheme every `theme="n"` reference in this workbook resolves against — anything
415
- * {@link setTheme} authored, over the preserved theme's `<a:clrScheme>`, over the Office default.
416
- *
417
- * Note the slot *order*: `theme="0"` is `lt1` and `theme="1"` is `dk1`, which is not the order the
418
- * slots appear in the theme part. See {@link THEME_COLOR_SLOTS}.
419
- */
420
179
  get themeColors() {
421
180
  if (this.#themeColors === undefined) {
422
181
  const xml = this.#themeXml();
423
- // A theme that declares no scheme (or none this reader decodes) falls back to the Office
424
- // default rather than resolving nothing: the file still renders against *some* scheme, and the
425
- // default is the one the writer would have shipped.
426
182
  const parsed = xml === undefined ? {} : parseThemeColorScheme(xml);
427
183
  const base = Object.keys(parsed).length === 0 ? DEFAULT_THEME_COLOR_SCHEME : parsed;
428
184
  this.#themeColors = { ...base, ...this.#authoredTheme.colors };
429
185
  }
430
186
  return this.#themeColors;
431
187
  }
432
- /** The theme's major (heading) and minor (body) typefaces, authored values over the source's. */
433
188
  get themeFonts() {
434
189
  const xml = this.#themeXml();
435
190
  const parsed = xml === undefined ? {} : parseThemeFontScheme(xml);
436
191
  const base = Object.keys(parsed).length === 0 ? DEFAULT_THEME_FONTS : parsed;
437
192
  return { ...base, ...this.#authoredTheme.fonts };
438
193
  }
439
- // Font id 0 exactly as a source file declared it, before anything was authored over it. Held apart
440
- // from the authored layer because the two answer different questions and only their separation makes
441
- // the round-trip faithful: a file's font 0 must ride through untouched, while an authored one must
442
- // win. Undefined for a workbook built from scratch, or read from a package with no styles part.
443
194
  #declaredDefaultFont;
444
- // The facets {@link setDefaultFont} has accumulated, merged in call order. Undefined until a caller
445
- // authors one, which is what tells {@link defaultFont} it may re-derive rather than pass through.
446
195
  #authoredDefaultFont;
447
- /**
448
- * The default font as the source package declared it — font id 0 of its styles part, the face every
449
- * cell that names no font of its own renders in. `undefined` for a workbook authored from scratch or
450
- * read from a package carrying no styles part: nothing was declared, and the library does not
451
- * fabricate a declaration on the file's behalf.
452
- *
453
- * This is the *round-trip* surface. {@link defaultFont} is what the workbook actually renders in,
454
- * which is this once anything has been authored over it.
455
- */
456
196
  get declaredDefaultFont() {
457
197
  return this.#declaredDefaultFont;
458
198
  }
459
- /**
460
- * Author the workbook's default font — the face, size and colour every cell with no font of its own
461
- * renders in, **empty cells included**. Merges into whatever the workbook already had, so
462
- * `setDefaultFont({size: 14})` keeps the resolved face and changes only the size, and calling it
463
- * twice accumulates. This is the one knob that reaches a cell no row or column default can: an
464
- * untouched cell in an unformatted column.
465
- *
466
- * It writes the styles part's font 0 and **nothing else** — in particular it does not rewrite the
467
- * theme's body typeface. The dependency runs the other way: with no default font authored, font 0
468
- * follows {@link themeFonts}'s minor face, so `setTheme({fonts: {minor}})` already reaches every
469
- * unstyled cell and needs no second call here. See {@link defaultFont} for the full chain.
470
- *
471
- * @throws {AuthoringError} if `size` is not a positive finite number, or `name` is empty — both
472
- * produce a styles part Excel renders from some other font without ever reporting why.
473
- */
474
199
  setDefaultFont(font) {
475
200
  if (font.size !== undefined && !(Number.isFinite(font.size) && font.size > 0)) {
476
201
  throw new AuthoringError(`default font size must be a positive number, not ${font.size}`);
@@ -480,27 +205,6 @@ export class Workbook {
480
205
  }
481
206
  this.#authoredDefaultFont = { ...this.#authoredDefaultFont, ...font };
482
207
  }
483
- /**
484
- * The font every cell that names none of its own renders in, resolved and complete — what the writer
485
- * emits as font id 0. Never `undefined`: a workbook always renders in *some* face, and the chain
486
- * below always reaches one.
487
- *
488
- * ```
489
- * authored default font > authored theme body face > the source file's font 0 > theme body face
490
- * ```
491
- *
492
- * The two authored levels outrank the file because authoring is an explicit act; between them
493
- * {@link setDefaultFont} wins on the face because it names font 0 outright while
494
- * {@link setTheme} names it only by implication. With **nothing** authored the file's own font 0
495
- * passes through verbatim — deliberately, because a producer resolves that face by script and we do
496
- * not: Excel writes `等线` as font 0 under a theme whose latin body face is `Calibri`, and
497
- * re-deriving would silently rewrite it.
498
- *
499
- * `family` and `scheme` describe the *theme's* body face, so they are carried exactly while the
500
- * resolved face still is that face and dropped when a caller names another — which is also what
501
- * Excel writes: a font 0 naming a non-theme face carries no `<scheme>` at all. Either may be stated
502
- * outright, in which case the caller's word stands.
503
- */
504
208
  get defaultFont() {
505
209
  const declared = this.#declaredDefaultFont;
506
210
  const authored = this.#authoredDefaultFont;
@@ -510,8 +214,6 @@ export class Workbook {
510
214
  }
511
215
  const bodyFace = this.themeFonts.minor ?? OFFICE_BODY_FACE;
512
216
  const face = authored?.name ?? authoredFace ?? declared?.name ?? bodyFace;
513
- // Size and colour are completed rather than merely merged: a font 0 that states neither is the
514
- // "missing default font" foreign readers warn about, so the emitted entry always carries both.
515
217
  const font = {
516
218
  size: 11,
517
219
  color: { theme: 1 },
@@ -534,26 +236,15 @@ export class Workbook {
534
236
  }
535
237
  return font;
536
238
  }
537
- /**
538
- * The theme part text this workbook should write, or `undefined` when nothing was authored and the
539
- * source theme (or the writer's default) should ride through untouched.
540
- *
541
- * Authoring generates *over* the existing part rather than from scratch — see
542
- * {@link applyThemeOverrides} — so a preserved theme keeps its format scheme, its unauthored slots'
543
- * exact encoding, and the relationships it carries.
544
- */
545
239
  authoredThemeXml() {
546
240
  const { colors, fonts } = this.#authoredTheme;
547
241
  if (Object.keys(colors).length === 0 && Object.keys(fonts).length === 0)
548
242
  return undefined;
549
243
  return applyThemeOverrides(this.#baseThemeXml(), { colors, fonts });
550
244
  }
551
- // The part authored overrides are applied on top of: the preserved source theme, else the default
552
- // one the writer would otherwise have emitted.
553
245
  #baseThemeXml() {
554
246
  return this.#themeXml() ?? DEFAULT_THEME_XML;
555
247
  }
556
- // The preserved theme part's text, decoded from the entry part of its closure.
557
248
  #themeXml() {
558
249
  const theme = this.#theme;
559
250
  if (theme === undefined)
@@ -561,64 +252,25 @@ export class Workbook {
561
252
  const entry = theme.parts.find((part) => part.path === theme.entryPath);
562
253
  return entry === undefined ? undefined : new TextDecoder().decode(entry.bytes);
563
254
  }
564
- /**
565
- * Resolve a colour reference to a concrete 8-hex ARGB string, or `undefined` when it does not
566
- * resolve to a fixed colour — an `auto` colour, one of the two system indexed colours, or a theme
567
- * slot this workbook's scheme does not declare.
568
- *
569
- * This is a *derived* view, not a rewrite: the {@link Color} stays exactly as its file encoded it,
570
- * so a round-trip re-emits `theme="4" tint="0.4"` rather than a literal ARGB. Resolving into the
571
- * model would sever every cell's link to the theme, so recolouring the workbook would stop working,
572
- * and would inflate the styles table with one distinct colour per shade.
573
- *
574
- * A `theme` reference resolves through {@link themeColors}; an `indexed` one through the workbook's
575
- * custom `<indexedColors>` palette when it declares one, else the built-in legacy palette. A `tint`
576
- * is applied last.
577
- */
578
255
  resolveColor(color) {
579
256
  return resolveColor(color, { theme: this.themeColors, indexed: this.#indexedPalette() });
580
257
  }
581
- // The workbook's custom palette as plain ARGB strings. `#indexedColors` holds verbatim
582
- // `<rgbColor rgb="…"/>` fragments — the form the writer re-emits — so the value is read out here
583
- // rather than stored twice in two shapes that could drift.
584
258
  #indexedPalette() {
585
259
  return this.#indexedColors.map((fragment) => /\brgb="([^"]*)"/.exec(fragment)?.[1] ?? '');
586
260
  }
587
- /** The named cell styles, in index order (index 0 is Normal); empty when only the default exists. */
588
261
  get namedStyles() {
589
262
  return this.#namedStyles;
590
263
  }
591
- /**
592
- * Register an identity a threaded comment can name — an author, or someone `@mentioned` in a message.
593
- * A message reaches it by {@link Comment.personId}, a mention by {@link Mention.personId}.
594
- *
595
- * Keyed by {@link Person.id} alone, so registering the same id twice replaces the entry rather than
596
- * adding a second: the id is the identity. Registering the same human twice under *different* ids is
597
- * legitimate and is what Excel itself does — see {@link restorePersons}. The id is normalised to the
598
- * brace-wrapped upper-case GUID form the format requires, so a `crypto.randomUUID()` is accepted as-is.
599
- *
600
- * @throws {SyntaxError} if the id is not a GUID.
601
- */
602
264
  addPerson(person) {
603
265
  const id = commentThreadGuid(person.id, 'a person id');
604
266
  this.#persons.set(id, { ...person, id });
605
267
  }
606
- /**
607
- * The registered threaded-comment identities, in the order they were read. That order carries no
608
- * meaning — Excel re-sorts the registry by person id when it saves — so nothing may depend on it.
609
- */
610
268
  get persons() {
611
269
  return [...this.#persons.values()];
612
270
  }
613
- /** Look up a registered identity by its {@link Person.id}, or `undefined` if the registry has none. */
614
271
  getPerson(id) {
615
272
  return this.#persons.get(id);
616
273
  }
617
- /**
618
- * Register a picture on the workbook and return its numeric id. Pass the id to
619
- * {@link Worksheet.addImage} to anchor the picture to a sheet; the same id may be anchored on any
620
- * number of sheets and positions, and the bytes are still stored only once.
621
- */
622
274
  addImage(options) {
623
275
  this.#media.push({
624
276
  extension: normalizeImageExtension(options.extension, options.buffer),
@@ -626,24 +278,15 @@ export class Workbook {
626
278
  });
627
279
  return this.#media.length - 1;
628
280
  }
629
- /** The registered images, indexed by the id {@link addImage} returned. */
630
281
  get media() {
631
282
  return this.#media;
632
283
  }
633
- /** Look up a registered image by its id, or `undefined` if no image carries that id. */
634
284
  getImage(id) {
635
285
  return this.#media[id];
636
286
  }
637
- /** The workbook's defined names, in the order they were registered. */
638
287
  get definedNames() {
639
288
  return this.#definedNames;
640
289
  }
641
- /**
642
- * Register a defined name on the workbook.
643
- *
644
- * @throws {AuthoringError} if the name is empty, or if a {@link DefinedName.scope} is given that names no
645
- * existing worksheet — a scoped name must target a sheet that is already part of the workbook.
646
- */
647
290
  defineName(definedName) {
648
291
  if (definedName.name.length === 0) {
649
292
  throw new AuthoringError('a defined name cannot be empty');
@@ -653,19 +296,12 @@ export class Workbook {
653
296
  }
654
297
  this.#definedNames.push(definedName);
655
298
  }
656
- /**
657
- * Create a worksheet and append it to the workbook.
658
- *
659
- * @throws {AuthoringError} if the name is empty, too long, contains a forbidden character,
660
- * or collides (case-insensitively) with an existing sheet.
661
- */
662
299
  addWorksheet(name, options = {}) {
663
300
  this.#assertValidSheetName(name);
664
301
  const sheet = new Worksheet(name, this.#nextSheetId++, options.state ?? 'visible');
665
302
  this.#worksheets.push(sheet);
666
303
  return sheet;
667
304
  }
668
- /** Look up a worksheet by name (case-insensitive) or by numeric id. */
669
305
  getWorksheet(nameOrId) {
670
306
  if (typeof nameOrId === 'number') {
671
307
  return this.#worksheets.find((sheet) => sheet.id === nameOrId);
@@ -673,6 +309,17 @@ export class Workbook {
673
309
  const target = nameOrId.toLowerCase();
674
310
  return this.#worksheets.find((sheet) => sheet.name.toLowerCase() === target);
675
311
  }
312
+ requireWorksheet(nameOrId) {
313
+ const sheet = this.getWorksheet(nameOrId);
314
+ if (sheet !== undefined)
315
+ return sheet;
316
+ const wanted = typeof nameOrId === 'number' ? `id ${nameOrId}` : JSON.stringify(nameOrId);
317
+ if (this.#worksheets.length === 0) {
318
+ throw new AuthoringError(`no worksheet ${wanted}: this workbook has no worksheets`);
319
+ }
320
+ const have = this.#worksheets.map((sheet) => JSON.stringify(sheet.name)).join(', ');
321
+ throw new AuthoringError(`no worksheet ${wanted}; this workbook has ${have}`);
322
+ }
676
323
  #assertValidSheetName(name) {
677
324
  if (name.length === 0) {
678
325
  throw new AuthoringError('worksheet name cannot be empty');
@@ -690,10 +337,6 @@ export class Workbook {
690
337
  throw new AuthoringError(`a worksheet named "${name}" already exists (names are case-insensitive)`);
691
338
  }
692
339
  }
693
- /**
694
- * The codec's channel into this workbook — see `core/internal.ts` for why these are not public
695
- * methods. Declared last so every private field it closes over is already in scope.
696
- */
697
340
  [INTERNAL] = {
698
341
  addPreservedReference: (reference) => {
699
342
  this.#preservedReferences.push(reference);