@particle-academy/holy-sheet 2.8.0 → 2.10.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 +29 -0
- package/README.md +7 -0
- package/agent-guide/professional-workbooks.md +179 -0
- package/dist/index.cjs +126 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +126 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.d.cts
CHANGED
|
@@ -63,6 +63,8 @@ interface SheetSchema {
|
|
|
63
63
|
print?: {
|
|
64
64
|
orientation?: "portrait" | "landscape";
|
|
65
65
|
fitToWidth?: boolean;
|
|
66
|
+
/** One page wide AND tall: a one-page summary with a chart prints whole. */
|
|
67
|
+
fitToPage?: boolean;
|
|
66
68
|
/** `{page}` / `{pages}` print the page number and count. */
|
|
67
69
|
footer?: {
|
|
68
70
|
left?: string;
|
|
@@ -78,10 +80,15 @@ interface ChartSchema {
|
|
|
78
80
|
title?: string;
|
|
79
81
|
/** Axis labels, e.g. "A5:A7" or "'Other sheet'!A5:A7". */
|
|
80
82
|
categories?: string;
|
|
83
|
+
/** points colours single points (0-based within values) to make the finding stand out. */
|
|
81
84
|
series: {
|
|
82
85
|
name?: string;
|
|
83
86
|
values: string;
|
|
84
87
|
color?: string;
|
|
88
|
+
points?: {
|
|
89
|
+
index: number;
|
|
90
|
+
color: string;
|
|
91
|
+
}[];
|
|
85
92
|
}[];
|
|
86
93
|
/** Top-left and bottom-right cells; defaults to beside the data. */
|
|
87
94
|
position?: {
|
|
@@ -257,7 +264,7 @@ interface SetMetaOp {
|
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
/** This package's own version, pinned to package.json by `version.test.ts`. */
|
|
260
|
-
declare const VERSION = "2.
|
|
267
|
+
declare const VERSION = "2.10.0";
|
|
261
268
|
type Any$6 = any;
|
|
262
269
|
/**
|
|
263
270
|
* Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
|
|
@@ -320,6 +327,12 @@ declare const Agent: {
|
|
|
320
327
|
* `meta.created` and its written copy.
|
|
321
328
|
*/
|
|
322
329
|
equivalent(a: Any$6, b: Any$6): boolean;
|
|
330
|
+
/**
|
|
331
|
+
* The agent guide: what makes this kind of file professional, and the schema
|
|
332
|
+
* idioms that produce it. Give it to a model with the schema; it works with any
|
|
333
|
+
* agent loop. Byte-identical to the PHP package's agent-guide/professional-workbooks.md.
|
|
334
|
+
*/
|
|
335
|
+
guide(): string;
|
|
323
336
|
version(): string;
|
|
324
337
|
};
|
|
325
338
|
|
|
@@ -329,7 +342,7 @@ type Any$5 = any;
|
|
|
329
342
|
* over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
|
|
330
343
|
*/
|
|
331
344
|
declare class HolySheet {
|
|
332
|
-
static readonly VERSION = "2.
|
|
345
|
+
static readonly VERSION = "2.10.0";
|
|
333
346
|
validate(schema: Any$5): ValidationError[];
|
|
334
347
|
toBytes(schema: Any$5): Uint8Array;
|
|
335
348
|
write(schema: Any$5, path: string): Promise<WriteResult>;
|
|
@@ -493,6 +506,8 @@ declare class ChartRange {
|
|
|
493
506
|
interface PrintSetup {
|
|
494
507
|
orientation: "portrait" | "landscape" | null;
|
|
495
508
|
fitToWidth: boolean;
|
|
509
|
+
/** One page wide AND tall. */
|
|
510
|
+
fitToPage: boolean;
|
|
496
511
|
footer: {
|
|
497
512
|
left: string;
|
|
498
513
|
center: string;
|
|
@@ -508,6 +523,10 @@ interface NormalizedChart {
|
|
|
508
523
|
name: string | null;
|
|
509
524
|
values: ChartRange;
|
|
510
525
|
color: string | null;
|
|
526
|
+
points: {
|
|
527
|
+
index: number;
|
|
528
|
+
color: string;
|
|
529
|
+
}[];
|
|
511
530
|
}[];
|
|
512
531
|
/** [col, row], both 0-based. */
|
|
513
532
|
from: [number, number];
|
package/dist/index.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ interface SheetSchema {
|
|
|
63
63
|
print?: {
|
|
64
64
|
orientation?: "portrait" | "landscape";
|
|
65
65
|
fitToWidth?: boolean;
|
|
66
|
+
/** One page wide AND tall: a one-page summary with a chart prints whole. */
|
|
67
|
+
fitToPage?: boolean;
|
|
66
68
|
/** `{page}` / `{pages}` print the page number and count. */
|
|
67
69
|
footer?: {
|
|
68
70
|
left?: string;
|
|
@@ -78,10 +80,15 @@ interface ChartSchema {
|
|
|
78
80
|
title?: string;
|
|
79
81
|
/** Axis labels, e.g. "A5:A7" or "'Other sheet'!A5:A7". */
|
|
80
82
|
categories?: string;
|
|
83
|
+
/** points colours single points (0-based within values) to make the finding stand out. */
|
|
81
84
|
series: {
|
|
82
85
|
name?: string;
|
|
83
86
|
values: string;
|
|
84
87
|
color?: string;
|
|
88
|
+
points?: {
|
|
89
|
+
index: number;
|
|
90
|
+
color: string;
|
|
91
|
+
}[];
|
|
85
92
|
}[];
|
|
86
93
|
/** Top-left and bottom-right cells; defaults to beside the data. */
|
|
87
94
|
position?: {
|
|
@@ -257,7 +264,7 @@ interface SetMetaOp {
|
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
/** This package's own version, pinned to package.json by `version.test.ts`. */
|
|
260
|
-
declare const VERSION = "2.
|
|
267
|
+
declare const VERSION = "2.10.0";
|
|
261
268
|
type Any$6 = any;
|
|
262
269
|
/**
|
|
263
270
|
* Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
|
|
@@ -320,6 +327,12 @@ declare const Agent: {
|
|
|
320
327
|
* `meta.created` and its written copy.
|
|
321
328
|
*/
|
|
322
329
|
equivalent(a: Any$6, b: Any$6): boolean;
|
|
330
|
+
/**
|
|
331
|
+
* The agent guide: what makes this kind of file professional, and the schema
|
|
332
|
+
* idioms that produce it. Give it to a model with the schema; it works with any
|
|
333
|
+
* agent loop. Byte-identical to the PHP package's agent-guide/professional-workbooks.md.
|
|
334
|
+
*/
|
|
335
|
+
guide(): string;
|
|
323
336
|
version(): string;
|
|
324
337
|
};
|
|
325
338
|
|
|
@@ -329,7 +342,7 @@ type Any$5 = any;
|
|
|
329
342
|
* over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
|
|
330
343
|
*/
|
|
331
344
|
declare class HolySheet {
|
|
332
|
-
static readonly VERSION = "2.
|
|
345
|
+
static readonly VERSION = "2.10.0";
|
|
333
346
|
validate(schema: Any$5): ValidationError[];
|
|
334
347
|
toBytes(schema: Any$5): Uint8Array;
|
|
335
348
|
write(schema: Any$5, path: string): Promise<WriteResult>;
|
|
@@ -493,6 +506,8 @@ declare class ChartRange {
|
|
|
493
506
|
interface PrintSetup {
|
|
494
507
|
orientation: "portrait" | "landscape" | null;
|
|
495
508
|
fitToWidth: boolean;
|
|
509
|
+
/** One page wide AND tall. */
|
|
510
|
+
fitToPage: boolean;
|
|
496
511
|
footer: {
|
|
497
512
|
left: string;
|
|
498
513
|
center: string;
|
|
@@ -508,6 +523,10 @@ interface NormalizedChart {
|
|
|
508
523
|
name: string | null;
|
|
509
524
|
values: ChartRange;
|
|
510
525
|
color: string | null;
|
|
526
|
+
points: {
|
|
527
|
+
index: number;
|
|
528
|
+
color: string;
|
|
529
|
+
}[];
|
|
511
530
|
}[];
|
|
512
531
|
/** [col, row], both 0-based. */
|
|
513
532
|
from: [number, number];
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/agent-guide.json
|
|
2
|
+
var agent_guide_default = {
|
|
3
|
+
guide: '# Agent guide: professional workbooks with holy-sheet\n\n<!--\n The agent guide for holy-sheet: what makes a workbook professional, and the\n schema idioms that produce it. Canonical copy: the PHP package\'s\n agent-guide/professional-workbooks.md; the Node and Python packages carry the same\n bytes (a test in each pins the hash). Distilled from rubric-judged builds in\n the Fancy document lab -- every rule here moved a measured score.\n-->\n\n## How to use this guide\n\nIt is written for an agent and works with any agent loop. Give it to the model\nas part of its instructions (a system prompt, a skill, a rules file), together\nwith the package\'s JSON schema (`Agent::toolDefinition()` / `Agent.toolDefinition()` / `tool_definition()`). Then the procedure is:\n\n1. Read the schema once and plan a workbook in full: structure, every figure,\n every derived number, computed and checked before anything is written.\n2. Build the whole JSON in one go, following the recipes below.\n3. Check it: `Agent::validate` (PHP), `Agent.validate` (Node) or `validate` (Python) returns errors with a hint each.\n4. Write it (`Agent::write` / `Agent.toBytes` / `to_bytes`). If you can render the file and look at it, do,\n but repair only what you can see is wrong: one-shot "improvements" made\n blind regressed more documents than they fixed.\n\nYou are building a workbook a CFO would forward to the board UNCHANGED. Meeting the\nbrief is the floor. A table of the given numbers is a data dump; a professional\nsheet tells the reader what it is, what the numbers mean, and how they were derived.\n\n## The bar (every item is required)\n\n1. **Title block.** Row 1: what the sheet is. Row 2: period, units/currency, source\n and as-of date. A reader who sees only this sheet knows exactly what it shows.\n2. **Analysis, not just data.** Add the columns a reader needs to interpret the\n figures: share of total, variance vs average or target, rank. All as FORMULAS.\n3. **Formulas for everything derived.** Totals, averages, shares, variances. Never a\n typed number where a formula belongs.\n4. **Headers sit over their data.** Text columns left-aligned, numeric columns\n right-aligned, header cells aligned the same way as the data below them.\n5. **Totals are distinct.** Bold, a top border, a light fill, and a clear label\n ("Total", "Average growth (simple)").\n6. **Formats.** Currency with the right decimals (whole dollars for millions-scale\n board figures unless cents matter), percentages to 1 decimal, ISO or written dates.\n Negative values in a warning colour.\n7. **Usable.** Frozen panes below the header row, widths that fit every value (never ####),\n a notes block stating method and definitions.\n\n## Never ship\n\n- A placeholder ("TBD", "[date]"). Name the period instead.\n- Rounded figures that disagree with the source. Board figures given to the cent show\n cents (decimals 2) unless you round consistently AND say "rounded" in the title block.\n- A sheet that prints across two pages. Keep the used width within ~7 inches\n (about 650 px of columnWidths in total) and keep every text cell short: one note per\n row, at most ~95 characters, so nothing spills past the table\'s right edge.\n\n## Layout: use the `cells` model for a professional sheet\n\n`columns` forces the header into row 1, which leaves no room for a title block. Build\nthe sheet from `cells` (keys are A1 addresses) instead:\n\n- A1 title: `{value:"Q3 2026 Revenue by Region", format:{bold:true, fontSize:16, color:"#111827"}}`,\n merged across the table: `mergedRegions:[{start:"A1", end:"E1"}]`.\n- A2 subtitle: `{value:"Quarter ended 30 Sep 2026 | USD | Source: management accounts", format:{italic:true, color:"#6B7280"}}`, merged A2:E2.\n- Row 4 header: each cell `{value:"Revenue", format:{bold:true, color:"#FFFFFF", backgroundColor:"#1F2937", textAlign:"right"}}`\n (textAlign "left" for text columns).\n- Data rows from row 5: `{value:1250000.5, format:{displayFormat:"currency", decimals:0}}`,\n `{value:0.184, format:{displayFormat:"percentage", decimals:1}}`.\n- Derived cells: `{formula:"B5/B$8", format:{displayFormat:"percentage", decimals:1}}`\n (formula WITHOUT a leading "=").\n- Total row: `{formula:"SUM(B5:B7)", format:{bold:true, displayFormat:"currency", decimals:0, borderTop:"#111827", backgroundColor:"#F3F4F6"}}`,\n and style the label and blank cells in that row the same way so the band is continuous.\n- Notes last (below the chart, when there is one): `{value:"Notes", format:{bold:true}}`\n then one definition per row in `#4B5563` (dark enough to read; not pale grey italic).\n- `frozenRows: 4` (title block + header stay visible), `columnWidths` in pixels:\n labels ~180, currency ~150, percentages ~110, dates ~120.\n- Breathing room: `rowHeights` in pixels by row NUMBER -- title row 36, header row 30,\n data rows 24, total row 28: `{"1": 36, "4": 30, "5": 24, "6": 24, "7": 24, "8": 28}`.\n Packed default rows read as a data dump.\n- Banding: give alternate data rows a very light fill (`backgroundColor:"#F9FAFB"`) and\n every data row a hairline bottom border (`borderBottom:"#E5E7EB"`), so the eye can\n follow a row across.\n- Print: a one-page summary (table, key point, chart, notes) is\n `print:{orientation:"portrait", fitToPage:true, footer:{left:"Q3 2026 Revenue Review",\n right:"Page {page} of {pages}"}}` -- `fitToPage` scales the whole sheet onto ONE page,\n so the page break can never cut the chart in half. Only a long data table (more than\n ~40 rows) uses `fitToWidth:true` instead, and then its chart goes on its own sheet.\n Use landscape when the table is wider than ~650px. Every workbook carries a print setup.\n\n## A chart, whenever the point is a comparison\n\nA board reader sees a comparison faster in a chart than in a column of figures, and\nreviewers mark a comparison sheet down when it has none. Add ONE native chart per\ncomparison (a real Excel chart over the cells, not a picture: it updates when the\nfigures change):\n\n```\ncharts: [{\n type: "bar",\n title: "EMEA delivered 48% of Q3 revenue",\n categories: "A5:A7",\n series: [{name: "Revenue", values: "B5:B7", color: "#94A3B8",\n points: [{index: 2, color: "#1F3A5F"}]}],\n dataLabels: true,\n format: "$#,##0.00",\n position: {from: "A11", to: "F27"}\n}]\n```\n\n- **Highlight the point the title is about.** Every bar one colour does not say\n which bar matters. Give the series a muted colour (`#94A3B8`) and the subject of\n the title -- the leader, the outlier, the line over budget -- the strong colour\n with `points: [{index, color}]` (index is 0-based within `values`). Flag\n over-budget or missed-target points in red (`#DC2626`).\n- **Labels show what the table shows.** The chart\'s `format` uses the SAME decimals\n as the cells it plots: a table in cents and a label rounded to dollars read as two\n different numbers. For millions, label in millions and say so in the title\n ("$M") -- never a rounded label beside an exact cell.\n\n- **Type.** `bar` (horizontal) for categories with names longer than a word, and for\n ranking; `column` for up to ~6 short categories; `line` for a trend over time\n periods; `pie` only for 2-5 parts of one whole, and never when a value is negative.\n- **Ranges are the data rows only**, never the header or the total row: a total\n plotted beside its parts dwarfs them.\n- **Title states the finding**, like the key point ("EMEA led Q3 revenue"), not\n the axis ("Revenue by region").\n- **One series, one colour** (the sheet\'s header colour). Add a second series only\n when the comparison is between the two series; the legend appears by itself.\n- **dataLabels: true** with `format` matching the cells (`"$#,##0"`, `"0.0%"`), so\n the reader does not have to read values off an axis.\n- **Place it directly below the key point**, starting in column A, about 6 columns\n wide and 16 rows tall (`from: "A{row}"`, `to: "F{row+16}"`), and put the notes\n BELOW the chart. The chart sits next to the table it summarises; the notes\n that define terms come last. Beside the table it widens the printed page; on top\n of cells it hides them. With `fitToPage` the whole stack prints on one page.\n\n## Recipes\n\n- **Board summary sheet:** title block; a table with value, share of total, change and\n variance vs average; total row; a one-line "Key point" cell above the notes stating\n the finding in words (e.g. "EMEA delivered 48.5% of revenue and grew fastest").\n- **Model with inputs:** an "Inputs" block (light yellow fill #FEF9C3 on input cells,\n labelled), calculations referencing the inputs by cell, outputs in a separate\n clearly labelled block. Use separate sheets when the model is larger than one screen.\n- **Detail + summary:** a "Summary" sheet first (the one that opens), a "Data" sheet with\n the full table; the summary\'s figures are formulas into Data.\n\n## Failures we have seen (do not repeat)\n\n- No title, period, units or source: a CFO cannot forward it. -> Title block, always.\n- Header labels left-aligned over right-aligned numbers. -> Align headers with data.\n- Only the given numbers, nothing derived. -> Add share, variance, rank.\n- An "average growth" that is an unweighted mean presented as if it were the total\n growth. -> Label it precisely, or compute the weighted figure.\n- Footnote in tiny grey italics as the only context. -> Title block plus notes.\n- A column repeating the same value on every row (a date already in the subtitle). -> Drop it.\n- One label doing two jobs ("Total revenue / average growth"). -> One row per measure,\n each with its own label and styling.\n- A long "key point" sentence in one cell running across the page. -> Merge it across\n the table width (mergedRegions) and keep it to one line of the table\'s width.\n\n- A chart split across two printed pages. -> `fitToPage: true`.\n- All bars one colour, so the flagged or leading item does not stand out. -> `points`.\n- A threshold "input" cell whose flag colours did not follow it (fills are static).\n -> Add an input only when everything it drives is a FORMULA (a Status column\n `IF(D6>$B$3,"Over","Within")`); state the threshold in the notes otherwise.\n- A total row with blank cells under columns that have a year-end or overall value.\n -> Fill every column of the total row with its overall figure, or a dash, never blank.\n- A "vs average" column of percentages with no unit. -> Name percentage points in the\n header ("vs avg (pp)").\n- Two versions of one measure (simple and weighted average) with no headline. -> Label\n one as the headline and say why in the notes.\n\n## Before you write the workbook, check\n\nTitle and subtitle present and merged; every derived value a formula with a correct\nrange; header alignment matches data; totals band continuous; widths fit; frozen\npanes set; negative figures coloured; a chart for the comparison over the data rows\nonly, with its subject highlighted and labels matching the cells; `fitToPage` on a\none-page summary; numbers re-added by hand once. Then write once.\n'
|
|
4
|
+
};
|
|
5
|
+
|
|
1
6
|
// src/util.ts
|
|
2
7
|
function isNumericString(v) {
|
|
3
8
|
if (typeof v === "number") return Number.isFinite(v);
|
|
@@ -916,6 +921,20 @@ function toInt(v) {
|
|
|
916
921
|
const n = Math.trunc(Number(v));
|
|
917
922
|
return Number.isFinite(n) ? n : 0;
|
|
918
923
|
}
|
|
924
|
+
function normalizePoints(points2, count) {
|
|
925
|
+
if (!isPlainObject(points2) && !Array.isArray(points2)) return [];
|
|
926
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const p of Object.values(points2)) {
|
|
928
|
+
const rec = isPlainObject(p) || Array.isArray(p) ? p : null;
|
|
929
|
+
const index = rec ? rec.index : void 0;
|
|
930
|
+
const color2 = rec ? rec.color : void 0;
|
|
931
|
+
const m = typeof color2 === "string" ? /^#([0-9A-Fa-f]{6})$/.exec(color2) : null;
|
|
932
|
+
if (typeof index === "number" && Number.isInteger(index) && index >= 0 && index < count && m) {
|
|
933
|
+
byIndex.set(index, m[1].toUpperCase());
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([index, color2]) => ({ index, color: color2 }));
|
|
937
|
+
}
|
|
919
938
|
var CHART_TYPES = ["column", "bar", "line", "pie"];
|
|
920
939
|
function normalizeCharts(charts, sheetName2, cells) {
|
|
921
940
|
if (!isPlainObject(charts) && !Array.isArray(charts)) return [];
|
|
@@ -934,7 +953,12 @@ function normalizeCharts(charts, sheetName2, cells) {
|
|
|
934
953
|
if (values2 === null) continue;
|
|
935
954
|
const rec = s;
|
|
936
955
|
const m = typeof rec.color === "string" ? /^#([0-9A-Fa-f]{6})$/.exec(rec.color) : null;
|
|
937
|
-
series.push({
|
|
956
|
+
series.push({
|
|
957
|
+
name: typeof rec.name === "string" ? rec.name : null,
|
|
958
|
+
values: values2,
|
|
959
|
+
color: m ? m[1].toUpperCase() : null,
|
|
960
|
+
points: normalizePoints(rec.points, values2.addresses().length)
|
|
961
|
+
});
|
|
938
962
|
}
|
|
939
963
|
if (series.length === 0) continue;
|
|
940
964
|
const stack = out.length;
|
|
@@ -981,6 +1005,7 @@ function normalizePrint(print) {
|
|
|
981
1005
|
const p = print;
|
|
982
1006
|
const orientation = p.orientation === "portrait" || p.orientation === "landscape" ? p.orientation : null;
|
|
983
1007
|
const fitToWidth = p.fitToWidth === true;
|
|
1008
|
+
const fitToPage = p.fitToPage === true;
|
|
984
1009
|
let footer = null;
|
|
985
1010
|
if (isPlainObject(p.footer) || Array.isArray(p.footer)) {
|
|
986
1011
|
const f = p.footer;
|
|
@@ -988,8 +1013,8 @@ function normalizePrint(print) {
|
|
|
988
1013
|
footer = { left: slot("left"), center: slot("center"), right: slot("right") };
|
|
989
1014
|
if (footer.left === "" && footer.center === "" && footer.right === "") footer = null;
|
|
990
1015
|
}
|
|
991
|
-
if (orientation === null && !fitToWidth && footer === null) return null;
|
|
992
|
-
return { orientation, fitToWidth, footer };
|
|
1016
|
+
if (orientation === null && !fitToWidth && !fitToPage && footer === null) return null;
|
|
1017
|
+
return { orientation, fitToWidth, fitToPage, footer };
|
|
993
1018
|
}
|
|
994
1019
|
|
|
995
1020
|
// src/schema/formula-linter.ts
|
|
@@ -1904,6 +1929,15 @@ var Validator = class {
|
|
|
1904
1929
|
"true scales the sheet to one page wide when printed."
|
|
1905
1930
|
));
|
|
1906
1931
|
}
|
|
1932
|
+
if (print.fitToPage != null && typeof print.fitToPage !== "boolean") {
|
|
1933
|
+
errors.push(error(
|
|
1934
|
+
`${path}.print.fitToPage`,
|
|
1935
|
+
"boolean",
|
|
1936
|
+
typeOf(print.fitToPage),
|
|
1937
|
+
print.fitToPage,
|
|
1938
|
+
"true scales the sheet onto ONE page, wide and tall: a one-page summary with a chart prints whole."
|
|
1939
|
+
));
|
|
1940
|
+
}
|
|
1907
1941
|
if (print.footer != null) {
|
|
1908
1942
|
if (!isPlainObject(print.footer) && !Array.isArray(print.footer)) {
|
|
1909
1943
|
errors.push(error(
|
|
@@ -1975,6 +2009,39 @@ var Validator = class {
|
|
|
1975
2009
|
return errors;
|
|
1976
2010
|
}
|
|
1977
2011
|
};
|
|
2012
|
+
function validatePoints(points2, at2, values2) {
|
|
2013
|
+
if (!Array.isArray(points2)) {
|
|
2014
|
+
return [
|
|
2015
|
+
error(
|
|
2016
|
+
at2,
|
|
2017
|
+
"a list of points",
|
|
2018
|
+
typeOf(points2),
|
|
2019
|
+
points2,
|
|
2020
|
+
'Colour single points: [{"index": 2, "color": "#C2410C"}], index 0-based within values.'
|
|
2021
|
+
)
|
|
2022
|
+
];
|
|
2023
|
+
}
|
|
2024
|
+
const count = values2 === null ? 0 : values2.addresses().length;
|
|
2025
|
+
const errors = [];
|
|
2026
|
+
points2.forEach((p, k) => {
|
|
2027
|
+
const rec = isPlainObject(p) || Array.isArray(p) ? p : null;
|
|
2028
|
+
const index = rec ? rec.index ?? null : null;
|
|
2029
|
+
const color2 = rec ? rec.color ?? null : null;
|
|
2030
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index >= count) {
|
|
2031
|
+
errors.push(error(
|
|
2032
|
+
`${at2}[${k}].index`,
|
|
2033
|
+
"an index within values (0 to " + Math.max(0, count - 1) + ")",
|
|
2034
|
+
typeOf(index),
|
|
2035
|
+
index,
|
|
2036
|
+
'The 0-based position of the point in its values range: "B5:B7" has points 0, 1 and 2.'
|
|
2037
|
+
));
|
|
2038
|
+
}
|
|
2039
|
+
if (typeof color2 !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(color2)) {
|
|
2040
|
+
errors.push(error(`${at2}[${k}].color`, "#RRGGBB", typeOf(color2), color2, 'A hex colour, like "#C2410C".'));
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
return errors;
|
|
2044
|
+
}
|
|
1978
2045
|
function validateCharts(charts, path, sheetName2) {
|
|
1979
2046
|
if (!Array.isArray(charts)) {
|
|
1980
2047
|
return [
|
|
@@ -2031,6 +2098,9 @@ function validateCharts(charts, path, sheetName2) {
|
|
|
2031
2098
|
errors.push(error(`${at2}.series[${j}].color`, "#RRGGBB", typeOf(color2), color2, 'A hex colour, like "#1F3A5F".'));
|
|
2032
2099
|
}
|
|
2033
2100
|
}
|
|
2101
|
+
if (isObj && s.points != null) {
|
|
2102
|
+
errors.push(...validatePoints(s.points, `${at2}.series[${j}].points`, ChartRange.parse(values2, sheetName2)));
|
|
2103
|
+
}
|
|
2034
2104
|
});
|
|
2035
2105
|
}
|
|
2036
2106
|
if (c.categories != null && ChartRange.parse(c.categories, sheetName2) === null) {
|
|
@@ -3135,6 +3205,7 @@ function sheetToSchema(sheet) {
|
|
|
3135
3205
|
const print = {};
|
|
3136
3206
|
if (sheet.print.orientation !== null) print["orientation"] = sheet.print.orientation;
|
|
3137
3207
|
if (sheet.print.fitToWidth) print["fitToWidth"] = true;
|
|
3208
|
+
if (sheet.print.fitToPage) print["fitToPage"] = true;
|
|
3138
3209
|
if (sheet.print.footer !== null) {
|
|
3139
3210
|
const footer = {};
|
|
3140
3211
|
for (const k of ["left", "center", "right"]) if (sheet.print.footer[k] !== "") footer[k] = sheet.print.footer[k];
|
|
@@ -3156,6 +3227,7 @@ function chartToSchema(chart, sheetName2) {
|
|
|
3156
3227
|
if (s.name !== null) series["name"] = s.name;
|
|
3157
3228
|
series["values"] = s.values.toSchema(sheetName2);
|
|
3158
3229
|
if (s.color !== null) series["color"] = "#" + s.color;
|
|
3230
|
+
if (s.points.length > 0) series["points"] = s.points.map((p) => ({ index: p.index, color: "#" + p.color }));
|
|
3159
3231
|
return series;
|
|
3160
3232
|
});
|
|
3161
3233
|
out["position"] = {
|
|
@@ -3680,7 +3752,7 @@ var ChartParser = {
|
|
|
3680
3752
|
const catRef = formula(ser, "cat");
|
|
3681
3753
|
categories = catRef !== null ? ChartRange.parse(catRef, sheetName2) : null;
|
|
3682
3754
|
}
|
|
3683
|
-
series.push({ name: seriesName(ser), values: values2, color: seriesColor(ser) });
|
|
3755
|
+
series.push({ name: seriesName(ser), values: values2, color: seriesColor(ser), points: points(ser) });
|
|
3684
3756
|
}
|
|
3685
3757
|
if (series.length === 0) return null;
|
|
3686
3758
|
const labels = el(plot, "dLbls");
|
|
@@ -3736,6 +3808,18 @@ function seriesName(ser) {
|
|
|
3736
3808
|
const cached = descendants(tx, "v")[0];
|
|
3737
3809
|
return cached ? cached.text : null;
|
|
3738
3810
|
}
|
|
3811
|
+
function points(ser) {
|
|
3812
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
3813
|
+
for (const dPt of els(ser, "dPt")) {
|
|
3814
|
+
const idx = at(el(dPt, "idx"), "val");
|
|
3815
|
+
const clr = descendants(dPt, "srgbClr")[0];
|
|
3816
|
+
const color2 = (clr ? at(clr, "val") ?? "" : "").toUpperCase();
|
|
3817
|
+
if (idx !== void 0 && /^[0-9]+$/.test(idx) && /^[0-9A-F]{6}$/.test(color2)) {
|
|
3818
|
+
byIndex.set(parseInt(idx, 10), color2);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
return [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([index, color2]) => ({ index, color: color2 }));
|
|
3822
|
+
}
|
|
3739
3823
|
function seriesColor(ser) {
|
|
3740
3824
|
const spPr = el(ser, "spPr");
|
|
3741
3825
|
if (!spPr) return null;
|
|
@@ -3931,10 +4015,11 @@ function parsePrint(xml) {
|
|
|
3931
4015
|
const o = at(setup, "orientation");
|
|
3932
4016
|
const orientation = o === "portrait" || o === "landscape" ? o : null;
|
|
3933
4017
|
const fitToWidth = fitToPage && setup !== void 0 && at(setup, "fitToWidth") === "1" && ["0", ""].includes(at(setup, "fitToHeight") ?? "");
|
|
4018
|
+
const fitToPageOnly = fitToPage && setup !== void 0 && at(setup, "fitToWidth") === "1" && at(setup, "fitToHeight") === "1";
|
|
3934
4019
|
const oddFooter = el(el(xml, "headerFooter"), "oddFooter");
|
|
3935
4020
|
const footer = oddFooter !== void 0 ? parseFooterCodes(oddFooter.text) : null;
|
|
3936
|
-
if (orientation === null && !fitToWidth && footer === null) return null;
|
|
3937
|
-
return { orientation, fitToWidth, footer };
|
|
4021
|
+
if (orientation === null && !fitToWidth && !fitToPageOnly && footer === null) return null;
|
|
4022
|
+
return { orientation, fitToWidth, fitToPage: fitToPageOnly, footer };
|
|
3938
4023
|
}
|
|
3939
4024
|
function parseFooterCodes(codes) {
|
|
3940
4025
|
const slots = { left: "", center: "", right: "" };
|
|
@@ -4412,7 +4497,12 @@ function seriesXml(i, s, chart, workbook) {
|
|
|
4412
4497
|
const mark = type === "line" ? '<c:marker><c:symbol val="circle"/><c:size val="6"/></c:marker>' : "";
|
|
4413
4498
|
const cat = chart.categories !== null ? "<c:cat>" + strRef(chart.categories, workbook) + "</c:cat>" : "";
|
|
4414
4499
|
const smooth = type === "line" ? '<c:smooth val="0"/>' : "";
|
|
4415
|
-
|
|
4500
|
+
let points2 = "";
|
|
4501
|
+
for (const p of s.points) {
|
|
4502
|
+
const solid = `<a:solidFill><a:srgbClr val="${p.color}"/></a:solidFill>`;
|
|
4503
|
+
points2 += `<c:dPt><c:idx val="${p.index}"/>` + (type === "line" ? `<c:marker><c:symbol val="circle"/><c:size val="6"/><c:spPr>${solid}<a:ln>${solid}</a:ln></c:spPr></c:marker><c:bubble3D val="0"/>` : type === "pie" ? `<c:bubble3D val="0"/><c:spPr>${solid}</c:spPr>` : `<c:invertIfNegative val="0"/><c:bubble3D val="0"/><c:spPr>${solid}</c:spPr>`) + "</c:dPt>";
|
|
4504
|
+
}
|
|
4505
|
+
return `<c:ser><c:idx val="${i}"/><c:order val="${i}"/>` + name + fill + invert + mark + points2 + cat + "<c:val>" + numRef(s.values, workbook) + "</c:val>" + smooth + "</c:ser>";
|
|
4416
4506
|
}
|
|
4417
4507
|
function strRef(range, workbook) {
|
|
4418
4508
|
let pts = "";
|
|
@@ -4626,10 +4716,12 @@ var XlsxWriter = class {
|
|
|
4626
4716
|
}
|
|
4627
4717
|
const legacyDrawing = sheet.hasComments() ? '<legacyDrawing r:id="rId1"/>' : "";
|
|
4628
4718
|
const print = sheet.print;
|
|
4629
|
-
const
|
|
4719
|
+
const fits = (print?.fitToWidth ?? false) || (print?.fitToPage ?? false);
|
|
4720
|
+
const sheetPrXml = fits ? '<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr>' : "";
|
|
4630
4721
|
let pageSetupXml = "";
|
|
4631
|
-
if (print !== null && (print.orientation !== null ||
|
|
4632
|
-
pageSetupXml = "<pageSetup" + (print.orientation !== null ? ` orientation="${print.orientation}"` : "") +
|
|
4722
|
+
if (print !== null && (print.orientation !== null || fits)) {
|
|
4723
|
+
pageSetupXml = "<pageSetup" + (print.orientation !== null ? ` orientation="${print.orientation}"` : "") + // fitToPage: one page wide AND tall. fitToWidth: one wide, as tall as it needs.
|
|
4724
|
+
(print.fitToPage ? ' fitToWidth="1" fitToHeight="1"' : print.fitToWidth ? ' fitToWidth="1" fitToHeight="0"' : "") + "/>";
|
|
4633
4725
|
}
|
|
4634
4726
|
const headerFooterXml = print?.footer ? `<headerFooter><oddFooter>${this.escape(footerCodes(print.footer))}</oddFooter></headerFooter>` : "";
|
|
4635
4727
|
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>";
|
|
@@ -5777,7 +5869,20 @@ var holy_sheet_schema_default = {
|
|
|
5777
5869
|
properties: {
|
|
5778
5870
|
name: { type: "string", description: "Shown in the legend." },
|
|
5779
5871
|
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." }
|
|
5872
|
+
color: { type: "string", description: "#RRGGBB. Ignored for a pie, whose slices take a palette." },
|
|
5873
|
+
points: {
|
|
5874
|
+
type: "array",
|
|
5875
|
+
description: 'Colour single points to make the finding stand out: the one bar or slice the title is about. index is 0-based within values: [{"index": 2, "color": "#C2410C"}].',
|
|
5876
|
+
items: {
|
|
5877
|
+
type: "object",
|
|
5878
|
+
additionalProperties: false,
|
|
5879
|
+
required: ["index", "color"],
|
|
5880
|
+
properties: {
|
|
5881
|
+
index: { type: "integer", minimum: 0 },
|
|
5882
|
+
color: { type: "string", description: "#RRGGBB" }
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5781
5886
|
}
|
|
5782
5887
|
}
|
|
5783
5888
|
},
|
|
@@ -5800,6 +5905,7 @@ var holy_sheet_schema_default = {
|
|
|
5800
5905
|
properties: {
|
|
5801
5906
|
orientation: { type: "string", enum: ["portrait", "landscape"] },
|
|
5802
5907
|
fitToWidth: { type: "boolean", description: "Scale the sheet to one page wide (as many pages tall as it needs)." },
|
|
5908
|
+
fitToPage: { type: "boolean", description: "Scale the sheet onto ONE page, wide and tall. Use it for a one-page summary with a chart, so the page break never cuts the chart." },
|
|
5803
5909
|
footer: {
|
|
5804
5910
|
type: "object",
|
|
5805
5911
|
additionalProperties: false,
|
|
@@ -6019,7 +6125,7 @@ var holy_sheet_schema_default = {
|
|
|
6019
6125
|
};
|
|
6020
6126
|
|
|
6021
6127
|
// src/agent.ts
|
|
6022
|
-
var VERSION = "2.
|
|
6128
|
+
var VERSION = "2.10.0";
|
|
6023
6129
|
function toU8(input) {
|
|
6024
6130
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
6025
6131
|
}
|
|
@@ -6123,6 +6229,14 @@ var Agent = {
|
|
|
6123
6229
|
equivalent(a, b) {
|
|
6124
6230
|
return SheetDiff.equivalent(a, b);
|
|
6125
6231
|
},
|
|
6232
|
+
/**
|
|
6233
|
+
* The agent guide: what makes this kind of file professional, and the schema
|
|
6234
|
+
* idioms that produce it. Give it to a model with the schema; it works with any
|
|
6235
|
+
* agent loop. Byte-identical to the PHP package's agent-guide/professional-workbooks.md.
|
|
6236
|
+
*/
|
|
6237
|
+
guide() {
|
|
6238
|
+
return agent_guide_default.guide;
|
|
6239
|
+
},
|
|
6126
6240
|
version() {
|
|
6127
6241
|
return VERSION;
|
|
6128
6242
|
}
|