@xlsxflow/core 1.1.1 → 1.1.3

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.
package/dist/index.d.cts CHANGED
@@ -372,6 +372,7 @@ interface WorkbookParts {
372
372
  declare function resolveWorkbookParts(readText: (path: string) => Promise<string>): Promise<WorkbookParts>;
373
373
  declare function partRelationships(readText: (path: string) => Promise<string>, partPath: string): Promise<Relationship[]>;
374
374
  declare function decodeXString(s: string): string;
375
+ declare function checkCellText(s: string, ref: string): string;
375
376
  declare function colIndex(ref: string): number;
376
377
  declare function colLetter(index: number): string;
377
378
  type RefRole = 'single' | 'start' | 'end';
@@ -579,6 +580,28 @@ declare class CfbReader {
579
580
  read(path: string): Uint8Array | undefined;
580
581
  }
581
582
 
583
+ declare class StylePatcher {
584
+ private xml;
585
+ private p;
586
+ private fonts;
587
+ private fills;
588
+ private borders;
589
+ private xfs;
590
+ private numFmts;
591
+ private nextNumFmt;
592
+ private added;
593
+ private memo;
594
+ constructor(xml: string);
595
+ get changed(): boolean;
596
+ patch(base: number, style: CellStyle): number;
597
+ dateFormat(base: number, d: Date): number;
598
+ toXml(): string;
599
+ private existing;
600
+ private add;
601
+ private items;
602
+ private prefix;
603
+ }
604
+
582
605
  type Placement = {
583
606
  range: string;
584
607
  } | {
@@ -597,5 +620,5 @@ declare const XlsxFlow: {
597
620
  readFile(filePath: string, options?: ParseOptions): Promise<ParseResult>;
598
621
  };
599
622
 
600
- export { CfbReader, ConditionalFormatter, DRAWING_NS, FormulaEngine, OdsWriter, SheetEditor, SheetReader, SheetWriter, StyleEngine, XlsxFlow, ZipRandomAccessParser, ZipStreamWriter, anchorXml, colIndex, colLetter, crc32, createBlobReader, createFileReader, createXmlStreamParser, dateToSerial, decodeXString, drawingPartXml, encodeXString, isCfb, legacyPasswordHash, mapFormulaRefs, parseCsv, parseWorksheet, partRelationships, readSharedStrings, recalcOnOpen, resolveWorkbookParts, sheetToJson, shiftFormula, streamToCsv };
623
+ export { CfbReader, ConditionalFormatter, DRAWING_NS, FormulaEngine, OdsWriter, SheetEditor, SheetReader, SheetWriter, StyleEngine, StylePatcher, XlsxFlow, ZipRandomAccessParser, ZipStreamWriter, anchorXml, checkCellText, colIndex, colLetter, crc32, createBlobReader, createFileReader, createXmlStreamParser, dateToSerial, decodeXString, drawingPartXml, encodeXString, isCfb, legacyPasswordHash, mapFormulaRefs, parseCsv, parseWorksheet, partRelationships, readSharedStrings, recalcOnOpen, resolveWorkbookParts, sheetToJson, shiftFormula, streamToCsv };
601
624
  export type { BorderSide, CellAlignment, CellBorder, CellComment, CellEdit, CellFill, CellFont, CellStyle, CellValue$1 as CellValue, CfbEntry, ColumnOptions, ComparisonOperator, ConditionalFormat, ConditionalFormatRule, CsvOptions, DataValidation, DefinedName, GradientFill, HighlightStyle, OdsSheetOptions, OdsWriterOptions, PageSetup, ParseOptions, Placement, RandomAccessReader, RefMapper, RefRole, Relationship, RichTextRun, Row, RowData, RowOptions, SheetComment, SheetImage, SheetMetadata, SheetOptions, SheetProtection, SheetView, StyledCell, TableOptions, WorkbookInfo, WorkbookParts, WorkbookProperties, WriterOptions };
package/dist/index.d.ts CHANGED
@@ -372,6 +372,7 @@ interface WorkbookParts {
372
372
  declare function resolveWorkbookParts(readText: (path: string) => Promise<string>): Promise<WorkbookParts>;
373
373
  declare function partRelationships(readText: (path: string) => Promise<string>, partPath: string): Promise<Relationship[]>;
374
374
  declare function decodeXString(s: string): string;
375
+ declare function checkCellText(s: string, ref: string): string;
375
376
  declare function colIndex(ref: string): number;
376
377
  declare function colLetter(index: number): string;
377
378
  type RefRole = 'single' | 'start' | 'end';
@@ -579,6 +580,28 @@ declare class CfbReader {
579
580
  read(path: string): Uint8Array | undefined;
580
581
  }
581
582
 
583
+ declare class StylePatcher {
584
+ private xml;
585
+ private p;
586
+ private fonts;
587
+ private fills;
588
+ private borders;
589
+ private xfs;
590
+ private numFmts;
591
+ private nextNumFmt;
592
+ private added;
593
+ private memo;
594
+ constructor(xml: string);
595
+ get changed(): boolean;
596
+ patch(base: number, style: CellStyle): number;
597
+ dateFormat(base: number, d: Date): number;
598
+ toXml(): string;
599
+ private existing;
600
+ private add;
601
+ private items;
602
+ private prefix;
603
+ }
604
+
582
605
  type Placement = {
583
606
  range: string;
584
607
  } | {
@@ -597,5 +620,5 @@ declare const XlsxFlow: {
597
620
  readFile(filePath: string, options?: ParseOptions): Promise<ParseResult>;
598
621
  };
599
622
 
600
- export { CfbReader, ConditionalFormatter, DRAWING_NS, FormulaEngine, OdsWriter, SheetEditor, SheetReader, SheetWriter, StyleEngine, XlsxFlow, ZipRandomAccessParser, ZipStreamWriter, anchorXml, colIndex, colLetter, crc32, createBlobReader, createFileReader, createXmlStreamParser, dateToSerial, decodeXString, drawingPartXml, encodeXString, isCfb, legacyPasswordHash, mapFormulaRefs, parseCsv, parseWorksheet, partRelationships, readSharedStrings, recalcOnOpen, resolveWorkbookParts, sheetToJson, shiftFormula, streamToCsv };
623
+ export { CfbReader, ConditionalFormatter, DRAWING_NS, FormulaEngine, OdsWriter, SheetEditor, SheetReader, SheetWriter, StyleEngine, StylePatcher, XlsxFlow, ZipRandomAccessParser, ZipStreamWriter, anchorXml, checkCellText, colIndex, colLetter, crc32, createBlobReader, createFileReader, createXmlStreamParser, dateToSerial, decodeXString, drawingPartXml, encodeXString, isCfb, legacyPasswordHash, mapFormulaRefs, parseCsv, parseWorksheet, partRelationships, readSharedStrings, recalcOnOpen, resolveWorkbookParts, sheetToJson, shiftFormula, streamToCsv };
601
624
  export type { BorderSide, CellAlignment, CellBorder, CellComment, CellEdit, CellFill, CellFont, CellStyle, CellValue$1 as CellValue, CfbEntry, ColumnOptions, ComparisonOperator, ConditionalFormat, ConditionalFormatRule, CsvOptions, DataValidation, DefinedName, GradientFill, HighlightStyle, OdsSheetOptions, OdsWriterOptions, PageSetup, ParseOptions, Placement, RandomAccessReader, RefMapper, RefRole, Relationship, RichTextRun, Row, RowData, RowOptions, SheetComment, SheetImage, SheetMetadata, SheetOptions, SheetProtection, SheetView, StyledCell, TableOptions, WorkbookInfo, WorkbookParts, WorkbookProperties, WriterOptions };
package/dist/index.mjs CHANGED
@@ -348,7 +348,16 @@ async function sheetToJson(parseResult, headerRowIndex = 0) {
348
348
  if (skip-- > 0)
349
349
  continue;
350
350
  if (headers.length === 0) {
351
- headers = row.cells.map((c, i) => (c !== null && c !== undefined ? String(c) : `Column${i + 1}`));
351
+ const seen = new Set();
352
+ headers = row.cells.map((c, i) => {
353
+ const base = c !== null && c !== undefined ? String(c) : `Column${i + 1}`;
354
+ // A repeated header gets a suffix ("Name_2") so its column isn't lost
355
+ let name = base;
356
+ for (let n = 2; seen.has(name); n++)
357
+ name = `${base}_${n}`;
358
+ seen.add(name);
359
+ return name;
360
+ });
352
361
  continue;
353
362
  }
354
363
  const obj = {};
@@ -453,6 +462,21 @@ function decodeXString(s) {
453
462
  // Excel's sheet size (A..XFD, 1..1048576)
454
463
  const MAX_COLUMNS$1 = 16384;
455
464
  const MAX_ROWS$1 = 1048576;
465
+ // Excel's limit on the characters in one cell
466
+ const MAX_CELL_TEXT = 32767;
467
+ function checkCellText(s, ref) {
468
+ if (s.length > MAX_CELL_TEXT)
469
+ throw new Error(`Cell ${ref} has ${s.length} characters, more than Excel's ${MAX_CELL_TEXT}.`);
470
+ return s;
471
+ }
472
+ // Colours are ARGB hex ("FFFF0000"); "FF0000" and "#FF0000" get an opaque alpha. Anything else
473
+ // makes Excel repair the file.
474
+ function argb(c) {
475
+ const m = /^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.exec(String(c));
476
+ if (!m)
477
+ throw new Error(`Invalid colour "${c}": use ARGB hex such as "FFFF0000".`);
478
+ return (m[1].length === 6 ? 'FF' + m[1] : m[1]).toUpperCase();
479
+ }
456
480
  // 0-based column of letters like "BC" (or a reference like "BC12"); -1 when there are none
457
481
  function colIndex(ref) {
458
482
  const match = ref.match(/[A-Za-z]+/);
@@ -552,6 +576,8 @@ function encodeXString(s) {
552
576
  .replace(/_(?=x[0-9A-Fa-f]{4}_)/g, '_x005F_')
553
577
  .replace(/[\x00-\x08\x0B\x0C\r\x0E-\x1F]/g, c => `_x${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}_`);
554
578
  }
579
+ // The format SheetWriter gives a date without one: the time only when there is one
580
+ const defaultDateFormat = (d) => d.getTime() % 86400000 === 0 ? 'yyyy-mm-dd' : 'yyyy-mm-dd hh:mm:ss';
555
581
  // True when a number format renders dates/times. Ignores quoted text, escapes, colours and
556
582
  // locale tags, so "0 \"days\"", "[Red]0.0" and "#,##0 \"USD\"" are not mistaken for dates.
557
583
  function isDateFormatCode(code) {
@@ -1448,7 +1474,8 @@ function parseWorksheet(xmlTokenStream, sharedStrings, styles, is1904 = false, o
1448
1474
  }
1449
1475
  else if (currentCellType === 's') {
1450
1476
  const index = parseInt(currentCellValue, 10);
1451
- resolvedValue = sharedStrings.get(index) ?? currentCellValue;
1477
+ // An index past the table points nowhere: the cell is empty
1478
+ resolvedValue = sharedStrings.get(index) ?? null;
1452
1479
  }
1453
1480
  else if (currentCellType === 'b') {
1454
1481
  resolvedValue = currentCellValue === '1' || currentCellValue === 'true';
@@ -1622,9 +1649,10 @@ async function readSheetImages(readText, readBytes, sheetPath) {
1622
1649
  const escapeXml$4 = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1623
1650
  const cellPos = (ref) => {
1624
1651
  const m = /^\$?([A-Za-z]{1,3})\$?(\d+)$/.exec(ref.trim());
1625
- if (!m)
1652
+ const col = m ? colIndex(m[1]) : -1, row = m ? parseInt(m[2], 10) - 1 : -1;
1653
+ if (col < 0 || col >= MAX_COLUMNS$1 || row < 0 || row >= MAX_ROWS$1)
1626
1654
  throw new Error(`Invalid anchor cell "${ref}".`);
1627
- return { col: colIndex(m[1]), row: parseInt(m[2], 10) - 1 };
1655
+ return { col, row };
1628
1656
  };
1629
1657
  const marker = (tag, { col, row }) => `<xdr:${tag}><xdr:col>${col}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>${row}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:${tag}>`;
1630
1658
  const DRAWING_NS = 'xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" ' +
@@ -1636,9 +1664,11 @@ const DRAWING_NS = 'xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/
1636
1664
  function anchorXml(place, natural, body, attrs = '') {
1637
1665
  if ('range' in place) {
1638
1666
  const [from, to = from] = place.range.split(':');
1639
- const end = cellPos(to);
1640
- // The object fills the range: it ends at the far edge of the last cell
1641
- return `<xdr:twoCellAnchor editAs="oneCell"${attrs}>${marker('from', cellPos(from))}${marker('to', { col: end.col + 1, row: end.row + 1 })}${body}<xdr:clientData/></xdr:twoCellAnchor>`;
1667
+ const a = cellPos(from), b = cellPos(to);
1668
+ // The object fills the range, given either way round: it ends at the far edge of the last cell
1669
+ const start = { col: Math.min(a.col, b.col), row: Math.min(a.row, b.row) };
1670
+ const end = { col: Math.max(a.col, b.col) + 1, row: Math.max(a.row, b.row) + 1 };
1671
+ return `<xdr:twoCellAnchor editAs="oneCell"${attrs}>${marker('from', start)}${marker('to', end)}${body}<xdr:clientData/></xdr:twoCellAnchor>`;
1642
1672
  }
1643
1673
  const { width: w, height: h } = natural;
1644
1674
  const width = place.width ?? (place.height ? w * place.height / h : w);
@@ -2623,22 +2653,22 @@ function fontXml(font, nameTag = 'name') {
2623
2653
  if (font.size)
2624
2654
  xml += `<sz val="${escapeXml$3(font.size)}"/>`;
2625
2655
  if (font.color)
2626
- xml += `<color rgb="${escapeXml$3(font.color)}"/>`;
2656
+ xml += `<color rgb="${argb(font.color)}"/>`;
2627
2657
  if (font.name)
2628
2658
  xml += `<${nameTag} val="${escapeXml$3(font.name)}"/>`;
2629
2659
  return xml;
2630
2660
  }
2631
- const borderSideXml = (tag, s) => s ? `<${tag} style="${escapeXml$3(s.style)}">${s.color ? `<color rgb="${escapeXml$3(s.color)}"/>` : ''}</${tag}>` : `<${tag}/>`;
2661
+ const borderSideXml = (tag, s) => s ? `<${tag} style="${escapeXml$3(s.style)}">${s.color ? `<color rgb="${argb(s.color)}"/>` : ''}</${tag}>` : `<${tag}/>`;
2632
2662
  function borderXml(b) {
2633
2663
  const side = borderSideXml;
2634
2664
  return `<border>${side('left', b.left)}${side('right', b.right)}${side('top', b.top)}${side('bottom', b.bottom)}<diagonal/></border>`;
2635
2665
  }
2636
2666
  function fillXml(fill) {
2637
2667
  if (fill.type === 'gradient') {
2638
- const stops = fill.stops.map(s => `<stop position="${s.position}"><color rgb="${escapeXml$3(s.color)}"/></stop>`).join('');
2668
+ const stops = fill.stops.map(s => `<stop position="${s.position}"><color rgb="${argb(s.color)}"/></stop>`).join('');
2639
2669
  return `<fill><gradientFill degree="${escapeXml$3(fill.degree ?? 0)}">${stops}</gradientFill></fill>`;
2640
2670
  }
2641
- return `<fill><patternFill patternType="solid"><fgColor rgb="${escapeXml$3(fill.fgColor)}"/></patternFill></fill>`;
2671
+ return `<fill><patternFill patternType="solid"><fgColor rgb="${argb(fill.fgColor)}"/></patternFill></fill>`;
2642
2672
  }
2643
2673
  // Attributes of an <alignment> element
2644
2674
  function alignmentAttrs(alignment) {
@@ -2680,7 +2710,7 @@ class StyleEngine {
2680
2710
  xml += `<numFmt numFmtId="${this.registerNumFmt(style.numFmt)}" formatCode="${escapeXml$3(style.numFmt)}"/>`;
2681
2711
  if (style.fill?.type === 'solid') {
2682
2712
  // A differential solid fill takes its colour from bgColor
2683
- const c = escapeXml$3(style.fill.fgColor);
2713
+ const c = argb(style.fill.fgColor);
2684
2714
  xml += `<fill><patternFill patternType="solid"><fgColor rgb="${c}"/><bgColor rgb="${c}"/></patternFill></fill>`;
2685
2715
  }
2686
2716
  if (style.border)
@@ -2808,7 +2838,7 @@ ${this.dxfs.size ? `<dxfs count="${this.dxfs.size}">${[...this.dxfs.keys()].map(
2808
2838
  function escapeXml$2(val) {
2809
2839
  return String(val).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2810
2840
  }
2811
- const color = (rgb) => `<color rgb="${escapeXml$2(rgb)}"/>`;
2841
+ const color = (rgb) => `<color rgb="${argb(rgb)}"/>`;
2812
2842
  const bound = (type, value) => value === undefined ? `<cfvo type="${type}"/>` : `<cfvo type="num" val="${escapeXml$2(value)}"/>`;
2813
2843
  const formula = (f) => `<formula>${escapeXml$2(String(f).replace(/^=/, ''))}</formula>`;
2814
2844
  // Excel string literal inside a formula
@@ -3360,7 +3390,6 @@ function escapeXml$1(val) {
3360
3390
  function isStyledCell$1(v) {
3361
3391
  return typeof v === 'object' && v !== null && 'value' in v;
3362
3392
  }
3363
- const DAY_MS = 86400000;
3364
3393
  // "A1:C10" -> "$A$1:$C$10"
3365
3394
  const absoluteRef = (ref) => ref.replace(/([A-Za-z]+)(\d+)/g, '$$$1$$$2');
3366
3395
  function stringXml(s) {
@@ -3638,7 +3667,7 @@ class SheetWriter {
3638
3667
  const page = options.pageSetup;
3639
3668
  const fitToPage = page && (page.fitToWidth !== undefined || page.fitToHeight !== undefined);
3640
3669
  const sheetPr = options.tabColor || fitToPage
3641
- ? `<sheetPr>${options.tabColor ? `<tabColor rgb="${escapeXml$1(options.tabColor)}"/>` : ''}${fitToPage ? '<pageSetUpPr fitToPage="1"/>' : ''}</sheetPr>`
3670
+ ? `<sheetPr>${options.tabColor ? `<tabColor rgb="${argb(options.tabColor)}"/>` : ''}${fitToPage ? '<pageSetUpPr fitToPage="1"/>' : ''}</sheetPr>`
3642
3671
  : '';
3643
3672
  const sheetFormatPr = outlineRow || outlineCol
3644
3673
  ? `<sheetFormatPr defaultRowHeight="15"${outlineRow ? ` outlineLevelRow="${outlineRow}"` : ''}${outlineCol ? ` outlineLevelCol="${outlineCol}"` : ''}/>`
@@ -3676,6 +3705,11 @@ class SheetWriter {
3676
3705
  if (done)
3677
3706
  break;
3678
3707
  const rowNum = ri + 1;
3708
+ // Past Excel's sheet size the file is damaged, so refuse it instead
3709
+ if (rowNum > MAX_ROWS$1)
3710
+ throw new Error(`Row ${rowNum} is past Excel's last row (${MAX_ROWS$1}).`);
3711
+ if (row.length > MAX_COLUMNS$1)
3712
+ throw new Error(`Row ${rowNum} has ${row.length} cells, more than Excel's ${MAX_COLUMNS$1} columns.`);
3679
3713
  chunkStr += optionRowsBefore(rowNum);
3680
3714
  if (rowOptions[nextRowOption]?.[0] === rowNum)
3681
3715
  nextRowOption++;
@@ -3686,7 +3720,7 @@ class SheetWriter {
3686
3720
  let cellStyle = styledCell.style;
3687
3721
  if (val instanceof Date && !cellStyle?.numFmt) {
3688
3722
  // A date needs a date format, or Excel shows the bare serial number
3689
- cellStyle = { ...cellStyle, numFmt: val.getTime() % DAY_MS === 0 ? 'yyyy-mm-dd' : 'yyyy-mm-dd hh:mm:ss' };
3723
+ cellStyle = { ...cellStyle, numFmt: defaultDateFormat(val) };
3690
3724
  }
3691
3725
  const style = cellStyle ? this.styleEngine.registerStyle(cellStyle) : 0;
3692
3726
  const sAttr = style > 0 ? ` s="${style}"` : '';
@@ -3737,6 +3771,7 @@ class SheetWriter {
3737
3771
  return `<c r="${colRef}"${sAttr}><v>${dateToSerial(val)}</v></c>`;
3738
3772
  }
3739
3773
  if (typeof val === 'string') {
3774
+ checkCellText(val, colRef);
3740
3775
  if (this.writerOptions.sharedStrings) {
3741
3776
  let index = this.sharedStrings.get(val);
3742
3777
  if (index === undefined)
@@ -4142,6 +4177,16 @@ class StylePatcher {
4142
4177
  this.memo.set(key, id);
4143
4178
  return id;
4144
4179
  }
4180
+ // The format for date `d` in a cell of format `base`: `base` when it already shows dates, otherwise
4181
+ // `base` with a date format, since a date in a General cell shows as its serial number
4182
+ dateFormat(base, d) {
4183
+ const xf = this.xfs[base] ?? this.added.cellXfs[base - this.xfs.length] ?? '';
4184
+ const id = parseInt(attr(/^<[^>]*>/.exec(xf)?.[0] ?? '', 'numFmtId') ?? '0', 10);
4185
+ const code = [...this.numFmts].find(([, v]) => v === id)?.[0];
4186
+ if (isBuiltinDateFormat(id) || (code !== undefined && isDateFormatCode(code)))
4187
+ return base;
4188
+ return this.patch(base, { numFmt: defaultDateFormat(d) });
4189
+ }
4145
4190
  toXml() {
4146
4191
  let xml = this.xml;
4147
4192
  for (const section of ['numFmts', 'fonts', 'fills', 'borders', 'cellXfs']) {
@@ -4697,7 +4742,7 @@ function editedCell(p, ref, attrs, edit) {
4697
4742
  }
4698
4743
  if (typeof edit === 'object')
4699
4744
  return `<${p}c r="${ref}"${attrs}><${p}f>${escapeXml$5(edit.formula.replace(/^=/, ''))}</${p}f></${p}c>`;
4700
- const text = escapeXml$5(encodeXString(edit));
4745
+ const text = escapeXml$5(encodeXString(checkCellText(edit, ref)));
4701
4746
  const t = /^\s|\s$/.test(edit) ? `<${p}t xml:space="preserve">${text}</${p}t>` : `<${p}t>${text}</${p}t>`;
4702
4747
  return `<${p}c r="${ref}"${attrs} t="inlineStr"><${p}is>${t}</${p}is></${p}c>`;
4703
4748
  }
@@ -4748,9 +4793,9 @@ class SheetEditor {
4748
4793
  this.cellEdits.set(sheetName, rows = new Map());
4749
4794
  for (const [ref, edit] of Object.entries(cells)) {
4750
4795
  const m = /^\$?([A-Za-z]{1,3})\$?(\d+)$/.exec(ref);
4751
- if (!m || parseInt(m[2], 10) < 1)
4796
+ const r = m ? parseInt(m[2], 10) : 0;
4797
+ if (!m || r < 1 || r > MAX_ROWS$1 || colIndex(m[1]) >= MAX_COLUMNS$1)
4752
4798
  throw new Error(`Invalid cell reference "${ref}".`);
4753
- const r = parseInt(m[2], 10);
4754
4799
  if (!rows.has(r))
4755
4800
  rows.set(r, new Map());
4756
4801
  rows.get(r).set(colIndex(m[1]), edit);
@@ -4936,7 +4981,10 @@ class SheetEditor {
4936
4981
  let patcher;
4937
4982
  const styled = [...editsByPath.values(), ...added.values()]
4938
4983
  .some(rows => [...rows.values()].some(cells => [...cells.values()].some(e => styleOf(e))));
4939
- if (styled) {
4984
+ // Dates may need a date format added to their cells
4985
+ const dated = [...editsByPath.values(), ...added.values()]
4986
+ .some(rows => [...rows.values()].some(cells => [...cells.values()].some(e => contentOf(e) instanceof Date)));
4987
+ if (styled || (dated && parts.styles)) {
4940
4988
  if (!parts.styles)
4941
4989
  throw new Error('Workbook has no styles part.');
4942
4990
  patcher = new StylePatcher(await read(parts.styles));
@@ -5013,6 +5061,10 @@ class SheetEditor {
5013
5061
  attrs = `${attrs.replace(/\ss="[^"]*"/, '')} s="${patcher.patch(base, style)}"`;
5014
5062
  }
5015
5063
  const content = contentOf(edit);
5064
+ if (content instanceof Date && patcher) {
5065
+ const base = parseInt(/\ss="(\d+)"/.exec(attrs)?.[1] ?? '0', 10);
5066
+ attrs = `${attrs.replace(/\ss="[^"]*"/, '')} s="${patcher.dateFormat(base, content)}"`;
5067
+ }
5016
5068
  if (content === KEEP) {
5017
5069
  return existing?.[2] !== undefined ? `<${p}c r="${ref}"${attrs}>${existing[2]}</${p}c>` : `<${p}c r="${ref}"${attrs}/>`;
5018
5070
  }
@@ -5162,7 +5214,7 @@ class SheetEditor {
5162
5214
  if (typeof val === 'number')
5163
5215
  return `<c r="${colRef}"><v>${val}</v></c>`;
5164
5216
  if (typeof val === 'string') {
5165
- return `<c r="${colRef}" t="inlineStr"><is><t xml:space="preserve">${escapeXml$5(encodeXString(val))}</t></is></c>`;
5217
+ return `<c r="${colRef}" t="inlineStr"><is><t xml:space="preserve">${escapeXml$5(encodeXString(checkCellText(val, colRef)))}</t></is></c>`;
5166
5218
  }
5167
5219
  return `<c r="${colRef}"/>`;
5168
5220
  }).join('');
@@ -5774,7 +5826,8 @@ async function* parseCsv(input, options = {}) {
5774
5826
  let first = true;
5775
5827
  const rows = [];
5776
5828
  const endField = () => {
5777
- row.push(quoted || !convert ? field : convertField(field));
5829
+ // An empty unquoted field is null in both modes; a quoted "" stays an empty string
5830
+ row.push(quoted ? field : !convert ? (field === '' ? null : field) : convertField(field));
5778
5831
  field = '';
5779
5832
  quoted = false;
5780
5833
  };
@@ -5906,11 +5959,14 @@ function textXml(s) {
5906
5959
  }
5907
5960
  const dateValue = (d) => d.toISOString().slice(0, 19); // UTC, like SheetWriter
5908
5961
  const hasTime = (d) => d.getTime() % 86400000 !== 0;
5909
- function cellXml(cell, span, covered) {
5962
+ function cellXml(cell, span, covered, evaluate) {
5910
5963
  if (covered)
5911
5964
  return '<table:covered-table-cell/>';
5912
- const value = isStyledCell(cell) ? cell.value : cell;
5965
+ let value = isStyledCell(cell) ? cell.value : cell;
5913
5966
  const formula = isStyledCell(cell) && cell.formula ? ` table:formula="${escapeXml(toOpenFormula(cell.formula))}"` : '';
5967
+ // A formula without a value gets its result stored, as SheetWriter does, for readers that don't recalculate
5968
+ if (formula && value == null)
5969
+ value = evaluate(cell.formula);
5914
5970
  if (value === null || value === undefined || (typeof value === 'number' && !isFinite(value))) {
5915
5971
  return formula || span ? `<table:table-cell${formula}${span}/>` : '<table:table-cell/>';
5916
5972
  }
@@ -6003,6 +6059,15 @@ class OdsWriter {
6003
6059
  const merges = (sheet.options.mergeCells ?? []).map(parseRange);
6004
6060
  const lastMergeRow = Math.max(-1, ...merges.map(m => m.r2));
6005
6061
  const mergeAt = (r, c) => merges.find(m => m.r1 === r && m.c1 === c);
6062
+ // Formulas resolve against the sheet they live in; streamed rows can't be looked back at
6063
+ const engine = new FormulaEngine();
6064
+ if (Array.isArray(sheet.rows)) {
6065
+ engine.loadData(sheet.rows.map(row => row.map(cell => {
6066
+ const v = isStyledCell(cell) ? cell.value : cell;
6067
+ return v instanceof Date ? dateToSerial(v) : v ?? null;
6068
+ })));
6069
+ }
6070
+ const evaluate = (f) => engine.evaluate(f);
6006
6071
  const covered = (r, c) => merges.some(m => r >= m.r1 && r <= m.r2 && c >= m.c1 && c <= m.c2 && (r !== m.r1 || c !== m.c1));
6007
6072
  let r = 0, batch = '';
6008
6073
  const writeRow = (row) => {
@@ -6011,7 +6076,7 @@ class OdsWriter {
6011
6076
  for (let c = 0; c < width; c++) {
6012
6077
  const m = mergeAt(r, c);
6013
6078
  const span = m ? ` table:number-columns-spanned="${m.c2 - m.c1 + 1}" table:number-rows-spanned="${m.r2 - m.r1 + 1}"` : '';
6014
- xml += cellXml(row[c], span, covered(r, c));
6079
+ xml += cellXml(row[c], span, covered(r, c), evaluate);
6015
6080
  }
6016
6081
  r++;
6017
6082
  return xml + (width ? '' : '<table:table-cell/>') + '</table:table-row>';
@@ -6095,5 +6160,5 @@ const XlsxFlow = {
6095
6160
  }
6096
6161
  };
6097
6162
 
6098
- export { CfbReader, ConditionalFormatter, DRAWING_NS, FormulaEngine, OdsWriter, SheetEditor, SheetReader, SheetWriter, StyleEngine, XlsxFlow, ZipRandomAccessParser, ZipStreamWriter, anchorXml, colIndex, colLetter, crc32, createBlobReader, createFileReader, createXmlStreamParser, dateToSerial, decodeXString, drawingPartXml, encodeXString, isCfb, legacyPasswordHash, mapFormulaRefs, parseCsv, parseWorksheet, partRelationships, readSharedStrings, recalcOnOpen, resolveWorkbookParts, sheetToJson, shiftFormula, streamToCsv };
6163
+ export { CfbReader, ConditionalFormatter, DRAWING_NS, FormulaEngine, OdsWriter, SheetEditor, SheetReader, SheetWriter, StyleEngine, StylePatcher, XlsxFlow, ZipRandomAccessParser, ZipStreamWriter, anchorXml, checkCellText, colIndex, colLetter, crc32, createBlobReader, createFileReader, createXmlStreamParser, dateToSerial, decodeXString, drawingPartXml, encodeXString, isCfb, legacyPasswordHash, mapFormulaRefs, parseCsv, parseWorksheet, partRelationships, readSharedStrings, recalcOnOpen, resolveWorkbookParts, sheetToJson, shiftFormula, streamToCsv };
6099
6164
  //# sourceMappingURL=index.mjs.map