@shbernal/ts-xlsx 1.0.3 → 1.2.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 (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,9 +1,3 @@
1
- // Worksheet serialisation: a Worksheet model into its `xl/worksheets/sheetN.xml` part and the sheet's
2
- // rels part. Owns the row/cell renderer the streaming writer also drives, and orchestrates the whole
3
- // sheet body — the print/page/view/protection blocks live in `sheet-properties.ts`, shared-formula
4
- // planning in `shared-formulas.ts`, each imported here rather than duplicated. Table *parts*
5
- // (`xl/tables/tableN.xml`) are `tables.ts`'s concern, alongside their reader — this module only wires
6
- // the sheet's `<tableParts>` back-references to them.
7
1
  import { decodeRange, encodeAddress, MAX_COLUMN } from '../../core/address.js';
8
2
  import { DEFAULT_DATE_NUMFMT, dateToSerial } from '../../core/date.js';
9
3
  import { mangleFormula } from '../../core/formula.js';
@@ -22,19 +16,11 @@ import { richTextRunsXml } from './rich-text.js';
22
16
  import { planSharedFormulas } from './shared-formulas.js';
23
17
  import { autoFilterXml, headerFooterXml, pageBreaksXml, pageMarginsXml, pageSetupXml, printOptionsXml, sheetProtectionXml, sheetPrXml, sheetViewsXml, } from './sheet-properties.js';
24
18
  import { x14Ext } from './x14-ext.js';
25
- /**
26
- * The used-cell extent of a sheet — the top-left/bottom-right grid bounds that fold into the
27
- * `<dimension>`. Rows carrying only formatting (a row height, an outline level) do not extend the
28
- * used range, matching how Excel records `<dimension>`, so {@link add} ignores them. A fresh extent
29
- * holds the `Infinity`/`-Infinity` sentinels; {@link isEmpty} reports that no used cell has been seen.
30
- */
31
19
  export class Extent {
32
20
  top = Infinity;
33
21
  left = Infinity;
34
22
  bottom = -Infinity;
35
23
  right = -Infinity;
36
- // Seed from a prior extent (the rows a streaming writer already flushed and evicted) so the buffered
37
- // pass folds its live rows onto the same bounds; unseeded, it starts empty.
38
24
  constructor(seed) {
39
25
  if (seed) {
40
26
  this.top = seed.top;
@@ -43,12 +29,9 @@ export class Extent {
43
29
  this.right = seed.right;
44
30
  }
45
31
  }
46
- /** Whether no used cell has been folded in yet — the sheet's dimension is then the lone cell `A1`. */
47
32
  get isEmpty() {
48
33
  return this.bottom === -Infinity;
49
34
  }
50
- /** Fold a rendered row's used-column span into the extent. `minCol` is `Infinity` when the row
51
- * carried no cells (only formatting), which extends nothing. */
52
35
  add(row, minCol, maxCol) {
53
36
  if (minCol === Infinity)
54
37
  return;
@@ -63,18 +46,9 @@ export class Extent {
63
46
  }
64
47
  }
65
48
  export function worksheetXml(sheet, tables, styles, references, hyperlinks, sharedStrings, active, flushed) {
66
- // A merge overlapping a table is Excel-invalid geometry; reject it before serialising
67
- // rather than emit a package a consumer repairs on open.
68
49
  validateMerges(sheet);
69
50
  const columnDefaults = buildColumnDefaults(sheet);
70
- // A cell filled from a shared formula is written as a master (seeding the group) or a clone
71
- // (referencing it by shared index); resolve every such role before the row loop so each cell knows
72
- // how to serialise its `<f>`. This also validates the master/clone geometry, throwing if a clone
73
- // precedes its master or its master carries no formula.
74
51
  const sharedRoles = planSharedFormulas(sheet);
75
- // A fully-hidden outline group's collapse toggle belongs on its summary row; derive that set once
76
- // so the row loop can stamp it even onto a summary row that carries no properties of its own. The
77
- // same pass yields the sheet's deepest row outline level for `<sheetFormatPr>`.
78
52
  const rowOutline = scanRowOutline(sheet);
79
53
  const collapsedSummaries = rowOutline.collapsedSummaries;
80
54
  const context = {
@@ -85,8 +59,6 @@ export function worksheetXml(sheet, tables, styles, references, hyperlinks, shar
85
59
  collapsedSummaries,
86
60
  };
87
61
  const liveRows = [];
88
- // Seed the used-cell extent with any rows the streaming writer already serialised and evicted, so
89
- // the dimension spans both them and the live rows below.
90
62
  const extent = new Extent(flushed?.extent);
91
63
  for (const entry of sheet.rows()) {
92
64
  const { xml, minCol, maxCol } = renderRow(entry, context);
@@ -98,9 +70,6 @@ export function worksheetXml(sheet, tables, styles, references, hyperlinks, shar
98
70
  const dimensionRef = extent.isEmpty
99
71
  ? 'A1'
100
72
  : `${encodeAddress(extent.left, extent.top)}:${encodeAddress(extent.right, extent.bottom)}`;
101
- // Merge the streaming writer's pre-rendered rows with the live ones into ascending row order — a
102
- // flushed row can carry any number, and rows may be committed out of order. The buffered path has no
103
- // flushed rows, so it skips the merge and its sort entirely.
104
73
  const orderedRows = flushed
105
74
  ? [...flushed.rows, ...liveRows].sort((a, b) => a.number - b.number)
106
75
  : liveRows;
@@ -113,75 +82,40 @@ export function worksheetXml(sheet, tables, styles, references, hyperlinks, shar
113
82
  sheetViewsXml(sheet.view, active) +
114
83
  sheetFormatPr(sheet.properties, {
115
84
  col: maxColumnOutlineLevel(sheet),
116
- // A streamed sheet's flushed rows are gone from the model; their deepest level rides along on
117
- // the flush record so the header still reports the whole sheet's outline.
118
85
  row: Math.max(rowOutline.maxLevel, flushed?.maxRowOutlineLevel ?? 0),
119
86
  }) +
120
87
  colsXml(sheet, styles) +
121
88
  sheetData +
122
89
  sheetProtectionXml(sheet.protection) +
123
- // CT_Worksheet order: <autoFilter> follows <sheetProtection> (and the scenarios block) and
124
- // precedes <mergeCells>. Its `_FilterDatabase` companion is emitted in the workbook part.
125
90
  autoFilterXml(sheet.autoFilter) +
126
91
  mergeCellsXml(sheet.merges) +
127
- // CT_Worksheet order: <conditionalFormatting> blocks follow <mergeCells>, then <dataValidations>,
128
- // then <hyperlinks> — all precede the print settings.
129
92
  conditionalFormattingsXml(sheet.conditionalFormattings, styles) +
130
93
  dataValidationsXml(sheet.dataValidations) +
131
94
  hyperlinksXml(hyperlinks) +
132
- // CT_Worksheet order: <printOptions> precedes <pageMargins>, which precedes <pageSetup>.
133
95
  printOptionsXml(sheet.printOptions) +
134
96
  pageMarginsXml(sheet.pageMargins) +
135
97
  pageSetupXml(sheet.pageSetup, references.printerSettingsRelId) +
136
98
  headerFooterXml(sheet.headerFooter) +
137
- // CT_Worksheet order: <rowBreaks> follows <headerFooter>, <colBreaks> follows <rowBreaks>, and
138
- // both precede the drawing block.
139
99
  pageBreaksXml(sheet.rowBreaks, 'rowBreaks') +
140
100
  pageBreaksXml(sheet.columnBreaks, 'colBreaks') +
141
- // Schema order near the tail: <drawing> (the images), then <legacyDrawing> (the VML holding the
142
- // note boxes), then <legacyDrawingHF> (a preserved header/footer image's VML), then <picture>
143
- // (the sheet background), then <tableParts>.
144
101
  refElement('drawing', references.drawingRelId) +
145
102
  refElement('legacyDrawing', references.legacyDrawingRelId) +
146
103
  refElement('legacyDrawingHF', references.legacyDrawingHFRelId) +
147
104
  refElement('picture', references.backgroundRelId) +
148
105
  tablePartsXml(tables) +
149
- // `<extLst>` is the final child of CT_Worksheet and a worksheet may carry at most one. Both the
150
- // x14 conditional-formatting extensions (data-bar gradient/negative-fill/axis) and the extended
151
- // (x14) data validations ride inside it as sibling `<ext>` blocks — so they are gathered here into
152
- // a single `<extLst>` rather than each emitting its own.
153
106
  worksheetExtLstXml(sheet, references.slicerRelIds) +
154
107
  '</worksheet>');
155
108
  }
156
- /**
157
- * A column's style facets are defaults its cells inherit unless they override them; the writer
158
- * composes each cell's full style up front (cell over row over column, per facet) so a cell that
159
- * overrides one facet still carries the column's others, rather than silently dropping them. Frozen
160
- * once by the streaming writer at its first flush so every eagerly-rendered row sees the same defaults.
161
- */
162
109
  export function buildColumnDefaults(sheet) {
163
110
  const columnDefaults = new Map();
164
- // `columns()` yields only columns that carry a format record, so the fallback is unreachable —
165
- // it is here because the handle's `properties` is honestly optional, not because a defined
166
- // column can lack one.
167
111
  for (const { index, properties } of sheet.columns())
168
112
  columnDefaults.set(index, properties ?? {});
169
113
  return columnDefaults;
170
114
  }
171
- /**
172
- * Serialise one row to its `<row>` element, or '' when the row has neither data nor its own
173
- * formatting. Returns the used-column bounds (`Infinity`/`-Infinity` when nothing was rendered) so a
174
- * caller can fold them into the sheet dimension. Shared by the buffered sheet pass and the streaming
175
- * writer's eager flush, so both emit byte-identical rows.
176
- */
177
115
  export function renderRow(entry, ctx) {
178
116
  const { number, cells, properties } = entry;
179
- // A cell earns a <c> element if it holds a value OR carries its own style: a formatted-but-empty
180
- // cell (a fill/border on a null value) is a real cell to Excel, and dropping it would lose the
181
- // formatting. A cell with neither is inherited from its row/column and needs no element of its own.
182
117
  const rendered = cells.filter((cell) => cell.value !== null || hasOwnStyle(cell));
183
118
  const attrs = rowAttrs(properties, ctx.styles, ctx.collapsedSummaries.has(number));
184
- // A row with neither data nor its own formatting has nothing to serialise.
185
119
  if (rendered.length === 0 && attrs === '')
186
120
  return { xml: '', minCol: Infinity, maxCol: -Infinity };
187
121
  const rowFill = properties?.fill;
@@ -201,28 +135,18 @@ export function renderRow(entry, ctx) {
201
135
  }
202
136
  return { xml: `<row r="${number}"${attrs}>${cellsXml}</row>`, minCol, maxCol };
203
137
  }
204
- // Compose a cell's full style by resolving each facet cell-over-row-over-column, so a cell that
205
- // overrides one facet still carries the row's fill and the column's other facets rather than silently
206
- // dropping them — the per-facet precedence Excel applies. The row contributes only a fill today;
207
- // quote-prefix and the named-style link are cell-only, with no row/column default to inherit.
208
138
  function composeCellStyle(cell, rowFill, colDef) {
209
139
  return {
210
140
  fill: cell.fill ?? rowFill ?? colDef?.fill,
211
- // A bare Date carries no format of its own, so it renders as a raw serial and reads back as a
212
- // number unless we apply a date format. An explicit cell/column format wins.
213
141
  numFmt: cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value),
214
142
  font: cell.font ?? colDef?.font,
215
143
  border: cell.border ?? colDef?.border,
216
144
  alignment: cell.alignment ?? colDef?.alignment,
217
145
  protection: cell.protection ?? colDef?.protection,
218
146
  quotePrefix: cell.quotePrefix,
219
- // Preserved so a round-trip keeps the cell tied to its named style rather than flattening it into
220
- // a purely-direct format.
221
147
  xfId: cell[NAMED_STYLE_ID],
222
148
  };
223
149
  }
224
- // Assemble the worksheet's single `<extLst>` from every x14 extension the sheet carries, or '' when it
225
- // carries none. Each producer returns a bare `<ext>` so they compose without nesting an `<extLst>`.
226
150
  function worksheetExtLstXml(sheet, slicerRelIds) {
227
151
  const exts = [
228
152
  conditionalFormattingsExtXml(sheet.conditionalFormattings),
@@ -231,17 +155,12 @@ function worksheetExtLstXml(sheet, slicerRelIds) {
231
155
  ].filter((ext) => ext !== '');
232
156
  return exts.length === 0 ? '' : `<extLst>${exts.join('')}</extLst>`;
233
157
  }
234
- // The worksheet-body `<x14:slicerList>` extension that reconnects a sheet to its preserved slicer
235
- // parts. Each `<x14:slicer>` names the sheet-local relationship id its slicer rel was re-emitted under,
236
- // so the wiring stays consistent even though the id is reassigned on write. '' when the sheet has none.
237
158
  function slicerListExtXml(slicerRelIds) {
238
159
  if (slicerRelIds.length === 0)
239
160
  return '';
240
161
  const slicers = slicerRelIds.map((relId) => `<x14:slicer r:id="${relId}"/>`).join('');
241
162
  return x14Ext(SLICER_LIST_EXT_URI, `<x14:slicerList>${slicers}</x14:slicerList>`);
242
163
  }
243
- // Excel forbids a merged range from intersecting a formatted table; such a file opens as
244
- // corrupt. The writer is the OOXML gatekeeper for this cross-feature geometry conflict.
245
164
  function validateMerges(sheet) {
246
165
  if (sheet.merges.length === 0 || sheet.tables.length === 0)
247
166
  return;
@@ -269,9 +188,6 @@ function mergeCellsXml(merges) {
269
188
  .join('');
270
189
  return `<mergeCells count="${merges.length}">${cells}</mergeCells>`;
271
190
  }
272
- // A tail reference element (`<drawing r:id="…"/>` and its `<legacyDrawing>`/`<legacyDrawingHF>`/
273
- // `<picture>` siblings) wiring the sheet to a part by relationship id, or '' when the sheet carries no
274
- // part of that kind — each such id is null then.
275
191
  function refElement(tag, relId) {
276
192
  return relId === null ? '' : `<${tag} r:id="${relId}"/>`;
277
193
  }
@@ -284,8 +200,6 @@ function tablePartsXml(tables) {
284
200
  export function worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedReferences, pivots) {
285
201
  const rels = [
286
202
  ...tables.map(({ relId, number }) => relationship(relId, REL.table, `../tables/table${number}.xml`)),
287
- // A pivot table hosted on this sheet is reached by a relationship of type pivotTable; Excel
288
- // discovers the pivot from the rels part, so the sheet body itself carries no reference to it.
289
203
  ...pivots.map((pivot) => relationship(pivot.sheetRelId, REL.pivotTable, `../pivotTables/pivotTable${pivot.number}.xml`)),
290
204
  ...(drawing === null
291
205
  ? []
@@ -296,8 +210,6 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
296
210
  relationship(comments.vmlRelId, REL.vmlDrawing, `../drawings/vmlDrawing${comments.number}.vml`),
297
211
  relationship(comments.commentsRelId, REL.comments, `../comments${comments.number}.xml`),
298
212
  ]),
299
- // A threaded-comment part, like a pivot table, is reached by relationship alone — no worksheet element
300
- // names it, so this relationship is the only thing that makes Excel look for the conversation.
301
213
  ...(threadedComments === null
302
214
  ? []
303
215
  : [
@@ -313,11 +225,7 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
313
225
  : [
314
226
  relationship(background.relId, REL.image, `../media/image${background.mediaNumber}.${background.extension}`),
315
227
  ]),
316
- // A preserved reference targets its entry part's new (package-absolute) path; a worksheet always
317
- // lives under `xl/worksheets/`, so the target is that path made relative to that directory.
318
228
  ...preservedReferences.map((reference) => relationship(reference.relId, reference.relType, escapeAttr(relativePartPath('xl/worksheets/sheet1.xml', reference.entryPath)))),
319
- // An external hyperlink's target is a URL outside the package, so its relationship carries
320
- // TargetMode="External". Internal links have no relId and contribute nothing here.
321
229
  ...hyperlinks
322
230
  .filter((link) => link.relId !== undefined && link.target !== undefined)
323
231
  .map((link) => relationship(link.relId, REL.hyperlink, escapeAttr(link.target), {
@@ -326,21 +234,13 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
326
234
  ];
327
235
  return relationshipsPart(rels);
328
236
  }
329
- // Excel's standard row height in points, emitted as the `defaultRowHeight` when the sheet does not
330
- // override it so a reader sees the same baseline Excel would write.
331
237
  const DEFAULT_ROW_HEIGHT = 15;
332
- // `<sheetFormatPr>` carries the sheet's grid defaults and, when the sheet groups anything, the depth
333
- // of its deepest outline. A consumer sizes the outline bars from those depths — the strips that sit
334
- // above the column headers and left of the row headers — so a grouped sheet that omits them lays its
335
- // grid out with no room reserved for a bar it then has to draw. Both are omitted at zero, as Excel
336
- // does, so an ungrouped sheet stays byte-clean.
337
238
  function sheetFormatPr(properties, outlineLevel) {
338
239
  const rowHeight = properties.defaultRowHeight ?? DEFAULT_ROW_HEIGHT;
339
240
  let attrs = ` defaultRowHeight="${numberText(rowHeight)}"`;
340
241
  if (properties.defaultColWidth !== undefined) {
341
242
  attrs += ` defaultColWidth="${numberText(properties.defaultColWidth)}"`;
342
243
  }
343
- // A non-standard default row height is only honoured by Excel when customHeight is set.
344
244
  if (properties.defaultRowHeight !== undefined)
345
245
  attrs += ' customHeight="1"';
346
246
  if (outlineLevel.col > 0)
@@ -349,9 +249,6 @@ function sheetFormatPr(properties, outlineLevel) {
349
249
  attrs += ` outlineLevelRow="${outlineLevel.row}"`;
350
250
  return `<sheetFormatPr${attrs}/>`;
351
251
  }
352
- // The deepest column outline level the sheet declares — the `outlineLevelCol` its `<sheetFormatPr>`
353
- // reports. A column past XFD contributes nothing: {@link colsXml} drops it as out-of-range, so its
354
- // group would have no `<col>` to sit on.
355
252
  function maxColumnOutlineLevel(sheet) {
356
253
  let max = 0;
357
254
  for (const column of sheet.columns()) {
@@ -362,19 +259,11 @@ function maxColumnOutlineLevel(sheet) {
362
259
  return max;
363
260
  }
364
261
  function colsXml(sheet, styles) {
365
- // Runs of adjacent columns that carry identical definitions are coalesced into a single
366
- // `<col min max>` span — Excel writes columns this way, and it keeps the part compact for a
367
- // sheet whose columns share a width or outline level. A gap in the indices or any difference
368
- // in the emitted attributes breaks the run.
369
262
  const runs = [];
370
263
  for (const { index, properties } of sheet.columns()) {
371
- // OOXML has no column past XFD (16384); a definition beyond it is corrupt to Excel,
372
- // so drop it rather than emit an out-of-range <col> range.
373
264
  if (index > MAX_COLUMN)
374
265
  continue;
375
266
  const body = colBody(properties ?? {}, styles);
376
- // A <col> with no width, visibility, or style says nothing; omit it entirely. That also covers
377
- // the column with no format record at all, which `columns()` does not in fact yield.
378
267
  if (body === null)
379
268
  continue;
380
269
  const last = runs[runs.length - 1];
@@ -390,9 +279,6 @@ function colsXml(sheet, styles) {
390
279
  const cols = runs.map((run) => `<col min="${run.min}" max="${run.max}"${run.body}/>`).join('');
391
280
  return `<cols>${cols}</cols>`;
392
281
  }
393
- // The attributes of a `<col>` sans its `min`/`max` span (each with a leading space), or `null` when
394
- // the column declares nothing worth emitting. Two columns with the same body are interchangeable, so
395
- // the body doubles as the equivalence key that {@link colsXml} coalesces adjacent runs by.
396
282
  function colBody(properties, styles) {
397
283
  let attrs = '';
398
284
  let meaningful = false;
@@ -412,9 +298,6 @@ function colBody(properties, styles) {
412
298
  attrs += ' collapsed="1"';
413
299
  meaningful = true;
414
300
  }
415
- // The column's style facets are carried as its own `<col>` style; its populated cells inherit
416
- // them via the composition above, and this `style` makes Excel apply them to the column's empty
417
- // cells too.
418
301
  const style = styles.styleId({
419
302
  fill: properties.fill,
420
303
  numFmt: properties.numFmt,
@@ -440,25 +323,13 @@ function rowAttrs(properties, styles, collapsedSummary) {
440
323
  if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
441
324
  attrs += ` outlineLevel="${properties.outlineLevel}"`;
442
325
  }
443
- // The collapse toggle is set explicitly by the author, or derived onto a summary row whose whole
444
- // detail group is hidden (see {@link collapsedSummaryRows}). It rides the summary row, never the
445
- // detail rows.
446
326
  if (properties.collapsed || collapsedSummary)
447
327
  attrs += ' collapsed="1"';
448
- // A row-level fill is a default format for the row's cells; customFormat="1" is what makes
449
- // Excel honour the row's `s`, and a cell without its own `s` then inherits it.
450
328
  const style = styles.styleId({ fill: properties.fill });
451
329
  if (style !== 0)
452
330
  attrs += ` s="${style}" customFormat="1"`;
453
331
  return attrs;
454
332
  }
455
- // A collapsed outline group is two coordinated facts: its detail rows carry outlineLevel and are
456
- // hidden, AND the summary row that terminates the group carries `collapsed`. Authors typically set
457
- // only outlineLevel + hidden on the detail rows, so the summary flag is derived here rather than
458
- // demanded of the caller: a row is a collapsed summary iff its adjacent detail run — the contiguous
459
- // higher-outline-level rows on the summary side — is non-empty and every row in it is hidden.
460
- // Placement follows the sheet's summaryBelow flag (Excel's default is summary below the detail); the
461
- // walk stops at the first row of level <= the summary's own, so a gap or a boundary ends the group.
462
333
  function scanRowOutline(sheet) {
463
334
  const level = new Map();
464
335
  const hidden = new Map();
@@ -488,9 +359,6 @@ function scanRowOutline(sheet) {
488
359
  }
489
360
  return { collapsedSummaries, maxLevel };
490
361
  }
491
- // A valid Date — whether the cell's own value or a formula's cached result — with no format of its
492
- // own gets the default date format so it renders and reads back as a date rather than a bare serial.
493
- // An Invalid Date and every non-date value contribute nothing here.
494
362
  function dateDefaultNumFmt(value) {
495
363
  const date = value instanceof Date
496
364
  ? value
@@ -507,16 +375,11 @@ function cellXml(cell, style, shared, sharedStrings) {
507
375
  if (formula !== undefined)
508
376
  return formula;
509
377
  if (value instanceof Date) {
510
- // An Invalid Date (new Date(NaN)) has no serial; keep the cell (and its style) but emit no
511
- // value rather than throwing, so one bad date never takes down the whole sheet's export.
512
378
  if (Number.isNaN(value.getTime()))
513
379
  return `<c r="${ref}"${s}/>`;
514
380
  return `<c r="${ref}"${s}><v>${numberText(dateToSerial(value))}</v></c>`;
515
381
  }
516
382
  if (typeof value === 'number') {
517
- // A non-finite number (NaN, ±Infinity) has no OOXML representation; keep the cell and its style
518
- // but emit no value rather than a bare "NaN"/"Infinity" token — the same graceful degradation an
519
- // Invalid Date gets, so one bad value never corrupts the sheet or takes down the whole export.
520
383
  if (!Number.isFinite(value))
521
384
  return `<c r="${ref}"${s}/>`;
522
385
  return `<c r="${ref}"${s}><v>${numberText(value)}</v></c>`;
@@ -525,46 +388,30 @@ function cellXml(cell, style, shared, sharedStrings) {
525
388
  return `<c r="${ref}"${s} t="b"><v>${value ? 1 : 0}</v></c>`;
526
389
  }
527
390
  if (typeof value === 'string') {
528
- // With shared strings on, the cell holds only the pool index (`t="s"`); otherwise the text
529
- // lives inline in the cell. Both decode to the same string on read.
530
391
  if (sharedStrings !== null) {
531
392
  return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
532
393
  }
533
394
  return `<c r="${ref}"${s} t="inlineStr"><is>${textElement(value)}</is></c>`;
534
395
  }
535
396
  if (isRichTextValue(value)) {
536
- // With shared strings on, rich text is pooled as a rich `<si>` (the cell holds only its index);
537
- // otherwise the runs live inline. Both decode back to the same runs on read.
538
397
  if (sharedStrings !== null) {
539
398
  return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
540
399
  }
541
400
  return `<c r="${ref}"${s} t="inlineStr"><is>${richTextRunsXml(value.richText)}</is></c>`;
542
401
  }
543
402
  if (isHyperlinkValue(value)) {
544
- // The cell holds only the visible label; the link itself rides in the sheet's <hyperlinks>.
545
- // The label is either a plain string or rich text, serialised the same way a cell value of
546
- // that kind would be.
547
403
  const label = typeof value.text === 'string'
548
404
  ? textElement(value.text)
549
405
  : richTextRunsXml(value.text.richText);
550
406
  return `<c r="${ref}"${s} t="inlineStr"><is>${label}</is></c>`;
551
407
  }
552
408
  if (isErrorValue(value)) {
553
- // An error literal serialises under t="e" with its code as the value. The codes are a closed
554
- // set of canonical spellings (see ERROR_CODES) with no XML-special characters, so no escaping.
555
409
  return `<c r="${ref}"${s} t="e"><v>${value.error}</v></c>`;
556
410
  }
557
- // A null value only reaches here for a formatted-but-empty cell (the row loop keeps it for its
558
- // style); emit the styled cell with no <v>, exactly how Excel stores a formatted blank.
559
411
  if (value === null)
560
412
  return `<c r="${ref}"${s}/>`;
561
- // Every ValueType kind is served by an arm above (a formula routes through its own writer), so
562
- // this is unreachable — it exists because the union is not exhaustively narrowed here.
563
413
  throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm — every CellValue kind is handled above`);
564
414
  }
565
- // Whether a cell carries any style facet of its own — the reason to serialise it even when empty.
566
- // A note is not a style: it lives in the comments part, not the cell's <c> element, so it does not
567
- // count here. Row/column-inherited formatting is likewise excluded; only the cell's own facets do.
568
415
  function hasOwnStyle(cell) {
569
416
  return (cell.fill !== undefined ||
570
417
  cell.numFmt !== undefined ||
@@ -575,13 +422,7 @@ function hasOwnStyle(cell) {
575
422
  cell.quotePrefix === true ||
576
423
  cell[NAMED_STYLE_ID] !== undefined);
577
424
  }
578
- // Serialise a formula cell — a shared-formula master or clone, a What-If data table, or a plain
579
- // formula — into its `<c>` element, or return undefined when the value is not a formula so `cellXml`
580
- // falls through to its value dispatch.
581
425
  function cellFormulaXml(ref, s, value, shared) {
582
- // A shared-formula master seeds the group with its formula text under `t="shared" ref si`; a clone
583
- // carries no text of its own, only a back-reference to the master's `si`. Its cached result still
584
- // travels with the cell.
585
426
  if (shared !== undefined) {
586
427
  if (shared.ref !== undefined && isFormulaValue(value)) {
587
428
  const f = `<f t="shared" ref="${shared.ref}" si="${shared.si}">${escapeText(mangleFormula(value.formula))}</f>`;
@@ -591,9 +432,6 @@ function cellFormulaXml(ref, s, value, shared) {
591
432
  return formulaBodyXml(ref, s, `<f t="shared" si="${shared.si}"/>`, result);
592
433
  }
593
434
  if (isDataTableFormulaValue(value)) {
594
- // A data-table formula carries no expression text — only its declaration attributes — which we
595
- // re-emit verbatim so a read-modify-write cycle preserves the What-If kind the library never
596
- // evaluates. The cached result travels as any formula result does.
597
435
  const attrs = `ref="${escapeAttr(value.ref)}"` +
598
436
  ` dt2D="${value.dataTable2D ? 1 : 0}"` +
599
437
  ` dtr="${value.dataTableRow ? 1 : 0}"` +
@@ -606,13 +444,7 @@ function cellFormulaXml(ref, s, value, shared) {
606
444
  }
607
445
  return undefined;
608
446
  }
609
- // Wrap a prepared `<f>` element (a plain formula, or a shared master/slave `<f>`) with the cell
610
- // element and its cached result, typing the cell by the result's kind exactly as a bare value of that
611
- // kind would be.
612
447
  function formulaBodyXml(ref, s, f, result) {
613
- // A non-finite cached result (a `1/0` that reached the model as Infinity/NaN) has no OOXML
614
- // representation; keep the formula but cache no value rather than emit a bare "NaN" — the same
615
- // graceful degradation a bare non-finite cell and an Invalid Date result get.
616
448
  if (result === undefined || (typeof result === 'number' && !Number.isFinite(result))) {
617
449
  return `<c r="${ref}"${s}>${f}</c>`;
618
450
  }
@@ -626,18 +458,12 @@ function formulaBodyXml(ref, s, f, result) {
626
458
  return `<c r="${ref}"${s} t="str">${f}<v>${escapeText(result)}</v></c>`;
627
459
  }
628
460
  if (isErrorValue(result)) {
629
- // A formula that evaluated to an error caches its code under t="e", exactly as a bare error
630
- // cell does — the reader's decodeResult mirrors decodeValue for this case.
631
461
  return `<c r="${ref}"${s} t="e">${f}<v>${result.error}</v></c>`;
632
462
  }
633
463
  if (result instanceof Date) {
634
- // A date-valued result caches its serial exactly as a bare date cell stores its value; the
635
- // cell's date number format (applied when its style is composed) is what makes both read back as
636
- // a Date. An Invalid Date has no serial, so cache no result rather than emit NaN.
637
464
  if (Number.isNaN(result.getTime()))
638
465
  return `<c r="${ref}"${s}>${f}</c>`;
639
466
  return `<c r="${ref}"${s}>${f}<v>${numberText(dateToSerial(result))}</v></c>`;
640
467
  }
641
- // Every FormulaResult kind is handled above; this guards a value that reached here past the model.
642
468
  throw new InternalError('writing a non-primitive formula result has no arm — every FormulaResult kind is handled above');
643
469
  }