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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/dist/esm/modules/excel/core/cell.js +171 -56
  4. package/dist/esm/modules/excel/core/row.js +10 -4
  5. package/dist/esm/modules/excel/core/style-sharing.js +132 -0
  6. package/dist/esm/modules/excel/core/table.js +14 -2
  7. package/dist/esm/modules/excel/core/worksheet-core.js +28 -10
  8. package/dist/esm/modules/excel/core/xlsx-io.js +7 -0
  9. package/dist/esm/modules/excel/surface/cell.js +31 -8
  10. package/dist/esm/modules/excel/surface/column.js +12 -1
  11. package/dist/esm/modules/excel/surface/row.js +12 -1
  12. package/dist/esm/modules/excel/surface/table.js +17 -9
  13. package/dist/esm/modules/excel/utils/copy-style.js +65 -21
  14. package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +9 -1
  15. package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +118 -29
  16. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +54 -2
  17. package/dist/esm/modules/xml/encode.js +23 -0
  18. package/dist/esm/modules/xml/stream-writer.js +8 -8
  19. package/dist/esm/modules/xml/writer.js +7 -7
  20. package/dist/iife/documonster.archive.iife.min.js +1 -1
  21. package/dist/iife/documonster.csv.iife.min.js +1 -1
  22. package/dist/iife/documonster.draw.iife.min.js +1 -1
  23. package/dist/iife/documonster.excel.iife.min.js +53 -53
  24. package/dist/iife/documonster.formula.iife.min.js +1 -1
  25. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  26. package/dist/iife/documonster.mermaid.iife.min.js +1 -1
  27. package/dist/iife/documonster.pdf.iife.min.js +10 -10
  28. package/dist/iife/documonster.stream.iife.min.js +1 -1
  29. package/dist/iife/documonster.word.iife.min.js +60 -60
  30. package/dist/iife/documonster.xml.iife.min.js +7 -7
  31. package/dist/types/modules/excel/core/cell.d.ts +59 -9
  32. package/dist/types/modules/excel/core/style-sharing.d.ts +48 -0
  33. package/dist/types/modules/excel/core/xlsx-io.d.ts +7 -0
  34. package/dist/types/modules/excel/surface/cell.d.ts +7 -0
  35. package/dist/types/modules/excel/surface/column.d.ts +8 -0
  36. package/dist/types/modules/excel/surface/row.d.ts +8 -0
  37. package/dist/types/modules/excel/surface/table.d.ts +13 -1
  38. package/dist/types/modules/excel/utils/copy-style.d.ts +23 -1
  39. package/dist/types/modules/excel/xlsx/xform/style/styles-xform.d.ts +28 -1
  40. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +10 -0
  41. package/dist/types/modules/xml/encode.d.ts +18 -0
  42. 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.13.1/dist/iife/documonster.excel.iife.min.js"></script>
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.13.1/dist/iife/documonster.excel.iife.min.js"></script>
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
- const font = (rowStyle && hasOwnKeys(rowStyle.font) && rowStyle.font) ||
58
- (colStyle && hasOwnKeys(colStyle.font) && colStyle.font);
59
- if (font) {
60
- style.font = structuredClone(font);
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
- const alignment = (rowStyle && hasOwnKeys(rowStyle.alignment) && rowStyle.alignment) ||
63
- (colStyle && hasOwnKeys(colStyle.alignment) && colStyle.alignment);
64
- if (alignment) {
65
- style.alignment = structuredClone(alignment);
83
+ if (hasOwnKeys(rowStyle.border)) {
84
+ style.border = sharedCellFacet(rowStyle, "border");
66
85
  }
67
- const border = (rowStyle && hasOwnKeys(rowStyle.border) && rowStyle.border) ||
68
- (colStyle && hasOwnKeys(colStyle.border) && colStyle.border);
69
- if (border) {
70
- style.border = structuredClone(border);
86
+ else if (hasOwnKeys(colStyle.border)) {
87
+ style.border = sharedCellFacet(colStyle, "border");
71
88
  }
72
- const fill = (rowStyle && hasOwnKeys(rowStyle.fill) && rowStyle.fill) ||
73
- (colStyle && hasOwnKeys(colStyle.fill) && colStyle.fill);
74
- if (fill) {
75
- style.fill = structuredClone(fill);
89
+ if (hasOwnKeys(rowStyle.fill)) {
90
+ style.fill = sharedCellFacet(rowStyle, "fill");
76
91
  }
77
- const protection = (rowStyle && hasOwnKeys(rowStyle.protection) && rowStyle.protection) ||
78
- (colStyle && hasOwnKeys(colStyle.protection) && colStyle.protection);
79
- if (protection) {
80
- style.protection = structuredClone(protection);
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.style.numFmt = value;
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.style.font = value;
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.style.alignment = value;
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.style.border = value;
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.style.fill = value;
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.style.protection = value;
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
- /** Read the cell's full style record (numFmt / font / alignment / border / fill / protection). */
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.style.numFmt = style.numFmt;
254
+ ownStyleContainer(c).numFmt = style.numFmt;
163
255
  }
164
256
  if (style.styleName !== undefined) {
165
- c.style.styleName = style.styleName;
166
- }
167
- if (style.font !== undefined) {
168
- c.style.font = style.font;
169
- }
170
- if (style.alignment !== undefined) {
171
- c.style.alignment = style.alignment;
172
- }
173
- if (style.border !== undefined) {
174
- c.style.border = style.border;
175
- }
176
- if (style.fill !== undefined) {
177
- c.style.fill = style.fill;
178
- }
179
- if (style.protection !== undefined) {
180
- c.style.protection = style.protection;
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
- * Assign a single style facet onto `target`, deep-cloning the value so the
185
- * target never aliases a shared sub-object (`numFmt` is a primitive and is
186
- * effectively copied by value). The generic `K` keeps the key and value types
187
- * linked — a widened `keyof Style` loop variable would collapse the index type
188
- * to the intersection of all facet types and break assignment.
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 setFacetCloned(target, key, value) {
191
- target[key] = typeof value === "object" && value !== null ? structuredClone(value) : value;
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.style.numFmt = defaultNumFmtFor(kind);
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.style.numFmt = defaultNumFmtFor(resolved);
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
- c.style = copyStyle(value.style) ?? {};
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, setFacetCloned } from "./cell.js";
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
- setFacetCloned(cell.style, name, value);
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 (const k of keys) {
148
- setFacetCloned(cell.style, k, style[k]);
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.assign(cell.style, style);
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, cellSetAlignment, cellSetBorder, cellSetComment, cellSetFill, cellSetFont, cellSetModel, cellSetNumFmt, cellSetProtection, cellSetValue, cellType, setFacet, setFacetCloned } from "./cell.js";
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
- cellSetFont(cell, value ? structuredClone(value) : value);
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
- cellSetAlignment(cell, value ? structuredClone(value) : value);
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
- cellSetProtection(cell, value ? structuredClone(value) : value);
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
- cellSetBorder(cell, value ? structuredClone(value) : value);
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
- cellSetFill(cell, value ? structuredClone(value) : value);
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; every cell gets a
464
- // deep-cloned copy so cells never alias the column's style sub-objects.
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 (const k of keys) {
470
- setFacetCloned(cell.style, k, style[k]);
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"