@shbernal/ts-xlsx 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,44 +1,16 @@
1
- // The write-side style table: the interned `<numFmts>`/`<fills>`/`<cellXfs>` backing styles.xml.
2
- //
3
- // OOXML styles are a *shared* table referenced by index: a cell (or a formatted row/column)
4
- // names a `<cellXfs>` entry via its `s` attribute, and that entry names a fill by id and a
5
- // number format by id. Identical styles must collapse to one entry — both to produce
6
- // well-formed OOXML and to keep write cost bounded on large, lightly-formatted sheets (the
7
- // historical performance cliff came from re-serialising a distinct style per cell). The
8
- // registry interns each distinct fill, number format, and xf, handing back a stable index.
9
- //
10
- // Fills, number formats, fonts, borders, alignment, and protection are modelled today.
11
- // Fills/fonts/borders are shared sub-tables the xf names by id, whereas alignment and protection
12
- // are child elements *of* the xf — so each is interned into the xf signature directly rather than
13
- // into its own id table, and an aligned/protected xf carries them as body children in that order.
14
- // An unstyled cell/row/column resolves to xf 0.
15
1
  import { isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isHorizontalAlignment, isNamedUnderlineStyle, isVerticalAlignment, } from '../../core/style.js';
16
2
  import { TABLE_STYLE_ELEMENT_TYPES } from '../../core/table-style.js';
17
3
  import { AuthoringError } from '../../errors.js';
18
4
  import { escapeAttr, XML_DECLARATION } from '../../xml/xml.js';
19
- import { decodeEntities, openElements } from '../../xml/xml-read.js';
5
+ import { decodeEntities } from '../../xml/xml-read.js';
6
+ import { colorAttrs } from './color-xml.js';
20
7
  import { MARKUP_COMPATIBILITY_NS, SPREADSHEETML_NS } from './namespaces.js';
21
- // Excel reserves fill ids 0 and 1 for the "none" and "gray125" patterns it always emits;
22
- // custom fills are numbered from 2 so a foreign reader's built-in assumptions still hold.
23
8
  const RESERVED_FILL_COUNT = 2;
24
- // Font id 0 is the always-present default font — the workbook's, not an assumed one; custom fonts
25
- // are numbered from 1.
26
9
  const RESERVED_FONT_COUNT = 1;
27
- // numFmt ids below 164 are reserved by ECMA-376 for the built-in formats every consumer
28
- // knows implicitly; custom format codes are numbered from 164 up. Id 0 is General (no code).
29
10
  const CUSTOM_NUMFMT_BASE = 164;
30
- // Border id 0 is the always-present empty border (every edge absent); custom borders from 1.
31
11
  const RESERVED_BORDER_COUNT = 1;
32
- // The Office default font's inner fragment, in the exact child order `fontXml` emits — the font 0 a
33
- // registry built without a workbook falls back to. A registry built *with* one derives font 0 from
34
- // its `defaultFont` instead, and for a plain `new Workbook()` that derivation lands on exactly this
35
- // string; `styles.test.ts` guards the two against drifting.
36
12
  const DEFAULT_FONT_BODY = '<sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/>';
37
- // The empty border: all five edges present but styleless. A border that overrides no edge
38
- // serialises to exactly this, so it interns to the default border id 0 rather than a new one.
39
13
  const DEFAULT_BORDER = '<border><left/><right/><top/><bottom/><diagonal/></border>';
40
- // The default xf: no facet, General format, linked to the Normal named style (xfId 0). Shared as the
41
- // first entry of both the cell-format and named-style tables; never mutated (formats only append).
42
14
  const DEFAULT_FORMAT = {
43
15
  fillId: 0,
44
16
  numFmtId: 0,
@@ -50,10 +22,7 @@ const DEFAULT_FORMAT = {
50
22
  xfId: 0,
51
23
  };
52
24
  export class StyleRegistry {
53
- // The `<font>` body emitted as id 0.
54
25
  #defaultFontBody;
55
- // Every serialised font body that means "id 0" — the emitted default, plus the one a source file
56
- // declared. See {@link StyleRegistryOptions.declaredDefaultFont} for why the second belongs here.
57
26
  #font0Bodies;
58
27
  constructor(options = {}) {
59
28
  this.#defaultFontBody =
@@ -62,62 +31,28 @@ export class StyleRegistry {
62
31
  ? [this.#defaultFontBody]
63
32
  : [this.#defaultFontBody, fontXml(options.declaredDefaultFont)]);
64
33
  }
65
- // Custom fill xml fragments, in id order; the emitted id is RESERVED_FILL_COUNT + index.
66
34
  #fillXml = [];
67
35
  #fillIdBySignature = new Map();
68
- // Custom number-format codes, in id order; the emitted id is CUSTOM_NUMFMT_BASE + index.
69
36
  #numFmtCodes = [];
70
37
  #numFmtIdByCode = new Map();
71
- // Custom font xml fragments, in id order; the emitted id is RESERVED_FONT_COUNT + index.
72
38
  #fontXml = [];
73
39
  #fontIdBySignature = new Map();
74
- // Custom border xml fragments, in id order; the emitted id is RESERVED_BORDER_COUNT + index.
75
40
  #borderXml = [];
76
41
  #borderIdBySignature = new Map();
77
- // xf 0 is the default (no fill/font/border/alignment/protection, General format); further entries append as styles appear.
78
42
  #formats = [DEFAULT_FORMAT];
79
43
  #xfIndexBySignature = new Map();
80
- // The named-style layer (`<cellStyleXfs>` / `<cellStyles>`): the base formats a cell's `xfId` links
81
- // into, and the names that label them. Index 0 is always Normal. A file with named styles seeds this
82
- // in place of the default via {@link seedNamedStyles}; otherwise the default alone is emitted.
83
44
  #cellStyleXfs = [DEFAULT_FORMAT];
84
45
  #cellStyleNames = [
85
46
  { name: 'Normal', builtinId: 0, xfId: 0 },
86
47
  ];
87
- // Differential styles (`<dxfs>`) that conditional formatting references by index. Fragments read
88
- // from a file are seeded first and kept verbatim so a foreign rule's dxfId stays valid; a style
89
- // authored on a rule is serialised and appended after them, dedup'd by its fragment.
90
48
  #dxfXml = [];
91
49
  #dxfIndexByFragment = new Map();
92
- // A custom indexed-color palette (`<colors><indexedColors>`) read from a file, each entry a verbatim
93
- // `<rgbColor rgb="…"/>`. Preserved and re-emitted unchanged so cells/fonts/borders that reference a
94
- // colour by `indexed="…"` keep their intended RGB; dropping it would silently resolve every indexed
95
- // colour to a different default-palette entry. Empty for a workbook that never overrode the palette.
96
50
  #indexedColors = [];
97
- // The most-recently-used colour swatches (`<colors><mruColors>`) read from a file, each entry a
98
- // verbatim `<color rgb="…"/>` fragment. Purely a UI convenience — the palette Excel offers under
99
- // "Recent Colors" — but it is the author's own working set, so dropping it on a re-write quietly
100
- // resets a habit. Empty for a workbook that never picked a custom colour.
101
51
  #mruColors = [];
102
- // The custom table/pivot style definitions (`<tableStyles>`) read from a file, kept verbatim, plus
103
- // the gallery names it nominates as defaults. A table's `tableStyleInfo/@name` can point at one of
104
- // these definitions, so dropping the block leaves that reference dangling and the table renders
105
- // unstyled. Excel writes the container (with both default attributes) into essentially every file
106
- // even when it declares no custom style at all.
107
52
  #tableStyles = { styles: [] };
108
- // Table styles authored on the workbook, serialised on registration and keyed by name so a second
109
- // definition of the same name replaces the first — as does one that overrides a preserved
110
- // definition, since two `<tableStyle>` elements sharing a name leave a table's reference ambiguous.
111
53
  #authoredTableStyles = new Map();
112
- /**
113
- * The `<cellXfs>` index for a composed cell/row/column style. A style with no facet needs
114
- * no entry and resolves to the default xf 0, so its owner emits no `s` attribute at all.
115
- */
116
54
  styleId(style) {
117
55
  const format = this.#composeFormat(style, style.xfId ?? 0);
118
- // An all-default format that links to no named style needs no entry and resolves to xf 0, so its
119
- // owner emits no `s` attribute. A non-zero xfId is itself information — the cell inherits a named
120
- // style — so it forces a real entry even when the direct facets are empty.
121
56
  if (isDefaultFormat(format))
122
57
  return 0;
123
58
  const signature = formatSignature(format);
@@ -129,17 +64,9 @@ export class StyleRegistry {
129
64
  }
130
65
  return index;
131
66
  }
132
- // Compose a style's facets into an interned {@link CellFormat}, interning each fill/font/border/
133
- // number-format into its shared sub-table. Shared by the cell-format path ({@link styleId}) and the
134
- // named-style path ({@link seedNamedStyles}), which differ only in which table the result lands in.
135
67
  #composeFormat(style, xfId) {
136
- // A `none` pattern is the reserved fill 0; a gradient is always a real, interned fill.
137
68
  const paints = style.fill !== undefined && (style.fill.type === 'gradient' || style.fill.pattern !== 'none');
138
69
  const fillId = paints ? this.#internFill(style.fill) : 0;
139
- // A number format is a format-code *string*; a caller that assigns a structured object (e.g. a
140
- // parsed `{id, formatCode}` copied from another cell) must not have it stringified into the styles
141
- // part as `formatCode="[object Object]"`, which Excel reports as a corrupt package. A non-string
142
- // format is dropped to the General format rather than corrupting the file.
143
70
  const numFmtId = typeof style.numFmt === 'string' && style.numFmt !== ''
144
71
  ? this.#internNumFmt(style.numFmt)
145
72
  : 0;
@@ -150,12 +77,6 @@ export class StyleRegistry {
150
77
  const quotePrefix = style.quotePrefix === true;
151
78
  return { fillId, numFmtId, fontId, borderId, alignment, protection, quotePrefix, xfId };
152
79
  }
153
- /**
154
- * Seed the named cell styles (`<cellStyleXfs>`/`<cellStyles>`) read from a file, in place of the
155
- * lone default, interning each style's facets into the shared sub-tables so its `fillId`/`fontId`/…
156
- * references stay valid against the rebuilt tables. Index 0 stays Normal. A cell's `xfId` indexes
157
- * this table, so it must be seeded before any {@link styleId} that carries an `xfId`.
158
- */
159
80
  seedNamedStyles(styles) {
160
81
  if (styles.length === 0)
161
82
  return;
@@ -172,90 +93,39 @@ export class StyleRegistry {
172
93
  this.#cellStyleNames.push(entry);
173
94
  });
174
95
  }
175
- /**
176
- * Seed the differential-style table with fragments read from a file, keeping each `<dxf>…</dxf>`
177
- * verbatim and at its original index. Call once before any {@link differentialStyleId}; authored
178
- * styles append after these.
179
- *
180
- * **Index stability is a contract, not an implementation detail.** A `dxfId` is an index into this
181
- * one table, and more than one preserved construct resolves through it: a conditional-formatting
182
- * rule's `dxfId`, and every `<tableStyleElement dxfId="…">` inside a preserved `<tableStyle>` (see
183
- * {@link seedTableStyles}). Those constructs are carried as opaque XML precisely *because* the
184
- * indices they name do not move. Renumbering, reordering, or de-duplicating the seeded entries
185
- * would silently re-point every one of them at a different format — a change no schema check and no
186
- * round-trip of our own can catch, because the file stays perfectly valid and merely renders wrong.
187
- */
188
96
  seedDifferentialStyles(fragments) {
189
97
  for (const fragment of fragments) {
190
98
  const index = this.#dxfXml.length;
191
99
  this.#dxfXml.push(fragment);
192
- // A seeded fragment can still be reused by an authored style identical to it, so index it too.
193
100
  if (!this.#dxfIndexByFragment.has(fragment))
194
101
  this.#dxfIndexByFragment.set(fragment, index);
195
102
  }
196
103
  }
197
- /**
198
- * Seed the custom indexed-color palette (`<colors><indexedColors>`) read from a file, each entry a
199
- * verbatim `<rgbColor rgb="…"/>` fragment. Re-emitting it unchanged is what keeps an `indexed="…"`
200
- * colour reference resolving to the RGB the source intended. An empty list leaves the workbook on
201
- * the default palette and emits no `<colors>` element.
202
- */
203
104
  seedIndexedColors(fragments) {
204
105
  this.#indexedColors.length = 0;
205
106
  this.#indexedColors.push(...fragments);
206
107
  }
207
- /**
208
- * Seed the most-recently-used colour swatches (`<colors><mruColors>`) read from a file, each entry a
209
- * verbatim `<color rgb="…"/>` fragment. An empty list emits no `<mruColors>` element.
210
- */
211
108
  seedMruColors(fragments) {
212
109
  this.#mruColors.length = 0;
213
110
  this.#mruColors.push(...fragments);
214
111
  }
215
- /**
216
- * Seed the custom table-style definitions (`<tableStyles>`) read from a file, each `<tableStyle>`
217
- * kept verbatim so a table's `tableStyleInfo/@name` still resolves and each element's `dxfId` still
218
- * indexes the differential-style table {@link seedDifferentialStyles} preserves at its original
219
- * indices. Replaces any block already held.
220
- */
221
112
  seedTableStyles(table) {
222
113
  this.#tableStyles = table;
223
114
  }
224
- /**
225
- * Serialise a table style authored on the workbook, interning each element's formatting into the
226
- * differential-style table and emitting the `dxfId` that reaches it. Call after
227
- * {@link seedTableStyles}, whose preserved definitions these append after.
228
- *
229
- * A definition here **replaces** a preserved one of the same name. Two `<tableStyle>` elements
230
- * sharing a name is ambiguous — a table's `tableStyleInfo/@name` would reach whichever a consumer
231
- * happened to index first — so authoring a name the source already used is read as overriding it,
232
- * which is what asking for it means.
233
- */
234
115
  addTableStyle(style) {
235
116
  const elements = TABLE_STYLE_ELEMENT_TYPES.flatMap((type) => {
236
117
  const element = style.elements[type];
237
118
  if (element === undefined)
238
119
  return [];
239
- // `size` defaults to 1, so it is written only when a band is genuinely wider than one row.
240
120
  const size = element.size !== undefined && element.size !== 1 ? ` size="${element.size}"` : '';
241
121
  return [
242
122
  `<tableStyleElement type="${type}"${size} dxfId="${this.differentialStyleId(element)}"/>`,
243
123
  ];
244
124
  });
245
- // `pivot`/`table` default to true, so each is written only when the caller opts a style out.
246
125
  const flags = (style.pivot === false ? ' pivot="0"' : '') + (style.table === false ? ' table="0"' : '');
247
126
  this.#authoredTableStyles.set(style.name, `<tableStyle name="${escapeAttr(style.name)}"${flags} count="${elements.length}">` +
248
127
  `${elements.join('')}</tableStyle>`);
249
128
  }
250
- /**
251
- * Intern an authored differential style, returning the `<dxfs>` index that references it — a
252
- * conditional-formatting rule's `dxfId`, or a table style element's. Identical styles collapse to
253
- * one entry, whichever feature asked for them, so a highlight rule and a table style's header row
254
- * painted the same way share a single `<dxf>`.
255
- *
256
- * Authored entries append after the seeded ones ({@link seedDifferentialStyles}), which is what
257
- * keeps every preserved reference pointing where it did.
258
- */
259
129
  differentialStyleId(style) {
260
130
  const fragment = dxfXml(style);
261
131
  let index = this.#dxfIndexByFragment.get(fragment);
@@ -285,8 +155,6 @@ export class StyleRegistry {
285
155
  }
286
156
  return id;
287
157
  }
288
- // A font whose partial carries no facet that differs from the default contributes nothing
289
- // and maps to font id 0; otherwise its serialised form is interned and dedup'd like a fill.
290
158
  #internFont(font) {
291
159
  const xml = fontXml(font);
292
160
  if (xml === '' || this.#font0Bodies.has(xml))
@@ -299,8 +167,6 @@ export class StyleRegistry {
299
167
  }
300
168
  return id;
301
169
  }
302
- // A border that overrides no edge serialises to the empty default border and maps to id 0;
303
- // otherwise its serialised form is interned and dedup'd like a fill or font.
304
170
  #internBorder(border) {
305
171
  const xml = borderXml(border);
306
172
  if (xml === DEFAULT_BORDER)
@@ -313,7 +179,6 @@ export class StyleRegistry {
313
179
  }
314
180
  return id;
315
181
  }
316
- /** Serialise the accumulated table into a complete, valid styles.xml part. */
317
182
  toXml() {
318
183
  const fillCount = RESERVED_FILL_COUNT + this.#fillXml.length;
319
184
  const fills = '<fill><patternFill patternType="none"/></fill>' +
@@ -340,13 +205,6 @@ export class StyleRegistry {
340
205
  this.#colorsXml() +
341
206
  '</styleSheet>');
342
207
  }
343
- // The `xmlns:…` declarations a preserved `<tableStyle>` fragment depends on, plus the
344
- // markup-compatibility attributes that tell a consumer to ignore what it does not understand. A
345
- // workbook carrying no such fragment emits nothing, so the ordinary stylesheet root is unchanged.
346
- //
347
- // This is the cost of verbatim preservation: a fragment carries its prefixes with it, and a prefix
348
- // no ancestor declares makes the whole part unparseable — a much louder failure than the dropped
349
- // table style the preservation exists to prevent. See {@link TableStyleTable.namespaces}.
350
208
  #foreignNamespaceAttrs() {
351
209
  const namespaces = this.#tableStyles.namespaces ?? [];
352
210
  if (namespaces.length === 0)
@@ -358,14 +216,6 @@ export class StyleRegistry {
358
216
  : ` xmlns:mc="${MARKUP_COMPATIBILITY_NS}" mc:Ignorable="${escapeAttr(ignorable.join(' '))}"`;
359
217
  return declarations.join('') + compatibility;
360
218
  }
361
- // <tableStyles> sits between <dxfs> and <colors> in CT_Stylesheet's child sequence. It is emitted
362
- // only when the workbook has something to say there — a preserved or authored style definition, or
363
- // a nominated default — so a workbook that authors none leaves every table on the built-in gallery
364
- // and writes nothing. `count` counts the definitions, not the attributes, so a container that only
365
- // nominates defaults (the shape Excel writes into nearly every file) is self-closing with count="0".
366
- //
367
- // Preserved definitions come first and authored ones after, except that an authored style replaces
368
- // the preserved definition it shares a name with — see {@link addTableStyle}.
369
219
  #tableStylesXml() {
370
220
  const { defaultTableStyle, defaultPivotStyle } = this.#tableStyles;
371
221
  const authored = this.#authoredTableStyles;
@@ -385,10 +235,6 @@ export class StyleRegistry {
385
235
  return `<tableStyles${attrs}/>`;
386
236
  return `<tableStyles${attrs}>${styles.join('')}</tableStyles>`;
387
237
  }
388
- // <colors> is the last modelled child of <styleSheet>, after <dxfs> and <tableStyles>. It holds the
389
- // custom <indexedColors> palette and then the <mruColors> swatch list, in that CT_Colors order. It
390
- // is emitted only when a file carried one of them, so an ordinary workbook stays on the built-in
391
- // indexed colours and writes no <colors> element.
392
238
  #colorsXml() {
393
239
  const indexed = this.#indexedColors.length === 0
394
240
  ? ''
@@ -398,16 +244,11 @@ export class StyleRegistry {
398
244
  return '';
399
245
  return `<colors>${indexed}${mru}</colors>`;
400
246
  }
401
- // <dxfs> holds the differential styles conditional formatting references by index. An empty table
402
- // is still emitted as a self-closing count="0" element, the shape Excel writes; a populated one
403
- // lists the seeded (foreign) fragments first, then any authored styles, preserving every index.
404
247
  #dxfsXml() {
405
248
  if (this.#dxfXml.length === 0)
406
249
  return '<dxfs count="0"/>';
407
250
  return `<dxfs count="${this.#dxfXml.length}">${this.#dxfXml.join('')}</dxfs>`;
408
251
  }
409
- // <numFmts> is the first child of <styleSheet> and is omitted entirely when no custom
410
- // format was used, matching how Excel writes an all-built-in workbook.
411
252
  #numFmtsXml() {
412
253
  if (this.#numFmtCodes.length === 0)
413
254
  return '';
@@ -417,8 +258,6 @@ export class StyleRegistry {
417
258
  return `<numFmts count="${this.#numFmtCodes.length}">${entries}</numFmts>`;
418
259
  }
419
260
  }
420
- // Whether a format is the do-nothing default: no facet, General number format, no quote prefix, and
421
- // linked to the Normal named style. Such a cellXfs entry adds nothing, so its owner needs no `s`.
422
261
  function isDefaultFormat(format) {
423
262
  return (format.fillId === 0 &&
424
263
  format.numFmtId === 0 &&
@@ -429,13 +268,10 @@ function isDefaultFormat(format) {
429
268
  !format.quotePrefix &&
430
269
  format.xfId === 0);
431
270
  }
432
- // A stable, collision-free key for a composed format so identical formats intern to one cellXfs entry.
433
271
  function formatSignature(format) {
434
272
  return (`fill:${format.fillId}|numFmt:${format.numFmtId}|font:${format.fontId}|border:${format.borderId}|` +
435
273
  `align:${format.alignment}|protect:${format.protection}|quote:${format.quotePrefix}|xfId:${format.xfId}`);
436
274
  }
437
- // Serialise one `<xf>`. A cellXfs entry passes its named-style link as `xfId`; a cellStyleXfs entry
438
- // (the base a cell links *to*) passes `null` so the attribute is omitted, since it nests no further.
439
275
  function xfXml(format, xfId) {
440
276
  const applyNumberFormat = format.numFmtId !== 0 ? ' applyNumberFormat="1"' : '';
441
277
  const applyFont = format.fontId !== 0 ? ' applyFont="1"' : '';
@@ -443,43 +279,27 @@ function xfXml(format, xfId) {
443
279
  const applyBorder = format.borderId !== 0 ? ' applyBorder="1"' : '';
444
280
  const applyAlignment = format.alignment !== '' ? ' applyAlignment="1"' : '';
445
281
  const applyProtection = format.protection !== '' ? ' applyProtection="1"' : '';
446
- // `quotePrefix` is a CT_Xf attribute (after xfId, before the apply flags in schema order); it is
447
- // its own switch — there is no `applyQuotePrefix` flag — so it is emitted only when set.
448
282
  const quotePrefix = format.quotePrefix ? ' quotePrefix="1"' : '';
449
283
  const xfIdAttr = xfId === null ? '' : ` xfId="${xfId}"`;
450
284
  const open = `<xf numFmtId="${format.numFmtId}" fontId="${format.fontId}" fillId="${format.fillId}" ` +
451
285
  `borderId="${format.borderId}"${xfIdAttr}${quotePrefix}` +
452
286
  `${applyNumberFormat}${applyFont}${applyFill}${applyBorder}${applyAlignment}${applyProtection}`;
453
- // Alignment and protection are child elements of the xf, in that schema order; an xf carrying
454
- // either (or both) is not self-closing, while a plain one stays self-closing as before.
455
287
  const body = (format.alignment === '' ? '' : `<alignment ${format.alignment}/>`) +
456
288
  (format.protection === '' ? '' : `<protection ${format.protection}/>`);
457
289
  return body === '' ? `${open}/>` : `${open}>${body}</xf>`;
458
290
  }
459
- // One `<cellStyle>` entry mapping a name (and, for a built-in, its gallery id) to a cellStyleXfs index.
460
291
  function cellStyleTag(entry) {
461
292
  const builtin = entry.builtinId === undefined ? '' : ` builtinId="${entry.builtinId}"`;
462
293
  return `<cellStyle name="${escapeAttr(entry.name)}" xfId="${entry.xfId}"${builtin}/>`;
463
294
  }
464
- // Reject an enum-typed style token the writer would otherwise emit verbatim. The public types already
465
- // forbid an out-of-contract value (VerticalAlignment, BorderStyle, FillPatternType, …), so this fires
466
- // only for a value smuggled past the types by an untyped caller — but the writer must never serialise
467
- // it: it would be schema-invalid OOXML that Excel silently tolerates yet the library's own reader
468
- // (which narrows every such token through the same guard) discards on read-back. Rejecting at the write
469
- // boundary keeps the writer symmetric with the reader — garbage out refused exactly as garbage in — so
470
- // a value the writer accepts is always one that round-trips.
471
295
  function checkedToken(value, isValid, kind) {
472
296
  if (!isValid(value)) {
473
297
  throw new AuthoringError(`Invalid ${kind} ${JSON.stringify(value)}: not a value the OOXML enumeration allows`);
474
298
  }
475
299
  return value;
476
300
  }
477
- // Serialise a cell's alignment as `<alignment>` attributes in ECMA-376 CT_CellAlignment order.
478
- // A facet at its default contributes nothing; an all-default alignment yields the empty string,
479
- // so it forces neither an <alignment> child nor a distinct xf.
480
301
  function alignmentAttrs(alignment) {
481
302
  const parts = [];
482
- // `general` is the type-dependent default and is expressed by omitting the attribute.
483
303
  if (alignment.horizontal !== undefined && alignment.horizontal !== 'general') {
484
304
  parts.push(`horizontal="${checkedToken(alignment.horizontal, isHorizontalAlignment, 'horizontal alignment')}"`);
485
305
  }
@@ -501,10 +321,6 @@ function alignmentAttrs(alignment) {
501
321
  }
502
322
  return parts.join(' ');
503
323
  }
504
- // Serialise a cell's protection as `<protection>` attributes. `locked` defaults to true in OOXML,
505
- // so only an explicitly unlocked cell writes `locked="0"`; `hidden` defaults to false, so only a
506
- // hidden cell writes `hidden="1"`. An all-default protection yields the empty string, forcing
507
- // neither a <protection> child nor a distinct xf.
508
324
  function protectionAttrs(protection) {
509
325
  const parts = [];
510
326
  if (protection.locked === false)
@@ -513,112 +329,9 @@ function protectionAttrs(protection) {
513
329
  parts.push('hidden="1"');
514
330
  return parts.join(' ');
515
331
  }
516
- /**
517
- * Extract the custom indexed-color palette (`<colors><indexedColors>`) from styles.xml as verbatim
518
- * `<rgbColor rgb="…"/>` fragments, or an empty list when the file rides the default palette. Kept raw
519
- * — rather than parsed into RGB and re-serialised — so the exact entries (count, order, casing) a
520
- * source file declared survive a round-trip and every `indexed="…"` reference keeps its RGB.
521
- */
522
- export function parseIndexedColors(stylesXml) {
523
- return elementFragments(stylesXml, 'indexedColors', 'rgbColor');
524
- }
525
- /**
526
- * Extract the most-recently-used colour swatches (`<colors><mruColors>`) from styles.xml as verbatim
527
- * `<color .../>` fragments, or an empty list when the file declares none. Kept raw for the same reason
528
- * the indexed palette is: the list is the author's own working set of colours and the model has no
529
- * use for its contents, only for not losing them.
530
- */
531
- export function parseMruColors(stylesXml) {
532
- return elementFragments(stylesXml, 'mruColors', 'color');
533
- }
534
- /**
535
- * Extract the `<tableStyles>` block from styles.xml: each `<tableStyle>` definition verbatim, plus the
536
- * container's nominated `defaultTableStyle`/`defaultPivotStyle`. See {@link TableStyleTable} for why
537
- * the definitions stay raw while the two names are decoded.
538
- *
539
- * A file with no such block — or with the self-closing `count="0"` container Excel writes when it has
540
- * only defaults to state — yields an empty {@link TableStyleTable.styles} and whichever names it did
541
- * carry.
542
- */
543
- export function parseTableStyles(stylesXml) {
544
- const styles = elementFragments(stylesXml, 'tableStyles', 'tableStyle');
545
- const table = { styles };
546
- for (const { attrs } of openElements(stylesXml, 'tableStyles')) {
547
- if (attrs.defaultTableStyle !== undefined)
548
- table.defaultTableStyle = attrs.defaultTableStyle;
549
- if (attrs.defaultPivotStyle !== undefined)
550
- table.defaultPivotStyle = attrs.defaultPivotStyle;
551
- break;
552
- }
553
- const namespaces = fragmentNamespaces(stylesXml, styles);
554
- if (namespaces.length > 0)
555
- table.namespaces = namespaces;
556
- return table;
557
- }
558
- // The namespace declarations the verbatim `<tableStyle>` fragments depend on, resolved against the
559
- // stylesheet root that scoped them. Only prefixes a fragment actually uses are carried, so an
560
- // ordinary file (whose fragments use none) adds nothing to the re-emitted root; a prefix a fragment
561
- // uses but the root never declared is skipped, because there is no URI to re-declare it with — the
562
- // source was already unparseable there and inventing a URI would not repair it.
563
- //
564
- // `ignorable` is copied from the source's own `mc:Ignorable` rather than assumed: a prefix the source
565
- // did *not* mark ignorable carries meaning the consumer must not skip, and marking it here would tell
566
- // every consumer to throw that meaning away.
567
- function fragmentNamespaces(stylesXml, fragments) {
568
- if (fragments.length === 0)
569
- return [];
570
- const declared = new Map();
571
- const ignorable = new Set();
572
- for (const { attrs } of openElements(stylesXml, 'styleSheet')) {
573
- for (const [name, value] of Object.entries(attrs)) {
574
- if (name.startsWith('xmlns:'))
575
- declared.set(name.slice('xmlns:'.length), value);
576
- }
577
- for (const prefix of (attrs['mc:Ignorable'] ?? '').split(/\s+/)) {
578
- if (prefix !== '')
579
- ignorable.add(prefix);
580
- }
581
- break;
582
- }
583
- const used = new Set();
584
- for (const fragment of fragments) {
585
- // A prefix appears either on an element (`<p:tag`, `</p:tag`) or on an attribute (` p:attr=`).
586
- for (const match of fragment.matchAll(/[\s</]([A-Za-z_][\w.-]*):[A-Za-z_]/g)) {
587
- used.add(match[1]);
588
- }
589
- }
590
- return [...used]
591
- .filter((prefix) => declared.has(prefix))
592
- .map((prefix) => ({
593
- prefix,
594
- uri: declared.get(prefix),
595
- ignorable: ignorable.has(prefix),
596
- }));
597
- }
598
- // The `name` a `<tableStyle>` fragment declares — the key a table's `tableStyleInfo/@name` matches
599
- // and, here, the key an authored definition overrides a preserved one by. Read out of the fragment
600
- // rather than stored beside it, so the two cannot drift; `name` is required by CT_TableStyle, and a
601
- // fragment without one is unreachable anyway and so can never collide.
602
332
  function tableStyleName(fragment) {
603
333
  return decodeEntities(/<tableStyle\b[^>]*\bname="([^"]*)"/.exec(fragment)?.[1] ?? '');
604
334
  }
605
- // The verbatim child fragments of a container element — the shape every preserved styles sub-table
606
- // takes. Scanning the container's inner text rather than the whole part is what keeps a `<color>` in
607
- // `<mruColors>` from being confused with the many other `<color>` elements a stylesheet carries, and
608
- // the `\b` after the child's name is what keeps `<tableStyles>` from matching as a `<tableStyle>`.
609
- function elementFragments(xml, container, child) {
610
- const block = new RegExp(`<${container}\\b[^>]*>([\\s\\S]*?)</${container}>`).exec(xml);
611
- if (block === null)
612
- return [];
613
- const inner = block[1] ?? '';
614
- const pattern = new RegExp(`<${child}\\b[^>]*/>|<${child}\\b[^>]*>[\\s\\S]*?</${child}>`, 'g');
615
- return [...inner.matchAll(pattern)].map((m) => m[0] ?? '');
616
- }
617
- // Serialise the facets a font overrides, in ECMA-376 child order. A boolean flag is emitted only
618
- // when true (its absence is the default false); an empty result means the font differs from the
619
- // default in nothing and needs no entry at all. The face element differs by context: a styles
620
- // `<font>` names it `<name>` (CT_Font), a rich-text run's `<rPr>` names it `<rFont>` (CT_RPrElt) —
621
- // otherwise the two share every child, so `nameTag` selects the face element and the rest is common.
622
335
  export function fontXml(font, nameTag = 'name') {
623
336
  const parts = [];
624
337
  if (font.bold)
@@ -649,10 +362,6 @@ export function fontXml(font, nameTag = 'name') {
649
362
  parts.push(`<scheme val="${checkedToken(font.scheme, isFontScheme, 'font scheme')}"/>`);
650
363
  return parts.join('');
651
364
  }
652
- // Serialise a differential style (CT_Dxf) in schema child order: font, numFmt, fill, border. Only the
653
- // facets present are emitted — a dxf overrides exactly what it names and lets the cell's own style show
654
- // through the rest. A dxf's pattern fill states the highlight through `bgColor`, matching how Excel
655
- // writes a "fill with colour" conditional format.
656
365
  export function dxfXml(style) {
657
366
  const parts = [];
658
367
  if (style.font !== undefined) {
@@ -660,8 +369,6 @@ export function dxfXml(style) {
660
369
  if (font !== '')
661
370
  parts.push(`<font>${font}</font>`);
662
371
  }
663
- // A dxf numFmt still needs an id; the code is what matters (dxf formats are not shared by id like
664
- // cell formats), so a fixed custom id carries it without a <numFmts> entry.
665
372
  if (typeof style.numFmt === 'string' && style.numFmt !== '') {
666
373
  parts.push(`<numFmt numFmtId="${CUSTOM_NUMFMT_BASE}" formatCode="${escapeFormatCode(style.numFmt)}"/>`);
667
374
  }
@@ -671,8 +378,6 @@ export function dxfXml(style) {
671
378
  parts.push(borderXml(style.border));
672
379
  return `<dxf>${parts.join('')}</dxf>`;
673
380
  }
674
- // The gradient element shared by cell fills and dxf fills. Linear gradients carry a `degree`; path
675
- // gradients carry inner-rectangle insets. A zero-valued attribute is its OOXML default and is omitted.
676
381
  function gradientFillXml(fill) {
677
382
  const attrs = (fill.gradient === 'path' ? ' type="path"' : '') +
678
383
  (fill.degree ? ` degree="${numberAttr(fill.degree)}"` : '') +
@@ -688,8 +393,6 @@ function gradientFillXml(fill) {
688
393
  function insetAttr(name, value) {
689
394
  return value ? ` ${name}="${numberAttr(value)}"` : '';
690
395
  }
691
- // `<u/>` is single underline (the same as an explicit "single"); the named variants carry a
692
- // val; false and "none" are the default no-underline and emit nothing.
693
396
  function underlineXml(underline) {
694
397
  if (underline === undefined || underline === false || underline === 'none')
695
398
  return '';
@@ -703,9 +406,6 @@ function numberAttr(value) {
703
406
  }
704
407
  return String(value);
705
408
  }
706
- // Serialise a border in ECMA-376 CT_Border child order (left, right, top, bottom, diagonal).
707
- // Every edge element is always present — a styleless `<left/>` is how OOXML says "no left
708
- // border" — so an all-absent border round-trips to the empty default rather than a new id.
709
409
  function borderXml(border) {
710
410
  const attrs = (border.diagonalUp ? ' diagonalUp="1"' : '') + (border.diagonalDown ? ' diagonalDown="1"' : '');
711
411
  return (`<border${attrs}>` +
@@ -716,8 +416,6 @@ function borderXml(border) {
716
416
  edgeXml('diagonal', border.diagonal) +
717
417
  '</border>');
718
418
  }
719
- // One border edge: a styleless self-closing tag when absent, else the style attribute plus an
720
- // optional colour child.
721
419
  function edgeXml(tag, edge) {
722
420
  if (edge === undefined)
723
421
  return `<${tag}/>`;
@@ -726,11 +424,6 @@ function edgeXml(tag, edge) {
726
424
  return `<${tag} style="${style}"/>`;
727
425
  return `<${tag} style="${style}"><color ${colorAttrs(edge.color)}/></${tag}>`;
728
426
  }
729
- // A format code sits in the `formatCode` attribute; only the markup-significant characters
730
- // need escaping. A code can legitimately contain `"` (quoted literals like `"$"`), `<`, `&`.
731
- // Unlike `escapeAttr`, a lone `'` is left untouched: it is not markup-significant inside a
732
- // double-quoted attribute, and Excel writes format codes with bare apostrophes, so leaving it
733
- // keeps the round-tripped code byte-identical to the source.
734
427
  function escapeFormatCode(code) {
735
428
  return code
736
429
  .replace(/&/g, '&amp;')
@@ -738,7 +431,6 @@ function escapeFormatCode(code) {
738
431
  .replace(/>/g, '&gt;')
739
432
  .replace(/"/g, '&quot;');
740
433
  }
741
- // A stable, collision-free key for a fill: identical fills share it, distinct ones don't.
742
434
  function fillSignature(fill) {
743
435
  if (fill.type === 'gradient') {
744
436
  const stops = fill.stops.map((s) => `${s.position}:${colorSignature(s.color)}`).join(',');
@@ -751,10 +443,6 @@ function colorSignature(color) {
751
443
  return '';
752
444
  return `${color.argb ?? ''}/${color.theme ?? ''}/${color.tint ?? ''}/${color.indexed ?? ''}`;
753
445
  }
754
- // The `<fill>` element for a pattern or gradient fill. The two callers differ only in the solid-fill
755
- // background fallback: a cell fill forces the automatic indexed placeholder onto a solid pattern that
756
- // names no background — omitting it makes Excel render the fill as flat black — whereas a dxf states
757
- // only the overrides it carries, so `solidBgFallback` gates that placeholder.
758
446
  function patternFillXml(fill, { solidBgFallback }) {
759
447
  if (fill.type === 'gradient')
760
448
  return `<fill>${gradientFillXml(fill)}</fill>`;
@@ -767,55 +455,3 @@ function patternFillXml(fill, { solidBgFallback }) {
767
455
  const pattern = checkedToken(fill.pattern, isFillPatternType, 'fill pattern');
768
456
  return `<fill><patternFill patternType="${pattern}">${fg}${bg}</patternFill></fill>`;
769
457
  }
770
- // OOXML wants a bare 8-hex ARGB (alpha + RGB). This single choke point — through which every
771
- // fill/font/border/tab colour flows — accepts two developer conveniences and rejects the rest loudly,
772
- // because a malformed rgb value does not error in Excel: it silently renders as flat black.
773
- // - A leading '#' is a CSS habit and is stripped ('#FFBFBFBF' → 'FFBFBFBF').
774
- // - A 6-hex RGB is promoted to ARGB with a fully-opaque alpha ('00FF00' → 'FF00FF00'), the common
775
- // case of a colour written without its alpha channel.
776
- // Anything not then exactly 8 hex digits is a programming error at the API surface, so it throws with
777
- // the offending value rather than writing corrupt XML. Casing is preserved so foreign files round-trip.
778
- function normalizeArgb(argb) {
779
- const hex = argb.startsWith('#') ? argb.slice(1) : argb;
780
- const rgb = hex.length === 6 ? `FF${hex}` : hex;
781
- if (!/^[0-9a-fA-F]{8}$/.test(rgb)) {
782
- throw new AuthoringError(`Invalid ARGB colour ${JSON.stringify(argb)}: expected 6 or 8 hexadecimal digits`);
783
- }
784
- return rgb;
785
- }
786
- export function colorAttrs(color) {
787
- const parts = [];
788
- if (color.argb !== undefined)
789
- parts.push(`rgb="${normalizeArgb(color.argb)}"`);
790
- if (color.theme !== undefined)
791
- parts.push(`theme="${color.theme}"`);
792
- if (color.tint !== undefined)
793
- parts.push(`tint="${color.tint}"`);
794
- if (color.indexed !== undefined)
795
- parts.push(`indexed="${color.indexed}"`);
796
- return parts.join(' ');
797
- }
798
- // The read counterpart of colorAttrs: decode a `<color>`/`<fgColor>`/… element's attributes.
799
- // theme/indexed must be integers and tint a finite number; a malformed foreign attribute is dropped
800
- // rather than propagated as NaN, so a downstream colorAttrs never emits `theme="NaN"`.
801
- export function parseColor(attrs) {
802
- const color = {};
803
- if (attrs.rgb !== undefined)
804
- color.argb = attrs.rgb;
805
- if (attrs.theme !== undefined) {
806
- const theme = Number(attrs.theme);
807
- if (Number.isInteger(theme))
808
- color.theme = theme;
809
- }
810
- if (attrs.tint !== undefined) {
811
- const tint = Number(attrs.tint);
812
- if (Number.isFinite(tint))
813
- color.tint = tint;
814
- }
815
- if (attrs.indexed !== undefined) {
816
- const indexed = Number(attrs.indexed);
817
- if (Number.isInteger(indexed))
818
- color.indexed = indexed;
819
- }
820
- return color;
821
- }