documonster 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README_zh.md +1 -1
- package/dist/esm/modules/excel/core/cell.js +171 -56
- package/dist/esm/modules/excel/core/row.js +10 -4
- package/dist/esm/modules/excel/core/style-sharing.js +132 -0
- package/dist/esm/modules/excel/core/table.js +14 -2
- package/dist/esm/modules/excel/core/worksheet-core.js +28 -10
- package/dist/esm/modules/excel/core/xlsx-io.js +7 -0
- package/dist/esm/modules/excel/surface/cell.js +31 -8
- package/dist/esm/modules/excel/surface/column.js +12 -1
- package/dist/esm/modules/excel/surface/row.js +12 -1
- package/dist/esm/modules/excel/surface/table.js +17 -9
- package/dist/esm/modules/excel/utils/copy-style.js +65 -21
- package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +9 -1
- package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +118 -29
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +54 -2
- package/dist/esm/modules/xml/encode.js +23 -0
- package/dist/esm/modules/xml/stream-writer.js +8 -8
- package/dist/esm/modules/xml/writer.js +7 -7
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.draw.iife.min.js +1 -1
- package/dist/iife/documonster.excel.iife.min.js +53 -53
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.mermaid.iife.min.js +1 -1
- package/dist/iife/documonster.pdf.iife.min.js +10 -10
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +60 -60
- package/dist/iife/documonster.xml.iife.min.js +7 -7
- package/dist/types/modules/excel/core/cell.d.ts +59 -9
- package/dist/types/modules/excel/core/style-sharing.d.ts +48 -0
- package/dist/types/modules/excel/core/xlsx-io.d.ts +7 -0
- package/dist/types/modules/excel/surface/cell.d.ts +7 -0
- package/dist/types/modules/excel/surface/column.d.ts +8 -0
- package/dist/types/modules/excel/surface/row.d.ts +8 -0
- package/dist/types/modules/excel/surface/table.d.ts +13 -1
- package/dist/types/modules/excel/utils/copy-style.d.ts +23 -1
- package/dist/types/modules/excel/xlsx/xform/style/styles-xform.d.ts +28 -1
- package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +10 -0
- package/dist/types/modules/xml/encode.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
211
211
|
|
|
212
212
|
```html
|
|
213
213
|
<!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
|
|
214
|
-
<script src="https://unpkg.com/documonster@0.
|
|
214
|
+
<script src="https://unpkg.com/documonster@0.14.0/dist/iife/documonster.excel.iife.min.js"></script>
|
|
215
215
|
<script>
|
|
216
216
|
const { Workbook, Cell } = Documonster.Excel;
|
|
217
217
|
const wb = Workbook.create();
|
package/README_zh.md
CHANGED
|
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
198
198
|
|
|
199
199
|
```html
|
|
200
200
|
<!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
|
|
201
|
-
<script src="https://unpkg.com/documonster@0.
|
|
201
|
+
<script src="https://unpkg.com/documonster@0.14.0/dist/iife/documonster.excel.iife.min.js"></script>
|
|
202
202
|
<script>
|
|
203
203
|
const { Workbook, Cell } = Documonster.Excel;
|
|
204
204
|
const wb = Workbook.create();
|
|
@@ -2,14 +2,20 @@ import { dataValidationAdd, dataValidationFind } from "./data-validations.js";
|
|
|
2
2
|
import { definedNamesAddEx, definedNamesGetNamesEx, definedNamesRemoveAllNames, definedNamesRemoveEx } from "./defined-names.js";
|
|
3
3
|
import { Enums } from "./enums.js";
|
|
4
4
|
import { isNoteData, noteCreate, noteFromModel, noteModel } from "./note.js";
|
|
5
|
+
import { EMPTY_CELL_STYLE, sharedCellContainer, sharedCellFacet } from "./style-sharing.js";
|
|
5
6
|
import { isTemporalPlainValue, partsToTemporal, temporalKindOf, temporalRefusal, temporalToParts } from "./temporal.js";
|
|
6
7
|
import { ExcelError, InvalidValueTypeError } from "../errors.js";
|
|
7
8
|
import { dateFormatKind, getCellDisplayText } from "../utils/cell-format.js";
|
|
8
9
|
import { colCache } from "../utils/col-cache.js";
|
|
9
|
-
import { copyStyle } from "../utils/copy-style.js";
|
|
10
|
+
import { copyStyle, copyStyleFacet } from "../utils/copy-style.js";
|
|
10
11
|
import { slideFormula } from "../utils/shared-formula.js";
|
|
11
12
|
import { isExcelPhantomDay, partsToSerial, partsToUtcDate, serialToParts, utcDateToParts } from "../../../utils/excel-serial.js";
|
|
12
13
|
import { excelToDate } from "#platform/utils/utils";
|
|
14
|
+
/**
|
|
15
|
+
* The style facets that are objects, and therefore the ones that can be shared by
|
|
16
|
+
* reference or need copying. `numFmt` and `styleName` are primitives.
|
|
17
|
+
*/
|
|
18
|
+
const OBJECT_FACETS = ["font", "alignment", "protection", "border", "fill"];
|
|
13
19
|
function hasOwnKeys(v) {
|
|
14
20
|
return !!v && (typeof v !== "object" || Object.keys(v).length > 0);
|
|
15
21
|
}
|
|
@@ -54,30 +60,43 @@ export function mergeCellStyle(rowStyle, colStyle, style) {
|
|
|
54
60
|
if (numFmt) {
|
|
55
61
|
style.numFmt = numFmt;
|
|
56
62
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
// Object facets are taken from the owner's shared snapshot rather than deep-copied
|
|
64
|
+
// per cell — see `style-sharing.ts`. Callers mark the cell `_sharedStyle`.
|
|
65
|
+
//
|
|
66
|
+
// Written out per facet rather than as a loop over a key list on purpose. This runs
|
|
67
|
+
// once per cell created — the hottest path in the library — and a loop reads
|
|
68
|
+
// `rowStyle[key]` through a variable, which V8 cannot inline the way it inlines
|
|
69
|
+
// `rowStyle.font`. Measured on 20k `Cell.setValue` calls into unstyled rows and
|
|
70
|
+
// columns, the loop cost 46% over this.
|
|
71
|
+
if (hasOwnKeys(rowStyle.font)) {
|
|
72
|
+
style.font = sharedCellFacet(rowStyle, "font");
|
|
73
|
+
}
|
|
74
|
+
else if (hasOwnKeys(colStyle.font)) {
|
|
75
|
+
style.font = sharedCellFacet(colStyle, "font");
|
|
76
|
+
}
|
|
77
|
+
if (hasOwnKeys(rowStyle.alignment)) {
|
|
78
|
+
style.alignment = sharedCellFacet(rowStyle, "alignment");
|
|
79
|
+
}
|
|
80
|
+
else if (hasOwnKeys(colStyle.alignment)) {
|
|
81
|
+
style.alignment = sharedCellFacet(colStyle, "alignment");
|
|
61
82
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (alignment) {
|
|
65
|
-
style.alignment = structuredClone(alignment);
|
|
83
|
+
if (hasOwnKeys(rowStyle.border)) {
|
|
84
|
+
style.border = sharedCellFacet(rowStyle, "border");
|
|
66
85
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (border) {
|
|
70
|
-
style.border = structuredClone(border);
|
|
86
|
+
else if (hasOwnKeys(colStyle.border)) {
|
|
87
|
+
style.border = sharedCellFacet(colStyle, "border");
|
|
71
88
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (fill) {
|
|
75
|
-
style.fill = structuredClone(fill);
|
|
89
|
+
if (hasOwnKeys(rowStyle.fill)) {
|
|
90
|
+
style.fill = sharedCellFacet(rowStyle, "fill");
|
|
76
91
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
else if (hasOwnKeys(colStyle.fill)) {
|
|
93
|
+
style.fill = sharedCellFacet(colStyle, "fill");
|
|
94
|
+
}
|
|
95
|
+
if (hasOwnKeys(rowStyle.protection)) {
|
|
96
|
+
style.protection = sharedCellFacet(rowStyle, "protection");
|
|
97
|
+
}
|
|
98
|
+
else if (hasOwnKeys(colStyle.protection)) {
|
|
99
|
+
style.protection = sharedCellFacet(colStyle, "protection");
|
|
81
100
|
}
|
|
82
101
|
return style;
|
|
83
102
|
}
|
|
@@ -98,6 +117,11 @@ export function cellCreate(row, column, address) {
|
|
|
98
117
|
column,
|
|
99
118
|
address,
|
|
100
119
|
style: mergeCellStyle(row.style, column.style, {}),
|
|
120
|
+
// Declared here rather than assigned conditionally so every cell keeps one
|
|
121
|
+
// hidden class. `mergeCellStyle` may have pointed a facet at the row's or
|
|
122
|
+
// column's shared snapshot; the flag only claims it *may* have, and
|
|
123
|
+
// `cellOwnStyle` is a cheap no-op when it did not.
|
|
124
|
+
_sharedStyle: true,
|
|
101
125
|
_mergeCount: 0
|
|
102
126
|
};
|
|
103
127
|
cell._value = Value.create(Types.Null, cell);
|
|
@@ -120,75 +144,153 @@ export function cellNumFmt(c) {
|
|
|
120
144
|
return c.style.numFmt;
|
|
121
145
|
}
|
|
122
146
|
export function cellSetNumFmt(c, value) {
|
|
123
|
-
c.
|
|
147
|
+
ownStyleContainer(c).numFmt = value;
|
|
124
148
|
}
|
|
125
149
|
export function cellFont(c) {
|
|
126
150
|
return c.style.font;
|
|
127
151
|
}
|
|
128
152
|
export function cellSetFont(c, value) {
|
|
129
|
-
c.
|
|
153
|
+
ownStyleContainer(c).font = value;
|
|
154
|
+
if (value !== null && typeof value === "object") {
|
|
155
|
+
// Stored by reference, so the caller still holds it — see {@link cellSetStyle}.
|
|
156
|
+
c._sharedStyle = true;
|
|
157
|
+
}
|
|
130
158
|
}
|
|
131
159
|
export function cellAlignment(c) {
|
|
132
160
|
return c.style.alignment;
|
|
133
161
|
}
|
|
134
162
|
export function cellSetAlignment(c, value) {
|
|
135
|
-
c.
|
|
163
|
+
ownStyleContainer(c).alignment = value;
|
|
164
|
+
if (value !== null && typeof value === "object") {
|
|
165
|
+
// Stored by reference, so the caller still holds it — see {@link cellSetStyle}.
|
|
166
|
+
c._sharedStyle = true;
|
|
167
|
+
}
|
|
136
168
|
}
|
|
137
169
|
export function cellBorder(c) {
|
|
138
170
|
return c.style.border;
|
|
139
171
|
}
|
|
140
172
|
export function cellSetBorder(c, value) {
|
|
141
|
-
c.
|
|
173
|
+
ownStyleContainer(c).border = value;
|
|
174
|
+
if (value !== null && typeof value === "object") {
|
|
175
|
+
// Stored by reference, so the caller still holds it — see {@link cellSetStyle}.
|
|
176
|
+
c._sharedStyle = true;
|
|
177
|
+
}
|
|
142
178
|
}
|
|
143
179
|
export function cellFill(c) {
|
|
144
180
|
return c.style.fill;
|
|
145
181
|
}
|
|
146
182
|
export function cellSetFill(c, value) {
|
|
147
|
-
c.
|
|
183
|
+
ownStyleContainer(c).fill = value;
|
|
184
|
+
if (value !== null && typeof value === "object") {
|
|
185
|
+
// Stored by reference, so the caller still holds it — see {@link cellSetStyle}.
|
|
186
|
+
c._sharedStyle = true;
|
|
187
|
+
}
|
|
148
188
|
}
|
|
149
189
|
export function cellProtection(c) {
|
|
150
190
|
return c.style.protection;
|
|
151
191
|
}
|
|
152
192
|
export function cellSetProtection(c, value) {
|
|
153
|
-
c.
|
|
193
|
+
ownStyleContainer(c).protection = value;
|
|
194
|
+
if (value !== null && typeof value === "object") {
|
|
195
|
+
// Stored by reference, so the caller still holds it — see {@link cellSetStyle}.
|
|
196
|
+
c._sharedStyle = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Give the cell its own style *container*, so it can be written to.
|
|
201
|
+
*
|
|
202
|
+
* A cell whose style came wholly from one source — every cell that loaded with a given
|
|
203
|
+
* `styleId` — points at one shared container rather than holding a copy of it, which
|
|
204
|
+
* is worth ~85 bytes per cell. The container is frozen, and **that is also the marker**:
|
|
205
|
+
* no second flag is needed, and any write that forgets to come through here throws
|
|
206
|
+
* instead of rewriting every cell that shares it.
|
|
207
|
+
*
|
|
208
|
+
* Only the container is unshared. The facets inside it stay shared until
|
|
209
|
+
* {@link cellOwnStyle}, because replacing one facet is no reason to copy the rest.
|
|
210
|
+
*/
|
|
211
|
+
function ownStyleContainer(c) {
|
|
212
|
+
if (Object.isFrozen(c.style)) {
|
|
213
|
+
c.style = { ...c.style };
|
|
214
|
+
}
|
|
215
|
+
return c.style;
|
|
154
216
|
}
|
|
155
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Give the cell its own copy of any facet it may be sharing, then return its style.
|
|
219
|
+
*
|
|
220
|
+
* This is the boundary that keeps "style isolation" true while cells share facets:
|
|
221
|
+
* a facet may be shared with the row/column snapshot it was inherited from, with
|
|
222
|
+
* sibling cells sharing that snapshot, or with a caller who handed it to a setter —
|
|
223
|
+
* so anything that lets a caller reach a facet must come through here first.
|
|
224
|
+
* `surface/` calls it on every reader that returns a facet; core internals and the
|
|
225
|
+
* writers read `c.style` directly and never materialise, which is what keeps a
|
|
226
|
+
* styled sheet's memory flat.
|
|
227
|
+
*
|
|
228
|
+
* Idempotent and allocation-free once the cell owns its facets.
|
|
229
|
+
*/
|
|
230
|
+
export function cellOwnStyle(c) {
|
|
231
|
+
if (c._sharedStyle) {
|
|
232
|
+
c._sharedStyle = false;
|
|
233
|
+
const style = ownStyleContainer(c);
|
|
234
|
+
for (const key of OBJECT_FACETS) {
|
|
235
|
+
const facet = style[key];
|
|
236
|
+
if (facet) {
|
|
237
|
+
style[key] = copyStyleFacet(key, facet);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return ownStyleContainer(c);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Read the cell's full style record (numFmt / font / alignment / border / fill /
|
|
245
|
+
* protection) **without** materialising it. Internal readers and the writers use
|
|
246
|
+
* this; a public reader must use {@link cellOwnStyle}.
|
|
247
|
+
*/
|
|
156
248
|
export function cellGetStyle(c) {
|
|
157
249
|
return c.style;
|
|
158
250
|
}
|
|
159
251
|
/** Merge a partial style into the cell's existing style. */
|
|
160
252
|
export function cellSetStyle(c, style) {
|
|
161
253
|
if (style.numFmt !== undefined) {
|
|
162
|
-
c.
|
|
254
|
+
ownStyleContainer(c).numFmt = style.numFmt;
|
|
163
255
|
}
|
|
164
256
|
if (style.styleName !== undefined) {
|
|
165
|
-
c.
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
257
|
+
ownStyleContainer(c).styleName = style.styleName;
|
|
258
|
+
}
|
|
259
|
+
// Object facets are stored by reference, so the caller — and any other cell set
|
|
260
|
+
// from the same object — still holds them, and the cell has to be marked as
|
|
261
|
+
// sharing. Before that flag existed, setting two cells from one style object
|
|
262
|
+
// aliased them permanently: mutating `Cell.getStyle(a).font` changed the other
|
|
263
|
+
// cell *and* the caller's object.
|
|
264
|
+
//
|
|
265
|
+
// Only mark when an object facet is actually stored. Marking unconditionally
|
|
266
|
+
// meant that a `numFmt`-only update re-shared the cell, so the *next* read
|
|
267
|
+
// re-copied facets the cell already owned privately — silently detaching a
|
|
268
|
+
// reference the caller was still holding from an earlier `Cell.getStyle`.
|
|
269
|
+
for (const key of OBJECT_FACETS) {
|
|
270
|
+
const facet = style[key];
|
|
271
|
+
if (facet !== undefined) {
|
|
272
|
+
ownStyleContainer(c)[key] = facet;
|
|
273
|
+
if (facet !== null && typeof facet === "object") {
|
|
274
|
+
c._sharedStyle = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
181
277
|
}
|
|
182
278
|
}
|
|
183
279
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
280
|
+
* Point a cell's facet at `value` without copying it, and mark the cell as sharing.
|
|
281
|
+
*
|
|
282
|
+
* This is the row/column propagation path — once per cell per facet — and `value` is
|
|
283
|
+
* the owner's shared snapshot, so every cell in a styled column ends up pointing at
|
|
284
|
+
* one object instead of holding its own copy. {@link cellOwnStyle} separates a cell
|
|
285
|
+
* again the moment a caller can reach the facet.
|
|
286
|
+
*
|
|
287
|
+
* The generic `K` keeps the key and value types linked — a widened `keyof Style` loop
|
|
288
|
+
* variable would collapse the index type to the intersection of all facet types and
|
|
289
|
+
* break assignment.
|
|
189
290
|
*/
|
|
190
|
-
export function
|
|
191
|
-
|
|
291
|
+
export function setFacetShared(cell, key, value) {
|
|
292
|
+
ownStyleContainer(cell)[key] = value;
|
|
293
|
+
cell._sharedStyle = true;
|
|
192
294
|
}
|
|
193
295
|
/**
|
|
194
296
|
* Assign a single style facet onto `target` by reference (no clone). Companion
|
|
@@ -289,7 +391,7 @@ export function cellSetValue(c, v) {
|
|
|
289
391
|
if (kind !== undefined && c.style.numFmt === undefined) {
|
|
290
392
|
// Only when the cell has none of its own: an explicit format the caller set is theirs, and a value assignment
|
|
291
393
|
// is not the place to overrule it.
|
|
292
|
-
c.
|
|
394
|
+
ownStyleContainer(c).numFmt = defaultNumFmtFor(kind);
|
|
293
395
|
}
|
|
294
396
|
}
|
|
295
397
|
/** Upper bound for each calendar field. `day` is checked against the month once the month is known. */
|
|
@@ -398,7 +500,7 @@ export function cellSetDateParts(c, parts, kind) {
|
|
|
398
500
|
// serial (60) but no `Date`, and building one would silently move it to March 1.
|
|
399
501
|
cellSetValue(c, excelToDate(partsToSerial(filled, date1904), date1904));
|
|
400
502
|
if (c.style.numFmt === undefined) {
|
|
401
|
-
c.
|
|
503
|
+
ownStyleContainer(c).numFmt = defaultNumFmtFor(resolved);
|
|
402
504
|
}
|
|
403
505
|
}
|
|
404
506
|
/**
|
|
@@ -494,6 +596,7 @@ export function cellMerge(c, master, ignoreStyle) {
|
|
|
494
596
|
c._value = Value.create(Types.Merge, c, master);
|
|
495
597
|
if (!ignoreStyle) {
|
|
496
598
|
c.style = copyStyle(master.style) ?? {};
|
|
599
|
+
c._sharedStyle = false;
|
|
497
600
|
}
|
|
498
601
|
}
|
|
499
602
|
export function cellUnmerge(c) {
|
|
@@ -502,6 +605,7 @@ export function cellUnmerge(c) {
|
|
|
502
605
|
c._value.release();
|
|
503
606
|
c._value = Value.create(Types.Null, c);
|
|
504
607
|
c.style = mergeCellStyle(c.row.style, c.column.style, { ...c.style });
|
|
608
|
+
c._sharedStyle = true;
|
|
505
609
|
}
|
|
506
610
|
}
|
|
507
611
|
export function cellIsMergedTo(c, master) {
|
|
@@ -688,10 +792,21 @@ export function cellSetModel(c, value) {
|
|
|
688
792
|
}
|
|
689
793
|
}
|
|
690
794
|
if (value.style) {
|
|
691
|
-
|
|
795
|
+
// Share the model's whole style *container*, not just its facets. The read path
|
|
796
|
+
// hands the same style model to every cell carrying a given `styleId`
|
|
797
|
+
// (`StylesXform.getStyleModel` caches one per id), so one container and one set of
|
|
798
|
+
// facet snapshots covers all of them — loading a 200k-cell sheet with a single
|
|
799
|
+
// logical style cost 110 MB of duplicated style objects before this.
|
|
800
|
+
//
|
|
801
|
+
// The container is frozen, which both enforces the sharing and marks it: a write
|
|
802
|
+
// has to come through `ownStyleContainer` and a missed one throws. `_sharedStyle`
|
|
803
|
+
// separately marks the facets inside it.
|
|
804
|
+
c.style = sharedCellContainer(value.style);
|
|
805
|
+
c._sharedStyle = true;
|
|
692
806
|
}
|
|
693
807
|
else {
|
|
694
|
-
c.style =
|
|
808
|
+
c.style = EMPTY_CELL_STYLE;
|
|
809
|
+
c._sharedStyle = false;
|
|
695
810
|
}
|
|
696
811
|
}
|
|
697
812
|
class NullValue {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { cellCol, cellGetModel, cellGetValue, cellType, setFacet,
|
|
1
|
+
import { cellCol, cellGetModel, cellGetValue, cellType, setFacet, setFacetShared } from "./cell.js";
|
|
2
2
|
import { Enums } from "./enums.js";
|
|
3
|
+
import { invalidateSharedCellStyle, sharedCellFacet } from "./style-sharing.js";
|
|
3
4
|
/**
|
|
4
5
|
* Assign one style facet to the row and to every cell it already holds.
|
|
5
6
|
*
|
|
@@ -12,9 +13,12 @@ import { Enums } from "./enums.js";
|
|
|
12
13
|
*/
|
|
13
14
|
function applyStyle(r, name, value) {
|
|
14
15
|
r.style[name] = value;
|
|
16
|
+
invalidateSharedCellStyle(r.style);
|
|
17
|
+
// One snapshot for the whole row, shared by every cell — see `style-sharing.ts`.
|
|
18
|
+
const shared = sharedCellFacet(r.style, name);
|
|
15
19
|
r.cells.forEach(cell => {
|
|
16
20
|
if (cell) {
|
|
17
|
-
|
|
21
|
+
setFacetShared(cell, name, shared);
|
|
18
22
|
}
|
|
19
23
|
});
|
|
20
24
|
}
|
|
@@ -142,10 +146,12 @@ export function rowSetStyle(r, style) {
|
|
|
142
146
|
for (const k of keys) {
|
|
143
147
|
setFacet(r.style, k, style[k]);
|
|
144
148
|
}
|
|
149
|
+
invalidateSharedCellStyle(r.style);
|
|
150
|
+
const shared = keys.map(k => sharedCellFacet(r.style, k));
|
|
145
151
|
r.cells.forEach(cell => {
|
|
146
152
|
if (cell) {
|
|
147
|
-
for (
|
|
148
|
-
|
|
153
|
+
for (let i = 0; i < keys.length; i++) {
|
|
154
|
+
setFacetShared(cell, keys[i], shared[i]);
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { copyStyleFacet } from "../utils/copy-style.js";
|
|
2
|
+
/**
|
|
3
|
+
* The single copy of a row's or column's style facets that every cell it covers
|
|
4
|
+
* points at.
|
|
5
|
+
*
|
|
6
|
+
* Without it each cell kept its own deep copy of the same facets — measured at 382
|
|
7
|
+
* bytes per cell, so a 200k-cell sheet carrying one logical style paid 73 MB to say
|
|
8
|
+
* the same thing 200,000 times.
|
|
9
|
+
*
|
|
10
|
+
* **A copy, not the owner's own facet object.** That is what leaves the owner's
|
|
11
|
+
* semantics untouched: `Column.getStyle(ws, 1).font.bold = false` must not reach
|
|
12
|
+
* cells that were already styled, exactly as it does not today. Cells point into the
|
|
13
|
+
* snapshot; a caller mutating the column mutates the column's own object.
|
|
14
|
+
*
|
|
15
|
+
* **Keyed by the style object, not the owner.** Ten places replace a row's or
|
|
16
|
+
* column's `style` wholesale (`applyDefn`, `rowSetModel`, `duplicateRow`,
|
|
17
|
+
* `spliceRows`, …). Keying on the object means every one of those invalidates by
|
|
18
|
+
* construction — a field on the owner would have needed all ten to remember, and the
|
|
19
|
+
* one that forgot would hand cells a snapshot of a style the owner no longer has.
|
|
20
|
+
* Only mutating a facet *on the same object* needs {@link invalidateSharedCellStyle},
|
|
21
|
+
* and those sites are the handful of `set*` calls in this module's callers.
|
|
22
|
+
*/
|
|
23
|
+
const snapshots = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* Freeze a snapshot facet and everything under it.
|
|
26
|
+
*
|
|
27
|
+
* A shared facet is reachable from hundreds of thousands of cells, so mutating one
|
|
28
|
+
* in place would silently rewrite all of them. Freezing turns that into a `TypeError`
|
|
29
|
+
* at the offending line — ESM is strict mode, so the assignment throws rather than
|
|
30
|
+
* being ignored. It is the enforcement half of the sharing invariant: `cellOwnStyle`
|
|
31
|
+
* hands out private copies, and this guarantees anything that skipped it fails loudly
|
|
32
|
+
* instead of corrupting a neighbour.
|
|
33
|
+
*
|
|
34
|
+
* `Cell.view` is the case that needs it most: it is documented as a read-only
|
|
35
|
+
* projection and its `font`/`alignment` are the shared objects, deliberately not
|
|
36
|
+
* materialised so that iterating a row stays allocation-free.
|
|
37
|
+
*
|
|
38
|
+
* Shape-agnostic on purpose — the per-facet nesting is already spelled out once in
|
|
39
|
+
* `copy-style.ts` and repeating it here would be a second definition to drift. Runs
|
|
40
|
+
* once per snapshot, not per cell. The `isFrozen` check also terminates on a cycle.
|
|
41
|
+
*/
|
|
42
|
+
function deepFreeze(value) {
|
|
43
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
44
|
+
Object.freeze(value);
|
|
45
|
+
for (const key of Object.keys(value)) {
|
|
46
|
+
deepFreeze(value[key]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The facet a cell covered by `style`'s owner should point at: the same object for
|
|
52
|
+
* every such cell, built on first use and frozen.
|
|
53
|
+
*
|
|
54
|
+
* `numFmt` and `styleName` are primitives, so they are returned as-is and never enter
|
|
55
|
+
* a snapshot.
|
|
56
|
+
*/
|
|
57
|
+
export function sharedCellFacet(style, key) {
|
|
58
|
+
const value = style[key];
|
|
59
|
+
if (!value || typeof value !== "object") {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
let snapshot = snapshots.get(style);
|
|
63
|
+
if (!snapshot) {
|
|
64
|
+
snapshot = {};
|
|
65
|
+
snapshots.set(style, snapshot);
|
|
66
|
+
}
|
|
67
|
+
let facet = snapshot[key];
|
|
68
|
+
if (facet === undefined) {
|
|
69
|
+
facet = copyStyleFacet(key, value);
|
|
70
|
+
deepFreeze(facet);
|
|
71
|
+
snapshot[key] = facet;
|
|
72
|
+
}
|
|
73
|
+
return facet;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The frozen style container every cell resolving to `style` should point at.
|
|
77
|
+
*
|
|
78
|
+
* A cell whose style comes wholly from one source — every cell that loaded with a
|
|
79
|
+
* given `styleId`, since the reader caches one style model per id — needs no container
|
|
80
|
+
* of its own. Sharing it is worth ~85 bytes per cell on top of the facets, and it is
|
|
81
|
+
* the last per-cell cost of a style that many cells agree on.
|
|
82
|
+
*
|
|
83
|
+
* Frozen for the same reason the facets are: it both enforces the sharing and marks
|
|
84
|
+
* it, so `ownStyleContainer` can recognise a shared container without a second flag
|
|
85
|
+
* and a write that skips it throws rather than rewriting every cell that shares it.
|
|
86
|
+
*
|
|
87
|
+
* The container's own facets are the snapshots, so `sharedCellFacet` and this agree by
|
|
88
|
+
* construction. Fields that are not facets — `numFmt`, `styleName`, and the xf-level
|
|
89
|
+
* `pivotButton`/`apply*` flags the reader preserves — are carried across verbatim.
|
|
90
|
+
*/
|
|
91
|
+
export function sharedCellContainer(style) {
|
|
92
|
+
let container = containers.get(style);
|
|
93
|
+
if (!container) {
|
|
94
|
+
container = { ...style };
|
|
95
|
+
for (const key of OBJECT_FACETS) {
|
|
96
|
+
if (container[key] !== undefined) {
|
|
97
|
+
container[key] = sharedCellFacet(style, key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
Object.freeze(container);
|
|
101
|
+
containers.set(style, container);
|
|
102
|
+
}
|
|
103
|
+
return container;
|
|
104
|
+
}
|
|
105
|
+
const containers = new WeakMap();
|
|
106
|
+
/**
|
|
107
|
+
* The container an unstyled cell points at.
|
|
108
|
+
*
|
|
109
|
+
* A cell always has a `style` object because 182 readers say `cell.style.font`, but a
|
|
110
|
+
* cell with no style has nothing to put in it, and allocating one per cell is pure
|
|
111
|
+
* overhead — 200k of them on a plain sheet. Frozen like every shared container, so
|
|
112
|
+
* `ownStyleContainer` gives a cell its own the moment anything writes to it.
|
|
113
|
+
*/
|
|
114
|
+
export const EMPTY_CELL_STYLE = Object.freeze({});
|
|
115
|
+
/** The style facets that are objects, and so the ones that can be shared or copied. */
|
|
116
|
+
const OBJECT_FACETS = ["font", "alignment", "protection", "border", "fill"];
|
|
117
|
+
/**
|
|
118
|
+
* Drop the snapshot of `style`, so the next cell that needs a facet takes a fresh one.
|
|
119
|
+
*
|
|
120
|
+
* Needed when a facet is replaced **on this same object** — a wholesale
|
|
121
|
+
* `owner.style = …` invalidates on its own, because the snapshot is keyed by object.
|
|
122
|
+
* Call it in two situations:
|
|
123
|
+
*
|
|
124
|
+
* - A `set*` that writes `owner.style.font = …` in place.
|
|
125
|
+
* - Handing the style to a caller, who may mutate it in place. Today a cell created
|
|
126
|
+
* after such a mutation copies the mutated facet, and invalidating on handout is
|
|
127
|
+
* what preserves that.
|
|
128
|
+
*/
|
|
129
|
+
export function invalidateSharedCellStyle(style) {
|
|
130
|
+
snapshots.delete(style);
|
|
131
|
+
containers.delete(style);
|
|
132
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { cellGetValue, cellSetValue } from "./cell.js";
|
|
1
|
+
import { cellGetValue, cellSetValue, setFacetShared } from "./cell.js";
|
|
2
|
+
import { sharedCellFacet } from "./style-sharing.js";
|
|
2
3
|
import { getRow, getSheetWorkbook, rowGetCell } from "./worksheet-core.js";
|
|
3
4
|
import { TableError } from "../errors.js";
|
|
4
5
|
import { colCache } from "../utils/col-cache.js";
|
|
@@ -225,9 +226,20 @@ export function tableValidate(t) {
|
|
|
225
226
|
}
|
|
226
227
|
export function tableStore(t) {
|
|
227
228
|
// where the table needs to store table data, headers, footers in the sheet...
|
|
229
|
+
//
|
|
230
|
+
// A table column's style is one object applied to its header, every body row and
|
|
231
|
+
// its totals cell, so it is a style owner in the sense of `style-sharing.ts`: the
|
|
232
|
+
// cells share one frozen snapshot of it rather than each holding a copy, and they
|
|
233
|
+
// are marked so that `Cell.get*` separates them again.
|
|
234
|
+
//
|
|
235
|
+
// This used to be `Object.assign(cell.style, style)`, which shared the facets *and*
|
|
236
|
+
// said nothing about it, so every cell in a table column was permanently aliased —
|
|
237
|
+
// `Cell.getStyle(a).font.bold = false` rewrote the whole column.
|
|
228
238
|
const assignStyle = (cell, style) => {
|
|
229
239
|
if (style) {
|
|
230
|
-
Object.
|
|
240
|
+
for (const key of Object.keys(style)) {
|
|
241
|
+
setFacetShared(cell, key, sharedCellFacet(style, key));
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
};
|
|
233
245
|
const { worksheet, table } = t;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { CellTypes, cellComment, cellCreate, cellGetValue,
|
|
1
|
+
import { CellTypes, cellComment, cellCreate, cellGetValue, cellSetComment, cellSetModel, cellSetNumFmt, cellSetValue, cellType, setFacet, setFacetShared } from "./cell.js";
|
|
2
2
|
import { columnHeaders } from "./column.js";
|
|
3
3
|
import { Enums } from "./enums.js";
|
|
4
4
|
import { rangeCreate, rangeRange } from "./range.js";
|
|
5
5
|
import { rowCreate, rowFindCell, rowHasValues, rowValues, resolveColumnKeyValue } from "./row.js";
|
|
6
|
+
import { invalidateSharedCellStyle, sharedCellFacet } from "./style-sharing.js";
|
|
6
7
|
import { ExcelError, InvalidAddressError } from "../errors.js";
|
|
7
8
|
import { colCache } from "../utils/col-cache.js";
|
|
8
9
|
import { copyStyle } from "../utils/copy-style.js";
|
|
@@ -416,32 +417,47 @@ export function columnSetNumFmt(c, value) {
|
|
|
416
417
|
}
|
|
417
418
|
export function columnSetFont(c, value) {
|
|
418
419
|
c.style.font = value;
|
|
420
|
+
invalidateSharedCellStyle(c.style);
|
|
421
|
+
// One snapshot for the whole column, shared by every cell — see `style-sharing.ts`.
|
|
422
|
+
const shared = sharedCellFacet(c.style, "font");
|
|
419
423
|
columnEachCell(c, cell => {
|
|
420
|
-
|
|
424
|
+
setFacetShared(cell, "font", shared);
|
|
421
425
|
});
|
|
422
426
|
}
|
|
423
427
|
export function columnSetAlignment(c, value) {
|
|
424
428
|
c.style.alignment = value;
|
|
429
|
+
invalidateSharedCellStyle(c.style);
|
|
430
|
+
// One snapshot for the whole column, shared by every cell — see `style-sharing.ts`.
|
|
431
|
+
const shared = sharedCellFacet(c.style, "alignment");
|
|
425
432
|
columnEachCell(c, cell => {
|
|
426
|
-
|
|
433
|
+
setFacetShared(cell, "alignment", shared);
|
|
427
434
|
});
|
|
428
435
|
}
|
|
429
436
|
export function columnSetProtection(c, value) {
|
|
430
437
|
c.style.protection = value;
|
|
438
|
+
invalidateSharedCellStyle(c.style);
|
|
439
|
+
// One snapshot for the whole column, shared by every cell — see `style-sharing.ts`.
|
|
440
|
+
const shared = sharedCellFacet(c.style, "protection");
|
|
431
441
|
columnEachCell(c, cell => {
|
|
432
|
-
|
|
442
|
+
setFacetShared(cell, "protection", shared);
|
|
433
443
|
});
|
|
434
444
|
}
|
|
435
445
|
export function columnSetBorder(c, value) {
|
|
436
446
|
c.style.border = value;
|
|
447
|
+
invalidateSharedCellStyle(c.style);
|
|
448
|
+
// One snapshot for the whole column, shared by every cell — see `style-sharing.ts`.
|
|
449
|
+
const shared = sharedCellFacet(c.style, "border");
|
|
437
450
|
columnEachCell(c, cell => {
|
|
438
|
-
|
|
451
|
+
setFacetShared(cell, "border", shared);
|
|
439
452
|
});
|
|
440
453
|
}
|
|
441
454
|
export function columnSetFill(c, value) {
|
|
442
455
|
c.style.fill = value;
|
|
456
|
+
invalidateSharedCellStyle(c.style);
|
|
457
|
+
// One snapshot for the whole column, shared by every cell — see `style-sharing.ts`.
|
|
458
|
+
const shared = sharedCellFacet(c.style, "fill");
|
|
443
459
|
columnEachCell(c, cell => {
|
|
444
|
-
|
|
460
|
+
setFacetShared(cell, "fill", shared);
|
|
445
461
|
});
|
|
446
462
|
}
|
|
447
463
|
/**
|
|
@@ -460,14 +476,16 @@ export function columnSetStyle(c, style) {
|
|
|
460
476
|
if (keys.length === 0) {
|
|
461
477
|
return;
|
|
462
478
|
}
|
|
463
|
-
// The column's own style holds each facet by reference;
|
|
464
|
-
//
|
|
479
|
+
// The column's own style holds each facet by reference; the cells share a single
|
|
480
|
+
// snapshot of it, so a column of 200k cells stores one copy rather than 200k.
|
|
465
481
|
for (const k of keys) {
|
|
466
482
|
setFacet(c.style, k, style[k]);
|
|
467
483
|
}
|
|
484
|
+
invalidateSharedCellStyle(c.style);
|
|
485
|
+
const shared = keys.map(k => sharedCellFacet(c.style, k));
|
|
468
486
|
columnEachCell(c, cell => {
|
|
469
|
-
for (
|
|
470
|
-
|
|
487
|
+
for (let i = 0; i < keys.length; i++) {
|
|
488
|
+
setFacetShared(cell, keys[i], shared[i]);
|
|
471
489
|
}
|
|
472
490
|
});
|
|
473
491
|
}
|
|
@@ -33,6 +33,13 @@ export function getXlsxIo(wb) {
|
|
|
33
33
|
* and the fallback wraps the existing memory (`Buffer.from(buffer, byteOffset,
|
|
34
34
|
* byteLength)` is a view, never a copy). Either way no bytes are duplicated, and
|
|
35
35
|
* the declared type cannot silently drift from the runtime one.
|
|
36
|
+
*
|
|
37
|
+
* **Timing this?** Measure with `NODE_ENV=production`. Outside production (and outside
|
|
38
|
+
* vitest) every write runs the OOXML self-check, which re-parses the package it just
|
|
39
|
+
* produced — measured at 38 ms against 124 ms on a 100-column × 200-row table, so a
|
|
40
|
+
* development-mode figure is roughly **3.3× the real one** and is not distributed
|
|
41
|
+
* across the write in the way a profile suggests. Pass `{ validate: false }` to
|
|
42
|
+
* suppress it explicitly; see `XlsxWriteOptions.validate`.
|
|
36
43
|
*/
|
|
37
44
|
export async function toBuffer(wb, options) {
|
|
38
45
|
const bytes = options?.format === "xlsb"
|