@shbernal/ts-xlsx 1.0.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 (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. package/package.json +117 -0
@@ -0,0 +1,643 @@
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
+ import { decodeRange, encodeAddress, MAX_COLUMN } from "../../core/address.js";
8
+ import { DEFAULT_DATE_NUMFMT, dateToSerial } from "../../core/date.js";
9
+ import { mangleFormula } from "../../core/formula.js";
10
+ import { NAMED_STYLE_ID } from "../../core/internal.js";
11
+ import { detectValueType, isDataTableFormulaValue, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, } from "../../core/value.js";
12
+ import { AuthoringError, InternalError } from "../../errors.js";
13
+ import { escapeAttr, escapeText, numberText, textElement, XML_DECLARATION } from "../../xml/xml.js";
14
+ import { relativePartPath } from "../opc/part-paths.js";
15
+ import { relationship, relationshipsPart } from "../opc/rels.js";
16
+ import { conditionalFormattingsExtXml, conditionalFormattingsXml } from "./conditional-formatting.js";
17
+ import { dataValidationsExtXml, dataValidationsXml } from "./data-validation.js";
18
+ import { hyperlinksXml } from "./hyperlinks.js";
19
+ import { SLICER_LIST_EXT_URI } from "./namespaces.js";
20
+ import { NS, REL } from "./relationships.js";
21
+ import { richTextRunsXml } from "./rich-text.js";
22
+ import { planSharedFormulas } from "./shared-formulas.js";
23
+ import { autoFilterXml, headerFooterXml, pageBreaksXml, pageMarginsXml, pageSetupXml, printOptionsXml, sheetProtectionXml, sheetPrXml, sheetViewsXml, } from "./sheet-properties.js";
24
+ 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
+ export class Extent {
32
+ top = Infinity;
33
+ left = Infinity;
34
+ bottom = -Infinity;
35
+ 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
+ constructor(seed) {
39
+ if (seed) {
40
+ this.top = seed.top;
41
+ this.left = seed.left;
42
+ this.bottom = seed.bottom;
43
+ this.right = seed.right;
44
+ }
45
+ }
46
+ /** Whether no used cell has been folded in yet — the sheet's dimension is then the lone cell `A1`. */
47
+ get isEmpty() {
48
+ return this.bottom === -Infinity;
49
+ }
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
+ add(row, minCol, maxCol) {
53
+ if (minCol === Infinity)
54
+ return;
55
+ if (row < this.top)
56
+ this.top = row;
57
+ if (row > this.bottom)
58
+ this.bottom = row;
59
+ if (minCol < this.left)
60
+ this.left = minCol;
61
+ if (maxCol > this.right)
62
+ this.right = maxCol;
63
+ }
64
+ }
65
+ 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
+ validateMerges(sheet);
69
+ 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
+ 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
+ const rowOutline = scanRowOutline(sheet);
79
+ const collapsedSummaries = rowOutline.collapsedSummaries;
80
+ const context = {
81
+ columnDefaults,
82
+ styles,
83
+ sharedStrings,
84
+ sharedRoles,
85
+ collapsedSummaries,
86
+ };
87
+ 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
+ const extent = new Extent(flushed?.extent);
91
+ for (const entry of sheet.rows()) {
92
+ const { xml, minCol, maxCol } = renderRow(entry, context);
93
+ if (xml === '')
94
+ continue;
95
+ liveRows.push({ number: entry.number, xml });
96
+ extent.add(entry.number, minCol, maxCol);
97
+ }
98
+ const dimensionRef = extent.isEmpty
99
+ ? 'A1'
100
+ : `${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
+ const orderedRows = flushed
105
+ ? [...flushed.rows, ...liveRows].sort((a, b) => a.number - b.number)
106
+ : liveRows;
107
+ const bodyXml = orderedRows.map((row) => row.xml).join('');
108
+ const sheetData = bodyXml === '' ? '<sheetData/>' : `<sheetData>${bodyXml}</sheetData>`;
109
+ return (XML_DECLARATION +
110
+ `<worksheet xmlns="${NS.main}" xmlns:r="${NS.docRels}">` +
111
+ sheetPrXml(sheet) +
112
+ `<dimension ref="${dimensionRef}"/>` +
113
+ sheetViewsXml(sheet.view, active) +
114
+ sheetFormatPr(sheet.properties, {
115
+ 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
+ row: Math.max(rowOutline.maxLevel, flushed?.maxRowOutlineLevel ?? 0),
119
+ }) +
120
+ colsXml(sheet, styles) +
121
+ sheetData +
122
+ 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
+ autoFilterXml(sheet.autoFilter) +
126
+ mergeCellsXml(sheet.merges) +
127
+ // CT_Worksheet order: <conditionalFormatting> blocks follow <mergeCells>, then <dataValidations>,
128
+ // then <hyperlinks> — all precede the print settings.
129
+ conditionalFormattingsXml(sheet.conditionalFormattings, styles) +
130
+ dataValidationsXml(sheet.dataValidations) +
131
+ hyperlinksXml(hyperlinks) +
132
+ // CT_Worksheet order: <printOptions> precedes <pageMargins>, which precedes <pageSetup>.
133
+ printOptionsXml(sheet.printOptions) +
134
+ pageMarginsXml(sheet.pageMargins) +
135
+ pageSetupXml(sheet.pageSetup, references.printerSettingsRelId) +
136
+ headerFooterXml(sheet.headerFooter) +
137
+ // CT_Worksheet order: <rowBreaks> follows <headerFooter>, <colBreaks> follows <rowBreaks>, and
138
+ // both precede the drawing block.
139
+ pageBreaksXml(sheet.rowBreaks, 'rowBreaks') +
140
+ 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
+ refElement('drawing', references.drawingRelId) +
145
+ refElement('legacyDrawing', references.legacyDrawingRelId) +
146
+ refElement('legacyDrawingHF', references.legacyDrawingHFRelId) +
147
+ refElement('picture', references.backgroundRelId) +
148
+ 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
+ worksheetExtLstXml(sheet, references.slicerRelIds) +
154
+ '</worksheet>');
155
+ }
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
+ export function buildColumnDefaults(sheet) {
163
+ 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
+ for (const { index, properties } of sheet.columns())
168
+ columnDefaults.set(index, properties ?? {});
169
+ return columnDefaults;
170
+ }
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
+ export function renderRow(entry, ctx) {
178
+ 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
+ const rendered = cells.filter((cell) => cell.value !== null || hasOwnStyle(cell));
183
+ 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
+ if (rendered.length === 0 && attrs === '')
186
+ return { xml: '', minCol: Infinity, maxCol: -Infinity };
187
+ const rowFill = properties?.fill;
188
+ const cellsXml = rendered
189
+ .map((cell) => {
190
+ const style = ctx.styles.styleId(composeCellStyle(cell, rowFill, ctx.columnDefaults.get(cell.col)));
191
+ return cellXml(cell, style, ctx.sharedRoles.get(cell.address), ctx.sharedStrings);
192
+ })
193
+ .join('');
194
+ let minCol = Infinity;
195
+ let maxCol = -Infinity;
196
+ for (const cell of rendered) {
197
+ if (cell.col < minCol)
198
+ minCol = cell.col;
199
+ if (cell.col > maxCol)
200
+ maxCol = cell.col;
201
+ }
202
+ return { xml: `<row r="${number}"${attrs}>${cellsXml}</row>`, minCol, maxCol };
203
+ }
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
+ function composeCellStyle(cell, rowFill, colDef) {
209
+ return {
210
+ 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
+ numFmt: cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value),
214
+ font: cell.font ?? colDef?.font,
215
+ border: cell.border ?? colDef?.border,
216
+ alignment: cell.alignment ?? colDef?.alignment,
217
+ protection: cell.protection ?? colDef?.protection,
218
+ 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
+ xfId: cell[NAMED_STYLE_ID],
222
+ };
223
+ }
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
+ function worksheetExtLstXml(sheet, slicerRelIds) {
227
+ const exts = [
228
+ conditionalFormattingsExtXml(sheet.conditionalFormattings),
229
+ dataValidationsExtXml(sheet.dataValidations),
230
+ slicerListExtXml(slicerRelIds),
231
+ ].filter((ext) => ext !== '');
232
+ return exts.length === 0 ? '' : `<extLst>${exts.join('')}</extLst>`;
233
+ }
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
+ function slicerListExtXml(slicerRelIds) {
238
+ if (slicerRelIds.length === 0)
239
+ return '';
240
+ const slicers = slicerRelIds.map((relId) => `<x14:slicer r:id="${relId}"/>`).join('');
241
+ return x14Ext(SLICER_LIST_EXT_URI, `<x14:slicerList>${slicers}</x14:slicerList>`);
242
+ }
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
+ function validateMerges(sheet) {
246
+ if (sheet.merges.length === 0 || sheet.tables.length === 0)
247
+ return;
248
+ for (const merge of sheet.merges) {
249
+ const { left, right, top, bottom } = decodeRange(merge);
250
+ if (left === undefined || right === undefined || top === undefined || bottom === undefined)
251
+ continue;
252
+ for (const table of sheet.tables) {
253
+ const region = table.region;
254
+ const overlaps = left <= region.right &&
255
+ right >= region.left &&
256
+ top <= region.bottom &&
257
+ bottom >= region.top;
258
+ if (overlaps) {
259
+ throw new AuthoringError(`merged range ${merge} overlaps table "${table.name}" (${table.range}) — Excel forbids a merge inside a table`);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ function mergeCellsXml(merges) {
265
+ if (merges.length === 0)
266
+ return '';
267
+ const cells = merges
268
+ .map((range) => `<mergeCell ref="${escapeAttr(decodeRange(range).dimensions)}"/>`)
269
+ .join('');
270
+ return `<mergeCells count="${merges.length}">${cells}</mergeCells>`;
271
+ }
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
+ function refElement(tag, relId) {
276
+ return relId === null ? '' : `<${tag} r:id="${relId}"/>`;
277
+ }
278
+ function tablePartsXml(tables) {
279
+ if (tables.length === 0)
280
+ return '';
281
+ const parts = tables.map(({ relId }) => `<tablePart r:id="${relId}"/>`).join('');
282
+ return `<tableParts count="${tables.length}">${parts}</tableParts>`;
283
+ }
284
+ export function worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedReferences, pivots) {
285
+ const rels = [
286
+ ...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
+ ...pivots.map((pivot) => relationship(pivot.sheetRelId, REL.pivotTable, `../pivotTables/pivotTable${pivot.number}.xml`)),
290
+ ...(drawing === null
291
+ ? []
292
+ : [relationship(drawing.relId, REL.drawing, `../drawings/drawing${drawing.number}.xml`)]),
293
+ ...(comments === null
294
+ ? []
295
+ : [
296
+ relationship(comments.vmlRelId, REL.vmlDrawing, `../drawings/vmlDrawing${comments.number}.vml`),
297
+ relationship(comments.commentsRelId, REL.comments, `../comments${comments.number}.xml`),
298
+ ]),
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
+ ...(threadedComments === null
302
+ ? []
303
+ : [
304
+ relationship(threadedComments.relId, REL.threadedComment, `../threadedComments/threadedComment${threadedComments.number}.xml`),
305
+ ]),
306
+ ...(printerSettings === null
307
+ ? []
308
+ : [
309
+ relationship(printerSettings.relId, REL.printerSettings, `../printerSettings/printerSettings${printerSettings.number}.bin`),
310
+ ]),
311
+ ...(background === null
312
+ ? []
313
+ : [
314
+ relationship(background.relId, REL.image, `../media/image${background.mediaNumber}.${background.extension}`),
315
+ ]),
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
+ ...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
+ ...hyperlinks
322
+ .filter((link) => link.relId !== undefined && link.target !== undefined)
323
+ .map((link) => relationship(link.relId, REL.hyperlink, escapeAttr(link.target), {
324
+ external: true,
325
+ })),
326
+ ];
327
+ return relationshipsPart(rels);
328
+ }
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
+ 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
+ function sheetFormatPr(properties, outlineLevel) {
338
+ const rowHeight = properties.defaultRowHeight ?? DEFAULT_ROW_HEIGHT;
339
+ let attrs = ` defaultRowHeight="${numberText(rowHeight)}"`;
340
+ if (properties.defaultColWidth !== undefined) {
341
+ attrs += ` defaultColWidth="${numberText(properties.defaultColWidth)}"`;
342
+ }
343
+ // A non-standard default row height is only honoured by Excel when customHeight is set.
344
+ if (properties.defaultRowHeight !== undefined)
345
+ attrs += ' customHeight="1"';
346
+ if (outlineLevel.col > 0)
347
+ attrs += ` outlineLevelCol="${outlineLevel.col}"`;
348
+ if (outlineLevel.row > 0)
349
+ attrs += ` outlineLevelRow="${outlineLevel.row}"`;
350
+ return `<sheetFormatPr${attrs}/>`;
351
+ }
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
+ function maxColumnOutlineLevel(sheet) {
356
+ let max = 0;
357
+ for (const column of sheet.columns()) {
358
+ if (column.index > MAX_COLUMN)
359
+ continue;
360
+ max = Math.max(max, column.outlineLevel ?? 0);
361
+ }
362
+ return max;
363
+ }
364
+ 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
+ const runs = [];
370
+ 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
+ if (index > MAX_COLUMN)
374
+ continue;
375
+ 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
+ if (body === null)
379
+ continue;
380
+ const last = runs[runs.length - 1];
381
+ if (last !== undefined && last.max === index - 1 && last.body === body) {
382
+ last.max = index;
383
+ }
384
+ else {
385
+ runs.push({ min: index, max: index, body });
386
+ }
387
+ }
388
+ if (runs.length === 0)
389
+ return '';
390
+ const cols = runs.map((run) => `<col min="${run.min}" max="${run.max}"${run.body}/>`).join('');
391
+ return `<cols>${cols}</cols>`;
392
+ }
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
+ function colBody(properties, styles) {
397
+ let attrs = '';
398
+ let meaningful = false;
399
+ if (properties.width !== undefined) {
400
+ attrs += ` width="${numberText(properties.width)}" customWidth="1"`;
401
+ meaningful = true;
402
+ }
403
+ if (properties.hidden) {
404
+ attrs += ' hidden="1"';
405
+ meaningful = true;
406
+ }
407
+ if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
408
+ attrs += ` outlineLevel="${properties.outlineLevel}"`;
409
+ meaningful = true;
410
+ }
411
+ if (properties.collapsed) {
412
+ attrs += ' collapsed="1"';
413
+ meaningful = true;
414
+ }
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
+ const style = styles.styleId({
419
+ fill: properties.fill,
420
+ numFmt: properties.numFmt,
421
+ font: properties.font,
422
+ border: properties.border,
423
+ alignment: properties.alignment,
424
+ protection: properties.protection,
425
+ });
426
+ if (style !== 0) {
427
+ attrs += ` style="${style}"`;
428
+ meaningful = true;
429
+ }
430
+ return meaningful ? attrs : null;
431
+ }
432
+ function rowAttrs(properties, styles, collapsedSummary) {
433
+ if (properties === undefined)
434
+ return collapsedSummary ? ' collapsed="1"' : '';
435
+ let attrs = '';
436
+ if (properties.height !== undefined)
437
+ attrs += ` ht="${numberText(properties.height)}" customHeight="1"`;
438
+ if (properties.hidden)
439
+ attrs += ' hidden="1"';
440
+ if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
441
+ attrs += ` outlineLevel="${properties.outlineLevel}"`;
442
+ }
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
+ if (properties.collapsed || collapsedSummary)
447
+ 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
+ const style = styles.styleId({ fill: properties.fill });
451
+ if (style !== 0)
452
+ attrs += ` s="${style}" customFormat="1"`;
453
+ return attrs;
454
+ }
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
+ function scanRowOutline(sheet) {
463
+ const level = new Map();
464
+ const hidden = new Map();
465
+ let maxLevel = 0;
466
+ for (const { number, properties } of sheet.rows()) {
467
+ const rowLevel = properties?.outlineLevel ?? 0;
468
+ level.set(number, rowLevel);
469
+ hidden.set(number, properties?.hidden ?? false);
470
+ if (rowLevel > maxLevel)
471
+ maxLevel = rowLevel;
472
+ }
473
+ const levelOf = (row) => level.get(row) ?? 0;
474
+ const step = sheet.outline.summaryBelow === false ? 1 : -1;
475
+ const collapsedSummaries = new Set();
476
+ for (const [summary, summaryLevel] of level) {
477
+ let detail = summary + step;
478
+ let sawDetail = false;
479
+ let allHidden = true;
480
+ while (levelOf(detail) > summaryLevel) {
481
+ sawDetail = true;
482
+ if (!hidden.get(detail))
483
+ allHidden = false;
484
+ detail += step;
485
+ }
486
+ if (sawDetail && allHidden)
487
+ collapsedSummaries.add(summary);
488
+ }
489
+ return { collapsedSummaries, maxLevel };
490
+ }
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
+ function dateDefaultNumFmt(value) {
495
+ const date = value instanceof Date
496
+ ? value
497
+ : (isFormulaValue(value) || isSharedFormulaValue(value)) && value.result instanceof Date
498
+ ? value.result
499
+ : undefined;
500
+ return date !== undefined && !Number.isNaN(date.getTime()) ? DEFAULT_DATE_NUMFMT : undefined;
501
+ }
502
+ function cellXml(cell, style, shared, sharedStrings) {
503
+ const ref = cell.address;
504
+ const value = cell.value;
505
+ const s = style !== 0 ? ` s="${style}"` : '';
506
+ const formula = cellFormulaXml(ref, s, value, shared);
507
+ if (formula !== undefined)
508
+ return formula;
509
+ 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
+ if (Number.isNaN(value.getTime()))
513
+ return `<c r="${ref}"${s}/>`;
514
+ return `<c r="${ref}"${s}><v>${numberText(dateToSerial(value))}</v></c>`;
515
+ }
516
+ 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
+ if (!Number.isFinite(value))
521
+ return `<c r="${ref}"${s}/>`;
522
+ return `<c r="${ref}"${s}><v>${numberText(value)}</v></c>`;
523
+ }
524
+ if (typeof value === 'boolean') {
525
+ return `<c r="${ref}"${s} t="b"><v>${value ? 1 : 0}</v></c>`;
526
+ }
527
+ 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
+ if (sharedStrings !== null) {
531
+ return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
532
+ }
533
+ return `<c r="${ref}"${s} t="inlineStr"><is>${textElement(value)}</is></c>`;
534
+ }
535
+ 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
+ if (sharedStrings !== null) {
539
+ return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
540
+ }
541
+ return `<c r="${ref}"${s} t="inlineStr"><is>${richTextRunsXml(value.richText)}</is></c>`;
542
+ }
543
+ 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
+ const label = typeof value.text === 'string'
548
+ ? textElement(value.text)
549
+ : richTextRunsXml(value.text.richText);
550
+ return `<c r="${ref}"${s} t="inlineStr"><is>${label}</is></c>`;
551
+ }
552
+ 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
+ return `<c r="${ref}"${s} t="e"><v>${value.error}</v></c>`;
556
+ }
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
+ if (value === null)
560
+ 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
+ throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm — every CellValue kind is handled above`);
564
+ }
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
+ function hasOwnStyle(cell) {
569
+ return (cell.fill !== undefined ||
570
+ cell.numFmt !== undefined ||
571
+ cell.font !== undefined ||
572
+ cell.border !== undefined ||
573
+ cell.alignment !== undefined ||
574
+ cell.protection !== undefined ||
575
+ cell.quotePrefix === true ||
576
+ cell[NAMED_STYLE_ID] !== undefined);
577
+ }
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
+ 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
+ if (shared !== undefined) {
586
+ if (shared.ref !== undefined && isFormulaValue(value)) {
587
+ const f = `<f t="shared" ref="${shared.ref}" si="${shared.si}">${escapeText(mangleFormula(value.formula))}</f>`;
588
+ return formulaBodyXml(ref, s, f, value.result);
589
+ }
590
+ const result = isSharedFormulaValue(value) ? value.result : undefined;
591
+ return formulaBodyXml(ref, s, `<f t="shared" si="${shared.si}"/>`, result);
592
+ }
593
+ 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
+ const attrs = `ref="${escapeAttr(value.ref)}"` +
598
+ ` dt2D="${value.dataTable2D ? 1 : 0}"` +
599
+ ` dtr="${value.dataTableRow ? 1 : 0}"` +
600
+ (value.r1 !== undefined ? ` r1="${escapeAttr(value.r1)}"` : '') +
601
+ (value.r2 !== undefined ? ` r2="${escapeAttr(value.r2)}"` : '');
602
+ return formulaBodyXml(ref, s, `<f t="dataTable" ${attrs}/>`, value.result);
603
+ }
604
+ if (isFormulaValue(value)) {
605
+ return formulaBodyXml(ref, s, `<f>${escapeText(mangleFormula(value.formula))}</f>`, value.result);
606
+ }
607
+ return undefined;
608
+ }
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
+ 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
+ if (result === undefined || (typeof result === 'number' && !Number.isFinite(result))) {
617
+ return `<c r="${ref}"${s}>${f}</c>`;
618
+ }
619
+ if (typeof result === 'number') {
620
+ return `<c r="${ref}"${s}>${f}<v>${numberText(result)}</v></c>`;
621
+ }
622
+ if (typeof result === 'boolean') {
623
+ return `<c r="${ref}"${s} t="b">${f}<v>${result ? 1 : 0}</v></c>`;
624
+ }
625
+ if (typeof result === 'string') {
626
+ return `<c r="${ref}"${s} t="str">${f}<v>${escapeText(result)}</v></c>`;
627
+ }
628
+ 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
+ return `<c r="${ref}"${s} t="e">${f}<v>${result.error}</v></c>`;
632
+ }
633
+ 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
+ if (Number.isNaN(result.getTime()))
638
+ return `<c r="${ref}"${s}>${f}</c>`;
639
+ return `<c r="${ref}"${s}>${f}<v>${numberText(dateToSerial(result))}</v></c>`;
640
+ }
641
+ // Every FormulaResult kind is handled above; this guards a value that reached here past the model.
642
+ throw new InternalError('writing a non-primitive formula result has no arm — every FormulaResult kind is handled above');
643
+ }