@shbernal/ts-xlsx 1.0.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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,732 @@
|
|
|
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
|
+
import { isCustomUiRelType, parseCustomUi } from "../customui/index.js";
|
|
8
|
+
import { AuthoringError } from "../errors.js";
|
|
9
|
+
import { addVbaReference, parseVbaProject, removeVbaModule, VBA_PROJECT_CONTENT_TYPE, VBA_PROJECT_PART_PATH, VBA_PROJECT_REL_TYPE, VbaAuthorError, vbaProjectSignatureKind, } from "../vba/index.js";
|
|
10
|
+
import { resolveColor } from "./color-resolution.js";
|
|
11
|
+
import { commentThreadGuid } from "./comment-thread.js";
|
|
12
|
+
import { replaceContents } from "./containers.js";
|
|
13
|
+
import { normalizeImageExtension } from "./image.js";
|
|
14
|
+
import { INTERNAL } from "./internal.js";
|
|
15
|
+
import { checkTableStyle } from "./table-style.js";
|
|
16
|
+
import { applyThemeOverrides, DEFAULT_THEME_COLOR_SCHEME, DEFAULT_THEME_FONTS, DEFAULT_THEME_XML, OFFICE_BODY_FACE, parseThemeColorScheme, parseThemeFontScheme, THEME_COLOR_SLOTS, } from "./theme.js";
|
|
17
|
+
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
|
+
export const DEFAULT_WORKBOOK_VIEW = {
|
|
28
|
+
x: -110,
|
|
29
|
+
y: -110,
|
|
30
|
+
width: 19420,
|
|
31
|
+
height: 12220,
|
|
32
|
+
activeTab: 0,
|
|
33
|
+
};
|
|
34
|
+
const MAX_SHEET_NAME_LENGTH = 31;
|
|
35
|
+
// Excel rejects these in a sheet name, plus a leading/trailing apostrophe.
|
|
36
|
+
const INVALID_SHEET_NAME_CHARS = /[*?:\\/[\]]/;
|
|
37
|
+
export class Workbook {
|
|
38
|
+
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
|
+
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
|
+
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
|
+
protection = undefined;
|
|
60
|
+
#worksheets = [];
|
|
61
|
+
#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
|
+
#media = [];
|
|
65
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#persons = new Map();
|
|
103
|
+
/** The worksheets in insertion order. */
|
|
104
|
+
get worksheets() {
|
|
105
|
+
return this.#worksheets;
|
|
106
|
+
}
|
|
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
|
+
get activeTabIndex() {
|
|
115
|
+
const { activeTab } = this.view;
|
|
116
|
+
return Number.isInteger(activeTab) && activeTab > 0 && activeTab < this.#worksheets.length
|
|
117
|
+
? activeTab
|
|
118
|
+
: 0;
|
|
119
|
+
}
|
|
120
|
+
/** The workbook-level preserved references, in the order they were read. */
|
|
121
|
+
get preservedReferences() {
|
|
122
|
+
return this.#preservedReferences;
|
|
123
|
+
}
|
|
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
|
+
#preservedRootReferences = [];
|
|
129
|
+
/** The package-root preserved references, in the order they were read. */
|
|
130
|
+
get preservedRootReferences() {
|
|
131
|
+
return this.#preservedRootReferences;
|
|
132
|
+
}
|
|
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
|
+
#customUiParsed = false;
|
|
136
|
+
#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 {@link CustomUiParseError} if a `customUI` part is present but its XML is malformed.
|
|
148
|
+
*/
|
|
149
|
+
get customUI() {
|
|
150
|
+
if (!this.#customUiParsed) {
|
|
151
|
+
this.#customUI = this.#preservedRootReferences
|
|
152
|
+
.filter((ref) => isCustomUiRelType(ref.relType))
|
|
153
|
+
.flatMap((ref) => {
|
|
154
|
+
const bytes = ref.parts.find((part) => part.path === ref.entryPath)?.bytes;
|
|
155
|
+
return bytes === undefined ? [] : [parseCustomUi(bytes)];
|
|
156
|
+
});
|
|
157
|
+
this.#customUiParsed = true;
|
|
158
|
+
}
|
|
159
|
+
return this.#customUI;
|
|
160
|
+
}
|
|
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
|
+
#vbaParsed = false;
|
|
164
|
+
#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 {@link VbaParseError} if a macro project is present but its `vbaProject.bin` is malformed.
|
|
172
|
+
*/
|
|
173
|
+
get vbaProject() {
|
|
174
|
+
if (!this.#vbaParsed) {
|
|
175
|
+
const bytes = this.#vbaProjectEntry()?.bytes;
|
|
176
|
+
this.#vbaProject = bytes ? parseVbaProject(bytes) : undefined;
|
|
177
|
+
this.#vbaParsed = true;
|
|
178
|
+
}
|
|
179
|
+
return this.#vbaProject;
|
|
180
|
+
}
|
|
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
|
+
get vbaProjectBytes() {
|
|
199
|
+
return this.#vbaProjectEntry()?.bytes.slice();
|
|
200
|
+
}
|
|
201
|
+
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
|
+
if (bytes !== undefined)
|
|
205
|
+
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
|
+
replaceContents(this.#preservedReferences, this.#preservedReferences.filter((r) => !r.relType.endsWith('/vbaProject')));
|
|
210
|
+
if (bytes !== undefined) {
|
|
211
|
+
this.#preservedReferences.push({
|
|
212
|
+
relType: VBA_PROJECT_REL_TYPE,
|
|
213
|
+
entryPath: VBA_PROJECT_PART_PATH,
|
|
214
|
+
parts: [
|
|
215
|
+
{
|
|
216
|
+
path: VBA_PROJECT_PART_PATH,
|
|
217
|
+
contentType: VBA_PROJECT_CONTENT_TYPE,
|
|
218
|
+
bytes: bytes.slice(),
|
|
219
|
+
rels: [],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
this.#vbaParsed = false;
|
|
225
|
+
this.#vbaProject = undefined;
|
|
226
|
+
}
|
|
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
|
+
get vbaProjectSigned() {
|
|
240
|
+
return this.#vbaSignatures().length > 0;
|
|
241
|
+
}
|
|
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
|
+
get vbaProjectSignatures() {
|
|
252
|
+
return this.#vbaSignatures();
|
|
253
|
+
}
|
|
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
|
+
#vbaSignatures() {
|
|
259
|
+
const ref = this.#vbaProjectRef();
|
|
260
|
+
const entry = ref?.parts.find((p) => p.path === ref.entryPath);
|
|
261
|
+
if (ref === undefined || entry === undefined)
|
|
262
|
+
return [];
|
|
263
|
+
const partByPath = new Map(ref.parts.map((p) => [p.path, p]));
|
|
264
|
+
const signatures = [];
|
|
265
|
+
for (const rel of entry.rels) {
|
|
266
|
+
const kind = vbaProjectSignatureKind(rel.type);
|
|
267
|
+
const part = kind === undefined ? undefined : partByPath.get(rel.targetPath);
|
|
268
|
+
if (kind !== undefined && part !== undefined) {
|
|
269
|
+
signatures.push({ kind, bytes: part.bytes.slice() });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return signatures;
|
|
273
|
+
}
|
|
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 {@link VbaAuthorError} if the workbook has no macro project, or `name` is not in the project,
|
|
284
|
+
* or names a `document`/`designer` module.
|
|
285
|
+
* @throws {@link VbaParseError} if the attached `vbaProject.bin` is malformed.
|
|
286
|
+
*/
|
|
287
|
+
removeVbaModule(name) {
|
|
288
|
+
const bytes = this.vbaProjectBytes;
|
|
289
|
+
if (bytes === undefined) {
|
|
290
|
+
throw new VbaAuthorError('workbook has no VBA project to remove a module from');
|
|
291
|
+
}
|
|
292
|
+
this.vbaProjectBytes = removeVbaModule(bytes, name);
|
|
293
|
+
}
|
|
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 {@link VbaAuthorError} if the workbook has no macro project, or any field of `ref` is invalid
|
|
301
|
+
* (see {@link VbaLibraryReference}).
|
|
302
|
+
* @throws {@link VbaParseError} if the attached `vbaProject.bin` is malformed.
|
|
303
|
+
*/
|
|
304
|
+
addVbaReference(ref) {
|
|
305
|
+
const bytes = this.vbaProjectBytes;
|
|
306
|
+
if (bytes === undefined) {
|
|
307
|
+
throw new VbaAuthorError('workbook has no VBA project to add a reference to');
|
|
308
|
+
}
|
|
309
|
+
this.vbaProjectBytes = addVbaReference(bytes, ref);
|
|
310
|
+
}
|
|
311
|
+
#vbaProjectRef() {
|
|
312
|
+
return this.#preservedReferences.find((r) => r.relType.endsWith('/vbaProject'));
|
|
313
|
+
}
|
|
314
|
+
#vbaProjectEntry() {
|
|
315
|
+
const ref = this.#vbaProjectRef();
|
|
316
|
+
return ref?.parts.find((p) => p.path === ref.entryPath);
|
|
317
|
+
}
|
|
318
|
+
/** The preserved differential-style (`<dxfs>`) fragments, in index order. */
|
|
319
|
+
get differentialStyles() {
|
|
320
|
+
return this.#dxfs;
|
|
321
|
+
}
|
|
322
|
+
/** The preserved custom indexed-color palette, in index order; empty when the default palette rules. */
|
|
323
|
+
get indexedColors() {
|
|
324
|
+
return this.#indexedColors;
|
|
325
|
+
}
|
|
326
|
+
/** The preserved most-recently-used colour swatches, in order; empty when the file declared none. */
|
|
327
|
+
get mruColors() {
|
|
328
|
+
return this.#mruColors;
|
|
329
|
+
}
|
|
330
|
+
/** The preserved `<tableStyles>` block; `styles` is empty when the file declared no custom style. */
|
|
331
|
+
get tableStyles() {
|
|
332
|
+
return this.#tableStyles;
|
|
333
|
+
}
|
|
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
|
+
#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 {@link 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
|
+
addTableStyle(style) {
|
|
365
|
+
checkTableStyle(style);
|
|
366
|
+
this.#customTableStyles.set(style.name, style);
|
|
367
|
+
}
|
|
368
|
+
/** The table styles authored on this workbook, in registration order. */
|
|
369
|
+
get customTableStyles() {
|
|
370
|
+
return [...this.#customTableStyles.values()];
|
|
371
|
+
}
|
|
372
|
+
/** The preserved theme part, or undefined when the workbook rides the library's default theme. */
|
|
373
|
+
get themePart() {
|
|
374
|
+
return this.#theme;
|
|
375
|
+
}
|
|
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
|
+
#themeColors;
|
|
380
|
+
// Colour slots and typefaces the caller authored, merged over whatever the workbook already had.
|
|
381
|
+
#authoredTheme = {
|
|
382
|
+
colors: {},
|
|
383
|
+
fonts: {},
|
|
384
|
+
};
|
|
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 {@link AuthoringError} if a colour is not 6 or 8 hexadecimal digits.
|
|
404
|
+
*/
|
|
405
|
+
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
|
+
applyThemeOverrides(this.#baseThemeXml(), overrides);
|
|
409
|
+
Object.assign(this.#authoredTheme.colors, overrides.colors ?? {});
|
|
410
|
+
this.#authoredTheme.fonts = { ...this.#authoredTheme.fonts, ...(overrides.fonts ?? {}) };
|
|
411
|
+
this.#themeColors = undefined;
|
|
412
|
+
}
|
|
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
|
+
get themeColors() {
|
|
421
|
+
if (this.#themeColors === undefined) {
|
|
422
|
+
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
|
+
const parsed = xml === undefined ? {} : parseThemeColorScheme(xml);
|
|
427
|
+
const base = Object.keys(parsed).length === 0 ? DEFAULT_THEME_COLOR_SCHEME : parsed;
|
|
428
|
+
this.#themeColors = { ...base, ...this.#authoredTheme.colors };
|
|
429
|
+
}
|
|
430
|
+
return this.#themeColors;
|
|
431
|
+
}
|
|
432
|
+
/** The theme's major (heading) and minor (body) typefaces, authored values over the source's. */
|
|
433
|
+
get themeFonts() {
|
|
434
|
+
const xml = this.#themeXml();
|
|
435
|
+
const parsed = xml === undefined ? {} : parseThemeFontScheme(xml);
|
|
436
|
+
const base = Object.keys(parsed).length === 0 ? DEFAULT_THEME_FONTS : parsed;
|
|
437
|
+
return { ...base, ...this.#authoredTheme.fonts };
|
|
438
|
+
}
|
|
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
|
+
#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
|
+
#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
|
+
get declaredDefaultFont() {
|
|
457
|
+
return this.#declaredDefaultFont;
|
|
458
|
+
}
|
|
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 {@link 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
|
+
setDefaultFont(font) {
|
|
475
|
+
if (font.size !== undefined && !(Number.isFinite(font.size) && font.size > 0)) {
|
|
476
|
+
throw new AuthoringError(`default font size must be a positive number, not ${font.size}`);
|
|
477
|
+
}
|
|
478
|
+
if (font.name !== undefined && font.name === '') {
|
|
479
|
+
throw new AuthoringError('default font name cannot be empty');
|
|
480
|
+
}
|
|
481
|
+
this.#authoredDefaultFont = { ...this.#authoredDefaultFont, ...font };
|
|
482
|
+
}
|
|
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
|
+
get defaultFont() {
|
|
505
|
+
const declared = this.#declaredDefaultFont;
|
|
506
|
+
const authored = this.#authoredDefaultFont;
|
|
507
|
+
const authoredFace = this.#authoredTheme.fonts.minor;
|
|
508
|
+
if (declared !== undefined && authored === undefined && authoredFace === undefined) {
|
|
509
|
+
return declared;
|
|
510
|
+
}
|
|
511
|
+
const bodyFace = this.themeFonts.minor ?? OFFICE_BODY_FACE;
|
|
512
|
+
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
|
+
const font = {
|
|
516
|
+
size: 11,
|
|
517
|
+
color: { theme: 1 },
|
|
518
|
+
...declared,
|
|
519
|
+
...authored,
|
|
520
|
+
name: face,
|
|
521
|
+
};
|
|
522
|
+
const followsTheme = face === bodyFace;
|
|
523
|
+
if (authored?.family === undefined) {
|
|
524
|
+
if (followsTheme)
|
|
525
|
+
font.family = 2;
|
|
526
|
+
else
|
|
527
|
+
delete font.family;
|
|
528
|
+
}
|
|
529
|
+
if (authored?.scheme === undefined) {
|
|
530
|
+
if (followsTheme)
|
|
531
|
+
font.scheme = 'minor';
|
|
532
|
+
else
|
|
533
|
+
delete font.scheme;
|
|
534
|
+
}
|
|
535
|
+
return font;
|
|
536
|
+
}
|
|
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
|
+
authoredThemeXml() {
|
|
546
|
+
const { colors, fonts } = this.#authoredTheme;
|
|
547
|
+
if (Object.keys(colors).length === 0 && Object.keys(fonts).length === 0)
|
|
548
|
+
return undefined;
|
|
549
|
+
return applyThemeOverrides(this.#baseThemeXml(), { colors, fonts });
|
|
550
|
+
}
|
|
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
|
+
#baseThemeXml() {
|
|
554
|
+
return this.#themeXml() ?? DEFAULT_THEME_XML;
|
|
555
|
+
}
|
|
556
|
+
// The preserved theme part's text, decoded from the entry part of its closure.
|
|
557
|
+
#themeXml() {
|
|
558
|
+
const theme = this.#theme;
|
|
559
|
+
if (theme === undefined)
|
|
560
|
+
return undefined;
|
|
561
|
+
const entry = theme.parts.find((part) => part.path === theme.entryPath);
|
|
562
|
+
return entry === undefined ? undefined : new TextDecoder().decode(entry.bytes);
|
|
563
|
+
}
|
|
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
|
+
resolveColor(color) {
|
|
579
|
+
return resolveColor(color, { theme: this.themeColors, indexed: this.#indexedPalette() });
|
|
580
|
+
}
|
|
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
|
+
#indexedPalette() {
|
|
585
|
+
return this.#indexedColors.map((fragment) => /\brgb="([^"]*)"/.exec(fragment)?.[1] ?? '');
|
|
586
|
+
}
|
|
587
|
+
/** The named cell styles, in index order (index 0 is Normal); empty when only the default exists. */
|
|
588
|
+
get namedStyles() {
|
|
589
|
+
return this.#namedStyles;
|
|
590
|
+
}
|
|
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
|
+
addPerson(person) {
|
|
603
|
+
const id = commentThreadGuid(person.id, 'a person id');
|
|
604
|
+
this.#persons.set(id, { ...person, id });
|
|
605
|
+
}
|
|
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
|
+
get persons() {
|
|
611
|
+
return [...this.#persons.values()];
|
|
612
|
+
}
|
|
613
|
+
/** Look up a registered identity by its {@link Person.id}, or `undefined` if the registry has none. */
|
|
614
|
+
getPerson(id) {
|
|
615
|
+
return this.#persons.get(id);
|
|
616
|
+
}
|
|
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
|
+
addImage(options) {
|
|
623
|
+
this.#media.push({
|
|
624
|
+
extension: normalizeImageExtension(options.extension, options.buffer),
|
|
625
|
+
data: options.buffer,
|
|
626
|
+
});
|
|
627
|
+
return this.#media.length - 1;
|
|
628
|
+
}
|
|
629
|
+
/** The registered images, indexed by the id {@link addImage} returned. */
|
|
630
|
+
get media() {
|
|
631
|
+
return this.#media;
|
|
632
|
+
}
|
|
633
|
+
/** Look up a registered image by its id, or `undefined` if no image carries that id. */
|
|
634
|
+
getImage(id) {
|
|
635
|
+
return this.#media[id];
|
|
636
|
+
}
|
|
637
|
+
/** The workbook's defined names, in the order they were registered. */
|
|
638
|
+
get definedNames() {
|
|
639
|
+
return this.#definedNames;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Register a defined name on the workbook.
|
|
643
|
+
*
|
|
644
|
+
* @throws {@link 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
|
+
defineName(definedName) {
|
|
648
|
+
if (definedName.name.length === 0) {
|
|
649
|
+
throw new AuthoringError('a defined name cannot be empty');
|
|
650
|
+
}
|
|
651
|
+
if (definedName.scope !== undefined && this.getWorksheet(definedName.scope) === undefined) {
|
|
652
|
+
throw new AuthoringError(`defined name "${definedName.name}" is scoped to unknown worksheet "${definedName.scope}"`);
|
|
653
|
+
}
|
|
654
|
+
this.#definedNames.push(definedName);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Create a worksheet and append it to the workbook.
|
|
658
|
+
*
|
|
659
|
+
* @throws {@link AuthoringError} if the name is empty, too long, contains a forbidden character,
|
|
660
|
+
* or collides (case-insensitively) with an existing sheet.
|
|
661
|
+
*/
|
|
662
|
+
addWorksheet(name, options = {}) {
|
|
663
|
+
this.#assertValidSheetName(name);
|
|
664
|
+
const sheet = new Worksheet(name, this.#nextSheetId++, options.state ?? 'visible');
|
|
665
|
+
this.#worksheets.push(sheet);
|
|
666
|
+
return sheet;
|
|
667
|
+
}
|
|
668
|
+
/** Look up a worksheet by name (case-insensitive) or by numeric id. */
|
|
669
|
+
getWorksheet(nameOrId) {
|
|
670
|
+
if (typeof nameOrId === 'number') {
|
|
671
|
+
return this.#worksheets.find((sheet) => sheet.id === nameOrId);
|
|
672
|
+
}
|
|
673
|
+
const target = nameOrId.toLowerCase();
|
|
674
|
+
return this.#worksheets.find((sheet) => sheet.name.toLowerCase() === target);
|
|
675
|
+
}
|
|
676
|
+
#assertValidSheetName(name) {
|
|
677
|
+
if (name.length === 0) {
|
|
678
|
+
throw new AuthoringError('worksheet name cannot be empty');
|
|
679
|
+
}
|
|
680
|
+
if (name.length > MAX_SHEET_NAME_LENGTH) {
|
|
681
|
+
throw new AuthoringError(`worksheet name "${name}" exceeds the ${MAX_SHEET_NAME_LENGTH}-character limit`);
|
|
682
|
+
}
|
|
683
|
+
if (INVALID_SHEET_NAME_CHARS.test(name)) {
|
|
684
|
+
throw new AuthoringError(`worksheet name "${name}" contains a character Excel forbids (* ? : \\ / [ ])`);
|
|
685
|
+
}
|
|
686
|
+
if (name.startsWith("'") || name.endsWith("'")) {
|
|
687
|
+
throw new AuthoringError(`worksheet name "${name}" cannot start or end with an apostrophe`);
|
|
688
|
+
}
|
|
689
|
+
if (this.getWorksheet(name) !== undefined) {
|
|
690
|
+
throw new AuthoringError(`a worksheet named "${name}" already exists (names are case-insensitive)`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
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
|
+
[INTERNAL] = {
|
|
698
|
+
addPreservedReference: (reference) => {
|
|
699
|
+
this.#preservedReferences.push(reference);
|
|
700
|
+
},
|
|
701
|
+
addPreservedRootReference: (reference) => {
|
|
702
|
+
this.#preservedRootReferences.push(reference);
|
|
703
|
+
},
|
|
704
|
+
restoreDifferentialStyles: (fragments) => {
|
|
705
|
+
replaceContents(this.#dxfs, fragments);
|
|
706
|
+
},
|
|
707
|
+
restoreIndexedColors: (fragments) => {
|
|
708
|
+
replaceContents(this.#indexedColors, fragments);
|
|
709
|
+
},
|
|
710
|
+
restoreMruColors: (fragments) => {
|
|
711
|
+
replaceContents(this.#mruColors, fragments);
|
|
712
|
+
},
|
|
713
|
+
restoreTableStyles: (table) => {
|
|
714
|
+
this.#tableStyles = table;
|
|
715
|
+
},
|
|
716
|
+
restoreThemePart: (theme) => {
|
|
717
|
+
this.#theme = theme;
|
|
718
|
+
this.#themeColors = undefined;
|
|
719
|
+
},
|
|
720
|
+
restoreNamedStyles: (styles) => {
|
|
721
|
+
replaceContents(this.#namedStyles, styles);
|
|
722
|
+
},
|
|
723
|
+
restoreDefaultFont: (font) => {
|
|
724
|
+
this.#declaredDefaultFont = font;
|
|
725
|
+
},
|
|
726
|
+
restorePersons: (persons) => {
|
|
727
|
+
this.#persons.clear();
|
|
728
|
+
for (const person of persons)
|
|
729
|
+
this.#persons.set(person.id, person);
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|