xls-codec 4.9.0 → 4.11.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 (38) hide show
  1. package/README.md +43 -19
  2. package/dist/biff/ptg-functions.cjs +3 -0
  3. package/dist/biff/ptg-functions.d.cts +3 -1
  4. package/dist/biff/ptg-functions.d.ts +3 -1
  5. package/dist/biff/ptg-functions.js +3 -1
  6. package/dist/biff/ptg-writer.cjs +546 -0
  7. package/dist/biff/ptg-writer.d.cts +7 -0
  8. package/dist/biff/ptg-writer.d.ts +7 -0
  9. package/dist/biff/ptg-writer.js +545 -0
  10. package/dist/biff/string-writer.cjs +6 -0
  11. package/dist/biff/string-writer.d.cts +3 -1
  12. package/dist/biff/string-writer.d.ts +3 -1
  13. package/dist/biff/string-writer.js +6 -1
  14. package/dist/content.cjs +1 -1
  15. package/dist/content.d.cts +1 -1
  16. package/dist/content.d.ts +1 -1
  17. package/dist/content.js +1 -1
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/workbook/comment-writer.cjs +75 -0
  23. package/dist/workbook/comment-writer.d.cts +10 -0
  24. package/dist/workbook/comment-writer.d.ts +10 -0
  25. package/dist/workbook/comment-writer.js +74 -0
  26. package/dist/workbook/encryption.cjs +67 -20
  27. package/dist/workbook/encryption.d.cts +2 -2
  28. package/dist/workbook/encryption.d.ts +2 -2
  29. package/dist/workbook/encryption.js +68 -21
  30. package/dist/workbook/sheet-writer.cjs +50 -2
  31. package/dist/workbook/sheet-writer.d.cts +1 -1
  32. package/dist/workbook/sheet-writer.d.ts +1 -1
  33. package/dist/workbook/sheet-writer.js +50 -2
  34. package/dist/written-cells.cjs +3 -3
  35. package/dist/written-cells.d.cts +2 -2
  36. package/dist/written-cells.d.ts +2 -2
  37. package/dist/written-cells.js +3 -3
  38. package/package.json +2 -2
@@ -8,8 +8,11 @@ const require_serial = require("../serial.cjs");
8
8
  const require_biff_builder = require("../biff/builder.cjs");
9
9
  const require_biff_record_writer = require("../biff/record-writer.cjs");
10
10
  const require_biff_bof_writer = require("../biff/bof-writer.cjs");
11
+ const require_biff_string_writer = require("../biff/string-writer.cjs");
11
12
  const require_workbook_globals_writer = require("./globals-writer.cjs");
13
+ const require_biff_ptg_writer = require("../biff/ptg-writer.cjs");
12
14
  const require_written_cells = require("../written-cells.cjs");
15
+ const require_workbook_comment_writer = require("./comment-writer.cjs");
13
16
  //#region src/workbook/sheet-writer.ts
14
17
  /** 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
18
  const MAX_ROW_INDEX = 65535;
@@ -241,7 +244,50 @@ function writeCellValueRecord(cell, xfIndex, ctx) {
241
244
  return require_biff_record_writer.writeRecord(513, cellHeader(cell, xfIndex).build());
242
245
  }
243
246
  }
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. */
247
+ /** [MS-XLS] 2.4.127's own FormulaValue tag vocabulary, restated here from workbook/sheet.ts's own read-side constants (not shared across the read/write boundary, matching this package's existing per-direction module convention): a cached result that is not a plain number tags byte 6-7 as 0xFFFF and names its own kind in byte 0. */
248
+ const FORMULA_VALUE_TAGGED = 65535;
249
+ const FORMULA_VALUE_STRING = 0;
250
+ const FORMULA_VALUE_BOOLEAN = 1;
251
+ const FORMULA_VALUE_ERROR = 2;
252
+ /** The 8-byte FormulaValue field for a cached result that is not a plain number: byte 0 names which of string/boolean/error it is, byte 2 carries a boolean's own 0/1 or an error's own BIFF8 code, and the trailing u16 is the FORMULA_VALUE_TAGGED marker every reader checks for before trusting the field as a literal IEEE 754 double. */
253
+ function taggedFormulaValueBytes(tag, valueByte) {
254
+ return new require_biff_builder.RecordBuilder().u8(tag).u8(0).u8(valueByte).u8(0).u16(0).u16(FORMULA_VALUE_TAGGED).build();
255
+ }
256
+ /** The Formula record's own 8-byte FormulaValue field for `cell.value` -- a plain little-endian f64 of the cell's own numeric/temporal serial for every numeric-shaped kind, or one of the tagged shapes above for a string, boolean, or error result. `empty` has no BIFF8 encoding that this package's own reader reads back as `empty` (a tagged-blank result reads back as an empty STRING, not an empty cell -- see workbook/sheet.ts's own taggedFormulaValue), so a formula whose value resolves to `empty` is refused outright rather than written as something the round trip would silently change the kind of. */
257
+ function formulaValueBytes(cell) {
258
+ const value = cell.value;
259
+ switch (value.kind) {
260
+ case "number":
261
+ case "percentage":
262
+ case "currency": return new require_biff_builder.RecordBuilder().f64(value.value).build();
263
+ case "date": return new require_biff_builder.RecordBuilder().f64(require_serial.isoDateToSerial(value.value, false)).build();
264
+ case "time": return new require_biff_builder.RecordBuilder().f64(require_serial.isoTimeToSerial(value.value)).build();
265
+ case "dateTime": return new require_biff_builder.RecordBuilder().f64(require_serial.isoDateTimeToSerial(value.value, false)).build();
266
+ case "boolean": return taggedFormulaValueBytes(FORMULA_VALUE_BOOLEAN, value.value ? 1 : 0);
267
+ case "error": {
268
+ const code = require_biff_errors.errorCodeOf(value.value);
269
+ if (code === void 0) throw new require_biff_write_errors.BiffWriteError(`cell at row ${cell.row}, column ${cell.column} carries a formula whose cached result is error text ${JSON.stringify(value.value)}, which is not one of the eight error values [MS-XLS] 2.5.10 defines`);
270
+ return taggedFormulaValueBytes(FORMULA_VALUE_ERROR, code);
271
+ }
272
+ case "string": return taggedFormulaValueBytes(FORMULA_VALUE_STRING, 0);
273
+ case "empty": throw new require_biff_write_errors.BiffWriteError(`cell at row ${cell.row}, column ${cell.column} carries a formula whose value resolves to an empty cell, which this writer cannot express as a Formula record's cached result`);
274
+ }
275
+ }
276
+ /** Formula ([MS-XLS] 2.4.127): a Cell, the 8-byte FormulaValue above, a flags word and a 4-byte calculation cache this writer has no data for (both written zero -- see the module comment on RECORD_CALCCOUNT and friends for the same "nothing this schema models" reasoning), then a CellParsedFormula -- a two-byte cce and that many bytes of compiled Ptg tokens from biff/ptg-writer.ts's own compileFormulaText. Never carries an RgbExtra trailer: this writer's formula compiler refuses any construct (an array-constant literal, a shared/array formula) that would need one, so cce always accounts for the whole of rgce. A string-kind result is followed by a String record ([MS-XLS] 2.4.268) carrying the cached text, exactly as workbook/sheet.ts's own reader expects to find it. */
277
+ function writeFormulaRecords(cell, xfIndex) {
278
+ const formula = cell.formula;
279
+ if (formula === void 0) throw new require_biff_write_errors.BiffWriteError(`internal error: writeFormulaRecords was called for the cell at row ${cell.row}, column ${cell.column}, which carries no formula`);
280
+ const rgce = require_biff_ptg_writer.compileFormulaText(formula);
281
+ const data = cellHeader(cell, xfIndex).bytes(formulaValueBytes(cell)).u16(0).u32(0).u16(rgce.length).bytes(rgce).build();
282
+ const records = [require_biff_record_writer.writeRecord(6, data)];
283
+ if (cell.value.kind === "string") records.push(require_biff_record_writer.writeRecord(519, require_biff_string_writer.writeXLUnicodeString(cell.value.value)));
284
+ return records;
285
+ }
286
+ /** The one or two records one cell contributes to the worksheet substream's cell table -- a Formula record (plus its String result, for a string-kind cell) when the cell carries a formula, the plain value record from writeCellValueRecord otherwise. */
287
+ function writeCellRecords(cell, xfIndex, ctx) {
288
+ return cell.formula !== void 0 ? writeFormulaRecords(cell, xfIndex) : [writeCellValueRecord(cell, xfIndex, ctx)];
289
+ }
290
+ /** 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, comment records for cells carrying one, EOF. */
245
291
  function buildWorksheetSubstream(sheet, ctx) {
246
292
  for (const cell of sheet.cells) checkedCellPosition(cell);
247
293
  const writtenCells = sheet.cells.filter(require_written_cells.writesCellRecord);
@@ -265,11 +311,13 @@ function buildWorksheetSubstream(sheet, ctx) {
265
311
  pieces.push(writeRowRecord(rowIndex, cellsInRow, declaredRows.get(rowIndex)));
266
312
  for (const cell of cellsInRow) {
267
313
  const xfIndex = ctx.xfIndexForCell(cell);
268
- pieces.push(writeCellValueRecord(cell, xfIndex, ctx));
314
+ pieces.push(...writeCellRecords(cell, xfIndex, ctx));
269
315
  }
270
316
  }
271
317
  const merges = mergedRangesOf(sheet.cells);
272
318
  if (merges.length > 0) pieces.push(writeMergeCellsRecord(merges));
319
+ const commentedCells = sheet.cells.filter((cell) => cell.comment !== void 0);
320
+ if (commentedCells.length > 0) pieces.push(...require_workbook_comment_writer.writeSheetComments(commentedCells));
273
321
  pieces.push(require_biff_record_writer.writeRecord(10, /* @__PURE__ */ new Uint8Array(0)));
274
322
  return require_biff_record_writer.concatRecords(...pieces);
275
323
  }
@@ -6,7 +6,7 @@ interface SheetWriteContext {
6
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
7
  sstIndexFor(text: string): number;
8
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. */
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, comment records for cells carrying one, EOF. */
10
10
  declare function buildWorksheetSubstream(sheet: ContentSheet, ctx: SheetWriteContext): Uint8Array<ArrayBuffer>;
11
11
  //#endregion
12
12
  export { SheetWriteContext, buildWorksheetSubstream };
@@ -6,7 +6,7 @@ interface SheetWriteContext {
6
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
7
  sstIndexFor(text: string): number;
8
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. */
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, comment records for cells carrying one, EOF. */
10
10
  declare function buildWorksheetSubstream(sheet: ContentSheet, ctx: SheetWriteContext): Uint8Array<ArrayBuffer>;
11
11
  //#endregion
12
12
  export { SheetWriteContext, buildWorksheetSubstream };
@@ -7,8 +7,11 @@ import { isoDateTimeToSerial, isoDateToSerial, isoTimeToSerial } from "../serial
7
7
  import { RecordBuilder } from "../biff/builder.js";
8
8
  import { concatRecords, writeRecord } from "../biff/record-writer.js";
9
9
  import { writeBofData } from "../biff/bof-writer.js";
10
+ import { writeXLUnicodeString } from "../biff/string-writer.js";
10
11
  import { GENERAL_CELL_XF_INDEX } from "./globals-writer.js";
12
+ import { compileFormulaText } from "../biff/ptg-writer.js";
11
13
  import { cellCarriesFormatting, writesCellRecord } from "../written-cells.js";
14
+ import { writeSheetComments } from "./comment-writer.js";
12
15
  //#region src/workbook/sheet-writer.ts
13
16
  /** 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
17
  const MAX_ROW_INDEX = 65535;
@@ -240,7 +243,50 @@ function writeCellValueRecord(cell, xfIndex, ctx) {
240
243
  return writeRecord(513, cellHeader(cell, xfIndex).build());
241
244
  }
242
245
  }
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. */
246
+ /** [MS-XLS] 2.4.127's own FormulaValue tag vocabulary, restated here from workbook/sheet.ts's own read-side constants (not shared across the read/write boundary, matching this package's existing per-direction module convention): a cached result that is not a plain number tags byte 6-7 as 0xFFFF and names its own kind in byte 0. */
247
+ const FORMULA_VALUE_TAGGED = 65535;
248
+ const FORMULA_VALUE_STRING = 0;
249
+ const FORMULA_VALUE_BOOLEAN = 1;
250
+ const FORMULA_VALUE_ERROR = 2;
251
+ /** The 8-byte FormulaValue field for a cached result that is not a plain number: byte 0 names which of string/boolean/error it is, byte 2 carries a boolean's own 0/1 or an error's own BIFF8 code, and the trailing u16 is the FORMULA_VALUE_TAGGED marker every reader checks for before trusting the field as a literal IEEE 754 double. */
252
+ function taggedFormulaValueBytes(tag, valueByte) {
253
+ return new RecordBuilder().u8(tag).u8(0).u8(valueByte).u8(0).u16(0).u16(FORMULA_VALUE_TAGGED).build();
254
+ }
255
+ /** The Formula record's own 8-byte FormulaValue field for `cell.value` -- a plain little-endian f64 of the cell's own numeric/temporal serial for every numeric-shaped kind, or one of the tagged shapes above for a string, boolean, or error result. `empty` has no BIFF8 encoding that this package's own reader reads back as `empty` (a tagged-blank result reads back as an empty STRING, not an empty cell -- see workbook/sheet.ts's own taggedFormulaValue), so a formula whose value resolves to `empty` is refused outright rather than written as something the round trip would silently change the kind of. */
256
+ function formulaValueBytes(cell) {
257
+ const value = cell.value;
258
+ switch (value.kind) {
259
+ case "number":
260
+ case "percentage":
261
+ case "currency": return new RecordBuilder().f64(value.value).build();
262
+ case "date": return new RecordBuilder().f64(isoDateToSerial(value.value, false)).build();
263
+ case "time": return new RecordBuilder().f64(isoTimeToSerial(value.value)).build();
264
+ case "dateTime": return new RecordBuilder().f64(isoDateTimeToSerial(value.value, false)).build();
265
+ case "boolean": return taggedFormulaValueBytes(FORMULA_VALUE_BOOLEAN, value.value ? 1 : 0);
266
+ case "error": {
267
+ const code = errorCodeOf(value.value);
268
+ if (code === void 0) throw new BiffWriteError(`cell at row ${cell.row}, column ${cell.column} carries a formula whose cached result is error text ${JSON.stringify(value.value)}, which is not one of the eight error values [MS-XLS] 2.5.10 defines`);
269
+ return taggedFormulaValueBytes(FORMULA_VALUE_ERROR, code);
270
+ }
271
+ case "string": return taggedFormulaValueBytes(FORMULA_VALUE_STRING, 0);
272
+ case "empty": throw new BiffWriteError(`cell at row ${cell.row}, column ${cell.column} carries a formula whose value resolves to an empty cell, which this writer cannot express as a Formula record's cached result`);
273
+ }
274
+ }
275
+ /** Formula ([MS-XLS] 2.4.127): a Cell, the 8-byte FormulaValue above, a flags word and a 4-byte calculation cache this writer has no data for (both written zero -- see the module comment on RECORD_CALCCOUNT and friends for the same "nothing this schema models" reasoning), then a CellParsedFormula -- a two-byte cce and that many bytes of compiled Ptg tokens from biff/ptg-writer.ts's own compileFormulaText. Never carries an RgbExtra trailer: this writer's formula compiler refuses any construct (an array-constant literal, a shared/array formula) that would need one, so cce always accounts for the whole of rgce. A string-kind result is followed by a String record ([MS-XLS] 2.4.268) carrying the cached text, exactly as workbook/sheet.ts's own reader expects to find it. */
276
+ function writeFormulaRecords(cell, xfIndex) {
277
+ const formula = cell.formula;
278
+ if (formula === void 0) throw new BiffWriteError(`internal error: writeFormulaRecords was called for the cell at row ${cell.row}, column ${cell.column}, which carries no formula`);
279
+ const rgce = compileFormulaText(formula);
280
+ const data = cellHeader(cell, xfIndex).bytes(formulaValueBytes(cell)).u16(0).u32(0).u16(rgce.length).bytes(rgce).build();
281
+ const records = [writeRecord(6, data)];
282
+ if (cell.value.kind === "string") records.push(writeRecord(519, writeXLUnicodeString(cell.value.value)));
283
+ return records;
284
+ }
285
+ /** The one or two records one cell contributes to the worksheet substream's cell table -- a Formula record (plus its String result, for a string-kind cell) when the cell carries a formula, the plain value record from writeCellValueRecord otherwise. */
286
+ function writeCellRecords(cell, xfIndex, ctx) {
287
+ return cell.formula !== void 0 ? writeFormulaRecords(cell, xfIndex) : [writeCellValueRecord(cell, xfIndex, ctx)];
288
+ }
289
+ /** 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, comment records for cells carrying one, EOF. */
244
290
  function buildWorksheetSubstream(sheet, ctx) {
245
291
  for (const cell of sheet.cells) checkedCellPosition(cell);
246
292
  const writtenCells = sheet.cells.filter(writesCellRecord);
@@ -264,11 +310,13 @@ function buildWorksheetSubstream(sheet, ctx) {
264
310
  pieces.push(writeRowRecord(rowIndex, cellsInRow, declaredRows.get(rowIndex)));
265
311
  for (const cell of cellsInRow) {
266
312
  const xfIndex = ctx.xfIndexForCell(cell);
267
- pieces.push(writeCellValueRecord(cell, xfIndex, ctx));
313
+ pieces.push(...writeCellRecords(cell, xfIndex, ctx));
268
314
  }
269
315
  }
270
316
  const merges = mergedRangesOf(sheet.cells);
271
317
  if (merges.length > 0) pieces.push(writeMergeCellsRecord(merges));
318
+ const commentedCells = sheet.cells.filter((cell) => cell.comment !== void 0);
319
+ if (commentedCells.length > 0) pieces.push(...writeSheetComments(commentedCells));
272
320
  pieces.push(writeRecord(10, /* @__PURE__ */ new Uint8Array(0)));
273
321
  return concatRecords(...pieces);
274
322
  }
@@ -14,12 +14,12 @@ function cellCarriesFormatting(cell) {
14
14
  return borders.left !== void 0 || borders.right !== void 0 || borders.top !== void 0 || borders.bottom !== void 0;
15
15
  }
16
16
  /**
17
- * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
17
+ * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, a Formula record for anything carrying a formula, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
18
18
  *
19
- * An unformatted empty cell is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them.
19
+ * An unformatted empty cell with no formula is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them. A formula is the identical case one level up: it lives only in a Formula record, so a cell carrying one is never dropped regardless of what its own cached `value` resolves to, even where that value alone would otherwise not have earned a record.
20
20
  */
21
21
  function writesCellRecord(cell) {
22
- return cell.value.kind !== "empty" || cellCarriesFormatting(cell);
22
+ return cell.value.kind !== "empty" || cellCarriesFormatting(cell) || cell.formula !== void 0;
23
23
  }
24
24
  //#endregion
25
25
  exports.cellCarriesFormatting = cellCarriesFormatting;
@@ -7,9 +7,9 @@ import { ContentSheetCell } from "document-schema.js";
7
7
  */
8
8
  declare function cellCarriesFormatting(cell: ContentSheetCell): boolean;
9
9
  /**
10
- * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
10
+ * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, a Formula record for anything carrying a formula, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
11
11
  *
12
- * An unformatted empty cell is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them.
12
+ * An unformatted empty cell with no formula is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them. A formula is the identical case one level up: it lives only in a Formula record, so a cell carrying one is never dropped regardless of what its own cached `value` resolves to, even where that value alone would otherwise not have earned a record.
13
13
  */
14
14
  declare function writesCellRecord(cell: ContentSheetCell): boolean;
15
15
  //#endregion
@@ -7,9 +7,9 @@ import { ContentSheetCell } from "document-schema.js";
7
7
  */
8
8
  declare function cellCarriesFormatting(cell: ContentSheetCell): boolean;
9
9
  /**
10
- * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
10
+ * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, a Formula record for anything carrying a formula, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
11
11
  *
12
- * An unformatted empty cell is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them.
12
+ * An unformatted empty cell with no formula is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them. A formula is the identical case one level up: it lives only in a Formula record, so a cell carrying one is never dropped regardless of what its own cached `value` resolves to, even where that value alone would otherwise not have earned a record.
13
13
  */
14
14
  declare function writesCellRecord(cell: ContentSheetCell): boolean;
15
15
  //#endregion
@@ -13,12 +13,12 @@ function cellCarriesFormatting(cell) {
13
13
  return borders.left !== void 0 || borders.right !== void 0 || borders.top !== void 0 || borders.bottom !== void 0;
14
14
  }
15
15
  /**
16
- * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
16
+ * Whether the writer emits a cell record for this cell: a value record for anything carrying a value, a Formula record for anything carrying a formula, or a Blank record for an `empty`-kind cell whose formatting is the only thing it has to say.
17
17
  *
18
- * An unformatted empty cell is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them.
18
+ * An unformatted empty cell with no formula is written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell, and a merged range's empty anchor is reconstructed from the MergeCells record alone. A formatted one is not that case -- its fill, borders, and alignment live only in the XF a cell record points at, so writing nothing for it discards them. A formula is the identical case one level up: it lives only in a Formula record, so a cell carrying one is never dropped regardless of what its own cached `value` resolves to, even where that value alone would otherwise not have earned a record.
19
19
  */
20
20
  function writesCellRecord(cell) {
21
- return cell.value.kind !== "empty" || cellCarriesFormatting(cell);
21
+ return cell.value.kind !== "empty" || cellCarriesFormatting(cell) || cell.formula !== void 0;
22
22
  }
23
23
  //#endregion
24
24
  export { cellCarriesFormatting, writesCellRecord };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xls-codec",
3
- "version": "4.9.0",
3
+ "version": "4.11.0",
4
4
  "description": "Hand-written reader for the legacy Excel Binary File Format (.xls, BIFF8) as specified by [MS-XLS], mapping a workbook's record stream onto the shared document-schema.js spreadsheet model - the .xls codec for the documents.js family.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "packageManager": "pnpm@11.6.0",
72
72
  "dependencies": {
73
- "archive-codec": "1.9.2",
73
+ "archive-codec": "1.10.0",
74
74
  "document-schema.js": "7.5.0",
75
75
  "excel-number-format": "1.1.1"
76
76
  },