@office-kit/xlsx 0.8.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 +21 -0
- package/README.md +319 -0
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/dist/cell/cell.d.ts +234 -0
- package/dist/cell/index.d.ts +4 -0
- package/dist/cell/rich-text.d.ts +37 -0
- package/dist/cell-D9CaNKnU.mjs +320 -0
- package/dist/cell-D9CaNKnU.mjs.map +1 -0
- package/dist/cell-style-BEDjMX1y.mjs +1579 -0
- package/dist/cell-style-BEDjMX1y.mjs.map +1 -0
- package/dist/cell.mjs +2 -0
- package/dist/chart/chart-xml.d.ts +16 -0
- package/dist/chart/chart.d.ts +735 -0
- package/dist/chart/cx/chartex-xml.d.ts +6 -0
- package/dist/chart/cx/chartex.d.ts +279 -0
- package/dist/chart/index.d.ts +6 -0
- package/dist/chart/user-shapes-xml.d.ts +4 -0
- package/dist/chart/user-shapes.d.ts +61 -0
- package/dist/chart.mjs +232 -0
- package/dist/chart.mjs.map +1 -0
- package/dist/chartsheet/chartsheet-xml.d.ts +17 -0
- package/dist/chartsheet/chartsheet.d.ts +121 -0
- package/dist/chartsheet/index.d.ts +2 -0
- package/dist/chartsheet-C3-tqkPy.mjs +23 -0
- package/dist/chartsheet-C3-tqkPy.mjs.map +1 -0
- package/dist/chartsheet.mjs +2 -0
- package/dist/colors-ovWAwnZI.mjs +67 -0
- package/dist/colors-ovWAwnZI.mjs.map +1 -0
- package/dist/compat/numbers.d.ts +14 -0
- package/dist/coordinate-96Ecci4d.mjs +276 -0
- package/dist/coordinate-96Ecci4d.mjs.map +1 -0
- package/dist/datetime-B2ySVlXt.mjs +71 -0
- package/dist/datetime-B2ySVlXt.mjs.map +1 -0
- package/dist/defined-names-CviWmtQg.mjs +89 -0
- package/dist/defined-names-CviWmtQg.mjs.map +1 -0
- package/dist/differential-D4dg-qtZ.mjs +37 -0
- package/dist/differential-D4dg-qtZ.mjs.map +1 -0
- package/dist/drawing/anchor.d.ts +63 -0
- package/dist/drawing/dml/colors.d.ts +109 -0
- package/dist/drawing/dml/dml-xml.d.ts +35 -0
- package/dist/drawing/dml/effect.d.ts +92 -0
- package/dist/drawing/dml/fill.d.ts +115 -0
- package/dist/drawing/dml/geometry.d.ts +113 -0
- package/dist/drawing/dml/line.d.ts +41 -0
- package/dist/drawing/dml/shape-properties.d.ts +33 -0
- package/dist/drawing/dml/text.d.ts +218 -0
- package/dist/drawing/drawing-xml.d.ts +5 -0
- package/dist/drawing/drawing.d.ts +117 -0
- package/dist/drawing/image.d.ts +40 -0
- package/dist/drawing/index.d.ts +14 -0
- package/dist/drawing-BxzLuryn.mjs +415 -0
- package/dist/drawing-BxzLuryn.mjs.map +1 -0
- package/dist/drawing.mjs +119 -0
- package/dist/drawing.mjs.map +1 -0
- package/dist/escape-DFTE7ZJc.mjs +51 -0
- package/dist/escape-DFTE7ZJc.mjs.map +1 -0
- package/dist/exceptions-D-CFwxgm.mjs +37 -0
- package/dist/exceptions-D-CFwxgm.mjs.map +1 -0
- package/dist/formula/tokenizer.d.ts +61 -0
- package/dist/formula/translate.d.ts +67 -0
- package/dist/inference-B3ES3KEJ.mjs +42 -0
- package/dist/inference-B3ES3KEJ.mjs.map +1 -0
- package/dist/io/browser.d.ts +41 -0
- package/dist/io/index.d.ts +7 -0
- package/dist/io/load.d.ts +46 -0
- package/dist/io/node-fs.d.ts +62 -0
- package/dist/io/node-save.d.ts +3 -0
- package/dist/io/node.d.ts +17 -0
- package/dist/io/save.d.ts +14 -0
- package/dist/io/sink.d.ts +54 -0
- package/dist/io/source.d.ts +14 -0
- package/dist/io.mjs +212 -0
- package/dist/io.mjs.map +1 -0
- package/dist/load-D5cbhoGx.mjs +1069 -0
- package/dist/load-D5cbhoGx.mjs.map +1 -0
- package/dist/manifest-Dps1-OpP.mjs +801 -0
- package/dist/manifest-Dps1-OpP.mjs.map +1 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.mjs +308 -0
- package/dist/node.mjs.map +1 -0
- package/dist/packaging/core.d.ts +45 -0
- package/dist/packaging/custom.d.ts +62 -0
- package/dist/packaging/extended.d.ts +45 -0
- package/dist/packaging/index.d.ts +10 -0
- package/dist/packaging/manifest.d.ts +24 -0
- package/dist/packaging/relationships.d.ts +30 -0
- package/dist/packaging.mjs +2 -0
- package/dist/parser-DuLejQy1.mjs +156 -0
- package/dist/parser-DuLejQy1.mjs.map +1 -0
- package/dist/reader-D1fNW9k1.mjs +534 -0
- package/dist/reader-D1fNW9k1.mjs.map +1 -0
- package/dist/save-RohQtgEZ.mjs +745 -0
- package/dist/save-RohQtgEZ.mjs.map +1 -0
- package/dist/schema/core.d.ts +133 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/serialize.d.ts +6 -0
- package/dist/schema.mjs +2 -0
- package/dist/serialize-55EnT30e.mjs +254 -0
- package/dist/serialize-55EnT30e.mjs.map +1 -0
- package/dist/serializer-BwbgHYJV.mjs +116 -0
- package/dist/serializer-BwbgHYJV.mjs.map +1 -0
- package/dist/streaming/index.d.ts +2 -0
- package/dist/streaming/read-only.d.ts +38 -0
- package/dist/streaming/write-only.d.ts +47 -0
- package/dist/streaming.mjs +612 -0
- package/dist/streaming.mjs.map +1 -0
- package/dist/styles/alignment.d.ts +33 -0
- package/dist/styles/alignment.schema.d.ts +3 -0
- package/dist/styles/borders.d.ts +40 -0
- package/dist/styles/borders.schema.d.ts +4 -0
- package/dist/styles/cell-style.d.ts +270 -0
- package/dist/styles/colors.d.ts +128 -0
- package/dist/styles/colors.schema.d.ts +3 -0
- package/dist/styles/differential.d.ts +41 -0
- package/dist/styles/fills.d.ts +54 -0
- package/dist/styles/fills.schema.d.ts +6 -0
- package/dist/styles/fonts.d.ts +44 -0
- package/dist/styles/fonts.schema.d.ts +3 -0
- package/dist/styles/index.d.ts +21 -0
- package/dist/styles/named-styles.d.ts +52 -0
- package/dist/styles/numbers.d.ts +39 -0
- package/dist/styles/numbers.schema.d.ts +3 -0
- package/dist/styles/protection.d.ts +9 -0
- package/dist/styles/protection.schema.d.ts +3 -0
- package/dist/styles/stylesheet-reader.d.ts +7 -0
- package/dist/styles/stylesheet-writer.d.ts +3 -0
- package/dist/styles/stylesheet.d.ts +95 -0
- package/dist/styles.mjs +4 -0
- package/dist/stylesheet-writer-C2eRmn22.mjs +8624 -0
- package/dist/stylesheet-writer-C2eRmn22.mjs.map +1 -0
- package/dist/table-DkX6UniA.mjs +113 -0
- package/dist/table-DkX6UniA.mjs.map +1 -0
- package/dist/tree-Bbs1C8Rc.mjs +192 -0
- package/dist/tree-Bbs1C8Rc.mjs.map +1 -0
- package/dist/units-rOMQqXh2.mjs +41 -0
- package/dist/units-rOMQqXh2.mjs.map +1 -0
- package/dist/user-shapes-DfmCGKB0.mjs +252 -0
- package/dist/user-shapes-DfmCGKB0.mjs.map +1 -0
- package/dist/utf8-D91g1XTG.mjs +143 -0
- package/dist/utf8-D91g1XTG.mjs.map +1 -0
- package/dist/utils/coordinate.d.ts +103 -0
- package/dist/utils/css.d.ts +18 -0
- package/dist/utils/datetime.d.ts +38 -0
- package/dist/utils/escape.d.ts +34 -0
- package/dist/utils/exceptions.d.ts +34 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/inference.d.ts +24 -0
- package/dist/utils/stable-stringify.d.ts +7 -0
- package/dist/utils/units.d.ts +14 -0
- package/dist/utils/utf8.d.ts +1 -0
- package/dist/utils.mjs +39 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/workbook/calc-properties.d.ts +47 -0
- package/dist/workbook/defined-names.d.ts +121 -0
- package/dist/workbook/file-recovery.d.ts +11 -0
- package/dist/workbook/file-sharing.d.ts +14 -0
- package/dist/workbook/file-version.d.ts +13 -0
- package/dist/workbook/function-groups.d.ts +10 -0
- package/dist/workbook/index.d.ts +24 -0
- package/dist/workbook/protection.d.ts +35 -0
- package/dist/workbook/shared-strings.d.ts +57 -0
- package/dist/workbook/smart-tags.d.ts +13 -0
- package/dist/workbook/views.d.ts +89 -0
- package/dist/workbook/workbook-properties.d.ts +57 -0
- package/dist/workbook/workbook.d.ts +643 -0
- package/dist/workbook-HGYNRBlV.mjs +636 -0
- package/dist/workbook-HGYNRBlV.mjs.map +1 -0
- package/dist/workbook.mjs +58 -0
- package/dist/workbook.mjs.map +1 -0
- package/dist/worksheet/auto-filter.d.ts +34 -0
- package/dist/worksheet/cell-range.d.ts +121 -0
- package/dist/worksheet/comments-xml.d.ts +24 -0
- package/dist/worksheet/comments.d.ts +13 -0
- package/dist/worksheet/conditional-formatting.d.ts +150 -0
- package/dist/worksheet/custom-sheet-views.d.ts +43 -0
- package/dist/worksheet/data-consolidate.d.ts +29 -0
- package/dist/worksheet/data-validations.d.ts +72 -0
- package/dist/worksheet/dimensions.d.ts +40 -0
- package/dist/worksheet/errors.d.ts +40 -0
- package/dist/worksheet/hyperlinks.d.ts +42 -0
- package/dist/worksheet/index.d.ts +46 -0
- package/dist/worksheet/ole-objects.d.ts +37 -0
- package/dist/worksheet/page-setup.d.ts +173 -0
- package/dist/worksheet/phonetic.d.ts +11 -0
- package/dist/worksheet/properties.d.ts +34 -0
- package/dist/worksheet/protected-ranges.d.ts +19 -0
- package/dist/worksheet/protection.d.ts +44 -0
- package/dist/worksheet/reader.d.ts +38 -0
- package/dist/worksheet/scenarios.d.ts +36 -0
- package/dist/worksheet/smart-tags.d.ts +23 -0
- package/dist/worksheet/sort-state.d.ts +28 -0
- package/dist/worksheet/table-xml.d.ts +5 -0
- package/dist/worksheet/table.d.ts +80 -0
- package/dist/worksheet/views.d.ts +47 -0
- package/dist/worksheet/web-publish.d.ts +21 -0
- package/dist/worksheet/worksheet.d.ts +935 -0
- package/dist/worksheet/writer.d.ts +72 -0
- package/dist/worksheet-CmCNoIgD.mjs +1726 -0
- package/dist/worksheet-CmCNoIgD.mjs.map +1 -0
- package/dist/worksheet.mjs +247 -0
- package/dist/worksheet.mjs.map +1 -0
- package/dist/writer-DspzfkNA.mjs +221 -0
- package/dist/writer-DspzfkNA.mjs.map +1 -0
- package/dist/xml/index.d.ts +10 -0
- package/dist/xml/iterparse.d.ts +22 -0
- package/dist/xml/namespaces.d.ts +91 -0
- package/dist/xml/parser.d.ts +7 -0
- package/dist/xml/serializer.d.ts +14 -0
- package/dist/xml/stream-writer.d.ts +39 -0
- package/dist/xml/tree.d.ts +37 -0
- package/dist/xml.mjs +140 -0
- package/dist/xml.mjs.map +1 -0
- package/dist/zip/decompression-guard.d.ts +70 -0
- package/dist/zip/index.d.ts +6 -0
- package/dist/zip/random-access-reader.d.ts +16 -0
- package/dist/zip/reader.d.ts +45 -0
- package/dist/zip/writer.d.ts +65 -0
- package/dist/zip/zip64-patch.d.ts +12 -0
- package/dist/zip.mjs +3 -0
- package/package.json +147 -0
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
import { o as OpenXmlSchemaError } from "./exceptions-D-CFwxgm.mjs";
|
|
2
|
+
import { Cn as parseRange, bt as setCell, ln as makeColor, nn as colorToHex } from "./worksheet-CmCNoIgD.mjs";
|
|
3
|
+
//#region src/utils/stable-stringify.ts
|
|
4
|
+
const sortKeysReplacer = (_key, value) => {
|
|
5
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
6
|
+
const obj = value;
|
|
7
|
+
const keys = Object.keys(obj).sort();
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const k of keys) out[k] = obj[k];
|
|
10
|
+
return out;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Stringify `value` with object keys sorted recursively. Equal logical
|
|
14
|
+
* values produce the same string regardless of insertion order; arrays
|
|
15
|
+
* preserve their element order. Circular references throw (the error
|
|
16
|
+
* comes straight from JSON.stringify's stack-overflow check).
|
|
17
|
+
*/
|
|
18
|
+
function stableStringify(value) {
|
|
19
|
+
return JSON.stringify(value, sortKeysReplacer);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/styles/alignment.ts
|
|
23
|
+
const HORIZONTAL_ALIGNMENTS = Object.freeze([
|
|
24
|
+
"general",
|
|
25
|
+
"left",
|
|
26
|
+
"center",
|
|
27
|
+
"right",
|
|
28
|
+
"fill",
|
|
29
|
+
"justify",
|
|
30
|
+
"centerContinuous",
|
|
31
|
+
"distributed"
|
|
32
|
+
]);
|
|
33
|
+
const VERTICAL_ALIGNMENTS = Object.freeze([
|
|
34
|
+
"top",
|
|
35
|
+
"center",
|
|
36
|
+
"bottom",
|
|
37
|
+
"justify",
|
|
38
|
+
"distributed"
|
|
39
|
+
]);
|
|
40
|
+
const HORIZONTAL_SET = new Set(HORIZONTAL_ALIGNMENTS);
|
|
41
|
+
const VERTICAL_SET = new Set(VERTICAL_ALIGNMENTS);
|
|
42
|
+
function makeAlignment(opts = {}) {
|
|
43
|
+
const out = {};
|
|
44
|
+
if (opts.horizontal !== void 0) {
|
|
45
|
+
if (!HORIZONTAL_SET.has(opts.horizontal)) throw new OpenXmlSchemaError(`Alignment horizontal must be one of [${HORIZONTAL_ALIGNMENTS.join(", ")}]; got "${opts.horizontal}"`);
|
|
46
|
+
out.horizontal = opts.horizontal;
|
|
47
|
+
}
|
|
48
|
+
if (opts.vertical !== void 0) {
|
|
49
|
+
if (!VERTICAL_SET.has(opts.vertical)) throw new OpenXmlSchemaError(`Alignment vertical must be one of [${VERTICAL_ALIGNMENTS.join(", ")}]; got "${opts.vertical}"`);
|
|
50
|
+
out.vertical = opts.vertical;
|
|
51
|
+
}
|
|
52
|
+
if (opts.textRotation !== void 0) {
|
|
53
|
+
const r = opts.textRotation;
|
|
54
|
+
if (!Number.isInteger(r) || r < 0 || r > 180 && r !== 255) throw new OpenXmlSchemaError(`Alignment textRotation must be 0..180 or 255; got ${r}`);
|
|
55
|
+
out.textRotation = r;
|
|
56
|
+
}
|
|
57
|
+
if (opts.wrapText !== void 0) out.wrapText = opts.wrapText;
|
|
58
|
+
if (opts.shrinkToFit !== void 0) out.shrinkToFit = opts.shrinkToFit;
|
|
59
|
+
if (opts.indent !== void 0) {
|
|
60
|
+
if (!Number.isFinite(opts.indent) || opts.indent < 0 || opts.indent > 255) throw new OpenXmlSchemaError(`Alignment indent must be 0..255; got ${opts.indent}`);
|
|
61
|
+
out.indent = opts.indent;
|
|
62
|
+
}
|
|
63
|
+
if (opts.relativeIndent !== void 0) {
|
|
64
|
+
const r = opts.relativeIndent;
|
|
65
|
+
if (!Number.isFinite(r) || r < -255 || r > 255) throw new OpenXmlSchemaError(`Alignment relativeIndent must be -255..255; got ${r}`);
|
|
66
|
+
out.relativeIndent = r;
|
|
67
|
+
}
|
|
68
|
+
if (opts.justifyLastLine !== void 0) out.justifyLastLine = opts.justifyLastLine;
|
|
69
|
+
if (opts.readingOrder !== void 0) {
|
|
70
|
+
if (!Number.isFinite(opts.readingOrder) || opts.readingOrder < 0) throw new OpenXmlSchemaError(`Alignment readingOrder must be >= 0; got ${opts.readingOrder}`);
|
|
71
|
+
out.readingOrder = opts.readingOrder;
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze(out);
|
|
74
|
+
}
|
|
75
|
+
makeAlignment();
|
|
76
|
+
const HORIZONTAL_TO_TEXT_ALIGN = {
|
|
77
|
+
general: void 0,
|
|
78
|
+
left: "left",
|
|
79
|
+
center: "center",
|
|
80
|
+
right: "right",
|
|
81
|
+
fill: "left",
|
|
82
|
+
justify: "justify",
|
|
83
|
+
centerContinuous: "center",
|
|
84
|
+
distributed: "justify"
|
|
85
|
+
};
|
|
86
|
+
const VERTICAL_TO_VERTICAL_ALIGN = {
|
|
87
|
+
top: "top",
|
|
88
|
+
center: "middle",
|
|
89
|
+
bottom: "bottom",
|
|
90
|
+
justify: "middle",
|
|
91
|
+
distributed: "middle"
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Translate an {@link Alignment} to a CSS-property record suitable for
|
|
95
|
+
* HTML preview. `horizontal` → `text-align`, `vertical` →
|
|
96
|
+
* `vertical-align` (table-cell semantics), `wrapText` → `white-space:
|
|
97
|
+
* pre-wrap`, `textRotation` → `transform: rotate(<-deg>)` (Excel rotates
|
|
98
|
+
* counter-clockwise relative to CSS) plus `transform-origin` to keep
|
|
99
|
+
* the text anchored, and `indent` → `padding-left: <n>em`. `255`
|
|
100
|
+
* stacked-text rotation maps to a 180° flip with `writing-mode`.
|
|
101
|
+
*
|
|
102
|
+
* Empty / undefined Alignment returns `{}`.
|
|
103
|
+
*/
|
|
104
|
+
function alignmentToCss(alignment) {
|
|
105
|
+
const css = {};
|
|
106
|
+
if (!alignment) return css;
|
|
107
|
+
if (alignment.horizontal !== void 0) {
|
|
108
|
+
const ta = HORIZONTAL_TO_TEXT_ALIGN[alignment.horizontal];
|
|
109
|
+
if (ta !== void 0) css["text-align"] = ta;
|
|
110
|
+
}
|
|
111
|
+
if (alignment.vertical !== void 0) {
|
|
112
|
+
const va = VERTICAL_TO_VERTICAL_ALIGN[alignment.vertical];
|
|
113
|
+
if (va !== void 0) css["vertical-align"] = va;
|
|
114
|
+
}
|
|
115
|
+
if (alignment.wrapText) css["white-space"] = "pre-wrap";
|
|
116
|
+
if (alignment.textRotation !== void 0) {
|
|
117
|
+
if (alignment.textRotation === 255) css["writing-mode"] = "vertical-rl";
|
|
118
|
+
else if (alignment.textRotation !== 0) {
|
|
119
|
+
css["transform"] = `rotate(-${alignment.textRotation}deg)`;
|
|
120
|
+
css["transform-origin"] = "center center";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (alignment.indent !== void 0 && alignment.indent > 0) css["padding-left"] = `${alignment.indent}em`;
|
|
124
|
+
return css;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/styles/borders.ts
|
|
128
|
+
const SIDE_STYLES = Object.freeze([
|
|
129
|
+
"thin",
|
|
130
|
+
"medium",
|
|
131
|
+
"thick",
|
|
132
|
+
"double",
|
|
133
|
+
"hair",
|
|
134
|
+
"dotted",
|
|
135
|
+
"dashed",
|
|
136
|
+
"dashDot",
|
|
137
|
+
"dashDotDot",
|
|
138
|
+
"mediumDashed",
|
|
139
|
+
"mediumDashDot",
|
|
140
|
+
"mediumDashDotDot",
|
|
141
|
+
"slantDashDot"
|
|
142
|
+
]);
|
|
143
|
+
const SIDE_STYLE_SET = new Set(SIDE_STYLES);
|
|
144
|
+
/** Build an immutable {@link Side}. */
|
|
145
|
+
function makeSide(opts = {}) {
|
|
146
|
+
const out = {};
|
|
147
|
+
if (opts.style !== void 0) {
|
|
148
|
+
if (!SIDE_STYLE_SET.has(opts.style)) throw new OpenXmlSchemaError(`Side style must be one of [${SIDE_STYLES.join(", ")}]; got "${opts.style}"`);
|
|
149
|
+
out.style = opts.style;
|
|
150
|
+
}
|
|
151
|
+
if (opts.color !== void 0) out.color = Object.isFrozen(opts.color) ? opts.color : makeColor(opts.color);
|
|
152
|
+
return Object.freeze(out);
|
|
153
|
+
}
|
|
154
|
+
/** Build an immutable {@link Border}. */
|
|
155
|
+
function makeBorder(opts = {}) {
|
|
156
|
+
const out = {};
|
|
157
|
+
if (opts.left !== void 0) out.left = freezeSide(opts.left);
|
|
158
|
+
if (opts.right !== void 0) out.right = freezeSide(opts.right);
|
|
159
|
+
if (opts.top !== void 0) out.top = freezeSide(opts.top);
|
|
160
|
+
if (opts.bottom !== void 0) out.bottom = freezeSide(opts.bottom);
|
|
161
|
+
if (opts.diagonal !== void 0) out.diagonal = freezeSide(opts.diagonal);
|
|
162
|
+
if (opts.vertical !== void 0) out.vertical = freezeSide(opts.vertical);
|
|
163
|
+
if (opts.horizontal !== void 0) out.horizontal = freezeSide(opts.horizontal);
|
|
164
|
+
if (opts.diagonalUp !== void 0) out.diagonalUp = opts.diagonalUp;
|
|
165
|
+
if (opts.diagonalDown !== void 0) out.diagonalDown = opts.diagonalDown;
|
|
166
|
+
if (opts.outline !== void 0) out.outline = opts.outline;
|
|
167
|
+
return Object.freeze(out);
|
|
168
|
+
}
|
|
169
|
+
const freezeSide = (s) => Object.isFrozen(s) ? s : makeSide(s);
|
|
170
|
+
makeSide();
|
|
171
|
+
/** Default empty border — every cell starts here until styled otherwise. */
|
|
172
|
+
const DEFAULT_BORDER = makeBorder();
|
|
173
|
+
/**
|
|
174
|
+
* Map an Excel {@link SideStyle} to a CSS `border` shorthand fragment (`<width>
|
|
175
|
+
* <style>`). Returns `undefined` for unmappable styles or a missing/no-style
|
|
176
|
+
* side. Colour is appended by the caller.
|
|
177
|
+
*/
|
|
178
|
+
function sideStyleToCss(style) {
|
|
179
|
+
switch (style) {
|
|
180
|
+
case "thin":
|
|
181
|
+
case "hair": return "1px solid";
|
|
182
|
+
case "medium": return "2px solid";
|
|
183
|
+
case "thick": return "3px solid";
|
|
184
|
+
case "double": return "3px double";
|
|
185
|
+
case "dotted": return "1px dotted";
|
|
186
|
+
case "dashed":
|
|
187
|
+
case "dashDot":
|
|
188
|
+
case "dashDotDot": return "1px dashed";
|
|
189
|
+
case "mediumDashed":
|
|
190
|
+
case "mediumDashDot":
|
|
191
|
+
case "mediumDashDotDot":
|
|
192
|
+
case "slantDashDot": return "2px dashed";
|
|
193
|
+
default: return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Translate a {@link Border} to a CSS-property record suitable for HTML
|
|
198
|
+
* preview. Each present side becomes `border-<edge>: <width> <style> <#color>`.
|
|
199
|
+
* Theme/auto/missing colours fall back to `currentColor`. Diagonal / vertical /
|
|
200
|
+
* horizontal sides are skipped (CSS has no native equivalent for in-cell
|
|
201
|
+
* strokes). Empty Border returns `{}`.
|
|
202
|
+
*/
|
|
203
|
+
function borderToCss(border) {
|
|
204
|
+
const css = {};
|
|
205
|
+
if (!border) return css;
|
|
206
|
+
const sides = [
|
|
207
|
+
["top", border.top],
|
|
208
|
+
["right", border.right],
|
|
209
|
+
["bottom", border.bottom],
|
|
210
|
+
["left", border.left]
|
|
211
|
+
];
|
|
212
|
+
for (const [edge, side] of sides) {
|
|
213
|
+
if (!side) continue;
|
|
214
|
+
const stroke = sideStyleToCss(side.style);
|
|
215
|
+
if (stroke === void 0) continue;
|
|
216
|
+
const argb = colorToHex(side.color);
|
|
217
|
+
const colour = argb !== void 0 ? `#${argb.slice(2)}` : "currentColor";
|
|
218
|
+
css[`border-${edge}`] = `${stroke} ${colour}`;
|
|
219
|
+
}
|
|
220
|
+
return css;
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/styles/fills.ts
|
|
224
|
+
const PATTERN_TYPES = Object.freeze([
|
|
225
|
+
"none",
|
|
226
|
+
"solid",
|
|
227
|
+
"darkDown",
|
|
228
|
+
"darkGray",
|
|
229
|
+
"darkGrid",
|
|
230
|
+
"darkHorizontal",
|
|
231
|
+
"darkTrellis",
|
|
232
|
+
"darkUp",
|
|
233
|
+
"darkVertical",
|
|
234
|
+
"gray0625",
|
|
235
|
+
"gray125",
|
|
236
|
+
"lightDown",
|
|
237
|
+
"lightGray",
|
|
238
|
+
"lightGrid",
|
|
239
|
+
"lightHorizontal",
|
|
240
|
+
"lightTrellis",
|
|
241
|
+
"lightUp",
|
|
242
|
+
"lightVertical",
|
|
243
|
+
"mediumGray"
|
|
244
|
+
]);
|
|
245
|
+
const PATTERN_TYPE_SET = new Set(PATTERN_TYPES);
|
|
246
|
+
function makePatternFill(opts = {}) {
|
|
247
|
+
const out = { kind: "pattern" };
|
|
248
|
+
if (opts.patternType !== void 0) {
|
|
249
|
+
if (!PATTERN_TYPE_SET.has(opts.patternType)) throw new OpenXmlSchemaError(`PatternFill patternType must be one of [${PATTERN_TYPES.join(", ")}]; got "${opts.patternType}"`);
|
|
250
|
+
out.patternType = opts.patternType;
|
|
251
|
+
}
|
|
252
|
+
if (opts.fgColor !== void 0) out.fgColor = freezeColor(opts.fgColor);
|
|
253
|
+
if (opts.bgColor !== void 0) out.bgColor = freezeColor(opts.bgColor);
|
|
254
|
+
return Object.freeze(out);
|
|
255
|
+
}
|
|
256
|
+
function makeGradientStop(position, color) {
|
|
257
|
+
if (!Number.isFinite(position) || position < 0 || position > 1) throw new OpenXmlSchemaError(`GradientStop position must be in [0, 1]; got ${position}`);
|
|
258
|
+
return Object.freeze({
|
|
259
|
+
position,
|
|
260
|
+
color: freezeColor(color)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function makeGradientFill(opts = {}) {
|
|
264
|
+
const out = {
|
|
265
|
+
kind: "gradient",
|
|
266
|
+
type: opts.type ?? "linear",
|
|
267
|
+
stops: opts.stops?.map((s) => freezeStop(s)) ?? []
|
|
268
|
+
};
|
|
269
|
+
if (out.type !== "linear" && out.type !== "path") throw new OpenXmlSchemaError(`GradientFill type must be "linear" or "path"; got "${String(out.type)}"`);
|
|
270
|
+
if (opts.degree !== void 0) out.degree = opts.degree;
|
|
271
|
+
if (opts.left !== void 0) out.left = opts.left;
|
|
272
|
+
if (opts.right !== void 0) out.right = opts.right;
|
|
273
|
+
if (opts.top !== void 0) out.top = opts.top;
|
|
274
|
+
if (opts.bottom !== void 0) out.bottom = opts.bottom;
|
|
275
|
+
Object.freeze(out.stops);
|
|
276
|
+
return Object.freeze(out);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Single-arg constructor that defers to the variant-specific maker based on
|
|
280
|
+
* `kind`. Useful when the caller has a plain object in hand and wants the
|
|
281
|
+
* freeze invariant applied uniformly.
|
|
282
|
+
*/
|
|
283
|
+
function makeFill(opts) {
|
|
284
|
+
if (opts.kind === "gradient") return makeGradientFill(opts);
|
|
285
|
+
return makePatternFill(opts);
|
|
286
|
+
}
|
|
287
|
+
/** The empty PatternFill — Excel's default cellXf[0] points here. */
|
|
288
|
+
const DEFAULT_EMPTY_FILL = makePatternFill();
|
|
289
|
+
/** The 'gray125' PatternFill — Excel's default cellXf[1]. */
|
|
290
|
+
const DEFAULT_GRAY_FILL = makePatternFill({ patternType: "gray125" });
|
|
291
|
+
const freezeColor = (c) => Object.isFrozen(c) ? c : makeColor(c);
|
|
292
|
+
const freezeStop = (s) => Object.isFrozen(s) ? s : makeGradientStop(s.position, s.color);
|
|
293
|
+
const argbToCssHex = (color) => {
|
|
294
|
+
const argb = colorToHex(color);
|
|
295
|
+
return argb ? `#${argb.slice(2)}` : void 0;
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Translate a {@link Fill} to a CSS-property record suitable for HTML preview.
|
|
299
|
+
* `'solid'` PatternFill renders as `background-color`, other pattern types
|
|
300
|
+
* collapse to bgColor (CSS has no built-in equivalent of Excel hatch patterns).
|
|
301
|
+
* GradientFill emits a CSS `background-image` with `linear-gradient(<angle>,
|
|
302
|
+
* …)` for `type='linear'` or `radial-gradient(circle, …)` for `type='path'`.
|
|
303
|
+
*
|
|
304
|
+
* theme/auto colours and unresolvable inputs are skipped (returns `{}`) so
|
|
305
|
+
* callers can spread without overwriting upstream defaults.
|
|
306
|
+
*/
|
|
307
|
+
function fillToCss(fill) {
|
|
308
|
+
const css = {};
|
|
309
|
+
if (!fill) return css;
|
|
310
|
+
if (fill.kind === "pattern") {
|
|
311
|
+
if (fill.patternType === "none" || fill.patternType === void 0) return css;
|
|
312
|
+
if (fill.patternType === "solid") {
|
|
313
|
+
const fg = argbToCssHex(fill.fgColor);
|
|
314
|
+
if (fg !== void 0) css["background-color"] = fg;
|
|
315
|
+
return css;
|
|
316
|
+
}
|
|
317
|
+
const bg = argbToCssHex(fill.bgColor) ?? argbToCssHex(fill.fgColor);
|
|
318
|
+
if (bg !== void 0) css["background-color"] = bg;
|
|
319
|
+
return css;
|
|
320
|
+
}
|
|
321
|
+
const stopParts = [];
|
|
322
|
+
for (const s of fill.stops) {
|
|
323
|
+
const hex = argbToCssHex(s.color);
|
|
324
|
+
if (hex === void 0) continue;
|
|
325
|
+
stopParts.push(`${hex} ${(s.position * 100).toFixed(2)}%`);
|
|
326
|
+
}
|
|
327
|
+
if (stopParts.length === 0) return css;
|
|
328
|
+
if (fill.type === "linear") css["background-image"] = `linear-gradient(${fill.degree ?? 0}deg, ${stopParts.join(", ")})`;
|
|
329
|
+
else css["background-image"] = `radial-gradient(circle, ${stopParts.join(", ")})`;
|
|
330
|
+
return css;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/styles/fonts.ts
|
|
334
|
+
const UNDERLINE_STYLES = Object.freeze([
|
|
335
|
+
"single",
|
|
336
|
+
"double",
|
|
337
|
+
"singleAccounting",
|
|
338
|
+
"doubleAccounting"
|
|
339
|
+
]);
|
|
340
|
+
const VERT_ALIGNS = Object.freeze([
|
|
341
|
+
"baseline",
|
|
342
|
+
"superscript",
|
|
343
|
+
"subscript"
|
|
344
|
+
]);
|
|
345
|
+
const FONT_SCHEMES = Object.freeze(["major", "minor"]);
|
|
346
|
+
const UNDERLINE_SET = new Set(UNDERLINE_STYLES);
|
|
347
|
+
const VERT_SET = new Set(VERT_ALIGNS);
|
|
348
|
+
const SCHEME_SET = new Set(FONT_SCHEMES);
|
|
349
|
+
function makeFont(opts = {}) {
|
|
350
|
+
const out = {};
|
|
351
|
+
if (opts.name !== void 0) {
|
|
352
|
+
if (typeof opts.name !== "string") throw new OpenXmlSchemaError(`Font name must be a string; got ${typeof opts.name}`);
|
|
353
|
+
out.name = opts.name;
|
|
354
|
+
}
|
|
355
|
+
if (opts.charset !== void 0) {
|
|
356
|
+
if (!Number.isInteger(opts.charset)) throw new OpenXmlSchemaError(`Font charset must be an integer; got ${opts.charset}`);
|
|
357
|
+
out.charset = opts.charset;
|
|
358
|
+
}
|
|
359
|
+
if (opts.family !== void 0) {
|
|
360
|
+
if (!Number.isInteger(opts.family) || opts.family < 0 || opts.family > 14) throw new OpenXmlSchemaError(`Font family must be 0..14; got ${opts.family}`);
|
|
361
|
+
out.family = opts.family;
|
|
362
|
+
}
|
|
363
|
+
if (opts.size !== void 0) {
|
|
364
|
+
if (!Number.isFinite(opts.size) || opts.size <= 0) throw new OpenXmlSchemaError(`Font size must be positive; got ${opts.size}`);
|
|
365
|
+
out.size = opts.size;
|
|
366
|
+
}
|
|
367
|
+
if (opts.color !== void 0) out.color = Object.isFrozen(opts.color) ? opts.color : makeColor(opts.color);
|
|
368
|
+
if (opts.bold !== void 0) out.bold = opts.bold;
|
|
369
|
+
if (opts.italic !== void 0) out.italic = opts.italic;
|
|
370
|
+
if (opts.strike !== void 0) out.strike = opts.strike;
|
|
371
|
+
if (opts.outline !== void 0) out.outline = opts.outline;
|
|
372
|
+
if (opts.shadow !== void 0) out.shadow = opts.shadow;
|
|
373
|
+
if (opts.condense !== void 0) out.condense = opts.condense;
|
|
374
|
+
if (opts.extend !== void 0) out.extend = opts.extend;
|
|
375
|
+
if (opts.underline !== void 0) {
|
|
376
|
+
if (!UNDERLINE_SET.has(opts.underline)) throw new OpenXmlSchemaError(`Font underline must be one of [${UNDERLINE_STYLES.join(", ")}]; got "${opts.underline}"`);
|
|
377
|
+
out.underline = opts.underline;
|
|
378
|
+
}
|
|
379
|
+
if (opts.vertAlign !== void 0) {
|
|
380
|
+
if (!VERT_SET.has(opts.vertAlign)) throw new OpenXmlSchemaError(`Font vertAlign must be one of [${VERT_ALIGNS.join(", ")}]; got "${opts.vertAlign}"`);
|
|
381
|
+
out.vertAlign = opts.vertAlign;
|
|
382
|
+
}
|
|
383
|
+
if (opts.scheme !== void 0) {
|
|
384
|
+
if (!SCHEME_SET.has(opts.scheme)) throw new OpenXmlSchemaError(`Font scheme must be one of [${FONT_SCHEMES.join(", ")}]; got "${opts.scheme}"`);
|
|
385
|
+
out.scheme = opts.scheme;
|
|
386
|
+
}
|
|
387
|
+
return Object.freeze(out);
|
|
388
|
+
}
|
|
389
|
+
/** Excel's default cell font: Calibri 11, minor scheme, theme=1 colour. */
|
|
390
|
+
const DEFAULT_FONT = makeFont({
|
|
391
|
+
name: "Calibri",
|
|
392
|
+
size: 11,
|
|
393
|
+
family: 2,
|
|
394
|
+
scheme: "minor",
|
|
395
|
+
color: makeColor({ theme: 1 })
|
|
396
|
+
});
|
|
397
|
+
/**
|
|
398
|
+
* Translate a {@link Font} to a CSS-property record suitable for HTML
|
|
399
|
+
* rendering / preview. Boolean toggles map to weight/style/decoration,
|
|
400
|
+
* `size` becomes `font-size: <n>pt`, and an explicit rgb/indexed
|
|
401
|
+
* `color` is rendered as `#RRGGBB` (alpha dropped). theme/auto colours
|
|
402
|
+
* are skipped — callers without a theme can't resolve them.
|
|
403
|
+
*
|
|
404
|
+
* `vertAlign='superscript'|'subscript'` lowers `font-size` to 0.83em
|
|
405
|
+
* (W3C convention) and sets `vertical-align`.
|
|
406
|
+
*
|
|
407
|
+
* Returns `{}` for an empty Font so callers can spread it without
|
|
408
|
+
* overwriting upstream defaults.
|
|
409
|
+
*/
|
|
410
|
+
function fontToCss(font) {
|
|
411
|
+
const css = {};
|
|
412
|
+
if (!font) return css;
|
|
413
|
+
if (font.name !== void 0) css["font-family"] = `'${font.name.replace(/'/g, "\\'")}'`;
|
|
414
|
+
if (font.size !== void 0) css["font-size"] = `${font.size}pt`;
|
|
415
|
+
if (font.bold) css["font-weight"] = "bold";
|
|
416
|
+
if (font.italic) css["font-style"] = "italic";
|
|
417
|
+
const decorations = [];
|
|
418
|
+
if (font.underline !== void 0) decorations.push("underline");
|
|
419
|
+
if (font.strike) decorations.push("line-through");
|
|
420
|
+
if (decorations.length > 0) css["text-decoration"] = decorations.join(" ");
|
|
421
|
+
if (font.color !== void 0) {
|
|
422
|
+
const argb = colorToHex(font.color);
|
|
423
|
+
if (argb !== void 0) css["color"] = `#${argb.slice(2)}`;
|
|
424
|
+
}
|
|
425
|
+
if (font.vertAlign === "superscript" || font.vertAlign === "subscript") {
|
|
426
|
+
css["vertical-align"] = font.vertAlign;
|
|
427
|
+
if (css["font-size"] === void 0) css["font-size"] = "0.83em";
|
|
428
|
+
}
|
|
429
|
+
return css;
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/styles/numbers.ts
|
|
433
|
+
/** Canonical OOXML built-in number formats — verbatim from openpyxl. */
|
|
434
|
+
const BUILTIN_FORMATS = Object.freeze({
|
|
435
|
+
0: "General",
|
|
436
|
+
1: "0",
|
|
437
|
+
2: "0.00",
|
|
438
|
+
3: "#,##0",
|
|
439
|
+
4: "#,##0.00",
|
|
440
|
+
5: "\"$\"#,##0_);(\"$\"#,##0)",
|
|
441
|
+
6: "\"$\"#,##0_);[Red](\"$\"#,##0)",
|
|
442
|
+
7: "\"$\"#,##0.00_);(\"$\"#,##0.00)",
|
|
443
|
+
8: "\"$\"#,##0.00_);[Red](\"$\"#,##0.00)",
|
|
444
|
+
9: "0%",
|
|
445
|
+
10: "0.00%",
|
|
446
|
+
11: "0.00E+00",
|
|
447
|
+
12: "# ?/?",
|
|
448
|
+
13: "# ??/??",
|
|
449
|
+
14: "mm-dd-yy",
|
|
450
|
+
15: "d-mmm-yy",
|
|
451
|
+
16: "d-mmm",
|
|
452
|
+
17: "mmm-yy",
|
|
453
|
+
18: "h:mm AM/PM",
|
|
454
|
+
19: "h:mm:ss AM/PM",
|
|
455
|
+
20: "h:mm",
|
|
456
|
+
21: "h:mm:ss",
|
|
457
|
+
22: "m/d/yy h:mm",
|
|
458
|
+
37: "#,##0_);(#,##0)",
|
|
459
|
+
38: "#,##0_);[Red](#,##0)",
|
|
460
|
+
39: "#,##0.00_);(#,##0.00)",
|
|
461
|
+
40: "#,##0.00_);[Red](#,##0.00)",
|
|
462
|
+
41: "_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)",
|
|
463
|
+
42: "_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)",
|
|
464
|
+
43: "_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)",
|
|
465
|
+
44: "_(\"$\"* #,##0.00_)_(\"$\"* \\(#,##0.00\\)_(\"$\"* \"-\"??_)_(@_)",
|
|
466
|
+
45: "mm:ss",
|
|
467
|
+
46: "[h]:mm:ss",
|
|
468
|
+
47: "mmss.0",
|
|
469
|
+
48: "##0.0E+0",
|
|
470
|
+
49: "@"
|
|
471
|
+
});
|
|
472
|
+
/** First numFmtId Excel reserves for user-defined formats. */
|
|
473
|
+
const BUILTIN_FORMATS_MAX_SIZE = 164;
|
|
474
|
+
const REVERSE = new Map(Object.entries(BUILTIN_FORMATS).map(([k, v]) => [v, Number(k)]));
|
|
475
|
+
const FORMAT_GENERAL = "General";
|
|
476
|
+
const FORMAT_TEXT = "@";
|
|
477
|
+
const FORMAT_NUMBER = "0";
|
|
478
|
+
const FORMAT_NUMBER_00 = "0.00";
|
|
479
|
+
const FORMAT_PERCENTAGE = "0%";
|
|
480
|
+
const FORMAT_PERCENTAGE_00 = "0.00%";
|
|
481
|
+
const FORMAT_DATE_DATETIME = "yyyy-mm-dd h:mm:ss";
|
|
482
|
+
const FORMAT_DATE_TIMEDELTA = "[hh]:mm:ss";
|
|
483
|
+
const FORMAT_DATE_YYYYMMDD2 = "yyyy-mm-dd";
|
|
484
|
+
/** Look up the format code for a given numFmtId, or `undefined` if unknown. */
|
|
485
|
+
function builtinFormatCode(id) {
|
|
486
|
+
return Object.hasOwn(BUILTIN_FORMATS, id) ? BUILTIN_FORMATS[id] : void 0;
|
|
487
|
+
}
|
|
488
|
+
/** Look up the numFmtId for a given format code, or `undefined` if not built-in. */
|
|
489
|
+
function builtinFormatId(code) {
|
|
490
|
+
return REVERSE.get(code);
|
|
491
|
+
}
|
|
492
|
+
/** True iff `code` is one of the OOXML built-in format strings. */
|
|
493
|
+
function isBuiltinFormat(code) {
|
|
494
|
+
return REVERSE.has(code);
|
|
495
|
+
}
|
|
496
|
+
const STRIP_RE = new RegExp(`\\[(BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW)\\]|"[^"]*"|\\[(?!hh?\\]|mm?\\]|ss?\\])[^\\]]*\\]`, "g");
|
|
497
|
+
const DATE_TOKEN_RE = /(?<![_\\])[dmhysDMHYS]/;
|
|
498
|
+
const TIMEDELTA_RE = /\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\](:ss(\.0*)?)?|\[ss?\](\.0*)?/i;
|
|
499
|
+
/**
|
|
500
|
+
* Heuristic: does the format string imply a date / time interpretation?
|
|
501
|
+
* Looks at only the first format section (positive-value branch); strips
|
|
502
|
+
* literals, colour codes and locale modifiers before scanning for date
|
|
503
|
+
* tokens (d/m/h/y/s, case-insensitive) that aren't escaped.
|
|
504
|
+
*/
|
|
505
|
+
function isDateFormat(code) {
|
|
506
|
+
if (code == null) return false;
|
|
507
|
+
const stripped = (code.split(";")[0] ?? "").replace(STRIP_RE, "");
|
|
508
|
+
return DATE_TOKEN_RE.test(stripped);
|
|
509
|
+
}
|
|
510
|
+
/** Heuristic: does the format string indicate a duration ([h]:mm:ss etc.)? */
|
|
511
|
+
function isTimedeltaFormat(code) {
|
|
512
|
+
if (code == null) return false;
|
|
513
|
+
const head = code.split(";")[0] ?? "";
|
|
514
|
+
return TIMEDELTA_RE.test(head);
|
|
515
|
+
}
|
|
516
|
+
/** Categorise a date format as 'date', 'time', 'datetime' or undefined. */
|
|
517
|
+
function classifyDateFormat(code) {
|
|
518
|
+
if (!isDateFormat(code)) return void 0;
|
|
519
|
+
const stripped = (code.split(";")[0] ?? "").replace(STRIP_RE, "");
|
|
520
|
+
let date = false;
|
|
521
|
+
let time = false;
|
|
522
|
+
for (const ch of stripped) {
|
|
523
|
+
if (ch === "d" || ch === "D" || ch === "y" || ch === "Y") date = true;
|
|
524
|
+
else if (ch === "h" || ch === "H" || ch === "s" || ch === "S") time = true;
|
|
525
|
+
if (date && time) break;
|
|
526
|
+
}
|
|
527
|
+
if (date && time) return "datetime";
|
|
528
|
+
if (date) return "date";
|
|
529
|
+
return "time";
|
|
530
|
+
}
|
|
531
|
+
function makeNumberFormat(opts) {
|
|
532
|
+
if (!Number.isInteger(opts.numFmtId) || opts.numFmtId < 0) throw new OpenXmlSchemaError(`NumberFormat numFmtId must be a non-negative integer; got ${opts.numFmtId}`);
|
|
533
|
+
if (typeof opts.formatCode !== "string") throw new OpenXmlSchemaError("NumberFormat formatCode must be a string");
|
|
534
|
+
return Object.freeze({
|
|
535
|
+
numFmtId: opts.numFmtId,
|
|
536
|
+
formatCode: opts.formatCode
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/styles/stylesheet.ts
|
|
541
|
+
/**
|
|
542
|
+
* Build a fresh Stylesheet pre-populated with Excel's required default entries.
|
|
543
|
+
* Mirrors openpyxl's empty Stylesheet: fonts: [DEFAULT_FONT] (index 0) fills:
|
|
544
|
+
* [DEFAULT_EMPTY_FILL,
|
|
545
|
+
* DEFAULT_GRAY_FILL] (indices 0, 1 — required)
|
|
546
|
+
* borders: [DEFAULT_BORDER] (index 0) cellXfs: empty (indices allocated on
|
|
547
|
+
* demand)
|
|
548
|
+
*/
|
|
549
|
+
function makeStylesheet() {
|
|
550
|
+
const fontKey = stableStringify(DEFAULT_FONT);
|
|
551
|
+
const fill0Key = stableStringify(DEFAULT_EMPTY_FILL);
|
|
552
|
+
const fill1Key = stableStringify(DEFAULT_GRAY_FILL);
|
|
553
|
+
const borderKey = stableStringify(DEFAULT_BORDER);
|
|
554
|
+
return {
|
|
555
|
+
fonts: [DEFAULT_FONT],
|
|
556
|
+
fills: [DEFAULT_EMPTY_FILL, DEFAULT_GRAY_FILL],
|
|
557
|
+
borders: [DEFAULT_BORDER],
|
|
558
|
+
numFmts: /* @__PURE__ */ new Map(),
|
|
559
|
+
cellXfs: [],
|
|
560
|
+
cellStyleXfs: [],
|
|
561
|
+
_fontIdByKey: new Map([[fontKey, 0]]),
|
|
562
|
+
_fillIdByKey: new Map([[fill0Key, 0], [fill1Key, 1]]),
|
|
563
|
+
_borderIdByKey: new Map([[borderKey, 0]]),
|
|
564
|
+
_xfIdByKey: /* @__PURE__ */ new Map(),
|
|
565
|
+
_styleXfIdByKey: /* @__PURE__ */ new Map(),
|
|
566
|
+
_numFmtIdByCode: /* @__PURE__ */ new Map()
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/** Add a Font to the pool, returning its 0-based index. Idempotent. */
|
|
570
|
+
function addFont(ss, font) {
|
|
571
|
+
return addToPool(font, ss.fonts, ss._fontIdByKey);
|
|
572
|
+
}
|
|
573
|
+
/** Add a Fill to the pool, returning its 0-based index. Idempotent. */
|
|
574
|
+
function addFill(ss, fill) {
|
|
575
|
+
return addToPool(fill, ss.fills, ss._fillIdByKey);
|
|
576
|
+
}
|
|
577
|
+
/** Add a Border to the pool, returning its 0-based index. Idempotent. */
|
|
578
|
+
function addBorder(ss, border) {
|
|
579
|
+
return addToPool(border, ss.borders, ss._borderIdByKey);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Resolve a number-format string to its numFmtId.
|
|
583
|
+
* - Built-in codes return their canonical OOXML ID.
|
|
584
|
+
* - Otherwise the custom code is registered (and allocated an ID
|
|
585
|
+
* ≥ {@link BUILTIN_FORMATS_MAX_SIZE}). Idempotent.
|
|
586
|
+
*/
|
|
587
|
+
function addNumFmt(ss, formatCode) {
|
|
588
|
+
const builtin = builtinFormatId(formatCode);
|
|
589
|
+
if (builtin !== void 0) return builtin;
|
|
590
|
+
const cached = ss._numFmtIdByCode.get(formatCode);
|
|
591
|
+
if (cached !== void 0) return cached;
|
|
592
|
+
const id = 164 + ss.numFmts.size;
|
|
593
|
+
ss.numFmts.set(id, formatCode);
|
|
594
|
+
ss._numFmtIdByCode.set(formatCode, id);
|
|
595
|
+
return id;
|
|
596
|
+
}
|
|
597
|
+
/** Add a CellXf to the cellXfs pool, returning its 0-based index. Idempotent. */
|
|
598
|
+
function addCellXf(ss, xf) {
|
|
599
|
+
validateCellXfRefs(ss, xf, false);
|
|
600
|
+
return addToPool(xf, ss.cellXfs, ss._xfIdByKey);
|
|
601
|
+
}
|
|
602
|
+
/** Add a CellXf to the cellStyleXfs pool. Idempotent. */
|
|
603
|
+
function addCellStyleXf(ss, xf) {
|
|
604
|
+
validateCellXfRefs(ss, xf, true);
|
|
605
|
+
return addToPool(xf, ss.cellStyleXfs, ss._styleXfIdByKey);
|
|
606
|
+
}
|
|
607
|
+
const addToPool = (value, pool, byKey) => {
|
|
608
|
+
const key = stableStringify(value);
|
|
609
|
+
const cached = byKey.get(key);
|
|
610
|
+
if (cached !== void 0) return cached;
|
|
611
|
+
const id = pool.length;
|
|
612
|
+
pool.push(value);
|
|
613
|
+
byKey.set(key, id);
|
|
614
|
+
return id;
|
|
615
|
+
};
|
|
616
|
+
const validateCellXfRefs = (ss, xf, isStyle) => {
|
|
617
|
+
if (xf.fontId < 0 || xf.fontId >= ss.fonts.length) throw new OpenXmlSchemaError(`CellXf.fontId ${xf.fontId} out of range [0, ${ss.fonts.length})`);
|
|
618
|
+
if (xf.fillId < 0 || xf.fillId >= ss.fills.length) throw new OpenXmlSchemaError(`CellXf.fillId ${xf.fillId} out of range [0, ${ss.fills.length})`);
|
|
619
|
+
if (xf.borderId < 0 || xf.borderId >= ss.borders.length) throw new OpenXmlSchemaError(`CellXf.borderId ${xf.borderId} out of range [0, ${ss.borders.length})`);
|
|
620
|
+
if (!Number.isInteger(xf.numFmtId) || xf.numFmtId < 0) throw new OpenXmlSchemaError(`CellXf.numFmtId must be a non-negative integer; got ${xf.numFmtId}`);
|
|
621
|
+
if (!isStyle && xf.xfId !== void 0) {
|
|
622
|
+
if (xf.xfId < 0 || xf.xfId >= ss.cellStyleXfs.length) throw new OpenXmlSchemaError(`CellXf.xfId ${xf.xfId} out of range [0, ${ss.cellStyleXfs.length})`);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
/** Read-only snapshot of every Font entry in the pool, indexed by id. */
|
|
626
|
+
function listFonts(ss) {
|
|
627
|
+
return ss.fonts;
|
|
628
|
+
}
|
|
629
|
+
/** Read-only snapshot of every Fill entry in the pool, indexed by id. */
|
|
630
|
+
function listFills(ss) {
|
|
631
|
+
return ss.fills;
|
|
632
|
+
}
|
|
633
|
+
/** Read-only snapshot of every Border entry in the pool, indexed by id. */
|
|
634
|
+
function listBorders(ss) {
|
|
635
|
+
return ss.borders;
|
|
636
|
+
}
|
|
637
|
+
/** Read-only snapshot of every CellXf entry in the cellXfs pool. */
|
|
638
|
+
function listCellXfs(ss) {
|
|
639
|
+
return ss.cellXfs;
|
|
640
|
+
}
|
|
641
|
+
/** Read-only snapshot of every CellStyleXf entry (named-style xfs). */
|
|
642
|
+
function listCellStyleXfs(ss) {
|
|
643
|
+
return ss.cellStyleXfs;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Convenience: build the default `cellXfs[0]` Excel emits — points at the
|
|
647
|
+
* workbook's font 0 / fill 0 / border 0 / numFmtId 0 (General).
|
|
648
|
+
*/
|
|
649
|
+
function defaultCellXf() {
|
|
650
|
+
return Object.freeze({
|
|
651
|
+
fontId: 0,
|
|
652
|
+
fillId: 0,
|
|
653
|
+
borderId: 0,
|
|
654
|
+
numFmtId: 0
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/styles/named-styles.ts
|
|
659
|
+
/**
|
|
660
|
+
* Register a NamedStyle on the Stylesheet:
|
|
661
|
+
* 1. add Font / Fill / Border / NumberFormat to their pools
|
|
662
|
+
* 2. push a CellXf with apply* flags into cellStyleXfs
|
|
663
|
+
* 3. append a {name, xfId, builtinId} entry into the workbook's
|
|
664
|
+
* namedStyles list (caller-managed; this function returns the
|
|
665
|
+
* xfId so callers can connect the dots)
|
|
666
|
+
* Idempotent on (name): re-registering by the same name returns the
|
|
667
|
+
* cached xfId.
|
|
668
|
+
*/
|
|
669
|
+
function addNamedStyle(ss, style) {
|
|
670
|
+
const cached = ss._namedStyleByName?.get(style.name);
|
|
671
|
+
if (cached !== void 0) return cached.xfId;
|
|
672
|
+
const xfId = addCellStyleXf(ss, {
|
|
673
|
+
fontId: style.font !== void 0 ? addFont(ss, style.font) : 0,
|
|
674
|
+
fillId: style.fill !== void 0 ? addFill(ss, style.fill) : 0,
|
|
675
|
+
borderId: style.border !== void 0 ? addBorder(ss, style.border) : 0,
|
|
676
|
+
numFmtId: style.numberFormat !== void 0 ? addNumFmt(ss, style.numberFormat) : 0,
|
|
677
|
+
...style.alignment !== void 0 ? { alignment: style.alignment } : {},
|
|
678
|
+
...style.protection !== void 0 ? { protection: style.protection } : {}
|
|
679
|
+
});
|
|
680
|
+
if (ss._namedStyleByName === void 0) ss._namedStyleByName = /* @__PURE__ */ new Map();
|
|
681
|
+
if (ss.namedStyles === void 0) ss.namedStyles = [];
|
|
682
|
+
const entry = {
|
|
683
|
+
name: style.name,
|
|
684
|
+
xfId,
|
|
685
|
+
...style.builtinId !== void 0 ? { builtinId: style.builtinId } : {},
|
|
686
|
+
...style.customBuiltin !== void 0 ? { customBuiltin: style.customBuiltin } : {},
|
|
687
|
+
...style.hidden !== void 0 ? { hidden: style.hidden } : {},
|
|
688
|
+
...style.iLevel !== void 0 ? { iLevel: style.iLevel } : {}
|
|
689
|
+
};
|
|
690
|
+
ss.namedStyles.push(entry);
|
|
691
|
+
ss._namedStyleByName.set(style.name, entry);
|
|
692
|
+
return xfId;
|
|
693
|
+
}
|
|
694
|
+
const BODY_FONT = makeFont({
|
|
695
|
+
name: "Calibri",
|
|
696
|
+
family: 2,
|
|
697
|
+
size: 12,
|
|
698
|
+
color: makeColor({ theme: 1 }),
|
|
699
|
+
scheme: "minor"
|
|
700
|
+
});
|
|
701
|
+
const fillRgb = (rgb, fg = true) => makePatternFill(fg ? {
|
|
702
|
+
patternType: "solid",
|
|
703
|
+
bgColor: makeColor({ rgb }),
|
|
704
|
+
fgColor: makeColor({ rgb })
|
|
705
|
+
} : { patternType: "solid" });
|
|
706
|
+
/** Curated subset of openpyxl's `styles` dict. Keys match the user-visible names. */
|
|
707
|
+
const BUILTIN_NAMED_STYLES = Object.freeze({
|
|
708
|
+
Normal: {
|
|
709
|
+
name: "Normal",
|
|
710
|
+
builtinId: 0,
|
|
711
|
+
font: BODY_FONT
|
|
712
|
+
},
|
|
713
|
+
Good: {
|
|
714
|
+
name: "Good",
|
|
715
|
+
builtinId: 26,
|
|
716
|
+
font: makeFont({
|
|
717
|
+
...BODY_FONT,
|
|
718
|
+
color: makeColor({ rgb: "FF006100" })
|
|
719
|
+
}),
|
|
720
|
+
fill: fillRgb("FFC6EFCE")
|
|
721
|
+
},
|
|
722
|
+
Bad: {
|
|
723
|
+
name: "Bad",
|
|
724
|
+
builtinId: 27,
|
|
725
|
+
font: makeFont({
|
|
726
|
+
...BODY_FONT,
|
|
727
|
+
color: makeColor({ rgb: "FF9C0006" })
|
|
728
|
+
}),
|
|
729
|
+
fill: fillRgb("FFFFC7CE")
|
|
730
|
+
},
|
|
731
|
+
Neutral: {
|
|
732
|
+
name: "Neutral",
|
|
733
|
+
builtinId: 28,
|
|
734
|
+
font: makeFont({
|
|
735
|
+
...BODY_FONT,
|
|
736
|
+
color: makeColor({ rgb: "FF9C5700" })
|
|
737
|
+
}),
|
|
738
|
+
fill: fillRgb("FFFFEB9C")
|
|
739
|
+
},
|
|
740
|
+
Calculation: {
|
|
741
|
+
name: "Calculation",
|
|
742
|
+
builtinId: 22,
|
|
743
|
+
font: makeFont({
|
|
744
|
+
...BODY_FONT,
|
|
745
|
+
bold: true,
|
|
746
|
+
color: makeColor({ rgb: "FFFA7D00" })
|
|
747
|
+
}),
|
|
748
|
+
fill: fillRgb("FFF2F2F2")
|
|
749
|
+
},
|
|
750
|
+
"Check Cell": {
|
|
751
|
+
name: "Check Cell",
|
|
752
|
+
builtinId: 23,
|
|
753
|
+
font: makeFont({
|
|
754
|
+
...BODY_FONT,
|
|
755
|
+
bold: true,
|
|
756
|
+
color: makeColor({ rgb: "FFFFFFFF" })
|
|
757
|
+
}),
|
|
758
|
+
fill: fillRgb("FFA5A5A5")
|
|
759
|
+
},
|
|
760
|
+
"Linked Cell": {
|
|
761
|
+
name: "Linked Cell",
|
|
762
|
+
builtinId: 24,
|
|
763
|
+
font: makeFont({
|
|
764
|
+
...BODY_FONT,
|
|
765
|
+
color: makeColor({ rgb: "FFFA7D00" })
|
|
766
|
+
})
|
|
767
|
+
},
|
|
768
|
+
Note: {
|
|
769
|
+
name: "Note",
|
|
770
|
+
builtinId: 10,
|
|
771
|
+
font: BODY_FONT,
|
|
772
|
+
fill: fillRgb("FFFFFFC0")
|
|
773
|
+
},
|
|
774
|
+
"Warning Text": {
|
|
775
|
+
name: "Warning Text",
|
|
776
|
+
builtinId: 11,
|
|
777
|
+
font: makeFont({
|
|
778
|
+
...BODY_FONT,
|
|
779
|
+
color: makeColor({ rgb: "FFFF0000" })
|
|
780
|
+
})
|
|
781
|
+
},
|
|
782
|
+
Input: {
|
|
783
|
+
name: "Input",
|
|
784
|
+
builtinId: 20,
|
|
785
|
+
font: makeFont({
|
|
786
|
+
...BODY_FONT,
|
|
787
|
+
color: makeColor({ rgb: "FF3F3F76" })
|
|
788
|
+
}),
|
|
789
|
+
fill: fillRgb("FFFFCC99")
|
|
790
|
+
},
|
|
791
|
+
Output: {
|
|
792
|
+
name: "Output",
|
|
793
|
+
builtinId: 21,
|
|
794
|
+
font: makeFont({
|
|
795
|
+
...BODY_FONT,
|
|
796
|
+
bold: true,
|
|
797
|
+
color: makeColor({ rgb: "FF3F3F3F" })
|
|
798
|
+
}),
|
|
799
|
+
fill: fillRgb("FFF2F2F2")
|
|
800
|
+
},
|
|
801
|
+
"Explanatory Text": {
|
|
802
|
+
name: "Explanatory Text",
|
|
803
|
+
builtinId: 53,
|
|
804
|
+
font: makeFont({
|
|
805
|
+
...BODY_FONT,
|
|
806
|
+
italic: true,
|
|
807
|
+
color: makeColor({ rgb: "FF7F7F7F" })
|
|
808
|
+
})
|
|
809
|
+
},
|
|
810
|
+
Title: {
|
|
811
|
+
name: "Title",
|
|
812
|
+
builtinId: 15,
|
|
813
|
+
font: makeFont({
|
|
814
|
+
name: "Cambria",
|
|
815
|
+
family: 2,
|
|
816
|
+
size: 18,
|
|
817
|
+
scheme: "major",
|
|
818
|
+
color: makeColor({ theme: 3 })
|
|
819
|
+
})
|
|
820
|
+
},
|
|
821
|
+
"Headline 1": {
|
|
822
|
+
name: "Headline 1",
|
|
823
|
+
builtinId: 16,
|
|
824
|
+
font: makeFont({
|
|
825
|
+
...BODY_FONT,
|
|
826
|
+
bold: true,
|
|
827
|
+
size: 15,
|
|
828
|
+
color: makeColor({ theme: 3 })
|
|
829
|
+
})
|
|
830
|
+
},
|
|
831
|
+
"Headline 2": {
|
|
832
|
+
name: "Headline 2",
|
|
833
|
+
builtinId: 17,
|
|
834
|
+
font: makeFont({
|
|
835
|
+
...BODY_FONT,
|
|
836
|
+
bold: true,
|
|
837
|
+
size: 13,
|
|
838
|
+
color: makeColor({ theme: 3 })
|
|
839
|
+
})
|
|
840
|
+
},
|
|
841
|
+
"Headline 3": {
|
|
842
|
+
name: "Headline 3",
|
|
843
|
+
builtinId: 18,
|
|
844
|
+
font: makeFont({
|
|
845
|
+
...BODY_FONT,
|
|
846
|
+
bold: true,
|
|
847
|
+
color: makeColor({ theme: 3 })
|
|
848
|
+
})
|
|
849
|
+
},
|
|
850
|
+
"Headline 4": {
|
|
851
|
+
name: "Headline 4",
|
|
852
|
+
builtinId: 19,
|
|
853
|
+
font: makeFont({
|
|
854
|
+
...BODY_FONT,
|
|
855
|
+
bold: true,
|
|
856
|
+
italic: true,
|
|
857
|
+
color: makeColor({ theme: 3 })
|
|
858
|
+
})
|
|
859
|
+
},
|
|
860
|
+
Total: {
|
|
861
|
+
name: "Total",
|
|
862
|
+
builtinId: 25,
|
|
863
|
+
font: makeFont({
|
|
864
|
+
...BODY_FONT,
|
|
865
|
+
bold: true
|
|
866
|
+
})
|
|
867
|
+
},
|
|
868
|
+
Comma: {
|
|
869
|
+
name: "Comma",
|
|
870
|
+
builtinId: 3,
|
|
871
|
+
font: BODY_FONT,
|
|
872
|
+
numberFormat: "#,##0.00"
|
|
873
|
+
},
|
|
874
|
+
"Comma [0]": {
|
|
875
|
+
name: "Comma [0]",
|
|
876
|
+
builtinId: 6,
|
|
877
|
+
font: BODY_FONT,
|
|
878
|
+
numberFormat: "#,##0"
|
|
879
|
+
},
|
|
880
|
+
Currency: {
|
|
881
|
+
name: "Currency",
|
|
882
|
+
builtinId: 4,
|
|
883
|
+
font: BODY_FONT,
|
|
884
|
+
numberFormat: "\"$\"#,##0.00"
|
|
885
|
+
},
|
|
886
|
+
"Currency [0]": {
|
|
887
|
+
name: "Currency [0]",
|
|
888
|
+
builtinId: 7,
|
|
889
|
+
font: BODY_FONT,
|
|
890
|
+
numberFormat: "\"$\"#,##0"
|
|
891
|
+
},
|
|
892
|
+
Percent: {
|
|
893
|
+
name: "Percent",
|
|
894
|
+
builtinId: 5,
|
|
895
|
+
font: BODY_FONT,
|
|
896
|
+
numberFormat: "0%"
|
|
897
|
+
},
|
|
898
|
+
Hyperlink: {
|
|
899
|
+
name: "Hyperlink",
|
|
900
|
+
builtinId: 8,
|
|
901
|
+
font: makeFont({
|
|
902
|
+
...BODY_FONT,
|
|
903
|
+
underline: "single",
|
|
904
|
+
color: makeColor({ theme: 10 })
|
|
905
|
+
})
|
|
906
|
+
},
|
|
907
|
+
"Followed Hyperlink": {
|
|
908
|
+
name: "Followed Hyperlink",
|
|
909
|
+
builtinId: 9,
|
|
910
|
+
font: makeFont({
|
|
911
|
+
...BODY_FONT,
|
|
912
|
+
underline: "single",
|
|
913
|
+
color: makeColor({ theme: 11 })
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
/**
|
|
918
|
+
* Register a built-in style with the supplied Stylesheet (idempotent).
|
|
919
|
+
* Returns the cellStyleXfs index. Throws OpenXmlSchemaError when the
|
|
920
|
+
* name is unknown.
|
|
921
|
+
*/
|
|
922
|
+
function ensureBuiltinStyle(ss, name) {
|
|
923
|
+
const spec = BUILTIN_NAMED_STYLES[name];
|
|
924
|
+
if (spec === void 0) throw new OpenXmlSchemaError(`ensureBuiltinStyle: unknown built-in style "${String(name)}"`);
|
|
925
|
+
return addNamedStyle(ss, spec);
|
|
926
|
+
}
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/styles/protection.ts
|
|
929
|
+
function makeProtection(opts = {}) {
|
|
930
|
+
const out = {};
|
|
931
|
+
if (opts.locked !== void 0) out.locked = opts.locked;
|
|
932
|
+
if (opts.hidden !== void 0) out.hidden = opts.hidden;
|
|
933
|
+
return Object.freeze(out);
|
|
934
|
+
}
|
|
935
|
+
/** Excel's Stylesheet always contains an entry equal to {locked:true, hidden:false}. */
|
|
936
|
+
const DEFAULT_PROTECTION = makeProtection({
|
|
937
|
+
locked: true,
|
|
938
|
+
hidden: false
|
|
939
|
+
});
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/styles/cell-style.ts
|
|
942
|
+
/** Default General number format code (numFmtId 0). */
|
|
943
|
+
const GENERAL_FORMAT_CODE = "General";
|
|
944
|
+
/** Resolve a cell's current CellXf, falling back to defaults when unset. */
|
|
945
|
+
function currentXf(ss, c) {
|
|
946
|
+
return ss.cellXfs[c.styleId] ?? defaultCellXf();
|
|
947
|
+
}
|
|
948
|
+
function getCellFont(wb, c) {
|
|
949
|
+
const xf = currentXf(wb.styles, c);
|
|
950
|
+
return wb.styles.fonts[xf.fontId] ?? DEFAULT_FONT;
|
|
951
|
+
}
|
|
952
|
+
function getCellFill(wb, c) {
|
|
953
|
+
const xf = currentXf(wb.styles, c);
|
|
954
|
+
return wb.styles.fills[xf.fillId] ?? DEFAULT_EMPTY_FILL;
|
|
955
|
+
}
|
|
956
|
+
function getCellBorder(wb, c) {
|
|
957
|
+
const xf = currentXf(wb.styles, c);
|
|
958
|
+
return wb.styles.borders[xf.borderId] ?? DEFAULT_BORDER;
|
|
959
|
+
}
|
|
960
|
+
function getCellAlignment(wb, c) {
|
|
961
|
+
return currentXf(wb.styles, c).alignment ?? {};
|
|
962
|
+
}
|
|
963
|
+
function getCellProtection(wb, c) {
|
|
964
|
+
return currentXf(wb.styles, c).protection ?? DEFAULT_PROTECTION;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Returns the cell's number-format **code** (e.g. `"0.00"`, `"General"`).
|
|
968
|
+
* Built-in IDs resolve through `builtinFormatCode`; custom IDs come from the
|
|
969
|
+
* workbook's numFmts map.
|
|
970
|
+
*/
|
|
971
|
+
function getCellNumberFormat(wb, c) {
|
|
972
|
+
const id = currentXf(wb.styles, c).numFmtId;
|
|
973
|
+
const builtin = builtinFormatCode(id);
|
|
974
|
+
if (builtin !== void 0) return builtin;
|
|
975
|
+
return wb.styles.numFmts.get(id) ?? GENERAL_FORMAT_CODE;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Aggregate `fontToCss` + `fillToCss` + `borderToCss` + `alignmentToCss` for a
|
|
979
|
+
* cell into a single CSS-property record. Resolves the cell's `styleId` against
|
|
980
|
+
* the workbook stylesheet, then merges the four partials. On key collision the
|
|
981
|
+
* priority is alignment > border > fill > font (alignment is most specific,
|
|
982
|
+
* font is the broad default). A fully-default cell (`styleId === 0` with empty
|
|
983
|
+
* pools) returns `{}`.
|
|
984
|
+
*/
|
|
985
|
+
function cellStyleToCss(wb, c) {
|
|
986
|
+
if (c.styleId === 0) {
|
|
987
|
+
const xf = wb.styles.cellXfs[0];
|
|
988
|
+
if (!xf || xf.fontId === 0 && xf.fillId === 0 && xf.borderId === 0 && xf.alignment === void 0) return {};
|
|
989
|
+
}
|
|
990
|
+
const font = getCellFont(wb, c);
|
|
991
|
+
const fill = getCellFill(wb, c);
|
|
992
|
+
const border = getCellBorder(wb, c);
|
|
993
|
+
const alignment = getCellAlignment(wb, c);
|
|
994
|
+
return {
|
|
995
|
+
...fontToCss(font),
|
|
996
|
+
...fillToCss(fill),
|
|
997
|
+
...borderToCss(border),
|
|
998
|
+
...alignmentToCss(alignment)
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Reserve cellXfs[0] for the implicit default xf when the pool is empty.
|
|
1003
|
+
* Excel's `<c>` elements without an `s=` attribute resolve to `cellXfs[0]`, so
|
|
1004
|
+
* the first time a caller styles any cell we need to make sure that slot stays
|
|
1005
|
+
* the default — otherwise unstyled cells in the same sheet would inherit the
|
|
1006
|
+
* freshly added styled xf.
|
|
1007
|
+
*
|
|
1008
|
+
* Idempotent: calling this on a non-empty pool is a no-op.
|
|
1009
|
+
*/
|
|
1010
|
+
const reserveDefaultXfSlot = (wb) => {
|
|
1011
|
+
if (wb.styles.cellXfs.length === 0) addCellXf(wb.styles, defaultCellXf());
|
|
1012
|
+
};
|
|
1013
|
+
/**
|
|
1014
|
+
* Replace one field on the cell's CellXf. Centralises the dedup + styleId
|
|
1015
|
+
* update so each `setCell*` is a single dispatch.
|
|
1016
|
+
*/
|
|
1017
|
+
function applyXfPatch(wb, c, patch) {
|
|
1018
|
+
reserveDefaultXfSlot(wb);
|
|
1019
|
+
const next = {
|
|
1020
|
+
...currentXf(wb.styles, c),
|
|
1021
|
+
...patch
|
|
1022
|
+
};
|
|
1023
|
+
c.styleId = addCellXf(wb.styles, next);
|
|
1024
|
+
}
|
|
1025
|
+
function setCellFont(wb, c, font) {
|
|
1026
|
+
applyXfPatch(wb, c, {
|
|
1027
|
+
fontId: addFont(wb.styles, font),
|
|
1028
|
+
applyFont: true
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function setCellFill(wb, c, fill) {
|
|
1032
|
+
applyXfPatch(wb, c, {
|
|
1033
|
+
fillId: addFill(wb.styles, fill),
|
|
1034
|
+
applyFill: true
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
function setCellBorder(wb, c, border) {
|
|
1038
|
+
applyXfPatch(wb, c, {
|
|
1039
|
+
borderId: addBorder(wb.styles, border),
|
|
1040
|
+
applyBorder: true
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
function setCellAlignment(wb, c, alignment) {
|
|
1044
|
+
applyXfPatch(wb, c, {
|
|
1045
|
+
alignment,
|
|
1046
|
+
applyAlignment: true
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function setCellProtection(wb, c, protection) {
|
|
1050
|
+
applyXfPatch(wb, c, {
|
|
1051
|
+
protection,
|
|
1052
|
+
applyProtection: true
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Set the cell's number format by its **code** string. Built-in codes resolve
|
|
1057
|
+
* to their canonical id; custom codes are registered via `addNumFmt`.
|
|
1058
|
+
*/
|
|
1059
|
+
function setCellNumberFormat(wb, c, formatCode) {
|
|
1060
|
+
applyXfPatch(wb, c, {
|
|
1061
|
+
numFmtId: addNumFmt(wb.styles, formatCode),
|
|
1062
|
+
applyNumberFormat: true
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Copy the source cell's `styleId` to the target cell. Both cells share the
|
|
1067
|
+
* same workbook stylesheet, so the styled appearance carries over without
|
|
1068
|
+
* allocating a new xf entry. Pass cells from different workbooks via {@link
|
|
1069
|
+
* cloneCellStyle} if you need a deep copy across workbooks.
|
|
1070
|
+
*/
|
|
1071
|
+
function copyCellStyle(_wb, source, target) {
|
|
1072
|
+
target.styleId = source.styleId;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Reset a cell back to the default (unstyled) appearance — equivalent to
|
|
1076
|
+
* Excel's "Clear Formatting" command. After the call, the cell inherits the
|
|
1077
|
+
* workbook's default font / fill / border / alignment / protection /
|
|
1078
|
+
* numberFormat. The underlying xf pool is **not** shrunk (Excel doesn't bother
|
|
1079
|
+
* either; the orphaned xf is harmless).
|
|
1080
|
+
*/
|
|
1081
|
+
function clearCellStyle(_wb, c) {
|
|
1082
|
+
c.styleId = 0;
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Range-level shortcut for {@link clearCellStyle}. Walks every cell actually
|
|
1086
|
+
* present in the range and resets its `styleId` to 0; cells that don't exist
|
|
1087
|
+
* yet are **not** materialised (no-op for sparse regions, unlike the styled
|
|
1088
|
+
* `setRange*` family which has to create cells to make the patch observable).
|
|
1089
|
+
*/
|
|
1090
|
+
function clearRangeStyle(wb, ws, range) {
|
|
1091
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1092
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
1093
|
+
const row = ws.rows.get(r);
|
|
1094
|
+
if (!row) continue;
|
|
1095
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
1096
|
+
const cell = row.get(c);
|
|
1097
|
+
if (cell) clearCellStyle(wb, cell);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Deep-copy the source cell's full xf (font / fill / border / alignment /
|
|
1103
|
+
* protection / numberFormat) into a possibly-different workbook. Returns the
|
|
1104
|
+
* new styleId in the target workbook.
|
|
1105
|
+
*/
|
|
1106
|
+
function cloneCellStyle(sourceWb, source, targetWb, target) {
|
|
1107
|
+
reserveDefaultXfSlot(targetWb);
|
|
1108
|
+
const srcXf = currentXf(sourceWb.styles, source);
|
|
1109
|
+
const srcFont = sourceWb.styles.fonts[srcXf.fontId] ?? DEFAULT_FONT;
|
|
1110
|
+
const srcFill = sourceWb.styles.fills[srcXf.fillId] ?? DEFAULT_EMPTY_FILL;
|
|
1111
|
+
const srcBorder = sourceWb.styles.borders[srcXf.borderId] ?? DEFAULT_BORDER;
|
|
1112
|
+
const srcNumFmt = builtinFormatCode(srcXf.numFmtId) ?? sourceWb.styles.numFmts.get(srcXf.numFmtId) ?? GENERAL_FORMAT_CODE;
|
|
1113
|
+
const next = {
|
|
1114
|
+
fontId: addFont(targetWb.styles, srcFont),
|
|
1115
|
+
fillId: addFill(targetWb.styles, srcFill),
|
|
1116
|
+
borderId: addBorder(targetWb.styles, srcBorder),
|
|
1117
|
+
numFmtId: addNumFmt(targetWb.styles, srcNumFmt)
|
|
1118
|
+
};
|
|
1119
|
+
if (srcXf.applyFont) next.applyFont = true;
|
|
1120
|
+
if (srcXf.applyFill) next.applyFill = true;
|
|
1121
|
+
if (srcXf.applyBorder) next.applyBorder = true;
|
|
1122
|
+
if (srcXf.applyNumberFormat) next.applyNumberFormat = true;
|
|
1123
|
+
if (srcXf.alignment !== void 0) {
|
|
1124
|
+
next.alignment = srcXf.alignment;
|
|
1125
|
+
next.applyAlignment = true;
|
|
1126
|
+
}
|
|
1127
|
+
if (srcXf.protection !== void 0) {
|
|
1128
|
+
next.protection = srcXf.protection;
|
|
1129
|
+
next.applyProtection = true;
|
|
1130
|
+
}
|
|
1131
|
+
target.styleId = addCellXf(targetWb.styles, next);
|
|
1132
|
+
return target.styleId;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Build a single CellXf id from a multi-axis style spec, then apply it to every
|
|
1136
|
+
* cell in `range`. The xf is registered once per style shape, so a 1000-cell
|
|
1137
|
+
* range allocates one xf — much faster than looping `setCellStyle` per cell.
|
|
1138
|
+
*/
|
|
1139
|
+
function setRangeStyle(wb, ws, range, opts) {
|
|
1140
|
+
const patch = {};
|
|
1141
|
+
if (opts.font !== void 0) {
|
|
1142
|
+
patch.fontId = addFont(wb.styles, opts.font);
|
|
1143
|
+
patch.applyFont = true;
|
|
1144
|
+
}
|
|
1145
|
+
if (opts.fill !== void 0) {
|
|
1146
|
+
patch.fillId = addFill(wb.styles, opts.fill);
|
|
1147
|
+
patch.applyFill = true;
|
|
1148
|
+
}
|
|
1149
|
+
if (opts.border !== void 0) {
|
|
1150
|
+
patch.borderId = addBorder(wb.styles, opts.border);
|
|
1151
|
+
patch.applyBorder = true;
|
|
1152
|
+
}
|
|
1153
|
+
if (opts.alignment !== void 0) {
|
|
1154
|
+
patch.alignment = opts.alignment;
|
|
1155
|
+
patch.applyAlignment = true;
|
|
1156
|
+
}
|
|
1157
|
+
if (opts.protection !== void 0) {
|
|
1158
|
+
patch.protection = opts.protection;
|
|
1159
|
+
patch.applyProtection = true;
|
|
1160
|
+
}
|
|
1161
|
+
if (opts.numberFormat !== void 0) {
|
|
1162
|
+
patch.numFmtId = addNumFmt(wb.styles, opts.numberFormat);
|
|
1163
|
+
patch.applyNumberFormat = true;
|
|
1164
|
+
}
|
|
1165
|
+
if (Object.keys(patch).length === 0) return;
|
|
1166
|
+
reserveDefaultXfSlot(wb);
|
|
1167
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1168
|
+
for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) {
|
|
1169
|
+
let cell = ws.rows.get(r)?.get(c);
|
|
1170
|
+
if (!cell) cell = setCell(ws, r, c);
|
|
1171
|
+
const next = {
|
|
1172
|
+
...currentXf(wb.styles, cell),
|
|
1173
|
+
...patch
|
|
1174
|
+
};
|
|
1175
|
+
cell.styleId = addCellXf(wb.styles, next);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Combined cell-style setter. Each axis is independent — pass any subset and
|
|
1180
|
+
* the corresponding `applyXxx` flags get set on the underlying CellXf. Avoids
|
|
1181
|
+
* 5+ separate stylesheet round-trips when a caller wants to style a single cell
|
|
1182
|
+
* across multiple axes (Excel dedupes the resulting xf record on every call).
|
|
1183
|
+
*/
|
|
1184
|
+
function setCellStyle(wb, c, opts) {
|
|
1185
|
+
const patch = {};
|
|
1186
|
+
if (opts.font !== void 0) {
|
|
1187
|
+
patch.fontId = addFont(wb.styles, opts.font);
|
|
1188
|
+
patch.applyFont = true;
|
|
1189
|
+
}
|
|
1190
|
+
if (opts.fill !== void 0) {
|
|
1191
|
+
patch.fillId = addFill(wb.styles, opts.fill);
|
|
1192
|
+
patch.applyFill = true;
|
|
1193
|
+
}
|
|
1194
|
+
if (opts.border !== void 0) {
|
|
1195
|
+
patch.borderId = addBorder(wb.styles, opts.border);
|
|
1196
|
+
patch.applyBorder = true;
|
|
1197
|
+
}
|
|
1198
|
+
if (opts.alignment !== void 0) {
|
|
1199
|
+
patch.alignment = opts.alignment;
|
|
1200
|
+
patch.applyAlignment = true;
|
|
1201
|
+
}
|
|
1202
|
+
if (opts.protection !== void 0) {
|
|
1203
|
+
patch.protection = opts.protection;
|
|
1204
|
+
patch.applyProtection = true;
|
|
1205
|
+
}
|
|
1206
|
+
if (opts.numberFormat !== void 0) {
|
|
1207
|
+
patch.numFmtId = addNumFmt(wb.styles, opts.numberFormat);
|
|
1208
|
+
patch.applyNumberFormat = true;
|
|
1209
|
+
}
|
|
1210
|
+
if (Object.keys(patch).length === 0) return;
|
|
1211
|
+
applyXfPatch(wb, c, patch);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Set the cell's background to a solid color. Accepts a hex string
|
|
1215
|
+
* (`'FFAAFFAA'`) or a partial `Color` object (`{ theme: 4, tint: 0.4 }`).
|
|
1216
|
+
* Equivalent to `setCellFill(wb, c, makePatternFill({ patternType: 'solid',
|
|
1217
|
+
* fgColor: makeColor(...) }))`.
|
|
1218
|
+
*/
|
|
1219
|
+
function setCellBackgroundColor(wb, c, color) {
|
|
1220
|
+
setCellFill(wb, c, makePatternFill({
|
|
1221
|
+
patternType: "solid",
|
|
1222
|
+
fgColor: typeof color === "string" ? makeColor({ rgb: color }) : makeColor(color)
|
|
1223
|
+
}));
|
|
1224
|
+
}
|
|
1225
|
+
/** Strip the cell's background fill, returning it to the default. */
|
|
1226
|
+
function clearCellBackground(wb, c) {
|
|
1227
|
+
setCellFill(wb, c, DEFAULT_EMPTY_FILL);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Range-level shortcut for `setCellBackgroundColor`. Each cell in the range
|
|
1231
|
+
* gets the same solid pattern fill via `setRangeStyle`, so the fill pool dedups
|
|
1232
|
+
* to a single entry across the whole range.
|
|
1233
|
+
*/
|
|
1234
|
+
function setRangeBackgroundColor(wb, ws, range, color) {
|
|
1235
|
+
setRangeStyle(wb, ws, range, { fill: makePatternFill({
|
|
1236
|
+
patternType: "solid",
|
|
1237
|
+
fgColor: typeof color === "string" ? makeColor({ rgb: color }) : makeColor(color)
|
|
1238
|
+
}) });
|
|
1239
|
+
}
|
|
1240
|
+
/** Range-level shortcut for `setCellFont` (full Font replacement). */
|
|
1241
|
+
function setRangeFont(wb, ws, range, font) {
|
|
1242
|
+
setRangeStyle(wb, ws, range, { font });
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Range-level shortcut for `setCellNumberFormat`. Stamps the same format-code
|
|
1246
|
+
* onto every cell in the range; the numFmt pool dedups the code so callers
|
|
1247
|
+
* don't pay per-cell pool churn.
|
|
1248
|
+
*/
|
|
1249
|
+
function setRangeNumberFormat(wb, ws, range, formatCode) {
|
|
1250
|
+
setRangeStyle(wb, ws, range, { numberFormat: formatCode });
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Range-level shortcut for `setCellProtection`. Stamps the same Protection
|
|
1254
|
+
* (locked / hidden) onto every cell in the range. Pass a full `Protection`
|
|
1255
|
+
* value or a partial — partials default missing fields to `false` per Excel's
|
|
1256
|
+
* `<protection>` semantics.
|
|
1257
|
+
*
|
|
1258
|
+
* Common usage: `setRangeProtection(wb, ws, 'B2:B100', { locked: false })` to
|
|
1259
|
+
* leave just an input column editable when the sheet is protected.
|
|
1260
|
+
*/
|
|
1261
|
+
function setRangeProtection(wb, ws, range, protection) {
|
|
1262
|
+
setRangeStyle(wb, ws, range, { protection: Object.isFrozen(protection) ? protection : {
|
|
1263
|
+
locked: protection.locked ?? false,
|
|
1264
|
+
hidden: protection.hidden ?? false
|
|
1265
|
+
} });
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Range-level shortcut for `wrapCellText`. Toggles "Wrap Text" on every cell in
|
|
1269
|
+
* the range while preserving each cell's existing alignment (horizontal /
|
|
1270
|
+
* vertical / textRotation / indent are not touched). Empty cells in the range
|
|
1271
|
+
* are materialised so the alignment patch is observable on round-trip.
|
|
1272
|
+
*/
|
|
1273
|
+
function setRangeWrapText(wb, ws, range, on = true) {
|
|
1274
|
+
reserveDefaultXfSlot(wb);
|
|
1275
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1276
|
+
for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) {
|
|
1277
|
+
let cell = ws.rows.get(r)?.get(c);
|
|
1278
|
+
if (!cell) cell = setCell(ws, r, c);
|
|
1279
|
+
wrapCellText(wb, cell, on);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Range-level Alignment setter. Two modes:
|
|
1284
|
+
*
|
|
1285
|
+
* - `mode: 'merge'` (default) — each cell's existing alignment is
|
|
1286
|
+
* preserved; the supplied partial overlays it. Use this when you
|
|
1287
|
+
* want to set just `horizontal` or `vertical` without wiping the
|
|
1288
|
+
* other axes.
|
|
1289
|
+
* - `mode: 'replace'` — each cell's alignment is **wholly replaced**
|
|
1290
|
+
* by the supplied value. Indent / textRotation / wrapText that
|
|
1291
|
+
* weren't supplied are dropped.
|
|
1292
|
+
*
|
|
1293
|
+
* Empty cells in the range are materialised so the patch is observable on
|
|
1294
|
+
* round-trip.
|
|
1295
|
+
*/
|
|
1296
|
+
function setRangeAlignment(wb, ws, range, alignment, mode = "merge") {
|
|
1297
|
+
reserveDefaultXfSlot(wb);
|
|
1298
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1299
|
+
if (mode === "replace") {
|
|
1300
|
+
setRangeStyle(wb, ws, range, { alignment: makeAlignment(alignment) });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) {
|
|
1304
|
+
let cell = ws.rows.get(r)?.get(c);
|
|
1305
|
+
if (!cell) cell = setCell(ws, r, c);
|
|
1306
|
+
const cur = currentXf(wb.styles, cell).alignment;
|
|
1307
|
+
setCellAlignment(wb, cell, mergeAlignment(cur, alignment));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const mergeFont = (current, patch) => makeFont({
|
|
1311
|
+
...current,
|
|
1312
|
+
...patch
|
|
1313
|
+
});
|
|
1314
|
+
/** Toggle bold on a cell. Preserves other font fields. */
|
|
1315
|
+
function setBold(wb, c, on = true) {
|
|
1316
|
+
setCellFont(wb, c, mergeFont(getCellFont(wb, c), { bold: on }));
|
|
1317
|
+
}
|
|
1318
|
+
/** Toggle italic on a cell. */
|
|
1319
|
+
function setItalic(wb, c, on = true) {
|
|
1320
|
+
setCellFont(wb, c, mergeFont(getCellFont(wb, c), { italic: on }));
|
|
1321
|
+
}
|
|
1322
|
+
/** Toggle strike-through on a cell. */
|
|
1323
|
+
function setStrikethrough(wb, c, on = true) {
|
|
1324
|
+
setCellFont(wb, c, mergeFont(getCellFont(wb, c), { strike: on }));
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Set the underline style. Pass `false` to drop underline; pass `'single' |
|
|
1328
|
+
* 'double' | 'singleAccounting' | 'doubleAccounting'` to apply that style; pass
|
|
1329
|
+
* `true` for the most common single-line.
|
|
1330
|
+
*/
|
|
1331
|
+
function setUnderline(wb, c, style = "single") {
|
|
1332
|
+
const { underline: _drop, ...rest } = getCellFont(wb, c);
|
|
1333
|
+
if (style === false) {
|
|
1334
|
+
setCellFont(wb, c, makeFont(rest));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const u = style === true ? "single" : style;
|
|
1338
|
+
setCellFont(wb, c, makeFont({
|
|
1339
|
+
...rest,
|
|
1340
|
+
underline: u
|
|
1341
|
+
}));
|
|
1342
|
+
}
|
|
1343
|
+
/** Set the font size in points (e.g. 14). Preserves other fields. */
|
|
1344
|
+
function setFontSize(wb, c, size) {
|
|
1345
|
+
setCellFont(wb, c, mergeFont(getCellFont(wb, c), { size }));
|
|
1346
|
+
}
|
|
1347
|
+
/** Set the font family name (e.g. "Arial"). Preserves other fields. */
|
|
1348
|
+
function setFontName(wb, c, name) {
|
|
1349
|
+
setCellFont(wb, c, mergeFont(getCellFont(wb, c), { name }));
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Set the font color. Accepts a hex string ("FFAA0033") or a partial `Color`
|
|
1353
|
+
* object (`{ theme: 4, tint: 0.4 }`). Preserves other font fields.
|
|
1354
|
+
*/
|
|
1355
|
+
function setFontColor(wb, c, color) {
|
|
1356
|
+
const colorObj = typeof color === "string" ? makeColor({ rgb: color }) : makeColor(color);
|
|
1357
|
+
setCellFont(wb, c, mergeFont(getCellFont(wb, c), { color: colorObj }));
|
|
1358
|
+
}
|
|
1359
|
+
const mergeAlignment = (current, patch) => {
|
|
1360
|
+
return makeAlignment({
|
|
1361
|
+
...current,
|
|
1362
|
+
...patch
|
|
1363
|
+
});
|
|
1364
|
+
};
|
|
1365
|
+
/**
|
|
1366
|
+
* Center a cell horizontally + vertically. Mirrors Excel's "Merge & Center" UI
|
|
1367
|
+
* button (without the merge — see {@link mergeCells} for that). Preserves any
|
|
1368
|
+
* other alignment fields already present.
|
|
1369
|
+
*/
|
|
1370
|
+
function centerCell(wb, c) {
|
|
1371
|
+
const cur = currentXf(wb.styles, c).alignment;
|
|
1372
|
+
setCellAlignment(wb, c, mergeAlignment(cur, {
|
|
1373
|
+
horizontal: "center",
|
|
1374
|
+
vertical: "center"
|
|
1375
|
+
}));
|
|
1376
|
+
}
|
|
1377
|
+
/** Toggle "Wrap Text" on a cell, preserving other alignment fields. */
|
|
1378
|
+
function wrapCellText(wb, c, wrap = true) {
|
|
1379
|
+
const cur = currentXf(wb.styles, c).alignment;
|
|
1380
|
+
setCellAlignment(wb, c, mergeAlignment(cur, { wrapText: wrap }));
|
|
1381
|
+
}
|
|
1382
|
+
/** Set the horizontal alignment in isolation. */
|
|
1383
|
+
function alignCellHorizontal(wb, c, horizontal) {
|
|
1384
|
+
const cur = currentXf(wb.styles, c).alignment;
|
|
1385
|
+
setCellAlignment(wb, c, mergeAlignment(cur, { horizontal }));
|
|
1386
|
+
}
|
|
1387
|
+
/** Set the vertical alignment in isolation. */
|
|
1388
|
+
function alignCellVertical(wb, c, vertical) {
|
|
1389
|
+
const cur = currentXf(wb.styles, c).alignment;
|
|
1390
|
+
setCellAlignment(wb, c, mergeAlignment(cur, { vertical }));
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Rotate the cell's text. `degrees` accepts 0..180 (clockwise) or 255 for
|
|
1394
|
+
* Excel's "vertical stacked" mode. Mirrors the rotate icons in the alignment
|
|
1395
|
+
* ribbon.
|
|
1396
|
+
*/
|
|
1397
|
+
function rotateCellText(wb, c, degrees) {
|
|
1398
|
+
const cur = currentXf(wb.styles, c).alignment;
|
|
1399
|
+
setCellAlignment(wb, c, mergeAlignment(cur, { textRotation: degrees }));
|
|
1400
|
+
}
|
|
1401
|
+
/** Set or clear the indent level (0..255). */
|
|
1402
|
+
function indentCell(wb, c, levels) {
|
|
1403
|
+
const cur = currentXf(wb.styles, c).alignment;
|
|
1404
|
+
setCellAlignment(wb, c, mergeAlignment(cur, { indent: levels }));
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Format a cell as currency. Produces one of:
|
|
1408
|
+
* - default → `"$#,##0.00"` (US dollar, 2 decimals)
|
|
1409
|
+
* - `{ symbol: "€" }` → `"€#,##0.00"`
|
|
1410
|
+
* - `{ symbol: "¥", decimals: 0 }` → `"¥#,##0"`
|
|
1411
|
+
* - `{ accounting: true }` → `"_-$* #,##0.00_-;-$* #,##0.00_-;_-$* \"-\"??_-;_-@_-"`
|
|
1412
|
+
* (Excel's "Accounting" subtype with right-aligned symbol).
|
|
1413
|
+
*/
|
|
1414
|
+
function setCellAsCurrency(wb, c, opts = {}) {
|
|
1415
|
+
const symbol = opts.symbol ?? "$";
|
|
1416
|
+
const decimals = opts.decimals ?? 2;
|
|
1417
|
+
const decTail = decimals > 0 ? `.${"0".repeat(decimals)}` : "";
|
|
1418
|
+
setCellNumberFormat(wb, c, opts.accounting ? `_-${symbol}* #,##0${decTail}_-;-${symbol}* #,##0${decTail}_-;_-${symbol}* "-"${"?".repeat(decimals)}_-;_-@_-` : `${symbol}#,##0${decTail}`);
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Format a cell as a percentage. `decimals` defaults to 0 → `"0%"`; `decimals:
|
|
1422
|
+
* 2` → `"0.00%"`. The cell value is multiplied by 100 by Excel during display.
|
|
1423
|
+
*/
|
|
1424
|
+
function setCellAsPercent(wb, c, decimals = 0) {
|
|
1425
|
+
if (!Number.isInteger(decimals) || decimals < 0) throw new OpenXmlSchemaError(`setCellAsPercent: decimals must be a non-negative integer; got ${decimals}`);
|
|
1426
|
+
setCellNumberFormat(wb, c, decimals === 0 ? "0%" : `0.${"0".repeat(decimals)}%`);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Format a cell as a date. `format` defaults to Excel's default
|
|
1430
|
+
* locale-independent ISO-style date `"yyyy-mm-dd"`. Common alternatives:
|
|
1431
|
+
* `"m/d/yyyy"`, `"dd-mmm-yy"`, `"yyyy-mm-dd hh:mm:ss"`.
|
|
1432
|
+
*/
|
|
1433
|
+
function setCellAsDate(wb, c, format = "yyyy-mm-dd") {
|
|
1434
|
+
setCellNumberFormat(wb, c, format);
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Format a cell as a thousands-separated number. `decimals` defaults to 0 →
|
|
1438
|
+
* `"#,##0"`; `decimals: 2` → `"#,##0.00"`.
|
|
1439
|
+
*/
|
|
1440
|
+
function setCellAsNumber(wb, c, decimals = 0) {
|
|
1441
|
+
if (!Number.isInteger(decimals) || decimals < 0) throw new OpenXmlSchemaError(`setCellAsNumber: decimals must be a non-negative integer; got ${decimals}`);
|
|
1442
|
+
setCellNumberFormat(wb, c, decimals === 0 ? "#,##0" : `#,##0.${"0".repeat(decimals)}`);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Apply Excel's stock "table header" formatting to a range: bold white text on
|
|
1446
|
+
* a dark fill, plus a thick bottom border. Override any axis via `opts` — pass
|
|
1447
|
+
* `bold: false` to drop the bold, or `fillColor: 'FF305496'` for a different
|
|
1448
|
+
* shade. Defaults match Excel's "Table Style Medium 2" header row.
|
|
1449
|
+
*/
|
|
1450
|
+
function formatAsHeader(wb, ws, range, opts = {}) {
|
|
1451
|
+
const fillColor = opts.fillColor === void 0 ? "FF305496" : typeof opts.fillColor === "string" ? makeColor({ rgb: opts.fillColor }) : makeColor(opts.fillColor);
|
|
1452
|
+
const fontColor = opts.fontColor === void 0 ? "FFFFFFFF" : typeof opts.fontColor === "string" ? makeColor({ rgb: opts.fontColor }) : makeColor(opts.fontColor);
|
|
1453
|
+
const bold = opts.bold ?? true;
|
|
1454
|
+
const borderStyle = opts.bottomBorder ?? "medium";
|
|
1455
|
+
const borderColorObj = opts.bottomBorderColor === void 0 ? void 0 : typeof opts.bottomBorderColor === "string" ? makeColor({ rgb: opts.bottomBorderColor }) : makeColor(opts.bottomBorderColor);
|
|
1456
|
+
const fillColorObj = typeof fillColor === "string" ? makeColor({ rgb: fillColor }) : fillColor;
|
|
1457
|
+
const styleOpts = {
|
|
1458
|
+
font: makeFont({
|
|
1459
|
+
bold,
|
|
1460
|
+
color: typeof fontColor === "string" ? makeColor({ rgb: fontColor }) : fontColor
|
|
1461
|
+
}),
|
|
1462
|
+
fill: makePatternFill({
|
|
1463
|
+
patternType: "solid",
|
|
1464
|
+
fgColor: fillColorObj
|
|
1465
|
+
})
|
|
1466
|
+
};
|
|
1467
|
+
if (borderStyle !== false) styleOpts.border = makeBorder({ bottom: makeSide({
|
|
1468
|
+
style: borderStyle,
|
|
1469
|
+
...borderColorObj ? { color: borderColorObj } : {}
|
|
1470
|
+
}) });
|
|
1471
|
+
setRangeStyle(wb, ws, range, styleOpts);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Apply a built-in Excel style ("Heading 1" / "Total" / "Good" / "Bad" /
|
|
1475
|
+
* "Calculation" / etc.) to a single cell. Registers the built-in on the
|
|
1476
|
+
* Stylesheet (idempotent) and points the cell's xf at it via `xfId` while
|
|
1477
|
+
* inheriting the matching font/fill/border/ numFmt ids so the cell renders
|
|
1478
|
+
* correctly on its own.
|
|
1479
|
+
*
|
|
1480
|
+
* Throws when `name` isn't in {@link BUILTIN_NAMED_STYLES}; use {@link
|
|
1481
|
+
* applyNamedStyle} for user-registered styles.
|
|
1482
|
+
*/
|
|
1483
|
+
function applyBuiltinStyle(wb, c, name) {
|
|
1484
|
+
applyNamedStyleByXfId(wb, c, ensureBuiltinStyle(wb.styles, name));
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Apply a NamedStyle that's already registered on the workbook (via
|
|
1488
|
+
* `addNamedStyle` or `ensureBuiltinStyle`) to a single cell, by name.
|
|
1489
|
+
*/
|
|
1490
|
+
function applyNamedStyle(wb, c, name) {
|
|
1491
|
+
const entry = wb.styles._namedStyleByName?.get(name);
|
|
1492
|
+
if (entry === void 0) throw new OpenXmlSchemaError(`applyNamedStyle: no named style "${name}" registered`);
|
|
1493
|
+
applyNamedStyleByXfId(wb, c, entry.xfId);
|
|
1494
|
+
}
|
|
1495
|
+
const applyNamedStyleByXfId = (wb, c, xfId) => {
|
|
1496
|
+
const styleXf = wb.styles.cellStyleXfs[xfId];
|
|
1497
|
+
if (!styleXf) throw new OpenXmlSchemaError(`applyNamedStyle: cellStyleXfs[${xfId}] missing`);
|
|
1498
|
+
const patch = {
|
|
1499
|
+
xfId,
|
|
1500
|
+
fontId: styleXf.fontId,
|
|
1501
|
+
fillId: styleXf.fillId,
|
|
1502
|
+
borderId: styleXf.borderId,
|
|
1503
|
+
numFmtId: styleXf.numFmtId,
|
|
1504
|
+
applyFont: true,
|
|
1505
|
+
applyFill: true,
|
|
1506
|
+
applyBorder: true,
|
|
1507
|
+
applyNumberFormat: true
|
|
1508
|
+
};
|
|
1509
|
+
if (styleXf.alignment !== void 0) {
|
|
1510
|
+
patch.alignment = styleXf.alignment;
|
|
1511
|
+
patch.applyAlignment = true;
|
|
1512
|
+
}
|
|
1513
|
+
if (styleXf.protection !== void 0) {
|
|
1514
|
+
patch.protection = styleXf.protection;
|
|
1515
|
+
patch.applyProtection = true;
|
|
1516
|
+
}
|
|
1517
|
+
applyXfPatch(wb, c, patch);
|
|
1518
|
+
};
|
|
1519
|
+
/**
|
|
1520
|
+
* Apply the same {@link SideStyle} to all four edges of a single cell. Optional
|
|
1521
|
+
* color via hex string or `Color` partial. Equivalent to `setCellBorder(wb, c,
|
|
1522
|
+
* makeBorder({ left, right, top, bottom: side }))` with all four sides
|
|
1523
|
+
* identical.
|
|
1524
|
+
*/
|
|
1525
|
+
function setCellBorderAll(wb, c, opts = { style: "thin" }) {
|
|
1526
|
+
const colorObj = opts.color === void 0 ? void 0 : typeof opts.color === "string" ? makeColor({ rgb: opts.color }) : makeColor(opts.color);
|
|
1527
|
+
const side = makeSide({
|
|
1528
|
+
style: opts.style,
|
|
1529
|
+
...colorObj ? { color: colorObj } : {}
|
|
1530
|
+
});
|
|
1531
|
+
setCellBorder(wb, c, makeBorder({
|
|
1532
|
+
left: side,
|
|
1533
|
+
right: side,
|
|
1534
|
+
top: side,
|
|
1535
|
+
bottom: side
|
|
1536
|
+
}));
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Draw an outer border around a rectangular range. Cells on the perimeter
|
|
1540
|
+
* receive a partial border (only the edges that face outside the range); inner
|
|
1541
|
+
* cells are unaffected unless `inner` is provided, in which case every cell in
|
|
1542
|
+
* the range receives a border combining its perimeter edges with the `inner`
|
|
1543
|
+
* style for the inside edges.
|
|
1544
|
+
*/
|
|
1545
|
+
function setRangeBorderBox(wb, ws, range, opts = { style: "thin" }) {
|
|
1546
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1547
|
+
const colorObj = opts.color === void 0 ? void 0 : typeof opts.color === "string" ? makeColor({ rgb: opts.color }) : makeColor(opts.color);
|
|
1548
|
+
const outer = makeSide({
|
|
1549
|
+
style: opts.style,
|
|
1550
|
+
...colorObj ? { color: colorObj } : {}
|
|
1551
|
+
});
|
|
1552
|
+
const inner = opts.inner !== void 0 ? makeSide({
|
|
1553
|
+
style: opts.inner,
|
|
1554
|
+
...colorObj ? { color: colorObj } : {}
|
|
1555
|
+
}) : void 0;
|
|
1556
|
+
for (let r = minRow; r <= maxRow; r++) for (let col = minCol; col <= maxCol; col++) {
|
|
1557
|
+
const onTop = r === minRow;
|
|
1558
|
+
const onBottom = r === maxRow;
|
|
1559
|
+
const onLeft = col === minCol;
|
|
1560
|
+
const onRight = col === maxCol;
|
|
1561
|
+
if (!inner && !onTop && !onBottom && !onLeft && !onRight) continue;
|
|
1562
|
+
const sides = {};
|
|
1563
|
+
const top = onTop ? outer : inner;
|
|
1564
|
+
const bottom = onBottom ? outer : inner;
|
|
1565
|
+
const left = onLeft ? outer : inner;
|
|
1566
|
+
const right = onRight ? outer : inner;
|
|
1567
|
+
if (top !== void 0) sides.top = top;
|
|
1568
|
+
if (bottom !== void 0) sides.bottom = bottom;
|
|
1569
|
+
if (left !== void 0) sides.left = left;
|
|
1570
|
+
if (right !== void 0) sides.right = right;
|
|
1571
|
+
let cell = ws.rows.get(r)?.get(col);
|
|
1572
|
+
if (!cell) cell = setCell(ws, r, col);
|
|
1573
|
+
setCellBorder(wb, cell, makeBorder(sides));
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
//#endregion
|
|
1577
|
+
export { ensureBuiltinStyle as $, setCellFill as A, makeNumberFormat as At, setRangeBackgroundColor as B, makeGradientFill as Bt, setCellAsCurrency as C, FORMAT_TEXT as Ct, setCellBackgroundColor as D, isBuiltinFormat as Dt, setCellAsPercent as E, classifyDateFormat as Et, setFontColor as F, fontToCss as Ft, setRangeStyle as G, makeBorder as Gt, setRangeFont as H, makePatternFill as Ht, setFontName as I, makeFont as It, setUnderline as J, VERTICAL_ALIGNMENTS as Jt, setRangeWrapText as K, makeSide as Kt, setFontSize as L, PATTERN_TYPES as Lt, setCellNumberFormat as M, FONT_SCHEMES as Mt, setCellProtection as N, UNDERLINE_STYLES as Nt, setCellBorder as O, isDateFormat as Ot, setCellStyle as P, VERT_ALIGNS as Pt, addNamedStyle as Q, setItalic as R, fillToCss as Rt, setCellAlignment as S, FORMAT_PERCENTAGE_00 as St, setCellAsNumber as T, builtinFormatId as Tt, setRangeNumberFormat as U, SIDE_STYLES as Ut, setRangeBorderBox as V, makeGradientStop as Vt, setRangeProtection as W, borderToCss as Wt, makeProtection as X, makeAlignment as Xt, wrapCellText as Y, alignmentToCss as Yt, BUILTIN_NAMED_STYLES as Z, stableStringify as Zt, getCellNumberFormat as _, FORMAT_DATE_YYYYMMDD2 as _t, cellStyleToCss as a, addNumFmt as at, rotateCellText as b, FORMAT_NUMBER_00 as bt, clearCellStyle as c, listCellStyleXfs as ct, copyCellStyle as d, listFonts as dt, addBorder as et, formatAsHeader as f, makeStylesheet as ft, getCellFont as g, FORMAT_DATE_TIMEDELTA as gt, getCellFill as h, FORMAT_DATE_DATETIME as ht, applyNamedStyle as i, addFont as it, setCellFont as j, DEFAULT_FONT as jt, setCellBorderAll as k, isTimedeltaFormat as kt, clearRangeStyle as l, listCellXfs as lt, getCellBorder as m, BUILTIN_FORMATS_MAX_SIZE as mt, alignCellVertical as n, addCellXf as nt, centerCell as o, defaultCellXf as ot, getCellAlignment as p, BUILTIN_FORMATS as pt, setStrikethrough as q, HORIZONTAL_ALIGNMENTS as qt, applyBuiltinStyle as r, addFill as rt, clearCellBackground as s, listBorders as st, alignCellHorizontal as t, addCellStyleXf as tt, cloneCellStyle as u, listFills as ut, getCellProtection as v, FORMAT_GENERAL as vt, setCellAsDate as w, builtinFormatCode as wt, setBold as x, FORMAT_PERCENTAGE as xt, indentCell as y, FORMAT_NUMBER as yt, setRangeAlignment as z, makeFill as zt };
|
|
1578
|
+
|
|
1579
|
+
//# sourceMappingURL=cell-style-BEDjMX1y.mjs.map
|