@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,1726 @@
|
|
|
1
|
+
import { o as OpenXmlSchemaError } from "./exceptions-D-CFwxgm.mjs";
|
|
2
|
+
import { a as columnLetterFromIndex, g as tupleToCoordinate, h as rangeBoundaries, i as columnIndexFromLetter, n as MAX_ROW, r as boundariesToRangeString, s as coordinateToTuple, t as MAX_COL } from "./coordinate-96Ecci4d.mjs";
|
|
3
|
+
import { o as cellValueAsString, y as makeCell } from "./cell-D9CaNKnU.mjs";
|
|
4
|
+
//#region src/worksheet/cell-range.ts
|
|
5
|
+
/** Build a CellRange from explicit 1-based bounds. */
|
|
6
|
+
function makeCellRange(minRow, minCol, maxRow, maxCol) {
|
|
7
|
+
if (!Number.isInteger(minRow) || !Number.isInteger(minCol) || !Number.isInteger(maxRow) || !Number.isInteger(maxCol)) throw new OpenXmlSchemaError("CellRange bounds must be integers");
|
|
8
|
+
if (minRow < 1 || maxRow < 1 || minRow > 1048576 || maxRow > 1048576) throw new OpenXmlSchemaError(`CellRange row bounds must be in [1, ${MAX_ROW}]`);
|
|
9
|
+
if (minCol < 1 || maxCol < 1 || minCol > 16384 || maxCol > 16384) throw new OpenXmlSchemaError(`CellRange col bounds must be in [1, ${MAX_COL}]`);
|
|
10
|
+
return {
|
|
11
|
+
minRow: Math.min(minRow, maxRow),
|
|
12
|
+
minCol: Math.min(minCol, maxCol),
|
|
13
|
+
maxRow: Math.max(minRow, maxRow),
|
|
14
|
+
maxCol: Math.max(minCol, maxCol)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Parse a range expression — wraps {@link rangeBoundaries}. */
|
|
18
|
+
function parseRange(input) {
|
|
19
|
+
return rangeBoundaries(input);
|
|
20
|
+
}
|
|
21
|
+
/** Format a CellRange back into the canonical OOXML string. */
|
|
22
|
+
function rangeToString(r) {
|
|
23
|
+
return boundariesToRangeString(r);
|
|
24
|
+
}
|
|
25
|
+
/** Inclusive containment of a single (row, col) within a range. */
|
|
26
|
+
function rangeContainsCell(r, row, col) {
|
|
27
|
+
return row >= r.minRow && row <= r.maxRow && col >= r.minCol && col <= r.maxCol;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A1-string convenience for {@link rangeContainsCell}. Parses `cellRef` (e.g.
|
|
31
|
+
* `"B3"`) and `rangeRef` (e.g. `"A1:C5"`) and returns `true` iff the cell sits
|
|
32
|
+
* inside the range (boundary-inclusive). Throws when either input is malformed.
|
|
33
|
+
*/
|
|
34
|
+
function isCellInRange(cellRef, rangeRef) {
|
|
35
|
+
const { col, row } = coordinateToTuple(cellRef);
|
|
36
|
+
return rangeContainsCell(parseRange(rangeRef), row, col);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A1-string convenience for {@link rangeContainsRange}. Returns `true` iff the
|
|
40
|
+
* `inner` range is wholly contained by `outer` (boundary-inclusive).
|
|
41
|
+
* Single-cell refs are accepted on either side via parseRange. Throws on
|
|
42
|
+
* malformed input.
|
|
43
|
+
*/
|
|
44
|
+
function isRangeInRange(inner, outer) {
|
|
45
|
+
return rangeContainsRange(parseRange(outer), parseRange(inner));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Expand (or shrink) an A1 range by adding `deltaRows` to its bottom edge and
|
|
49
|
+
* `deltaCols` to its right edge. The top-left corner is preserved. Negative
|
|
50
|
+
* deltas shrink the range; the result must still have at least 1 row and 1
|
|
51
|
+
* column (otherwise throws).
|
|
52
|
+
*
|
|
53
|
+
* Useful for "this is the data range — also reserve room for a totals row" or
|
|
54
|
+
* "include one more column to the right" patterns.
|
|
55
|
+
*/
|
|
56
|
+
function expandRangeStr(range, deltaRows, deltaCols) {
|
|
57
|
+
if (!Number.isInteger(deltaRows) || !Number.isInteger(deltaCols)) throw new OpenXmlSchemaError("expandRangeStr: deltas must be integers");
|
|
58
|
+
const r = parseRange(range);
|
|
59
|
+
return rangeToString(makeCellRange(r.minRow, r.minCol, r.maxRow + deltaRows, r.maxCol + deltaCols));
|
|
60
|
+
}
|
|
61
|
+
/** Inclusive containment of `inner` within `outer`. */
|
|
62
|
+
function rangeContainsRange(outer, inner) {
|
|
63
|
+
return inner.minRow >= outer.minRow && inner.maxRow <= outer.maxRow && inner.minCol >= outer.minCol && inner.maxCol <= outer.maxCol;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Shift a range by (dr, dc) integer offsets. The returned range is clamped to
|
|
67
|
+
* the OOXML grid; callers that want hard bounds should pass values that keep
|
|
68
|
+
* the result inside the spec.
|
|
69
|
+
*/
|
|
70
|
+
function shiftRange(r, dr, dc) {
|
|
71
|
+
if (!Number.isInteger(dr) || !Number.isInteger(dc)) throw new OpenXmlSchemaError("shiftRange: dr / dc must be integers");
|
|
72
|
+
return makeCellRange(r.minRow + dr, r.minCol + dc, r.maxRow + dr, r.maxCol + dc);
|
|
73
|
+
}
|
|
74
|
+
/** Bounding-box union of two ranges. Always non-null. */
|
|
75
|
+
function unionRange(a, b) {
|
|
76
|
+
return {
|
|
77
|
+
minRow: Math.min(a.minRow, b.minRow),
|
|
78
|
+
minCol: Math.min(a.minCol, b.minCol),
|
|
79
|
+
maxRow: Math.max(a.maxRow, b.maxRow),
|
|
80
|
+
maxCol: Math.max(a.maxCol, b.maxCol)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Returns the rectangular intersection of two ranges, or `null` when disjoint. */
|
|
84
|
+
function intersectionRange(a, b) {
|
|
85
|
+
const minRow = Math.max(a.minRow, b.minRow);
|
|
86
|
+
const minCol = Math.max(a.minCol, b.minCol);
|
|
87
|
+
const maxRow = Math.min(a.maxRow, b.maxRow);
|
|
88
|
+
const maxCol = Math.min(a.maxCol, b.maxCol);
|
|
89
|
+
if (minRow > maxRow || minCol > maxCol) return null;
|
|
90
|
+
return {
|
|
91
|
+
minRow,
|
|
92
|
+
minCol,
|
|
93
|
+
maxRow,
|
|
94
|
+
maxCol
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** True iff two ranges share at least one cell. */
|
|
98
|
+
function rangesOverlap(a, b) {
|
|
99
|
+
return intersectionRange(a, b) !== null;
|
|
100
|
+
}
|
|
101
|
+
/** Inclusive cell count covered by a range. */
|
|
102
|
+
function rangeArea(r) {
|
|
103
|
+
return (r.maxRow - r.minRow + 1) * (r.maxCol - r.minCol + 1);
|
|
104
|
+
}
|
|
105
|
+
/** Parse an sqref string: `"A1:B2 D5 E10:F20"`. Whitespace-delimited. */
|
|
106
|
+
function parseMultiCellRange(input) {
|
|
107
|
+
const ranges = [];
|
|
108
|
+
for (const piece of input.split(/\s+/)) {
|
|
109
|
+
if (piece.length === 0) continue;
|
|
110
|
+
ranges.push(parseRange(piece));
|
|
111
|
+
}
|
|
112
|
+
return { ranges };
|
|
113
|
+
}
|
|
114
|
+
/** Format a MultiCellRange back into an sqref string. */
|
|
115
|
+
function multiCellRangeToString(m) {
|
|
116
|
+
return m.ranges.map(rangeToString).join(" ");
|
|
117
|
+
}
|
|
118
|
+
/** True iff any contained range covers (row, col). */
|
|
119
|
+
function multiCellRangeContainsCell(m, row, col) {
|
|
120
|
+
for (const r of m.ranges) if (rangeContainsCell(r, row, col)) return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/styles/colors.ts
|
|
125
|
+
/**
|
|
126
|
+
* Legacy 64-entry palette indexed colours fall back to. Verbatim from
|
|
127
|
+
* openpyxl/openpyxl/styles/colors.py — must not be reordered.
|
|
128
|
+
*/
|
|
129
|
+
const COLOR_INDEX = Object.freeze([
|
|
130
|
+
"00000000",
|
|
131
|
+
"00FFFFFF",
|
|
132
|
+
"00FF0000",
|
|
133
|
+
"0000FF00",
|
|
134
|
+
"000000FF",
|
|
135
|
+
"00FFFF00",
|
|
136
|
+
"00FF00FF",
|
|
137
|
+
"0000FFFF",
|
|
138
|
+
"00000000",
|
|
139
|
+
"00FFFFFF",
|
|
140
|
+
"00FF0000",
|
|
141
|
+
"0000FF00",
|
|
142
|
+
"000000FF",
|
|
143
|
+
"00FFFF00",
|
|
144
|
+
"00FF00FF",
|
|
145
|
+
"0000FFFF",
|
|
146
|
+
"00800000",
|
|
147
|
+
"00008000",
|
|
148
|
+
"00000080",
|
|
149
|
+
"00808000",
|
|
150
|
+
"00800080",
|
|
151
|
+
"00008080",
|
|
152
|
+
"00C0C0C0",
|
|
153
|
+
"00808080",
|
|
154
|
+
"009999FF",
|
|
155
|
+
"00993366",
|
|
156
|
+
"00FFFFCC",
|
|
157
|
+
"00CCFFFF",
|
|
158
|
+
"00660066",
|
|
159
|
+
"00FF8080",
|
|
160
|
+
"000066CC",
|
|
161
|
+
"00CCCCFF",
|
|
162
|
+
"00000080",
|
|
163
|
+
"00FF00FF",
|
|
164
|
+
"00FFFF00",
|
|
165
|
+
"0000FFFF",
|
|
166
|
+
"00800080",
|
|
167
|
+
"00800000",
|
|
168
|
+
"00008080",
|
|
169
|
+
"000000FF",
|
|
170
|
+
"0000CCFF",
|
|
171
|
+
"00CCFFFF",
|
|
172
|
+
"00CCFFCC",
|
|
173
|
+
"00FFFF99",
|
|
174
|
+
"0099CCFF",
|
|
175
|
+
"00FF99CC",
|
|
176
|
+
"00CC99FF",
|
|
177
|
+
"00FFCC99",
|
|
178
|
+
"003366FF",
|
|
179
|
+
"0033CCCC",
|
|
180
|
+
"0099CC00",
|
|
181
|
+
"00FFCC00",
|
|
182
|
+
"00FF9900",
|
|
183
|
+
"00FF6600",
|
|
184
|
+
"00666699",
|
|
185
|
+
"00969696",
|
|
186
|
+
"00003366",
|
|
187
|
+
"00339966",
|
|
188
|
+
"00003300",
|
|
189
|
+
"00333300",
|
|
190
|
+
"00993300",
|
|
191
|
+
"00993366",
|
|
192
|
+
"00333399",
|
|
193
|
+
"00333333"
|
|
194
|
+
]);
|
|
195
|
+
const ARGB_RE = /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6})$/;
|
|
196
|
+
/**
|
|
197
|
+
* Normalise an aRGB hex string. Accepts either 6 or 8 hex digits; 6-digit input
|
|
198
|
+
* is padded to 8 by prefixing `00` (alpha=0 = fully opaque per Excel
|
|
199
|
+
* convention). Returns the canonical uppercase form.
|
|
200
|
+
*/
|
|
201
|
+
function normaliseRgb(value) {
|
|
202
|
+
if (typeof value !== "string" || !ARGB_RE.test(value)) throw new OpenXmlSchemaError(`Color rgb must be 6 or 8 hex digits; got "${value}"`);
|
|
203
|
+
return (value.length === 6 ? `00${value}` : value).toUpperCase();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build an immutable {@link Color}. Validates ranges (indexed in [0, 65], tint
|
|
207
|
+
* in [-1, 1]) and normalises rgb hex.
|
|
208
|
+
*/
|
|
209
|
+
function makeColor(opts = {}) {
|
|
210
|
+
const out = {};
|
|
211
|
+
if (opts.rgb !== void 0) out.rgb = normaliseRgb(opts.rgb);
|
|
212
|
+
if (opts.indexed !== void 0) {
|
|
213
|
+
if (!Number.isInteger(opts.indexed) || opts.indexed < 0 || opts.indexed > 65) throw new OpenXmlSchemaError(`Color indexed must be in [0, 65]; got ${opts.indexed}`);
|
|
214
|
+
out.indexed = opts.indexed;
|
|
215
|
+
}
|
|
216
|
+
if (opts.theme !== void 0) {
|
|
217
|
+
if (!Number.isInteger(opts.theme) || opts.theme < 0) throw new OpenXmlSchemaError(`Color theme must be a non-negative integer; got ${opts.theme}`);
|
|
218
|
+
out.theme = opts.theme;
|
|
219
|
+
}
|
|
220
|
+
if (opts.auto !== void 0) out.auto = opts.auto;
|
|
221
|
+
if (opts.tint !== void 0) {
|
|
222
|
+
if (!Number.isFinite(opts.tint) || opts.tint < -1 || opts.tint > 1) throw new OpenXmlSchemaError(`Color tint must be in [-1, 1]; got ${opts.tint}`);
|
|
223
|
+
out.tint = opts.tint;
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze(out);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolve `indexed` references against {@link COLOR_INDEX}. Returns undefined
|
|
229
|
+
* for 64/65 (system fg/bg, not in the palette) or out-of-range.
|
|
230
|
+
*/
|
|
231
|
+
function resolveIndexedColor(idx) {
|
|
232
|
+
return COLOR_INDEX[idx];
|
|
233
|
+
}
|
|
234
|
+
/** Shortcut for the common opaque solid colour. */
|
|
235
|
+
function rgbColor(hex) {
|
|
236
|
+
return makeColor({ rgb: hex });
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Read a {@link Color} value-object back to a normalised ARGB hex. Resolution
|
|
240
|
+
* order: explicit `rgb` → `indexed` palette lookup. Returns `undefined` for
|
|
241
|
+
* `theme` / `auto` / empty inputs (unresolvable without a theme), so callers
|
|
242
|
+
* can fall back to a default.
|
|
243
|
+
*/
|
|
244
|
+
function colorToHex(color) {
|
|
245
|
+
if (!color) return void 0;
|
|
246
|
+
if (color.rgb !== void 0) return normaliseRgb(color.rgb);
|
|
247
|
+
if (color.indexed !== void 0) {
|
|
248
|
+
const rgb = resolveIndexedColor(color.indexed);
|
|
249
|
+
if (rgb) return rgb.toUpperCase();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Compute the relative luminance of an ARGB / RGB hex string per the WCAG 2.x
|
|
254
|
+
* formula. Returns a value in `[0, 1]` where 0 is black and 1 is white. The
|
|
255
|
+
* alpha channel (if present) is ignored.
|
|
256
|
+
*/
|
|
257
|
+
function luminance(hex) {
|
|
258
|
+
const rgb = normaliseRgb(hex);
|
|
259
|
+
const r = Number.parseInt(rgb.slice(2, 4), 16) / 255;
|
|
260
|
+
const g = Number.parseInt(rgb.slice(4, 6), 16) / 255;
|
|
261
|
+
const b = Number.parseInt(rgb.slice(6, 8), 16) / 255;
|
|
262
|
+
const lin = (c) => c <= .03928 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
263
|
+
return .2126 * lin(r) + .7152 * lin(g) + .0722 * lin(b);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* WCAG contrast ratio between two ARGB hex colors. Returns a value in `[1,
|
|
267
|
+
* 21]`; 1 = identical luminance, 21 = pure black on pure white. The order of
|
|
268
|
+
* arguments doesn't matter.
|
|
269
|
+
*/
|
|
270
|
+
function contrastRatio(hexA, hexB) {
|
|
271
|
+
const lA = luminance(hexA);
|
|
272
|
+
const lB = luminance(hexB);
|
|
273
|
+
const [hi, lo] = lA >= lB ? [lA, lB] : [lB, lA];
|
|
274
|
+
return (hi + .05) / (lo + .05);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Pick the higher-contrast text color (`'FF000000'` black or `'FFFFFFFF'`
|
|
278
|
+
* white) for a background hex. Useful when applying a solid fill and wanting
|
|
279
|
+
* the cell text to stay readable.
|
|
280
|
+
*/
|
|
281
|
+
function pickReadableTextColor(backgroundHex) {
|
|
282
|
+
return luminance(backgroundHex) < .179 ? "FFFFFFFF" : "FF000000";
|
|
283
|
+
}
|
|
284
|
+
const splitArgb = (hex) => {
|
|
285
|
+
const rgb = normaliseRgb(hex);
|
|
286
|
+
return {
|
|
287
|
+
a: Number.parseInt(rgb.slice(0, 2), 16),
|
|
288
|
+
r: Number.parseInt(rgb.slice(2, 4), 16),
|
|
289
|
+
g: Number.parseInt(rgb.slice(4, 6), 16),
|
|
290
|
+
b: Number.parseInt(rgb.slice(6, 8), 16)
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
const toHexByte = (n) => {
|
|
294
|
+
return Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0").toUpperCase();
|
|
295
|
+
};
|
|
296
|
+
const clampUnit = (x) => Math.max(0, Math.min(1, x));
|
|
297
|
+
/**
|
|
298
|
+
* Lighten a color by mixing it with white. `amount` is in `[0, 1]`: 0 returns
|
|
299
|
+
* the input unchanged, 1 returns pure white. Alpha channel is preserved.
|
|
300
|
+
* Equivalent to `mixColors(hex, 'FFFFFFFF', amount)`.
|
|
301
|
+
*/
|
|
302
|
+
function lighten(hex, amount) {
|
|
303
|
+
const t = clampUnit(amount);
|
|
304
|
+
const { a, r, g, b } = splitArgb(hex);
|
|
305
|
+
return `${toHexByte(a)}${toHexByte(r + (255 - r) * t)}${toHexByte(g + (255 - g) * t)}${toHexByte(b + (255 - b) * t)}`;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Darken a color by mixing it with black. `amount` is in `[0, 1]`: 0 returns
|
|
309
|
+
* the input unchanged, 1 returns pure black (preserving the alpha channel).
|
|
310
|
+
*/
|
|
311
|
+
function darken(hex, amount) {
|
|
312
|
+
const t = clampUnit(amount);
|
|
313
|
+
const { a, r, g, b } = splitArgb(hex);
|
|
314
|
+
return `${toHexByte(a)}${toHexByte(r * (1 - t))}${toHexByte(g * (1 - t))}${toHexByte(b * (1 - t))}`;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Linearly interpolate between two ARGB colors. `t = 0` returns `hexA`; `t = 1`
|
|
318
|
+
* returns `hexB`; intermediate values mix per channel (including alpha).
|
|
319
|
+
*/
|
|
320
|
+
function mixColors(hexA, hexB, t) {
|
|
321
|
+
const k = clampUnit(t);
|
|
322
|
+
const a = splitArgb(hexA);
|
|
323
|
+
const b = splitArgb(hexB);
|
|
324
|
+
return `${toHexByte(a.a + (b.a - a.a) * k)}${toHexByte(a.r + (b.r - a.r) * k)}${toHexByte(a.g + (b.g - a.g) * k)}${toHexByte(a.b + (b.b - a.b) * k)}`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Convert an ARGB / RGB hex to its HSL representation. Returns `{ h, s, l, a }`
|
|
328
|
+
* with `h ∈ [0, 360)`, `s ∈ [0, 1]`, `l ∈ [0, 1]`, `a ∈ [0, 255]` (alpha as the
|
|
329
|
+
* original byte). Useful for theme tweaking (rotate hue, desaturate, etc.)
|
|
330
|
+
* before round-tripping through {@link hslToHex}.
|
|
331
|
+
*/
|
|
332
|
+
function hexToHsl(hex) {
|
|
333
|
+
const { a, r: rb, g: gb, b: bb } = splitArgb(hex);
|
|
334
|
+
const r = rb / 255;
|
|
335
|
+
const g = gb / 255;
|
|
336
|
+
const b = bb / 255;
|
|
337
|
+
const max = Math.max(r, g, b);
|
|
338
|
+
const min = Math.min(r, g, b);
|
|
339
|
+
const l = (max + min) / 2;
|
|
340
|
+
let h = 0;
|
|
341
|
+
let s = 0;
|
|
342
|
+
if (max !== min) {
|
|
343
|
+
const d = max - min;
|
|
344
|
+
s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
345
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
346
|
+
else if (max === g) h = ((b - r) / d + 2) * 60;
|
|
347
|
+
else h = ((r - g) / d + 4) * 60;
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
h,
|
|
351
|
+
s,
|
|
352
|
+
l,
|
|
353
|
+
a
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Rotate the hue of a color by `degrees` (positive = clockwise). Saturation and
|
|
358
|
+
* lightness are preserved; alpha is preserved. Equivalent to `hexToHsl` →
|
|
359
|
+
* adjust `h` → `hslToHex`.
|
|
360
|
+
*/
|
|
361
|
+
function rotateHue(hex, degrees) {
|
|
362
|
+
const { h, s, l, a } = hexToHsl(hex);
|
|
363
|
+
return hslToHex(h + degrees, s, l, a);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Adjust the saturation of a color by `delta` (added directly to the `[0, 1]`
|
|
367
|
+
* saturation channel and clamped). Positive = more vivid, negative = closer to
|
|
368
|
+
* gray. Hue, lightness, and alpha are preserved.
|
|
369
|
+
*/
|
|
370
|
+
function adjustSaturation(hex, delta) {
|
|
371
|
+
const { h, s, l, a } = hexToHsl(hex);
|
|
372
|
+
return hslToHex(h, s + delta, l, a);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Adjust the lightness of a color by `delta` (added directly to the `[0, 1]`
|
|
376
|
+
* lightness channel and clamped). Positive = lighter, negative = darker.
|
|
377
|
+
* Distinct from {@link lighten} / {@link darken} which mix toward white/black
|
|
378
|
+
* in RGB space.
|
|
379
|
+
*/
|
|
380
|
+
function adjustLightness(hex, delta) {
|
|
381
|
+
const { h, s, l, a } = hexToHsl(hex);
|
|
382
|
+
return hslToHex(h, s, l + delta, a);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Convert HSL components back to an ARGB hex string. `h` wraps mod-360, `s` and
|
|
386
|
+
* `l` clamp to `[0, 1]`. `alpha` is the byte (default 255 = opaque), placed in
|
|
387
|
+
* the high byte of the result.
|
|
388
|
+
*/
|
|
389
|
+
function hslToHex(h, s, l, alpha = 255) {
|
|
390
|
+
const hh = (h % 360 + 360) % 360;
|
|
391
|
+
const ss = clampUnit(s);
|
|
392
|
+
const ll = clampUnit(l);
|
|
393
|
+
const c = (1 - Math.abs(2 * ll - 1)) * ss;
|
|
394
|
+
const x = c * (1 - Math.abs(hh / 60 % 2 - 1));
|
|
395
|
+
const m = ll - c / 2;
|
|
396
|
+
let rp = 0;
|
|
397
|
+
let gp = 0;
|
|
398
|
+
let bp = 0;
|
|
399
|
+
if (hh < 60) {
|
|
400
|
+
rp = c;
|
|
401
|
+
gp = x;
|
|
402
|
+
} else if (hh < 120) {
|
|
403
|
+
rp = x;
|
|
404
|
+
gp = c;
|
|
405
|
+
} else if (hh < 180) {
|
|
406
|
+
gp = c;
|
|
407
|
+
bp = x;
|
|
408
|
+
} else if (hh < 240) {
|
|
409
|
+
gp = x;
|
|
410
|
+
bp = c;
|
|
411
|
+
} else if (hh < 300) {
|
|
412
|
+
rp = x;
|
|
413
|
+
bp = c;
|
|
414
|
+
} else {
|
|
415
|
+
rp = c;
|
|
416
|
+
bp = x;
|
|
417
|
+
}
|
|
418
|
+
return `${toHexByte(Math.max(0, Math.min(255, Math.round(alpha))))}${toHexByte((rp + m) * 255)}${toHexByte((gp + m) * 255)}${toHexByte((bp + m) * 255)}`;
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/worksheet/dimensions.ts
|
|
422
|
+
/** Build a single-column ColumnDimension entry covering `col`. */
|
|
423
|
+
function makeColumnDimension(col, opts = {}) {
|
|
424
|
+
return {
|
|
425
|
+
min: col,
|
|
426
|
+
max: col,
|
|
427
|
+
...opts.width !== void 0 ? { width: opts.width } : {},
|
|
428
|
+
...opts.customWidth !== void 0 ? { customWidth: opts.customWidth } : {},
|
|
429
|
+
...opts.hidden !== void 0 ? { hidden: opts.hidden } : {},
|
|
430
|
+
...opts.bestFit !== void 0 ? { bestFit: opts.bestFit } : {},
|
|
431
|
+
...opts.outlineLevel !== void 0 ? { outlineLevel: opts.outlineLevel } : {},
|
|
432
|
+
...opts.style !== void 0 ? { style: opts.style } : {},
|
|
433
|
+
...opts.collapsed !== void 0 ? { collapsed: opts.collapsed } : {}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function makeRowDimension(opts = {}) {
|
|
437
|
+
return {
|
|
438
|
+
...opts.height !== void 0 ? { height: opts.height } : {},
|
|
439
|
+
...opts.customHeight !== void 0 ? { customHeight: opts.customHeight } : {},
|
|
440
|
+
...opts.hidden !== void 0 ? { hidden: opts.hidden } : {},
|
|
441
|
+
...opts.outlineLevel !== void 0 ? { outlineLevel: opts.outlineLevel } : {},
|
|
442
|
+
...opts.collapsed !== void 0 ? { collapsed: opts.collapsed } : {},
|
|
443
|
+
...opts.style !== void 0 ? { style: opts.style } : {}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/worksheet/hyperlinks.ts
|
|
448
|
+
function makeHyperlink(opts) {
|
|
449
|
+
if (opts.ref === void 0 || opts.ref.length === 0) throw new OpenXmlSchemaError("Hyperlink: ref is required");
|
|
450
|
+
return {
|
|
451
|
+
ref: opts.ref,
|
|
452
|
+
...opts.target !== void 0 ? { target: opts.target } : {},
|
|
453
|
+
...opts.location !== void 0 ? { location: opts.location } : {},
|
|
454
|
+
...opts.tooltip !== void 0 ? { tooltip: opts.tooltip } : {},
|
|
455
|
+
...opts.display !== void 0 ? { display: opts.display } : {},
|
|
456
|
+
...opts.rId !== void 0 ? { rId: opts.rId } : {}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/worksheet/views.ts
|
|
461
|
+
/** Build a SheetView with sensible defaults. */
|
|
462
|
+
function makeSheetView(opts = {}) {
|
|
463
|
+
return {
|
|
464
|
+
workbookViewId: opts.workbookViewId ?? 0,
|
|
465
|
+
...opts.tabSelected !== void 0 ? { tabSelected: opts.tabSelected } : {},
|
|
466
|
+
...opts.showGridLines !== void 0 ? { showGridLines: opts.showGridLines } : {},
|
|
467
|
+
...opts.showRowColHeaders !== void 0 ? { showRowColHeaders: opts.showRowColHeaders } : {},
|
|
468
|
+
...opts.showFormulas !== void 0 ? { showFormulas: opts.showFormulas } : {},
|
|
469
|
+
...opts.showZeros !== void 0 ? { showZeros: opts.showZeros } : {},
|
|
470
|
+
...opts.rightToLeft !== void 0 ? { rightToLeft: opts.rightToLeft } : {},
|
|
471
|
+
...opts.view !== void 0 ? { view: opts.view } : {},
|
|
472
|
+
...opts.topLeftCell !== void 0 ? { topLeftCell: opts.topLeftCell } : {},
|
|
473
|
+
...opts.zoomScale !== void 0 ? { zoomScale: opts.zoomScale } : {},
|
|
474
|
+
...opts.zoomScaleNormal !== void 0 ? { zoomScaleNormal: opts.zoomScaleNormal } : {},
|
|
475
|
+
...opts.pane ? { pane: opts.pane } : {},
|
|
476
|
+
...opts.selection ? { selection: opts.selection } : {}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Build a frozen Pane from a top-left coordinate. Per Excel semantics:
|
|
481
|
+
* - "B2" → freeze 1 row + 1 col → xSplit=1, ySplit=1, activePane='bottomRight'
|
|
482
|
+
* - "A2" → freeze 1 row only → ySplit=1, activePane='bottomLeft'
|
|
483
|
+
* - "B1" → freeze 1 col only → xSplit=1, activePane='topRight'
|
|
484
|
+
* - "A1" → no freeze; throws (caller should clear `ws.views[].pane`).
|
|
485
|
+
*/
|
|
486
|
+
function makeFreezePane(topLeftRef) {
|
|
487
|
+
const { col, row } = coordinateToTuple(topLeftRef);
|
|
488
|
+
if (col === 1 && row === 1) throw new OpenXmlSchemaError("makeFreezePane: \"A1\" is not a valid freeze ref (no rows or columns to freeze)");
|
|
489
|
+
const xSplit = col - 1;
|
|
490
|
+
const ySplit = row - 1;
|
|
491
|
+
let activePane;
|
|
492
|
+
if (xSplit > 0 && ySplit > 0) activePane = "bottomRight";
|
|
493
|
+
else if (ySplit > 0) activePane = "bottomLeft";
|
|
494
|
+
else activePane = "topRight";
|
|
495
|
+
const pane = {
|
|
496
|
+
state: "frozen",
|
|
497
|
+
topLeftCell: topLeftRef,
|
|
498
|
+
activePane
|
|
499
|
+
};
|
|
500
|
+
if (xSplit > 0) pane.xSplit = xSplit;
|
|
501
|
+
if (ySplit > 0) pane.ySplit = ySplit;
|
|
502
|
+
return pane;
|
|
503
|
+
}
|
|
504
|
+
/** Inverse of {@link makeFreezePane}. Returns the top-left ref of the bottomRight pane, or undefined. */
|
|
505
|
+
function freezePaneRef(view) {
|
|
506
|
+
const pane = view.pane;
|
|
507
|
+
if (!pane || pane.state !== "frozen") return void 0;
|
|
508
|
+
if (pane.topLeftCell) return pane.topLeftCell;
|
|
509
|
+
const xSplit = pane.xSplit ?? 0;
|
|
510
|
+
const ySplit = pane.ySplit ?? 0;
|
|
511
|
+
return tupleToCoordinate(xSplit + 1, ySplit + 1);
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/worksheet/comments.ts
|
|
515
|
+
function makeLegacyComment(opts) {
|
|
516
|
+
return {
|
|
517
|
+
ref: opts.ref,
|
|
518
|
+
author: opts.author,
|
|
519
|
+
text: opts.text
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/worksheet/properties.ts
|
|
524
|
+
const makeSheetProperties = (opts = {}) => {
|
|
525
|
+
const out = {};
|
|
526
|
+
if (opts.codeName !== void 0) out.codeName = opts.codeName;
|
|
527
|
+
if (opts.enableFormatConditionsCalculation !== void 0) out.enableFormatConditionsCalculation = opts.enableFormatConditionsCalculation;
|
|
528
|
+
if (opts.filterMode !== void 0) out.filterMode = opts.filterMode;
|
|
529
|
+
if (opts.published !== void 0) out.published = opts.published;
|
|
530
|
+
if (opts.syncHorizontal !== void 0) out.syncHorizontal = opts.syncHorizontal;
|
|
531
|
+
if (opts.syncRef !== void 0) out.syncRef = opts.syncRef;
|
|
532
|
+
if (opts.syncVertical !== void 0) out.syncVertical = opts.syncVertical;
|
|
533
|
+
if (opts.transitionEvaluation !== void 0) out.transitionEvaluation = opts.transitionEvaluation;
|
|
534
|
+
if (opts.transitionEntry !== void 0) out.transitionEntry = opts.transitionEntry;
|
|
535
|
+
if (opts.tabColor !== void 0) out.tabColor = opts.tabColor;
|
|
536
|
+
if (opts.outlinePr !== void 0) out.outlinePr = opts.outlinePr;
|
|
537
|
+
if (opts.pageSetUpPr !== void 0) out.pageSetUpPr = opts.pageSetUpPr;
|
|
538
|
+
return out;
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/worksheet/worksheet.ts
|
|
542
|
+
/** Build a Worksheet shell. */
|
|
543
|
+
function makeWorksheet(title) {
|
|
544
|
+
if (typeof title !== "string" || title.length === 0) throw new OpenXmlSchemaError("Worksheet title must be a non-empty string");
|
|
545
|
+
return {
|
|
546
|
+
title,
|
|
547
|
+
rows: /* @__PURE__ */ new Map(),
|
|
548
|
+
_appendRowCursor: 0,
|
|
549
|
+
mergedCells: [],
|
|
550
|
+
views: [],
|
|
551
|
+
columnDimensions: /* @__PURE__ */ new Map(),
|
|
552
|
+
rowDimensions: /* @__PURE__ */ new Map(),
|
|
553
|
+
hyperlinks: [],
|
|
554
|
+
dataValidations: [],
|
|
555
|
+
tables: [],
|
|
556
|
+
legacyComments: [],
|
|
557
|
+
conditionalFormatting: [],
|
|
558
|
+
cellWatches: [],
|
|
559
|
+
ignoredErrors: [],
|
|
560
|
+
rowBreaks: [],
|
|
561
|
+
colBreaks: [],
|
|
562
|
+
customProperties: [],
|
|
563
|
+
webPublishItems: [],
|
|
564
|
+
protectedRanges: [],
|
|
565
|
+
smartTags: [],
|
|
566
|
+
customSheetViews: [],
|
|
567
|
+
oleObjects: [],
|
|
568
|
+
controls: []
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const validateRowCol = (row, col) => {
|
|
572
|
+
if (!Number.isInteger(row) || row < 1 || row > 1048576) throw new OpenXmlSchemaError(`Worksheet row ${row} out of range [1, ${MAX_ROW}]`);
|
|
573
|
+
if (!Number.isInteger(col) || col < 1 || col > 16384) throw new OpenXmlSchemaError(`Worksheet col ${col} out of range [1, ${MAX_COL}]`);
|
|
574
|
+
};
|
|
575
|
+
/** Resolve a 1-based or "A1" coordinate; returns the populated Cell or undefined. */
|
|
576
|
+
function getCell(ws, row, col) {
|
|
577
|
+
return ws.rows.get(row)?.get(col);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Create or update a Cell at (row, col). Existing cells keep their styleId /
|
|
581
|
+
* hyperlinkId / commentId unless explicitly overridden.
|
|
582
|
+
*/
|
|
583
|
+
function setCell(ws, row, col, value = null, styleId) {
|
|
584
|
+
let rowMap = ws.rows.get(row);
|
|
585
|
+
let cell = rowMap?.get(col);
|
|
586
|
+
if (cell === void 0) {
|
|
587
|
+
cell = makeCell(row, col, value, styleId ?? 0);
|
|
588
|
+
if (rowMap === void 0) {
|
|
589
|
+
rowMap = /* @__PURE__ */ new Map();
|
|
590
|
+
ws.rows.set(row, rowMap);
|
|
591
|
+
}
|
|
592
|
+
rowMap.set(col, cell);
|
|
593
|
+
} else {
|
|
594
|
+
cell.value = value;
|
|
595
|
+
if (styleId !== void 0) cell.styleId = styleId;
|
|
596
|
+
}
|
|
597
|
+
if (row > ws._appendRowCursor) ws._appendRowCursor = row;
|
|
598
|
+
return cell;
|
|
599
|
+
}
|
|
600
|
+
/** Delete a single cell from the sheet. Empty rows are pruned. */
|
|
601
|
+
function deleteCell(ws, row, col) {
|
|
602
|
+
const rowMap = ws.rows.get(row);
|
|
603
|
+
if (rowMap === void 0) return;
|
|
604
|
+
rowMap.delete(col);
|
|
605
|
+
if (rowMap.size === 0) ws.rows.delete(row);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Delete every populated cell inside a range. Returns the number of cells
|
|
609
|
+
* removed. Row maps that go empty are pruned. Column / row dimensions, merges,
|
|
610
|
+
* comments etc. are left untouched.
|
|
611
|
+
*/
|
|
612
|
+
function clearRange(ws, range) {
|
|
613
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
614
|
+
let n = 0;
|
|
615
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
616
|
+
const rowMap = ws.rows.get(r);
|
|
617
|
+
if (!rowMap) continue;
|
|
618
|
+
for (let c = minCol; c <= maxCol; c++) if (rowMap.delete(c)) n++;
|
|
619
|
+
if (rowMap.size === 0) ws.rows.delete(r);
|
|
620
|
+
}
|
|
621
|
+
return n;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Wipe every populated cell on the worksheet, leaving styles, dimensions,
|
|
625
|
+
* merges, comments, hyperlinks etc. intact. Returns the count of cells removed.
|
|
626
|
+
* Useful when a sheet should be re-filled from scratch but its formatting kept.
|
|
627
|
+
*/
|
|
628
|
+
function clearAllCells(ws) {
|
|
629
|
+
let n = 0;
|
|
630
|
+
for (const rowMap of ws.rows.values()) n += rowMap.size;
|
|
631
|
+
ws.rows.clear();
|
|
632
|
+
ws._appendRowCursor = 0;
|
|
633
|
+
return n;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Append a row of values starting at the next empty row. Returns the row index
|
|
637
|
+
* (1-based). Mirrors openpyxl's `Worksheet.append`. `null` / `undefined`
|
|
638
|
+
* entries leave the cell empty.
|
|
639
|
+
*/
|
|
640
|
+
function appendRow(ws, values) {
|
|
641
|
+
const row = ws._appendRowCursor + 1;
|
|
642
|
+
for (let i = 0; i < values.length; i++) {
|
|
643
|
+
const value = values[i];
|
|
644
|
+
if (value === void 0 || value === null) continue;
|
|
645
|
+
setCell(ws, row, i + 1, value);
|
|
646
|
+
}
|
|
647
|
+
ws._appendRowCursor = row;
|
|
648
|
+
return row;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Bulk version of {@link appendRow}: append a 2D array of values one row at a
|
|
652
|
+
* time. Returns `{firstRow, lastRow}` — both 1-based, inclusive. An empty input
|
|
653
|
+
* returns `{firstRow, lastRow: firstRow - 1}` so callers can detect the no-op
|
|
654
|
+
* without throwing.
|
|
655
|
+
*
|
|
656
|
+
* Common usage: `appendRows(ws, csvParsedRows)` for fast import.
|
|
657
|
+
*/
|
|
658
|
+
function appendRows(ws, rows) {
|
|
659
|
+
const firstRow = ws._appendRowCursor + 1;
|
|
660
|
+
if (rows.length === 0) return {
|
|
661
|
+
firstRow,
|
|
662
|
+
lastRow: firstRow - 1
|
|
663
|
+
};
|
|
664
|
+
let lastRow = firstRow - 1;
|
|
665
|
+
for (const row of rows) lastRow = appendRow(ws, row);
|
|
666
|
+
return {
|
|
667
|
+
firstRow,
|
|
668
|
+
lastRow
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Write a 2D array of values to the sheet starting at the given A1 anchor cell.
|
|
673
|
+
* Distinct from {@link appendRows} (which always writes past
|
|
674
|
+
* `_appendRowCursor`) — this lets you place a block at an arbitrary location,
|
|
675
|
+
* e.g. mid-sheet table updates.
|
|
676
|
+
*
|
|
677
|
+
* `null` / `undefined` entries leave the corresponding cell **untouched**
|
|
678
|
+
* (existing cell + style are preserved). Pre-existing cells inside the written
|
|
679
|
+
* rectangle are overwritten in place, so their `styleId` survives the write.
|
|
680
|
+
*
|
|
681
|
+
* Returns the bounding-box of the written area as 1-based inclusive
|
|
682
|
+
* coordinates. An empty rows array returns `undefined` rather than an invalid
|
|
683
|
+
* zero-area range.
|
|
684
|
+
*/
|
|
685
|
+
function writeRange(ws, startRef, values) {
|
|
686
|
+
if (values.length === 0) return void 0;
|
|
687
|
+
const { col: startCol, row: startRow } = coordinateToTuple(startRef);
|
|
688
|
+
let maxRow = startRow;
|
|
689
|
+
let maxCol = startCol;
|
|
690
|
+
for (let i = 0; i < values.length; i++) {
|
|
691
|
+
const row = values[i];
|
|
692
|
+
if (row === void 0) continue;
|
|
693
|
+
const r = startRow + i;
|
|
694
|
+
for (let j = 0; j < row.length; j++) {
|
|
695
|
+
const v = row[j];
|
|
696
|
+
if (v === void 0 || v === null) continue;
|
|
697
|
+
const c = startCol + j;
|
|
698
|
+
setCell(ws, r, c, v);
|
|
699
|
+
if (c > maxCol) maxCol = c;
|
|
700
|
+
}
|
|
701
|
+
if (r > maxRow) maxRow = r;
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
minRow: startRow,
|
|
705
|
+
maxRow,
|
|
706
|
+
minCol: startCol,
|
|
707
|
+
maxCol
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Iterate the worksheet rows rectangularly. Yields one row per row in
|
|
712
|
+
* `[minRow, maxRow]` — including entirely empty rows — and each yielded row
|
|
713
|
+
* has length `maxCol - minCol + 1`. Missing cell positions are `undefined`
|
|
714
|
+
* (no placeholder Cell allocations).
|
|
715
|
+
*
|
|
716
|
+
* Defaults: `minRow=1`, `maxRow=getMaxRow(ws)`, `minCol=1`,
|
|
717
|
+
* `maxCol=getMaxCol(ws)`. The default extent is the populated bounding box,
|
|
718
|
+
* not the 1M × 16K sheet limit, so the rectangular default doesn't iterate
|
|
719
|
+
* the whole grid for a small sheet.
|
|
720
|
+
*
|
|
721
|
+
* To iterate populated rows only, filter:
|
|
722
|
+
* `[...iterRows(ws)].filter(row => row.some((c) => c !== undefined))`. To
|
|
723
|
+
* iterate populated cells without row boundaries, use {@link iterCells}.
|
|
724
|
+
*/
|
|
725
|
+
function* iterRows(ws, opts = {}) {
|
|
726
|
+
const { minRow = 1, maxRow = getMaxRow(ws), minCol = 1, maxCol = getMaxCol(ws) } = opts;
|
|
727
|
+
if (maxRow < minRow || maxCol < minCol) return;
|
|
728
|
+
const width = maxCol - minCol + 1;
|
|
729
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
730
|
+
const rowMap = ws.rows.get(r);
|
|
731
|
+
const out = new Array(width).fill(void 0);
|
|
732
|
+
if (rowMap !== void 0) for (let c = minCol; c <= maxCol; c++) {
|
|
733
|
+
const cell = rowMap.get(c);
|
|
734
|
+
if (cell !== void 0) out[c - minCol] = cell;
|
|
735
|
+
}
|
|
736
|
+
yield out;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Same rectangular iteration as {@link iterRows}, but yields each cell's
|
|
741
|
+
* `.value`. Missing cell positions become `null` — already the canonical empty
|
|
742
|
+
* marker in `CellValue`.
|
|
743
|
+
*/
|
|
744
|
+
function* iterValues(ws, opts = {}) {
|
|
745
|
+
for (const row of iterRows(ws, opts)) yield row.map((c) => c === void 0 ? null : c.value);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Yield every populated cell in the worksheet as a flat stream (row-major,
|
|
749
|
+
* columns ascending). Distinct from {@link iterRows} which yields one row
|
|
750
|
+
* per row in the bounding box — use this when the caller wants only the
|
|
751
|
+
* populated cells without row boundaries or rectangular padding.
|
|
752
|
+
*/
|
|
753
|
+
function* iterCells(ws, opts = {}) {
|
|
754
|
+
for (const row of iterRows(ws, opts)) for (const cell of row) if (cell !== void 0) yield cell;
|
|
755
|
+
}
|
|
756
|
+
/** Effective max row index based on populated cells (0 when empty). */
|
|
757
|
+
function getMaxRow(ws) {
|
|
758
|
+
let m = 0;
|
|
759
|
+
for (const r of ws.rows.keys()) if (r > m) m = r;
|
|
760
|
+
return m;
|
|
761
|
+
}
|
|
762
|
+
/** Effective max column index based on populated cells (0 when empty). */
|
|
763
|
+
function getMaxCol(ws) {
|
|
764
|
+
let m = 0;
|
|
765
|
+
for (const rowMap of ws.rows.values()) for (const c of rowMap.keys()) if (c > m) m = c;
|
|
766
|
+
return m;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Sorted list of every row index that holds at least one populated cell.
|
|
770
|
+
* Returns `[]` for an empty worksheet. Useful when a caller wants to iterate
|
|
771
|
+
* only the rows the user actually populated, without walking 1..maxRow in dense
|
|
772
|
+
* fashion.
|
|
773
|
+
*/
|
|
774
|
+
function getPopulatedRowIndices(ws) {
|
|
775
|
+
const out = [];
|
|
776
|
+
for (const [r, rowMap] of ws.rows) if (rowMap.size > 0) out.push(r);
|
|
777
|
+
return out.sort((a, b) => a - b);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Sorted list of every column index that holds at least one populated cell
|
|
781
|
+
* anywhere on the sheet. Distinct columns only; returns `[]` for an empty
|
|
782
|
+
* worksheet.
|
|
783
|
+
*/
|
|
784
|
+
function getPopulatedColumnIndices(ws) {
|
|
785
|
+
const seen = /* @__PURE__ */ new Set();
|
|
786
|
+
for (const rowMap of ws.rows.values()) for (const c of rowMap.keys()) seen.add(c);
|
|
787
|
+
return [...seen].sort((a, b) => a - b);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Bucket a single CellValue into one of the {@link CellsByKindCounts} keys.
|
|
791
|
+
* Shared between {@link countCellsByKind} and the workbook-wide overview so
|
|
792
|
+
* the two never drift in how they classify (e.g. `Date` vs `duration`).
|
|
793
|
+
*/
|
|
794
|
+
function classifyCellValue(v) {
|
|
795
|
+
if (v === null) return "null";
|
|
796
|
+
if (typeof v === "string") return "string";
|
|
797
|
+
if (typeof v === "number") return "number";
|
|
798
|
+
if (typeof v === "boolean") return "boolean";
|
|
799
|
+
if (v instanceof Date) return "date";
|
|
800
|
+
const kind = v.kind;
|
|
801
|
+
if (kind === "duration") return "duration";
|
|
802
|
+
if (kind === "error") return "error";
|
|
803
|
+
if (kind === "rich-text") return "rich-text";
|
|
804
|
+
return "formula";
|
|
805
|
+
}
|
|
806
|
+
function countCellsByKind(ws) {
|
|
807
|
+
const out = {
|
|
808
|
+
null: 0,
|
|
809
|
+
string: 0,
|
|
810
|
+
number: 0,
|
|
811
|
+
boolean: 0,
|
|
812
|
+
date: 0,
|
|
813
|
+
duration: 0,
|
|
814
|
+
error: 0,
|
|
815
|
+
"rich-text": 0,
|
|
816
|
+
formula: 0
|
|
817
|
+
};
|
|
818
|
+
for (const cell of iterCells(ws)) out[classifyCellValue(cell.value)]++;
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* True iff the worksheet has zero non-empty cells. Equivalent to
|
|
823
|
+
* `getNonEmptyCellCount(ws) === 0` but short-circuits on the first non-null
|
|
824
|
+
* value found, so the cost is O(first non-empty cell) rather than O(populated
|
|
825
|
+
* cells).
|
|
826
|
+
*/
|
|
827
|
+
function isWorksheetEmpty(ws) {
|
|
828
|
+
for (const cell of iterCells(ws)) if (cell.value !== null) return false;
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Count non-empty cells. Distinct from {@link countCells} which counts every
|
|
833
|
+
* materialised cell (including ones whose `value === null`): this skips cells
|
|
834
|
+
* with a `null` value, plus optionally formulas / rich-text per the opts.
|
|
835
|
+
*
|
|
836
|
+
* Useful for "how many real values does this sheet contain" stats vs the
|
|
837
|
+
* materialised footprint.
|
|
838
|
+
*/
|
|
839
|
+
function getNonEmptyCellCount(ws, opts = {}) {
|
|
840
|
+
const includeFormulas = opts.includeFormulas ?? true;
|
|
841
|
+
const includeRichText = opts.includeRichText ?? true;
|
|
842
|
+
let n = 0;
|
|
843
|
+
for (const cell of iterCells(ws)) {
|
|
844
|
+
const v = cell.value;
|
|
845
|
+
if (v === null) continue;
|
|
846
|
+
if (typeof v === "object" && v !== null) {
|
|
847
|
+
const kind = v.kind;
|
|
848
|
+
if (kind === "formula" && !includeFormulas) continue;
|
|
849
|
+
if (kind === "rich-text" && !includeRichText) continue;
|
|
850
|
+
}
|
|
851
|
+
n++;
|
|
852
|
+
}
|
|
853
|
+
return n;
|
|
854
|
+
}
|
|
855
|
+
/** Total populated cell count. */
|
|
856
|
+
function countCells(ws) {
|
|
857
|
+
let n = 0;
|
|
858
|
+
for (const rowMap of ws.rows.values()) n += rowMap.size;
|
|
859
|
+
return n;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Bounding-box of the populated cells: `{ minRow, maxRow, minCol, maxCol }`
|
|
863
|
+
* covering every cell in `ws.rows`. Returns `undefined` when the sheet is
|
|
864
|
+
* empty. Walks the sparse store once.
|
|
865
|
+
*/
|
|
866
|
+
function getDataExtent(ws) {
|
|
867
|
+
let minRow = Number.POSITIVE_INFINITY;
|
|
868
|
+
let maxRow = 0;
|
|
869
|
+
let minCol = Number.POSITIVE_INFINITY;
|
|
870
|
+
let maxCol = 0;
|
|
871
|
+
let touched = false;
|
|
872
|
+
for (const [r, rowMap] of ws.rows) {
|
|
873
|
+
if (rowMap.size === 0) continue;
|
|
874
|
+
if (r < minRow) minRow = r;
|
|
875
|
+
if (r > maxRow) maxRow = r;
|
|
876
|
+
for (const c of rowMap.keys()) {
|
|
877
|
+
if (c < minCol) minCol = c;
|
|
878
|
+
if (c > maxCol) maxCol = c;
|
|
879
|
+
}
|
|
880
|
+
touched = true;
|
|
881
|
+
}
|
|
882
|
+
if (!touched) return void 0;
|
|
883
|
+
return {
|
|
884
|
+
minRow,
|
|
885
|
+
maxRow,
|
|
886
|
+
minCol,
|
|
887
|
+
maxCol
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Iterate every populated cell, yielding those for which `predicate` returns
|
|
892
|
+
* true. Iteration order is row-then-column ascending. Cells whose `.value ===
|
|
893
|
+
* null` (empty placeholders carrying only style or comment metadata) are still
|
|
894
|
+
* visited.
|
|
895
|
+
*/
|
|
896
|
+
function* findCells(ws, predicate) {
|
|
897
|
+
const rowKeys = [...ws.rows.keys()].sort((a, b) => a - b);
|
|
898
|
+
for (const r of rowKeys) {
|
|
899
|
+
const rowMap = ws.rows.get(r);
|
|
900
|
+
if (!rowMap) continue;
|
|
901
|
+
const cols = [...rowMap.keys()].sort((a, b) => a - b);
|
|
902
|
+
for (const c of cols) {
|
|
903
|
+
const cell = rowMap.get(c);
|
|
904
|
+
if (cell !== void 0 && predicate(cell)) yield cell;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Iterate the populated cells inside a rectangular range. Cells that don't
|
|
910
|
+
* exist in the sparse store are skipped (no auto-allocate). Use {@link
|
|
911
|
+
* applyToRange} when you need every coordinate visited regardless of
|
|
912
|
+
* population.
|
|
913
|
+
*/
|
|
914
|
+
function* getCellsInRange(ws, range) {
|
|
915
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
916
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
917
|
+
const rowMap = ws.rows.get(r);
|
|
918
|
+
if (!rowMap) continue;
|
|
919
|
+
for (let col = minCol; col <= maxCol; col++) {
|
|
920
|
+
const cell = rowMap.get(col);
|
|
921
|
+
if (cell !== void 0) yield cell;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/** Resolve an "A1" coordinate to a numeric (col, row) pair on the sheet. */
|
|
926
|
+
function setCellByCoord(ws, coord, value, styleId) {
|
|
927
|
+
const m = /^([A-Za-z]{1,3})([1-9][0-9]*)$/.exec(coord);
|
|
928
|
+
if (m === null || m[1] === void 0 || m[2] === void 0) throw new OpenXmlSchemaError(`setCellByCoord: invalid coordinate "${coord}"`);
|
|
929
|
+
const col = columnIndexFromLetter(m[1]);
|
|
930
|
+
return setCell(ws, Number.parseInt(m[2], 10), col, value, styleId);
|
|
931
|
+
}
|
|
932
|
+
/** Convenience getter accepting an "A1" coordinate. */
|
|
933
|
+
function getCellByCoord(ws, coord) {
|
|
934
|
+
const m = /^([A-Za-z]{1,3})([1-9][0-9]*)$/.exec(coord);
|
|
935
|
+
if (m === null || m[1] === void 0 || m[2] === void 0) return void 0;
|
|
936
|
+
const col = columnIndexFromLetter(m[1]);
|
|
937
|
+
return getCell(ws, Number.parseInt(m[2], 10), col);
|
|
938
|
+
}
|
|
939
|
+
const toCellRange = (refOrRange) => typeof refOrRange === "string" ? parseRange(refOrRange) : refOrRange;
|
|
940
|
+
/**
|
|
941
|
+
* Merge a range. The top-left cell keeps its value; every other cell in the
|
|
942
|
+
* range is dropped from `ws.rows` so the on-wire `<sheetData>` won't carry
|
|
943
|
+
* phantom cells underneath the merge. Mirrors openpyxl's
|
|
944
|
+
* `MergedCellRange.format()`. Idempotent for an identical range, throws when
|
|
945
|
+
* the range overlaps an existing merge.
|
|
946
|
+
*/
|
|
947
|
+
function mergeCells(ws, refOrRange) {
|
|
948
|
+
const range = toCellRange(refOrRange);
|
|
949
|
+
for (const existing of ws.mergedCells) {
|
|
950
|
+
if (rangeToString(existing) === rangeToString(range)) return existing;
|
|
951
|
+
if (rangesOverlap(existing, range)) throw new OpenXmlSchemaError(`mergeCells: range ${rangeToString(range)} overlaps existing merged range ${rangeToString(existing)}`);
|
|
952
|
+
}
|
|
953
|
+
for (let r = range.minRow; r <= range.maxRow; r++) for (let c = range.minCol; c <= range.maxCol; c++) {
|
|
954
|
+
if (r === range.minRow && c === range.minCol) continue;
|
|
955
|
+
ws.rows.get(r)?.delete(c);
|
|
956
|
+
const row = ws.rows.get(r);
|
|
957
|
+
if (row && row.size === 0) ws.rows.delete(r);
|
|
958
|
+
}
|
|
959
|
+
ws.mergedCells.push(range);
|
|
960
|
+
return range;
|
|
961
|
+
}
|
|
962
|
+
/** Drop a previously-merged range. No-op if the range isn't registered. */
|
|
963
|
+
function unmergeCells(ws, refOrRange) {
|
|
964
|
+
const target = rangeToString(toCellRange(refOrRange));
|
|
965
|
+
const idx = ws.mergedCells.findIndex((r) => rangeToString(r) === target);
|
|
966
|
+
if (idx < 0) return false;
|
|
967
|
+
ws.mergedCells.splice(idx, 1);
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
/** Read-only iterator over the worksheet's merged ranges. */
|
|
971
|
+
function getMergedCells(ws) {
|
|
972
|
+
return ws.mergedCells;
|
|
973
|
+
}
|
|
974
|
+
/** True iff (row, col) sits inside any merged range — top-left included. */
|
|
975
|
+
function isMergedCell(ws, row, col) {
|
|
976
|
+
for (const range of ws.mergedCells) if (rangeContainsCell(range, row, col)) return true;
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Look up the merged range covering (row, col), or `undefined` if the
|
|
981
|
+
* coordinate isn't inside any merge. Lets callers introspect a merge without
|
|
982
|
+
* iterating `getMergedCells` themselves.
|
|
983
|
+
*/
|
|
984
|
+
function getMergedRangeAt(ws, row, col) {
|
|
985
|
+
for (const range of ws.mergedCells) if (rangeContainsCell(range, row, col)) return range;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Drop the merge that contains (row, col), if any. Returns `true` when a merge
|
|
989
|
+
* was unregistered. Useful when callers know a cell coordinate but not the
|
|
990
|
+
* original merge bounds.
|
|
991
|
+
*/
|
|
992
|
+
function unmergeCellsAt(ws, row, col) {
|
|
993
|
+
for (let i = 0; i < ws.mergedCells.length; i++) {
|
|
994
|
+
const r = ws.mergedCells[i];
|
|
995
|
+
if (r && rangeContainsCell(r, row, col)) {
|
|
996
|
+
ws.mergedCells.splice(i, 1);
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Drop every merged range on the worksheet. Returns the count of merges
|
|
1004
|
+
* removed. Cells that were inside the merges keep their values — only the merge
|
|
1005
|
+
* metadata is gone.
|
|
1006
|
+
*/
|
|
1007
|
+
function removeAllMergedRanges(ws) {
|
|
1008
|
+
const n = ws.mergedCells.length;
|
|
1009
|
+
ws.mergedCells = [];
|
|
1010
|
+
return n;
|
|
1011
|
+
}
|
|
1012
|
+
/** Lazily get-or-create the primary SheetView so view-mutating helpers don't have to branch. */
|
|
1013
|
+
const ensurePrimaryView = (ws) => {
|
|
1014
|
+
let view = ws.views[0];
|
|
1015
|
+
if (!view) {
|
|
1016
|
+
view = makeSheetView();
|
|
1017
|
+
ws.views.push(view);
|
|
1018
|
+
}
|
|
1019
|
+
return view;
|
|
1020
|
+
};
|
|
1021
|
+
/**
|
|
1022
|
+
* Freeze rows / columns above + left of `topLeftRef` ("B2" → 1 row + 1 col).
|
|
1023
|
+
* Pass `undefined` to clear any existing freeze. Targets the workbook's primary
|
|
1024
|
+
* SheetView (`ws.views[0]`); creates one if absent.
|
|
1025
|
+
*/
|
|
1026
|
+
function setFreezePanes(ws, topLeftRef) {
|
|
1027
|
+
if (topLeftRef === void 0) {
|
|
1028
|
+
if (ws.views[0]) delete ws.views[0].pane;
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const view = ensurePrimaryView(ws);
|
|
1032
|
+
view.pane = makeFreezePane(topLeftRef);
|
|
1033
|
+
}
|
|
1034
|
+
/** Inverse of {@link setFreezePanes}; returns the top-left ref or undefined when no freeze is active. */
|
|
1035
|
+
function getFreezePanes(ws) {
|
|
1036
|
+
const view = ws.views[0];
|
|
1037
|
+
if (!view) return void 0;
|
|
1038
|
+
return freezePaneRef(view);
|
|
1039
|
+
}
|
|
1040
|
+
/** Freeze both top `rows` rows AND left `cols` columns. */
|
|
1041
|
+
function freezePanes(ws, rows, cols) {
|
|
1042
|
+
if (!Number.isInteger(rows) || rows < 1) throw new OpenXmlSchemaError(`freezePanes: rows must be a positive integer; got ${rows}`);
|
|
1043
|
+
if (!Number.isInteger(cols) || cols < 1) throw new OpenXmlSchemaError(`freezePanes: cols must be a positive integer; got ${cols}`);
|
|
1044
|
+
setFreezePanes(ws, `${columnLetterFromIndex(cols + 1)}${rows + 1}`);
|
|
1045
|
+
}
|
|
1046
|
+
/** Lazily get-or-create `ws.sheetProperties` so tab-color helpers don't have to branch. */
|
|
1047
|
+
const ensureSheetProperties = (ws) => {
|
|
1048
|
+
if (!ws.sheetProperties) ws.sheetProperties = makeSheetProperties();
|
|
1049
|
+
return ws.sheetProperties;
|
|
1050
|
+
};
|
|
1051
|
+
const colorFrom = (input) => typeof input === "string" ? makeColor({ rgb: input }) : makeColor(input);
|
|
1052
|
+
/**
|
|
1053
|
+
* Set the sheet tab strip colour. Accepts either a hex string (`"FF0070C0"`) or
|
|
1054
|
+
* a partial `Color` object (`{ theme: 4, tint: 0.4 }`).
|
|
1055
|
+
*/
|
|
1056
|
+
function setSheetTabColor(ws, color) {
|
|
1057
|
+
const c = colorFrom(color);
|
|
1058
|
+
ensureSheetProperties(ws).tabColor = c;
|
|
1059
|
+
return c;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Set the zoom scale (percent) on the primary SheetView. Excel accepts integer
|
|
1063
|
+
* percentages in `[10, 400]`.
|
|
1064
|
+
*/
|
|
1065
|
+
function setSheetZoom(ws, scale) {
|
|
1066
|
+
if (!Number.isInteger(scale) || scale < 10 || scale > 400) throw new OpenXmlSchemaError(`setSheetZoom: scale must be an integer in [10, 400]; got ${scale}`);
|
|
1067
|
+
ensurePrimaryView(ws).zoomScale = scale;
|
|
1068
|
+
}
|
|
1069
|
+
/** Switch the sheet view between Excel's "Normal" / "Page Break Preview" / "Page Layout" modes. */
|
|
1070
|
+
function setSheetViewMode(ws, mode) {
|
|
1071
|
+
ensurePrimaryView(ws).view = mode;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Set values across a rectangular range from a 2-D array. `rows[0]` is laid
|
|
1075
|
+
* down starting at the top-left of `range`; subsequent rows follow. `null` /
|
|
1076
|
+
* `undefined` entries skip the cell. Useful for dropping a header + data block
|
|
1077
|
+
* in one call.
|
|
1078
|
+
*/
|
|
1079
|
+
function setRangeValues(ws, range, rows) {
|
|
1080
|
+
const { minRow, minCol } = parseRange(range);
|
|
1081
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1082
|
+
const row = rows[i];
|
|
1083
|
+
if (!row) continue;
|
|
1084
|
+
for (let j = 0; j < row.length; j++) {
|
|
1085
|
+
const v = row[j];
|
|
1086
|
+
if (v === null || v === void 0) continue;
|
|
1087
|
+
setCell(ws, minRow + i, minCol + j, v);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Iterate over every cell coordinate in a range, calling `visit` once per (row,
|
|
1093
|
+
* col). Allocates the cell on first touch so callers can mutate it freely.
|
|
1094
|
+
*/
|
|
1095
|
+
function applyToRange(ws, range, visit) {
|
|
1096
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1097
|
+
for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) {
|
|
1098
|
+
let cell = ws.rows.get(r)?.get(c);
|
|
1099
|
+
if (!cell) cell = setCell(ws, r, c);
|
|
1100
|
+
visit(cell, r, c);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Read a rectangular range as a dense 2-D array of values. Empty cells yield
|
|
1105
|
+
* `null`. The shape is `[maxRow - minRow + 1] × [maxCol - minCol + 1]`. Inverse
|
|
1106
|
+
* of {@link setRangeValues}.
|
|
1107
|
+
*/
|
|
1108
|
+
function getRangeValues(ws, range) {
|
|
1109
|
+
const { minRow, maxRow, minCol, maxCol } = parseRange(range);
|
|
1110
|
+
const rowsOut = [];
|
|
1111
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
1112
|
+
const rowMap = ws.rows.get(r);
|
|
1113
|
+
const row = [];
|
|
1114
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
1115
|
+
const cell = rowMap?.get(c);
|
|
1116
|
+
row.push(cell ? cell.value : null);
|
|
1117
|
+
}
|
|
1118
|
+
rowsOut.push(row);
|
|
1119
|
+
}
|
|
1120
|
+
return rowsOut;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Copy every populated cell from `source` to `target` (within the same
|
|
1124
|
+
* worksheet, or across worksheets via `targetWs`). Cells are shallow-cloned:
|
|
1125
|
+
* `value` and `styleId` carry over but `row`/`col` are rewritten. The target's
|
|
1126
|
+
* existing cells in the destination extent are overwritten; cells outside are
|
|
1127
|
+
* untouched.
|
|
1128
|
+
*
|
|
1129
|
+
* The source and target ranges define the top-left corner — their dimensions
|
|
1130
|
+
* need not match. If the target range is smaller than the source, only the
|
|
1131
|
+
* cells that fit within the target's extent are copied; if larger, only the
|
|
1132
|
+
* source's extent is filled.
|
|
1133
|
+
*
|
|
1134
|
+
* Returns the number of cells copied.
|
|
1135
|
+
*/
|
|
1136
|
+
function copyRange(ws, source, target, opts = {}) {
|
|
1137
|
+
const dest = opts.targetWs ?? ws;
|
|
1138
|
+
const sameSheet = dest === ws;
|
|
1139
|
+
const src = parseRange(source);
|
|
1140
|
+
const dst = parseRange(target);
|
|
1141
|
+
const dr = dst.minRow - src.minRow;
|
|
1142
|
+
const dc = dst.minCol - src.minCol;
|
|
1143
|
+
const maxRowOffset = Math.min(src.maxRow - src.minRow, dst.maxRow - dst.minRow);
|
|
1144
|
+
const maxColOffset = Math.min(src.maxCol - src.minCol, dst.maxCol - dst.minCol);
|
|
1145
|
+
let n = 0;
|
|
1146
|
+
for (let i = 0; i <= maxRowOffset; i++) {
|
|
1147
|
+
const srcRow = ws.rows.get(src.minRow + i);
|
|
1148
|
+
if (!srcRow) continue;
|
|
1149
|
+
for (let j = 0; j <= maxColOffset; j++) {
|
|
1150
|
+
const srcCell = srcRow.get(src.minCol + j);
|
|
1151
|
+
if (!srcCell) continue;
|
|
1152
|
+
const newCell = setCell(dest, src.minRow + i + dr, src.minCol + j + dc, srcCell.value, srcCell.styleId);
|
|
1153
|
+
if (sameSheet) {
|
|
1154
|
+
if (srcCell.hyperlinkId !== void 0) newCell.hyperlinkId = srcCell.hyperlinkId;
|
|
1155
|
+
if (srcCell.commentId !== void 0) newCell.commentId = srcCell.commentId;
|
|
1156
|
+
}
|
|
1157
|
+
n++;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return n;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Move every populated cell from `source` to `target`. Equivalent to
|
|
1164
|
+
* `copyRange` followed by clearing the source. When the ranges overlap on the
|
|
1165
|
+
* same sheet, the copy walks in the direction that preserves data — high-to-low
|
|
1166
|
+
* along any axis where the move shifts forward, low-to-high otherwise — so
|
|
1167
|
+
* cells aren't overwritten before they've been read. Returns the number of
|
|
1168
|
+
* cells moved.
|
|
1169
|
+
*/
|
|
1170
|
+
function moveRange(ws, source, target, opts = {}) {
|
|
1171
|
+
const dest = opts.targetWs ?? ws;
|
|
1172
|
+
const sameSheet = dest === ws;
|
|
1173
|
+
const src = parseRange(source);
|
|
1174
|
+
const dst = parseRange(target);
|
|
1175
|
+
const dr = dst.minRow - src.minRow;
|
|
1176
|
+
const dc = dst.minCol - src.minCol;
|
|
1177
|
+
const maxRowOffset = Math.min(src.maxRow - src.minRow, dst.maxRow - dst.minRow);
|
|
1178
|
+
const maxColOffset = Math.min(src.maxCol - src.minCol, dst.maxCol - dst.minCol);
|
|
1179
|
+
const snap = [];
|
|
1180
|
+
for (let i = 0; i <= maxRowOffset; i++) {
|
|
1181
|
+
const srcRow = ws.rows.get(src.minRow + i);
|
|
1182
|
+
if (!srcRow) continue;
|
|
1183
|
+
for (let j = 0; j <= maxColOffset; j++) {
|
|
1184
|
+
const srcCell = srcRow.get(src.minCol + j);
|
|
1185
|
+
if (!srcCell) continue;
|
|
1186
|
+
snap.push({
|
|
1187
|
+
row: src.minRow + i,
|
|
1188
|
+
col: src.minCol + j,
|
|
1189
|
+
cell: srcCell
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
for (let i = 0; i <= maxRowOffset; i++) {
|
|
1194
|
+
const rowIdx = src.minRow + i;
|
|
1195
|
+
const srcRow = ws.rows.get(rowIdx);
|
|
1196
|
+
if (!srcRow) continue;
|
|
1197
|
+
for (let j = 0; j <= maxColOffset; j++) srcRow.delete(src.minCol + j);
|
|
1198
|
+
if (srcRow.size === 0) ws.rows.delete(rowIdx);
|
|
1199
|
+
}
|
|
1200
|
+
for (const entry of snap) {
|
|
1201
|
+
const { row, col, cell } = entry;
|
|
1202
|
+
const newCell = setCell(dest, row + dr, col + dc, cell.value, cell.styleId);
|
|
1203
|
+
if (sameSheet) {
|
|
1204
|
+
if (cell.hyperlinkId !== void 0) newCell.hyperlinkId = cell.hyperlinkId;
|
|
1205
|
+
if (cell.commentId !== void 0) newCell.commentId = cell.commentId;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return snap.length;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Enumerate the populated cells of a row in column order. Unlike {@link
|
|
1212
|
+
* getRowValues}, this skips empty columns and yields the cell objects (not just
|
|
1213
|
+
* their values). Returns `[]` when the row is absent or empty.
|
|
1214
|
+
*/
|
|
1215
|
+
function getCellsInRow(ws, row) {
|
|
1216
|
+
const rowMap = ws.rows.get(row);
|
|
1217
|
+
if (!rowMap || rowMap.size === 0) return [];
|
|
1218
|
+
const sortedCols = [...rowMap.keys()].sort((a, b) => a - b);
|
|
1219
|
+
const out = [];
|
|
1220
|
+
for (const col of sortedCols) {
|
|
1221
|
+
const cell = rowMap.get(col);
|
|
1222
|
+
if (cell) out.push(cell);
|
|
1223
|
+
}
|
|
1224
|
+
return out;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Enumerate the populated cells of a column in row order. Walks the row map and
|
|
1228
|
+
* collects whichever rows carry the column. Returns `[]` when the worksheet has
|
|
1229
|
+
* no cell in that column.
|
|
1230
|
+
*/
|
|
1231
|
+
function getCellsInColumn(ws, col) {
|
|
1232
|
+
const sortedRows = [...ws.rows.keys()].sort((a, b) => a - b);
|
|
1233
|
+
const out = [];
|
|
1234
|
+
for (const r of sortedRows) {
|
|
1235
|
+
const cell = ws.rows.get(r)?.get(col);
|
|
1236
|
+
if (cell) out.push(cell);
|
|
1237
|
+
}
|
|
1238
|
+
return out;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Look up the ColumnDimension covering `col`. The search walks every registered
|
|
1242
|
+
* entry's `min..max` range; that's fine for the typical spreadsheet (a handful
|
|
1243
|
+
* of column entries) and stays simple.
|
|
1244
|
+
*/
|
|
1245
|
+
function getColumnDimension(ws, col) {
|
|
1246
|
+
for (const dim of ws.columnDimensions.values()) if (col >= dim.min && col <= dim.max) return dim;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Set a single-column ColumnDimension entry covering `col`. Shadows any
|
|
1250
|
+
* existing run that overlaps — runs are not split for now (callers that need
|
|
1251
|
+
* range-spanning entries can write directly into `ws.columnDimensions`).
|
|
1252
|
+
*/
|
|
1253
|
+
function setColumnDimension(ws, col, opts) {
|
|
1254
|
+
validateRowCol(1, col);
|
|
1255
|
+
for (const [key, dim] of ws.columnDimensions) if (col >= dim.min && col <= dim.max) ws.columnDimensions.delete(key);
|
|
1256
|
+
const entry = makeColumnDimension(col, opts);
|
|
1257
|
+
ws.columnDimensions.set(col, entry);
|
|
1258
|
+
return entry;
|
|
1259
|
+
}
|
|
1260
|
+
/** Convenience: set a column's width, leaving other fields untouched. */
|
|
1261
|
+
function setColumnWidth(ws, col, width) {
|
|
1262
|
+
return setColumnDimension(ws, col, {
|
|
1263
|
+
...getColumnDimension(ws, col),
|
|
1264
|
+
width,
|
|
1265
|
+
customWidth: true
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
/** Convenience: hide a column. */
|
|
1269
|
+
function hideColumn(ws, col) {
|
|
1270
|
+
return setColumnDimension(ws, col, {
|
|
1271
|
+
...getColumnDimension(ws, col),
|
|
1272
|
+
hidden: true
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Convenience: unhide a column. Drops the `hidden` flag from the column's
|
|
1277
|
+
* dimension entry (and removes the entry altogether when no other fields
|
|
1278
|
+
* remain).
|
|
1279
|
+
*/
|
|
1280
|
+
function unhideColumn(ws, col) {
|
|
1281
|
+
const existing = getColumnDimension(ws, col);
|
|
1282
|
+
if (!existing) return;
|
|
1283
|
+
const { hidden: _drop, ...rest } = existing;
|
|
1284
|
+
const { min: _min, max: _max, ...passthrough } = rest;
|
|
1285
|
+
if (Object.keys(passthrough).length === 0) ws.columnDimensions.delete(existing.min);
|
|
1286
|
+
else setColumnDimension(ws, col, passthrough);
|
|
1287
|
+
}
|
|
1288
|
+
/** Bulk-hide every column in `[fromCol, toCol]`. */
|
|
1289
|
+
function hideColumns(ws, fromCol, toCol) {
|
|
1290
|
+
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol < 1 || toCol < fromCol) throw new OpenXmlSchemaError(`hideColumns: invalid column range [${fromCol}, ${toCol}]`);
|
|
1291
|
+
for (let c = fromCol; c <= toCol; c++) hideColumn(ws, c);
|
|
1292
|
+
}
|
|
1293
|
+
/** Bulk-unhide every column in `[fromCol, toCol]`. */
|
|
1294
|
+
function unhideColumns(ws, fromCol, toCol) {
|
|
1295
|
+
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol < 1 || toCol < fromCol) throw new OpenXmlSchemaError(`unhideColumns: invalid column range [${fromCol}, ${toCol}]`);
|
|
1296
|
+
for (let c = fromCol; c <= toCol; c++) unhideColumn(ws, c);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Set the default column width (characters) for cells without an explicit
|
|
1300
|
+
* ColumnDimension entry. Mirrors Excel's "Default Width" dialog. Pass
|
|
1301
|
+
* `undefined` to clear.
|
|
1302
|
+
*/
|
|
1303
|
+
function setDefaultColumnWidth(ws, width) {
|
|
1304
|
+
if (width === void 0) {
|
|
1305
|
+
delete ws.defaultColumnWidth;
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (!Number.isFinite(width) || width < 0) throw new OpenXmlSchemaError(`setDefaultColumnWidth: width must be a non-negative number; got ${width}`);
|
|
1309
|
+
ws.defaultColumnWidth = width;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Set the default row height (points) for rows without an explicit RowDimension
|
|
1313
|
+
* entry. Mirrors Excel's "Default Row Height" dialog. Pass `undefined` to
|
|
1314
|
+
* clear.
|
|
1315
|
+
*/
|
|
1316
|
+
function setDefaultRowHeight(ws, height) {
|
|
1317
|
+
if (height === void 0) {
|
|
1318
|
+
delete ws.defaultRowHeight;
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (!Number.isFinite(height) || height < 0) throw new OpenXmlSchemaError(`setDefaultRowHeight: height must be a non-negative number; got ${height}`);
|
|
1322
|
+
ws.defaultRowHeight = height;
|
|
1323
|
+
}
|
|
1324
|
+
const validateOutlineLevel = (level) => {
|
|
1325
|
+
if (!Number.isInteger(level) || level < 1 || level > 7) throw new OpenXmlSchemaError(`outlineLevel must be an integer in [1, 7]; got ${level}`);
|
|
1326
|
+
};
|
|
1327
|
+
/**
|
|
1328
|
+
* Mirror Excel's "Data → Group → Rows" by stamping every row in `[fromRow,
|
|
1329
|
+
* toRow]` with an outline depth of `level` (default 1). Allocates a
|
|
1330
|
+
* RowDimension for each row that doesn't already have one. Ungroup with {@link
|
|
1331
|
+
* ungroupRows}.
|
|
1332
|
+
*/
|
|
1333
|
+
function groupRows(ws, fromRow, toRow, level = 1) {
|
|
1334
|
+
validateOutlineLevel(level);
|
|
1335
|
+
if (!Number.isInteger(fromRow) || !Number.isInteger(toRow) || fromRow < 1 || toRow < fromRow) throw new OpenXmlSchemaError(`groupRows: invalid row range [${fromRow}, ${toRow}]`);
|
|
1336
|
+
for (let r = fromRow; r <= toRow; r++) {
|
|
1337
|
+
const existing = getRowDimension(ws, r) ?? {};
|
|
1338
|
+
setRowDimension(ws, r, {
|
|
1339
|
+
...existing,
|
|
1340
|
+
outlineLevel: level
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Drop the outline grouping for every row in `[fromRow, toRow]`. Removes the
|
|
1346
|
+
* `outlineLevel` field from each affected RowDimension.
|
|
1347
|
+
*/
|
|
1348
|
+
function ungroupRows(ws, fromRow, toRow) {
|
|
1349
|
+
if (!Number.isInteger(fromRow) || !Number.isInteger(toRow) || fromRow < 1 || toRow < fromRow) throw new OpenXmlSchemaError(`ungroupRows: invalid row range [${fromRow}, ${toRow}]`);
|
|
1350
|
+
for (let r = fromRow; r <= toRow; r++) {
|
|
1351
|
+
const existing = ws.rowDimensions.get(r);
|
|
1352
|
+
if (!existing) continue;
|
|
1353
|
+
const { outlineLevel: _drop, ...rest } = existing;
|
|
1354
|
+
if (Object.keys(rest).length === 0) ws.rowDimensions.delete(r);
|
|
1355
|
+
else ws.rowDimensions.set(r, rest);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Mirror Excel's "Data → Group → Columns" by stamping every column in
|
|
1360
|
+
* `[fromCol, toCol]` with an outline depth of `level` (default 1).
|
|
1361
|
+
*/
|
|
1362
|
+
function groupColumns(ws, fromCol, toCol, level = 1) {
|
|
1363
|
+
validateOutlineLevel(level);
|
|
1364
|
+
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol < 1 || toCol < fromCol) throw new OpenXmlSchemaError(`groupColumns: invalid column range [${fromCol}, ${toCol}]`);
|
|
1365
|
+
for (let c = fromCol; c <= toCol; c++) {
|
|
1366
|
+
const existing = getColumnDimension(ws, c);
|
|
1367
|
+
setColumnDimension(ws, c, {
|
|
1368
|
+
...existing,
|
|
1369
|
+
outlineLevel: level
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Drop the outline grouping for every column in `[fromCol, toCol]`. Removes the
|
|
1375
|
+
* `outlineLevel` field from each affected ColumnDimension.
|
|
1376
|
+
*/
|
|
1377
|
+
function ungroupColumns(ws, fromCol, toCol) {
|
|
1378
|
+
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol < 1 || toCol < fromCol) throw new OpenXmlSchemaError(`ungroupColumns: invalid column range [${fromCol}, ${toCol}]`);
|
|
1379
|
+
for (let c = fromCol; c <= toCol; c++) {
|
|
1380
|
+
const existing = getColumnDimension(ws, c);
|
|
1381
|
+
if (!existing) continue;
|
|
1382
|
+
const { outlineLevel: _drop, ...rest } = existing;
|
|
1383
|
+
const { min: _min, max: _max, ...passthrough } = rest;
|
|
1384
|
+
if (Object.keys(passthrough).length === 0) ws.columnDimensions.delete(existing.min);
|
|
1385
|
+
else setColumnDimension(ws, c, passthrough);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Collapse a row outline group: hide every row in `[fromRow, toRow]` and mark
|
|
1390
|
+
* them `collapsed: true`. Mirrors Excel's `−` button on a grouped row strip.
|
|
1391
|
+
* Rows must already carry an `outlineLevel` from {@link groupRows} for the
|
|
1392
|
+
* collapse to render correctly.
|
|
1393
|
+
*/
|
|
1394
|
+
function collapseRowGroup(ws, fromRow, toRow) {
|
|
1395
|
+
if (!Number.isInteger(fromRow) || !Number.isInteger(toRow) || fromRow < 1 || toRow < fromRow) throw new OpenXmlSchemaError(`collapseRowGroup: invalid row range [${fromRow}, ${toRow}]`);
|
|
1396
|
+
for (let r = fromRow; r <= toRow; r++) {
|
|
1397
|
+
const existing = getRowDimension(ws, r) ?? {};
|
|
1398
|
+
setRowDimension(ws, r, {
|
|
1399
|
+
...existing,
|
|
1400
|
+
hidden: true,
|
|
1401
|
+
collapsed: true
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Expand a row outline group: drop the `hidden` and `collapsed` flags on every
|
|
1407
|
+
* row in `[fromRow, toRow]`. Leaves `outlineLevel` and other dimensions intact.
|
|
1408
|
+
*/
|
|
1409
|
+
function expandRowGroup(ws, fromRow, toRow) {
|
|
1410
|
+
if (!Number.isInteger(fromRow) || !Number.isInteger(toRow) || fromRow < 1 || toRow < fromRow) throw new OpenXmlSchemaError(`expandRowGroup: invalid row range [${fromRow}, ${toRow}]`);
|
|
1411
|
+
for (let r = fromRow; r <= toRow; r++) {
|
|
1412
|
+
const existing = ws.rowDimensions.get(r);
|
|
1413
|
+
if (!existing) continue;
|
|
1414
|
+
const { hidden: _h, collapsed: _c, ...rest } = existing;
|
|
1415
|
+
if (Object.keys(rest).length === 0) ws.rowDimensions.delete(r);
|
|
1416
|
+
else ws.rowDimensions.set(r, rest);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Collapse a column outline group: hide + mark `collapsed: true` for every
|
|
1421
|
+
* column in `[fromCol, toCol]`. Columns must already carry an `outlineLevel`
|
|
1422
|
+
* from {@link groupColumns} for the collapse to render correctly.
|
|
1423
|
+
*/
|
|
1424
|
+
function collapseColumnGroup(ws, fromCol, toCol) {
|
|
1425
|
+
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol < 1 || toCol < fromCol) throw new OpenXmlSchemaError(`collapseColumnGroup: invalid column range [${fromCol}, ${toCol}]`);
|
|
1426
|
+
for (let c = fromCol; c <= toCol; c++) {
|
|
1427
|
+
const existing = getColumnDimension(ws, c);
|
|
1428
|
+
setColumnDimension(ws, c, {
|
|
1429
|
+
...existing,
|
|
1430
|
+
hidden: true,
|
|
1431
|
+
collapsed: true
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Expand a column outline group: drop `hidden` and `collapsed` from every
|
|
1437
|
+
* column in `[fromCol, toCol]`. Leaves `outlineLevel` intact.
|
|
1438
|
+
*/
|
|
1439
|
+
function expandColumnGroup(ws, fromCol, toCol) {
|
|
1440
|
+
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol < 1 || toCol < fromCol) throw new OpenXmlSchemaError(`expandColumnGroup: invalid column range [${fromCol}, ${toCol}]`);
|
|
1441
|
+
for (let c = fromCol; c <= toCol; c++) {
|
|
1442
|
+
const existing = getColumnDimension(ws, c);
|
|
1443
|
+
if (!existing) continue;
|
|
1444
|
+
const { hidden: _h, collapsed: _coll, ...rest } = existing;
|
|
1445
|
+
const { min: _min, max: _max, ...passthrough } = rest;
|
|
1446
|
+
if (Object.keys(passthrough).length === 0) ws.columnDimensions.delete(existing.min);
|
|
1447
|
+
else setColumnDimension(ws, c, passthrough);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Per-cell effective string length, optionally scaled by the cell's font size
|
|
1452
|
+
* relative to Excel's default 11pt baseline.
|
|
1453
|
+
*/
|
|
1454
|
+
const effectiveLength = (cell, wb) => {
|
|
1455
|
+
const len = cellValueAsString(cell.value).length;
|
|
1456
|
+
if (!wb) return len;
|
|
1457
|
+
const fontId = wb.styles.cellXfs[cell.styleId]?.fontId ?? 0;
|
|
1458
|
+
return len * ((wb.styles.fonts[fontId]?.size ?? 11) / 11);
|
|
1459
|
+
};
|
|
1460
|
+
/**
|
|
1461
|
+
* Approximate autofit for every column with at least one populated cell. Walks
|
|
1462
|
+
* the worksheet once collecting per-column widest-length + applies {@link
|
|
1463
|
+
* autofitColumn} per column. `opts.workbook` enables font-size-aware scaling;
|
|
1464
|
+
* without it the helper falls back to plain string length.
|
|
1465
|
+
*/
|
|
1466
|
+
function autofitColumns(ws, opts = {}) {
|
|
1467
|
+
const padding = opts.padding ?? 2;
|
|
1468
|
+
const minWidth = opts.min ?? 4;
|
|
1469
|
+
const maxWidth = Math.min(opts.max ?? 80, 255);
|
|
1470
|
+
const widest = /* @__PURE__ */ new Map();
|
|
1471
|
+
for (const rowMap of ws.rows.values()) for (const [col, cell] of rowMap) {
|
|
1472
|
+
const len = effectiveLength(cell, opts.workbook);
|
|
1473
|
+
if (len > (widest.get(col) ?? 0)) widest.set(col, len);
|
|
1474
|
+
}
|
|
1475
|
+
for (const [col, w] of widest) {
|
|
1476
|
+
if (w === 0) continue;
|
|
1477
|
+
setColumnWidth(ws, col, Math.max(minWidth, Math.min(maxWidth, w + padding)));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Set widths for many columns in one call. `widths` maps either:
|
|
1482
|
+
* - an array `[12, 16, 20]` interpreted positionally starting at
|
|
1483
|
+
* column `startCol` (default 1), or
|
|
1484
|
+
* - a `Record<number, number>` keyed by 1-based column index.
|
|
1485
|
+
* Each entry sets `customWidth: true`.
|
|
1486
|
+
*/
|
|
1487
|
+
function setColumnWidths(ws, widths, startCol = 1) {
|
|
1488
|
+
if (Array.isArray(widths)) for (let i = 0; i < widths.length; i++) {
|
|
1489
|
+
const w = widths[i];
|
|
1490
|
+
if (typeof w !== "number" || !Number.isFinite(w)) continue;
|
|
1491
|
+
setColumnWidth(ws, startCol + i, w);
|
|
1492
|
+
}
|
|
1493
|
+
else for (const [k, w] of Object.entries(widths)) {
|
|
1494
|
+
const col = Number.parseInt(k, 10);
|
|
1495
|
+
if (!Number.isInteger(col) || col < 1) continue;
|
|
1496
|
+
if (typeof w !== "number" || !Number.isFinite(w)) continue;
|
|
1497
|
+
setColumnWidth(ws, col, w);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/** Look up a row's dimension entry. */
|
|
1501
|
+
function getRowDimension(ws, row) {
|
|
1502
|
+
return ws.rowDimensions.get(row);
|
|
1503
|
+
}
|
|
1504
|
+
function setRowDimension(ws, row, opts) {
|
|
1505
|
+
validateRowCol(row, 1);
|
|
1506
|
+
const entry = makeRowDimension(opts);
|
|
1507
|
+
ws.rowDimensions.set(row, entry);
|
|
1508
|
+
return entry;
|
|
1509
|
+
}
|
|
1510
|
+
/** Convenience: set a row's height, marking customHeight=true. */
|
|
1511
|
+
function setRowHeight(ws, row, height) {
|
|
1512
|
+
return setRowDimension(ws, row, {
|
|
1513
|
+
...getRowDimension(ws, row),
|
|
1514
|
+
height,
|
|
1515
|
+
customHeight: true
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Set heights for many rows in one call. `heights` accepts an array (positional
|
|
1520
|
+
* from `startRow`, default 1) or a `Record<number, number>` keyed by 1-based
|
|
1521
|
+
* row index. Each entry sets `customHeight: true`.
|
|
1522
|
+
*/
|
|
1523
|
+
function setRowHeights(ws, heights, startRow = 1) {
|
|
1524
|
+
if (Array.isArray(heights)) for (let i = 0; i < heights.length; i++) {
|
|
1525
|
+
const h = heights[i];
|
|
1526
|
+
if (typeof h !== "number" || !Number.isFinite(h)) continue;
|
|
1527
|
+
setRowHeight(ws, startRow + i, h);
|
|
1528
|
+
}
|
|
1529
|
+
else for (const [k, h] of Object.entries(heights)) {
|
|
1530
|
+
const row = Number.parseInt(k, 10);
|
|
1531
|
+
if (!Number.isInteger(row) || row < 1) continue;
|
|
1532
|
+
if (typeof h !== "number" || !Number.isFinite(h)) continue;
|
|
1533
|
+
setRowHeight(ws, row, h);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
/** Convenience: hide a row. */
|
|
1537
|
+
function hideRow(ws, row) {
|
|
1538
|
+
return setRowDimension(ws, row, {
|
|
1539
|
+
...getRowDimension(ws, row),
|
|
1540
|
+
hidden: true
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Convenience: unhide a row. Drops the `hidden` flag from the row's dimension
|
|
1545
|
+
* entry (and removes the entry altogether when no other fields remain).
|
|
1546
|
+
*/
|
|
1547
|
+
function unhideRow(ws, row) {
|
|
1548
|
+
const existing = ws.rowDimensions.get(row);
|
|
1549
|
+
if (!existing) return;
|
|
1550
|
+
const { hidden: _drop, ...rest } = existing;
|
|
1551
|
+
if (Object.keys(rest).length === 0) ws.rowDimensions.delete(row);
|
|
1552
|
+
else ws.rowDimensions.set(row, rest);
|
|
1553
|
+
}
|
|
1554
|
+
/** Bulk-hide every row in `[fromRow, toRow]`. */
|
|
1555
|
+
function hideRows(ws, fromRow, toRow) {
|
|
1556
|
+
if (!Number.isInteger(fromRow) || !Number.isInteger(toRow) || fromRow < 1 || toRow < fromRow) throw new OpenXmlSchemaError(`hideRows: invalid row range [${fromRow}, ${toRow}]`);
|
|
1557
|
+
for (let r = fromRow; r <= toRow; r++) hideRow(ws, r);
|
|
1558
|
+
}
|
|
1559
|
+
/** Bulk-unhide every row in `[fromRow, toRow]`. */
|
|
1560
|
+
function unhideRows(ws, fromRow, toRow) {
|
|
1561
|
+
if (!Number.isInteger(fromRow) || !Number.isInteger(toRow) || fromRow < 1 || toRow < fromRow) throw new OpenXmlSchemaError(`unhideRows: invalid row range [${fromRow}, ${toRow}]`);
|
|
1562
|
+
for (let r = fromRow; r <= toRow; r++) unhideRow(ws, r);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Replace any prior hyperlink on the same `ref` with the given options. Pass `{
|
|
1566
|
+
* target }` for an external URL, `{ location }` for an internal jump, or both.
|
|
1567
|
+
* Returns the resulting Hyperlink record.
|
|
1568
|
+
*/
|
|
1569
|
+
function setHyperlink(ws, ref, opts) {
|
|
1570
|
+
if (opts.target === void 0 && opts.location === void 0) throw new OpenXmlSchemaError("setHyperlink: one of target / location is required");
|
|
1571
|
+
removeHyperlink(ws, ref);
|
|
1572
|
+
const hl = makeHyperlink({
|
|
1573
|
+
ref,
|
|
1574
|
+
...opts
|
|
1575
|
+
});
|
|
1576
|
+
ws.hyperlinks.push(hl);
|
|
1577
|
+
return hl;
|
|
1578
|
+
}
|
|
1579
|
+
/** Remove the hyperlink registered against `ref`. Returns true if anything was removed. */
|
|
1580
|
+
function removeHyperlink(ws, ref) {
|
|
1581
|
+
const i = ws.hyperlinks.findIndex((h) => h.ref === ref);
|
|
1582
|
+
if (i < 0) return false;
|
|
1583
|
+
ws.hyperlinks.splice(i, 1);
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
/** Drop every hyperlink on the worksheet. Returns the count removed. */
|
|
1587
|
+
function removeAllHyperlinks(ws) {
|
|
1588
|
+
const n = ws.hyperlinks.length;
|
|
1589
|
+
ws.hyperlinks = [];
|
|
1590
|
+
return n;
|
|
1591
|
+
}
|
|
1592
|
+
/** Read-only snapshot of every hyperlink on the sheet. */
|
|
1593
|
+
function listHyperlinks(ws) {
|
|
1594
|
+
return ws.hyperlinks;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Resolve a cell to its hyperlink (if any). Walks every hyperlink entry on the
|
|
1598
|
+
* worksheet and returns the first whose `ref` (a single cell `"A1"` or a range
|
|
1599
|
+
* `"A1:B5"`) covers the cell's coordinate. Returns `undefined` when no entry
|
|
1600
|
+
* matches.
|
|
1601
|
+
*/
|
|
1602
|
+
function getCellHyperlink(ws, c) {
|
|
1603
|
+
for (const h of ws.hyperlinks) if (rangeContainsCell(parseRange(h.ref), c.row, c.col)) return h;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Resolve a cell to its legacy comment (if any). Same matching rule as {@link
|
|
1607
|
+
* getCellHyperlink} — the comment's `ref` may be a single cell or a range, and
|
|
1608
|
+
* the first containing entry wins.
|
|
1609
|
+
*/
|
|
1610
|
+
function getCellComment(ws, c) {
|
|
1611
|
+
for (const cm of ws.legacyComments) if (rangeContainsCell(parseRange(cm.ref), c.row, c.col)) return cm;
|
|
1612
|
+
}
|
|
1613
|
+
/** Append a DataValidation entry. */
|
|
1614
|
+
function addDataValidation(ws, dv) {
|
|
1615
|
+
ws.dataValidations.push(dv);
|
|
1616
|
+
return dv;
|
|
1617
|
+
}
|
|
1618
|
+
/** Drop every validation whose sqref overlaps `ref` (string parse). Returns count removed. */
|
|
1619
|
+
function removeDataValidations(ws, predicate) {
|
|
1620
|
+
const before = ws.dataValidations.length;
|
|
1621
|
+
ws.dataValidations = ws.dataValidations.filter((dv) => !predicate(dv));
|
|
1622
|
+
return before - ws.dataValidations.length;
|
|
1623
|
+
}
|
|
1624
|
+
/** Read-only snapshot of every data validation block on the sheet. */
|
|
1625
|
+
function listDataValidations(ws) {
|
|
1626
|
+
return ws.dataValidations;
|
|
1627
|
+
}
|
|
1628
|
+
/** Drop every data validation block on the worksheet. Returns the count removed. */
|
|
1629
|
+
function removeAllDataValidations(ws) {
|
|
1630
|
+
const n = ws.dataValidations.length;
|
|
1631
|
+
ws.dataValidations = [];
|
|
1632
|
+
return n;
|
|
1633
|
+
}
|
|
1634
|
+
/** Set or replace the worksheet's AutoFilter. Pass `undefined` to clear. */
|
|
1635
|
+
function setAutoFilter(ws, filter) {
|
|
1636
|
+
if (filter === void 0) {
|
|
1637
|
+
delete ws.autoFilter;
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
ws.autoFilter = filter;
|
|
1641
|
+
}
|
|
1642
|
+
/** Read the current AutoFilter, if any. */
|
|
1643
|
+
function getAutoFilter(ws) {
|
|
1644
|
+
return ws.autoFilter;
|
|
1645
|
+
}
|
|
1646
|
+
/** Append a table. The id and displayName must be workbook-unique — the caller is responsible. */
|
|
1647
|
+
function addTable(ws, table) {
|
|
1648
|
+
ws.tables.push(table);
|
|
1649
|
+
return table;
|
|
1650
|
+
}
|
|
1651
|
+
/** Look up a table by displayName. */
|
|
1652
|
+
function getTable(ws, displayName) {
|
|
1653
|
+
return ws.tables.find((t) => t.displayName === displayName);
|
|
1654
|
+
}
|
|
1655
|
+
/** Read-only snapshot of every Excel table defined on the sheet. */
|
|
1656
|
+
function listTables(ws) {
|
|
1657
|
+
return ws.tables;
|
|
1658
|
+
}
|
|
1659
|
+
/** Drop a table by displayName. Returns true when something was removed. */
|
|
1660
|
+
function removeTable(ws, displayName) {
|
|
1661
|
+
const i = ws.tables.findIndex((t) => t.displayName === displayName);
|
|
1662
|
+
if (i < 0) return false;
|
|
1663
|
+
ws.tables.splice(i, 1);
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
/** Drop every Excel table on the worksheet. Returns the count removed. */
|
|
1667
|
+
function removeAllTables(ws) {
|
|
1668
|
+
const n = ws.tables.length;
|
|
1669
|
+
ws.tables = [];
|
|
1670
|
+
return n;
|
|
1671
|
+
}
|
|
1672
|
+
/** Add or replace the comment at `ref`. */
|
|
1673
|
+
function setComment(ws, opts) {
|
|
1674
|
+
const i = ws.legacyComments.findIndex((c) => c.ref === opts.ref);
|
|
1675
|
+
const c = makeLegacyComment(opts);
|
|
1676
|
+
if (i < 0) ws.legacyComments.push(c);
|
|
1677
|
+
else ws.legacyComments[i] = c;
|
|
1678
|
+
return c;
|
|
1679
|
+
}
|
|
1680
|
+
/** Read-only snapshot of every legacy comment on the sheet. */
|
|
1681
|
+
function listComments(ws) {
|
|
1682
|
+
return ws.legacyComments;
|
|
1683
|
+
}
|
|
1684
|
+
/** Drop every legacy comment on the worksheet. Returns the count removed. */
|
|
1685
|
+
function removeAllComments(ws) {
|
|
1686
|
+
const n = ws.legacyComments.length;
|
|
1687
|
+
ws.legacyComments = [];
|
|
1688
|
+
return n;
|
|
1689
|
+
}
|
|
1690
|
+
/** Append a conditional formatting block. */
|
|
1691
|
+
function addConditionalFormatting(ws, cf) {
|
|
1692
|
+
ws.conditionalFormatting.push(cf);
|
|
1693
|
+
return cf;
|
|
1694
|
+
}
|
|
1695
|
+
/** Drop every conditional-formatting block on the worksheet. Returns the count removed. */
|
|
1696
|
+
function removeAllConditionalFormatting(ws) {
|
|
1697
|
+
const n = ws.conditionalFormatting.length;
|
|
1698
|
+
ws.conditionalFormatting = [];
|
|
1699
|
+
return n;
|
|
1700
|
+
}
|
|
1701
|
+
/** Pin a cell to the Watch Window. Returns the pushed entry. */
|
|
1702
|
+
function addCellWatch(ws, watch) {
|
|
1703
|
+
ws.cellWatches.push(watch);
|
|
1704
|
+
return watch;
|
|
1705
|
+
}
|
|
1706
|
+
/** Remove cell watches matching `predicate`. Returns the count removed. */
|
|
1707
|
+
function removeCellWatches(ws, predicate) {
|
|
1708
|
+
const before = ws.cellWatches.length;
|
|
1709
|
+
ws.cellWatches = ws.cellWatches.filter((w) => !predicate(w));
|
|
1710
|
+
return before - ws.cellWatches.length;
|
|
1711
|
+
}
|
|
1712
|
+
/** Append an ignored-error region. */
|
|
1713
|
+
function addIgnoredError(ws, ie) {
|
|
1714
|
+
ws.ignoredErrors.push(ie);
|
|
1715
|
+
return ie;
|
|
1716
|
+
}
|
|
1717
|
+
/** Remove ignored-error entries matching `predicate`. Returns the count removed. */
|
|
1718
|
+
function removeIgnoredErrors(ws, predicate) {
|
|
1719
|
+
const before = ws.ignoredErrors.length;
|
|
1720
|
+
ws.ignoredErrors = ws.ignoredErrors.filter((ie) => !predicate(ie));
|
|
1721
|
+
return before - ws.ignoredErrors.length;
|
|
1722
|
+
}
|
|
1723
|
+
//#endregion
|
|
1724
|
+
export { iterRows as $, makeRowDimension as $t, getCellsInRow as A, unionRange as An, setRangeValues as At, getPopulatedRowIndices as B, unhideColumns as Bt, getAutoFilter as C, parseRange as Cn, setColumnWidth as Ct, getCellHyperlink as D, rangeToString as Dn, setDefaultRowHeight as Dt, getCellComment as E, rangeContainsRange as En, setDefaultColumnWidth as Et, getMaxRow as F, setSheetViewMode as Ft, groupRows as G, writeRange as Gt, getRowDimension as H, unhideRows as Ht, getMergedCells as I, setSheetZoom as It, hideRow as J, freezePaneRef as Jt, hideColumn as K, makeSheetProperties as Kt, getMergedRangeAt as L, ungroupColumns as Lt, getDataExtent as M, setRowHeight as Mt, getFreezePanes as N, setRowHeights as Nt, getCellsInColumn as O, rangesOverlap as On, setFreezePanes as Ot, getMaxCol as P, setSheetTabColor as Pt, iterCells as Q, makeColumnDimension as Qt, getNonEmptyCellCount as R, ungroupRows as Rt, freezePanes as S, parseMultiCellRange as Sn, setColumnDimension as St, getCellByCoord as T, rangeContainsCell as Tn, setComment as Tt, getTable as U, unmergeCells as Ut, getRangeValues as V, unhideRow as Vt, groupColumns as W, unmergeCellsAt as Wt, isMergedCell as X, makeSheetView as Xt, hideRows as Y, makeFreezePane as Yt, isWorksheetEmpty as Z, makeHyperlink as Zt, countCellsByKind as _, intersectionRange as _n, removeIgnoredErrors as _t, addTable as a, hexToHsl as an, makeWorksheet as at, expandRowGroup as b, multiCellRangeContainsCell as bn, setCell as bt, applyToRange as c, luminance as cn, removeAllComments as ct, clearAllCells as d, normaliseRgb as dn, removeAllHyperlinks as dt, adjustLightness as en, iterValues as et, clearRange as f, pickReadableTextColor as fn, removeAllMergedRanges as ft, countCells as g, expandRangeStr as gn, removeHyperlink as gt, copyRange as h, rotateHue as hn, removeDataValidations as ht, addIgnoredError as i, darken as in, listTables as it, getColumnDimension as j, setRowDimension as jt, getCellsInRange as k, shiftRange as kn, setHyperlink as kt, autofitColumns as l, makeColor as ln, removeAllConditionalFormatting as lt, collapseRowGroup as m, rgbColor as mn, removeCellWatches as mt, addConditionalFormatting as n, colorToHex as nn, listDataValidations as nt, appendRow as o, hslToHex as on, mergeCells as ot, collapseColumnGroup as p, resolveIndexedColor as pn, removeAllTables as pt, hideColumns as q, makeLegacyComment as qt, addDataValidation as r, contrastRatio as rn, listHyperlinks as rt, appendRows as s, lighten as sn, moveRange as st, addCellWatch as t, adjustSaturation as tn, listComments as tt, classifyCellValue as u, mixColors as un, removeAllDataValidations as ut, deleteCell as v, isCellInRange as vn, removeTable as vt, getCell as w, rangeArea as wn, setColumnWidths as wt, findCells as x, multiCellRangeToString as xn, setCellByCoord as xt, expandColumnGroup as y, isRangeInRange as yn, setAutoFilter as yt, getPopulatedColumnIndices as z, unhideColumn as zt };
|
|
1725
|
+
|
|
1726
|
+
//# sourceMappingURL=worksheet-CmCNoIgD.mjs.map
|