xls-codec 1.0.2 → 2.0.1

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 (124) hide show
  1. package/README.md +128 -23
  2. package/dist/biff/bof-writer.cjs +18 -0
  3. package/dist/biff/bof-writer.d.cts +5 -0
  4. package/dist/biff/bof-writer.d.ts +5 -0
  5. package/dist/biff/bof-writer.js +17 -0
  6. package/dist/biff/builder.cjs +48 -0
  7. package/dist/biff/builder.d.cts +17 -0
  8. package/dist/biff/builder.d.ts +17 -0
  9. package/dist/biff/builder.js +47 -0
  10. package/dist/biff/cursor.cjs +4 -0
  11. package/dist/biff/cursor.d.cts +1 -27
  12. package/dist/biff/cursor.d.ts +1 -27
  13. package/dist/biff/cursor.js +4 -0
  14. package/dist/biff/errors.cjs +7 -0
  15. package/dist/biff/errors.d.cts +3 -1
  16. package/dist/biff/errors.d.ts +3 -1
  17. package/dist/biff/errors.js +7 -1
  18. package/dist/biff/print-setup.cjs +108 -0
  19. package/dist/biff/print-setup.d.cts +2 -0
  20. package/dist/biff/print-setup.d.ts +2 -0
  21. package/dist/biff/print-setup.js +103 -0
  22. package/dist/biff/ptg-functions.cjs +383 -0
  23. package/dist/biff/ptg-functions.d.cts +7 -0
  24. package/dist/biff/ptg-functions.d.ts +7 -0
  25. package/dist/biff/ptg-functions.js +381 -0
  26. package/dist/biff/ptg.cjs +319 -0
  27. package/dist/biff/ptg.d.cts +2 -0
  28. package/dist/biff/ptg.d.ts +2 -0
  29. package/dist/biff/ptg.js +318 -0
  30. package/dist/biff/record-types.cjs +60 -0
  31. package/dist/biff/record-types.d.cts +41 -1
  32. package/dist/biff/record-types.d.ts +41 -1
  33. package/dist/biff/record-types.js +41 -1
  34. package/dist/biff/record-writer.cjs +29 -0
  35. package/dist/biff/record-writer.d.cts +7 -0
  36. package/dist/biff/record-writer.d.ts +7 -0
  37. package/dist/biff/record-writer.js +27 -0
  38. package/dist/biff/string-writer.cjs +58 -0
  39. package/dist/biff/string-writer.d.cts +13 -0
  40. package/dist/biff/string-writer.d.ts +13 -0
  41. package/dist/biff/string-writer.js +55 -0
  42. package/dist/biff/strings.d.cts +1 -1
  43. package/dist/biff/strings.d.ts +1 -1
  44. package/dist/biff/substreams.d.cts +1 -20
  45. package/dist/biff/substreams.d.ts +1 -20
  46. package/dist/biff/write-errors.cjs +10 -0
  47. package/dist/biff/write-errors.d.cts +6 -0
  48. package/dist/biff/write-errors.d.ts +6 -0
  49. package/dist/biff/write-errors.js +9 -0
  50. package/dist/biff/xf-colors.cjs +374 -0
  51. package/dist/biff/xf-colors.d.cts +2 -0
  52. package/dist/biff/xf-colors.d.ts +2 -0
  53. package/dist/biff/xf-colors.js +339 -0
  54. package/dist/biff/xf-writer.cjs +96 -0
  55. package/dist/biff/xf-writer.d.cts +29 -0
  56. package/dist/biff/xf-writer.d.ts +29 -0
  57. package/dist/biff/xf-writer.js +90 -0
  58. package/dist/container.cjs +13 -4
  59. package/dist/container.d.cts +10 -3
  60. package/dist/container.d.ts +10 -3
  61. package/dist/container.js +12 -4
  62. package/dist/content.cjs +123 -18
  63. package/dist/content.js +123 -18
  64. package/dist/cursor-VMtw9uVP.d.cts +30 -0
  65. package/dist/cursor-VMtw9uVP.d.ts +30 -0
  66. package/dist/index.cjs +71 -5
  67. package/dist/index.d.cts +20 -10
  68. package/dist/index.d.ts +20 -10
  69. package/dist/index.js +17 -7
  70. package/dist/metadata.cjs +14 -0
  71. package/dist/metadata.d.cts +6 -0
  72. package/dist/metadata.d.ts +6 -0
  73. package/dist/metadata.js +13 -0
  74. package/dist/print-names-D-njuzVw.d.cts +41 -0
  75. package/dist/print-names-DUlpVE00.d.ts +41 -0
  76. package/dist/print-setup-B_ihDvm5.d.cts +57 -0
  77. package/dist/print-setup-B_ihDvm5.d.ts +57 -0
  78. package/dist/ptg-B2K8t3js.d.cts +21 -0
  79. package/dist/ptg-B2K8t3js.d.ts +21 -0
  80. package/dist/serial.cjs +41 -0
  81. package/dist/serial.d.cts +7 -1
  82. package/dist/serial.d.ts +7 -1
  83. package/dist/serial.js +39 -1
  84. package/dist/substreams-D7dQiJbp.d.ts +21 -0
  85. package/dist/substreams-Ddtvn_Vr.d.cts +21 -0
  86. package/dist/units.cjs +37 -0
  87. package/dist/units.d.cts +15 -1
  88. package/dist/units.d.ts +15 -1
  89. package/dist/units.js +33 -1
  90. package/dist/workbook/globals-writer.cjs +122 -0
  91. package/dist/workbook/globals-writer.d.cts +39 -0
  92. package/dist/workbook/globals-writer.d.ts +39 -0
  93. package/dist/workbook/globals-writer.js +120 -0
  94. package/dist/workbook/globals.cjs +72 -8
  95. package/dist/workbook/globals.d.cts +26 -2
  96. package/dist/workbook/globals.d.ts +26 -2
  97. package/dist/workbook/globals.js +71 -7
  98. package/dist/workbook/print-names.cjs +241 -0
  99. package/dist/workbook/print-names.d.cts +2 -0
  100. package/dist/workbook/print-names.d.ts +2 -0
  101. package/dist/workbook/print-names.js +238 -0
  102. package/dist/workbook/sheet-writer.cjs +277 -0
  103. package/dist/workbook/sheet-writer.d.cts +12 -0
  104. package/dist/workbook/sheet-writer.d.ts +12 -0
  105. package/dist/workbook/sheet-writer.js +276 -0
  106. package/dist/workbook/sheet.cjs +125 -17
  107. package/dist/workbook/sheet.d.cts +38 -4
  108. package/dist/workbook/sheet.d.ts +38 -4
  109. package/dist/workbook/sheet.js +125 -17
  110. package/dist/write.cjs +311 -0
  111. package/dist/write.d.cts +13 -0
  112. package/dist/write.d.ts +13 -0
  113. package/dist/write.js +309 -0
  114. package/dist/written-cells.cjs +26 -0
  115. package/dist/written-cells.d.cts +16 -0
  116. package/dist/written-cells.d.ts +16 -0
  117. package/dist/written-cells.js +24 -0
  118. package/dist/xf-colors-CehHZtBy.d.cts +91 -0
  119. package/dist/xf-colors-CpykR3B9.d.ts +91 -0
  120. package/package.json +4 -3
  121. package/dist/number-format.cjs +0 -298
  122. package/dist/number-format.d.cts +0 -32
  123. package/dist/number-format.d.ts +0 -32
  124. package/dist/number-format.js +0 -296
@@ -0,0 +1,277 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_biff_record_types = require("../biff/record-types.cjs");
3
+ const require_units = require("../units.cjs");
4
+ const require_biff_print_setup = require("../biff/print-setup.cjs");
5
+ const require_biff_write_errors = require("../biff/write-errors.cjs");
6
+ const require_serial = require("../serial.cjs");
7
+ const require_biff_builder = require("../biff/builder.cjs");
8
+ const require_biff_record_writer = require("../biff/record-writer.cjs");
9
+ const require_biff_errors = require("../biff/errors.cjs");
10
+ const require_biff_bof_writer = require("../biff/bof-writer.cjs");
11
+ const require_workbook_globals_writer = require("./globals-writer.cjs");
12
+ const require_written_cells = require("../written-cells.cjs");
13
+ //#region src/workbook/sheet-writer.ts
14
+ /** BIFF8's own 16-bit row index and 8-bit column index ceilings ([MS-XLS] 2.4.221's Rw structure and 2.4.53's Col256U structure): 65536 rows (0-65535), 256 columns (0-255) -- unlike xlsx's much larger grid. A cell outside this range cannot be expressed in BIFF8 at all, so it is refused rather than silently truncated into a wrapped index. */
15
+ const MAX_ROW_INDEX = 65535;
16
+ const MAX_COLUMN_INDEX = 255;
17
+ /** Excel 97-2003's own classic default row height (12.75pt) and default column width (in coldx units), used for a row/column this writer emits a record for but that carries no explicit size of its own -- a row with only `hidden` declared, or a ColInfo entry with no `widthPt`. */
18
+ const DEFAULT_ROW_HEIGHT_TWIPS = 255;
19
+ const DEFAULT_COLUMN_WIDTH_UNITS = 2340;
20
+ /** [MS-XLS] 2.4.221's own Row.reserved3: "MUST be 1, and MUST be ignored." */
21
+ const ROW_RESERVED3 = 1;
22
+ const ROW_FLAG_HIDDEN_BIT = 5;
23
+ const ROW_FLAG_UNSYNCED_BIT = 6;
24
+ const COLINFO_FLAG_HIDDEN = 1;
25
+ function checkedCellPosition(cell) {
26
+ if (cell.row > MAX_ROW_INDEX || cell.column > MAX_COLUMN_INDEX) throw new require_biff_write_errors.BiffWriteError(`cell at row ${cell.row}, column ${cell.column} is outside BIFF8's own grid (rows 0-${MAX_ROW_INDEX}, columns 0-${MAX_COLUMN_INDEX}); a .xls workbook cannot address it`);
27
+ }
28
+ /** The smallest and largest of a number list (undefined for an empty one), computed by a single reduce pass rather than `Math.min(...values)`/`Math.max(...values)` -- a spread call over a large cell/row list would risk the same argument-count ceiling biff/strings.ts's own readCharacters comment notes for String.fromCharCode. Folded through `reduce` rather than indexed (`values[0]`) so the accumulator's own null-check is what narrows the running min/max, never an assertion that the array is non-empty. */
29
+ function minMax(values) {
30
+ return values.reduce((running, value) => running === void 0 ? {
31
+ min: value,
32
+ max: value
33
+ } : {
34
+ min: Math.min(running.min, value),
35
+ max: Math.max(running.max, value)
36
+ }, void 0);
37
+ }
38
+ /** Dimensions ([MS-XLS] 2.4.90): the sheet's used range, derived from exactly the cells this writer emits a record for -- so a decorated empty cell counts (a real producer's used range covers a cell carrying direct formatting) while an undecorated one, having neither data nor formatting, does not. */
39
+ function writeDimensionsRecord(writtenCells) {
40
+ const rows = minMax(writtenCells.map((cell) => cell.row));
41
+ const columns = minMax(writtenCells.map((cell) => cell.column));
42
+ const rwMic = rows?.min ?? 0;
43
+ const rwMac = rows === void 0 ? 0 : rows.max + 1;
44
+ const colMic = columns?.min ?? 0;
45
+ const colMac = columns === void 0 ? 0 : columns.max + 1;
46
+ const data = new require_biff_builder.RecordBuilder().u32(rwMic).u32(rwMac).u16(colMic).u16(colMac).u16(0).build();
47
+ return require_biff_record_writer.writeRecord(512, data);
48
+ }
49
+ /** ColInfo ([MS-XLS] 2.4.53): one record per input column entry, covering exactly that one column (colFirst === colLast) rather than merging adjacent same-width columns into a range -- both are legal, and workbook/sheet.ts's own readColInfo expands either shape back into one RawColumn per column, so the two round-trip identically. */
50
+ function writeColInfoRecord(column) {
51
+ if (column.index > MAX_COLUMN_INDEX) throw new require_biff_write_errors.BiffWriteError(`column ${column.index} is outside BIFF8's own 256-column grid (columns 0-${MAX_COLUMN_INDEX})`);
52
+ const coldx = column.widthPt !== void 0 ? require_units.pointsToColumnWidth(column.widthPt) : DEFAULT_COLUMN_WIDTH_UNITS;
53
+ const flags = column.hidden === true ? COLINFO_FLAG_HIDDEN : 0;
54
+ const data = new require_biff_builder.RecordBuilder().u16(column.index).u16(column.index).u16(coldx).u16(require_workbook_globals_writer.GENERAL_CELL_XF_INDEX).u16(flags).u16(0).build();
55
+ return require_biff_record_writer.writeRecord(125, data);
56
+ }
57
+ /** Row ([MS-XLS] 2.4.221): the row's own populated-column range (from its cells, not the sheet's own Dimensions), height, and hidden/manually-set flags. */
58
+ function writeRowRecord(rowIndex, cellsInRow, declared) {
59
+ const columnRange = minMax(cellsInRow.map((cell) => cell.column));
60
+ const colMic = columnRange?.min ?? 0;
61
+ const colMac = columnRange === void 0 ? 0 : columnRange.max + 1;
62
+ const heightTwips = declared?.heightPt !== void 0 ? require_units.pointsToTwips(declared.heightPt) : DEFAULT_ROW_HEIGHT_TWIPS;
63
+ const fDyZero = declared?.hidden === true ? 1 : 0;
64
+ const fUnsynced = declared?.heightPt !== void 0 ? 1 : 0;
65
+ const flagsByte = fDyZero << ROW_FLAG_HIDDEN_BIT | fUnsynced << ROW_FLAG_UNSYNCED_BIT;
66
+ const data = new require_biff_builder.RecordBuilder().u16(rowIndex).u16(colMic).u16(colMac).u16(heightTwips).u16(0).u16(0).u8(flagsByte).u8(ROW_RESERVED3).u16(0).build();
67
+ return require_biff_record_writer.writeRecord(520, data);
68
+ }
69
+ /** Every merged range the sheet's own cells declare via colSpan/rowSpan, regardless of whether the anchor cell carries a value -- an 'empty'-kind anchor still merges, and content.ts's own applyMerges reconstructs it independently from the MergeCells record this produces. */
70
+ function mergedRangesOf(cells) {
71
+ const ranges = [];
72
+ for (const cell of cells) {
73
+ const rowSpan = cell.rowSpan ?? 1;
74
+ const colSpan = cell.colSpan ?? 1;
75
+ if (rowSpan > 1 || colSpan > 1) ranges.push({
76
+ startRow: cell.row,
77
+ startColumn: cell.column,
78
+ endRow: cell.row + rowSpan - 1,
79
+ endColumn: cell.column + colSpan - 1
80
+ });
81
+ }
82
+ return ranges;
83
+ }
84
+ /** MergeCells ([MS-XLS] 2.4.168): a count then that many Ref8 structures ([MS-XLS] 2.5.208), each rowFirst/rowLast/colFirst/colLast -- the order workbook/sheet.ts's own readMergeCells reads them in. */
85
+ function writeMergeCellsRecord(ranges) {
86
+ const builder = new require_biff_builder.RecordBuilder().u16(ranges.length);
87
+ for (const range of ranges) builder.u16(range.startRow).u16(range.endRow).u16(range.startColumn).u16(range.endColumn);
88
+ return require_biff_record_writer.writeRecord(229, builder.build());
89
+ }
90
+ /** CalcCount's own cIter ([MS-XLS] 2.4.31): "MUST be greater than or equal to one and less than or equal to 32767". Excel's own default iteration limit, and what a real LibreOffice-written BIFF8 carries. */
91
+ const CALC_ITERATION_LIMIT = 100;
92
+ /** CalcDelta's own numDelta ([MS-XLS] 2.4.32): Excel's own default minimum change for iterative calculation to continue. */
93
+ const CALC_ITERATION_DELTA = .001;
94
+ /**
95
+ * The calculation-state records [MS-XLS] 2.1.7.20.6's GLOBALS production requires ahead of PrintRowCol, none of which carries anything document-schema.js models.
96
+ *
97
+ * They are written for two reasons, one of them empirical. The grammar makes them mandatory -- `GLOBALS = CalcMode CalcCount CalcRefMode CalcIter CalcDelta CalcSaveRecalc PrintRowCol PrintGrid GridSet Guts DefaultRowHeight WsBool ...`, with no brackets on any of them -- so a substream that opens straight with a print setting is not a conformant worksheet at all. And LibreOffice's own BIFF8 importer silently discards whichever page-settings record comes FIRST in a worksheet substream: with PrintRowCol in that slot, a `.xls` this writer produced with row and column headers enabled opened in LibreOffice with them off, while every other print setting in the same file came through correctly. Moving any other record into that slot fixes it, which is what these do -- verified by writing the same workbook with and without them and re-reading each through `soffice --convert-to fods`.
98
+ *
99
+ * The values are Excel's own defaults (automatic recalculation, A1 references, iteration off), matching what a real LibreOffice-written file carries for a workbook nobody has changed the calculation settings of. CalcMode is deliberately not among them: the production names it, but LibreOffice does not write one into a worksheet substream either, and the records below already satisfy the constraint this comment exists for.
100
+ */
101
+ function writeCalculationStateRecords() {
102
+ return [
103
+ require_biff_record_writer.writeRecord(12, new require_biff_builder.RecordBuilder().u16(CALC_ITERATION_LIMIT).build()),
104
+ require_biff_record_writer.writeRecord(15, new require_biff_builder.RecordBuilder().u16(1).build()),
105
+ require_biff_record_writer.writeRecord(17, new require_biff_builder.RecordBuilder().u16(0).build()),
106
+ require_biff_record_writer.writeRecord(16, new require_biff_builder.RecordBuilder().f64(CALC_ITERATION_DELTA).build()),
107
+ require_biff_record_writer.writeRecord(95, new require_biff_builder.RecordBuilder().u16(1).build())
108
+ ];
109
+ }
110
+ /** Setup's own iRes/iVRes ([MS-XLS] 2.4.257), a printer resolution in DPI. 300 is what a real LibreOffice-written BIFF8 carries and a sane default for a writer with no printer to ask; the field is undefined whenever fNoPls is set, which this writer never sets, so it must carry something real. */
111
+ const SETUP_PRINT_RESOLUTION_DPI = 300;
112
+ /** Setup's own iCopies: one copy, the only sensible value for a file that is not being sent to a printer right now. */
113
+ const SETUP_COPIES = 1;
114
+ /** Setup's own numHdr/numFtr ([MS-XLS] 2.4.257), the header and footer margins in inches. Excel's own Normal preset value; Margins has no header/footer field for a real one to come from, and content.ts's read side discards these for the same reason. */
115
+ const SETUP_HEADER_FOOTER_MARGIN_INCHES = .3;
116
+ /** Setup's own iFitWidth/iFitHeight when the sheet is not in fit-to-page mode at all. Written rather than left at 0 because 0 means "as many pages as necessary" -- a real value a reader must not see while fFitToPage is clear and mistake for an intent the sheet never had. */
117
+ const SETUP_INACTIVE_FIT_PAGES = 1;
118
+ /** Setup's own iScale when the sheet IS in fit-to-page mode: 100%, actual size, the inactive value a real producer leaves behind (confirmed against LibreOffice-written BIFF8, whose fit-to-page sheets carry exactly this). */
119
+ const SETUP_INACTIVE_SCALE_PERCENT = 100;
120
+ /** Setup's own iPaperSize for a page size no entry of its code table names: [MS-XLS] 2.4.257's "The value 0, or values greater than or equal to 256, specify custom printer paper sizes." */
121
+ const SETUP_CUSTOM_PAPER_SIZE = 0;
122
+ /** [MS-XLS] 2.4.257's own ceiling on iFitWidth and iFitHeight: "MUST be less than or equal to 32767." */
123
+ const SETUP_MAX_FIT_PAGES = 32767;
124
+ /** iScale is an unsigned 16-bit percentage with no documented ceiling of its own, so the field's own width is the only bound there is. 1 is the floor: 0 is not a scaling factor, and a sub-1% one has no whole-percent spelling. */
125
+ const SETUP_MIN_SCALE_PERCENT = 1;
126
+ const SETUP_MAX_SCALE_PERCENT = 65535;
127
+ /**
128
+ * Clamps a schema value into the range its own Setup field can hold, since `ContentSheetPrintSettings` bounds neither a scale percentage nor a fit-to-page count from above.
129
+ *
130
+ * Clamping rather than throwing, and rather than letting the value wrap: a print scale is a presentational field, and refusing a whole workbook over an absurd one would lose its cells for no gain (the same trade the paper-code case above settles the same way). Silently wrapping is the option neither of those beats -- a scale of 65540% would land in the file as 4%, which is a different intent stated confidently.
131
+ */
132
+ function clampedSetupField(value, minimum, maximum) {
133
+ return Math.min(Math.max(Math.round(value), minimum), maximum);
134
+ }
135
+ /**
136
+ * Setup ([MS-XLS] 2.4.257): iPaperSize, iScale, iPageStart, iFitWidth, iFitHeight, the flags word, iRes, iVRes, numHdr, numFtr, iCopies.
137
+ *
138
+ * fNoPls is deliberately never set. It would declare this record's own paper size, scale, and orientation undefined -- exactly the three fields it exists here to carry -- and [MS-XLS] pairs it with a Pls record holding a printer driver's DEVMODE blob, which this writer has none of.
139
+ *
140
+ * A page size no code in the table names is written as SETUP_CUSTOM_PAPER_SIZE rather than as a standard paper it is not. The dimensions themselves are genuinely unwritable -- Setup addresses paper only by code, and [MS-XLS]'s own escape hatch for a size outside the table is that same Pls record -- so the choice is between saying "custom" and saying something false. Saying "custom" is what the spec's own iPaperSize table provides the value for, and it leaves a reader (this package's own included) free to fall back to its documented default instead of confidently reporting Letter for a page that is not Letter. This matters in practice rather than in theory: a spreadsheet converted from a slide deck or a drawing carries that source's own canvas as its page size, which is almost never a named paper, and refusing the conversion outright over a presentational field would lose the cells too.
141
+ */
142
+ function writeSetupRecord(settings) {
143
+ const paper = require_biff_print_setup.paperSelectionFor(settings.pageSize) ?? {
144
+ code: SETUP_CUSTOM_PAPER_SIZE,
145
+ portrait: settings.pageSize.widthPt <= settings.pageSize.heightPt
146
+ };
147
+ const fitToPages = settings.fitToPages;
148
+ const fields = {
149
+ paperCode: paper.code,
150
+ scalePercent: fitToPages === void 0 ? clampedSetupField(settings.scalePercent ?? SETUP_INACTIVE_SCALE_PERCENT, SETUP_MIN_SCALE_PERCENT, SETUP_MAX_SCALE_PERCENT) : SETUP_INACTIVE_SCALE_PERCENT,
151
+ fitWidth: fitToPages === void 0 ? SETUP_INACTIVE_FIT_PAGES : clampedSetupField(fitToPages.width, 1, SETUP_MAX_FIT_PAGES),
152
+ fitHeight: fitToPages === void 0 ? SETUP_INACTIVE_FIT_PAGES : clampedSetupField(fitToPages.height, 1, SETUP_MAX_FIT_PAGES),
153
+ leftToRight: settings.pageOrder === "overThenDown",
154
+ portrait: paper.portrait,
155
+ noPls: false,
156
+ noOrientation: false
157
+ };
158
+ const data = new require_biff_builder.RecordBuilder().u16(fields.paperCode).u16(fields.scalePercent).u16(0).u16(fields.fitWidth).u16(fields.fitHeight).u16(require_biff_print_setup.packSetupFlags(fields)).u16(SETUP_PRINT_RESOLUTION_DPI).u16(SETUP_PRINT_RESOLUTION_DPI).f64(SETUP_HEADER_FOOTER_MARGIN_INCHES).f64(SETUP_HEADER_FOOTER_MARGIN_INCHES).u16(SETUP_COPIES).build();
159
+ return require_biff_record_writer.writeRecord(161, data);
160
+ }
161
+ /** Any of the four margin records ([MS-XLS] 2.4.151, 2.4.219, 2.4.328, 2.4.27): a single Xnum stating that margin in inches. All four share one field layout, so one writer serves them all -- the mirror of workbook/sheet.ts's own single readMargin. */
162
+ function writeMarginRecord(recordType, points) {
163
+ return require_biff_record_writer.writeRecord(recordType, new require_biff_builder.RecordBuilder().f64(require_units.pointsToInches(points)).build());
164
+ }
165
+ /** PrintGrid ([MS-XLS] 2.4.202) and PrintRowCol ([MS-XLS] 2.4.203), each a single 16-bit boolean. */
166
+ function writeBooleanRecord(recordType, value) {
167
+ return require_biff_record_writer.writeRecord(recordType, new require_biff_builder.RecordBuilder().u16(value ? 1 : 0).build());
168
+ }
169
+ /** WsBool ([MS-XLS] 2.4.351). Only fFitToPage is set from real data; every other bit is written clear, which is what a sheet with no outline, no dialog behaviour, no synchronised scrolling, and no transition formula handling means -- and is exactly the set of facts ContentSheet carries nothing about. */
170
+ function writeWsBoolRecord(fitToPage) {
171
+ return require_biff_record_writer.writeRecord(129, new require_biff_builder.RecordBuilder().u16(fitToPage ? 256 : 0).build());
172
+ }
173
+ /**
174
+ * HorizontalPageBreaks ([MS-XLS] 2.4.142) or VerticalPageBreaks ([MS-XLS] 2.4.343): a count then that many six-byte structures, each the break's own index followed by the start and end of its extent along the other axis.
175
+ *
176
+ * Both structures share that three-field shape, so one writer serves both -- the extent's own end differs, which is what `extentEnd` carries: ContentSheetPrintSettings models a break as a whole-axis index with no extent, so every break written here runs the full width or height of BIFF8's own grid. Written in ascending index order, which is the sort [MS-XLS] requires of both arrays; the caller's own indices are sorted first rather than assumed sorted.
177
+ */
178
+ function writePageBreaksRecord(recordType, indices, extentEnd) {
179
+ const sorted = [...new Set(indices)].sort((a, b) => a - b);
180
+ const builder = new require_biff_builder.RecordBuilder().u16(sorted.length);
181
+ for (const index of sorted) builder.u16(index).u16(0).u16(extentEnd);
182
+ return require_biff_record_writer.writeRecord(recordType, builder.build());
183
+ }
184
+ /**
185
+ * `manualBreaks.rows`/`.columns` is not bounded to BIFF8's own grid the way a cell's own row/column is (`writeCellRecord`'s `MAX_ROW_INDEX`/`MAX_COLUMN_INDEX` guard above throws for exactly that reason). `writePageBreaksRecord` writes each index into a 16-bit field regardless (`RecordBuilder.u16` masks with `0xffff`), so an out-of-grid break would otherwise silently wrap to a plausible-looking in-grid one -- a break asked for at row 70000 landing at row 4464 -- with nothing downstream to notice.
186
+ *
187
+ * Dropped rather than clamped: unlike a print range or a repeated header band, where clamping to the grid's own last row/column is genuinely what "to the bottom of the sheet" means once the grid shrinks under it (print-names.ts's own clampToGrid), a page break is a single position, and clamping one would insert a break at the grid's own edge the caller never asked for. Dropping states the honest thing an out-of-grid break means once BIFF8's own ceiling has been applied: no such row/column exists for it to sit at.
188
+ */
189
+ function inGridBreaks(indices, maxIndex) {
190
+ return indices.filter((index) => index <= maxIndex);
191
+ }
192
+ /**
193
+ * Every print-settings record one sheet needs, in [MS-XLS] 2.1.7.20.6's own order.
194
+ *
195
+ * That order is two productions, back to back, both of which the worksheet substream places ahead of COLUMNS, Dimensions, and the cell table: `GLOBALS = ... PrintRowCol PrintGrid GridSet Guts DefaultRowHeight WsBool [Sync] [LPr] [HorizontalPageBreaks] [VerticalPageBreaks]` and `PAGESETUP = Header Footer HCenter VCenter [LeftMargin] [RightMargin] [TopMargin] [BottomMargin] [Pls *Continue] [Setup]`. The mandatory records this writer does not emit at all (the calculation-state family, GridSet, Guts, DefaultRowHeight, Header/Footer, HCenter/VCenter) are the same UI and interoperability bookkeeping it already omits everywhere else -- see this package's README -- so what remains is the optional subset that actually carries print settings, in the relative order those two productions give it.
196
+ *
197
+ * Every record here is written unconditionally, including for a sheet whose settings are exactly the Normal preset. A print setting has no "absent" spelling in ContentSheetPrintSettings -- gridlines, headers, page order, page size, and all four margins are required fields -- so there is no way to tell a sheet that asked for the preset from one that never stated anything, and writing the values out is what makes the round trip exact either way.
198
+ */
199
+ function writePrintSettingsRecords(settings) {
200
+ const pieces = [
201
+ ...writeCalculationStateRecords(),
202
+ writeBooleanRecord(42, settings.headers),
203
+ writeBooleanRecord(43, settings.gridlines),
204
+ writeWsBoolRecord(settings.fitToPages !== void 0)
205
+ ];
206
+ const breaks = settings.manualBreaks;
207
+ if (breaks !== void 0) {
208
+ const rowBreaks = inGridBreaks(breaks.rows, MAX_ROW_INDEX);
209
+ if (rowBreaks.length > 0) pieces.push(writePageBreaksRecord(27, rowBreaks, MAX_COLUMN_INDEX));
210
+ const columnBreaks = inGridBreaks(breaks.columns, MAX_COLUMN_INDEX);
211
+ if (columnBreaks.length > 0) pieces.push(writePageBreaksRecord(26, columnBreaks, MAX_ROW_INDEX));
212
+ }
213
+ pieces.push(writeMarginRecord(38, settings.margins.leftPt), writeMarginRecord(39, settings.margins.rightPt), writeMarginRecord(40, settings.margins.topPt), writeMarginRecord(41, settings.margins.bottomPt), writeSetupRecord(settings));
214
+ return pieces;
215
+ }
216
+ function cellHeader(cell, xfIndex) {
217
+ return new require_biff_builder.RecordBuilder().u16(cell.row).u16(cell.column).u16(xfIndex);
218
+ }
219
+ /** One cell record, keyed by ContentCellValue's own discriminant: Number for every numeric/temporal kind ([MS-XLS] 2.4.180 -- always the full IEEE 754 double, never the packed RK encoding, which is a compaction optimisation this writer does not implement), LabelSst for a string ([MS-XLS] 2.4.149, through the workbook-wide shared string table), BoolErr for a boolean or error value ([MS-XLS] 2.4.24), and Blank for an 'empty' cell whose formatting is the only thing it carries ([MS-XLS] 2.4.20 -- a cell header and nothing else, so the XF its ixfe names is the whole content). An unformatted empty cell never reaches here at all: written-cells.ts's own predicate filters it out upstream, since there is nothing for it to say. */
220
+ function writeCellValueRecord(cell, xfIndex, ctx) {
221
+ const value = cell.value;
222
+ switch (value.kind) {
223
+ case "number":
224
+ case "percentage":
225
+ case "currency": return require_biff_record_writer.writeRecord(515, cellHeader(cell, xfIndex).f64(value.value).build());
226
+ case "date": return require_biff_record_writer.writeRecord(515, cellHeader(cell, xfIndex).f64(require_serial.isoDateToSerial(value.value, false)).build());
227
+ case "time": return require_biff_record_writer.writeRecord(515, cellHeader(cell, xfIndex).f64(require_serial.isoTimeToSerial(value.value)).build());
228
+ case "dateTime": return require_biff_record_writer.writeRecord(515, cellHeader(cell, xfIndex).f64(require_serial.isoDateTimeToSerial(value.value, false)).build());
229
+ case "string": {
230
+ const index = ctx.sstIndexFor(value.value);
231
+ return require_biff_record_writer.writeRecord(253, cellHeader(cell, xfIndex).u32(index).build());
232
+ }
233
+ case "boolean": return require_biff_record_writer.writeRecord(517, cellHeader(cell, xfIndex).u8(value.value ? 1 : 0).u8(0).build());
234
+ case "error": {
235
+ const code = require_biff_errors.errorCodeOf(value.value);
236
+ if (code === void 0) throw new require_biff_write_errors.BiffWriteError(`cell at row ${cell.row}, column ${cell.column} carries error text ${JSON.stringify(value.value)}, which is not one of the eight error values [MS-XLS] 2.5.10 defines`);
237
+ return require_biff_record_writer.writeRecord(517, cellHeader(cell, xfIndex).u8(code).u8(1).build());
238
+ }
239
+ case "empty":
240
+ if (!require_written_cells.cellCarriesFormatting(cell)) throw new require_biff_write_errors.BiffWriteError(`internal error: writeCellValueRecord was called for the undecorated empty cell at row ${cell.row}, column ${cell.column}, which written-cells.ts's own predicate must filter out before reaching here`);
241
+ return require_biff_record_writer.writeRecord(513, cellHeader(cell, xfIndex).build());
242
+ }
243
+ }
244
+ /** Builds one worksheet's own substream: BOF, the print-settings records, Dimensions, ColInfo per column, Row + value-cell records per populated or declared row (in ascending row then column order), MergeCells if the sheet declares any, EOF. */
245
+ function buildWorksheetSubstream(sheet, ctx) {
246
+ for (const cell of sheet.cells) checkedCellPosition(cell);
247
+ const writtenCells = sheet.cells.filter(require_written_cells.writesCellRecord);
248
+ const cellsByRow = /* @__PURE__ */ new Map();
249
+ for (const cell of writtenCells) {
250
+ const existing = cellsByRow.get(cell.row);
251
+ if (existing === void 0) cellsByRow.set(cell.row, [cell]);
252
+ else existing.push(cell);
253
+ }
254
+ for (const cells of cellsByRow.values()) cells.sort((a, b) => a.column - b.column);
255
+ const declaredRows = new Map(sheet.rows.map((row) => [row.index, row]));
256
+ const sortedRowIndices = [.../* @__PURE__ */ new Set([...cellsByRow.keys(), ...declaredRows.keys()])].sort((a, b) => a - b);
257
+ const pieces = [
258
+ require_biff_record_writer.writeRecord(require_biff_record_types.RECORD_BOF, require_biff_bof_writer.writeBofData(16)),
259
+ ...writePrintSettingsRecords(sheet.printSettings),
260
+ writeDimensionsRecord(writtenCells)
261
+ ];
262
+ for (const column of sheet.columns) pieces.push(writeColInfoRecord(column));
263
+ for (const rowIndex of sortedRowIndices) {
264
+ const cellsInRow = cellsByRow.get(rowIndex) ?? [];
265
+ pieces.push(writeRowRecord(rowIndex, cellsInRow, declaredRows.get(rowIndex)));
266
+ for (const cell of cellsInRow) {
267
+ const xfIndex = ctx.xfIndexForCell(cell);
268
+ pieces.push(writeCellValueRecord(cell, xfIndex, ctx));
269
+ }
270
+ }
271
+ const merges = mergedRangesOf(sheet.cells);
272
+ if (merges.length > 0) pieces.push(writeMergeCellsRecord(merges));
273
+ pieces.push(require_biff_record_writer.writeRecord(10, /* @__PURE__ */ new Uint8Array(0)));
274
+ return require_biff_record_writer.concatRecords(...pieces);
275
+ }
276
+ //#endregion
277
+ exports.buildWorksheetSubstream = buildWorksheetSubstream;
@@ -0,0 +1,12 @@
1
+ import { ContentSheet, ContentSheetCell } from "document-schema.js";
2
+ //#region src/workbook/sheet-writer.d.ts
3
+ interface SheetWriteContext {
4
+ /** The XF index ([MS-XLS] 2.5.168 IXFCell) a cell's own (number format, alignment, decoration) combination resolves to -- GENERAL_CELL_XF_INDEX for a cell with General formatting, general/bottom alignment, and no background/borders, one of the workbook's other cell XFs otherwise. write.ts's own cell-format interning pass is what assigns and deduplicates these. */
5
+ xfIndexForCell(cell: ContentSheetCell): number;
6
+ /** The shared string table index for a string cell's own text; every string a sheet writes must already be registered in the workbook-wide table before this is called. */
7
+ sstIndexFor(text: string): number;
8
+ }
9
+ /** Builds one worksheet's own substream: BOF, the print-settings records, Dimensions, ColInfo per column, Row + value-cell records per populated or declared row (in ascending row then column order), MergeCells if the sheet declares any, EOF. */
10
+ declare function buildWorksheetSubstream(sheet: ContentSheet, ctx: SheetWriteContext): Uint8Array<ArrayBuffer>;
11
+ //#endregion
12
+ export { SheetWriteContext, buildWorksheetSubstream };
@@ -0,0 +1,12 @@
1
+ import { ContentSheet, ContentSheetCell } from "document-schema.js";
2
+ //#region src/workbook/sheet-writer.d.ts
3
+ interface SheetWriteContext {
4
+ /** The XF index ([MS-XLS] 2.5.168 IXFCell) a cell's own (number format, alignment, decoration) combination resolves to -- GENERAL_CELL_XF_INDEX for a cell with General formatting, general/bottom alignment, and no background/borders, one of the workbook's other cell XFs otherwise. write.ts's own cell-format interning pass is what assigns and deduplicates these. */
5
+ xfIndexForCell(cell: ContentSheetCell): number;
6
+ /** The shared string table index for a string cell's own text; every string a sheet writes must already be registered in the workbook-wide table before this is called. */
7
+ sstIndexFor(text: string): number;
8
+ }
9
+ /** Builds one worksheet's own substream: BOF, the print-settings records, Dimensions, ColInfo per column, Row + value-cell records per populated or declared row (in ascending row then column order), MergeCells if the sheet declares any, EOF. */
10
+ declare function buildWorksheetSubstream(sheet: ContentSheet, ctx: SheetWriteContext): Uint8Array<ArrayBuffer>;
11
+ //#endregion
12
+ export { SheetWriteContext, buildWorksheetSubstream };
@@ -0,0 +1,276 @@
1
+ import { RECORD_BOF } from "../biff/record-types.js";
2
+ import { pointsToColumnWidth, pointsToInches, pointsToTwips } from "../units.js";
3
+ import { packSetupFlags, paperSelectionFor } from "../biff/print-setup.js";
4
+ import { BiffWriteError } from "../biff/write-errors.js";
5
+ import { isoDateTimeToSerial, isoDateToSerial, isoTimeToSerial } from "../serial.js";
6
+ import { RecordBuilder } from "../biff/builder.js";
7
+ import { concatRecords, writeRecord } from "../biff/record-writer.js";
8
+ import { errorCodeOf } from "../biff/errors.js";
9
+ import { writeBofData } from "../biff/bof-writer.js";
10
+ import { GENERAL_CELL_XF_INDEX } from "./globals-writer.js";
11
+ import { cellCarriesFormatting, writesCellRecord } from "../written-cells.js";
12
+ //#region src/workbook/sheet-writer.ts
13
+ /** BIFF8's own 16-bit row index and 8-bit column index ceilings ([MS-XLS] 2.4.221's Rw structure and 2.4.53's Col256U structure): 65536 rows (0-65535), 256 columns (0-255) -- unlike xlsx's much larger grid. A cell outside this range cannot be expressed in BIFF8 at all, so it is refused rather than silently truncated into a wrapped index. */
14
+ const MAX_ROW_INDEX = 65535;
15
+ const MAX_COLUMN_INDEX = 255;
16
+ /** Excel 97-2003's own classic default row height (12.75pt) and default column width (in coldx units), used for a row/column this writer emits a record for but that carries no explicit size of its own -- a row with only `hidden` declared, or a ColInfo entry with no `widthPt`. */
17
+ const DEFAULT_ROW_HEIGHT_TWIPS = 255;
18
+ const DEFAULT_COLUMN_WIDTH_UNITS = 2340;
19
+ /** [MS-XLS] 2.4.221's own Row.reserved3: "MUST be 1, and MUST be ignored." */
20
+ const ROW_RESERVED3 = 1;
21
+ const ROW_FLAG_HIDDEN_BIT = 5;
22
+ const ROW_FLAG_UNSYNCED_BIT = 6;
23
+ const COLINFO_FLAG_HIDDEN = 1;
24
+ function checkedCellPosition(cell) {
25
+ if (cell.row > MAX_ROW_INDEX || cell.column > MAX_COLUMN_INDEX) throw new BiffWriteError(`cell at row ${cell.row}, column ${cell.column} is outside BIFF8's own grid (rows 0-${MAX_ROW_INDEX}, columns 0-${MAX_COLUMN_INDEX}); a .xls workbook cannot address it`);
26
+ }
27
+ /** The smallest and largest of a number list (undefined for an empty one), computed by a single reduce pass rather than `Math.min(...values)`/`Math.max(...values)` -- a spread call over a large cell/row list would risk the same argument-count ceiling biff/strings.ts's own readCharacters comment notes for String.fromCharCode. Folded through `reduce` rather than indexed (`values[0]`) so the accumulator's own null-check is what narrows the running min/max, never an assertion that the array is non-empty. */
28
+ function minMax(values) {
29
+ return values.reduce((running, value) => running === void 0 ? {
30
+ min: value,
31
+ max: value
32
+ } : {
33
+ min: Math.min(running.min, value),
34
+ max: Math.max(running.max, value)
35
+ }, void 0);
36
+ }
37
+ /** Dimensions ([MS-XLS] 2.4.90): the sheet's used range, derived from exactly the cells this writer emits a record for -- so a decorated empty cell counts (a real producer's used range covers a cell carrying direct formatting) while an undecorated one, having neither data nor formatting, does not. */
38
+ function writeDimensionsRecord(writtenCells) {
39
+ const rows = minMax(writtenCells.map((cell) => cell.row));
40
+ const columns = minMax(writtenCells.map((cell) => cell.column));
41
+ const rwMic = rows?.min ?? 0;
42
+ const rwMac = rows === void 0 ? 0 : rows.max + 1;
43
+ const colMic = columns?.min ?? 0;
44
+ const colMac = columns === void 0 ? 0 : columns.max + 1;
45
+ const data = new RecordBuilder().u32(rwMic).u32(rwMac).u16(colMic).u16(colMac).u16(0).build();
46
+ return writeRecord(512, data);
47
+ }
48
+ /** ColInfo ([MS-XLS] 2.4.53): one record per input column entry, covering exactly that one column (colFirst === colLast) rather than merging adjacent same-width columns into a range -- both are legal, and workbook/sheet.ts's own readColInfo expands either shape back into one RawColumn per column, so the two round-trip identically. */
49
+ function writeColInfoRecord(column) {
50
+ if (column.index > MAX_COLUMN_INDEX) throw new BiffWriteError(`column ${column.index} is outside BIFF8's own 256-column grid (columns 0-${MAX_COLUMN_INDEX})`);
51
+ const coldx = column.widthPt !== void 0 ? pointsToColumnWidth(column.widthPt) : DEFAULT_COLUMN_WIDTH_UNITS;
52
+ const flags = column.hidden === true ? COLINFO_FLAG_HIDDEN : 0;
53
+ const data = new RecordBuilder().u16(column.index).u16(column.index).u16(coldx).u16(GENERAL_CELL_XF_INDEX).u16(flags).u16(0).build();
54
+ return writeRecord(125, data);
55
+ }
56
+ /** Row ([MS-XLS] 2.4.221): the row's own populated-column range (from its cells, not the sheet's own Dimensions), height, and hidden/manually-set flags. */
57
+ function writeRowRecord(rowIndex, cellsInRow, declared) {
58
+ const columnRange = minMax(cellsInRow.map((cell) => cell.column));
59
+ const colMic = columnRange?.min ?? 0;
60
+ const colMac = columnRange === void 0 ? 0 : columnRange.max + 1;
61
+ const heightTwips = declared?.heightPt !== void 0 ? pointsToTwips(declared.heightPt) : DEFAULT_ROW_HEIGHT_TWIPS;
62
+ const fDyZero = declared?.hidden === true ? 1 : 0;
63
+ const fUnsynced = declared?.heightPt !== void 0 ? 1 : 0;
64
+ const flagsByte = fDyZero << ROW_FLAG_HIDDEN_BIT | fUnsynced << ROW_FLAG_UNSYNCED_BIT;
65
+ const data = new RecordBuilder().u16(rowIndex).u16(colMic).u16(colMac).u16(heightTwips).u16(0).u16(0).u8(flagsByte).u8(ROW_RESERVED3).u16(0).build();
66
+ return writeRecord(520, data);
67
+ }
68
+ /** Every merged range the sheet's own cells declare via colSpan/rowSpan, regardless of whether the anchor cell carries a value -- an 'empty'-kind anchor still merges, and content.ts's own applyMerges reconstructs it independently from the MergeCells record this produces. */
69
+ function mergedRangesOf(cells) {
70
+ const ranges = [];
71
+ for (const cell of cells) {
72
+ const rowSpan = cell.rowSpan ?? 1;
73
+ const colSpan = cell.colSpan ?? 1;
74
+ if (rowSpan > 1 || colSpan > 1) ranges.push({
75
+ startRow: cell.row,
76
+ startColumn: cell.column,
77
+ endRow: cell.row + rowSpan - 1,
78
+ endColumn: cell.column + colSpan - 1
79
+ });
80
+ }
81
+ return ranges;
82
+ }
83
+ /** MergeCells ([MS-XLS] 2.4.168): a count then that many Ref8 structures ([MS-XLS] 2.5.208), each rowFirst/rowLast/colFirst/colLast -- the order workbook/sheet.ts's own readMergeCells reads them in. */
84
+ function writeMergeCellsRecord(ranges) {
85
+ const builder = new RecordBuilder().u16(ranges.length);
86
+ for (const range of ranges) builder.u16(range.startRow).u16(range.endRow).u16(range.startColumn).u16(range.endColumn);
87
+ return writeRecord(229, builder.build());
88
+ }
89
+ /** CalcCount's own cIter ([MS-XLS] 2.4.31): "MUST be greater than or equal to one and less than or equal to 32767". Excel's own default iteration limit, and what a real LibreOffice-written BIFF8 carries. */
90
+ const CALC_ITERATION_LIMIT = 100;
91
+ /** CalcDelta's own numDelta ([MS-XLS] 2.4.32): Excel's own default minimum change for iterative calculation to continue. */
92
+ const CALC_ITERATION_DELTA = .001;
93
+ /**
94
+ * The calculation-state records [MS-XLS] 2.1.7.20.6's GLOBALS production requires ahead of PrintRowCol, none of which carries anything document-schema.js models.
95
+ *
96
+ * They are written for two reasons, one of them empirical. The grammar makes them mandatory -- `GLOBALS = CalcMode CalcCount CalcRefMode CalcIter CalcDelta CalcSaveRecalc PrintRowCol PrintGrid GridSet Guts DefaultRowHeight WsBool ...`, with no brackets on any of them -- so a substream that opens straight with a print setting is not a conformant worksheet at all. And LibreOffice's own BIFF8 importer silently discards whichever page-settings record comes FIRST in a worksheet substream: with PrintRowCol in that slot, a `.xls` this writer produced with row and column headers enabled opened in LibreOffice with them off, while every other print setting in the same file came through correctly. Moving any other record into that slot fixes it, which is what these do -- verified by writing the same workbook with and without them and re-reading each through `soffice --convert-to fods`.
97
+ *
98
+ * The values are Excel's own defaults (automatic recalculation, A1 references, iteration off), matching what a real LibreOffice-written file carries for a workbook nobody has changed the calculation settings of. CalcMode is deliberately not among them: the production names it, but LibreOffice does not write one into a worksheet substream either, and the records below already satisfy the constraint this comment exists for.
99
+ */
100
+ function writeCalculationStateRecords() {
101
+ return [
102
+ writeRecord(12, new RecordBuilder().u16(CALC_ITERATION_LIMIT).build()),
103
+ writeRecord(15, new RecordBuilder().u16(1).build()),
104
+ writeRecord(17, new RecordBuilder().u16(0).build()),
105
+ writeRecord(16, new RecordBuilder().f64(CALC_ITERATION_DELTA).build()),
106
+ writeRecord(95, new RecordBuilder().u16(1).build())
107
+ ];
108
+ }
109
+ /** Setup's own iRes/iVRes ([MS-XLS] 2.4.257), a printer resolution in DPI. 300 is what a real LibreOffice-written BIFF8 carries and a sane default for a writer with no printer to ask; the field is undefined whenever fNoPls is set, which this writer never sets, so it must carry something real. */
110
+ const SETUP_PRINT_RESOLUTION_DPI = 300;
111
+ /** Setup's own iCopies: one copy, the only sensible value for a file that is not being sent to a printer right now. */
112
+ const SETUP_COPIES = 1;
113
+ /** Setup's own numHdr/numFtr ([MS-XLS] 2.4.257), the header and footer margins in inches. Excel's own Normal preset value; Margins has no header/footer field for a real one to come from, and content.ts's read side discards these for the same reason. */
114
+ const SETUP_HEADER_FOOTER_MARGIN_INCHES = .3;
115
+ /** Setup's own iFitWidth/iFitHeight when the sheet is not in fit-to-page mode at all. Written rather than left at 0 because 0 means "as many pages as necessary" -- a real value a reader must not see while fFitToPage is clear and mistake for an intent the sheet never had. */
116
+ const SETUP_INACTIVE_FIT_PAGES = 1;
117
+ /** Setup's own iScale when the sheet IS in fit-to-page mode: 100%, actual size, the inactive value a real producer leaves behind (confirmed against LibreOffice-written BIFF8, whose fit-to-page sheets carry exactly this). */
118
+ const SETUP_INACTIVE_SCALE_PERCENT = 100;
119
+ /** Setup's own iPaperSize for a page size no entry of its code table names: [MS-XLS] 2.4.257's "The value 0, or values greater than or equal to 256, specify custom printer paper sizes." */
120
+ const SETUP_CUSTOM_PAPER_SIZE = 0;
121
+ /** [MS-XLS] 2.4.257's own ceiling on iFitWidth and iFitHeight: "MUST be less than or equal to 32767." */
122
+ const SETUP_MAX_FIT_PAGES = 32767;
123
+ /** iScale is an unsigned 16-bit percentage with no documented ceiling of its own, so the field's own width is the only bound there is. 1 is the floor: 0 is not a scaling factor, and a sub-1% one has no whole-percent spelling. */
124
+ const SETUP_MIN_SCALE_PERCENT = 1;
125
+ const SETUP_MAX_SCALE_PERCENT = 65535;
126
+ /**
127
+ * Clamps a schema value into the range its own Setup field can hold, since `ContentSheetPrintSettings` bounds neither a scale percentage nor a fit-to-page count from above.
128
+ *
129
+ * Clamping rather than throwing, and rather than letting the value wrap: a print scale is a presentational field, and refusing a whole workbook over an absurd one would lose its cells for no gain (the same trade the paper-code case above settles the same way). Silently wrapping is the option neither of those beats -- a scale of 65540% would land in the file as 4%, which is a different intent stated confidently.
130
+ */
131
+ function clampedSetupField(value, minimum, maximum) {
132
+ return Math.min(Math.max(Math.round(value), minimum), maximum);
133
+ }
134
+ /**
135
+ * Setup ([MS-XLS] 2.4.257): iPaperSize, iScale, iPageStart, iFitWidth, iFitHeight, the flags word, iRes, iVRes, numHdr, numFtr, iCopies.
136
+ *
137
+ * fNoPls is deliberately never set. It would declare this record's own paper size, scale, and orientation undefined -- exactly the three fields it exists here to carry -- and [MS-XLS] pairs it with a Pls record holding a printer driver's DEVMODE blob, which this writer has none of.
138
+ *
139
+ * A page size no code in the table names is written as SETUP_CUSTOM_PAPER_SIZE rather than as a standard paper it is not. The dimensions themselves are genuinely unwritable -- Setup addresses paper only by code, and [MS-XLS]'s own escape hatch for a size outside the table is that same Pls record -- so the choice is between saying "custom" and saying something false. Saying "custom" is what the spec's own iPaperSize table provides the value for, and it leaves a reader (this package's own included) free to fall back to its documented default instead of confidently reporting Letter for a page that is not Letter. This matters in practice rather than in theory: a spreadsheet converted from a slide deck or a drawing carries that source's own canvas as its page size, which is almost never a named paper, and refusing the conversion outright over a presentational field would lose the cells too.
140
+ */
141
+ function writeSetupRecord(settings) {
142
+ const paper = paperSelectionFor(settings.pageSize) ?? {
143
+ code: SETUP_CUSTOM_PAPER_SIZE,
144
+ portrait: settings.pageSize.widthPt <= settings.pageSize.heightPt
145
+ };
146
+ const fitToPages = settings.fitToPages;
147
+ const fields = {
148
+ paperCode: paper.code,
149
+ scalePercent: fitToPages === void 0 ? clampedSetupField(settings.scalePercent ?? SETUP_INACTIVE_SCALE_PERCENT, SETUP_MIN_SCALE_PERCENT, SETUP_MAX_SCALE_PERCENT) : SETUP_INACTIVE_SCALE_PERCENT,
150
+ fitWidth: fitToPages === void 0 ? SETUP_INACTIVE_FIT_PAGES : clampedSetupField(fitToPages.width, 1, SETUP_MAX_FIT_PAGES),
151
+ fitHeight: fitToPages === void 0 ? SETUP_INACTIVE_FIT_PAGES : clampedSetupField(fitToPages.height, 1, SETUP_MAX_FIT_PAGES),
152
+ leftToRight: settings.pageOrder === "overThenDown",
153
+ portrait: paper.portrait,
154
+ noPls: false,
155
+ noOrientation: false
156
+ };
157
+ const data = new RecordBuilder().u16(fields.paperCode).u16(fields.scalePercent).u16(0).u16(fields.fitWidth).u16(fields.fitHeight).u16(packSetupFlags(fields)).u16(SETUP_PRINT_RESOLUTION_DPI).u16(SETUP_PRINT_RESOLUTION_DPI).f64(SETUP_HEADER_FOOTER_MARGIN_INCHES).f64(SETUP_HEADER_FOOTER_MARGIN_INCHES).u16(SETUP_COPIES).build();
158
+ return writeRecord(161, data);
159
+ }
160
+ /** Any of the four margin records ([MS-XLS] 2.4.151, 2.4.219, 2.4.328, 2.4.27): a single Xnum stating that margin in inches. All four share one field layout, so one writer serves them all -- the mirror of workbook/sheet.ts's own single readMargin. */
161
+ function writeMarginRecord(recordType, points) {
162
+ return writeRecord(recordType, new RecordBuilder().f64(pointsToInches(points)).build());
163
+ }
164
+ /** PrintGrid ([MS-XLS] 2.4.202) and PrintRowCol ([MS-XLS] 2.4.203), each a single 16-bit boolean. */
165
+ function writeBooleanRecord(recordType, value) {
166
+ return writeRecord(recordType, new RecordBuilder().u16(value ? 1 : 0).build());
167
+ }
168
+ /** WsBool ([MS-XLS] 2.4.351). Only fFitToPage is set from real data; every other bit is written clear, which is what a sheet with no outline, no dialog behaviour, no synchronised scrolling, and no transition formula handling means -- and is exactly the set of facts ContentSheet carries nothing about. */
169
+ function writeWsBoolRecord(fitToPage) {
170
+ return writeRecord(129, new RecordBuilder().u16(fitToPage ? 256 : 0).build());
171
+ }
172
+ /**
173
+ * HorizontalPageBreaks ([MS-XLS] 2.4.142) or VerticalPageBreaks ([MS-XLS] 2.4.343): a count then that many six-byte structures, each the break's own index followed by the start and end of its extent along the other axis.
174
+ *
175
+ * Both structures share that three-field shape, so one writer serves both -- the extent's own end differs, which is what `extentEnd` carries: ContentSheetPrintSettings models a break as a whole-axis index with no extent, so every break written here runs the full width or height of BIFF8's own grid. Written in ascending index order, which is the sort [MS-XLS] requires of both arrays; the caller's own indices are sorted first rather than assumed sorted.
176
+ */
177
+ function writePageBreaksRecord(recordType, indices, extentEnd) {
178
+ const sorted = [...new Set(indices)].sort((a, b) => a - b);
179
+ const builder = new RecordBuilder().u16(sorted.length);
180
+ for (const index of sorted) builder.u16(index).u16(0).u16(extentEnd);
181
+ return writeRecord(recordType, builder.build());
182
+ }
183
+ /**
184
+ * `manualBreaks.rows`/`.columns` is not bounded to BIFF8's own grid the way a cell's own row/column is (`writeCellRecord`'s `MAX_ROW_INDEX`/`MAX_COLUMN_INDEX` guard above throws for exactly that reason). `writePageBreaksRecord` writes each index into a 16-bit field regardless (`RecordBuilder.u16` masks with `0xffff`), so an out-of-grid break would otherwise silently wrap to a plausible-looking in-grid one -- a break asked for at row 70000 landing at row 4464 -- with nothing downstream to notice.
185
+ *
186
+ * Dropped rather than clamped: unlike a print range or a repeated header band, where clamping to the grid's own last row/column is genuinely what "to the bottom of the sheet" means once the grid shrinks under it (print-names.ts's own clampToGrid), a page break is a single position, and clamping one would insert a break at the grid's own edge the caller never asked for. Dropping states the honest thing an out-of-grid break means once BIFF8's own ceiling has been applied: no such row/column exists for it to sit at.
187
+ */
188
+ function inGridBreaks(indices, maxIndex) {
189
+ return indices.filter((index) => index <= maxIndex);
190
+ }
191
+ /**
192
+ * Every print-settings record one sheet needs, in [MS-XLS] 2.1.7.20.6's own order.
193
+ *
194
+ * That order is two productions, back to back, both of which the worksheet substream places ahead of COLUMNS, Dimensions, and the cell table: `GLOBALS = ... PrintRowCol PrintGrid GridSet Guts DefaultRowHeight WsBool [Sync] [LPr] [HorizontalPageBreaks] [VerticalPageBreaks]` and `PAGESETUP = Header Footer HCenter VCenter [LeftMargin] [RightMargin] [TopMargin] [BottomMargin] [Pls *Continue] [Setup]`. The mandatory records this writer does not emit at all (the calculation-state family, GridSet, Guts, DefaultRowHeight, Header/Footer, HCenter/VCenter) are the same UI and interoperability bookkeeping it already omits everywhere else -- see this package's README -- so what remains is the optional subset that actually carries print settings, in the relative order those two productions give it.
195
+ *
196
+ * Every record here is written unconditionally, including for a sheet whose settings are exactly the Normal preset. A print setting has no "absent" spelling in ContentSheetPrintSettings -- gridlines, headers, page order, page size, and all four margins are required fields -- so there is no way to tell a sheet that asked for the preset from one that never stated anything, and writing the values out is what makes the round trip exact either way.
197
+ */
198
+ function writePrintSettingsRecords(settings) {
199
+ const pieces = [
200
+ ...writeCalculationStateRecords(),
201
+ writeBooleanRecord(42, settings.headers),
202
+ writeBooleanRecord(43, settings.gridlines),
203
+ writeWsBoolRecord(settings.fitToPages !== void 0)
204
+ ];
205
+ const breaks = settings.manualBreaks;
206
+ if (breaks !== void 0) {
207
+ const rowBreaks = inGridBreaks(breaks.rows, MAX_ROW_INDEX);
208
+ if (rowBreaks.length > 0) pieces.push(writePageBreaksRecord(27, rowBreaks, MAX_COLUMN_INDEX));
209
+ const columnBreaks = inGridBreaks(breaks.columns, MAX_COLUMN_INDEX);
210
+ if (columnBreaks.length > 0) pieces.push(writePageBreaksRecord(26, columnBreaks, MAX_ROW_INDEX));
211
+ }
212
+ pieces.push(writeMarginRecord(38, settings.margins.leftPt), writeMarginRecord(39, settings.margins.rightPt), writeMarginRecord(40, settings.margins.topPt), writeMarginRecord(41, settings.margins.bottomPt), writeSetupRecord(settings));
213
+ return pieces;
214
+ }
215
+ function cellHeader(cell, xfIndex) {
216
+ return new RecordBuilder().u16(cell.row).u16(cell.column).u16(xfIndex);
217
+ }
218
+ /** One cell record, keyed by ContentCellValue's own discriminant: Number for every numeric/temporal kind ([MS-XLS] 2.4.180 -- always the full IEEE 754 double, never the packed RK encoding, which is a compaction optimisation this writer does not implement), LabelSst for a string ([MS-XLS] 2.4.149, through the workbook-wide shared string table), BoolErr for a boolean or error value ([MS-XLS] 2.4.24), and Blank for an 'empty' cell whose formatting is the only thing it carries ([MS-XLS] 2.4.20 -- a cell header and nothing else, so the XF its ixfe names is the whole content). An unformatted empty cell never reaches here at all: written-cells.ts's own predicate filters it out upstream, since there is nothing for it to say. */
219
+ function writeCellValueRecord(cell, xfIndex, ctx) {
220
+ const value = cell.value;
221
+ switch (value.kind) {
222
+ case "number":
223
+ case "percentage":
224
+ case "currency": return writeRecord(515, cellHeader(cell, xfIndex).f64(value.value).build());
225
+ case "date": return writeRecord(515, cellHeader(cell, xfIndex).f64(isoDateToSerial(value.value, false)).build());
226
+ case "time": return writeRecord(515, cellHeader(cell, xfIndex).f64(isoTimeToSerial(value.value)).build());
227
+ case "dateTime": return writeRecord(515, cellHeader(cell, xfIndex).f64(isoDateTimeToSerial(value.value, false)).build());
228
+ case "string": {
229
+ const index = ctx.sstIndexFor(value.value);
230
+ return writeRecord(253, cellHeader(cell, xfIndex).u32(index).build());
231
+ }
232
+ case "boolean": return writeRecord(517, cellHeader(cell, xfIndex).u8(value.value ? 1 : 0).u8(0).build());
233
+ case "error": {
234
+ const code = errorCodeOf(value.value);
235
+ if (code === void 0) throw new BiffWriteError(`cell at row ${cell.row}, column ${cell.column} carries error text ${JSON.stringify(value.value)}, which is not one of the eight error values [MS-XLS] 2.5.10 defines`);
236
+ return writeRecord(517, cellHeader(cell, xfIndex).u8(code).u8(1).build());
237
+ }
238
+ case "empty":
239
+ if (!cellCarriesFormatting(cell)) throw new BiffWriteError(`internal error: writeCellValueRecord was called for the undecorated empty cell at row ${cell.row}, column ${cell.column}, which written-cells.ts's own predicate must filter out before reaching here`);
240
+ return writeRecord(513, cellHeader(cell, xfIndex).build());
241
+ }
242
+ }
243
+ /** Builds one worksheet's own substream: BOF, the print-settings records, Dimensions, ColInfo per column, Row + value-cell records per populated or declared row (in ascending row then column order), MergeCells if the sheet declares any, EOF. */
244
+ function buildWorksheetSubstream(sheet, ctx) {
245
+ for (const cell of sheet.cells) checkedCellPosition(cell);
246
+ const writtenCells = sheet.cells.filter(writesCellRecord);
247
+ const cellsByRow = /* @__PURE__ */ new Map();
248
+ for (const cell of writtenCells) {
249
+ const existing = cellsByRow.get(cell.row);
250
+ if (existing === void 0) cellsByRow.set(cell.row, [cell]);
251
+ else existing.push(cell);
252
+ }
253
+ for (const cells of cellsByRow.values()) cells.sort((a, b) => a.column - b.column);
254
+ const declaredRows = new Map(sheet.rows.map((row) => [row.index, row]));
255
+ const sortedRowIndices = [.../* @__PURE__ */ new Set([...cellsByRow.keys(), ...declaredRows.keys()])].sort((a, b) => a - b);
256
+ const pieces = [
257
+ writeRecord(RECORD_BOF, writeBofData(16)),
258
+ ...writePrintSettingsRecords(sheet.printSettings),
259
+ writeDimensionsRecord(writtenCells)
260
+ ];
261
+ for (const column of sheet.columns) pieces.push(writeColInfoRecord(column));
262
+ for (const rowIndex of sortedRowIndices) {
263
+ const cellsInRow = cellsByRow.get(rowIndex) ?? [];
264
+ pieces.push(writeRowRecord(rowIndex, cellsInRow, declaredRows.get(rowIndex)));
265
+ for (const cell of cellsInRow) {
266
+ const xfIndex = ctx.xfIndexForCell(cell);
267
+ pieces.push(writeCellValueRecord(cell, xfIndex, ctx));
268
+ }
269
+ }
270
+ const merges = mergedRangesOf(sheet.cells);
271
+ if (merges.length > 0) pieces.push(writeMergeCellsRecord(merges));
272
+ pieces.push(writeRecord(10, /* @__PURE__ */ new Uint8Array(0)));
273
+ return concatRecords(...pieces);
274
+ }
275
+ //#endregion
276
+ export { buildWorksheetSubstream };