@shbernal/ts-xlsx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,821 @@
|
|
|
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
|
+
import { isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isHorizontalAlignment, isNamedUnderlineStyle, isVerticalAlignment, } from "../../core/style.js";
|
|
16
|
+
import { TABLE_STYLE_ELEMENT_TYPES } from "../../core/table-style.js";
|
|
17
|
+
import { AuthoringError } from "../../errors.js";
|
|
18
|
+
import { escapeAttr, XML_DECLARATION } from "../../xml/xml.js";
|
|
19
|
+
import { decodeEntities, openElements } from "../../xml/xml-read.js";
|
|
20
|
+
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
|
+
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
|
+
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
|
+
const CUSTOM_NUMFMT_BASE = 164;
|
|
30
|
+
// Border id 0 is the always-present empty border (every edge absent); custom borders from 1.
|
|
31
|
+
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
|
+
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
|
+
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
|
+
const DEFAULT_FORMAT = {
|
|
43
|
+
fillId: 0,
|
|
44
|
+
numFmtId: 0,
|
|
45
|
+
fontId: 0,
|
|
46
|
+
borderId: 0,
|
|
47
|
+
alignment: '',
|
|
48
|
+
protection: '',
|
|
49
|
+
quotePrefix: false,
|
|
50
|
+
xfId: 0,
|
|
51
|
+
};
|
|
52
|
+
export class StyleRegistry {
|
|
53
|
+
// The `<font>` body emitted as id 0.
|
|
54
|
+
#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
|
+
#font0Bodies;
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.#defaultFontBody =
|
|
60
|
+
options.defaultFont === undefined ? DEFAULT_FONT_BODY : fontXml(options.defaultFont);
|
|
61
|
+
this.#font0Bodies = new Set(options.declaredDefaultFont === undefined
|
|
62
|
+
? [this.#defaultFontBody]
|
|
63
|
+
: [this.#defaultFontBody, fontXml(options.declaredDefaultFont)]);
|
|
64
|
+
}
|
|
65
|
+
// Custom fill xml fragments, in id order; the emitted id is RESERVED_FILL_COUNT + index.
|
|
66
|
+
#fillXml = [];
|
|
67
|
+
#fillIdBySignature = new Map();
|
|
68
|
+
// Custom number-format codes, in id order; the emitted id is CUSTOM_NUMFMT_BASE + index.
|
|
69
|
+
#numFmtCodes = [];
|
|
70
|
+
#numFmtIdByCode = new Map();
|
|
71
|
+
// Custom font xml fragments, in id order; the emitted id is RESERVED_FONT_COUNT + index.
|
|
72
|
+
#fontXml = [];
|
|
73
|
+
#fontIdBySignature = new Map();
|
|
74
|
+
// Custom border xml fragments, in id order; the emitted id is RESERVED_BORDER_COUNT + index.
|
|
75
|
+
#borderXml = [];
|
|
76
|
+
#borderIdBySignature = new Map();
|
|
77
|
+
// xf 0 is the default (no fill/font/border/alignment/protection, General format); further entries append as styles appear.
|
|
78
|
+
#formats = [DEFAULT_FORMAT];
|
|
79
|
+
#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
|
+
#cellStyleXfs = [DEFAULT_FORMAT];
|
|
84
|
+
#cellStyleNames = [
|
|
85
|
+
{ name: 'Normal', builtinId: 0, xfId: 0 },
|
|
86
|
+
];
|
|
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
|
+
#dxfXml = [];
|
|
91
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
styleId(style) {
|
|
117
|
+
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
|
+
if (isDefaultFormat(format))
|
|
122
|
+
return 0;
|
|
123
|
+
const signature = formatSignature(format);
|
|
124
|
+
let index = this.#xfIndexBySignature.get(signature);
|
|
125
|
+
if (index === undefined) {
|
|
126
|
+
index = this.#formats.length;
|
|
127
|
+
this.#formats.push(format);
|
|
128
|
+
this.#xfIndexBySignature.set(signature, index);
|
|
129
|
+
}
|
|
130
|
+
return index;
|
|
131
|
+
}
|
|
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
|
+
#composeFormat(style, xfId) {
|
|
136
|
+
// A `none` pattern is the reserved fill 0; a gradient is always a real, interned fill.
|
|
137
|
+
const paints = style.fill !== undefined && (style.fill.type === 'gradient' || style.fill.pattern !== 'none');
|
|
138
|
+
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
|
+
const numFmtId = typeof style.numFmt === 'string' && style.numFmt !== ''
|
|
144
|
+
? this.#internNumFmt(style.numFmt)
|
|
145
|
+
: 0;
|
|
146
|
+
const fontId = style.font ? this.#internFont(style.font) : 0;
|
|
147
|
+
const borderId = style.border ? this.#internBorder(style.border) : 0;
|
|
148
|
+
const alignment = style.alignment ? alignmentAttrs(style.alignment) : '';
|
|
149
|
+
const protection = style.protection ? protectionAttrs(style.protection) : '';
|
|
150
|
+
const quotePrefix = style.quotePrefix === true;
|
|
151
|
+
return { fillId, numFmtId, fontId, borderId, alignment, protection, quotePrefix, xfId };
|
|
152
|
+
}
|
|
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
|
+
seedNamedStyles(styles) {
|
|
160
|
+
if (styles.length === 0)
|
|
161
|
+
return;
|
|
162
|
+
this.#cellStyleXfs.length = 0;
|
|
163
|
+
this.#cellStyleNames.length = 0;
|
|
164
|
+
styles.forEach((style, index) => {
|
|
165
|
+
this.#cellStyleXfs.push(this.#composeFormat(style, 0));
|
|
166
|
+
const entry = {
|
|
167
|
+
name: style.name ?? `Style ${index}`,
|
|
168
|
+
xfId: index,
|
|
169
|
+
};
|
|
170
|
+
if (style.builtinId !== undefined)
|
|
171
|
+
entry.builtinId = style.builtinId;
|
|
172
|
+
this.#cellStyleNames.push(entry);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
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
|
+
seedDifferentialStyles(fragments) {
|
|
189
|
+
for (const fragment of fragments) {
|
|
190
|
+
const index = this.#dxfXml.length;
|
|
191
|
+
this.#dxfXml.push(fragment);
|
|
192
|
+
// A seeded fragment can still be reused by an authored style identical to it, so index it too.
|
|
193
|
+
if (!this.#dxfIndexByFragment.has(fragment))
|
|
194
|
+
this.#dxfIndexByFragment.set(fragment, index);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
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
|
+
seedIndexedColors(fragments) {
|
|
204
|
+
this.#indexedColors.length = 0;
|
|
205
|
+
this.#indexedColors.push(...fragments);
|
|
206
|
+
}
|
|
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
|
+
seedMruColors(fragments) {
|
|
212
|
+
this.#mruColors.length = 0;
|
|
213
|
+
this.#mruColors.push(...fragments);
|
|
214
|
+
}
|
|
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
|
+
seedTableStyles(table) {
|
|
222
|
+
this.#tableStyles = table;
|
|
223
|
+
}
|
|
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
|
+
addTableStyle(style) {
|
|
235
|
+
const elements = TABLE_STYLE_ELEMENT_TYPES.flatMap((type) => {
|
|
236
|
+
const element = style.elements[type];
|
|
237
|
+
if (element === undefined)
|
|
238
|
+
return [];
|
|
239
|
+
// `size` defaults to 1, so it is written only when a band is genuinely wider than one row.
|
|
240
|
+
const size = element.size !== undefined && element.size !== 1 ? ` size="${element.size}"` : '';
|
|
241
|
+
return [
|
|
242
|
+
`<tableStyleElement type="${type}"${size} dxfId="${this.differentialStyleId(element)}"/>`,
|
|
243
|
+
];
|
|
244
|
+
});
|
|
245
|
+
// `pivot`/`table` default to true, so each is written only when the caller opts a style out.
|
|
246
|
+
const flags = (style.pivot === false ? ' pivot="0"' : '') + (style.table === false ? ' table="0"' : '');
|
|
247
|
+
this.#authoredTableStyles.set(style.name, `<tableStyle name="${escapeAttr(style.name)}"${flags} count="${elements.length}">` +
|
|
248
|
+
`${elements.join('')}</tableStyle>`);
|
|
249
|
+
}
|
|
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
|
+
differentialStyleId(style) {
|
|
260
|
+
const fragment = dxfXml(style);
|
|
261
|
+
let index = this.#dxfIndexByFragment.get(fragment);
|
|
262
|
+
if (index === undefined) {
|
|
263
|
+
index = this.#dxfXml.length;
|
|
264
|
+
this.#dxfXml.push(fragment);
|
|
265
|
+
this.#dxfIndexByFragment.set(fragment, index);
|
|
266
|
+
}
|
|
267
|
+
return index;
|
|
268
|
+
}
|
|
269
|
+
#internFill(fill) {
|
|
270
|
+
const signature = fillSignature(fill);
|
|
271
|
+
let id = this.#fillIdBySignature.get(signature);
|
|
272
|
+
if (id === undefined) {
|
|
273
|
+
id = RESERVED_FILL_COUNT + this.#fillXml.length;
|
|
274
|
+
this.#fillXml.push(patternFillXml(fill, { solidBgFallback: true }));
|
|
275
|
+
this.#fillIdBySignature.set(signature, id);
|
|
276
|
+
}
|
|
277
|
+
return id;
|
|
278
|
+
}
|
|
279
|
+
#internNumFmt(code) {
|
|
280
|
+
let id = this.#numFmtIdByCode.get(code);
|
|
281
|
+
if (id === undefined) {
|
|
282
|
+
id = CUSTOM_NUMFMT_BASE + this.#numFmtCodes.length;
|
|
283
|
+
this.#numFmtCodes.push(code);
|
|
284
|
+
this.#numFmtIdByCode.set(code, id);
|
|
285
|
+
}
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
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
|
+
#internFont(font) {
|
|
291
|
+
const xml = fontXml(font);
|
|
292
|
+
if (xml === '' || this.#font0Bodies.has(xml))
|
|
293
|
+
return 0;
|
|
294
|
+
let id = this.#fontIdBySignature.get(xml);
|
|
295
|
+
if (id === undefined) {
|
|
296
|
+
id = RESERVED_FONT_COUNT + this.#fontXml.length;
|
|
297
|
+
this.#fontXml.push(`<font>${xml}</font>`);
|
|
298
|
+
this.#fontIdBySignature.set(xml, id);
|
|
299
|
+
}
|
|
300
|
+
return id;
|
|
301
|
+
}
|
|
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
|
+
#internBorder(border) {
|
|
305
|
+
const xml = borderXml(border);
|
|
306
|
+
if (xml === DEFAULT_BORDER)
|
|
307
|
+
return 0;
|
|
308
|
+
let id = this.#borderIdBySignature.get(xml);
|
|
309
|
+
if (id === undefined) {
|
|
310
|
+
id = RESERVED_BORDER_COUNT + this.#borderXml.length;
|
|
311
|
+
this.#borderXml.push(xml);
|
|
312
|
+
this.#borderIdBySignature.set(xml, id);
|
|
313
|
+
}
|
|
314
|
+
return id;
|
|
315
|
+
}
|
|
316
|
+
/** Serialise the accumulated table into a complete, valid styles.xml part. */
|
|
317
|
+
toXml() {
|
|
318
|
+
const fillCount = RESERVED_FILL_COUNT + this.#fillXml.length;
|
|
319
|
+
const fills = '<fill><patternFill patternType="none"/></fill>' +
|
|
320
|
+
'<fill><patternFill patternType="gray125"/></fill>' +
|
|
321
|
+
this.#fillXml.join('');
|
|
322
|
+
const cellXfs = this.#formats.map((format) => xfXml(format, format.xfId)).join('');
|
|
323
|
+
const cellStyleXfs = this.#cellStyleXfs.map((format) => xfXml(format, null)).join('');
|
|
324
|
+
const cellStyles = this.#cellStyleNames.map(cellStyleTag).join('');
|
|
325
|
+
const fontCount = RESERVED_FONT_COUNT + this.#fontXml.length;
|
|
326
|
+
const fonts = `<font>${this.#defaultFontBody}</font>` + this.#fontXml.join('');
|
|
327
|
+
const borderCount = RESERVED_BORDER_COUNT + this.#borderXml.length;
|
|
328
|
+
const borders = DEFAULT_BORDER + this.#borderXml.join('');
|
|
329
|
+
return (XML_DECLARATION +
|
|
330
|
+
`<styleSheet xmlns="${SPREADSHEETML_NS}"${this.#foreignNamespaceAttrs()}>` +
|
|
331
|
+
this.#numFmtsXml() +
|
|
332
|
+
`<fonts count="${fontCount}">${fonts}</fonts>` +
|
|
333
|
+
`<fills count="${fillCount}">${fills}</fills>` +
|
|
334
|
+
`<borders count="${borderCount}">${borders}</borders>` +
|
|
335
|
+
`<cellStyleXfs count="${this.#cellStyleXfs.length}">${cellStyleXfs}</cellStyleXfs>` +
|
|
336
|
+
`<cellXfs count="${this.#formats.length}">${cellXfs}</cellXfs>` +
|
|
337
|
+
`<cellStyles count="${this.#cellStyleNames.length}">${cellStyles}</cellStyles>` +
|
|
338
|
+
this.#dxfsXml() +
|
|
339
|
+
this.#tableStylesXml() +
|
|
340
|
+
this.#colorsXml() +
|
|
341
|
+
'</styleSheet>');
|
|
342
|
+
}
|
|
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
|
+
#foreignNamespaceAttrs() {
|
|
351
|
+
const namespaces = this.#tableStyles.namespaces ?? [];
|
|
352
|
+
if (namespaces.length === 0)
|
|
353
|
+
return '';
|
|
354
|
+
const declarations = namespaces.map((ns) => ` xmlns:${ns.prefix}="${escapeAttr(ns.uri)}"`);
|
|
355
|
+
const ignorable = namespaces.filter((ns) => ns.ignorable).map((ns) => ns.prefix);
|
|
356
|
+
const compatibility = ignorable.length === 0
|
|
357
|
+
? ''
|
|
358
|
+
: ` xmlns:mc="${MARKUP_COMPATIBILITY_NS}" mc:Ignorable="${escapeAttr(ignorable.join(' '))}"`;
|
|
359
|
+
return declarations.join('') + compatibility;
|
|
360
|
+
}
|
|
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
|
+
#tableStylesXml() {
|
|
370
|
+
const { defaultTableStyle, defaultPivotStyle } = this.#tableStyles;
|
|
371
|
+
const authored = this.#authoredTableStyles;
|
|
372
|
+
const preserved = this.#tableStyles.styles.filter((fragment) => !authored.has(tableStyleName(fragment)));
|
|
373
|
+
const styles = [...preserved, ...authored.values()];
|
|
374
|
+
if (styles.length === 0 && defaultTableStyle === undefined && defaultPivotStyle === undefined) {
|
|
375
|
+
return '';
|
|
376
|
+
}
|
|
377
|
+
const attrs = ` count="${styles.length}"` +
|
|
378
|
+
(defaultTableStyle === undefined
|
|
379
|
+
? ''
|
|
380
|
+
: ` defaultTableStyle="${escapeAttr(defaultTableStyle)}"`) +
|
|
381
|
+
(defaultPivotStyle === undefined
|
|
382
|
+
? ''
|
|
383
|
+
: ` defaultPivotStyle="${escapeAttr(defaultPivotStyle)}"`);
|
|
384
|
+
if (styles.length === 0)
|
|
385
|
+
return `<tableStyles${attrs}/>`;
|
|
386
|
+
return `<tableStyles${attrs}>${styles.join('')}</tableStyles>`;
|
|
387
|
+
}
|
|
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
|
+
#colorsXml() {
|
|
393
|
+
const indexed = this.#indexedColors.length === 0
|
|
394
|
+
? ''
|
|
395
|
+
: `<indexedColors>${this.#indexedColors.join('')}</indexedColors>`;
|
|
396
|
+
const mru = this.#mruColors.length === 0 ? '' : `<mruColors>${this.#mruColors.join('')}</mruColors>`;
|
|
397
|
+
if (indexed === '' && mru === '')
|
|
398
|
+
return '';
|
|
399
|
+
return `<colors>${indexed}${mru}</colors>`;
|
|
400
|
+
}
|
|
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
|
+
#dxfsXml() {
|
|
405
|
+
if (this.#dxfXml.length === 0)
|
|
406
|
+
return '<dxfs count="0"/>';
|
|
407
|
+
return `<dxfs count="${this.#dxfXml.length}">${this.#dxfXml.join('')}</dxfs>`;
|
|
408
|
+
}
|
|
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
|
+
#numFmtsXml() {
|
|
412
|
+
if (this.#numFmtCodes.length === 0)
|
|
413
|
+
return '';
|
|
414
|
+
const entries = this.#numFmtCodes
|
|
415
|
+
.map((code, i) => `<numFmt numFmtId="${CUSTOM_NUMFMT_BASE + i}" formatCode="${escapeFormatCode(code)}"/>`)
|
|
416
|
+
.join('');
|
|
417
|
+
return `<numFmts count="${this.#numFmtCodes.length}">${entries}</numFmts>`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
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
|
+
function isDefaultFormat(format) {
|
|
423
|
+
return (format.fillId === 0 &&
|
|
424
|
+
format.numFmtId === 0 &&
|
|
425
|
+
format.fontId === 0 &&
|
|
426
|
+
format.borderId === 0 &&
|
|
427
|
+
format.alignment === '' &&
|
|
428
|
+
format.protection === '' &&
|
|
429
|
+
!format.quotePrefix &&
|
|
430
|
+
format.xfId === 0);
|
|
431
|
+
}
|
|
432
|
+
// A stable, collision-free key for a composed format so identical formats intern to one cellXfs entry.
|
|
433
|
+
function formatSignature(format) {
|
|
434
|
+
return (`fill:${format.fillId}|numFmt:${format.numFmtId}|font:${format.fontId}|border:${format.borderId}|` +
|
|
435
|
+
`align:${format.alignment}|protect:${format.protection}|quote:${format.quotePrefix}|xfId:${format.xfId}`);
|
|
436
|
+
}
|
|
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
|
+
function xfXml(format, xfId) {
|
|
440
|
+
const applyNumberFormat = format.numFmtId !== 0 ? ' applyNumberFormat="1"' : '';
|
|
441
|
+
const applyFont = format.fontId !== 0 ? ' applyFont="1"' : '';
|
|
442
|
+
const applyFill = format.fillId !== 0 ? ' applyFill="1"' : '';
|
|
443
|
+
const applyBorder = format.borderId !== 0 ? ' applyBorder="1"' : '';
|
|
444
|
+
const applyAlignment = format.alignment !== '' ? ' applyAlignment="1"' : '';
|
|
445
|
+
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
|
+
const quotePrefix = format.quotePrefix ? ' quotePrefix="1"' : '';
|
|
449
|
+
const xfIdAttr = xfId === null ? '' : ` xfId="${xfId}"`;
|
|
450
|
+
const open = `<xf numFmtId="${format.numFmtId}" fontId="${format.fontId}" fillId="${format.fillId}" ` +
|
|
451
|
+
`borderId="${format.borderId}"${xfIdAttr}${quotePrefix}` +
|
|
452
|
+
`${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
|
+
const body = (format.alignment === '' ? '' : `<alignment ${format.alignment}/>`) +
|
|
456
|
+
(format.protection === '' ? '' : `<protection ${format.protection}/>`);
|
|
457
|
+
return body === '' ? `${open}/>` : `${open}>${body}</xf>`;
|
|
458
|
+
}
|
|
459
|
+
// One `<cellStyle>` entry mapping a name (and, for a built-in, its gallery id) to a cellStyleXfs index.
|
|
460
|
+
function cellStyleTag(entry) {
|
|
461
|
+
const builtin = entry.builtinId === undefined ? '' : ` builtinId="${entry.builtinId}"`;
|
|
462
|
+
return `<cellStyle name="${escapeAttr(entry.name)}" xfId="${entry.xfId}"${builtin}/>`;
|
|
463
|
+
}
|
|
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
|
+
function checkedToken(value, isValid, kind) {
|
|
472
|
+
if (!isValid(value)) {
|
|
473
|
+
throw new AuthoringError(`Invalid ${kind} ${JSON.stringify(value)}: not a value the OOXML enumeration allows`);
|
|
474
|
+
}
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
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
|
+
function alignmentAttrs(alignment) {
|
|
481
|
+
const parts = [];
|
|
482
|
+
// `general` is the type-dependent default and is expressed by omitting the attribute.
|
|
483
|
+
if (alignment.horizontal !== undefined && alignment.horizontal !== 'general') {
|
|
484
|
+
parts.push(`horizontal="${checkedToken(alignment.horizontal, isHorizontalAlignment, 'horizontal alignment')}"`);
|
|
485
|
+
}
|
|
486
|
+
if (alignment.vertical !== undefined) {
|
|
487
|
+
parts.push(`vertical="${checkedToken(alignment.vertical, isVerticalAlignment, 'vertical alignment')}"`);
|
|
488
|
+
}
|
|
489
|
+
if (alignment.textRotation !== undefined && alignment.textRotation !== 0) {
|
|
490
|
+
parts.push(`textRotation="${numberAttr(alignment.textRotation)}"`);
|
|
491
|
+
}
|
|
492
|
+
if (alignment.wrapText)
|
|
493
|
+
parts.push('wrapText="1"');
|
|
494
|
+
if (alignment.indent !== undefined && alignment.indent !== 0) {
|
|
495
|
+
parts.push(`indent="${numberAttr(alignment.indent)}"`);
|
|
496
|
+
}
|
|
497
|
+
if (alignment.shrinkToFit)
|
|
498
|
+
parts.push('shrinkToFit="1"');
|
|
499
|
+
if (alignment.readingOrder !== undefined && alignment.readingOrder !== 0) {
|
|
500
|
+
parts.push(`readingOrder="${numberAttr(alignment.readingOrder)}"`);
|
|
501
|
+
}
|
|
502
|
+
return parts.join(' ');
|
|
503
|
+
}
|
|
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
|
+
function protectionAttrs(protection) {
|
|
509
|
+
const parts = [];
|
|
510
|
+
if (protection.locked === false)
|
|
511
|
+
parts.push('locked="0"');
|
|
512
|
+
if (protection.hidden === true)
|
|
513
|
+
parts.push('hidden="1"');
|
|
514
|
+
return parts.join(' ');
|
|
515
|
+
}
|
|
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
|
+
function tableStyleName(fragment) {
|
|
603
|
+
return decodeEntities(/<tableStyle\b[^>]*\bname="([^"]*)"/.exec(fragment)?.[1] ?? '');
|
|
604
|
+
}
|
|
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
|
+
export function fontXml(font, nameTag = 'name') {
|
|
623
|
+
const parts = [];
|
|
624
|
+
if (font.bold)
|
|
625
|
+
parts.push('<b/>');
|
|
626
|
+
if (font.italic)
|
|
627
|
+
parts.push('<i/>');
|
|
628
|
+
if (font.strike)
|
|
629
|
+
parts.push('<strike/>');
|
|
630
|
+
if (font.outline)
|
|
631
|
+
parts.push('<outline/>');
|
|
632
|
+
const underline = underlineXml(font.underline);
|
|
633
|
+
if (underline !== '')
|
|
634
|
+
parts.push(underline);
|
|
635
|
+
if (font.vertAlign !== undefined) {
|
|
636
|
+
parts.push(`<vertAlign val="${checkedToken(font.vertAlign, isFontVerticalAlignment, 'font vertical alignment')}"/>`);
|
|
637
|
+
}
|
|
638
|
+
if (font.size !== undefined)
|
|
639
|
+
parts.push(`<sz val="${numberAttr(font.size)}"/>`);
|
|
640
|
+
if (font.color !== undefined)
|
|
641
|
+
parts.push(`<color ${colorAttrs(font.color)}/>`);
|
|
642
|
+
if (font.name !== undefined)
|
|
643
|
+
parts.push(`<${nameTag} val="${escapeAttr(font.name)}"/>`);
|
|
644
|
+
if (font.family !== undefined)
|
|
645
|
+
parts.push(`<family val="${numberAttr(font.family)}"/>`);
|
|
646
|
+
if (font.charset !== undefined)
|
|
647
|
+
parts.push(`<charset val="${numberAttr(font.charset)}"/>`);
|
|
648
|
+
if (font.scheme !== undefined && font.scheme !== 'none')
|
|
649
|
+
parts.push(`<scheme val="${checkedToken(font.scheme, isFontScheme, 'font scheme')}"/>`);
|
|
650
|
+
return parts.join('');
|
|
651
|
+
}
|
|
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
|
+
export function dxfXml(style) {
|
|
657
|
+
const parts = [];
|
|
658
|
+
if (style.font !== undefined) {
|
|
659
|
+
const font = fontXml(style.font);
|
|
660
|
+
if (font !== '')
|
|
661
|
+
parts.push(`<font>${font}</font>`);
|
|
662
|
+
}
|
|
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
|
+
if (typeof style.numFmt === 'string' && style.numFmt !== '') {
|
|
666
|
+
parts.push(`<numFmt numFmtId="${CUSTOM_NUMFMT_BASE}" formatCode="${escapeFormatCode(style.numFmt)}"/>`);
|
|
667
|
+
}
|
|
668
|
+
if (style.fill !== undefined)
|
|
669
|
+
parts.push(patternFillXml(style.fill, { solidBgFallback: false }));
|
|
670
|
+
if (style.border !== undefined)
|
|
671
|
+
parts.push(borderXml(style.border));
|
|
672
|
+
return `<dxf>${parts.join('')}</dxf>`;
|
|
673
|
+
}
|
|
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
|
+
function gradientFillXml(fill) {
|
|
677
|
+
const attrs = (fill.gradient === 'path' ? ' type="path"' : '') +
|
|
678
|
+
(fill.degree ? ` degree="${numberAttr(fill.degree)}"` : '') +
|
|
679
|
+
insetAttr('left', fill.left) +
|
|
680
|
+
insetAttr('right', fill.right) +
|
|
681
|
+
insetAttr('top', fill.top) +
|
|
682
|
+
insetAttr('bottom', fill.bottom);
|
|
683
|
+
const stops = fill.stops
|
|
684
|
+
.map((stop) => `<stop position="${numberAttr(stop.position)}"><color ${colorAttrs(stop.color)}/></stop>`)
|
|
685
|
+
.join('');
|
|
686
|
+
return `<gradientFill${attrs}>${stops}</gradientFill>`;
|
|
687
|
+
}
|
|
688
|
+
function insetAttr(name, value) {
|
|
689
|
+
return value ? ` ${name}="${numberAttr(value)}"` : '';
|
|
690
|
+
}
|
|
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
|
+
function underlineXml(underline) {
|
|
694
|
+
if (underline === undefined || underline === false || underline === 'none')
|
|
695
|
+
return '';
|
|
696
|
+
if (underline === true || underline === 'single')
|
|
697
|
+
return '<u/>';
|
|
698
|
+
return `<u val="${checkedToken(underline, isNamedUnderlineStyle, 'underline style')}"/>`;
|
|
699
|
+
}
|
|
700
|
+
function numberAttr(value) {
|
|
701
|
+
if (!Number.isFinite(value)) {
|
|
702
|
+
throw new AuthoringError(`cannot serialise a non-finite font metric (${value})`);
|
|
703
|
+
}
|
|
704
|
+
return String(value);
|
|
705
|
+
}
|
|
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
|
+
function borderXml(border) {
|
|
710
|
+
const attrs = (border.diagonalUp ? ' diagonalUp="1"' : '') + (border.diagonalDown ? ' diagonalDown="1"' : '');
|
|
711
|
+
return (`<border${attrs}>` +
|
|
712
|
+
edgeXml('left', border.left) +
|
|
713
|
+
edgeXml('right', border.right) +
|
|
714
|
+
edgeXml('top', border.top) +
|
|
715
|
+
edgeXml('bottom', border.bottom) +
|
|
716
|
+
edgeXml('diagonal', border.diagonal) +
|
|
717
|
+
'</border>');
|
|
718
|
+
}
|
|
719
|
+
// One border edge: a styleless self-closing tag when absent, else the style attribute plus an
|
|
720
|
+
// optional colour child.
|
|
721
|
+
function edgeXml(tag, edge) {
|
|
722
|
+
if (edge === undefined)
|
|
723
|
+
return `<${tag}/>`;
|
|
724
|
+
const style = checkedToken(edge.style, isBorderStyle, 'border style');
|
|
725
|
+
if (edge.color === undefined)
|
|
726
|
+
return `<${tag} style="${style}"/>`;
|
|
727
|
+
return `<${tag} style="${style}"><color ${colorAttrs(edge.color)}/></${tag}>`;
|
|
728
|
+
}
|
|
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
|
+
function escapeFormatCode(code) {
|
|
735
|
+
return code
|
|
736
|
+
.replace(/&/g, '&')
|
|
737
|
+
.replace(/</g, '<')
|
|
738
|
+
.replace(/>/g, '>')
|
|
739
|
+
.replace(/"/g, '"');
|
|
740
|
+
}
|
|
741
|
+
// A stable, collision-free key for a fill: identical fills share it, distinct ones don't.
|
|
742
|
+
function fillSignature(fill) {
|
|
743
|
+
if (fill.type === 'gradient') {
|
|
744
|
+
const stops = fill.stops.map((s) => `${s.position}:${colorSignature(s.color)}`).join(',');
|
|
745
|
+
return `grad|${fill.gradient}|${fill.degree ?? ''}|${fill.left ?? ''}/${fill.right ?? ''}/${fill.top ?? ''}/${fill.bottom ?? ''}|${stops}`;
|
|
746
|
+
}
|
|
747
|
+
return `${fill.pattern}|${colorSignature(fill.fgColor)}|${colorSignature(fill.bgColor)}`;
|
|
748
|
+
}
|
|
749
|
+
function colorSignature(color) {
|
|
750
|
+
if (color === undefined)
|
|
751
|
+
return '';
|
|
752
|
+
return `${color.argb ?? ''}/${color.theme ?? ''}/${color.tint ?? ''}/${color.indexed ?? ''}`;
|
|
753
|
+
}
|
|
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
|
+
function patternFillXml(fill, { solidBgFallback }) {
|
|
759
|
+
if (fill.type === 'gradient')
|
|
760
|
+
return `<fill>${gradientFillXml(fill)}</fill>`;
|
|
761
|
+
const fg = fill.fgColor ? `<fgColor ${colorAttrs(fill.fgColor)}/>` : '';
|
|
762
|
+
const bg = fill.bgColor
|
|
763
|
+
? `<bgColor ${colorAttrs(fill.bgColor)}/>`
|
|
764
|
+
: solidBgFallback && fill.pattern === 'solid'
|
|
765
|
+
? '<bgColor indexed="64"/>'
|
|
766
|
+
: '';
|
|
767
|
+
const pattern = checkedToken(fill.pattern, isFillPatternType, 'fill pattern');
|
|
768
|
+
return `<fill><patternFill patternType="${pattern}">${fg}${bg}</patternFill></fill>`;
|
|
769
|
+
}
|
|
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
|
+
}
|