@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.
- package/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/dist/core/workbook.js
CHANGED
|
@@ -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);
|