@particle-academy/holy-sheet 2.4.2 → 2.5.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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @particle-academy/holy-sheet
2
2
 
3
- [![Fancy UI suite](art/fancy-ui.svg)](https://particle.academy)
3
+ [![Fancified](art/fancified.svg)](https://particle.academy)
4
4
 
5
5
  Zero-dependency, **isomorphic** (browser + Node) `.xlsx` writer + reader +
6
6
  formula linter for agentic document creation. The Node/TypeScript mirror of the
7
7
  PHP [`particle-academy/holy-sheet`](https://github.com/Particle-Academy/holy-sheet)
8
- — same schema in, same `.xlsx` out (feature-parity with PHP 1.2.0; the PHP
9
- package has since shipped 1.3.0, whose additions are not yet mirrored here).
8
+ — same schema in, same `.xlsx` out (the PHP package's `dumpJson` and `Toolkit`
9
+ are not mirrored here; `CHANGELOG.md` records which PHP releases each fix mirrors).
10
10
 
11
11
  ```ts
12
12
  import { Agent } from "@particle-academy/holy-sheet";
package/dist/index.cjs CHANGED
@@ -287,6 +287,39 @@ var Cell = class {
287
287
  }
288
288
  };
289
289
 
290
+ // src/schema/column-widths.ts
291
+ var ColumnWidths = {
292
+ MAX_INDEX: 16383,
293
+ index(key) {
294
+ if (typeof key === "number") {
295
+ return Number.isInteger(key) && key >= 0 && key <= ColumnWidths.MAX_INDEX ? key : null;
296
+ }
297
+ if (typeof key === "string" && /^[0-9]{1,5}$/.test(key)) {
298
+ const index = parseInt(key, 10);
299
+ return index <= ColumnWidths.MAX_INDEX ? index : null;
300
+ }
301
+ return null;
302
+ },
303
+ width(px) {
304
+ if (typeof px === "number") {
305
+ return Number.isFinite(px) && px >= 0 ? px : null;
306
+ }
307
+ if (typeof px === "string" && /^[0-9]+(\.[0-9]+)?$/.test(px)) {
308
+ return Number(px);
309
+ }
310
+ return null;
311
+ },
312
+ /**
313
+ * A column letter key (`"B"`, `"aa"`) as an index, for repair only. One or two
314
+ * letters (A to ZZ): a longer run like `"abc"` is more likely a mistake than
315
+ * column ABC, and a repair must not guess.
316
+ */
317
+ fromLetters(key) {
318
+ if (typeof key !== "string" || !/^[A-Za-z]{1,2}$/.test(key.trim())) return null;
319
+ return CellAddress.index(key.trim());
320
+ }
321
+ };
322
+
290
323
  // src/workbook/cell-comment.ts
291
324
  var CellComment = class {
292
325
  constructor(text, author = null, color2 = null) {
@@ -582,7 +615,7 @@ var Normalizer = class {
582
615
  name,
583
616
  cells,
584
617
  this.normalizeMerges(sheet.mergedRegions ?? []),
585
- this.normalizeColumnWidths(sheet.columnWidths ?? {}),
618
+ this.normalizeWidths(sheet),
586
619
  toInt(sheet.frozenRows ?? 0),
587
620
  toInt(sheet.frozenCols ?? 0)
588
621
  );
@@ -628,10 +661,43 @@ var Normalizer = class {
628
661
  }
629
662
  return out;
630
663
  }
664
+ /**
665
+ * The two ways a width can be stated, folded into one map.
666
+ *
667
+ * `columns[].width` widens the column at that POSITION; the sheet-level
668
+ * `columnWidths` map keys the same columns by 0-based index. Both have been
669
+ * described in the shared `skills/holy-sheet.schema.json` ("Same as
670
+ * columnWidths but per-column") for as long as the field has existed, and only
671
+ * the map was ever read — so a width written the way the schema documents it
672
+ * emitted no `<cols>` element at all, with no error and no repair note
673
+ * (holy-sheet#8, fixed in PHP 2.4.0; this is the twin).
674
+ *
675
+ * The MAP IS APPLIED LAST and wins, matching PHP. It is the mechanism that
676
+ * already worked, so anyone who reached for it to work around this bug must not
677
+ * now find a leftover `width` quietly overriding it.
678
+ */
679
+ normalizeWidths(sheet) {
680
+ const out = /* @__PURE__ */ new Map();
681
+ for (const [colIdx, columnDef] of (sheet.columns ?? []).entries()) {
682
+ if (columnDef === null || typeof columnDef !== "object") continue;
683
+ if (columnDef.width === void 0) continue;
684
+ const index = ColumnWidths.index(colIdx);
685
+ const width = ColumnWidths.width(columnDef.width);
686
+ if (index === null || width === null) continue;
687
+ out.set(index, width);
688
+ }
689
+ for (const [index, width] of this.normalizeColumnWidths(sheet.columnWidths ?? {})) {
690
+ out.set(index, width);
691
+ }
692
+ return new Map([...out.entries()].sort((a, b) => a[0] - b[0]));
693
+ }
631
694
  normalizeColumnWidths(widths) {
632
695
  const out = /* @__PURE__ */ new Map();
633
696
  for (const [key, px] of Object.entries(widths)) {
634
- out.set(parseInt(key, 10), Number(px));
697
+ const index = ColumnWidths.index(key);
698
+ const width = ColumnWidths.width(px);
699
+ if (index === null || width === null) continue;
700
+ out.set(index, width);
635
701
  }
636
702
  return out;
637
703
  }
@@ -1329,6 +1395,30 @@ var Repairer = class {
1329
1395
  sheet.theme = "default";
1330
1396
  this.repairs.push(`changed '${path}.theme' from '${original}' to 'default' (unknown theme)`);
1331
1397
  }
1398
+ if (sheet.columnWidths != null && (isPlainObject(sheet.columnWidths) || Array.isArray(sheet.columnWidths))) {
1399
+ const widths = {};
1400
+ let changed = false;
1401
+ for (const [key, px] of Object.entries(sheet.columnWidths)) {
1402
+ let index = ColumnWidths.index(key);
1403
+ if (index === null) {
1404
+ const letters = ColumnWidths.fromLetters(key);
1405
+ if (letters !== null) {
1406
+ index = letters;
1407
+ this.repairs.push(`converted '${path}.columnWidths.${key}' to column index ${index}`);
1408
+ changed = true;
1409
+ }
1410
+ }
1411
+ if (index === null || ColumnWidths.width(px) === null) {
1412
+ this.repairs.push(`dropped '${path}.columnWidths.${key}' (not a column index and a width)`);
1413
+ changed = true;
1414
+ continue;
1415
+ }
1416
+ widths[String(index)] = px;
1417
+ }
1418
+ if (changed) {
1419
+ sheet.columnWidths = widths;
1420
+ }
1421
+ }
1332
1422
  if (sheet.cells != null && isPlainObject(sheet.cells)) {
1333
1423
  const cleaned = {};
1334
1424
  let changed = false;
@@ -1534,6 +1624,43 @@ var Validator = class {
1534
1624
  )
1535
1625
  );
1536
1626
  }
1627
+ if (sheet.columnWidths != null) {
1628
+ if (!isPlainObject(sheet.columnWidths) && !Array.isArray(sheet.columnWidths)) {
1629
+ errors.push(
1630
+ error(
1631
+ `${path}.columnWidths`,
1632
+ "object keyed by 0-based column index",
1633
+ typeOf(sheet.columnWidths),
1634
+ sheet.columnWidths,
1635
+ 'Column widths map a 0-based column index to pixels: {"0": 120, "1": 80}.'
1636
+ )
1637
+ );
1638
+ } else {
1639
+ for (const [key, px] of Object.entries(sheet.columnWidths)) {
1640
+ if (ColumnWidths.index(key) === null) {
1641
+ errors.push(
1642
+ error(
1643
+ `${path}.columnWidths.${key}`,
1644
+ `a 0-based column index from 0 to ${ColumnWidths.MAX_INDEX}`,
1645
+ typeOf(key),
1646
+ key,
1647
+ 'Keys are 0-based column indexes: "0" is column A, "1" is column B. Use the index, not the letter.'
1648
+ )
1649
+ );
1650
+ } else if (ColumnWidths.width(px) === null) {
1651
+ errors.push(
1652
+ error(
1653
+ `${path}.columnWidths.${key}`,
1654
+ "a non-negative number of pixels",
1655
+ typeOf(px),
1656
+ px,
1657
+ "A width is a number of pixels, like 120."
1658
+ )
1659
+ );
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1537
1664
  return errors;
1538
1665
  }
1539
1666
  validateColumn(col, path) {
@@ -1565,6 +1692,17 @@ var Validator = class {
1565
1692
  )
1566
1693
  );
1567
1694
  }
1695
+ if (col.width !== void 0 && ColumnWidths.width(col.width) === null) {
1696
+ errors.push(
1697
+ error(
1698
+ `${path}.width`,
1699
+ "a non-negative number of pixels",
1700
+ typeOf(col.width),
1701
+ col.width,
1702
+ "A column width is a number of pixels, like 120. The sheet-level columnWidths map takes precedence over it."
1703
+ )
1704
+ );
1705
+ }
1568
1706
  return errors;
1569
1707
  }
1570
1708
  };
@@ -5041,7 +5179,7 @@ var holy_sheet_schema_default = {
5041
5179
  };
5042
5180
 
5043
5181
  // src/agent.ts
5044
- var VERSION = "2.4.2";
5182
+ var VERSION = "2.5.0";
5045
5183
  function toU8(input) {
5046
5184
  return input instanceof Uint8Array ? input : new Uint8Array(input);
5047
5185
  }