@particle-academy/holy-sheet 2.4.3 → 2.6.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.d.cts CHANGED
@@ -223,7 +223,7 @@ interface SetMetaOp {
223
223
  }
224
224
 
225
225
  /** This package's own version, pinned to package.json by `version.test.ts`. */
226
- declare const VERSION = "2.4.3";
226
+ declare const VERSION = "2.6.0";
227
227
  type Any$6 = any;
228
228
  /**
229
229
  * Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
@@ -295,7 +295,7 @@ type Any$5 = any;
295
295
  * over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
296
296
  */
297
297
  declare class HolySheet {
298
- static readonly VERSION = "2.4.3";
298
+ static readonly VERSION = "2.6.0";
299
299
  validate(schema: Any$5): ValidationError[];
300
300
  toBytes(schema: Any$5): Uint8Array;
301
301
  write(schema: Any$5, path: string): Promise<WriteResult>;
@@ -460,6 +460,22 @@ declare class Normalizer {
460
460
  private normalizeSheet;
461
461
  private normalizeCellMap;
462
462
  private normalizeMerges;
463
+ /**
464
+ * The two ways a width can be stated, folded into one map.
465
+ *
466
+ * `columns[].width` widens the column at that POSITION; the sheet-level
467
+ * `columnWidths` map keys the same columns by 0-based index. Both have been
468
+ * described in the shared `skills/holy-sheet.schema.json` ("Same as
469
+ * columnWidths but per-column") for as long as the field has existed, and only
470
+ * the map was ever read — so a width written the way the schema documents it
471
+ * emitted no `<cols>` element at all, with no error and no repair note
472
+ * (holy-sheet#8, fixed in PHP 2.4.0; this is the twin).
473
+ *
474
+ * The MAP IS APPLIED LAST and wins, matching PHP. It is the mechanism that
475
+ * already worked, so anyone who reached for it to work around this bug must not
476
+ * now find a leftover `width` quietly overriding it.
477
+ */
478
+ private normalizeWidths;
463
479
  private normalizeColumnWidths;
464
480
  private columnFormat;
465
481
  private buildCell;
package/dist/index.d.ts CHANGED
@@ -223,7 +223,7 @@ interface SetMetaOp {
223
223
  }
224
224
 
225
225
  /** This package's own version, pinned to package.json by `version.test.ts`. */
226
- declare const VERSION = "2.4.3";
226
+ declare const VERSION = "2.6.0";
227
227
  type Any$6 = any;
228
228
  /**
229
229
  * Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
@@ -295,7 +295,7 @@ type Any$5 = any;
295
295
  * over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
296
296
  */
297
297
  declare class HolySheet {
298
- static readonly VERSION = "2.4.3";
298
+ static readonly VERSION = "2.6.0";
299
299
  validate(schema: Any$5): ValidationError[];
300
300
  toBytes(schema: Any$5): Uint8Array;
301
301
  write(schema: Any$5, path: string): Promise<WriteResult>;
@@ -460,6 +460,22 @@ declare class Normalizer {
460
460
  private normalizeSheet;
461
461
  private normalizeCellMap;
462
462
  private normalizeMerges;
463
+ /**
464
+ * The two ways a width can be stated, folded into one map.
465
+ *
466
+ * `columns[].width` widens the column at that POSITION; the sheet-level
467
+ * `columnWidths` map keys the same columns by 0-based index. Both have been
468
+ * described in the shared `skills/holy-sheet.schema.json` ("Same as
469
+ * columnWidths but per-column") for as long as the field has existed, and only
470
+ * the map was ever read — so a width written the way the schema documents it
471
+ * emitted no `<cols>` element at all, with no error and no repair note
472
+ * (holy-sheet#8, fixed in PHP 2.4.0; this is the twin).
473
+ *
474
+ * The MAP IS APPLIED LAST and wins, matching PHP. It is the mechanism that
475
+ * already worked, so anyone who reached for it to work around this bug must not
476
+ * now find a leftover `width` quietly overriding it.
477
+ */
478
+ private normalizeWidths;
463
479
  private normalizeColumnWidths;
464
480
  private columnFormat;
465
481
  private buildCell;
package/dist/index.js CHANGED
@@ -493,6 +493,54 @@ var DateConverter = {
493
493
  }
494
494
  };
495
495
 
496
+ // src/exceptions.ts
497
+ var SchemaException = class _SchemaException extends Error {
498
+ constructor(errors, message) {
499
+ super(message ?? summarize(errors));
500
+ this.name = "SchemaException";
501
+ this.errors = errors;
502
+ Object.setPrototypeOf(this, _SchemaException.prototype);
503
+ }
504
+ static fromErrors(errors) {
505
+ return new _SchemaException(errors);
506
+ }
507
+ getErrors() {
508
+ return this.errors;
509
+ }
510
+ };
511
+ function summarize(errors) {
512
+ if (errors.length === 1) {
513
+ const e = errors[0];
514
+ return `[holy-sheet] schema invalid at ${e.path}: expected ${e.expected}, got ${e.got}`;
515
+ }
516
+ const first2 = errors[0];
517
+ const rest = errors.length - 1;
518
+ return `[holy-sheet] schema invalid at ${first2.path}: expected ${first2.expected}, got ${first2.got} (+${rest} more)`;
519
+ }
520
+ var UnsupportedFormatException = class _UnsupportedFormatException extends Error {
521
+ constructor(message, path = null, mimetype = null) {
522
+ super(message);
523
+ this.name = "UnsupportedFormatException";
524
+ this.path = path;
525
+ this.mimetype = mimetype;
526
+ Object.setPrototypeOf(this, _UnsupportedFormatException.prototype);
527
+ }
528
+ static notAZip(path = null) {
529
+ return new _UnsupportedFormatException(
530
+ `[holy-sheet] cannot read ${path ?? "this file"}: it is not a zip archive, and xlsx and ods both are`,
531
+ path
532
+ );
533
+ }
534
+ static unknownPackage(path, mimetype) {
535
+ const what = mimetype !== null && mimetype !== "" ? `its mimetype is ${mimetype}` : "it has neither xl/workbook.xml nor an OpenDocument mimetype";
536
+ return new _UnsupportedFormatException(
537
+ `[holy-sheet] cannot read ${path ?? "this file"}: ${what}. Supported: xlsx, ods`,
538
+ path,
539
+ mimetype !== "" ? mimetype : null
540
+ );
541
+ }
542
+ };
543
+
496
544
  // src/schema/theme.ts
497
545
  var Theme = class {
498
546
  constructor(key) {
@@ -570,7 +618,7 @@ var Normalizer = class {
570
618
  const columnFormat = columnFormats.get(c) ?? null;
571
619
  const rowBand = theme.dataFormat(r);
572
620
  const merged = rowBand !== null ? columnFormat ? columnFormat.mergeWith(rowBand) : rowBand : columnFormat;
573
- cells.set(address, this.buildCell(address, value, merged));
621
+ cells.set(address, this.buildCell(address, value, merged, name));
574
622
  });
575
623
  });
576
624
  if (sheet.totals && isPlainObject(sheet.totals) && Object.keys(sheet.totals).length > 0 && rows.length > 0 && columns.length > 0) {
@@ -594,7 +642,7 @@ var Normalizer = class {
594
642
  }
595
643
  }
596
644
  if (sheet.cells != null) {
597
- for (const [address, cell] of this.normalizeCellMap(sheet.cells)) {
645
+ for (const [address, cell] of this.normalizeCellMap(sheet.cells, name)) {
598
646
  const beneath = cells.get(address);
599
647
  cells.set(
600
648
  address,
@@ -613,19 +661,23 @@ var Normalizer = class {
613
661
  name,
614
662
  cells,
615
663
  this.normalizeMerges(sheet.mergedRegions ?? []),
616
- this.normalizeColumnWidths(sheet.columnWidths ?? {}),
664
+ this.normalizeWidths(sheet),
617
665
  toInt(sheet.frozenRows ?? 0),
618
666
  toInt(sheet.frozenCols ?? 0)
619
667
  );
620
668
  }
621
- normalizeCellMap(map) {
669
+ normalizeCellMap(map, sheetName2) {
622
670
  const cells = /* @__PURE__ */ new Map();
623
671
  for (const [address, cellData] of Object.entries(map)) {
624
- if (!isPlainObject(cellData)) {
672
+ if (!isCellSpec(cellData)) {
625
673
  const [bare, formula] = promoteFormula(cellData);
626
674
  cells.set(
627
675
  address,
628
- new Cell(address, formula !== null ? null : this.coerceValue(bare, null), formula)
676
+ new Cell(
677
+ address,
678
+ formula !== null ? null : this.coerceValue(bare, null, address, sheetName2),
679
+ formula
680
+ )
629
681
  );
630
682
  continue;
631
683
  }
@@ -640,7 +692,7 @@ var Normalizer = class {
640
692
  address,
641
693
  new Cell(
642
694
  address,
643
- this.coerceValue(rawValue, format),
695
+ this.coerceValue(rawValue, format, address, sheetName2),
644
696
  cellData?.formula ?? null,
645
697
  format,
646
698
  comment,
@@ -659,6 +711,36 @@ var Normalizer = class {
659
711
  }
660
712
  return out;
661
713
  }
714
+ /**
715
+ * The two ways a width can be stated, folded into one map.
716
+ *
717
+ * `columns[].width` widens the column at that POSITION; the sheet-level
718
+ * `columnWidths` map keys the same columns by 0-based index. Both have been
719
+ * described in the shared `skills/holy-sheet.schema.json` ("Same as
720
+ * columnWidths but per-column") for as long as the field has existed, and only
721
+ * the map was ever read — so a width written the way the schema documents it
722
+ * emitted no `<cols>` element at all, with no error and no repair note
723
+ * (holy-sheet#8, fixed in PHP 2.4.0; this is the twin).
724
+ *
725
+ * The MAP IS APPLIED LAST and wins, matching PHP. It is the mechanism that
726
+ * already worked, so anyone who reached for it to work around this bug must not
727
+ * now find a leftover `width` quietly overriding it.
728
+ */
729
+ normalizeWidths(sheet) {
730
+ const out = /* @__PURE__ */ new Map();
731
+ for (const [colIdx, columnDef] of (sheet.columns ?? []).entries()) {
732
+ if (columnDef === null || typeof columnDef !== "object") continue;
733
+ if (columnDef.width === void 0) continue;
734
+ const index = ColumnWidths.index(colIdx);
735
+ const width = ColumnWidths.width(columnDef.width);
736
+ if (index === null || width === null) continue;
737
+ out.set(index, width);
738
+ }
739
+ for (const [index, width] of this.normalizeColumnWidths(sheet.columnWidths ?? {})) {
740
+ out.set(index, width);
741
+ }
742
+ return new Map([...out.entries()].sort((a, b) => a[0] - b[0]));
743
+ }
662
744
  normalizeColumnWidths(widths) {
663
745
  const out = /* @__PURE__ */ new Map();
664
746
  for (const [key, px] of Object.entries(widths)) {
@@ -691,8 +773,8 @@ var Normalizer = class {
691
773
  return null;
692
774
  }
693
775
  }
694
- buildCell(address, value, columnFormat) {
695
- if (isPlainObject(value)) {
776
+ buildCell(address, value, columnFormat, sheetName2) {
777
+ if (isCellSpec(value)) {
696
778
  const cellFormat = isPlainObject(value.format) ? CellFormat.fromInput(value.format) : null;
697
779
  const merged = columnFormat ? columnFormat.mergeWith(cellFormat) : cellFormat;
698
780
  const comment = isPlainObject(value.comment) ? new CellComment(
@@ -703,7 +785,7 @@ var Normalizer = class {
703
785
  const rawValue = value.value ?? null;
704
786
  return new Cell(
705
787
  address,
706
- this.coerceValue(rawValue, merged),
788
+ this.coerceValue(rawValue, merged, address, sheetName2),
707
789
  value.formula ?? null,
708
790
  merged,
709
791
  comment,
@@ -713,12 +795,12 @@ var Normalizer = class {
713
795
  const [bare, formula] = promoteFormula(value);
714
796
  return new Cell(
715
797
  address,
716
- formula !== null ? null : this.coerceValue(bare, columnFormat),
798
+ formula !== null ? null : this.coerceValue(bare, columnFormat, address, sheetName2),
717
799
  formula,
718
800
  columnFormat
719
801
  );
720
802
  }
721
- coerceValue(value, format) {
803
+ coerceValue(value, format, address = "?", sheetName2) {
722
804
  if (value === null || value === void 0) return null;
723
805
  const df = format?.displayFormat;
724
806
  if (df === "date" || df === "datetime") {
@@ -732,12 +814,36 @@ var Normalizer = class {
732
814
  if (value instanceof Date) {
733
815
  return DateConverter.toSerial(value, true);
734
816
  }
735
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
817
+ if (typeof value === "bigint") {
736
818
  return String(value);
737
819
  }
820
+ if (typeof value === "object" && !Array.isArray(value) && hasOwnToString(value)) {
821
+ return String(value);
822
+ }
823
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
824
+ const where = sheetName2 === void 0 ? address : `${sheetName2}!${address}`;
825
+ const got = typeOf(value);
826
+ throw SchemaException.fromErrors([
827
+ {
828
+ path: where,
829
+ expected: "a string, number, bool, null, Date, or an object with its own toString",
830
+ got,
831
+ value,
832
+ hint: Array.isArray(value) ? 'This cell holds an array. The usual cause is a `rows` entry nested one level too deep: `rows: [[["x"]]]` makes cell A1 an array, where `rows: [["x"]]` makes it the string "x". For a cell with formatting use the object form: {value: "x", format: {...}}.' : "Give the cell a scalar or a Date, or define toString() on the object so the conversion is yours rather than implicit."
833
+ }
834
+ ]);
835
+ }
738
836
  return value;
739
837
  }
740
838
  };
839
+ function isCellSpec(value) {
840
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
841
+ return Object.getPrototypeOf(value) === Object.prototype;
842
+ }
843
+ function hasOwnToString(value) {
844
+ const fn = value.toString;
845
+ return typeof fn === "function" && fn !== Object.prototype.toString;
846
+ }
741
847
  function promoteFormula(value) {
742
848
  if (typeof value === "string" && value.length > 1 && value[0] === "=") {
743
849
  return [null, value.slice(1)];
@@ -1269,54 +1375,6 @@ function phpRound(x, n) {
1269
1375
  return Math.sign(x) * Math.round(Math.abs(x) * factor) / factor;
1270
1376
  }
1271
1377
 
1272
- // src/exceptions.ts
1273
- var SchemaException = class _SchemaException extends Error {
1274
- constructor(errors, message) {
1275
- super(message ?? summarize(errors));
1276
- this.name = "SchemaException";
1277
- this.errors = errors;
1278
- Object.setPrototypeOf(this, _SchemaException.prototype);
1279
- }
1280
- static fromErrors(errors) {
1281
- return new _SchemaException(errors);
1282
- }
1283
- getErrors() {
1284
- return this.errors;
1285
- }
1286
- };
1287
- function summarize(errors) {
1288
- if (errors.length === 1) {
1289
- const e = errors[0];
1290
- return `[holy-sheet] schema invalid at ${e.path}: expected ${e.expected}, got ${e.got}`;
1291
- }
1292
- const first2 = errors[0];
1293
- const rest = errors.length - 1;
1294
- return `[holy-sheet] schema invalid at ${first2.path}: expected ${first2.expected}, got ${first2.got} (+${rest} more)`;
1295
- }
1296
- var UnsupportedFormatException = class _UnsupportedFormatException extends Error {
1297
- constructor(message, path = null, mimetype = null) {
1298
- super(message);
1299
- this.name = "UnsupportedFormatException";
1300
- this.path = path;
1301
- this.mimetype = mimetype;
1302
- Object.setPrototypeOf(this, _UnsupportedFormatException.prototype);
1303
- }
1304
- static notAZip(path = null) {
1305
- return new _UnsupportedFormatException(
1306
- `[holy-sheet] cannot read ${path ?? "this file"}: it is not a zip archive, and xlsx and ods both are`,
1307
- path
1308
- );
1309
- }
1310
- static unknownPackage(path, mimetype) {
1311
- const what = mimetype !== null && mimetype !== "" ? `its mimetype is ${mimetype}` : "it has neither xl/workbook.xml nor an OpenDocument mimetype";
1312
- return new _UnsupportedFormatException(
1313
- `[holy-sheet] cannot read ${path ?? "this file"}: ${what}. Supported: xlsx, ods`,
1314
- path,
1315
- mimetype !== "" ? mimetype : null
1316
- );
1317
- }
1318
- };
1319
-
1320
1378
  // src/schema/repairer.ts
1321
1379
  var VALID_THEMES = ["default", "minimal", "plain", "business"];
1322
1380
  var ISO_DATE2 = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/;
@@ -1563,7 +1621,21 @@ var Validator = class {
1563
1621
  "Each row is an array of cell values, in column order."
1564
1622
  )
1565
1623
  );
1624
+ return;
1566
1625
  }
1626
+ row.forEach((cell, k) => {
1627
+ if (Array.isArray(cell)) {
1628
+ errors.push(
1629
+ error(
1630
+ `${path}.rows[${j}][${k}]`,
1631
+ "a cell value or a cell object",
1632
+ "array",
1633
+ cell,
1634
+ 'This cell is an array, which usually means the row is nested one level too deep: `rows: [[["x"]]]` makes cell A1 an array, where `rows: [["x"]]` makes it "x". For a cell with formatting use the object form: {value: "x", format: {...}}.'
1635
+ )
1636
+ );
1637
+ }
1638
+ });
1567
1639
  });
1568
1640
  }
1569
1641
  }
@@ -1660,6 +1732,17 @@ var Validator = class {
1660
1732
  )
1661
1733
  );
1662
1734
  }
1735
+ if (col.width !== void 0 && ColumnWidths.width(col.width) === null) {
1736
+ errors.push(
1737
+ error(
1738
+ `${path}.width`,
1739
+ "a non-negative number of pixels",
1740
+ typeOf(col.width),
1741
+ col.width,
1742
+ "A column width is a number of pixels, like 120. The sheet-level columnWidths map takes precedence over it."
1743
+ )
1744
+ );
1745
+ }
1663
1746
  return errors;
1664
1747
  }
1665
1748
  };
@@ -5136,7 +5219,7 @@ var holy_sheet_schema_default = {
5136
5219
  };
5137
5220
 
5138
5221
  // src/agent.ts
5139
- var VERSION = "2.4.3";
5222
+ var VERSION = "2.6.0";
5140
5223
  function toU8(input) {
5141
5224
  return input instanceof Uint8Array ? input : new Uint8Array(input);
5142
5225
  }