@shbernal/ts-xlsx 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/dist/io/xlsx/styles.js
CHANGED
|
@@ -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
|
|
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, '&')
|
|
@@ -738,7 +431,6 @@ function escapeFormatCode(code) {
|
|
|
738
431
|
.replace(/>/g, '>')
|
|
739
432
|
.replace(/"/g, '"');
|
|
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
|
-
}
|