@office-kit/xlsx 0.11.1 → 0.13.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 (75) hide show
  1. package/README.md +4 -1
  2. package/dist/cell/cell.d.ts +44 -9
  3. package/dist/cell/index.d.ts +1 -1
  4. package/dist/{cell-BucgGwl3.mjs → cell-Df3cKRIl.mjs} +72 -30
  5. package/dist/cell-Df3cKRIl.mjs.map +1 -0
  6. package/dist/{cell-style-DuvS_Lsg.mjs → cell-style-BFmJOmcx.mjs} +130 -93
  7. package/dist/cell-style-BFmJOmcx.mjs.map +1 -0
  8. package/dist/cell.mjs +2 -2
  9. package/dist/{coordinate-96Ecci4d.mjs → coordinate-Bt5Gni2I.mjs} +19 -8
  10. package/dist/coordinate-Bt5Gni2I.mjs.map +1 -0
  11. package/dist/{defined-names-CviWmtQg.mjs → defined-names-Ctu3F6ls.mjs} +4 -3
  12. package/dist/defined-names-Ctu3F6ls.mjs.map +1 -0
  13. package/dist/{differential-DvKU8fJt.mjs → differential-H9SjEeIU.mjs} +2 -2
  14. package/dist/{differential-DvKU8fJt.mjs.map → differential-H9SjEeIU.mjs.map} +1 -1
  15. package/dist/{drawing-BxzLuryn.mjs → drawing-ZJ3h4VHD.mjs} +2 -2
  16. package/dist/{drawing-BxzLuryn.mjs.map → drawing-ZJ3h4VHD.mjs.map} +1 -1
  17. package/dist/drawing.mjs +1 -1
  18. package/dist/{inference-BrI9uIY4.mjs → inference-BMvv9cUY.mjs} +18 -3
  19. package/dist/inference-BMvv9cUY.mjs.map +1 -0
  20. package/dist/io/save.d.ts +15 -3
  21. package/dist/io.mjs +2 -2
  22. package/dist/{load-Bghb1YHY.mjs → load-CJdGh50R.mjs} +6 -6
  23. package/dist/{load-Bghb1YHY.mjs.map → load-CJdGh50R.mjs.map} +1 -1
  24. package/dist/node.mjs +1 -1
  25. package/dist/{save-Bqcf8w3g.mjs → save-CjarR7Rp.mjs} +10 -8
  26. package/dist/save-CjarR7Rp.mjs.map +1 -0
  27. package/dist/streaming/write-only.d.ts +18 -14
  28. package/dist/streaming.mjs +21 -51
  29. package/dist/streaming.mjs.map +1 -1
  30. package/dist/styles/cell-style.d.ts +78 -32
  31. package/dist/styles/fonts.d.ts +10 -1
  32. package/dist/styles/index.d.ts +3 -3
  33. package/dist/styles/stylesheet.d.ts +26 -0
  34. package/dist/styles.mjs +4 -4
  35. package/dist/{stylesheet-writer-HxaX7bGy.mjs → stylesheet-writer-D7Uug85X.mjs} +31 -18
  36. package/dist/stylesheet-writer-D7Uug85X.mjs.map +1 -0
  37. package/dist/{table-1P8we2gS.mjs → table-Ccro4rrz.mjs} +11 -9
  38. package/dist/table-Ccro4rrz.mjs.map +1 -0
  39. package/dist/utils/coordinate.d.ts +30 -4
  40. package/dist/utils/formula-text.d.ts +7 -0
  41. package/dist/utils/index.d.ts +1 -1
  42. package/dist/utils.mjs +2 -2
  43. package/dist/workbook/calc-properties.d.ts +8 -1
  44. package/dist/workbook/index.d.ts +1 -1
  45. package/dist/{workbook-BlZVXiyG.mjs → workbook-B15-T4cs.mjs} +6 -6
  46. package/dist/{workbook-BlZVXiyG.mjs.map → workbook-B15-T4cs.mjs.map} +1 -1
  47. package/dist/workbook.mjs +45 -3
  48. package/dist/workbook.mjs.map +1 -1
  49. package/dist/worksheet/cell-range.d.ts +10 -3
  50. package/dist/worksheet/data-validations.d.ts +1 -1
  51. package/dist/worksheet/index.d.ts +3 -3
  52. package/dist/worksheet/table-validate.d.ts +7 -0
  53. package/dist/worksheet/table.d.ts +4 -1
  54. package/dist/worksheet/views.d.ts +5 -0
  55. package/dist/worksheet/worksheet.d.ts +106 -65
  56. package/dist/worksheet/writer.d.ts +7 -0
  57. package/dist/{worksheet-C5Ensf_j.mjs → worksheet-4xW-i8j9.mjs} +235 -51
  58. package/dist/worksheet-4xW-i8j9.mjs.map +1 -0
  59. package/dist/worksheet.mjs +3 -3
  60. package/dist/{writer-IyFccCRJ.mjs → writer-Z7cF_CBk.mjs} +60 -8
  61. package/dist/writer-Z7cF_CBk.mjs.map +1 -0
  62. package/dist/zip/index.d.ts +1 -1
  63. package/dist/zip/writer.d.ts +20 -1
  64. package/dist/zip.mjs +1 -1
  65. package/package.json +1 -1
  66. package/dist/cell-BucgGwl3.mjs.map +0 -1
  67. package/dist/cell-style-DuvS_Lsg.mjs.map +0 -1
  68. package/dist/coordinate-96Ecci4d.mjs.map +0 -1
  69. package/dist/defined-names-CviWmtQg.mjs.map +0 -1
  70. package/dist/inference-BrI9uIY4.mjs.map +0 -1
  71. package/dist/save-Bqcf8w3g.mjs.map +0 -1
  72. package/dist/stylesheet-writer-HxaX7bGy.mjs.map +0 -1
  73. package/dist/table-1P8we2gS.mjs.map +0 -1
  74. package/dist/worksheet-C5Ensf_j.mjs.map +0 -1
  75. package/dist/writer-IyFccCRJ.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { o as OpenXmlSchemaError } from "./exceptions-D-CFwxgm.mjs";
2
- import { a as columnLetterFromIndex, g as tupleToCoordinate, h as rangeBoundaries, i as columnIndexFromLetter, n as MAX_ROW, r as boundariesToRangeString, s as coordinateToTuple, t as MAX_COL } from "./coordinate-96Ecci4d.mjs";
3
- import { o as cellValueAsString, y as makeCell } from "./cell-BucgGwl3.mjs";
2
+ import { a as columnLetterFromIndex, g as tupleToCoordinate, h as rangeBoundaries, i as columnIndexFromLetter, n as MAX_ROW, r as boundariesToRangeString, s as coordinateToTuple, t as MAX_COL } from "./coordinate-Bt5Gni2I.mjs";
3
+ import { N as richTextToString, b as makeCell, h as isFormulaValue, o as cellValueAsString, p as isErrorValue, v as isRichTextValue } from "./cell-Df3cKRIl.mjs";
4
4
  //#region src/styles/colors.ts
5
5
  /**
6
6
  * Legacy 64-entry palette indexed colours fall back to. Verbatim from
@@ -311,9 +311,17 @@ function makeCellRange(minRow, minCol, maxRow, maxCol) {
311
311
  maxCol: Math.max(minCol, maxCol)
312
312
  };
313
313
  }
314
- /** Parse a range expression — wraps {@link rangeBoundaries}. */
314
+ /**
315
+ * Resolve a {@link RangeRef} to numeric bounds: A1 expressions go through
316
+ * {@link rangeBoundaries}, pre-computed bounds through {@link makeCellRange}.
317
+ * Both paths validate against the grid and normalise inverted bounds, so
318
+ * `{ minRow: 5, maxRow: 1 }` behaves like `"A5:A1"` rather than iterating zero
319
+ * rows, and a fractional or off-grid bound throws here instead of half-way
320
+ * through the caller's loop.
321
+ */
315
322
  function parseRange(input) {
316
- return rangeBoundaries(input);
323
+ if (typeof input === "string") return rangeBoundaries(input);
324
+ return makeCellRange(input.minRow, input.minCol, input.maxRow, input.maxCol);
317
325
  }
318
326
  /** Format a CellRange back into the canonical OOXML string. */
319
327
  function rangeToString(r) {
@@ -566,6 +574,104 @@ function makeHyperlink(opts) {
566
574
  };
567
575
  }
568
576
  //#endregion
577
+ //#region src/worksheet/table-validate.ts
578
+ const TWO_CORNER_REF_RE = /^[A-Za-z]{1,3}[1-9][0-9]*(:[A-Za-z]{1,3}[1-9][0-9]*)?$/;
579
+ /**
580
+ * Excel stores table header text as a string, so only a string, rich text or a
581
+ * formula caching a string can be paired with a column name. Reading a number
582
+ * or a date as its text form would mean guessing at the number format the
583
+ * header carries, and a wrong guess either blesses a file Excel repairs or
584
+ * rejects one it accepts.
585
+ */
586
+ const readHeaderCell = (value) => {
587
+ if (value === null) return { kind: "empty" };
588
+ if (typeof value === "string") return value === "" ? { kind: "empty" } : {
589
+ kind: "text",
590
+ text: value
591
+ };
592
+ if (isRichTextValue(value)) {
593
+ const text = richTextToString(value.runs);
594
+ return text === "" ? { kind: "empty" } : {
595
+ kind: "text",
596
+ text
597
+ };
598
+ }
599
+ if (isFormulaValue(value)) {
600
+ const cached = value.cachedValue;
601
+ if (cached === void 0) return {
602
+ kind: "nonText",
603
+ held: "holds a formula with no cached value"
604
+ };
605
+ if (typeof cached !== "string") return {
606
+ kind: "nonText",
607
+ held: "holds a formula whose cached value is not text"
608
+ };
609
+ return cached === "" ? { kind: "empty" } : {
610
+ kind: "text",
611
+ text: cached
612
+ };
613
+ }
614
+ if (typeof value === "number") return {
615
+ kind: "nonText",
616
+ held: `holds the number ${value}`
617
+ };
618
+ if (typeof value === "boolean") return {
619
+ kind: "nonText",
620
+ held: `holds the boolean ${value}`
621
+ };
622
+ if (value instanceof Date) return {
623
+ kind: "nonText",
624
+ held: "holds a date"
625
+ };
626
+ if (isErrorValue(value)) return {
627
+ kind: "nonText",
628
+ held: `holds the error ${value.code}`
629
+ };
630
+ return {
631
+ kind: "nonText",
632
+ held: "holds a duration"
633
+ };
634
+ };
635
+ const TEXT_REMEDY = "Write the header row before adding the table, or pass headerRowCount: 0 for a header-less table.";
636
+ const NON_TEXT_REMEDY = "Excel stores header text as a string, so the header cell has to hold text.";
637
+ /**
638
+ * Reject a table whose declared geometry or column names disagree with the
639
+ * sheet underneath it.
640
+ */
641
+ const validateTableAgainstSheet = (ws, table) => {
642
+ const where = `table "${table.displayName}"`;
643
+ const ref = table.ref.trim();
644
+ if (!TWO_CORNER_REF_RE.test(ref)) throw new OpenXmlSchemaError(`${where}: ref "${table.ref}" is not a plain two-corner cell range like "A1:C4"`);
645
+ const bounds = rangeBoundaries(ref);
646
+ const { columns } = table;
647
+ const width = bounds.maxCol - bounds.minCol + 1;
648
+ if (columns.length !== width) throw new OpenXmlSchemaError(`${where}: ref "${table.ref}" spans ${width} column(s) but ${columns.length} column(s) were supplied`);
649
+ const headerRows = table.headerRowCount ?? 1;
650
+ const totalsRows = table.totalsRowCount ?? 0;
651
+ for (const field of ["headerRowCount", "totalsRowCount"]) {
652
+ const count = table[field];
653
+ if (count !== void 0 && (!Number.isInteger(count) || count < 0 || count > 4294967295)) throw new OpenXmlSchemaError(`${where}: ${field} must be an unsigned 32-bit integer, got ${count}`);
654
+ }
655
+ const height = bounds.maxRow - bounds.minRow + 1;
656
+ if (height < headerRows + totalsRows) throw new OpenXmlSchemaError(`${where}: ref "${table.ref}" is ${height} row(s) tall, which cannot contain ${headerRows} header row(s) and ${totalsRows} totals row(s)`);
657
+ const seen = /* @__PURE__ */ new Set();
658
+ for (const [i, column] of columns.entries()) {
659
+ if (column.name === "") throw new OpenXmlSchemaError(`${where}: column ${i + 1} has no name; every table column needs one`);
660
+ const key = column.name.toLowerCase();
661
+ if (seen.has(key)) throw new OpenXmlSchemaError(`${where}: column ${i + 1} repeats the name "${column.name}"; table column names have to be unique`);
662
+ seen.add(key);
663
+ }
664
+ if (headerRows === 0) return;
665
+ const headerRow = ws.rows.get(bounds.minRow);
666
+ for (const [i, column] of columns.entries()) {
667
+ const col = bounds.minCol + i;
668
+ const header = readHeaderCell(headerRow?.get(col)?.value ?? null);
669
+ if (header.kind === "text" && header.text === column.name) continue;
670
+ const held = header.kind === "text" ? `holds "${header.text}"` : header.kind === "empty" ? "is empty" : header.held;
671
+ throw new OpenXmlSchemaError(`${where}: header cell ${tupleToCoordinate(col, bounds.minRow)} ${held} but column ${i + 1} is named "${column.name}". ${header.kind === "nonText" ? NON_TEXT_REMEDY : TEXT_REMEDY}`);
672
+ }
673
+ };
674
+ //#endregion
569
675
  //#region src/worksheet/worksheet.ts
570
676
  /** Build a Worksheet shell. */
571
677
  function makeWorksheet(title) {
@@ -605,10 +711,17 @@ function getCell(ws, row, col) {
605
711
  return ws.rows.get(row)?.get(col);
606
712
  }
607
713
  /**
608
- * Create or update a Cell at (row, col). Existing cells keep their styleId /
609
- * hyperlinkId / commentId unless explicitly overridden.
714
+ * Write a Cell at (row, col), creating it when the coordinate is empty.
715
+ * `value` always lands on the cell, so an existing value is replaced. Use
716
+ * {@link ensureCell} to reach a cell without writing to it. An existing cell
717
+ * keeps its styleId / hyperlinkId / commentId unless `styleId` is passed.
718
+ *
719
+ * `null` is the explicit empty value: it clears the value and leaves the cell
720
+ * in the sheet with its fill, border and number format, the way Excel's
721
+ * Delete key does. {@link deleteCell} drops the cell entirely, formatting
722
+ * included, and {@link clearRange} does the same across a rectangle.
610
723
  */
611
- function setCell(ws, row, col, value = null, styleId) {
724
+ function setCell(ws, row, col, value, styleId) {
612
725
  let rowMap = ws.rows.get(row);
613
726
  let cell = rowMap?.get(col);
614
727
  if (cell === void 0) {
@@ -625,6 +738,22 @@ function setCell(ws, row, col, value = null, styleId) {
625
738
  if (row > ws._appendRowCursor) ws._appendRowCursor = row;
626
739
  return cell;
627
740
  }
741
+ /**
742
+ * Get the Cell at (row, col), allocating an empty one when the coordinate is
743
+ * not populated yet. An existing cell is returned untouched, value and all,
744
+ * which makes this the way to reach a cell you are about to style or attach a
745
+ * formula to.
746
+ *
747
+ * {@link mergeCells} drops the cells underneath a merge; reaching one of those
748
+ * coordinates allocates it again, and the written `<sheetData>` then carries a
749
+ * blank `<c>` under the merge. A merged block's value lives on its top-left
750
+ * cell, so address that coordinate when the block is what you mean.
751
+ */
752
+ function ensureCell(ws, row, col) {
753
+ const existing = ws.rows.get(row)?.get(col);
754
+ if (existing !== void 0) return existing;
755
+ return setCell(ws, row, col, null);
756
+ }
628
757
  /** Delete a single cell from the sheet. Empty rows are pruned. */
629
758
  function deleteCell(ws, row, col) {
630
759
  const rowMap = ws.rows.get(row);
@@ -663,34 +792,44 @@ function clearAllCells(ws) {
663
792
  /**
664
793
  * Append a row of values starting at the next empty row. Returns the row index
665
794
  * (1-based). Mirrors openpyxl's `Worksheet.append`. `null` / `undefined`
666
- * entries leave the cell empty.
795
+ * entries leave the cell empty unless `opts.styleIds` names a style for that
796
+ * column.
667
797
  */
668
- function appendRow(ws, values) {
798
+ function appendRow(ws, values, opts = {}) {
669
799
  const row = ws._appendRowCursor + 1;
670
- for (let i = 0; i < values.length; i++) {
800
+ const styleIds = opts.styleIds;
801
+ const width = styleIds === void 0 ? values.length : Math.max(values.length, styleIds.length);
802
+ for (let i = 0; i < width; i++) {
671
803
  const value = values[i];
672
- if (value === void 0 || value === null) continue;
673
- setCell(ws, row, i + 1, value);
804
+ const styleId = styleIds?.[i];
805
+ if ((value === void 0 || value === null) && styleId === void 0) continue;
806
+ setCell(ws, row, i + 1, value ?? null, styleId);
674
807
  }
675
808
  ws._appendRowCursor = row;
676
809
  return row;
677
810
  }
678
811
  /**
679
812
  * Bulk version of {@link appendRow}: append a 2D array of values one row at a
680
- * time. Returns `{firstRow, lastRow}` both 1-based, inclusive. An empty input
681
- * returns `{firstRow, lastRow: firstRow - 1}` so callers can detect the no-op
682
- * without throwing.
813
+ * time. Returns `{firstRow, lastRow}`, both 1-based and inclusive. An empty
814
+ * input returns `{firstRow, lastRow: firstRow - 1}` so callers can detect the
815
+ * no-op without throwing.
683
816
  *
684
817
  * Common usage: `appendRows(ws, csvParsedRows)` for fast import.
818
+ *
819
+ * `opts` is column-indexed, not row-indexed: the same
820
+ * {@link AppendRowOptions.styleIds} apply to every row, which is the point when
821
+ * a column has one format down the whole table. Rows shorter than `styleIds`
822
+ * still get the trailing styled blanks described there, so trim per row when
823
+ * the input is ragged.
685
824
  */
686
- function appendRows(ws, rows) {
825
+ function appendRows(ws, rows, opts = {}) {
687
826
  const firstRow = ws._appendRowCursor + 1;
688
827
  if (rows.length === 0) return {
689
828
  firstRow,
690
829
  lastRow: firstRow - 1
691
830
  };
692
831
  let lastRow = firstRow - 1;
693
- for (const row of rows) lastRow = appendRow(ws, row);
832
+ for (const row of rows) lastRow = appendRow(ws, row, opts);
694
833
  return {
695
834
  firstRow,
696
835
  lastRow
@@ -712,7 +851,9 @@ function appendRows(ws, rows) {
712
851
  */
713
852
  function writeRange(ws, startRef, values) {
714
853
  if (values.length === 0) return void 0;
715
- const { col: startCol, row: startRow } = coordinateToTuple(startRef);
854
+ const anchor = typeof startRef === "string" ? coordinateToTuple(startRef) : startRef;
855
+ if (typeof startRef !== "string") validateRowCol(anchor.row, anchor.col);
856
+ const { col: startCol, row: startRow } = anchor;
716
857
  let maxRow = startRow;
717
858
  let maxCol = startCol;
718
859
  for (let i = 0; i < values.length; i++) {
@@ -950,30 +1091,53 @@ function* getCellsInRange(ws, range) {
950
1091
  }
951
1092
  }
952
1093
  }
953
- /** Resolve an "A1" coordinate to a numeric (col, row) pair on the sheet. */
1094
+ const PLAIN_COORD_RE = /^([A-Za-z]{1,3})([1-9][0-9]*)$/;
1095
+ const plainCoordToRowCol = (coord) => {
1096
+ const m = PLAIN_COORD_RE.exec(coord);
1097
+ if (m === null || m[1] === void 0 || m[2] === void 0) return void 0;
1098
+ return {
1099
+ row: Number.parseInt(m[2], 10),
1100
+ col: columnIndexFromLetter(m[1])
1101
+ };
1102
+ };
1103
+ /**
1104
+ * A1-addressed {@link setCell}: resolves `coord` to a numeric (row, col) and
1105
+ * writes `value` there. Throws `OpenXmlSchemaError` when `coord` is not a
1106
+ * plain A1 reference.
1107
+ */
954
1108
  function setCellByCoord(ws, coord, value, styleId) {
955
- const m = /^([A-Za-z]{1,3})([1-9][0-9]*)$/.exec(coord);
956
- if (m === null || m[1] === void 0 || m[2] === void 0) throw new OpenXmlSchemaError(`setCellByCoord: invalid coordinate "${coord}"`);
957
- const col = columnIndexFromLetter(m[1]);
958
- return setCell(ws, Number.parseInt(m[2], 10), col, value, styleId);
1109
+ const rc = plainCoordToRowCol(coord);
1110
+ if (rc === void 0) throw new OpenXmlSchemaError(`setCellByCoord: invalid coordinate "${coord}"`);
1111
+ return setCell(ws, rc.row, rc.col, value, styleId);
959
1112
  }
960
1113
  /** Convenience getter accepting an "A1" coordinate. */
961
1114
  function getCellByCoord(ws, coord) {
962
- const m = /^([A-Za-z]{1,3})([1-9][0-9]*)$/.exec(coord);
963
- if (m === null || m[1] === void 0 || m[2] === void 0) return void 0;
964
- const col = columnIndexFromLetter(m[1]);
965
- return getCell(ws, Number.parseInt(m[2], 10), col);
1115
+ const rc = plainCoordToRowCol(coord);
1116
+ if (rc === void 0) return void 0;
1117
+ return getCell(ws, rc.row, rc.col);
1118
+ }
1119
+ /**
1120
+ * A1-addressed {@link ensureCell}. Throws `OpenXmlSchemaError` when `coord` is
1121
+ * not a plain A1 reference.
1122
+ */
1123
+ function ensureCellByCoord(ws, coord) {
1124
+ const rc = plainCoordToRowCol(coord);
1125
+ if (rc === void 0) throw new OpenXmlSchemaError(`ensureCellByCoord: invalid coordinate "${coord}"`);
1126
+ return ensureCell(ws, rc.row, rc.col);
966
1127
  }
967
- const toCellRange = (refOrRange) => typeof refOrRange === "string" ? parseRange(refOrRange) : refOrRange;
968
1128
  /**
969
1129
  * Merge a range. The top-left cell keeps its value; every other cell in the
970
1130
  * range is dropped from `ws.rows` so the on-wire `<sheetData>` won't carry
971
1131
  * phantom cells underneath the merge. Mirrors openpyxl's
972
1132
  * `MergedCellRange.format()`. Idempotent for an identical range, throws when
973
1133
  * the range overlaps an existing merge.
1134
+ *
1135
+ * The registered range is a validated, normalised copy of `refOrRange`, so
1136
+ * mutating a bounds object afterwards can't rewrite a merge that is already on
1137
+ * the sheet.
974
1138
  */
975
1139
  function mergeCells(ws, refOrRange) {
976
- const range = toCellRange(refOrRange);
1140
+ const range = parseRange(refOrRange);
977
1141
  for (const existing of ws.mergedCells) {
978
1142
  if (rangeToString(existing) === rangeToString(range)) return existing;
979
1143
  if (rangesOverlap(existing, range)) throw new OpenXmlSchemaError(`mergeCells: range ${rangeToString(range)} overlaps existing merged range ${rangeToString(existing)}`);
@@ -989,7 +1153,7 @@ function mergeCells(ws, refOrRange) {
989
1153
  }
990
1154
  /** Drop a previously-merged range. No-op if the range isn't registered. */
991
1155
  function unmergeCells(ws, refOrRange) {
992
- const target = rangeToString(toCellRange(refOrRange));
1156
+ const target = rangeToString(parseRange(refOrRange));
993
1157
  const idx = ws.mergedCells.findIndex((r) => rangeToString(r) === target);
994
1158
  if (idx < 0) return false;
995
1159
  ws.mergedCells.splice(idx, 1);
@@ -1047,30 +1211,38 @@ const ensurePrimaryView = (ws) => {
1047
1211
  return view;
1048
1212
  };
1049
1213
  /**
1050
- * Freeze rows / columns above + left of `topLeftRef` ("B2" 1 row + 1 col).
1214
+ * Freeze rows / columns above + left of the given top-left cell. Takes either
1215
+ * the A1 ref of the first unfrozen cell (`"B2"` freezes 1 row + 1 column) or
1216
+ * the counts directly (`{ rows: 1, cols: 0 }` freezes the header row alone).
1051
1217
  * Pass `undefined` to clear any existing freeze. Targets the workbook's primary
1052
1218
  * SheetView (`ws.views[0]`); creates one if absent.
1053
1219
  */
1054
- function setFreezePanes(ws, topLeftRef) {
1055
- if (topLeftRef === void 0) {
1220
+ function setFreezePanes(ws, topLeft) {
1221
+ if (topLeft === void 0) {
1056
1222
  if (ws.views[0]) delete ws.views[0].pane;
1057
1223
  return;
1058
1224
  }
1225
+ const ref = typeof topLeft === "string" ? topLeft : freezeCountsToRef(topLeft);
1059
1226
  const view = ensurePrimaryView(ws);
1060
- view.pane = makeFreezePane(topLeftRef);
1227
+ view.pane = makeFreezePane(ref);
1061
1228
  }
1229
+ /**
1230
+ * Translate freeze counts to the A1 ref of the first unfrozen cell. Zero on
1231
+ * both axes composes to `"A1"`, rejected by {@link makeFreezePane} as the
1232
+ * no-op freeze, so that condition has one message in one place however it was
1233
+ * spelled.
1234
+ */
1235
+ const freezeCountsToRef = ({ rows, cols }) => {
1236
+ if (!Number.isInteger(rows) || rows < 0) throw new OpenXmlSchemaError(`setFreezePanes: rows must be a non-negative integer; got ${rows}`);
1237
+ if (!Number.isInteger(cols) || cols < 0) throw new OpenXmlSchemaError(`setFreezePanes: cols must be a non-negative integer; got ${cols}`);
1238
+ return `${columnLetterFromIndex(cols + 1)}${rows + 1}`;
1239
+ };
1062
1240
  /** Inverse of {@link setFreezePanes}; returns the top-left ref or undefined when no freeze is active. */
1063
1241
  function getFreezePanes(ws) {
1064
1242
  const view = ws.views[0];
1065
1243
  if (!view) return void 0;
1066
1244
  return freezePaneRef(view);
1067
1245
  }
1068
- /** Freeze both top `rows` rows AND left `cols` columns. */
1069
- function freezePanes(ws, rows, cols) {
1070
- if (!Number.isInteger(rows) || rows < 1) throw new OpenXmlSchemaError(`freezePanes: rows must be a positive integer; got ${rows}`);
1071
- if (!Number.isInteger(cols) || cols < 1) throw new OpenXmlSchemaError(`freezePanes: cols must be a positive integer; got ${cols}`);
1072
- setFreezePanes(ws, `${columnLetterFromIndex(cols + 1)}${rows + 1}`);
1073
- }
1074
1246
  /** Lazily get-or-create `ws.sheetProperties` so tab-color helpers don't have to branch. */
1075
1247
  const ensureSheetProperties = (ws) => {
1076
1248
  if (!ws.sheetProperties) ws.sheetProperties = makeSheetProperties();
@@ -1103,13 +1275,21 @@ function setSheetViewMode(ws, mode) {
1103
1275
  * down starting at the top-left of `range`; subsequent rows follow. `null` /
1104
1276
  * `undefined` entries skip the cell. Useful for dropping a header + data block
1105
1277
  * in one call.
1278
+ *
1279
+ * Values past the range's bottom or right edge are dropped rather than written
1280
+ * outside it, the way {@link copyRange} clips to its target extent. Use
1281
+ * {@link writeRange} for the unbounded form, which takes an anchor and grows to
1282
+ * fit the array.
1106
1283
  */
1107
1284
  function setRangeValues(ws, range, rows) {
1108
- const { minRow, minCol } = parseRange(range);
1109
- for (let i = 0; i < rows.length; i++) {
1285
+ const { minRow, maxRow, minCol, maxCol } = parseRange(range);
1286
+ const height = Math.min(rows.length, maxRow - minRow + 1);
1287
+ const width = maxCol - minCol + 1;
1288
+ for (let i = 0; i < height; i++) {
1110
1289
  const row = rows[i];
1111
1290
  if (!row) continue;
1112
- for (let j = 0; j < row.length; j++) {
1291
+ const rowWidth = Math.min(row.length, width);
1292
+ for (let j = 0; j < rowWidth; j++) {
1113
1293
  const v = row[j];
1114
1294
  if (v === null || v === void 0) continue;
1115
1295
  setCell(ws, minRow + i, minCol + j, v);
@@ -1122,11 +1302,7 @@ function setRangeValues(ws, range, rows) {
1122
1302
  */
1123
1303
  function applyToRange(ws, range, visit) {
1124
1304
  const { minRow, maxRow, minCol, maxCol } = parseRange(range);
1125
- for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) {
1126
- let cell = ws.rows.get(r)?.get(c);
1127
- if (!cell) cell = setCell(ws, r, c);
1128
- visit(cell, r, c);
1129
- }
1305
+ for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) visit(ensureCell(ws, r, c), r, c);
1130
1306
  }
1131
1307
  /**
1132
1308
  * Read a rectangular range as a dense 2-D array of values. Empty cells yield
@@ -1675,8 +1851,16 @@ function setAutoFilter(ws, filter) {
1675
1851
  function getAutoFilter(ws) {
1676
1852
  return ws.autoFilter;
1677
1853
  }
1678
- /** Append a table. The id and displayName must be workbook-unique — the caller is responsible. */
1854
+ /**
1855
+ * Append a table, rejecting one whose geometry or column names disagree with
1856
+ * the cells under it: the column count has to match the width of `ref`, `ref`
1857
+ * has to contain the header and totals rows, column names have
1858
+ * to be unique and non-empty, and every header cell has to hold its column's
1859
+ * name as text. The id and displayName must be workbook-unique, and stay the
1860
+ * caller's responsibility: neither is visible from a single sheet.
1861
+ */
1679
1862
  function addTable(ws, table) {
1863
+ validateTableAgainstSheet(ws, table);
1680
1864
  ws.tables.push(table);
1681
1865
  return table;
1682
1866
  }
@@ -1753,6 +1937,6 @@ function removeIgnoredErrors(ws, predicate) {
1753
1937
  return before - ws.ignoredErrors.length;
1754
1938
  }
1755
1939
  //#endregion
1756
- export { iterRows as $, makeRowDimension as $t, getCellsInRow as A, rotateHue as An, setRangeValues as At, getPopulatedRowIndices as B, unhideColumns as Bt, getAutoFilter as C, luminance as Cn, setColumnWidth as Ct, getCellHyperlink as D, pickReadableTextColor as Dn, setDefaultRowHeight as Dt, getCellComment as E, normaliseRgb as En, setDefaultColumnWidth as Et, getMaxRow as F, setSheetViewMode as Ft, groupRows as G, writeRange as Gt, getRowDimension as H, unhideRows as Ht, getMergedCells as I, setSheetZoom as It, hideRow as J, makeLegacyComment as Jt, hideColumn as K, makeHyperlink as Kt, getMergedRangeAt as L, ungroupColumns as Lt, getDataExtent as M, setRowHeight as Mt, getFreezePanes as N, setRowHeights as Nt, getCellsInColumn as O, resolveIndexedColor as On, setFreezePanes as Ot, getMaxCol as P, setSheetTabColor as Pt, iterCells as Q, makeColumnDimension as Qt, getNonEmptyCellCount as R, ungroupRows as Rt, freezePanes as S, lighten as Sn, setColumnDimension as St, getCellByCoord as T, mixColors as Tn, setComment as Tt, getTable as U, unmergeCells as Ut, getRangeValues as V, unhideRow as Vt, groupColumns as W, unmergeCellsAt as Wt, isMergedCell as X, makeFreezePane as Xt, hideRows as Y, freezePaneRef as Yt, isWorksheetEmpty as Z, makeSheetView as Zt, countCellsByKind as _, colorToHex as _n, removeIgnoredErrors as _t, addTable as a, multiCellRangeToString as an, makeWorksheet as at, expandRowGroup as b, hexToHsl as bn, setCell as bt, applyToRange as c, rangeArea as cn, removeAllComments as ct, clearAllCells as d, rangeToString as dn, removeAllHyperlinks as dt, expandRangeStr as en, iterValues as et, clearRange as f, rangesOverlap as fn, removeAllMergedRanges as ft, countCells as g, adjustSaturation as gn, removeHyperlink as gt, copyRange as h, adjustLightness as hn, removeDataValidations as ht, addIgnoredError as i, multiCellRangeContainsCell as in, listTables as it, getColumnDimension as j, setRowDimension as jt, getCellsInRange as k, rgbColor as kn, setHyperlink as kt, autofitColumns as l, rangeContainsCell as ln, removeAllConditionalFormatting as lt, collapseRowGroup as m, unionRange as mn, removeCellWatches as mt, addConditionalFormatting as n, isCellInRange as nn, listDataValidations as nt, appendRow as o, parseMultiCellRange as on, mergeCells as ot, collapseColumnGroup as p, shiftRange as pn, removeAllTables as pt, hideColumns as q, makeSheetProperties as qt, addDataValidation as r, isRangeInRange as rn, listHyperlinks as rt, appendRows as s, parseRange as sn, moveRange as st, addCellWatch as t, intersectionRange as tn, listComments as tt, classifyCellValue as u, rangeContainsRange as un, removeAllDataValidations as ut, deleteCell as v, contrastRatio as vn, removeTable as vt, getCell as w, makeColor as wn, setColumnWidths as wt, findCells as x, hslToHex as xn, setCellByCoord as xt, expandColumnGroup as y, darken as yn, setAutoFilter as yt, getPopulatedColumnIndices as z, unhideColumn as zt };
1940
+ export { iterCells as $, makeColumnDimension as $t, getCellsInRange as A, rgbColor as An, setHyperlink as At, getPopulatedColumnIndices as B, unhideColumn as Bt, findCells as C, lighten as Cn, setColumnDimension as Ct, getCellComment as D, normaliseRgb as Dn, setDefaultColumnWidth as Dt, getCellByCoord as E, mixColors as En, setComment as Et, getMaxCol as F, setSheetTabColor as Ft, groupColumns as G, unmergeCellsAt as Gt, getRangeValues as H, unhideRow as Ht, getMaxRow as I, setSheetViewMode as It, hideColumns as J, makeSheetProperties as Jt, groupRows as K, writeRange as Kt, getMergedCells as L, setSheetZoom as Lt, getColumnDimension as M, setRowDimension as Mt, getDataExtent as N, setRowHeight as Nt, getCellHyperlink as O, pickReadableTextColor as On, setDefaultRowHeight as Ot, getFreezePanes as P, setRowHeights as Pt, isWorksheetEmpty as Q, makeSheetView as Qt, getMergedRangeAt as R, ungroupColumns as Rt, expandRowGroup as S, hslToHex as Sn, setCellByCoord as St, getCell as T, makeColor as Tn, setColumnWidths as Tt, getRowDimension as U, unhideRows as Ut, getPopulatedRowIndices as V, unhideColumns as Vt, getTable as W, unmergeCells as Wt, hideRows as X, freezePaneRef as Xt, hideRow as Y, makeLegacyComment as Yt, isMergedCell as Z, makeFreezePane as Zt, countCellsByKind as _, adjustSaturation as _n, removeHyperlink as _t, addTable as a, multiCellRangeContainsCell as an, listTables as at, ensureCellByCoord as b, darken as bn, setAutoFilter as bt, applyToRange as c, parseRange as cn, moveRange as ct, clearAllCells as d, rangeContainsRange as dn, removeAllDataValidations as dt, makeRowDimension as en, iterRows as et, clearRange as f, rangeToString as fn, removeAllHyperlinks as ft, countCells as g, adjustLightness as gn, removeDataValidations as gt, copyRange as h, unionRange as hn, removeCellWatches as ht, addIgnoredError as i, isRangeInRange as in, listHyperlinks as it, getCellsInRow as j, rotateHue as jn, setRangeValues as jt, getCellsInColumn as k, resolveIndexedColor as kn, setFreezePanes as kt, autofitColumns as l, rangeArea as ln, removeAllComments as lt, collapseRowGroup as m, shiftRange as mn, removeAllTables as mt, addConditionalFormatting as n, intersectionRange as nn, listComments as nt, appendRow as o, multiCellRangeToString as on, makeWorksheet as ot, collapseColumnGroup as p, rangesOverlap as pn, removeAllMergedRanges as pt, hideColumn as q, makeHyperlink as qt, addDataValidation as r, isCellInRange as rn, listDataValidations as rt, appendRows as s, parseMultiCellRange as sn, mergeCells as st, addCellWatch as t, expandRangeStr as tn, iterValues as tt, classifyCellValue as u, rangeContainsCell as un, removeAllConditionalFormatting as ut, deleteCell as v, colorToHex as vn, removeIgnoredErrors as vt, getAutoFilter as w, luminance as wn, setColumnWidth as wt, expandColumnGroup as x, hexToHsl as xn, setCell as xt, ensureCell as y, contrastRatio as yn, removeTable as yt, getNonEmptyCellCount as z, ungroupRows as zt };
1757
1941
 
1758
- //# sourceMappingURL=worksheet-C5Ensf_j.mjs.map
1942
+ //# sourceMappingURL=worksheet-4xW-i8j9.mjs.map