@particle-academy/holy-sheet 2.6.0 → 2.8.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.
package/dist/index.js CHANGED
@@ -77,9 +77,9 @@ var Inference = {
77
77
  return { header: headerName, type: "auto" };
78
78
  }
79
79
  };
80
- function nonNullSample(values) {
80
+ function nonNullSample(values2) {
81
81
  const out = [];
82
- for (const v of values) {
82
+ for (const v of values2) {
83
83
  if (v === null || v === void 0) continue;
84
84
  out.push(v);
85
85
  if (out.length >= SAMPLE_SIZE) break;
@@ -269,10 +269,10 @@ var CellAddress = {
269
269
 
270
270
  // src/workbook/cell.ts
271
271
  var Cell = class {
272
- constructor(address, value, formula = null, format = null, comment = null, cachedValue = null) {
272
+ constructor(address, value, formula2 = null, format = null, comment = null, cachedValue = null) {
273
273
  this.address = address;
274
274
  this.value = value;
275
- this.formula = formula;
275
+ this.formula = formula2;
276
276
  this.format = format;
277
277
  this.comment = comment;
278
278
  this.cachedValue = cachedValue;
@@ -320,8 +320,8 @@ var ColumnWidths = {
320
320
 
321
321
  // src/workbook/cell-comment.ts
322
322
  var CellComment = class {
323
- constructor(text, author = null, color2 = null) {
324
- this.text = text;
323
+ constructor(text2, author = null, color2 = null) {
324
+ this.text = text2;
325
325
  this.author = author;
326
326
  this.color = color2;
327
327
  }
@@ -415,14 +415,20 @@ var MergedRegion = class {
415
415
  };
416
416
 
417
417
  // src/workbook/sheet.ts
418
- var Sheet = class {
419
- constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0) {
418
+ var Sheet = class _Sheet {
419
+ constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0, rowHeights = /* @__PURE__ */ new Map(), print = null, charts = []) {
420
420
  this.name = name;
421
421
  this.cells = cells;
422
422
  this.mergedRegions = mergedRegions;
423
423
  this.columnWidths = columnWidths;
424
424
  this.frozenRows = frozenRows;
425
425
  this.frozenCols = frozenCols;
426
+ this.rowHeights = rowHeights;
427
+ this.print = print;
428
+ this.charts = charts;
429
+ }
430
+ withCharts(charts) {
431
+ return new _Sheet(this.name, this.cells, this.mergedRegions, this.columnWidths, this.frozenRows, this.frozenCols, this.rowHeights, this.print, charts);
426
432
  }
427
433
  /** rowNumber → (colLetter → Cell), sorted by ascending row number. */
428
434
  rows() {
@@ -453,6 +459,59 @@ var Sheet = class {
453
459
  }
454
460
  };
455
461
 
462
+ // src/workbook/chart-range.ts
463
+ var ChartRange = class _ChartRange {
464
+ constructor(sheet, c1, r1, c2, r2) {
465
+ this.sheet = sheet;
466
+ this.c1 = c1;
467
+ this.r1 = r1;
468
+ this.c2 = c2;
469
+ this.r2 = r2;
470
+ }
471
+ static parse(ref, defaultSheet) {
472
+ if (typeof ref !== "string") return null;
473
+ let text2 = ref.trim();
474
+ let sheet = defaultSheet;
475
+ const bang = text2.lastIndexOf("!");
476
+ if (bang >= 0) {
477
+ let sheetPart = text2.slice(0, bang);
478
+ text2 = text2.slice(bang + 1);
479
+ if (sheetPart.length >= 2 && sheetPart.startsWith("'") && sheetPart.endsWith("'")) {
480
+ sheetPart = sheetPart.slice(1, -1).replace(/''/g, "'");
481
+ }
482
+ if (sheetPart === "") return null;
483
+ sheet = sheetPart;
484
+ }
485
+ const ends = text2.replace(/\$/g, "").split(":");
486
+ if (ends.length > 2) return null;
487
+ const a = CellAddress.parse(ends[0]);
488
+ const b = CellAddress.parse(ends[1] ?? ends[0]);
489
+ if (a === null || b === null || a[1] < 1 || b[1] < 1) return null;
490
+ return new _ChartRange(sheet, Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[0], b[0]), Math.max(a[1], b[1]));
491
+ }
492
+ /** 'Q3 Revenue'!$B$5:$B$7 */
493
+ formula() {
494
+ const from = "$" + CellAddress.letter(this.c1) + "$" + this.r1;
495
+ const to = "$" + CellAddress.letter(this.c2) + "$" + this.r2;
496
+ return "'" + this.sheet.replace(/'/g, "''") + "'!" + from + (from === to ? "" : ":" + to);
497
+ }
498
+ /** "B5:B7" on its own sheet, "'Data'!B5:B7" on another. */
499
+ toSchema(ownSheet) {
500
+ const from = CellAddress.letter(this.c1) + this.r1;
501
+ const to = CellAddress.letter(this.c2) + this.r2;
502
+ const ref = from === to ? from : from + ":" + to;
503
+ return this.sheet === ownSheet ? ref : "'" + this.sheet.replace(/'/g, "''") + "'!" + ref;
504
+ }
505
+ /** A1 addresses, row by row. */
506
+ addresses() {
507
+ const out = [];
508
+ for (let r = this.r1; r <= this.r2; r++) {
509
+ for (let c = this.c1; c <= this.c2; c++) out.push(CellAddress.letter(c) + r);
510
+ }
511
+ return out;
512
+ }
513
+ };
514
+
456
515
  // src/workbook/workbook.ts
457
516
  var Workbook = class {
458
517
  constructor(sheets, meta = {}) {
@@ -663,20 +722,23 @@ var Normalizer = class {
663
722
  this.normalizeMerges(sheet.mergedRegions ?? []),
664
723
  this.normalizeWidths(sheet),
665
724
  toInt(sheet.frozenRows ?? 0),
666
- toInt(sheet.frozenCols ?? 0)
725
+ toInt(sheet.frozenCols ?? 0),
726
+ normalizeRowHeights(sheet.rowHeights),
727
+ normalizePrint(sheet.print),
728
+ normalizeCharts(sheet.charts, name, cells)
667
729
  );
668
730
  }
669
731
  normalizeCellMap(map, sheetName2) {
670
732
  const cells = /* @__PURE__ */ new Map();
671
733
  for (const [address, cellData] of Object.entries(map)) {
672
734
  if (!isCellSpec(cellData)) {
673
- const [bare, formula] = promoteFormula(cellData);
735
+ const [bare, formula2] = promoteFormula(cellData);
674
736
  cells.set(
675
737
  address,
676
738
  new Cell(
677
739
  address,
678
- formula !== null ? null : this.coerceValue(bare, null, address, sheetName2),
679
- formula
740
+ formula2 !== null ? null : this.coerceValue(bare, null, address, sheetName2),
741
+ formula2
680
742
  )
681
743
  );
682
744
  continue;
@@ -792,11 +854,11 @@ var Normalizer = class {
792
854
  value.computedValue ?? null
793
855
  );
794
856
  }
795
- const [bare, formula] = promoteFormula(value);
857
+ const [bare, formula2] = promoteFormula(value);
796
858
  return new Cell(
797
859
  address,
798
- formula !== null ? null : this.coerceValue(bare, columnFormat, address, sheetName2),
799
- formula,
860
+ formula2 !== null ? null : this.coerceValue(bare, columnFormat, address, sheetName2),
861
+ formula2,
800
862
  columnFormat
801
863
  );
802
864
  }
@@ -854,6 +916,81 @@ function toInt(v) {
854
916
  const n = Math.trunc(Number(v));
855
917
  return Number.isFinite(n) ? n : 0;
856
918
  }
919
+ var CHART_TYPES = ["column", "bar", "line", "pie"];
920
+ function normalizeCharts(charts, sheetName2, cells) {
921
+ if (!isPlainObject(charts) && !Array.isArray(charts)) return [];
922
+ let lastCol = -1;
923
+ for (const address of cells.keys()) {
924
+ const parsed = CellAddress.parse(address);
925
+ if (parsed !== null) lastCol = Math.max(lastCol, parsed[0]);
926
+ }
927
+ const out = [];
928
+ for (const chart of Object.values(charts)) {
929
+ if (!isPlainObject(chart) || !CHART_TYPES.includes(chart.type)) continue;
930
+ const series = [];
931
+ const rawSeries = isPlainObject(chart.series) || Array.isArray(chart.series) ? Object.values(chart.series) : [];
932
+ for (const s of rawSeries) {
933
+ const values2 = isPlainObject(s) || Array.isArray(s) ? ChartRange.parse(s.values, sheetName2) : null;
934
+ if (values2 === null) continue;
935
+ const rec = s;
936
+ const m = typeof rec.color === "string" ? /^#([0-9A-Fa-f]{6})$/.exec(rec.color) : null;
937
+ series.push({ name: typeof rec.name === "string" ? rec.name : null, values: values2, color: m ? m[1].toUpperCase() : null });
938
+ }
939
+ if (series.length === 0) continue;
940
+ const stack = out.length;
941
+ let from = [lastCol + 2, 1 + 18 * stack];
942
+ let to = [from[0] + 8, from[1] + 16];
943
+ if (isPlainObject(chart.position) || Array.isArray(chart.position)) {
944
+ const p = chart.position;
945
+ const a = typeof p.from === "string" ? CellAddress.parse(p.from) : null;
946
+ const b = typeof p.to === "string" ? CellAddress.parse(p.to) : null;
947
+ if (a !== null && b !== null && b[0] > a[0] && b[1] > a[1]) {
948
+ from = [a[0], a[1] - 1];
949
+ to = [b[0], b[1] - 1];
950
+ }
951
+ }
952
+ out.push({
953
+ type: chart.type,
954
+ title: typeof chart.title === "string" && chart.title !== "" ? chart.title : null,
955
+ categories: ChartRange.parse(chart.categories, sheetName2),
956
+ series,
957
+ from,
958
+ to,
959
+ legend: typeof chart.legend === "boolean" ? chart.legend : null,
960
+ dataLabels: chart.dataLabels === true,
961
+ format: typeof chart.format === "string" && chart.format !== "" ? chart.format : null
962
+ });
963
+ }
964
+ return out;
965
+ }
966
+ function phpNumeric(v) {
967
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
968
+ return isNumericString(v) ? numericStringToNumber(v) : null;
969
+ }
970
+ function normalizeRowHeights(map) {
971
+ const out = /* @__PURE__ */ new Map();
972
+ if (!isPlainObject(map) && !Array.isArray(map)) return out;
973
+ for (const [row, px] of Object.entries(map)) {
974
+ const n = phpNumeric(px);
975
+ if (/^[1-9]\d{0,6}$/.test(row) && n !== null && n >= 0) out.set(parseInt(row, 10), n);
976
+ }
977
+ return new Map([...out.entries()].sort((a, b) => a[0] - b[0]));
978
+ }
979
+ function normalizePrint(print) {
980
+ if (!isPlainObject(print) && !Array.isArray(print)) return null;
981
+ const p = print;
982
+ const orientation = p.orientation === "portrait" || p.orientation === "landscape" ? p.orientation : null;
983
+ const fitToWidth = p.fitToWidth === true;
984
+ let footer = null;
985
+ if (isPlainObject(p.footer) || Array.isArray(p.footer)) {
986
+ const f = p.footer;
987
+ const slot = (k) => typeof f[k] === "string" ? f[k] : "";
988
+ footer = { left: slot("left"), center: slot("center"), right: slot("right") };
989
+ if (footer.left === "" && footer.center === "" && footer.right === "") footer = null;
990
+ }
991
+ if (orientation === null && !fitToWidth && footer === null) return null;
992
+ return { orientation, fitToWidth, footer };
993
+ }
857
994
 
858
995
  // src/schema/formula-linter.ts
859
996
  var ERR_VALUE = "#VALUE!";
@@ -916,9 +1053,9 @@ var FormulaLinter = class {
916
1053
  isError(v) {
917
1054
  return typeof v === "string" && v.length > 0 && v[0] === "#";
918
1055
  }
919
- evaluate(formula, defaultSheet, stack) {
1056
+ evaluate(formula2, defaultSheet, stack) {
920
1057
  try {
921
- const c = { tokens: this.tokenize(formula), pos: 0 };
1058
+ const c = { tokens: this.tokenize(formula2), pos: 0 };
922
1059
  const result = this.parseExpr(c, defaultSheet, stack);
923
1060
  if (c.pos < c.tokens.length) return ERR_NAME;
924
1061
  return result;
@@ -1179,13 +1316,13 @@ var FormulaLinter = class {
1179
1316
  const row1 = Math.min(a[1], b[1]);
1180
1317
  const col2 = Math.max(a[0], b[0]);
1181
1318
  const row2 = Math.max(a[1], b[1]);
1182
- const values = [];
1319
+ const values2 = [];
1183
1320
  for (let r = row1; r <= row2; r++) {
1184
1321
  for (let col = col1; col <= col2; col++) {
1185
- values.push(this.resolveCell(sheet, CellAddress.letter(col) + r, stack));
1322
+ values2.push(this.resolveCell(sheet, CellAddress.letter(col) + r, stack));
1186
1323
  }
1187
1324
  }
1188
- return values;
1325
+ return values2;
1189
1326
  }
1190
1327
  expectOp(c, op) {
1191
1328
  const t = c.tokens[c.pos];
@@ -1317,12 +1454,12 @@ var FormulaLinter = class {
1317
1454
  return truthy ? args[1] : args[2] ?? false;
1318
1455
  }
1319
1456
  // ---- Hints ----
1320
- hint(error2, formula, sheet) {
1457
+ hint(error2, formula2, sheet) {
1321
1458
  switch (error2) {
1322
1459
  case ERR_VALUE:
1323
- return this.hintValue(formula, sheet);
1460
+ return this.hintValue(formula2, sheet);
1324
1461
  case ERR_REF:
1325
- return this.hintRef(formula);
1462
+ return this.hintRef(formula2);
1326
1463
  case ERR_NAME:
1327
1464
  return "The formula references an unknown function or has a syntax error. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.";
1328
1465
  case ERR_DIV0:
@@ -1333,8 +1470,8 @@ var FormulaLinter = class {
1333
1470
  return "Formula evaluation failed.";
1334
1471
  }
1335
1472
  }
1336
- hintRef(formula) {
1337
- for (const m of formula.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
1473
+ hintRef(formula2) {
1474
+ for (const m of formula2.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
1338
1475
  const name = this.cleanSheetName(m[1]);
1339
1476
  if (!this.sheetNames.has(name.toLowerCase())) {
1340
1477
  return `The formula refers to a sheet named '${name}', and this workbook has no such sheet. Its sheets are: ${[...this.sheetNames.values()].join(", ")}. Quote a name that contains spaces or punctuation: 'My Sheet'!A1.`;
@@ -1342,10 +1479,10 @@ var FormulaLinter = class {
1342
1479
  }
1343
1480
  return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1344
1481
  }
1345
- hintValue(formula, sheet) {
1482
+ hintValue(formula2, sheet) {
1346
1483
  const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
1347
1484
  const offenders = [];
1348
- for (const m of formula.matchAll(re)) {
1485
+ for (const m of formula2.matchAll(re)) {
1349
1486
  const sheetName2 = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
1350
1487
  const a1 = m[2].toUpperCase() + m[3];
1351
1488
  const key = sheetName2 + "!" + a1;
@@ -1468,11 +1605,11 @@ var Repairer = class {
1468
1605
  sheet.columns.forEach((col, colIdx) => {
1469
1606
  if (!isPlainObject(col)) return;
1470
1607
  if (col.type !== void 0 && col.type !== "auto") return;
1471
- const values = [];
1608
+ const values2 = [];
1472
1609
  for (const row of sheet.rows) {
1473
- if (Array.isArray(row) && colIdx < row.length) values.push(row[colIdx]);
1610
+ if (Array.isArray(row) && colIdx < row.length) values2.push(row[colIdx]);
1474
1611
  }
1475
- const nonNull = values.filter((v) => v !== null && v !== void 0);
1612
+ const nonNull = values2.filter((v) => v !== null && v !== void 0);
1476
1613
  if (nonNull.length === 0) return;
1477
1614
  const allDateLike = nonNull.every((v) => typeof v === "string" && ISO_DATE2.test(v));
1478
1615
  if (allDateLike) {
@@ -1701,6 +1838,98 @@ var Validator = class {
1701
1838
  }
1702
1839
  }
1703
1840
  }
1841
+ if (sheet.rowHeights != null) {
1842
+ if (!isPlainObject(sheet.rowHeights) && !Array.isArray(sheet.rowHeights)) {
1843
+ errors.push(
1844
+ error(
1845
+ `${path}.rowHeights`,
1846
+ "object keyed by 1-based row number",
1847
+ typeOf(sheet.rowHeights),
1848
+ sheet.rowHeights,
1849
+ 'Row heights map a row NUMBER to pixels: {"1": 32, "4": 28}.'
1850
+ )
1851
+ );
1852
+ } else {
1853
+ for (const [row, px] of Object.entries(sheet.rowHeights)) {
1854
+ const n = typeof px === "number" ? px : isNumericString(px) ? numericStringToNumber(px) : null;
1855
+ if (!/^[1-9]\d{0,6}$/.test(row)) {
1856
+ errors.push(error(
1857
+ `${path}.rowHeights.${row}`,
1858
+ "a 1-based row number",
1859
+ typeOf(row),
1860
+ row,
1861
+ 'Keys are row numbers as Excel shows them: "1" is the first row.'
1862
+ ));
1863
+ } else if (n === null || !Number.isFinite(n) || n < 0) {
1864
+ errors.push(error(
1865
+ `${path}.rowHeights.${row}`,
1866
+ "a non-negative number of pixels",
1867
+ typeOf(px),
1868
+ px,
1869
+ "A height is a number of pixels, like 28 (20 is a default row)."
1870
+ ));
1871
+ }
1872
+ }
1873
+ }
1874
+ }
1875
+ if (sheet.charts != null) {
1876
+ errors.push(...validateCharts(sheet.charts, path, typeof sheet.name === "string" ? sheet.name : ""));
1877
+ }
1878
+ if (sheet.print != null) {
1879
+ const print = sheet.print;
1880
+ if (!isPlainObject(print) && !Array.isArray(print)) {
1881
+ errors.push(error(
1882
+ `${path}.print`,
1883
+ "object {orientation?, fitToWidth?, footer?}",
1884
+ typeOf(print),
1885
+ print,
1886
+ 'Print setup: {"orientation": "landscape", "fitToWidth": true, "footer": {"right": "Page {page} of {pages}"}}.'
1887
+ ));
1888
+ } else {
1889
+ if (print.orientation != null && print.orientation !== "portrait" && print.orientation !== "landscape") {
1890
+ errors.push(error(
1891
+ `${path}.print.orientation`,
1892
+ "one of: portrait, landscape",
1893
+ typeOf(print.orientation),
1894
+ print.orientation,
1895
+ "Use landscape for a sheet wider than it is tall."
1896
+ ));
1897
+ }
1898
+ if (print.fitToWidth != null && typeof print.fitToWidth !== "boolean") {
1899
+ errors.push(error(
1900
+ `${path}.print.fitToWidth`,
1901
+ "boolean",
1902
+ typeOf(print.fitToWidth),
1903
+ print.fitToWidth,
1904
+ "true scales the sheet to one page wide when printed."
1905
+ ));
1906
+ }
1907
+ if (print.footer != null) {
1908
+ if (!isPlainObject(print.footer) && !Array.isArray(print.footer)) {
1909
+ errors.push(error(
1910
+ `${path}.print.footer`,
1911
+ "object {left?, center?, right?}",
1912
+ typeOf(print.footer),
1913
+ print.footer,
1914
+ "A footer has up to three slots of text."
1915
+ ));
1916
+ } else {
1917
+ const footer = print.footer;
1918
+ for (const slot of ["left", "center", "right"]) {
1919
+ if (footer[slot] != null && typeof footer[slot] !== "string") {
1920
+ errors.push(error(
1921
+ `${path}.print.footer.${slot}`,
1922
+ "string",
1923
+ typeOf(footer[slot]),
1924
+ footer[slot],
1925
+ "Footer text; {page} and {pages} print the page number and count."
1926
+ ));
1927
+ }
1928
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+ }
1704
1933
  return errors;
1705
1934
  }
1706
1935
  validateColumn(col, path) {
@@ -1746,6 +1975,113 @@ var Validator = class {
1746
1975
  return errors;
1747
1976
  }
1748
1977
  };
1978
+ function validateCharts(charts, path, sheetName2) {
1979
+ if (!Array.isArray(charts)) {
1980
+ return [
1981
+ error(
1982
+ `${path}.charts`,
1983
+ "a list of charts",
1984
+ typeOf(charts),
1985
+ charts,
1986
+ 'charts is a list: [{"type": "column", "categories": "A5:A7", "series": [{"name": "Revenue", "values": "B5:B7"}]}].'
1987
+ )
1988
+ ];
1989
+ }
1990
+ const errors = [];
1991
+ charts.forEach((chart, i) => {
1992
+ const at2 = `${path}.charts[${i}]`;
1993
+ if (!isPlainObject(chart) && !Array.isArray(chart)) {
1994
+ errors.push(error(at2, "object", typeOf(chart), chart, "A chart is an object with a type and series."));
1995
+ return;
1996
+ }
1997
+ const c = chart;
1998
+ if (!CHART_TYPES.includes(c.type)) {
1999
+ errors.push(error(
2000
+ `${at2}.type`,
2001
+ "one of: " + CHART_TYPES.join(", "),
2002
+ typeOf(c.type ?? null),
2003
+ c.type ?? null,
2004
+ "column compares values across categories; bar does the same horizontally (long labels); line shows change over time; pie shows parts of one whole."
2005
+ ));
2006
+ }
2007
+ const series = c.series;
2008
+ if (!Array.isArray(series) || series.length === 0) {
2009
+ errors.push(error(
2010
+ `${at2}.series`,
2011
+ "a non-empty list of series",
2012
+ typeOf(series ?? null),
2013
+ series ?? null,
2014
+ 'Each series names the range it plots: [{"name": "Revenue", "values": "B5:B7"}].'
2015
+ ));
2016
+ } else {
2017
+ series.forEach((s, j) => {
2018
+ const isObj = isPlainObject(s) || Array.isArray(s);
2019
+ const values2 = isObj ? s.values ?? null : s;
2020
+ if (!isObj || ChartRange.parse(values2, sheetName2) === null) {
2021
+ errors.push(error(
2022
+ `${at2}.series[${j}].values`,
2023
+ 'a cell range like "B5:B7"',
2024
+ typeOf(values2),
2025
+ values2,
2026
+ `A range of cells: "B5:B7" on this sheet, or "'Other sheet'!B5:B7".`
2027
+ ));
2028
+ } else {
2029
+ const color2 = s.color;
2030
+ if (color2 != null && (typeof color2 !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(color2))) {
2031
+ errors.push(error(`${at2}.series[${j}].color`, "#RRGGBB", typeOf(color2), color2, 'A hex colour, like "#1F3A5F".'));
2032
+ }
2033
+ }
2034
+ });
2035
+ }
2036
+ if (c.categories != null && ChartRange.parse(c.categories, sheetName2) === null) {
2037
+ errors.push(error(
2038
+ `${at2}.categories`,
2039
+ 'a cell range like "A5:A7"',
2040
+ typeOf(c.categories),
2041
+ c.categories,
2042
+ 'The labels along the axis, as a range: "A5:A7".'
2043
+ ));
2044
+ }
2045
+ if (c.position != null) {
2046
+ const p = c.position;
2047
+ const isObj = isPlainObject(p) || Array.isArray(p);
2048
+ const a = isObj && typeof p.from === "string" ? CellAddress.parse(p.from) : null;
2049
+ const b = isObj && typeof p.to === "string" ? CellAddress.parse(p.to) : null;
2050
+ if (a === null || b === null || b[0] <= a[0] || b[1] <= a[1]) {
2051
+ errors.push(error(
2052
+ `${at2}.position`,
2053
+ '{"from": top-left cell, "to": bottom-right cell}',
2054
+ typeOf(p),
2055
+ p,
2056
+ 'Two cells, the second below and to the right of the first: {"from": "E2", "to": "L18"} is 7 columns by 16 rows.'
2057
+ ));
2058
+ }
2059
+ }
2060
+ for (const key of ["title", "format"]) {
2061
+ if (c[key] != null && typeof c[key] !== "string") {
2062
+ errors.push(error(
2063
+ `${at2}.${key}`,
2064
+ "string",
2065
+ typeOf(c[key]),
2066
+ c[key],
2067
+ key === "format" ? 'An Excel number format for the axis and labels, like "$#,##0" or "0.0%".' : "The chart title."
2068
+ ));
2069
+ }
2070
+ }
2071
+ for (const key of ["dataLabels", "legend"]) {
2072
+ if (c[key] != null && typeof c[key] !== "boolean") {
2073
+ errors.push(error(
2074
+ `${at2}.${key}`,
2075
+ "boolean",
2076
+ typeOf(c[key]),
2077
+ c[key],
2078
+ key === "dataLabels" ? "true prints each value on the chart." : "Defaults to on for several series or a pie."
2079
+ ));
2080
+ }
2081
+ }
2082
+ });
2083
+ return errors;
2084
+ }
1749
2085
  function error(path, expected, got, value, hint) {
1750
2086
  return { path, expected, got, value, hint };
1751
2087
  }
@@ -2262,15 +2598,15 @@ var FormatSniffer = {
2262
2598
  };
2263
2599
 
2264
2600
  // src/reader/ods/ods-formula.ts
2265
- function odsFormulaToA1(formula) {
2601
+ function odsFormulaToA1(formula2) {
2266
2602
  let grammar = "of";
2267
- const m = /^(of|oooc|msoxl):/i.exec(formula);
2603
+ const m = /^(of|oooc|msoxl):/i.exec(formula2);
2268
2604
  if (m) {
2269
2605
  grammar = m[1].toLowerCase();
2270
- formula = formula.slice(m[0].length);
2606
+ formula2 = formula2.slice(m[0].length);
2271
2607
  }
2272
- if (formula.startsWith("=")) formula = formula.slice(1);
2273
- return grammar === "msoxl" ? formula : translate(formula);
2608
+ if (formula2.startsWith("=")) formula2 = formula2.slice(1);
2609
+ return grammar === "msoxl" ? formula2 : translate(formula2);
2274
2610
  }
2275
2611
  function translate(s) {
2276
2612
  let out = "";
@@ -2619,8 +2955,8 @@ var OdsStyles = class {
2619
2955
  switch (el2.name) {
2620
2956
  case "number-style": {
2621
2957
  if (has2("scientific-number") || has2("fraction")) return null;
2622
- for (const text of parts.filter((p) => p.name === "text")) {
2623
- const iso = currencyCode(ownText(text).replace(/[-()"\u00A0]/g, ""));
2958
+ for (const text2 of parts.filter((p) => p.name === "text")) {
2959
+ const iso = currencyCode(ownText(text2).replace(/[-()"\u00A0]/g, ""));
2624
2960
  if (iso !== null) return { displayFormat: "currency", decimals: decimals ?? 0, currency: iso };
2625
2961
  }
2626
2962
  if (!number) return null;
@@ -2790,6 +3126,45 @@ function sheetToSchema(sheet) {
2790
3126
  }
2791
3127
  if (sheet.frozenRows > 0) out["frozenRows"] = sheet.frozenRows;
2792
3128
  if (sheet.frozenCols > 0) out["frozenCols"] = sheet.frozenCols;
3129
+ if (sheet.rowHeights.size > 0) {
3130
+ const heights = {};
3131
+ for (const [k, v] of sheet.rowHeights) heights[k] = v;
3132
+ out["rowHeights"] = heights;
3133
+ }
3134
+ if (sheet.print !== null) {
3135
+ const print = {};
3136
+ if (sheet.print.orientation !== null) print["orientation"] = sheet.print.orientation;
3137
+ if (sheet.print.fitToWidth) print["fitToWidth"] = true;
3138
+ if (sheet.print.footer !== null) {
3139
+ const footer = {};
3140
+ for (const k of ["left", "center", "right"]) if (sheet.print.footer[k] !== "") footer[k] = sheet.print.footer[k];
3141
+ print["footer"] = footer;
3142
+ }
3143
+ out["print"] = print;
3144
+ }
3145
+ if (sheet.charts.length > 0) {
3146
+ out["charts"] = sheet.charts.map((c) => chartToSchema(c, sheet.name));
3147
+ }
3148
+ return out;
3149
+ }
3150
+ function chartToSchema(chart, sheetName2) {
3151
+ const out = { type: chart.type };
3152
+ if (chart.title !== null) out["title"] = chart.title;
3153
+ if (chart.categories !== null) out["categories"] = chart.categories.toSchema(sheetName2);
3154
+ out["series"] = chart.series.map((s) => {
3155
+ const series = {};
3156
+ if (s.name !== null) series["name"] = s.name;
3157
+ series["values"] = s.values.toSchema(sheetName2);
3158
+ if (s.color !== null) series["color"] = "#" + s.color;
3159
+ return series;
3160
+ });
3161
+ out["position"] = {
3162
+ from: CellAddress.letter(chart.from[0]) + (chart.from[1] + 1),
3163
+ to: CellAddress.letter(chart.to[0]) + (chart.to[1] + 1)
3164
+ };
3165
+ if (chart.dataLabels) out["dataLabels"] = true;
3166
+ if (chart.legend !== null) out["legend"] = chart.legend;
3167
+ if (chart.format !== null) out["format"] = chart.format;
2793
3168
  return out;
2794
3169
  }
2795
3170
  function cellToSchema(cell) {
@@ -2897,10 +3272,10 @@ function parseXml(src, options = {}) {
2897
3272
  if (namespaces) root.content = [];
2898
3273
  const stack = [root];
2899
3274
  const scopes = [{ xml: XML_NS }];
2900
- const appendText = (text) => {
3275
+ const appendText = (text2) => {
2901
3276
  const top = stack[stack.length - 1];
2902
- top.text += text;
2903
- if (namespaces && text !== "") top.content.push(text);
3277
+ top.text += text2;
3278
+ if (namespaces && text2 !== "") top.content.push(text2);
2904
3279
  };
2905
3280
  while (i < n) {
2906
3281
  if (src[i] === "<") {
@@ -3121,7 +3496,7 @@ function hasContent(cell) {
3121
3496
  }
3122
3497
  function readCell(cell) {
3123
3498
  const type = attr(cell, NS.OFFICE, "value-type") ?? null;
3124
- const text = paragraphs(cell);
3499
+ const text2 = paragraphs(cell);
3125
3500
  let value = null;
3126
3501
  let seconds = null;
3127
3502
  let dateHasTime = false;
@@ -3150,20 +3525,20 @@ function readCell(cell) {
3150
3525
  case "string": {
3151
3526
  const error2 = attr(cell, NS.CALCEXT, "value-type") === "error";
3152
3527
  const stored = attr(cell, NS.OFFICE, "string-value");
3153
- value = stored !== void 0 && !error2 ? stored : text ?? "";
3528
+ value = stored !== void 0 && !error2 ? stored : text2 ?? "";
3154
3529
  break;
3155
3530
  }
3156
3531
  default:
3157
- value = text !== null && text !== "" ? text : null;
3532
+ value = text2 !== null && text2 !== "" ? text2 : null;
3158
3533
  }
3159
- let formula = null;
3534
+ let formula2 = null;
3160
3535
  let cached = null;
3161
3536
  const formulaAttr = attr(cell, NS.TABLE, "formula");
3162
3537
  if (formulaAttr !== void 0) {
3163
3538
  const translated = odsFormulaToA1(formulaAttr);
3164
3539
  const literalBoolean = type === "boolean" && /^[ \t\n\r\f\v]*(TRUE|FALSE)\(\)[ \t\n\r\f\v]*$/i.test(translated);
3165
3540
  if (!literalBoolean) {
3166
- formula = translated;
3541
+ formula2 = translated;
3167
3542
  cached = seconds !== null ? serial(seconds) : value;
3168
3543
  value = null;
3169
3544
  seconds = null;
@@ -3179,7 +3554,7 @@ function readCell(cell) {
3179
3554
  const currency = attr(cell, NS.OFFICE, "currency");
3180
3555
  return {
3181
3556
  value,
3182
- formula,
3557
+ formula: formula2,
3183
3558
  cached,
3184
3559
  type,
3185
3560
  currency: currency !== void 0 ? phpTrim(currency) : null,
@@ -3248,6 +3623,127 @@ function readMeta(xml) {
3248
3623
  return out;
3249
3624
  }
3250
3625
 
3626
+ // src/reader/chart-parser.ts
3627
+ function descendants(node, name) {
3628
+ const out = [];
3629
+ const stack = node ? [...node.children].reverse() : [];
3630
+ while (stack.length > 0) {
3631
+ const n = stack.pop();
3632
+ if (n.name === name) out.push(n);
3633
+ for (let i = n.children.length - 1; i >= 0; i--) stack.push(n.children[i]);
3634
+ }
3635
+ return out;
3636
+ }
3637
+ function load(xml) {
3638
+ if (/<!DOCTYPE/i.test(xml)) return null;
3639
+ return parseXml(xml);
3640
+ }
3641
+ function wrap2(root) {
3642
+ return { name: "#document", attrs: {}, children: [root], text: "" };
3643
+ }
3644
+ var ChartParser = {
3645
+ anchors(drawingXml) {
3646
+ const root = load(drawingXml);
3647
+ if (!root) return [];
3648
+ const out = [];
3649
+ for (const anchor of descendants(wrap2(root), "twoCellAnchor")) {
3650
+ const from = marker(anchor, "from");
3651
+ const to = marker(anchor, "to");
3652
+ const chart = descendants(anchor, "chart")[0];
3653
+ const rel = chart ? at(chart, "id") ?? "" : "";
3654
+ if (from !== null && to !== null && rel !== "") out.push({ from, to, rel });
3655
+ }
3656
+ return out;
3657
+ },
3658
+ chart(chartXml, sheetName2, from, to) {
3659
+ const root = load(chartXml);
3660
+ if (!root) return null;
3661
+ const doc = wrap2(root);
3662
+ let plot;
3663
+ let type = null;
3664
+ for (const tag of ["barChart", "lineChart", "pieChart"]) {
3665
+ const found = descendants(doc, tag)[0];
3666
+ if (found) {
3667
+ plot = found;
3668
+ type = tag === "lineChart" ? "line" : tag === "pieChart" ? "pie" : at(el(found, "barDir"), "val") === "bar" ? "bar" : "column";
3669
+ break;
3670
+ }
3671
+ }
3672
+ if (!plot || type === null) return null;
3673
+ let categories = null;
3674
+ const series = [];
3675
+ for (const ser of els(plot, "ser")) {
3676
+ const valRef = formula(ser, "val");
3677
+ const values2 = valRef !== null ? ChartRange.parse(valRef, sheetName2) : null;
3678
+ if (values2 === null) continue;
3679
+ if (categories === null) {
3680
+ const catRef = formula(ser, "cat");
3681
+ categories = catRef !== null ? ChartRange.parse(catRef, sheetName2) : null;
3682
+ }
3683
+ series.push({ name: seriesName(ser), values: values2, color: seriesColor(ser) });
3684
+ }
3685
+ if (series.length === 0) return null;
3686
+ const labels = el(plot, "dLbls");
3687
+ const dataLabels2 = labels !== void 0 && at(el(labels, "showVal"), "val") === "1";
3688
+ let format = null;
3689
+ for (const host of [labels, descendants(doc, "valAx")[0]]) {
3690
+ const numFmt = host ? el(host, "numFmt") : void 0;
3691
+ if (numFmt && at(numFmt, "sourceLinked") === "0" && (at(numFmt, "formatCode") ?? "") !== "") {
3692
+ format = at(numFmt, "formatCode");
3693
+ break;
3694
+ }
3695
+ }
3696
+ let title = null;
3697
+ const titleEl = descendants(doc, "title")[0];
3698
+ if (titleEl) {
3699
+ const text2 = descendants(titleEl, "t").map((t) => t.text).join("");
3700
+ title = text2 !== "" ? text2 : null;
3701
+ }
3702
+ return {
3703
+ type,
3704
+ title,
3705
+ categories,
3706
+ series,
3707
+ from,
3708
+ to,
3709
+ legend: descendants(doc, "legend").length > 0 ? true : null,
3710
+ dataLabels: dataLabels2,
3711
+ format
3712
+ };
3713
+ }
3714
+ };
3715
+ function marker(anchor, tag) {
3716
+ const m = el(anchor, tag);
3717
+ if (!m) return null;
3718
+ const col = descendants(m, "col")[0]?.text.trim() ?? "";
3719
+ const row = descendants(m, "row")[0]?.text.trim() ?? "";
3720
+ return /^\d+$/.test(col) && /^\d+$/.test(row) ? [parseInt(col, 10), parseInt(row, 10)] : null;
3721
+ }
3722
+ function formula(ser, name) {
3723
+ const host = el(ser, name);
3724
+ if (!host) return null;
3725
+ for (const ref of ["numRef", "strRef"]) {
3726
+ const f = el(el(host, ref), "f");
3727
+ if (f && f.text.trim() !== "") return f.text.trim();
3728
+ }
3729
+ return null;
3730
+ }
3731
+ function seriesName(ser) {
3732
+ const tx = el(ser, "tx");
3733
+ if (!tx) return null;
3734
+ const v = el(tx, "v");
3735
+ if (v) return v.text;
3736
+ const cached = descendants(tx, "v")[0];
3737
+ return cached ? cached.text : null;
3738
+ }
3739
+ function seriesColor(ser) {
3740
+ const spPr = el(ser, "spPr");
3741
+ if (!spPr) return null;
3742
+ const clr = descendants(spPr, "srgbClr")[0];
3743
+ const val = (clr ? at(clr, "val") ?? "" : "").toUpperCase();
3744
+ return /^[0-9A-F]{6}$/.test(val) ? val : null;
3745
+ }
3746
+
3251
3747
  // src/reader/comments-parser.ts
3252
3748
  var CommentsParser = {
3253
3749
  parse(commentsXml) {
@@ -3412,9 +3908,62 @@ var WorksheetParser = {
3412
3908
  const merges = parseMerges(xml);
3413
3909
  const columnWidths = parseColumnWidths(xml);
3414
3910
  const [frozenRows, frozenCols] = parseFrozen(xml);
3415
- return new Sheet(name, cells, merges, columnWidths, frozenRows, frozenCols);
3911
+ return new Sheet(name, cells, merges, columnWidths, frozenRows, frozenCols, parseRowHeights(xml), parsePrint(xml));
3416
3912
  }
3417
3913
  };
3914
+ function parseRowHeights(xml) {
3915
+ const out = /* @__PURE__ */ new Map();
3916
+ const sheetData = el(xml, "sheetData");
3917
+ if (!sheetData) return out;
3918
+ for (const row of els(sheetData, "row")) {
3919
+ const r = parseInt(at(row, "r") ?? "0", 10);
3920
+ const custom = at(row, "customHeight");
3921
+ const ht = at(row, "ht");
3922
+ if (r > 0 && ht !== void 0 && (custom === "1" || custom === "true")) {
3923
+ out.set(r, Math.round(parseFloat(ht) / 0.75 * 100) / 100);
3924
+ }
3925
+ }
3926
+ return out;
3927
+ }
3928
+ function parsePrint(xml) {
3929
+ const fitToPage = ["1", "true"].includes(at(el(el(xml, "sheetPr"), "pageSetUpPr"), "fitToPage") ?? "");
3930
+ const setup = el(xml, "pageSetup");
3931
+ const o = at(setup, "orientation");
3932
+ const orientation = o === "portrait" || o === "landscape" ? o : null;
3933
+ const fitToWidth = fitToPage && setup !== void 0 && at(setup, "fitToWidth") === "1" && ["0", ""].includes(at(setup, "fitToHeight") ?? "");
3934
+ const oddFooter = el(el(xml, "headerFooter"), "oddFooter");
3935
+ const footer = oddFooter !== void 0 ? parseFooterCodes(oddFooter.text) : null;
3936
+ if (orientation === null && !fitToWidth && footer === null) return null;
3937
+ return { orientation, fitToWidth, footer };
3938
+ }
3939
+ function parseFooterCodes(codes) {
3940
+ const slots = { left: "", center: "", right: "" };
3941
+ let current = "center";
3942
+ const len = codes.length;
3943
+ for (let i = 0; i < len; i++) {
3944
+ const ch = codes[i];
3945
+ if (ch !== "&" || i + 1 >= len) {
3946
+ slots[current] += ch;
3947
+ continue;
3948
+ }
3949
+ const next = codes[++i];
3950
+ if (next === "&") slots[current] += "&";
3951
+ else if (next === "L") current = "left";
3952
+ else if (next === "C") current = "center";
3953
+ else if (next === "R") current = "right";
3954
+ else if (next === "P") slots[current] += "{page}";
3955
+ else if (next === "N") slots[current] += "{pages}";
3956
+ else if (next === '"') {
3957
+ const close = codes.indexOf('"', i + 1);
3958
+ i = close === -1 ? len : close;
3959
+ } else if (/[0-9]/.test(next)) {
3960
+ while (i + 1 < len && /[0-9]/.test(codes[i + 1])) i++;
3961
+ } else if (next === "K") {
3962
+ i = Math.min(len - 1, i + 6);
3963
+ }
3964
+ }
3965
+ return slots.left === "" && slots.center === "" && slots.right === "" ? null : slots;
3966
+ }
3418
3967
  function parseCells(xml, stylesIndex, comments, sharedStrings) {
3419
3968
  const cells = /* @__PURE__ */ new Map();
3420
3969
  const sheetData = el(xml, "sheetData");
@@ -3426,13 +3975,13 @@ function parseCells(xml, stylesIndex, comments, sharedStrings) {
3426
3975
  const type = c.attrs["t"] ?? "n";
3427
3976
  const styleIdx = c.attrs["s"] !== void 0 ? parseInt(c.attrs["s"], 10) : 0;
3428
3977
  const format = stylesIndex[styleIdx] ?? null;
3429
- let formula = null;
3978
+ let formula2 = null;
3430
3979
  let cachedValue = null;
3431
3980
  let value = null;
3432
3981
  const f = el(c, "f");
3433
3982
  const v = el(c, "v");
3434
3983
  if (f) {
3435
- formula = f.text;
3984
+ formula2 = f.text;
3436
3985
  cachedValue = v ? coerceValue(v.text, type) : null;
3437
3986
  } else if (type === "inlineStr" && el(c, "is") && el(el(c, "is"), "t")) {
3438
3987
  value = el(el(c, "is"), "t").text;
@@ -3443,7 +3992,7 @@ function parseCells(xml, stylesIndex, comments, sharedStrings) {
3443
3992
  value = coerceValue(v.text, type);
3444
3993
  }
3445
3994
  const comment = comments[address] ?? null;
3446
- cells.set(address, new Cell(address, value, formula, format, comment, cachedValue));
3995
+ cells.set(address, new Cell(address, value, formula2, format, comment, cachedValue));
3447
3996
  }
3448
3997
  }
3449
3998
  return cells;
@@ -3534,7 +4083,26 @@ var XlsxReader = class {
3534
4083
  if (commentsXml) comments = { ...comments, ...CommentsParser.parse(commentsXml) };
3535
4084
  }
3536
4085
  }
3537
- sheets.push(WorksheetParser.parse(worksheetXml, name, stylesIndex, comments, sharedStrings));
4086
+ let sheet = WorksheetParser.parse(worksheetXml, name, stylesIndex, comments, sharedStrings);
4087
+ if (sheetRelsXml) {
4088
+ const charts = [];
4089
+ for (const dr of Object.values(RelsParser.byType(RelsParser.parse(sheetRelsXml), "/drawing"))) {
4090
+ const drawingPath = resolveRelativePath(sheetPath, dr.Target);
4091
+ const slash = drawingPath.lastIndexOf("/");
4092
+ const drawingXml = getText(drawingPath);
4093
+ const drawingRelsXml = getText(drawingPath.slice(0, slash) + "/_rels/" + drawingPath.slice(slash + 1) + ".rels");
4094
+ if (drawingXml === null || drawingRelsXml === null) continue;
4095
+ const drawingRels = RelsParser.parse(drawingRelsXml);
4096
+ for (const anchor of ChartParser.anchors(drawingXml)) {
4097
+ const target2 = drawingRels[anchor.rel]?.Target;
4098
+ const chartXml = typeof target2 === "string" ? getText(resolveRelativePath(drawingPath, target2)) : null;
4099
+ const chart = chartXml !== null ? ChartParser.chart(chartXml, name, anchor.from, anchor.to) : null;
4100
+ if (chart !== null) charts.push(chart);
4101
+ }
4102
+ }
4103
+ if (charts.length > 0) sheet = sheet.withCharts(charts);
4104
+ }
4105
+ sheets.push(sheet);
3538
4106
  i++;
3539
4107
  }
3540
4108
  const meta = this.parseDocProps(getText("docProps/core.xml"));
@@ -3788,8 +4356,132 @@ function hexToArgb(hex2) {
3788
4356
  return "FF000000";
3789
4357
  }
3790
4358
 
4359
+ // src/writer/chart-xml.ts
4360
+ var NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
4361
+ var NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
4362
+ var NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
4363
+ var ChartXml = {
4364
+ drawing(charts, firstChartNum) {
4365
+ let anchors = "";
4366
+ charts.forEach((chart, k) => {
4367
+ const id = k + 2;
4368
+ const num = firstChartNum + k;
4369
+ anchors += '<xdr:twoCellAnchor editAs="oneCell">' + marker2("from", chart.from) + marker2("to", chart.to) + `<xdr:graphicFrame macro=""><xdr:nvGraphicFramePr><xdr:cNvPr id="${id}" name="Chart ${num}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></xdr:xfrm><a:graphic><a:graphicData uri="${NS_C}"><c:chart xmlns:c="${NS_C}" xmlns:r="${NS_R}" r:id="rId${k + 1}"/></a:graphicData></a:graphic></xdr:graphicFrame><xdr:clientData/></xdr:twoCellAnchor>`;
4370
+ });
4371
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="${NS_A}">` + anchors + "</xdr:wsDr>";
4372
+ },
4373
+ drawingRels(charts, firstChartNum) {
4374
+ let rels = "";
4375
+ charts.forEach((_, k) => {
4376
+ rels += `<Relationship Id="rId${k + 1}" Type="${NS_R}/chart" Target="../charts/chart${firstChartNum + k}.xml"/>`;
4377
+ });
4378
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
4379
+ },
4380
+ chart(chart, workbook) {
4381
+ const type = chart.type;
4382
+ let series = "";
4383
+ chart.series.forEach((s, i) => {
4384
+ series += seriesXml(i, s, chart, workbook);
4385
+ });
4386
+ const labels = chart.dataLabels ? dataLabels(type, chart.format) : "";
4387
+ let plot;
4388
+ if (type === "line") {
4389
+ plot = '<c:lineChart><c:grouping val="standard"/><c:varyColors val="0"/>' + series + labels + '<c:marker val="1"/><c:axId val="10"/><c:axId val="20"/></c:lineChart>';
4390
+ } else if (type === "pie") {
4391
+ plot = '<c:pieChart><c:varyColors val="1"/>' + series + labels + '<c:firstSliceAng val="0"/></c:pieChart>';
4392
+ } else {
4393
+ plot = `<c:barChart><c:barDir val="${type === "bar" ? "bar" : "col"}"/><c:grouping val="clustered"/><c:varyColors val="0"/>` + series + labels + '<c:gapWidth val="80"/><c:axId val="10"/><c:axId val="20"/></c:barChart>';
4394
+ }
4395
+ const axesXml = type === "pie" ? "" : axes(type === "bar", chart.format);
4396
+ const legend = chart.legend ?? (chart.series.length > 1 || type === "pie");
4397
+ const title = chart.title !== null ? `<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="1200" b="1"/></a:pPr><a:r><a:rPr lang="en-US" sz="1200" b="1"/><a:t>${text(chart.title)}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val="0"/></c:title><c:autoTitleDeleted val="0"/>` : '<c:autoTitleDeleted val="1"/>';
4398
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><c:chartSpace xmlns:c="${NS_C}" xmlns:a="${NS_A}" xmlns:r="${NS_R}"><c:roundedCorners val="0"/><c:chart>` + title + "<c:plotArea><c:layout/>" + plot + axesXml + "</c:plotArea>" + (legend ? '<c:legend><c:legendPos val="b"/><c:overlay val="0"/></c:legend>' : "") + '<c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/></c:chart><c:spPr><a:noFill/><a:ln><a:noFill/></a:ln></c:spPr>' + textProps("595959", 900) + "</c:chartSpace>";
4399
+ }
4400
+ };
4401
+ function marker2(tag, at2) {
4402
+ return `<xdr:${tag}><xdr:col>${at2[0]}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>${at2[1]}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:${tag}>`;
4403
+ }
4404
+ function seriesXml(i, s, chart, workbook) {
4405
+ const type = chart.type;
4406
+ const name = s.name !== null ? `<c:tx><c:v>${text(s.name)}</c:v></c:tx>` : "";
4407
+ let fill = "";
4408
+ if (s.color !== null && type !== "pie") {
4409
+ fill = type === "line" ? `<c:spPr><a:ln w="28575" cap="rnd"><a:solidFill><a:srgbClr val="${s.color}"/></a:solidFill><a:round/></a:ln></c:spPr>` : `<c:spPr><a:solidFill><a:srgbClr val="${s.color}"/></a:solidFill></c:spPr>`;
4410
+ }
4411
+ const invert = type === "column" || type === "bar" ? '<c:invertIfNegative val="0"/>' : "";
4412
+ const mark = type === "line" ? '<c:marker><c:symbol val="circle"/><c:size val="6"/></c:marker>' : "";
4413
+ const cat = chart.categories !== null ? "<c:cat>" + strRef(chart.categories, workbook) + "</c:cat>" : "";
4414
+ const smooth = type === "line" ? '<c:smooth val="0"/>' : "";
4415
+ return `<c:ser><c:idx val="${i}"/><c:order val="${i}"/>` + name + fill + invert + mark + cat + "<c:val>" + numRef(s.values, workbook) + "</c:val>" + smooth + "</c:ser>";
4416
+ }
4417
+ function strRef(range, workbook) {
4418
+ let pts = "";
4419
+ const vals = values(range, workbook);
4420
+ vals.forEach((v, idx) => {
4421
+ if (v !== null) pts += `<c:pt idx="${idx}"><c:v>${text(category(v))}</c:v></c:pt>`;
4422
+ });
4423
+ return `<c:strRef><c:f>${text(range.formula())}</c:f><c:strCache><c:ptCount val="${vals.length}"/>` + pts + "</c:strCache></c:strRef>";
4424
+ }
4425
+ function numRef(range, workbook) {
4426
+ let pts = "";
4427
+ const vals = values(range, workbook);
4428
+ vals.forEach((v, idx) => {
4429
+ if (typeof v === "number") pts += `<c:pt idx="${idx}"><c:v>${formatFloat(v)}</c:v></c:pt>`;
4430
+ });
4431
+ return `<c:numRef><c:f>${text(range.formula())}</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="${vals.length}"/>` + pts + "</c:numCache></c:numRef>";
4432
+ }
4433
+ function values(range, workbook) {
4434
+ const sheet = workbook.sheets.find((s) => s.name === range.sheet);
4435
+ return range.addresses().map((address) => {
4436
+ const cell = sheet?.cells.get(address);
4437
+ if (cell === void 0) return null;
4438
+ const v = cell.formula !== null ? cell.cachedValue : cell.value;
4439
+ return v === void 0 ? null : v;
4440
+ });
4441
+ }
4442
+ function category(v) {
4443
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
4444
+ if (typeof v === "number") return formatFloat(v);
4445
+ return v;
4446
+ }
4447
+ function dataLabels(type, format) {
4448
+ const pos = type === "line" ? "t" : type === "pie" ? "bestFit" : "outEnd";
4449
+ return "<c:dLbls>" + (format !== null ? `<c:numFmt formatCode="${attr2(format)}" sourceLinked="0"/>` : "") + "<c:spPr><a:noFill/><a:ln><a:noFill/></a:ln></c:spPr>" + textProps("404040", 900) + `<c:dLblPos val="${pos}"/><c:showLegendKey val="0"/><c:showVal val="1"/><c:showCatName val="0"/><c:showSerName val="0"/><c:showPercent val="0"/><c:showBubbleSize val="0"/></c:dLbls>`;
4450
+ }
4451
+ function axes(horizontal, format) {
4452
+ const catPos = horizontal ? "l" : "b";
4453
+ const valPos = horizontal ? "b" : "l";
4454
+ const numFmt = format !== null ? `<c:numFmt formatCode="${attr2(format)}" sourceLinked="0"/>` : '<c:numFmt formatCode="General" sourceLinked="1"/>';
4455
+ return `<c:catAx><c:axId val="10"/><c:scaling><c:orientation val="${horizontal ? "maxMin" : "minMax"}"/></c:scaling><c:delete val="0"/><c:axPos val="${catPos}"/><c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="none"/><c:minorTickMark val="none"/><c:tickLblPos val="low"/><c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="BFBFBF"/></a:solidFill></a:ln></c:spPr><c:crossAx val="20"/><c:crosses val="autoZero"/><c:auto val="1"/><c:lblAlgn val="ctr"/><c:lblOffset val="100"/><c:noMultiLvlLbl val="0"/></c:catAx><c:valAx><c:axId val="20"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="${valPos}"/><c:majorGridlines><c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="E5E7EB"/></a:solidFill></a:ln></c:spPr></c:majorGridlines>` + numFmt + `<c:majorTickMark val="none"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:spPr><a:ln><a:noFill/></a:ln></c:spPr><c:crossAx val="10"/><c:crosses val="${horizontal ? "max" : "autoZero"}"/><c:crossBetween val="between"/></c:valAx>`;
4456
+ }
4457
+ function textProps(color2, size) {
4458
+ return `<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="${size}"><a:solidFill><a:srgbClr val="${color2}"/></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US"/></a:p></c:txPr>`;
4459
+ }
4460
+ function text(s) {
4461
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4462
+ }
4463
+ function attr2(s) {
4464
+ return text(s).replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4465
+ }
4466
+
3791
4467
  // src/writer/xlsx-writer.ts
3792
4468
  var encoder2 = new TextEncoder();
4469
+ function footerCodes(footer) {
4470
+ let codes = "";
4471
+ for (const [slot, code] of [["left", "&L"], ["center", "&C"], ["right", "&R"]]) {
4472
+ if (footer[slot] !== "") {
4473
+ codes += code + footer[slot].replace(/&/g, "&&").replace(/\{pages\}/g, "&N").replace(/\{page\}/g, "&P");
4474
+ }
4475
+ }
4476
+ return codes;
4477
+ }
4478
+ function drawingRelId(comments) {
4479
+ return comments ? "rId3" : "rId1";
4480
+ }
4481
+ function decimal(value) {
4482
+ const out = value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
4483
+ return out === "" || out === "-0" ? "0" : out;
4484
+ }
3793
4485
  var XlsxWriter = class {
3794
4486
  toBytes(workbook) {
3795
4487
  const styles = new StylesRegistry();
@@ -3809,14 +4501,29 @@ var XlsxWriter = class {
3809
4501
  add("xl/styles.xml", styles.toXml());
3810
4502
  add("docProps/core.xml", this.coreXml(workbook));
3811
4503
  add("docProps/app.xml", this.appXml());
4504
+ const firstChart = [];
4505
+ let next = 1;
4506
+ workbook.sheets.forEach((sheet, i) => {
4507
+ firstChart[i] = next;
4508
+ next += sheet.charts.length;
4509
+ });
3812
4510
  workbook.sheets.forEach((sheet, i) => {
3813
4511
  const n = i + 1;
3814
4512
  add(`xl/worksheets/sheet${n}.xml`, sheetXmls[i]);
4513
+ if (sheet.hasComments() || sheet.charts.length > 0) {
4514
+ add(`xl/worksheets/_rels/sheet${n}.xml.rels`, this.sheetRelsXml(n, sheet.hasComments(), sheet.charts.length > 0));
4515
+ }
3815
4516
  if (sheet.hasComments()) {
3816
- add(`xl/worksheets/_rels/sheet${n}.xml.rels`, this.sheetRelsXml(n));
3817
4517
  add(`xl/comments${n}.xml`, this.commentsXml(sheet));
3818
4518
  add(`xl/drawings/vmlDrawing${n}.vml`, this.vmlDrawingXml(sheet));
3819
4519
  }
4520
+ if (sheet.charts.length > 0) {
4521
+ add(`xl/drawings/drawing${n}.xml`, ChartXml.drawing(sheet.charts, firstChart[i]));
4522
+ add(`xl/drawings/_rels/drawing${n}.xml.rels`, ChartXml.drawingRels(sheet.charts, firstChart[i]));
4523
+ sheet.charts.forEach((chart, k) => {
4524
+ add(`xl/charts/chart${firstChart[i] + k}.xml`, ChartXml.chart(chart, workbook));
4525
+ });
4526
+ }
3820
4527
  });
3821
4528
  return zipSync(files);
3822
4529
  }
@@ -3830,6 +4537,16 @@ var XlsxWriter = class {
3830
4537
  const n = i + 1;
3831
4538
  overrides += `<Override PartName="/xl/comments${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>`;
3832
4539
  }
4540
+ let chartNum = 1;
4541
+ workbook.sheets.forEach((sheet, i) => {
4542
+ if (sheet.charts.length === 0) return;
4543
+ const n = i + 1;
4544
+ overrides += `<Override PartName="/xl/drawings/drawing${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>`;
4545
+ for (let k = 0; k < sheet.charts.length; k++) {
4546
+ overrides += `<Override PartName="/xl/charts/chart${chartNum}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>`;
4547
+ chartNum++;
4548
+ }
4549
+ });
3833
4550
  let defaults = '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>';
3834
4551
  if (sheetsWithComments.length > 0) {
3835
4552
  defaults += '<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>';
@@ -3858,8 +4575,16 @@ var XlsxWriter = class {
3858
4575
  rels += `<Relationship Id="rId${workbook.sheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
3859
4576
  return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
3860
4577
  }
3861
- sheetRelsXml(sheetNum) {
3862
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing${sheetNum}.vml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../comments${sheetNum}.xml"/></Relationships>`;
4578
+ /** Comments keep rId1 (vml) and rId2 (comments); the chart drawing takes the next id. */
4579
+ sheetRelsXml(sheetNum, comments, charts) {
4580
+ let rels = "";
4581
+ if (comments) {
4582
+ rels += `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing${sheetNum}.vml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../comments${sheetNum}.xml"/>`;
4583
+ }
4584
+ if (charts) {
4585
+ rels += `<Relationship Id="${drawingRelId(comments)}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing${sheetNum}.xml"/>`;
4586
+ }
4587
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
3863
4588
  }
3864
4589
  sheetXml(sheet, styles) {
3865
4590
  let sheetViewsXml = "";
@@ -3878,14 +4603,20 @@ var XlsxWriter = class {
3878
4603
  }
3879
4604
  colsXml += "</cols>";
3880
4605
  }
4606
+ const rows = sheet.rows();
4607
+ for (const rowIndex of sheet.rowHeights.keys()) {
4608
+ if (!rows.has(rowIndex)) rows.set(rowIndex, /* @__PURE__ */ new Map());
4609
+ }
3881
4610
  let rowsXml = "";
3882
- for (const [rowIndex, row] of sheet.rows()) {
4611
+ for (const [rowIndex, row] of [...rows.entries()].sort((a, b) => a[0] - b[0])) {
3883
4612
  let cellsXml = "";
3884
4613
  const sortedCols = [...row.keys()].sort();
3885
4614
  for (const col of sortedCols) {
3886
4615
  cellsXml += this.cellXml(row.get(col), styles);
3887
4616
  }
3888
- rowsXml += `<row r="${rowIndex}">${cellsXml}</row>`;
4617
+ const height = sheet.rowHeights.get(rowIndex);
4618
+ const ht = height !== void 0 ? ` ht="${decimal(height * 0.75)}" customHeight="1"` : "";
4619
+ rowsXml += cellsXml === "" ? `<row r="${rowIndex}"${ht}/>` : `<row r="${rowIndex}"${ht}>${cellsXml}</row>`;
3889
4620
  }
3890
4621
  let mergesXml = "";
3891
4622
  if (sheet.mergedRegions.length > 0) {
@@ -3894,7 +4625,14 @@ var XlsxWriter = class {
3894
4625
  mergesXml += "</mergeCells>";
3895
4626
  }
3896
4627
  const legacyDrawing = sheet.hasComments() ? '<legacyDrawing r:id="rId1"/>' : "";
3897
- return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' + sheetViewsXml + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + legacyDrawing + "</worksheet>";
4628
+ const print = sheet.print;
4629
+ const sheetPrXml = print?.fitToWidth ? '<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr>' : "";
4630
+ let pageSetupXml = "";
4631
+ if (print !== null && (print.orientation !== null || print.fitToWidth)) {
4632
+ pageSetupXml = "<pageSetup" + (print.orientation !== null ? ` orientation="${print.orientation}"` : "") + (print.fitToWidth ? ' fitToWidth="1" fitToHeight="0"' : "") + "/>";
4633
+ }
4634
+ const headerFooterXml = print?.footer ? `<headerFooter><oddFooter>${this.escape(footerCodes(print.footer))}</oddFooter></headerFooter>` : "";
4635
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' + sheetPrXml + sheetViewsXml + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + pageSetupXml + headerFooterXml + (sheet.charts.length > 0 ? `<drawing r:id="${drawingRelId(sheet.hasComments())}"/>` : "") + legacyDrawing + "</worksheet>";
3898
4636
  }
3899
4637
  cellXml(cell, styles) {
3900
4638
  const ref = cell.address;
@@ -3934,8 +4672,8 @@ var XlsxWriter = class {
3934
4672
  const author = entry.comment.author ?? "Author";
3935
4673
  if (!authors.includes(author)) authors.push(author);
3936
4674
  const authorIdx = authors.indexOf(author);
3937
- const text = this.escape(entry.comment.text);
3938
- commentList += `<comment ref="${entry.address}" authorId="${authorIdx}"><text><r><t xml:space="preserve">${text}</t></r></text></comment>`;
4675
+ const text2 = this.escape(entry.comment.text);
4676
+ commentList += `<comment ref="${entry.address}" authorId="${authorIdx}"><text><r><t xml:space="preserve">${text2}</t></r></text></comment>`;
3939
4677
  }
3940
4678
  let authorsXml = "<authors>";
3941
4679
  for (const a of authors) authorsXml += `<author>${this.escape(a)}</author>`;
@@ -4815,19 +5553,19 @@ function canonEntries(root) {
4815
5553
  continue;
4816
5554
  }
4817
5555
  stack.pop();
4818
- const text = top.list ? "[" + top.parts.join(",") + "]" : "{" + top.parts.join(",") + "}";
5556
+ const text2 = top.list ? "[" + top.parts.join(",") + "]" : "{" + top.parts.join(",") + "}";
4819
5557
  const parent = stack[stack.length - 1];
4820
5558
  if (parent === void 0) {
4821
- return text;
5559
+ return text2;
4822
5560
  }
4823
- parent.parts.push(member(parent, top.key, text));
5561
+ parent.parts.push(member(parent, top.key, text2));
4824
5562
  }
4825
5563
  }
4826
5564
  function entriesFor(value) {
4827
5565
  return Array.isArray(value) ? Array.from(value, (item, i) => [String(i), item]) : entriesOf(value);
4828
5566
  }
4829
- function member(frame, key, text) {
4830
- return frame.list ? text : quote(key) + ":" + text;
5567
+ function member(frame, key, text2) {
5568
+ return frame.list ? text2 : quote(key) + ":" + text2;
4831
5569
  }
4832
5570
  function scalar(value) {
4833
5571
  switch (typeof value) {
@@ -4845,11 +5583,11 @@ function scalar(value) {
4845
5583
  return JSON.stringify(value) ?? "null";
4846
5584
  }
4847
5585
  var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
4848
- function quote(text) {
4849
- if (LONE_SURROGATE.test(text)) {
5586
+ function quote(text2) {
5587
+ if (LONE_SURROGATE.test(text2)) {
4850
5588
  throw unencodable("Single unpaired UTF-16 surrogate");
4851
5589
  }
4852
- return JSON.stringify(text);
5590
+ return JSON.stringify(text2);
4853
5591
  }
4854
5592
  function unencodable(reason) {
4855
5593
  return new TypeError(`[holy-sheet] cannot compare a value JSON cannot hold: ${reason}`);
@@ -5011,6 +5749,68 @@ var holy_sheet_schema_default = {
5011
5749
  type: "integer",
5012
5750
  minimum: 0,
5013
5751
  description: "How many columns stay visible while the sheet scrolls sideways. Set to 1 when the first column is the label for the row."
5752
+ },
5753
+ rowHeights: {
5754
+ type: "object",
5755
+ description: 'Row heights in pixels, keyed by 1-based row NUMBER as Excel shows it: {"1": 32, "4": 28}. A default row is 20; give a title row or a header row room (28-36) instead of packing text against its fill.',
5756
+ propertyNames: { pattern: "^[1-9][0-9]*$" },
5757
+ additionalProperties: { type: "number", minimum: 0 }
5758
+ },
5759
+ charts: {
5760
+ type: "array",
5761
+ description: "Native Excel charts over CELL RANGES on this sheet: edit a figure and the chart follows. Use one whenever the sheet compares values -- a board reader sees the comparison before the numbers. Keep them quiet: a title that states the point, dataLabels with a format, one colour per series.",
5762
+ items: {
5763
+ type: "object",
5764
+ additionalProperties: false,
5765
+ required: ["type", "series"],
5766
+ properties: {
5767
+ type: { type: "string", enum: ["column", "bar", "line", "pie"], description: "column compares categories; bar does it horizontally (long labels, ranked lists); line shows change over time; pie shows parts of ONE whole (at most ~5 slices)." },
5768
+ title: { type: "string" },
5769
+ categories: { type: "string", description: `The labels along the axis (or the pie's slices), as a range: "A5:A7". Another sheet: "'Data'!A5:A7".` },
5770
+ series: {
5771
+ type: "array",
5772
+ minItems: 1,
5773
+ items: {
5774
+ type: "object",
5775
+ additionalProperties: false,
5776
+ required: ["values"],
5777
+ properties: {
5778
+ name: { type: "string", description: "Shown in the legend." },
5779
+ values: { type: "string", description: 'The numbers, as a range the same length as categories: "B5:B7".' },
5780
+ color: { type: "string", description: "#RRGGBB. Ignored for a pie, whose slices take a palette." }
5781
+ }
5782
+ }
5783
+ },
5784
+ position: {
5785
+ type: "object",
5786
+ additionalProperties: false,
5787
+ description: 'Where the chart sits: from its top-left cell to its bottom-right cell, {"from": "E2", "to": "L18"}. Omitted: to the right of the data, 8 columns by 16 rows.',
5788
+ properties: { from: { type: "string" }, to: { type: "string" } }
5789
+ },
5790
+ dataLabels: { type: "boolean", description: "Print each value on the chart. Usually right for a board chart." },
5791
+ legend: { type: "boolean", description: "Defaults to on for several series or a pie, off for one series." },
5792
+ format: { type: "string", description: 'Excel number format for the value axis and labels: "$#,##0", "0.0%", "$#,##0,K".' }
5793
+ }
5794
+ }
5795
+ },
5796
+ print: {
5797
+ type: "object",
5798
+ additionalProperties: false,
5799
+ description: "How the sheet prints. A sheet wider than a page splits its columns across pages unless fitToWidth is set.",
5800
+ properties: {
5801
+ orientation: { type: "string", enum: ["portrait", "landscape"] },
5802
+ fitToWidth: { type: "boolean", description: "Scale the sheet to one page wide (as many pages tall as it needs)." },
5803
+ footer: {
5804
+ type: "object",
5805
+ additionalProperties: false,
5806
+ description: 'A printed footer: up to three slots. {page} and {pages} print the page number and count: {"left": "Q3 Revenue Review", "right": "Page {page} of {pages}"}.',
5807
+ properties: {
5808
+ left: { type: "string" },
5809
+ center: { type: "string" },
5810
+ right: { type: "string" }
5811
+ }
5812
+ }
5813
+ }
5014
5814
  }
5015
5815
  }
5016
5816
  },
@@ -5219,7 +6019,7 @@ var holy_sheet_schema_default = {
5219
6019
  };
5220
6020
 
5221
6021
  // src/agent.ts
5222
- var VERSION = "2.6.0";
6022
+ var VERSION = "2.8.0";
5223
6023
  function toU8(input) {
5224
6024
  return input instanceof Uint8Array ? input : new Uint8Array(input);
5225
6025
  }