@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.
Files changed (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. 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
+ }