@particle-academy/holy-sheet 2.6.0 → 2.7.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/CHANGELOG.md +10 -0
- package/dist/index.cjs +248 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -7
- package/dist/index.d.ts +30 -7
- package/dist/index.js +248 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [2.7.0] — 2026-10-19
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`print`** (`orientation`, `fitToWidth`, `footer` with `{page}` / `{pages}`)
|
|
18
|
+
and **`rowHeights`** (pixels by 1-based row number). Read back, validated and
|
|
19
|
+
published in the schema. **Nothing to do**: a workbook without them is
|
|
20
|
+
byte-identical. Same as `particle-academy/holy-sheet` 2.6.0 and
|
|
21
|
+
`fancy-holy-sheet` 0.6.0; parity holds (`printSetup` fixture).
|
|
22
|
+
|
|
13
23
|
## [2.6.0] — 2026-10-11
|
|
14
24
|
|
|
15
25
|
### Fixed
|
package/dist/index.cjs
CHANGED
|
@@ -418,13 +418,15 @@ var MergedRegion = class {
|
|
|
418
418
|
|
|
419
419
|
// src/workbook/sheet.ts
|
|
420
420
|
var Sheet = class {
|
|
421
|
-
constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0) {
|
|
421
|
+
constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0, rowHeights = /* @__PURE__ */ new Map(), print = null) {
|
|
422
422
|
this.name = name;
|
|
423
423
|
this.cells = cells;
|
|
424
424
|
this.mergedRegions = mergedRegions;
|
|
425
425
|
this.columnWidths = columnWidths;
|
|
426
426
|
this.frozenRows = frozenRows;
|
|
427
427
|
this.frozenCols = frozenCols;
|
|
428
|
+
this.rowHeights = rowHeights;
|
|
429
|
+
this.print = print;
|
|
428
430
|
}
|
|
429
431
|
/** rowNumber → (colLetter → Cell), sorted by ascending row number. */
|
|
430
432
|
rows() {
|
|
@@ -665,7 +667,9 @@ var Normalizer = class {
|
|
|
665
667
|
this.normalizeMerges(sheet.mergedRegions ?? []),
|
|
666
668
|
this.normalizeWidths(sheet),
|
|
667
669
|
toInt(sheet.frozenRows ?? 0),
|
|
668
|
-
toInt(sheet.frozenCols ?? 0)
|
|
670
|
+
toInt(sheet.frozenCols ?? 0),
|
|
671
|
+
normalizeRowHeights(sheet.rowHeights),
|
|
672
|
+
normalizePrint(sheet.print)
|
|
669
673
|
);
|
|
670
674
|
}
|
|
671
675
|
normalizeCellMap(map, sheetName2) {
|
|
@@ -856,6 +860,34 @@ function toInt(v) {
|
|
|
856
860
|
const n = Math.trunc(Number(v));
|
|
857
861
|
return Number.isFinite(n) ? n : 0;
|
|
858
862
|
}
|
|
863
|
+
function phpNumeric(v) {
|
|
864
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
865
|
+
return isNumericString(v) ? numericStringToNumber(v) : null;
|
|
866
|
+
}
|
|
867
|
+
function normalizeRowHeights(map) {
|
|
868
|
+
const out = /* @__PURE__ */ new Map();
|
|
869
|
+
if (!isPlainObject(map) && !Array.isArray(map)) return out;
|
|
870
|
+
for (const [row, px] of Object.entries(map)) {
|
|
871
|
+
const n = phpNumeric(px);
|
|
872
|
+
if (/^[1-9]\d{0,6}$/.test(row) && n !== null && n >= 0) out.set(parseInt(row, 10), n);
|
|
873
|
+
}
|
|
874
|
+
return new Map([...out.entries()].sort((a, b) => a[0] - b[0]));
|
|
875
|
+
}
|
|
876
|
+
function normalizePrint(print) {
|
|
877
|
+
if (!isPlainObject(print) && !Array.isArray(print)) return null;
|
|
878
|
+
const p = print;
|
|
879
|
+
const orientation = p.orientation === "portrait" || p.orientation === "landscape" ? p.orientation : null;
|
|
880
|
+
const fitToWidth = p.fitToWidth === true;
|
|
881
|
+
let footer = null;
|
|
882
|
+
if (isPlainObject(p.footer) || Array.isArray(p.footer)) {
|
|
883
|
+
const f = p.footer;
|
|
884
|
+
const slot = (k) => typeof f[k] === "string" ? f[k] : "";
|
|
885
|
+
footer = { left: slot("left"), center: slot("center"), right: slot("right") };
|
|
886
|
+
if (footer.left === "" && footer.center === "" && footer.right === "") footer = null;
|
|
887
|
+
}
|
|
888
|
+
if (orientation === null && !fitToWidth && footer === null) return null;
|
|
889
|
+
return { orientation, fitToWidth, footer };
|
|
890
|
+
}
|
|
859
891
|
|
|
860
892
|
// src/schema/formula-linter.ts
|
|
861
893
|
var ERR_VALUE = "#VALUE!";
|
|
@@ -1703,6 +1735,95 @@ var Validator = class {
|
|
|
1703
1735
|
}
|
|
1704
1736
|
}
|
|
1705
1737
|
}
|
|
1738
|
+
if (sheet.rowHeights != null) {
|
|
1739
|
+
if (!isPlainObject(sheet.rowHeights) && !Array.isArray(sheet.rowHeights)) {
|
|
1740
|
+
errors.push(
|
|
1741
|
+
error(
|
|
1742
|
+
`${path}.rowHeights`,
|
|
1743
|
+
"object keyed by 1-based row number",
|
|
1744
|
+
typeOf(sheet.rowHeights),
|
|
1745
|
+
sheet.rowHeights,
|
|
1746
|
+
'Row heights map a row NUMBER to pixels: {"1": 32, "4": 28}.'
|
|
1747
|
+
)
|
|
1748
|
+
);
|
|
1749
|
+
} else {
|
|
1750
|
+
for (const [row, px] of Object.entries(sheet.rowHeights)) {
|
|
1751
|
+
const n = typeof px === "number" ? px : isNumericString(px) ? numericStringToNumber(px) : null;
|
|
1752
|
+
if (!/^[1-9]\d{0,6}$/.test(row)) {
|
|
1753
|
+
errors.push(error(
|
|
1754
|
+
`${path}.rowHeights.${row}`,
|
|
1755
|
+
"a 1-based row number",
|
|
1756
|
+
typeOf(row),
|
|
1757
|
+
row,
|
|
1758
|
+
'Keys are row numbers as Excel shows them: "1" is the first row.'
|
|
1759
|
+
));
|
|
1760
|
+
} else if (n === null || !Number.isFinite(n) || n < 0) {
|
|
1761
|
+
errors.push(error(
|
|
1762
|
+
`${path}.rowHeights.${row}`,
|
|
1763
|
+
"a non-negative number of pixels",
|
|
1764
|
+
typeOf(px),
|
|
1765
|
+
px,
|
|
1766
|
+
"A height is a number of pixels, like 28 (20 is a default row)."
|
|
1767
|
+
));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (sheet.print != null) {
|
|
1773
|
+
const print = sheet.print;
|
|
1774
|
+
if (!isPlainObject(print) && !Array.isArray(print)) {
|
|
1775
|
+
errors.push(error(
|
|
1776
|
+
`${path}.print`,
|
|
1777
|
+
"object {orientation?, fitToWidth?, footer?}",
|
|
1778
|
+
typeOf(print),
|
|
1779
|
+
print,
|
|
1780
|
+
'Print setup: {"orientation": "landscape", "fitToWidth": true, "footer": {"right": "Page {page} of {pages}"}}.'
|
|
1781
|
+
));
|
|
1782
|
+
} else {
|
|
1783
|
+
if (print.orientation != null && print.orientation !== "portrait" && print.orientation !== "landscape") {
|
|
1784
|
+
errors.push(error(
|
|
1785
|
+
`${path}.print.orientation`,
|
|
1786
|
+
"one of: portrait, landscape",
|
|
1787
|
+
typeOf(print.orientation),
|
|
1788
|
+
print.orientation,
|
|
1789
|
+
"Use landscape for a sheet wider than it is tall."
|
|
1790
|
+
));
|
|
1791
|
+
}
|
|
1792
|
+
if (print.fitToWidth != null && typeof print.fitToWidth !== "boolean") {
|
|
1793
|
+
errors.push(error(
|
|
1794
|
+
`${path}.print.fitToWidth`,
|
|
1795
|
+
"boolean",
|
|
1796
|
+
typeOf(print.fitToWidth),
|
|
1797
|
+
print.fitToWidth,
|
|
1798
|
+
"true scales the sheet to one page wide when printed."
|
|
1799
|
+
));
|
|
1800
|
+
}
|
|
1801
|
+
if (print.footer != null) {
|
|
1802
|
+
if (!isPlainObject(print.footer) && !Array.isArray(print.footer)) {
|
|
1803
|
+
errors.push(error(
|
|
1804
|
+
`${path}.print.footer`,
|
|
1805
|
+
"object {left?, center?, right?}",
|
|
1806
|
+
typeOf(print.footer),
|
|
1807
|
+
print.footer,
|
|
1808
|
+
"A footer has up to three slots of text."
|
|
1809
|
+
));
|
|
1810
|
+
} else {
|
|
1811
|
+
const footer = print.footer;
|
|
1812
|
+
for (const slot of ["left", "center", "right"]) {
|
|
1813
|
+
if (footer[slot] != null && typeof footer[slot] !== "string") {
|
|
1814
|
+
errors.push(error(
|
|
1815
|
+
`${path}.print.footer.${slot}`,
|
|
1816
|
+
"string",
|
|
1817
|
+
typeOf(footer[slot]),
|
|
1818
|
+
footer[slot],
|
|
1819
|
+
"Footer text; {page} and {pages} print the page number and count."
|
|
1820
|
+
));
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1706
1827
|
return errors;
|
|
1707
1828
|
}
|
|
1708
1829
|
validateColumn(col, path) {
|
|
@@ -2792,6 +2913,22 @@ function sheetToSchema(sheet) {
|
|
|
2792
2913
|
}
|
|
2793
2914
|
if (sheet.frozenRows > 0) out["frozenRows"] = sheet.frozenRows;
|
|
2794
2915
|
if (sheet.frozenCols > 0) out["frozenCols"] = sheet.frozenCols;
|
|
2916
|
+
if (sheet.rowHeights.size > 0) {
|
|
2917
|
+
const heights = {};
|
|
2918
|
+
for (const [k, v] of sheet.rowHeights) heights[k] = v;
|
|
2919
|
+
out["rowHeights"] = heights;
|
|
2920
|
+
}
|
|
2921
|
+
if (sheet.print !== null) {
|
|
2922
|
+
const print = {};
|
|
2923
|
+
if (sheet.print.orientation !== null) print["orientation"] = sheet.print.orientation;
|
|
2924
|
+
if (sheet.print.fitToWidth) print["fitToWidth"] = true;
|
|
2925
|
+
if (sheet.print.footer !== null) {
|
|
2926
|
+
const footer = {};
|
|
2927
|
+
for (const k of ["left", "center", "right"]) if (sheet.print.footer[k] !== "") footer[k] = sheet.print.footer[k];
|
|
2928
|
+
print["footer"] = footer;
|
|
2929
|
+
}
|
|
2930
|
+
out["print"] = print;
|
|
2931
|
+
}
|
|
2795
2932
|
return out;
|
|
2796
2933
|
}
|
|
2797
2934
|
function cellToSchema(cell) {
|
|
@@ -3414,9 +3551,62 @@ var WorksheetParser = {
|
|
|
3414
3551
|
const merges = parseMerges(xml);
|
|
3415
3552
|
const columnWidths = parseColumnWidths(xml);
|
|
3416
3553
|
const [frozenRows, frozenCols] = parseFrozen(xml);
|
|
3417
|
-
return new Sheet(name, cells, merges, columnWidths, frozenRows, frozenCols);
|
|
3554
|
+
return new Sheet(name, cells, merges, columnWidths, frozenRows, frozenCols, parseRowHeights(xml), parsePrint(xml));
|
|
3418
3555
|
}
|
|
3419
3556
|
};
|
|
3557
|
+
function parseRowHeights(xml) {
|
|
3558
|
+
const out = /* @__PURE__ */ new Map();
|
|
3559
|
+
const sheetData = el(xml, "sheetData");
|
|
3560
|
+
if (!sheetData) return out;
|
|
3561
|
+
for (const row of els(sheetData, "row")) {
|
|
3562
|
+
const r = parseInt(at(row, "r") ?? "0", 10);
|
|
3563
|
+
const custom = at(row, "customHeight");
|
|
3564
|
+
const ht = at(row, "ht");
|
|
3565
|
+
if (r > 0 && ht !== void 0 && (custom === "1" || custom === "true")) {
|
|
3566
|
+
out.set(r, Math.round(parseFloat(ht) / 0.75 * 100) / 100);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
return out;
|
|
3570
|
+
}
|
|
3571
|
+
function parsePrint(xml) {
|
|
3572
|
+
const fitToPage = ["1", "true"].includes(at(el(el(xml, "sheetPr"), "pageSetUpPr"), "fitToPage") ?? "");
|
|
3573
|
+
const setup = el(xml, "pageSetup");
|
|
3574
|
+
const o = at(setup, "orientation");
|
|
3575
|
+
const orientation = o === "portrait" || o === "landscape" ? o : null;
|
|
3576
|
+
const fitToWidth = fitToPage && setup !== void 0 && at(setup, "fitToWidth") === "1" && ["0", ""].includes(at(setup, "fitToHeight") ?? "");
|
|
3577
|
+
const oddFooter = el(el(xml, "headerFooter"), "oddFooter");
|
|
3578
|
+
const footer = oddFooter !== void 0 ? parseFooterCodes(oddFooter.text) : null;
|
|
3579
|
+
if (orientation === null && !fitToWidth && footer === null) return null;
|
|
3580
|
+
return { orientation, fitToWidth, footer };
|
|
3581
|
+
}
|
|
3582
|
+
function parseFooterCodes(codes) {
|
|
3583
|
+
const slots = { left: "", center: "", right: "" };
|
|
3584
|
+
let current = "center";
|
|
3585
|
+
const len = codes.length;
|
|
3586
|
+
for (let i = 0; i < len; i++) {
|
|
3587
|
+
const ch = codes[i];
|
|
3588
|
+
if (ch !== "&" || i + 1 >= len) {
|
|
3589
|
+
slots[current] += ch;
|
|
3590
|
+
continue;
|
|
3591
|
+
}
|
|
3592
|
+
const next = codes[++i];
|
|
3593
|
+
if (next === "&") slots[current] += "&";
|
|
3594
|
+
else if (next === "L") current = "left";
|
|
3595
|
+
else if (next === "C") current = "center";
|
|
3596
|
+
else if (next === "R") current = "right";
|
|
3597
|
+
else if (next === "P") slots[current] += "{page}";
|
|
3598
|
+
else if (next === "N") slots[current] += "{pages}";
|
|
3599
|
+
else if (next === '"') {
|
|
3600
|
+
const close = codes.indexOf('"', i + 1);
|
|
3601
|
+
i = close === -1 ? len : close;
|
|
3602
|
+
} else if (/[0-9]/.test(next)) {
|
|
3603
|
+
while (i + 1 < len && /[0-9]/.test(codes[i + 1])) i++;
|
|
3604
|
+
} else if (next === "K") {
|
|
3605
|
+
i = Math.min(len - 1, i + 6);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
return slots.left === "" && slots.center === "" && slots.right === "" ? null : slots;
|
|
3609
|
+
}
|
|
3420
3610
|
function parseCells(xml, stylesIndex, comments, sharedStrings) {
|
|
3421
3611
|
const cells = /* @__PURE__ */ new Map();
|
|
3422
3612
|
const sheetData = el(xml, "sheetData");
|
|
@@ -3792,6 +3982,19 @@ function hexToArgb(hex2) {
|
|
|
3792
3982
|
|
|
3793
3983
|
// src/writer/xlsx-writer.ts
|
|
3794
3984
|
var encoder2 = new TextEncoder();
|
|
3985
|
+
function footerCodes(footer) {
|
|
3986
|
+
let codes = "";
|
|
3987
|
+
for (const [slot, code] of [["left", "&L"], ["center", "&C"], ["right", "&R"]]) {
|
|
3988
|
+
if (footer[slot] !== "") {
|
|
3989
|
+
codes += code + footer[slot].replace(/&/g, "&&").replace(/\{pages\}/g, "&N").replace(/\{page\}/g, "&P");
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
return codes;
|
|
3993
|
+
}
|
|
3994
|
+
function decimal(value) {
|
|
3995
|
+
const out = value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
|
3996
|
+
return out === "" || out === "-0" ? "0" : out;
|
|
3997
|
+
}
|
|
3795
3998
|
var XlsxWriter = class {
|
|
3796
3999
|
toBytes(workbook) {
|
|
3797
4000
|
const styles = new StylesRegistry();
|
|
@@ -3880,14 +4083,20 @@ var XlsxWriter = class {
|
|
|
3880
4083
|
}
|
|
3881
4084
|
colsXml += "</cols>";
|
|
3882
4085
|
}
|
|
4086
|
+
const rows = sheet.rows();
|
|
4087
|
+
for (const rowIndex of sheet.rowHeights.keys()) {
|
|
4088
|
+
if (!rows.has(rowIndex)) rows.set(rowIndex, /* @__PURE__ */ new Map());
|
|
4089
|
+
}
|
|
3883
4090
|
let rowsXml = "";
|
|
3884
|
-
for (const [rowIndex, row] of
|
|
4091
|
+
for (const [rowIndex, row] of [...rows.entries()].sort((a, b) => a[0] - b[0])) {
|
|
3885
4092
|
let cellsXml = "";
|
|
3886
4093
|
const sortedCols = [...row.keys()].sort();
|
|
3887
4094
|
for (const col of sortedCols) {
|
|
3888
4095
|
cellsXml += this.cellXml(row.get(col), styles);
|
|
3889
4096
|
}
|
|
3890
|
-
|
|
4097
|
+
const height = sheet.rowHeights.get(rowIndex);
|
|
4098
|
+
const ht = height !== void 0 ? ` ht="${decimal(height * 0.75)}" customHeight="1"` : "";
|
|
4099
|
+
rowsXml += cellsXml === "" ? `<row r="${rowIndex}"${ht}/>` : `<row r="${rowIndex}"${ht}>${cellsXml}</row>`;
|
|
3891
4100
|
}
|
|
3892
4101
|
let mergesXml = "";
|
|
3893
4102
|
if (sheet.mergedRegions.length > 0) {
|
|
@@ -3896,7 +4105,14 @@ var XlsxWriter = class {
|
|
|
3896
4105
|
mergesXml += "</mergeCells>";
|
|
3897
4106
|
}
|
|
3898
4107
|
const legacyDrawing = sheet.hasComments() ? '<legacyDrawing r:id="rId1"/>' : "";
|
|
3899
|
-
|
|
4108
|
+
const print = sheet.print;
|
|
4109
|
+
const sheetPrXml = print?.fitToWidth ? '<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr>' : "";
|
|
4110
|
+
let pageSetupXml = "";
|
|
4111
|
+
if (print !== null && (print.orientation !== null || print.fitToWidth)) {
|
|
4112
|
+
pageSetupXml = "<pageSetup" + (print.orientation !== null ? ` orientation="${print.orientation}"` : "") + (print.fitToWidth ? ' fitToWidth="1" fitToHeight="0"' : "") + "/>";
|
|
4113
|
+
}
|
|
4114
|
+
const headerFooterXml = print?.footer ? `<headerFooter><oddFooter>${this.escape(footerCodes(print.footer))}</oddFooter></headerFooter>` : "";
|
|
4115
|
+
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 + legacyDrawing + "</worksheet>";
|
|
3900
4116
|
}
|
|
3901
4117
|
cellXml(cell, styles) {
|
|
3902
4118
|
const ref = cell.address;
|
|
@@ -5013,6 +5229,31 @@ var holy_sheet_schema_default = {
|
|
|
5013
5229
|
type: "integer",
|
|
5014
5230
|
minimum: 0,
|
|
5015
5231
|
description: "How many columns stay visible while the sheet scrolls sideways. Set to 1 when the first column is the label for the row."
|
|
5232
|
+
},
|
|
5233
|
+
rowHeights: {
|
|
5234
|
+
type: "object",
|
|
5235
|
+
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.',
|
|
5236
|
+
propertyNames: { pattern: "^[1-9][0-9]*$" },
|
|
5237
|
+
additionalProperties: { type: "number", minimum: 0 }
|
|
5238
|
+
},
|
|
5239
|
+
print: {
|
|
5240
|
+
type: "object",
|
|
5241
|
+
additionalProperties: false,
|
|
5242
|
+
description: "How the sheet prints. A sheet wider than a page splits its columns across pages unless fitToWidth is set.",
|
|
5243
|
+
properties: {
|
|
5244
|
+
orientation: { type: "string", enum: ["portrait", "landscape"] },
|
|
5245
|
+
fitToWidth: { type: "boolean", description: "Scale the sheet to one page wide (as many pages tall as it needs)." },
|
|
5246
|
+
footer: {
|
|
5247
|
+
type: "object",
|
|
5248
|
+
additionalProperties: false,
|
|
5249
|
+
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}"}.',
|
|
5250
|
+
properties: {
|
|
5251
|
+
left: { type: "string" },
|
|
5252
|
+
center: { type: "string" },
|
|
5253
|
+
right: { type: "string" }
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5016
5257
|
}
|
|
5017
5258
|
}
|
|
5018
5259
|
},
|
|
@@ -5221,7 +5462,7 @@ var holy_sheet_schema_default = {
|
|
|
5221
5462
|
};
|
|
5222
5463
|
|
|
5223
5464
|
// src/agent.ts
|
|
5224
|
-
var VERSION = "2.
|
|
5465
|
+
var VERSION = "2.7.0";
|
|
5225
5466
|
function toU8(input) {
|
|
5226
5467
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
5227
5468
|
}
|