@particle-academy/holy-sheet 1.0.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.cjs ADDED
@@ -0,0 +1,3247 @@
1
+ 'use strict';
2
+
3
+ // src/util.ts
4
+ function isNumericString(v) {
5
+ if (typeof v === "number") return Number.isFinite(v);
6
+ if (typeof v !== "string") return false;
7
+ return /^\s*[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/.test(v);
8
+ }
9
+ function numericStringToNumber(v) {
10
+ return v.includes(".") ? parseFloat(v) : parseInt(v, 10);
11
+ }
12
+ function formatFloat(v) {
13
+ let s = v.toFixed(14);
14
+ s = s.replace(/0+$/, "").replace(/\.$/, "");
15
+ return s === "" || s === "-0" ? "0" : s;
16
+ }
17
+ function typeOf(value) {
18
+ if (value === null || value === void 0) return "null";
19
+ if (Array.isArray(value)) return "array";
20
+ switch (typeof value) {
21
+ case "object":
22
+ return "object";
23
+ case "boolean":
24
+ return "bool";
25
+ case "number":
26
+ return Number.isInteger(value) ? "int" : "float";
27
+ case "string":
28
+ return "string";
29
+ default:
30
+ return typeof value;
31
+ }
32
+ }
33
+ function isPlainObject(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+
37
+ // src/schema/inference.ts
38
+ var SAMPLE_SIZE = 50;
39
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
40
+ var ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
41
+ var HEADER_INTEGER = /(^|[\s_])(count|qty|quantity|num|number|id|n)([\s_]|$)/i;
42
+ var HEADER_CURRENCY = /(price|amount|cost|revenue|fee|total|salary|budget|balance)/i;
43
+ var HEADER_PERCENT = /(rate|percent|growth|yoy|margin|share|ratio)/i;
44
+ var Inference = {
45
+ detect(columnValues, headerName, options = {}) {
46
+ const sample = nonNullSample(columnValues);
47
+ if (sample.length === 0) return { header: headerName, type: "auto" };
48
+ if (allBoolean(sample)) return { header: headerName, type: "boolean" };
49
+ const allNum = allNumeric(sample);
50
+ const allInRange = allNum && allInRange01(sample);
51
+ const allInt = allNum && allInteger(sample);
52
+ const allDate = allMatch(sample, ISO_DATE);
53
+ const allDateTime = allMatch(sample, ISO_DATETIME);
54
+ if (allDate) return { header: headerName, type: "date" };
55
+ if (allDateTime) return { header: headerName, type: "datetime" };
56
+ if (allNum) {
57
+ if (HEADER_PERCENT.test(headerName) && allInRange) {
58
+ return { header: headerName, type: "percent", decimals: detectDecimals(sample) || 1 };
59
+ }
60
+ if (HEADER_CURRENCY.test(headerName)) {
61
+ return {
62
+ header: headerName,
63
+ type: "currency",
64
+ currency: options.currency ?? "USD",
65
+ decimals: detectDecimals(sample) ?? void 0
66
+ };
67
+ }
68
+ if (HEADER_INTEGER.test(headerName) && allInt) {
69
+ return { header: headerName, type: "integer" };
70
+ }
71
+ if (allInt) return { header: headerName, type: "integer" };
72
+ const col = { header: headerName, type: "number" };
73
+ const decimals = detectDecimals(sample);
74
+ if (decimals !== null && decimals > 0) col.decimals = decimals;
75
+ return col;
76
+ }
77
+ if (allStringish(sample)) return { header: headerName, type: "string" };
78
+ return { header: headerName, type: "auto" };
79
+ }
80
+ };
81
+ function nonNullSample(values) {
82
+ const out = [];
83
+ for (const v of values) {
84
+ if (v === null || v === void 0) continue;
85
+ out.push(v);
86
+ if (out.length >= SAMPLE_SIZE) break;
87
+ }
88
+ return out;
89
+ }
90
+ function allBoolean(sample) {
91
+ return sample.every((v) => typeof v === "boolean");
92
+ }
93
+ function allNumeric(sample) {
94
+ return sample.every((v) => {
95
+ if (typeof v === "boolean") return false;
96
+ if (typeof v === "number") return Number.isFinite(v);
97
+ return typeof v === "string" && isNumericString(v);
98
+ });
99
+ }
100
+ function allInteger(sample) {
101
+ return sample.every((v) => {
102
+ if (typeof v === "number") return Number.isInteger(v);
103
+ return typeof v === "string" && /^-?\d+$/.test(v);
104
+ });
105
+ }
106
+ function allInRange01(sample) {
107
+ return sample.every((v) => {
108
+ const f = Number(v);
109
+ return f >= 0 && f <= 1;
110
+ });
111
+ }
112
+ function detectDecimals(sample) {
113
+ let max = 0;
114
+ let sawFloat = false;
115
+ for (const v of sample) {
116
+ const s = typeof v === "string" ? v : String(v);
117
+ if (s.includes(".")) {
118
+ sawFloat = true;
119
+ const frac = (s.split(".", 2)[1] ?? "").replace(/0+$/, "");
120
+ if (frac.length > max) max = frac.length;
121
+ }
122
+ }
123
+ if (!sawFloat) return 0;
124
+ return Math.max(max, 2);
125
+ }
126
+ function allMatch(sample, regex) {
127
+ return sample.every((v) => typeof v === "string" && regex.test(v));
128
+ }
129
+ function allStringish(sample) {
130
+ return sample.every((v) => typeof v === "string");
131
+ }
132
+
133
+ // src/helpers/array-builder.ts
134
+ var ArrayBuilder = {
135
+ build(rows, headers = null, sheetName = "Sheet 1", options = {}) {
136
+ let hdrs = headers;
137
+ let data = rows;
138
+ if (hdrs === null) {
139
+ if (rows.length === 0) return wrap([], [], sheetName, options);
140
+ hdrs = (rows[0] ?? []).map((v) => String(v));
141
+ data = rows.slice(1);
142
+ }
143
+ const columns = inferColumns(data, hdrs, options);
144
+ return wrap(columns, data, sheetName, options);
145
+ }
146
+ };
147
+ function inferColumns(rows, headers, options) {
148
+ return headers.map((headerName, i) => {
149
+ const columnValues = rows.map((row) => row[i] ?? null);
150
+ return Inference.detect(columnValues, String(headerName), options);
151
+ });
152
+ }
153
+ function wrap(columns, rows, sheetName, options) {
154
+ const sheet = { name: sheetName, columns, rows };
155
+ if (options.theme !== void 0) sheet.theme = options.theme;
156
+ if (options.totals && typeof options.totals === "object") sheet.totals = options.totals;
157
+ if (options.frozenRows !== void 0) sheet.frozenRows = Math.trunc(Number(options.frozenRows));
158
+ if (options.frozenCols !== void 0) sheet.frozenCols = Math.trunc(Number(options.frozenCols));
159
+ return { sheets: [sheet] };
160
+ }
161
+
162
+ // src/helpers/csv-builder.ts
163
+ var CsvBuilder = {
164
+ build(csv, options = {}) {
165
+ const delimiter = options.delimiter ?? ",";
166
+ const enclosure = options.enclosure ?? '"';
167
+ const sheetName = options.sheetName ?? "Sheet 1";
168
+ const rows = parseRows(csv, delimiter, enclosure);
169
+ if (rows.length === 0) {
170
+ return { sheets: [{ name: sheetName, columns: [], rows: [] }] };
171
+ }
172
+ const headers = rows[0].map((v) => String(v));
173
+ const dataRows = rows.slice(1).map(
174
+ (row) => row.map((cell) => isNumericString(cell) ? numericStringToNumber(cell) : cell)
175
+ );
176
+ return ArrayBuilder.build(dataRows, headers, sheetName, options);
177
+ }
178
+ };
179
+ function parseRows(csv, delimiter, quote) {
180
+ const rows = [];
181
+ let field = "";
182
+ let row = [];
183
+ let inQuotes = false;
184
+ let started = false;
185
+ const pushField = () => {
186
+ row.push(field);
187
+ field = "";
188
+ started = true;
189
+ };
190
+ const pushRow = () => {
191
+ pushField();
192
+ rows.push(row);
193
+ row = [];
194
+ started = false;
195
+ };
196
+ for (let i = 0; i < csv.length; i++) {
197
+ const ch = csv[i];
198
+ if (inQuotes) {
199
+ if (ch === quote) {
200
+ if (csv[i + 1] === quote) {
201
+ field += quote;
202
+ i++;
203
+ } else {
204
+ inQuotes = false;
205
+ }
206
+ } else {
207
+ field += ch;
208
+ }
209
+ continue;
210
+ }
211
+ if (ch === quote) {
212
+ inQuotes = true;
213
+ started = true;
214
+ continue;
215
+ }
216
+ if (ch === delimiter) {
217
+ pushField();
218
+ continue;
219
+ }
220
+ if (ch === "\r") {
221
+ if (csv[i + 1] === "\n") i++;
222
+ pushRow();
223
+ continue;
224
+ }
225
+ if (ch === "\n") {
226
+ pushRow();
227
+ continue;
228
+ }
229
+ field += ch;
230
+ started = true;
231
+ }
232
+ if (field !== "" || row.length > 0 || started) pushRow();
233
+ return rows.filter((r) => !(r.length === 1 && r[0] === ""));
234
+ }
235
+
236
+ // src/workbook/cell-address.ts
237
+ var CellAddress = {
238
+ /** 0-based column index → Excel-style letters. */
239
+ letter(index) {
240
+ if (index < 0) {
241
+ throw new Error(`[holy-sheet] column index must be \u2265 0, got ${index}`);
242
+ }
243
+ let letters = "";
244
+ let n = index;
245
+ do {
246
+ letters = String.fromCharCode(65 + n % 26) + letters;
247
+ n = Math.floor(n / 26) - 1;
248
+ } while (n >= 0);
249
+ return letters;
250
+ },
251
+ /** Column-letters → 0-based index. */
252
+ index(letters) {
253
+ const up = letters.trim().toUpperCase();
254
+ if (up === "" || !/^[A-Z]+$/.test(up)) {
255
+ throw new Error(`[holy-sheet] invalid column letters: '${up}'`);
256
+ }
257
+ let idx = 0;
258
+ for (let i = 0; i < up.length; i++) {
259
+ idx = idx * 26 + (up.charCodeAt(i) - 64);
260
+ }
261
+ return idx - 1;
262
+ },
263
+ /** Parse an A1 address into [columnIndex, rowNumber] (1-based row), or null. */
264
+ parse(address) {
265
+ const m = /^([A-Z]+)(\d+)$/.exec(address.trim().toUpperCase());
266
+ if (!m) return null;
267
+ return [CellAddress.index(m[1]), parseInt(m[2], 10)];
268
+ }
269
+ };
270
+
271
+ // src/workbook/cell.ts
272
+ var Cell = class {
273
+ constructor(address, value, formula = null, format = null, comment = null, cachedValue = null) {
274
+ this.address = address;
275
+ this.value = value;
276
+ this.formula = formula;
277
+ this.format = format;
278
+ this.comment = comment;
279
+ this.cachedValue = cachedValue;
280
+ }
281
+ excelType() {
282
+ if (this.formula !== null) return "str";
283
+ if (typeof this.value === "string") return "inlineStr";
284
+ if (typeof this.value === "boolean") return "b";
285
+ return "n";
286
+ }
287
+ };
288
+
289
+ // src/workbook/cell-comment.ts
290
+ var CellComment = class {
291
+ constructor(text, author = null, color = null) {
292
+ this.text = text;
293
+ this.author = author;
294
+ this.color = color;
295
+ }
296
+ };
297
+
298
+ // src/workbook/cell-format.ts
299
+ var CellFormat = class _CellFormat {
300
+ constructor(p = {}) {
301
+ this.bold = p.bold ?? false;
302
+ this.italic = p.italic ?? false;
303
+ this.textAlign = p.textAlign ?? null;
304
+ this.displayFormat = p.displayFormat ?? null;
305
+ this.decimals = p.decimals ?? null;
306
+ this.color = p.color ?? null;
307
+ this.backgroundColor = p.backgroundColor ?? null;
308
+ this.fontSize = p.fontSize ?? null;
309
+ this.borderTop = p.borderTop ?? null;
310
+ this.borderRight = p.borderRight ?? null;
311
+ this.borderBottom = p.borderBottom ?? null;
312
+ this.borderLeft = p.borderLeft ?? null;
313
+ this.currency = p.currency ?? null;
314
+ }
315
+ /** Stable dedup key (JSON of ordered fields — replaces PHP md5(serialize)). */
316
+ key() {
317
+ return JSON.stringify([
318
+ this.bold,
319
+ this.italic,
320
+ this.textAlign,
321
+ this.displayFormat,
322
+ this.decimals,
323
+ this.color,
324
+ this.backgroundColor,
325
+ this.fontSize,
326
+ this.borderTop,
327
+ this.borderRight,
328
+ this.borderBottom,
329
+ this.borderLeft,
330
+ this.currency
331
+ ]);
332
+ }
333
+ isEmpty() {
334
+ return !this.bold && !this.italic && this.textAlign === null && this.displayFormat === null && this.decimals === null && this.color === null && this.backgroundColor === null && this.fontSize === null && this.borderTop === null && this.borderRight === null && this.borderBottom === null && this.borderLeft === null && this.currency === null;
335
+ }
336
+ static fromInput(a) {
337
+ return new _CellFormat({
338
+ bold: Boolean(a.bold ?? false),
339
+ italic: Boolean(a.italic ?? false),
340
+ textAlign: a.textAlign ?? null,
341
+ displayFormat: a.displayFormat ?? null,
342
+ decimals: a.decimals != null ? Math.trunc(a.decimals) : null,
343
+ color: a.color ?? null,
344
+ backgroundColor: a.backgroundColor ?? null,
345
+ fontSize: a.fontSize != null ? Math.trunc(a.fontSize) : null,
346
+ borderTop: a.borderTop ?? null,
347
+ borderRight: a.borderRight ?? null,
348
+ borderBottom: a.borderBottom ?? null,
349
+ borderLeft: a.borderLeft ?? null,
350
+ currency: a.currency ?? null
351
+ });
352
+ }
353
+ /** Merge another format on top of this one (other wins where set). */
354
+ mergeWith(other) {
355
+ if (other === null) return this;
356
+ return new _CellFormat({
357
+ bold: other.bold || this.bold,
358
+ italic: other.italic || this.italic,
359
+ textAlign: other.textAlign ?? this.textAlign,
360
+ displayFormat: other.displayFormat ?? this.displayFormat,
361
+ decimals: other.decimals ?? this.decimals,
362
+ color: other.color ?? this.color,
363
+ backgroundColor: other.backgroundColor ?? this.backgroundColor,
364
+ fontSize: other.fontSize ?? this.fontSize,
365
+ borderTop: other.borderTop ?? this.borderTop,
366
+ borderRight: other.borderRight ?? this.borderRight,
367
+ borderBottom: other.borderBottom ?? this.borderBottom,
368
+ borderLeft: other.borderLeft ?? this.borderLeft,
369
+ currency: other.currency ?? this.currency
370
+ });
371
+ }
372
+ };
373
+
374
+ // src/workbook/merged-region.ts
375
+ var MergedRegion = class {
376
+ constructor(start, end) {
377
+ this.start = start;
378
+ this.end = end;
379
+ }
380
+ ref() {
381
+ return `${this.start}:${this.end}`;
382
+ }
383
+ };
384
+
385
+ // src/workbook/sheet.ts
386
+ var Sheet = class {
387
+ constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0) {
388
+ this.name = name;
389
+ this.cells = cells;
390
+ this.mergedRegions = mergedRegions;
391
+ this.columnWidths = columnWidths;
392
+ this.frozenRows = frozenRows;
393
+ this.frozenCols = frozenCols;
394
+ }
395
+ /** rowNumber → (colLetter → Cell), sorted by ascending row number. */
396
+ rows() {
397
+ const rows = /* @__PURE__ */ new Map();
398
+ for (const [address, cell] of this.cells) {
399
+ const m = /^([A-Z]+)(\d+)$/.exec(address);
400
+ if (!m) continue;
401
+ const rowNum = parseInt(m[2], 10);
402
+ let row = rows.get(rowNum);
403
+ if (!row) {
404
+ row = /* @__PURE__ */ new Map();
405
+ rows.set(rowNum, row);
406
+ }
407
+ row.set(m[1], cell);
408
+ }
409
+ return new Map([...rows.entries()].sort((a, b) => a[0] - b[0]));
410
+ }
411
+ hasComments() {
412
+ for (const c of this.cells.values()) if (c.comment !== null) return true;
413
+ return false;
414
+ }
415
+ comments() {
416
+ const out = [];
417
+ for (const c of this.cells.values()) {
418
+ if (c.comment !== null) out.push({ address: c.address, comment: c.comment });
419
+ }
420
+ return out;
421
+ }
422
+ };
423
+
424
+ // src/workbook/workbook.ts
425
+ var Workbook = class {
426
+ constructor(sheets, meta = {}) {
427
+ this.sheets = sheets;
428
+ this.meta = meta;
429
+ }
430
+ };
431
+
432
+ // src/writer/format/date-converter.ts
433
+ function parseToUtcMs(value) {
434
+ const s = value.trim();
435
+ if (s === "") return null;
436
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2})(?:\.\d+)?)?)?\s*(Z|[+-]\d{2}:?\d{2})?$/.exec(
437
+ s
438
+ );
439
+ if (m) {
440
+ const [, y, mo, d, h, mi, se, tz] = m;
441
+ let ms = Date.UTC(+y, +mo - 1, +d, h ? +h : 0, mi ? +mi : 0, se ? +se : 0);
442
+ if (tz && tz !== "Z") {
443
+ const sign = tz[0] === "-" ? -1 : 1;
444
+ const clean = tz.slice(1).replace(":", "");
445
+ const offMin = sign * (parseInt(clean.slice(0, 2), 10) * 60 + parseInt(clean.slice(2, 4) || "0", 10));
446
+ ms -= offMin * 6e4;
447
+ }
448
+ return ms;
449
+ }
450
+ const fallback = Date.parse(s);
451
+ return Number.isNaN(fallback) ? null : fallback;
452
+ }
453
+ var DateConverter = {
454
+ toSerial(value, includeTime = false) {
455
+ const ts = value instanceof Date ? value.getTime() : parseToUtcMs(value);
456
+ if (ts === null) return 0;
457
+ const epoch = Date.UTC(1899, 11, 30, 0, 0, 0);
458
+ let days = (ts - epoch) / 864e5;
459
+ if (!includeTime) days = Math.floor(days);
460
+ return days;
461
+ }
462
+ };
463
+
464
+ // src/schema/theme.ts
465
+ var Theme = class {
466
+ constructor(key) {
467
+ this.key = key;
468
+ }
469
+ headerFormat() {
470
+ switch (this.key) {
471
+ case "default":
472
+ case "business":
473
+ return new CellFormat({
474
+ bold: true,
475
+ color: "#FFFFFF",
476
+ backgroundColor: this.key === "business" ? "#1F2937" : "#374151"
477
+ });
478
+ case "minimal":
479
+ return new CellFormat({ bold: true, borderBottom: "#000000" });
480
+ default:
481
+ return null;
482
+ }
483
+ }
484
+ dataFormat(rowIndexZeroBased) {
485
+ if ((this.key === "default" || this.key === "business") && rowIndexZeroBased % 2 === 1) {
486
+ return new CellFormat({ backgroundColor: "#F3F4F6" });
487
+ }
488
+ return null;
489
+ }
490
+ totalsFormat() {
491
+ switch (this.key) {
492
+ case "default":
493
+ case "business":
494
+ case "minimal":
495
+ return new CellFormat({ bold: true, borderTop: "#000000" });
496
+ default:
497
+ return null;
498
+ }
499
+ }
500
+ };
501
+
502
+ // src/schema/normalizer.ts
503
+ var Normalizer = class {
504
+ normalize(schema) {
505
+ const sheets = [];
506
+ for (const sheetSchema of schema.sheets) {
507
+ sheets.push(this.normalizeSheet(sheetSchema));
508
+ }
509
+ return new Workbook(sheets, schema.meta ?? {});
510
+ }
511
+ normalizeSheet(sheet) {
512
+ const name = String(sheet.name);
513
+ if (sheet.cells != null) {
514
+ return new Sheet(
515
+ name,
516
+ this.normalizeCellMap(sheet.cells),
517
+ this.normalizeMerges(sheet.mergedRegions ?? []),
518
+ this.normalizeColumnWidths(sheet.columnWidths ?? {}),
519
+ toInt(sheet.frozenRows ?? 0),
520
+ toInt(sheet.frozenCols ?? 0)
521
+ );
522
+ }
523
+ const cells = /* @__PURE__ */ new Map();
524
+ const columns = sheet.columns ?? [];
525
+ const rows = sheet.rows ?? [];
526
+ const themeKey = sheet.theme ?? "default";
527
+ const theme = new Theme(themeKey);
528
+ let headerOffset = 0;
529
+ const columnFormats = /* @__PURE__ */ new Map();
530
+ const columnByHeader = /* @__PURE__ */ new Map();
531
+ columns.forEach((columnDef, colIdx) => {
532
+ columnFormats.set(colIdx, this.columnFormat(columnDef));
533
+ if (isPlainObject(columnDef) && columnDef.header !== void 0) {
534
+ columnByHeader.set(String(columnDef.header), colIdx);
535
+ }
536
+ });
537
+ if (columns.length > 0) {
538
+ columns.forEach((columnDef, col) => {
539
+ const address = CellAddress.letter(col) + "1";
540
+ const header = isPlainObject(columnDef) ? columnDef.header ?? "" : String(columnDef);
541
+ cells.set(address, new Cell(address, String(header), null, theme.headerFormat()));
542
+ });
543
+ headerOffset = 1;
544
+ }
545
+ rows.forEach((row, r) => {
546
+ row.forEach((value, c) => {
547
+ const address = CellAddress.letter(c) + (r + 1 + headerOffset);
548
+ const columnFormat = columnFormats.get(c) ?? null;
549
+ const rowBand = theme.dataFormat(r);
550
+ const merged = rowBand !== null ? columnFormat ? columnFormat.mergeWith(rowBand) : rowBand : columnFormat;
551
+ cells.set(address, this.buildCell(address, value, merged));
552
+ });
553
+ });
554
+ if (sheet.totals && isPlainObject(sheet.totals) && Object.keys(sheet.totals).length > 0 && rows.length > 0 && columns.length > 0) {
555
+ const totalsRow = rows.length + 1 + headerOffset;
556
+ const totalsTheme = theme.totalsFormat();
557
+ const labelAddr = CellAddress.letter(0) + totalsRow;
558
+ cells.set(labelAddr, new Cell(labelAddr, "Total", null, totalsTheme));
559
+ for (const [headerKey, aggOp] of Object.entries(sheet.totals)) {
560
+ if (!columnByHeader.has(headerKey)) continue;
561
+ const colIdx = columnByHeader.get(headerKey);
562
+ const colLetter = CellAddress.letter(colIdx);
563
+ const rangeStart = colLetter + (headerOffset + 1);
564
+ const rangeEnd = colLetter + (rows.length + headerOffset);
565
+ const func = String(aggOp).toUpperCase();
566
+ if (!["SUM", "AVG", "COUNT", "MIN", "MAX"].includes(func)) continue;
567
+ const excelFunc = func === "AVG" ? "AVERAGE" : func;
568
+ const address = colLetter + totalsRow;
569
+ const colFmt = columnFormats.get(colIdx) ?? null;
570
+ const combined = colFmt ? colFmt.mergeWith(totalsTheme) : totalsTheme;
571
+ cells.set(address, new Cell(address, null, `${excelFunc}(${rangeStart}:${rangeEnd})`, combined));
572
+ }
573
+ }
574
+ return new Sheet(
575
+ name,
576
+ cells,
577
+ this.normalizeMerges(sheet.mergedRegions ?? []),
578
+ this.normalizeColumnWidths(sheet.columnWidths ?? {}),
579
+ toInt(sheet.frozenRows ?? 0),
580
+ toInt(sheet.frozenCols ?? 0)
581
+ );
582
+ }
583
+ normalizeCellMap(map) {
584
+ const cells = /* @__PURE__ */ new Map();
585
+ for (const [address, cellData] of Object.entries(map)) {
586
+ const format = cellData && isPlainObject(cellData.format) ? CellFormat.fromInput(cellData.format) : null;
587
+ const comment = cellData && isPlainObject(cellData.comment) ? new CellComment(
588
+ String(cellData.comment.text ?? ""),
589
+ cellData.comment.author ?? null,
590
+ cellData.comment.color ?? null
591
+ ) : null;
592
+ const rawValue = cellData?.value ?? null;
593
+ cells.set(
594
+ address,
595
+ new Cell(
596
+ address,
597
+ this.coerceValue(rawValue, format),
598
+ cellData?.formula ?? null,
599
+ format,
600
+ comment,
601
+ cellData?.computedValue ?? null
602
+ )
603
+ );
604
+ }
605
+ return cells;
606
+ }
607
+ normalizeMerges(list) {
608
+ const out = [];
609
+ for (const m of list) {
610
+ if (m && m.start !== void 0 && m.end !== void 0) {
611
+ out.push(new MergedRegion(String(m.start), String(m.end)));
612
+ }
613
+ }
614
+ return out;
615
+ }
616
+ normalizeColumnWidths(widths) {
617
+ const out = /* @__PURE__ */ new Map();
618
+ for (const [key, px] of Object.entries(widths)) {
619
+ out.set(parseInt(key, 10), Number(px));
620
+ }
621
+ return out;
622
+ }
623
+ columnFormat(columnDef) {
624
+ if (typeof columnDef === "string") return null;
625
+ const type = columnDef.type ?? "auto";
626
+ const decimals = columnDef.decimals != null ? toInt(columnDef.decimals) : null;
627
+ const currency = columnDef.currency ?? null;
628
+ switch (type) {
629
+ case "integer":
630
+ return new CellFormat({ displayFormat: "number", decimals: 0 });
631
+ case "number":
632
+ return decimals !== null ? new CellFormat({ displayFormat: "number", decimals }) : null;
633
+ case "percent":
634
+ return new CellFormat({ displayFormat: "percentage", decimals: decimals ?? 1 });
635
+ case "currency":
636
+ return new CellFormat({ displayFormat: "currency", decimals: decimals ?? 2, currency });
637
+ case "date":
638
+ return new CellFormat({ displayFormat: "date" });
639
+ case "datetime":
640
+ return new CellFormat({ displayFormat: "datetime" });
641
+ default:
642
+ return null;
643
+ }
644
+ }
645
+ buildCell(address, value, columnFormat) {
646
+ if (isPlainObject(value)) {
647
+ const cellFormat = isPlainObject(value.format) ? CellFormat.fromInput(value.format) : null;
648
+ const merged = columnFormat ? columnFormat.mergeWith(cellFormat) : cellFormat;
649
+ const comment = isPlainObject(value.comment) ? new CellComment(
650
+ String(value.comment.text ?? ""),
651
+ value.comment.author ?? null,
652
+ value.comment.color ?? null
653
+ ) : null;
654
+ const rawValue = value.value ?? null;
655
+ return new Cell(
656
+ address,
657
+ this.coerceValue(rawValue, merged),
658
+ value.formula ?? null,
659
+ merged,
660
+ comment,
661
+ value.computedValue ?? null
662
+ );
663
+ }
664
+ return new Cell(address, this.coerceValue(value, columnFormat), null, columnFormat);
665
+ }
666
+ coerceValue(value, format) {
667
+ if (value === null || value === void 0) return null;
668
+ const df = format?.displayFormat;
669
+ if (df === "date" || df === "datetime") {
670
+ if (value instanceof Date || typeof value === "string" && value.trim() !== "") {
671
+ return DateConverter.toSerial(value, df === "datetime");
672
+ }
673
+ }
674
+ if (typeof value === "string" && isNumericString(value)) {
675
+ return numericStringToNumber(value);
676
+ }
677
+ if (value instanceof Date) {
678
+ return DateConverter.toSerial(value, true);
679
+ }
680
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
681
+ return String(value);
682
+ }
683
+ return value;
684
+ }
685
+ };
686
+ function toInt(v) {
687
+ const n = Math.trunc(Number(v));
688
+ return Number.isFinite(n) ? n : 0;
689
+ }
690
+
691
+ // src/schema/formula-linter.ts
692
+ var ERR_VALUE = "#VALUE!";
693
+ var ERR_REF = "#REF!";
694
+ var ERR_NAME = "#NAME?";
695
+ var ERR_DIV0 = "#DIV/0!";
696
+ var ERR_CIRC = "#CIRC!";
697
+ var LinterError = class extends Error {
698
+ constructor(errorCode) {
699
+ super(errorCode);
700
+ this.errorCode = errorCode;
701
+ }
702
+ };
703
+ var FormulaLinter = class {
704
+ constructor() {
705
+ this.index = /* @__PURE__ */ new Map();
706
+ this.cache = /* @__PURE__ */ new Map();
707
+ }
708
+ lint(schema) {
709
+ const workbook = new Normalizer().normalize(schema);
710
+ this.index = this.buildIndex(workbook);
711
+ this.cache = /* @__PURE__ */ new Map();
712
+ const issues = [];
713
+ for (const sheet of workbook.sheets) {
714
+ for (const [address, cell] of sheet.cells) {
715
+ if (cell.formula === null) continue;
716
+ const key = sheet.name + "!" + address;
717
+ const result = this.evaluate(cell.formula, sheet.name, [key]);
718
+ this.cache.set(key, result);
719
+ if (this.isError(result)) {
720
+ issues.push({
721
+ sheet: sheet.name,
722
+ address,
723
+ formula: cell.formula,
724
+ error: result,
725
+ hint: this.hint(result, cell.formula, sheet.name)
726
+ });
727
+ }
728
+ }
729
+ }
730
+ return issues;
731
+ }
732
+ buildIndex(wb) {
733
+ const index = /* @__PURE__ */ new Map();
734
+ for (const sheet of wb.sheets) {
735
+ for (const [address, cell] of sheet.cells) {
736
+ index.set(sheet.name + "!" + address, cell);
737
+ }
738
+ }
739
+ return index;
740
+ }
741
+ isError(v) {
742
+ return typeof v === "string" && v.length > 0 && v[0] === "#";
743
+ }
744
+ evaluate(formula, defaultSheet, stack) {
745
+ try {
746
+ const c = { tokens: this.tokenize(formula), pos: 0 };
747
+ const result = this.parseExpr(c, defaultSheet, stack);
748
+ if (c.pos < c.tokens.length) return ERR_NAME;
749
+ return result;
750
+ } catch (e) {
751
+ if (e instanceof LinterError) return e.errorCode;
752
+ return ERR_NAME;
753
+ }
754
+ }
755
+ // ---- Tokenizer ----
756
+ tokenize(src) {
757
+ const tokens = [];
758
+ let i = 0;
759
+ const len = src.length;
760
+ const isSpace = (ch) => /\s/.test(ch);
761
+ const isDigit = (ch) => ch >= "0" && ch <= "9";
762
+ const isAlpha = (ch) => /[A-Za-z]/.test(ch);
763
+ const isAlnum = (ch) => /[A-Za-z0-9]/.test(ch);
764
+ while (i < len) {
765
+ const ch = src[i];
766
+ if (isSpace(ch)) {
767
+ i++;
768
+ continue;
769
+ }
770
+ if (isDigit(ch) || ch === "." && i + 1 < len && isDigit(src[i + 1])) {
771
+ const start = i;
772
+ while (i < len && (isDigit(src[i]) || src[i] === ".")) i++;
773
+ tokens.push({ type: "NUMBER", value: src.slice(start, i) });
774
+ continue;
775
+ }
776
+ if (ch === '"') {
777
+ i++;
778
+ const start = i;
779
+ while (i < len && src[i] !== '"') i++;
780
+ const value = src.slice(start, i);
781
+ i++;
782
+ tokens.push({ type: "STRING", value });
783
+ continue;
784
+ }
785
+ if (isAlpha(ch) || ch === "_" || ch === "$") {
786
+ const start = i;
787
+ while (i < len && (isAlnum(src[i]) || src[i] === "_" || src[i] === "$" || src[i] === ".")) i++;
788
+ tokens.push({ type: "IDENT", value: src.slice(start, i) });
789
+ continue;
790
+ }
791
+ if (i + 1 < len) {
792
+ const two = src.slice(i, i + 2);
793
+ if (two === "<=" || two === ">=" || two === "<>") {
794
+ tokens.push({ type: "OP", value: two });
795
+ i += 2;
796
+ continue;
797
+ }
798
+ }
799
+ if ("+-*/^%&=<>(),:!".includes(ch)) {
800
+ tokens.push({ type: "OP", value: ch });
801
+ i++;
802
+ continue;
803
+ }
804
+ throw new LinterError(ERR_NAME);
805
+ }
806
+ return tokens;
807
+ }
808
+ // ---- Parser (recursive descent) ----
809
+ peek(c) {
810
+ return c.tokens[c.pos];
811
+ }
812
+ isOp(c, ...vals) {
813
+ const t = c.tokens[c.pos];
814
+ return !!t && t.type === "OP" && vals.includes(t.value);
815
+ }
816
+ parseExpr(c, sheet, stack) {
817
+ let left = this.parseConcat(c, sheet, stack);
818
+ while (this.isOp(c, "=", "<", ">", "<=", ">=", "<>")) {
819
+ const op = c.tokens[c.pos].value;
820
+ c.pos++;
821
+ const right = this.parseConcat(c, sheet, stack);
822
+ left = this.compare(left, right, op);
823
+ }
824
+ return left;
825
+ }
826
+ parseConcat(c, sheet, stack) {
827
+ let left = this.parseArith(c, sheet, stack);
828
+ while (this.isOp(c, "&")) {
829
+ c.pos++;
830
+ const right = this.parseArith(c, sheet, stack);
831
+ left = this.coerceString(left) + this.coerceString(right);
832
+ }
833
+ return left;
834
+ }
835
+ parseArith(c, sheet, stack) {
836
+ let left = this.parseTerm(c, sheet, stack);
837
+ while (this.isOp(c, "+", "-")) {
838
+ const op = c.tokens[c.pos].value;
839
+ c.pos++;
840
+ const right = this.parseTerm(c, sheet, stack);
841
+ const a = this.coerceNumber(left);
842
+ const b = this.coerceNumber(right);
843
+ left = op === "+" ? a + b : a - b;
844
+ }
845
+ return left;
846
+ }
847
+ parseTerm(c, sheet, stack) {
848
+ let left = this.parseUnary(c, sheet, stack);
849
+ while (this.isOp(c, "*", "/", "%", "^")) {
850
+ const op = c.tokens[c.pos].value;
851
+ c.pos++;
852
+ const right = this.parseUnary(c, sheet, stack);
853
+ const a = this.coerceNumber(left);
854
+ const b = this.coerceNumber(right);
855
+ switch (op) {
856
+ case "*":
857
+ left = a * b;
858
+ break;
859
+ case "/":
860
+ if (b === 0) throw new LinterError(ERR_DIV0);
861
+ left = a / b;
862
+ break;
863
+ case "%":
864
+ left = a / 100 * b;
865
+ break;
866
+ case "^":
867
+ left = a ** b;
868
+ break;
869
+ }
870
+ }
871
+ return left;
872
+ }
873
+ parseUnary(c, sheet, stack) {
874
+ if (this.isOp(c, "-")) {
875
+ c.pos++;
876
+ const v = this.parseUnary(c, sheet, stack);
877
+ return -this.coerceNumber(v);
878
+ }
879
+ if (this.isOp(c, "+")) {
880
+ c.pos++;
881
+ return this.parseUnary(c, sheet, stack);
882
+ }
883
+ return this.parsePrimary(c, sheet, stack);
884
+ }
885
+ parsePrimary(c, sheet, stack) {
886
+ const tok = this.peek(c);
887
+ if (!tok) throw new LinterError(ERR_NAME);
888
+ if (tok.type === "OP" && tok.value === "(") {
889
+ c.pos++;
890
+ const val = this.parseExpr(c, sheet, stack);
891
+ this.expectOp(c, ")");
892
+ return val;
893
+ }
894
+ if (tok.type === "NUMBER") {
895
+ c.pos++;
896
+ return parseFloat(tok.value);
897
+ }
898
+ if (tok.type === "STRING") {
899
+ c.pos++;
900
+ return tok.value;
901
+ }
902
+ if (tok.type === "IDENT") {
903
+ const next = c.tokens[c.pos + 1];
904
+ const next2 = c.tokens[c.pos + 2];
905
+ if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
906
+ const sheetName = this.cleanSheetName(tok.value);
907
+ c.pos += 2;
908
+ const startTok = c.tokens[c.pos].value;
909
+ c.pos++;
910
+ return this.resolveRefOrRange(startTok, sheetName, c, stack);
911
+ }
912
+ if (next && next.type === "OP" && next.value === "(") {
913
+ const name = tok.value.toUpperCase();
914
+ c.pos += 2;
915
+ const args = [];
916
+ if (!this.isOp(c, ")")) {
917
+ for (; ; ) {
918
+ args.push(this.parseExpr(c, sheet, stack));
919
+ if (this.isOp(c, ",")) {
920
+ c.pos++;
921
+ continue;
922
+ }
923
+ break;
924
+ }
925
+ }
926
+ this.expectOp(c, ")");
927
+ return this.callFunction(name, args);
928
+ }
929
+ const upper = tok.value.toUpperCase();
930
+ if (upper === "TRUE" || upper === "FALSE") {
931
+ c.pos++;
932
+ return upper === "TRUE";
933
+ }
934
+ c.pos++;
935
+ return this.resolveRefOrRange(tok.value, sheet, c, stack);
936
+ }
937
+ throw new LinterError(ERR_NAME);
938
+ }
939
+ resolveRefOrRange(startRef, defaultSheet, c, stack) {
940
+ const cleanStart = this.cleanRef(startRef);
941
+ if (cleanStart === null) throw new LinterError(ERR_REF);
942
+ const next = c.tokens[c.pos + 1];
943
+ if (this.isOp(c, ":") && next && next.type === "IDENT") {
944
+ const endRef = next.value;
945
+ c.pos += 2;
946
+ const cleanEnd = this.cleanRef(endRef);
947
+ if (cleanEnd === null) throw new LinterError(ERR_REF);
948
+ return this.resolveRange(defaultSheet, cleanStart, cleanEnd, stack);
949
+ }
950
+ return this.resolveCell(defaultSheet, cleanStart, stack);
951
+ }
952
+ cleanRef(ref) {
953
+ const r = ref.replace(/\$/g, "").toUpperCase();
954
+ return /^[A-Z]+\d+$/.test(r) ? r : null;
955
+ }
956
+ cleanSheetName(name) {
957
+ if (name.length >= 2 && name[0] === "'" && name[name.length - 1] === "'") {
958
+ return name.slice(1, -1);
959
+ }
960
+ return name;
961
+ }
962
+ resolveCell(sheet, a1, stack) {
963
+ const key = sheet + "!" + a1;
964
+ if (stack.includes(key)) return ERR_CIRC;
965
+ if (this.cache.has(key)) return this.cache.get(key);
966
+ const cell = this.index.get(key);
967
+ if (!cell) return null;
968
+ if (cell.formula !== null) {
969
+ const result = this.evaluate(cell.formula, sheet, [...stack, key]);
970
+ this.cache.set(key, result);
971
+ return result;
972
+ }
973
+ return cell.value;
974
+ }
975
+ resolveRange(sheet, start, end, stack) {
976
+ const a = CellAddress.parse(start);
977
+ const b = CellAddress.parse(end);
978
+ if (a === null || b === null) throw new LinterError(ERR_REF);
979
+ const col1 = Math.min(a[0], b[0]);
980
+ const row1 = Math.min(a[1], b[1]);
981
+ const col2 = Math.max(a[0], b[0]);
982
+ const row2 = Math.max(a[1], b[1]);
983
+ const values = [];
984
+ for (let r = row1; r <= row2; r++) {
985
+ for (let col = col1; col <= col2; col++) {
986
+ values.push(this.resolveCell(sheet, CellAddress.letter(col) + r, stack));
987
+ }
988
+ }
989
+ return values;
990
+ }
991
+ expectOp(c, op) {
992
+ const t = c.tokens[c.pos];
993
+ if (!t || t.type !== "OP" || t.value !== op) throw new LinterError(ERR_NAME);
994
+ c.pos++;
995
+ }
996
+ // ---- Coercion / comparison / functions ----
997
+ coerceNumber(v) {
998
+ if (this.isError(v)) throw new LinterError(v);
999
+ if (Array.isArray(v)) {
1000
+ for (const item of v) {
1001
+ if (typeof item === "number") return item;
1002
+ if (typeof item === "string" && isNumericString(item)) return Number(item);
1003
+ }
1004
+ throw new LinterError(ERR_VALUE);
1005
+ }
1006
+ if (typeof v === "number") return v;
1007
+ if (typeof v === "boolean") return v ? 1 : 0;
1008
+ if (v === null) return 0;
1009
+ if (typeof v === "string" && isNumericString(v)) return Number(v);
1010
+ throw new LinterError(ERR_VALUE);
1011
+ }
1012
+ coerceString(v) {
1013
+ if (this.isError(v)) throw new LinterError(v);
1014
+ if (v === null) return "";
1015
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
1016
+ if (Array.isArray(v)) return v.map((x) => this.coerceString(x)).join("");
1017
+ return String(v);
1018
+ }
1019
+ compare(a, b, op) {
1020
+ const cmp = this.cmpVal(a, b);
1021
+ switch (op) {
1022
+ case "=":
1023
+ return cmp === 0;
1024
+ case "<>":
1025
+ return cmp !== 0;
1026
+ case "<":
1027
+ return cmp < 0;
1028
+ case ">":
1029
+ return cmp > 0;
1030
+ case "<=":
1031
+ return cmp <= 0;
1032
+ case ">=":
1033
+ return cmp >= 0;
1034
+ default:
1035
+ return false;
1036
+ }
1037
+ }
1038
+ cmpVal(a, b) {
1039
+ const an = typeof a === "number" || typeof a === "string" && isNumericString(a);
1040
+ const bn = typeof b === "number" || typeof b === "string" && isNumericString(b);
1041
+ if (an && bn) {
1042
+ const x = Number(a);
1043
+ const y = Number(b);
1044
+ return x < y ? -1 : x > y ? 1 : 0;
1045
+ }
1046
+ const sa = String(a);
1047
+ const sb = String(b);
1048
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
1049
+ }
1050
+ callFunction(name, args) {
1051
+ const flat = this.flatten(args);
1052
+ switch (name) {
1053
+ case "SUM":
1054
+ return flat.filter((v) => this.isNumericLike(v)).reduce((s, v) => s + this.maybeNum(v), 0);
1055
+ case "AVERAGE":
1056
+ case "AVG":
1057
+ return this.avg(flat);
1058
+ case "COUNT":
1059
+ return flat.filter((v) => this.isNumericLike(v)).length;
1060
+ case "COUNTA":
1061
+ return flat.filter((v) => v !== null && v !== "").length;
1062
+ case "MIN":
1063
+ return this.minMax(flat, true);
1064
+ case "MAX":
1065
+ return this.minMax(flat, false);
1066
+ case "IF":
1067
+ return this.ifFn(args);
1068
+ case "ROUND":
1069
+ return phpRound(this.coerceNumber(args[0] ?? 0), Math.trunc(this.coerceNumber(args[1] ?? 0)));
1070
+ case "ABS":
1071
+ return Math.abs(this.coerceNumber(args[0] ?? 0));
1072
+ case "LEN":
1073
+ return this.coerceString(args[0] ?? "").length;
1074
+ case "UPPER":
1075
+ return this.coerceString(args[0] ?? "").toUpperCase();
1076
+ case "LOWER":
1077
+ return this.coerceString(args[0] ?? "").toLowerCase();
1078
+ case "CONCAT":
1079
+ case "CONCATENATE":
1080
+ return flat.map((v) => this.coerceString(v)).join("");
1081
+ case "TRUE":
1082
+ return true;
1083
+ case "FALSE":
1084
+ return false;
1085
+ default:
1086
+ return ERR_NAME;
1087
+ }
1088
+ }
1089
+ flatten(args) {
1090
+ const out = [];
1091
+ for (const a of args) {
1092
+ if (Array.isArray(a)) for (const x of a) out.push(x);
1093
+ else out.push(a);
1094
+ }
1095
+ return out;
1096
+ }
1097
+ isNumericLike(v) {
1098
+ return typeof v === "number" || typeof v === "string" && isNumericString(v) || typeof v === "boolean";
1099
+ }
1100
+ maybeNum(v) {
1101
+ return typeof v === "boolean" ? v ? 1 : 0 : Number(v);
1102
+ }
1103
+ avg(flat) {
1104
+ const nums = flat.filter((v) => this.isNumericLike(v));
1105
+ if (nums.length === 0) return ERR_DIV0;
1106
+ return nums.reduce((s, v) => s + this.maybeNum(v), 0) / nums.length;
1107
+ }
1108
+ minMax(flat, min) {
1109
+ const nums = flat.filter((v) => this.isNumericLike(v));
1110
+ if (nums.length === 0) return 0;
1111
+ const vals = nums.map((v) => this.maybeNum(v));
1112
+ return min ? Math.min(...vals) : Math.max(...vals);
1113
+ }
1114
+ ifFn(args) {
1115
+ if (args.length < 2) return ERR_VALUE;
1116
+ const cond = args[0];
1117
+ const truthy = typeof cond === "boolean" ? cond : cond !== null && cond !== 0 && cond !== "" && cond !== "FALSE";
1118
+ return truthy ? args[1] : args[2] ?? false;
1119
+ }
1120
+ // ---- Hints ----
1121
+ hint(error2, formula, sheet) {
1122
+ switch (error2) {
1123
+ case ERR_VALUE:
1124
+ return this.hintValue(formula, sheet);
1125
+ case ERR_REF:
1126
+ return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1127
+ case ERR_NAME:
1128
+ 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.";
1129
+ case ERR_DIV0:
1130
+ return "Division by zero \u2014 the divisor evaluated to 0.";
1131
+ case ERR_CIRC:
1132
+ return "Circular reference \u2014 the formula directly or transitively depends on its own cell.";
1133
+ default:
1134
+ return "Formula evaluation failed.";
1135
+ }
1136
+ }
1137
+ hintValue(formula, sheet) {
1138
+ const re = /(?:([A-Za-z][A-Za-z0-9_]*)!)?\$?([A-Z]+)\$?(\d+)/gi;
1139
+ const offenders = [];
1140
+ for (const m of formula.matchAll(re)) {
1141
+ const sheetName = m[1] ? m[1] : sheet;
1142
+ const a1 = m[2].toUpperCase() + m[3];
1143
+ const key = sheetName + "!" + a1;
1144
+ const cell = this.index.get(key);
1145
+ if (!cell) continue;
1146
+ const value = this.cache.has(key) ? this.cache.get(key) : cell.formula !== null ? null : cell.value;
1147
+ if (typeof value === "string" && !isNumericString(value) && value !== "") {
1148
+ const row = parseInt(m[3], 10);
1149
+ const col = m[2];
1150
+ const next = sheetName + "!" + col.toUpperCase() + (row + 1);
1151
+ const nextCell = this.index.get(next);
1152
+ let suggestion = "";
1153
+ if (nextCell && isNumericString(nextCell.value)) {
1154
+ suggestion = ` Did you mean ${col.toUpperCase()}${row + 1}? (it holds ${String(nextCell.value)})`;
1155
+ }
1156
+ offenders.push(`${a1} = "${value}" (string)${suggestion}`);
1157
+ }
1158
+ }
1159
+ if (offenders.length > 0) {
1160
+ return "Arithmetic on a non-numeric cell: " + offenders.join("; ");
1161
+ }
1162
+ return "A non-numeric value was used in arithmetic. Check that all referenced cells contain numbers.";
1163
+ }
1164
+ };
1165
+ function phpRound(x, n) {
1166
+ const factor = 10 ** n;
1167
+ return Math.sign(x) * Math.round(Math.abs(x) * factor) / factor;
1168
+ }
1169
+
1170
+ // src/exceptions.ts
1171
+ var SchemaException = class _SchemaException extends Error {
1172
+ constructor(errors, message) {
1173
+ super(message ?? summarize(errors));
1174
+ this.name = "SchemaException";
1175
+ this.errors = errors;
1176
+ Object.setPrototypeOf(this, _SchemaException.prototype);
1177
+ }
1178
+ static fromErrors(errors) {
1179
+ return new _SchemaException(errors);
1180
+ }
1181
+ getErrors() {
1182
+ return this.errors;
1183
+ }
1184
+ };
1185
+ function summarize(errors) {
1186
+ if (errors.length === 1) {
1187
+ const e = errors[0];
1188
+ return `[holy-sheet] schema invalid at ${e.path}: expected ${e.expected}, got ${e.got}`;
1189
+ }
1190
+ const first = errors[0];
1191
+ const rest = errors.length - 1;
1192
+ return `[holy-sheet] schema invalid at ${first.path}: expected ${first.expected}, got ${first.got} (+${rest} more)`;
1193
+ }
1194
+
1195
+ // src/schema/repairer.ts
1196
+ var VALID_THEMES = ["default", "minimal", "plain", "business"];
1197
+ var ISO_DATE2 = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/;
1198
+ var NUMERIC_TYPES = ["number", "integer", "currency", "percent"];
1199
+ var Repairer = class {
1200
+ constructor() {
1201
+ this.repairs = [];
1202
+ }
1203
+ repair(schema) {
1204
+ this.repairs = [];
1205
+ let s = clone(schema);
1206
+ s = this.repairTopLevel(s);
1207
+ if (Array.isArray(s.sheets)) {
1208
+ s.sheets = s.sheets.map((sheet, i) => this.repairSheet(sheet, `sheets[${i}]`));
1209
+ }
1210
+ return [s, this.repairs];
1211
+ }
1212
+ repairTopLevel(schema) {
1213
+ if (schema.sheets == null && schema.sheet != null) {
1214
+ const value = schema.sheet;
1215
+ delete schema.sheet;
1216
+ schema.sheets = Array.isArray(value) ? value : [value];
1217
+ this.repairs.push("renamed top-level 'sheet' \u2192 'sheets'");
1218
+ }
1219
+ return schema;
1220
+ }
1221
+ repairSheet(sheet, path) {
1222
+ if (!isPlainObject(sheet)) return sheet;
1223
+ if (sheet.rows == null && sheet.row != null) {
1224
+ sheet.rows = sheet.row;
1225
+ delete sheet.row;
1226
+ this.repairs.push(`renamed '${path}.row' \u2192 '${path}.rows'`);
1227
+ }
1228
+ if (sheet.rows != null && isPlainObject(sheet.rows)) {
1229
+ const keys = Object.keys(sheet.rows);
1230
+ const allIntegerKeys = keys.every((k) => /^\d+$/.test(k));
1231
+ if (allIntegerKeys) {
1232
+ sheet.rows = Object.values(sheet.rows);
1233
+ this.repairs.push(`converted '${path}.rows' from integer-keyed object to indexed list`);
1234
+ }
1235
+ }
1236
+ if (sheet.theme != null && !VALID_THEMES.includes(sheet.theme)) {
1237
+ const original = sheet.theme;
1238
+ sheet.theme = "default";
1239
+ this.repairs.push(`changed '${path}.theme' from '${original}' to 'default' (unknown theme)`);
1240
+ }
1241
+ if (sheet.cells != null && isPlainObject(sheet.cells)) {
1242
+ const cleaned = {};
1243
+ let changed = false;
1244
+ for (const [addr, data] of Object.entries(sheet.cells)) {
1245
+ const trimmed = addr.trim();
1246
+ if (trimmed !== addr) changed = true;
1247
+ cleaned[trimmed] = data;
1248
+ }
1249
+ if (changed) {
1250
+ sheet.cells = cleaned;
1251
+ this.repairs.push(`trimmed whitespace from cell addresses in '${path}.cells'`);
1252
+ }
1253
+ }
1254
+ if (sheet.columns != null && sheet.rows != null && Array.isArray(sheet.columns) && Array.isArray(sheet.rows)) {
1255
+ this.repairColumnTypeInference(sheet, path);
1256
+ this.repairStringifiedNumerics(sheet);
1257
+ }
1258
+ return sheet;
1259
+ }
1260
+ repairColumnTypeInference(sheet, path) {
1261
+ sheet.columns.forEach((col, colIdx) => {
1262
+ if (!isPlainObject(col)) return;
1263
+ if (col.type !== void 0 && col.type !== "auto") return;
1264
+ const values = [];
1265
+ for (const row of sheet.rows) {
1266
+ if (Array.isArray(row) && colIdx < row.length) values.push(row[colIdx]);
1267
+ }
1268
+ const nonNull = values.filter((v) => v !== null && v !== void 0);
1269
+ if (nonNull.length === 0) return;
1270
+ const allDateLike = nonNull.every((v) => typeof v === "string" && ISO_DATE2.test(v));
1271
+ if (allDateLike) {
1272
+ const type = String(nonNull[0]).includes("T") ? "datetime" : "date";
1273
+ sheet.columns[colIdx].type = type;
1274
+ this.repairs.push(`inferred '${path}.columns[${colIdx}].type' = '${type}' from row values`);
1275
+ }
1276
+ });
1277
+ }
1278
+ repairStringifiedNumerics(sheet) {
1279
+ sheet.columns.forEach((col, colIdx) => {
1280
+ if (!isPlainObject(col)) return;
1281
+ const type = col.type ?? "auto";
1282
+ if (!NUMERIC_TYPES.includes(type)) return;
1283
+ sheet.rows.forEach((row) => {
1284
+ if (!Array.isArray(row)) return;
1285
+ const value = row[colIdx];
1286
+ if (typeof value === "string" && isNumericString(value)) {
1287
+ row[colIdx] = numericStringToNumber(value);
1288
+ }
1289
+ });
1290
+ });
1291
+ }
1292
+ };
1293
+ function clone(v) {
1294
+ return typeof structuredClone === "function" ? structuredClone(v) : JSON.parse(JSON.stringify(v));
1295
+ }
1296
+
1297
+ // src/schema/validator.ts
1298
+ var VALID_THEMES2 = ["default", "minimal", "plain", "business"];
1299
+ var ALLOWED_TYPES = [
1300
+ "auto",
1301
+ "string",
1302
+ "number",
1303
+ "integer",
1304
+ "boolean",
1305
+ "date",
1306
+ "datetime",
1307
+ "currency",
1308
+ "percent",
1309
+ "formula"
1310
+ ];
1311
+ var Validator = class {
1312
+ validate(schema) {
1313
+ const errors = [];
1314
+ if (schema == null || !Array.isArray(schema.sheets)) {
1315
+ errors.push(
1316
+ error(
1317
+ "sheets",
1318
+ "array",
1319
+ typeOf(schema?.sheets ?? null),
1320
+ schema?.sheets ?? null,
1321
+ 'Top-level "sheets" must be an array of sheet definitions.'
1322
+ )
1323
+ );
1324
+ return errors;
1325
+ }
1326
+ if (schema.sheets.length === 0) {
1327
+ errors.push(
1328
+ error("sheets", "non-empty array", "empty array", [], "A workbook must contain at least one sheet.")
1329
+ );
1330
+ return errors;
1331
+ }
1332
+ schema.sheets.forEach((sheet, i) => {
1333
+ errors.push(...this.validateSheet(sheet, `sheets[${i}]`));
1334
+ });
1335
+ return errors;
1336
+ }
1337
+ assert(schema) {
1338
+ const errors = this.validate(schema);
1339
+ if (errors.length > 0) throw SchemaException.fromErrors(errors);
1340
+ }
1341
+ validateAndRepair(schema) {
1342
+ const [repairedSchema, repairs] = new Repairer().repair(schema);
1343
+ const errors = this.validate(repairedSchema);
1344
+ return { schema: repairedSchema, errors, repairs };
1345
+ }
1346
+ validateSheet(sheet, path) {
1347
+ const errors = [];
1348
+ if (!isPlainObject(sheet)) {
1349
+ return [
1350
+ error(
1351
+ path,
1352
+ "object",
1353
+ typeOf(sheet),
1354
+ sheet,
1355
+ 'Each sheet must be an object with at least a "name" key.'
1356
+ )
1357
+ ];
1358
+ }
1359
+ if (typeof sheet.name !== "string" || sheet.name.trim() === "") {
1360
+ errors.push(
1361
+ error(
1362
+ `${path}.name`,
1363
+ "non-empty string",
1364
+ typeOf(sheet.name ?? null),
1365
+ sheet.name ?? null,
1366
+ "Sheet name is required and visible in Excel's tab strip."
1367
+ )
1368
+ );
1369
+ }
1370
+ const hasColumnsRows = sheet.rows != null || sheet.columns != null;
1371
+ const hasCells = sheet.cells != null;
1372
+ if (!hasColumnsRows && !hasCells) {
1373
+ errors.push(
1374
+ error(
1375
+ path,
1376
+ "object with rows OR cells",
1377
+ "object without either",
1378
+ sheet,
1379
+ "A sheet needs either {columns, rows} (row-oriented) or {cells} (sparse A1-keyed) data."
1380
+ )
1381
+ );
1382
+ }
1383
+ if (sheet.columns != null) {
1384
+ if (!Array.isArray(sheet.columns)) {
1385
+ errors.push(
1386
+ error(
1387
+ `${path}.columns`,
1388
+ "array",
1389
+ typeOf(sheet.columns),
1390
+ sheet.columns,
1391
+ "Columns must be an array of column definitions."
1392
+ )
1393
+ );
1394
+ } else {
1395
+ sheet.columns.forEach((col, j) => {
1396
+ errors.push(...this.validateColumn(col, `${path}.columns[${j}]`));
1397
+ });
1398
+ }
1399
+ }
1400
+ if (sheet.rows != null) {
1401
+ if (!Array.isArray(sheet.rows)) {
1402
+ errors.push(
1403
+ error(`${path}.rows`, "array", typeOf(sheet.rows), sheet.rows, "Rows must be an array of arrays.")
1404
+ );
1405
+ } else {
1406
+ sheet.rows.forEach((row, j) => {
1407
+ if (!Array.isArray(row)) {
1408
+ errors.push(
1409
+ error(
1410
+ `${path}.rows[${j}]`,
1411
+ "array",
1412
+ typeOf(row),
1413
+ row,
1414
+ "Each row is an array of cell values, in column order."
1415
+ )
1416
+ );
1417
+ }
1418
+ });
1419
+ }
1420
+ }
1421
+ if (sheet.cells != null) {
1422
+ if (!isPlainObject(sheet.cells)) {
1423
+ errors.push(
1424
+ error(
1425
+ `${path}.cells`,
1426
+ "object keyed by A1 address",
1427
+ typeOf(sheet.cells),
1428
+ sheet.cells,
1429
+ 'Cells must be an object/map keyed by A1 references like "A1", "B2".'
1430
+ )
1431
+ );
1432
+ }
1433
+ }
1434
+ if (sheet.theme != null && !VALID_THEMES2.includes(sheet.theme)) {
1435
+ errors.push(
1436
+ error(
1437
+ `${path}.theme`,
1438
+ "one of: default, minimal, plain, business",
1439
+ "unknown",
1440
+ sheet.theme,
1441
+ "Pick a built-in theme or omit for default."
1442
+ )
1443
+ );
1444
+ }
1445
+ return errors;
1446
+ }
1447
+ validateColumn(col, path) {
1448
+ if (!isPlainObject(col)) {
1449
+ return [
1450
+ error(path, "object", typeOf(col), col, 'Each column is an object with at least a "header" key.')
1451
+ ];
1452
+ }
1453
+ const errors = [];
1454
+ if (typeof col.header !== "string") {
1455
+ errors.push(
1456
+ error(
1457
+ `${path}.header`,
1458
+ "string",
1459
+ typeOf(col.header ?? null),
1460
+ col.header ?? null,
1461
+ "Column header is the visible label in row 1."
1462
+ )
1463
+ );
1464
+ }
1465
+ if (col.type !== void 0 && !ALLOWED_TYPES.includes(col.type)) {
1466
+ errors.push(
1467
+ error(
1468
+ `${path}.type`,
1469
+ "one of: " + ALLOWED_TYPES.join(", "),
1470
+ "unknown",
1471
+ col.type,
1472
+ 'Pick a supported type or omit for "auto" (inferred per cell).'
1473
+ )
1474
+ );
1475
+ }
1476
+ return errors;
1477
+ }
1478
+ };
1479
+ function error(path, expected, got, value, hint) {
1480
+ return { path, expected, got, value, hint };
1481
+ }
1482
+
1483
+ // src/zip/crc32.ts
1484
+ var TABLE = /* @__PURE__ */ (() => {
1485
+ const t = new Uint32Array(256);
1486
+ for (let n = 0; n < 256; n++) {
1487
+ let c = n;
1488
+ for (let k = 0; k < 8; k++) {
1489
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1490
+ }
1491
+ t[n] = c >>> 0;
1492
+ }
1493
+ return t;
1494
+ })();
1495
+ function crc32(bytes) {
1496
+ let crc = 4294967295;
1497
+ for (let i = 0; i < bytes.length; i++) {
1498
+ crc = TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
1499
+ }
1500
+ return (crc ^ 4294967295) >>> 0;
1501
+ }
1502
+
1503
+ // src/zip/zip-writer.ts
1504
+ var encoder = new TextEncoder();
1505
+ var DOS_TIME = 0;
1506
+ var DOS_DATE = 33;
1507
+ function zipSync(files) {
1508
+ const localParts = [];
1509
+ const centralParts = [];
1510
+ let offset = 0;
1511
+ for (const f of files) {
1512
+ const nameBytes = encoder.encode(f.name);
1513
+ const crc = crc32(f.data);
1514
+ const size = f.data.length;
1515
+ const lh = new Uint8Array(30 + nameBytes.length);
1516
+ const ldv = new DataView(lh.buffer);
1517
+ ldv.setUint32(0, 67324752, true);
1518
+ ldv.setUint16(4, 20, true);
1519
+ ldv.setUint16(6, 2048, true);
1520
+ ldv.setUint16(8, 0, true);
1521
+ ldv.setUint16(10, DOS_TIME, true);
1522
+ ldv.setUint16(12, DOS_DATE, true);
1523
+ ldv.setUint32(14, crc, true);
1524
+ ldv.setUint32(18, size, true);
1525
+ ldv.setUint32(22, size, true);
1526
+ ldv.setUint16(26, nameBytes.length, true);
1527
+ ldv.setUint16(28, 0, true);
1528
+ lh.set(nameBytes, 30);
1529
+ localParts.push(lh, f.data);
1530
+ const cd = new Uint8Array(46 + nameBytes.length);
1531
+ const cdv = new DataView(cd.buffer);
1532
+ cdv.setUint32(0, 33639248, true);
1533
+ cdv.setUint16(4, 20, true);
1534
+ cdv.setUint16(6, 20, true);
1535
+ cdv.setUint16(8, 2048, true);
1536
+ cdv.setUint16(10, 0, true);
1537
+ cdv.setUint16(12, DOS_TIME, true);
1538
+ cdv.setUint16(14, DOS_DATE, true);
1539
+ cdv.setUint32(16, crc, true);
1540
+ cdv.setUint32(20, size, true);
1541
+ cdv.setUint32(24, size, true);
1542
+ cdv.setUint16(28, nameBytes.length, true);
1543
+ cdv.setUint16(30, 0, true);
1544
+ cdv.setUint16(32, 0, true);
1545
+ cdv.setUint16(34, 0, true);
1546
+ cdv.setUint16(36, 0, true);
1547
+ cdv.setUint32(38, 0, true);
1548
+ cdv.setUint32(42, offset, true);
1549
+ cd.set(nameBytes, 46);
1550
+ centralParts.push(cd);
1551
+ offset += lh.length + f.data.length;
1552
+ }
1553
+ const localSize = offset;
1554
+ const centralSize = centralParts.reduce((s, c) => s + c.length, 0);
1555
+ const eocd = new Uint8Array(22);
1556
+ const edv = new DataView(eocd.buffer);
1557
+ edv.setUint32(0, 101010256, true);
1558
+ edv.setUint16(4, 0, true);
1559
+ edv.setUint16(6, 0, true);
1560
+ edv.setUint16(8, files.length, true);
1561
+ edv.setUint16(10, files.length, true);
1562
+ edv.setUint32(12, centralSize, true);
1563
+ edv.setUint32(16, localSize, true);
1564
+ edv.setUint16(20, 0, true);
1565
+ const out = new Uint8Array(localSize + centralSize + 22);
1566
+ let p = 0;
1567
+ for (const part of localParts) {
1568
+ out.set(part, p);
1569
+ p += part.length;
1570
+ }
1571
+ for (const part of centralParts) {
1572
+ out.set(part, p);
1573
+ p += part.length;
1574
+ }
1575
+ out.set(eocd, p);
1576
+ return out;
1577
+ }
1578
+
1579
+ // src/zip/inflate.ts
1580
+ var Tree = class {
1581
+ constructor() {
1582
+ // number of codes with a given bit length
1583
+ this.table = new Uint16Array(16);
1584
+ // symbols sorted by code
1585
+ this.trans = new Uint16Array(288);
1586
+ }
1587
+ };
1588
+ var LENGTH_BITS = new Uint8Array([
1589
+ 0,
1590
+ 0,
1591
+ 0,
1592
+ 0,
1593
+ 0,
1594
+ 0,
1595
+ 0,
1596
+ 0,
1597
+ 1,
1598
+ 1,
1599
+ 1,
1600
+ 1,
1601
+ 2,
1602
+ 2,
1603
+ 2,
1604
+ 2,
1605
+ 3,
1606
+ 3,
1607
+ 3,
1608
+ 3,
1609
+ 4,
1610
+ 4,
1611
+ 4,
1612
+ 4,
1613
+ 5,
1614
+ 5,
1615
+ 5,
1616
+ 5,
1617
+ 0
1618
+ ]);
1619
+ var LENGTH_BASE = new Uint16Array([
1620
+ 3,
1621
+ 4,
1622
+ 5,
1623
+ 6,
1624
+ 7,
1625
+ 8,
1626
+ 9,
1627
+ 10,
1628
+ 11,
1629
+ 13,
1630
+ 15,
1631
+ 17,
1632
+ 19,
1633
+ 23,
1634
+ 27,
1635
+ 31,
1636
+ 35,
1637
+ 43,
1638
+ 51,
1639
+ 59,
1640
+ 67,
1641
+ 83,
1642
+ 99,
1643
+ 115,
1644
+ 131,
1645
+ 163,
1646
+ 195,
1647
+ 227,
1648
+ 258
1649
+ ]);
1650
+ var DIST_BITS = new Uint8Array([
1651
+ 0,
1652
+ 0,
1653
+ 0,
1654
+ 0,
1655
+ 1,
1656
+ 1,
1657
+ 2,
1658
+ 2,
1659
+ 3,
1660
+ 3,
1661
+ 4,
1662
+ 4,
1663
+ 5,
1664
+ 5,
1665
+ 6,
1666
+ 6,
1667
+ 7,
1668
+ 7,
1669
+ 8,
1670
+ 8,
1671
+ 9,
1672
+ 9,
1673
+ 10,
1674
+ 10,
1675
+ 11,
1676
+ 11,
1677
+ 12,
1678
+ 12,
1679
+ 13,
1680
+ 13
1681
+ ]);
1682
+ var DIST_BASE = new Uint16Array([
1683
+ 1,
1684
+ 2,
1685
+ 3,
1686
+ 4,
1687
+ 5,
1688
+ 7,
1689
+ 9,
1690
+ 13,
1691
+ 17,
1692
+ 25,
1693
+ 33,
1694
+ 49,
1695
+ 65,
1696
+ 97,
1697
+ 129,
1698
+ 193,
1699
+ 257,
1700
+ 385,
1701
+ 513,
1702
+ 769,
1703
+ 1025,
1704
+ 1537,
1705
+ 2049,
1706
+ 3073,
1707
+ 4097,
1708
+ 6145,
1709
+ 8193,
1710
+ 12289,
1711
+ 16385,
1712
+ 24577
1713
+ ]);
1714
+ var CLC_INDEX = new Uint8Array([
1715
+ 16,
1716
+ 17,
1717
+ 18,
1718
+ 0,
1719
+ 8,
1720
+ 7,
1721
+ 9,
1722
+ 6,
1723
+ 10,
1724
+ 5,
1725
+ 11,
1726
+ 4,
1727
+ 12,
1728
+ 3,
1729
+ 13,
1730
+ 2,
1731
+ 14,
1732
+ 1,
1733
+ 15
1734
+ ]);
1735
+ function buildFixedTrees(lt, dt) {
1736
+ let i;
1737
+ for (i = 0; i < 7; i++) lt.table[i] = 0;
1738
+ lt.table[7] = 24;
1739
+ lt.table[8] = 152;
1740
+ lt.table[9] = 112;
1741
+ for (i = 0; i < 24; i++) lt.trans[i] = 256 + i;
1742
+ for (i = 0; i < 144; i++) lt.trans[24 + i] = i;
1743
+ for (i = 0; i < 8; i++) lt.trans[24 + 144 + i] = 280 + i;
1744
+ for (i = 0; i < 112; i++) lt.trans[24 + 144 + 8 + i] = 144 + i;
1745
+ for (i = 0; i < 5; i++) dt.table[i] = 0;
1746
+ dt.table[5] = 32;
1747
+ for (i = 0; i < 32; i++) dt.trans[i] = i;
1748
+ }
1749
+ var OFFS = new Uint16Array(16);
1750
+ function buildTree(t, lengths, off, num) {
1751
+ let i;
1752
+ let sum = 0;
1753
+ for (i = 0; i < 16; i++) t.table[i] = 0;
1754
+ for (i = 0; i < num; i++) t.table[lengths[off + i]]++;
1755
+ t.table[0] = 0;
1756
+ for (i = 0; i < 16; i++) {
1757
+ OFFS[i] = sum;
1758
+ sum += t.table[i];
1759
+ }
1760
+ for (i = 0; i < num; i++) {
1761
+ const len = lengths[off + i];
1762
+ if (len) t.trans[OFFS[len]++] = i;
1763
+ }
1764
+ }
1765
+ var Reader = class {
1766
+ constructor(source) {
1767
+ this.index = 0;
1768
+ this.tag = 0;
1769
+ this.bitcount = 0;
1770
+ this.source = source;
1771
+ }
1772
+ };
1773
+ function getBit(d) {
1774
+ if (d.bitcount-- === 0) {
1775
+ d.tag = d.source[d.index++];
1776
+ d.bitcount = 7;
1777
+ }
1778
+ const bit = d.tag & 1;
1779
+ d.tag >>>= 1;
1780
+ return bit;
1781
+ }
1782
+ function readBits(d, num, base) {
1783
+ if (!num) return base;
1784
+ let val = 0;
1785
+ for (let i = 0; i < num; i++) val |= getBit(d) << i;
1786
+ return val + base;
1787
+ }
1788
+ function decodeSymbol(d, t) {
1789
+ let sum = 0;
1790
+ let cur = 0;
1791
+ let len = 0;
1792
+ do {
1793
+ cur = 2 * cur + getBit(d);
1794
+ len++;
1795
+ sum += t.table[len];
1796
+ cur -= t.table[len];
1797
+ } while (cur >= 0);
1798
+ return t.trans[sum + cur];
1799
+ }
1800
+ function decodeTrees(d, lt, dt) {
1801
+ const lengths = new Uint8Array(288 + 32);
1802
+ const hlit = readBits(d, 5, 257);
1803
+ const hdist = readBits(d, 5, 1);
1804
+ const hclen = readBits(d, 4, 4);
1805
+ let i;
1806
+ for (i = 0; i < 19; i++) lengths[i] = 0;
1807
+ for (i = 0; i < hclen; i++) {
1808
+ const clen = readBits(d, 3, 0);
1809
+ lengths[CLC_INDEX[i]] = clen;
1810
+ }
1811
+ const codeTree = new Tree();
1812
+ buildTree(codeTree, lengths, 0, 19);
1813
+ for (let num = 0; num < hlit + hdist; ) {
1814
+ const sym = decodeSymbol(d, codeTree);
1815
+ switch (sym) {
1816
+ case 16: {
1817
+ const prev = lengths[num - 1];
1818
+ for (let length = readBits(d, 2, 3); length; length--) lengths[num++] = prev;
1819
+ break;
1820
+ }
1821
+ case 17:
1822
+ for (let length = readBits(d, 3, 3); length; length--) lengths[num++] = 0;
1823
+ break;
1824
+ case 18:
1825
+ for (let length = readBits(d, 7, 11); length; length--) lengths[num++] = 0;
1826
+ break;
1827
+ default:
1828
+ lengths[num++] = sym;
1829
+ break;
1830
+ }
1831
+ }
1832
+ buildTree(lt, lengths, 0, hlit);
1833
+ buildTree(dt, lengths, hlit, hdist);
1834
+ }
1835
+ var Out = class {
1836
+ constructor() {
1837
+ this.buf = new Uint8Array(1024);
1838
+ this.len = 0;
1839
+ }
1840
+ push(b) {
1841
+ if (this.len >= this.buf.length) {
1842
+ const next = new Uint8Array(this.buf.length * 2);
1843
+ next.set(this.buf);
1844
+ this.buf = next;
1845
+ }
1846
+ this.buf[this.len++] = b;
1847
+ }
1848
+ };
1849
+ function inflateBlockData(d, out, lt, dt) {
1850
+ for (; ; ) {
1851
+ const sym = decodeSymbol(d, lt);
1852
+ if (sym === 256) return;
1853
+ if (sym < 256) {
1854
+ out.push(sym);
1855
+ } else {
1856
+ const lengthSym = sym - 257;
1857
+ const length = readBits(d, LENGTH_BITS[lengthSym], LENGTH_BASE[lengthSym]);
1858
+ const distSym = decodeSymbol(d, dt);
1859
+ const dist = readBits(d, DIST_BITS[distSym], DIST_BASE[distSym]);
1860
+ const offs = out.len - dist;
1861
+ for (let i = 0; i < length; i++) out.push(out.buf[offs + i]);
1862
+ }
1863
+ }
1864
+ }
1865
+ function inflateUncompressedBlock(d, out) {
1866
+ d.bitcount = 0;
1867
+ let length = d.source[d.index + 1] * 256 + d.source[d.index];
1868
+ d.index += 4;
1869
+ for (; length; length--) out.push(d.source[d.index++]);
1870
+ }
1871
+ function inflateRaw(source) {
1872
+ const d = new Reader(source);
1873
+ const out = new Out();
1874
+ const lt = new Tree();
1875
+ const dt = new Tree();
1876
+ let bfinal;
1877
+ do {
1878
+ bfinal = getBit(d);
1879
+ const btype = readBits(d, 2, 0);
1880
+ if (btype === 0) {
1881
+ inflateUncompressedBlock(d, out);
1882
+ } else if (btype === 1) {
1883
+ buildFixedTrees(lt, dt);
1884
+ inflateBlockData(d, out, lt, dt);
1885
+ } else if (btype === 2) {
1886
+ decodeTrees(d, lt, dt);
1887
+ inflateBlockData(d, out, lt, dt);
1888
+ } else {
1889
+ throw new Error("holy-sheet: invalid DEFLATE block type");
1890
+ }
1891
+ } while (!bfinal);
1892
+ return out.buf.subarray(0, out.len);
1893
+ }
1894
+
1895
+ // src/zip/zip-reader.ts
1896
+ var decoder = new TextDecoder();
1897
+ function unzipSync(data) {
1898
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1899
+ let eocd = -1;
1900
+ for (let i = data.length - 22; i >= 0; i--) {
1901
+ if (dv.getUint32(i, true) === 101010256) {
1902
+ eocd = i;
1903
+ break;
1904
+ }
1905
+ }
1906
+ if (eocd < 0) throw new Error("holy-sheet: not a zip archive (no EOCD record)");
1907
+ const count = dv.getUint16(eocd + 10, true);
1908
+ let cd = dv.getUint32(eocd + 16, true);
1909
+ const result = {};
1910
+ for (let n = 0; n < count; n++) {
1911
+ if (dv.getUint32(cd, true) !== 33639248) break;
1912
+ const method = dv.getUint16(cd + 10, true);
1913
+ const compSize = dv.getUint32(cd + 20, true);
1914
+ const nameLen = dv.getUint16(cd + 28, true);
1915
+ const extraLen = dv.getUint16(cd + 30, true);
1916
+ const commentLen = dv.getUint16(cd + 32, true);
1917
+ const localOffset = dv.getUint32(cd + 42, true);
1918
+ const name = decoder.decode(data.subarray(cd + 46, cd + 46 + nameLen));
1919
+ const lhNameLen = dv.getUint16(localOffset + 26, true);
1920
+ const lhExtraLen = dv.getUint16(localOffset + 28, true);
1921
+ const dataStart = localOffset + 30 + lhNameLen + lhExtraLen;
1922
+ const raw = data.subarray(dataStart, dataStart + compSize);
1923
+ let content;
1924
+ if (method === 0) content = raw.slice();
1925
+ else if (method === 8) content = inflateRaw(raw);
1926
+ else throw new Error(`holy-sheet: unsupported zip method ${method} for "${name}"`);
1927
+ result[name] = content;
1928
+ cd += 46 + nameLen + extraLen + commentLen;
1929
+ }
1930
+ return result;
1931
+ }
1932
+
1933
+ // src/reader/xml.ts
1934
+ function localName(name) {
1935
+ const idx = name.indexOf(":");
1936
+ return idx >= 0 ? name.slice(idx + 1) : name;
1937
+ }
1938
+ function unescapeXml(s) {
1939
+ if (s.indexOf("&") < 0) return s;
1940
+ return s.replace(/&(#x?[0-9a-fA-F]+|\w+);/g, (m, ent) => {
1941
+ switch (ent) {
1942
+ case "amp":
1943
+ return "&";
1944
+ case "lt":
1945
+ return "<";
1946
+ case "gt":
1947
+ return ">";
1948
+ case "quot":
1949
+ return '"';
1950
+ case "apos":
1951
+ return "'";
1952
+ default:
1953
+ if (ent[0] === "#") {
1954
+ const code = ent[1] === "x" || ent[1] === "X" ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
1955
+ return Number.isFinite(code) ? String.fromCodePoint(code) : m;
1956
+ }
1957
+ return m;
1958
+ }
1959
+ });
1960
+ }
1961
+ function findTagEnd(src, from) {
1962
+ let quote = "";
1963
+ for (let i = from; i < src.length; i++) {
1964
+ const ch = src[i];
1965
+ if (quote) {
1966
+ if (ch === quote) quote = "";
1967
+ } else if (ch === '"' || ch === "'") {
1968
+ quote = ch;
1969
+ } else if (ch === ">") {
1970
+ return i;
1971
+ }
1972
+ }
1973
+ return src.length;
1974
+ }
1975
+ var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
1976
+ function parseTag(inner) {
1977
+ const trimmed = inner.trim();
1978
+ let i = 0;
1979
+ while (i < trimmed.length && !/\s/.test(trimmed[i])) i++;
1980
+ const name = trimmed.slice(0, i);
1981
+ const attrs = {};
1982
+ const rest = trimmed.slice(i);
1983
+ for (const m of rest.matchAll(ATTR_RE)) {
1984
+ const key = localName(m[1]);
1985
+ const value = unescapeXml(m[3] ?? m[4] ?? "");
1986
+ attrs[key] = value;
1987
+ }
1988
+ return { name, attrs };
1989
+ }
1990
+ function parseXml(src) {
1991
+ let i = 0;
1992
+ const n = src.length;
1993
+ const root = { name: "#root", attrs: {}, children: [], text: "" };
1994
+ const stack = [root];
1995
+ while (i < n) {
1996
+ if (src[i] === "<") {
1997
+ if (src.startsWith("<!--", i)) {
1998
+ const end2 = src.indexOf("-->", i);
1999
+ i = end2 < 0 ? n : end2 + 3;
2000
+ continue;
2001
+ }
2002
+ if (src.startsWith("<![CDATA[", i)) {
2003
+ const end2 = src.indexOf("]]>", i);
2004
+ stack[stack.length - 1].text += src.slice(i + 9, end2 < 0 ? n : end2);
2005
+ i = end2 < 0 ? n : end2 + 3;
2006
+ continue;
2007
+ }
2008
+ if (src.startsWith("<?", i)) {
2009
+ const end2 = src.indexOf("?>", i);
2010
+ i = end2 < 0 ? n : end2 + 2;
2011
+ continue;
2012
+ }
2013
+ if (src.startsWith("<!", i)) {
2014
+ const end2 = src.indexOf(">", i);
2015
+ i = end2 < 0 ? n : end2 + 1;
2016
+ continue;
2017
+ }
2018
+ if (src[i + 1] === "/") {
2019
+ const end2 = src.indexOf(">", i);
2020
+ if (stack.length > 1) stack.pop();
2021
+ i = end2 < 0 ? n : end2 + 1;
2022
+ continue;
2023
+ }
2024
+ const end = findTagEnd(src, i + 1);
2025
+ const tagContent = src.slice(i + 1, end);
2026
+ const selfClosing = tagContent.endsWith("/");
2027
+ const { name, attrs } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
2028
+ const node = { name: localName(name), attrs, children: [], text: "" };
2029
+ stack[stack.length - 1].children.push(node);
2030
+ if (!selfClosing) stack.push(node);
2031
+ i = end + 1;
2032
+ } else {
2033
+ const next = src.indexOf("<", i);
2034
+ const stop = next < 0 ? n : next;
2035
+ stack[stack.length - 1].text += unescapeXml(src.slice(i, stop));
2036
+ i = stop;
2037
+ }
2038
+ }
2039
+ return root.children[0] ?? null;
2040
+ }
2041
+ function el(node, name) {
2042
+ return node?.children.find((c) => c.name === name);
2043
+ }
2044
+ function els(node, name) {
2045
+ return node ? node.children.filter((c) => c.name === name) : [];
2046
+ }
2047
+ function at(node, name) {
2048
+ return node?.attrs[name];
2049
+ }
2050
+
2051
+ // src/reader/comments-parser.ts
2052
+ var CommentsParser = {
2053
+ parse(commentsXml) {
2054
+ const xml = parseXml(commentsXml);
2055
+ if (!xml) return {};
2056
+ const authors = els(el(xml, "authors"), "author").map((a) => a.text);
2057
+ const out = {};
2058
+ for (const c of els(el(xml, "commentList"), "comment")) {
2059
+ const ref = c.attrs["ref"] ?? "";
2060
+ const authorIdx = c.attrs["authorId"] !== void 0 ? parseInt(c.attrs["authorId"], 10) : -1;
2061
+ const author = authors[authorIdx] ?? null;
2062
+ out[ref] = new CellComment(extractText(el(c, "text")), author);
2063
+ }
2064
+ return out;
2065
+ }
2066
+ };
2067
+ function extractText(textNode) {
2068
+ if (!textNode) return "";
2069
+ const parts = [];
2070
+ for (const r of els(textNode, "r")) parts.push(el(r, "t")?.text ?? "");
2071
+ for (const t of els(textNode, "t")) parts.push(t.text);
2072
+ return parts.join("");
2073
+ }
2074
+
2075
+ // src/reader/format/date-inverter.ts
2076
+ var DateInverter = {
2077
+ toIso(serial, includeTime = false) {
2078
+ const epoch = Date.UTC(1899, 11, 30, 0, 0, 0);
2079
+ const ms = epoch + Math.round(serial * 86400) * 1e3;
2080
+ const d = new Date(ms);
2081
+ const pad = (n) => String(n).padStart(2, "0");
2082
+ const date = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
2083
+ if (!includeTime) return date;
2084
+ return `${date}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
2085
+ }
2086
+ };
2087
+
2088
+ // src/reader/rels-parser.ts
2089
+ var RelsParser = {
2090
+ parse(relsXml) {
2091
+ const xml = parseXml(relsXml);
2092
+ if (!xml) return {};
2093
+ const rels = {};
2094
+ for (const rel of els(xml, "Relationship")) {
2095
+ const id = rel.attrs["Id"] ?? "";
2096
+ rels[id] = { Target: rel.attrs["Target"] ?? "", Type: rel.attrs["Type"] ?? "" };
2097
+ }
2098
+ return rels;
2099
+ },
2100
+ byType(rels, typeUriContains) {
2101
+ const out = {};
2102
+ for (const [id, rel] of Object.entries(rels)) {
2103
+ if (rel.Type.includes(typeUriContains)) out[id] = rel;
2104
+ }
2105
+ return out;
2106
+ }
2107
+ };
2108
+
2109
+ // src/reader/shared-strings-parser.ts
2110
+ var SharedStringsParser = {
2111
+ parse(xml) {
2112
+ const doc = parseXml(xml);
2113
+ if (!doc) return [];
2114
+ return els(doc, "si").map(renderSi);
2115
+ }
2116
+ };
2117
+ function renderSi(si) {
2118
+ const t = el(si, "t");
2119
+ if (t) return t.text;
2120
+ return els(si, "r").map((run) => el(run, "t")?.text ?? "").join("");
2121
+ }
2122
+
2123
+ // src/reader/format/num-fmt-parser.ts
2124
+ var BUILTIN = {
2125
+ 0: "General",
2126
+ 1: "0",
2127
+ 2: "0.00",
2128
+ 3: "#,##0",
2129
+ 4: "#,##0.00",
2130
+ 9: "0%",
2131
+ 10: "0.00%",
2132
+ 11: "0.00E+00",
2133
+ 12: "# ?/?",
2134
+ 13: "# ??/??",
2135
+ 14: "mm-dd-yy",
2136
+ 15: "d-mmm-yy",
2137
+ 16: "d-mmm",
2138
+ 17: "mmm-yy",
2139
+ 18: "h:mm AM/PM",
2140
+ 19: "h:mm:ss AM/PM",
2141
+ 20: "h:mm",
2142
+ 21: "h:mm:ss",
2143
+ 22: "m/d/yy h:mm",
2144
+ 37: "#,##0 ;(#,##0)",
2145
+ 38: "#,##0 ;[Red](#,##0)",
2146
+ 39: "#,##0.00;(#,##0.00)",
2147
+ 40: "#,##0.00;[Red](#,##0.00)",
2148
+ 45: "mm:ss",
2149
+ 46: "[h]:mm:ss",
2150
+ 47: "mmss.0",
2151
+ 48: "##0.0E+0",
2152
+ 49: "@"
2153
+ };
2154
+ var SYMBOL_TO_ISO = {
2155
+ $: "USD",
2156
+ "\u20AC": "EUR",
2157
+ "\xA3": "GBP",
2158
+ "\xA5": "JPY",
2159
+ "\u20B9": "INR",
2160
+ "\u20A9": "KRW"
2161
+ };
2162
+ var NumFmtParser = {
2163
+ parse(formatCode) {
2164
+ if (formatCode === null) return null;
2165
+ const code = formatCode;
2166
+ if (code === "" || code === "General" || code === "@") {
2167
+ return { displayFormat: code === "@" ? "text" : "auto" };
2168
+ }
2169
+ if (looksLikeDate(code)) {
2170
+ return { displayFormat: hasTimeComponent(code) ? "datetime" : "date" };
2171
+ }
2172
+ if (code.includes("%")) {
2173
+ return { displayFormat: "percentage", decimals: decimalsAfter(code) };
2174
+ }
2175
+ const m = /^"([^"]+)"/u.exec(code);
2176
+ if (m) {
2177
+ const iso = SYMBOL_TO_ISO[m[1]];
2178
+ const result = { displayFormat: "currency", decimals: decimalsAfter(code) };
2179
+ if (iso !== void 0) result.currency = iso;
2180
+ return result;
2181
+ }
2182
+ if (/^#?,?#?#?0(\.0+)?$/.test(code.split(";")[0])) {
2183
+ return { displayFormat: "number", decimals: decimalsAfter(code) };
2184
+ }
2185
+ return null;
2186
+ },
2187
+ parseBuiltin(id) {
2188
+ const code = BUILTIN[id];
2189
+ if (code === void 0) return null;
2190
+ return NumFmtParser.parse(code);
2191
+ }
2192
+ };
2193
+ function looksLikeDate(code) {
2194
+ const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
2195
+ return /[ymd]/i.test(stripped);
2196
+ }
2197
+ function hasTimeComponent(code) {
2198
+ const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
2199
+ return /[hHsS]/.test(stripped) || stripped.toLowerCase().includes("mm:");
2200
+ }
2201
+ function decimalsAfter(code) {
2202
+ const section = code.split(";")[0];
2203
+ const m = /0\.(0+)/.exec(section);
2204
+ return m ? m[1].length : 0;
2205
+ }
2206
+
2207
+ // src/reader/styles-parser.ts
2208
+ var StylesParser = {
2209
+ parse(stylesXml) {
2210
+ const xml = parseXml(stylesXml);
2211
+ if (!xml) return [];
2212
+ const numFmts = parseNumFmts(xml);
2213
+ const fonts = parseFonts(xml);
2214
+ const fills = parseFills(xml);
2215
+ const borders = parseBorders(xml);
2216
+ const xfs = [];
2217
+ for (const xf of els(el(xml, "cellXfs"), "xf")) {
2218
+ const fontId = xf.attrs["fontId"] !== void 0 ? parseInt(xf.attrs["fontId"], 10) : 0;
2219
+ const fillId = xf.attrs["fillId"] !== void 0 ? parseInt(xf.attrs["fillId"], 10) : 0;
2220
+ const borderId = xf.attrs["borderId"] !== void 0 ? parseInt(xf.attrs["borderId"], 10) : 0;
2221
+ const numFmtId = xf.attrs["numFmtId"] !== void 0 ? parseInt(xf.attrs["numFmtId"], 10) : 0;
2222
+ const alignment = el(xf, "alignment");
2223
+ const align = alignment && alignment.attrs["horizontal"] !== void 0 ? alignment.attrs["horizontal"] : null;
2224
+ const font = fonts[fontId] ?? null;
2225
+ const fill = fills[fillId] ?? null;
2226
+ const border = borders[borderId] ?? null;
2227
+ const customCode = numFmts[numFmtId];
2228
+ const numFmtParsed = customCode !== void 0 ? NumFmtParser.parse(customCode) : NumFmtParser.parseBuiltin(numFmtId);
2229
+ if (xfs.length === 0 && font === null && fill === null && border === null && !numFmtParsed && align === null) {
2230
+ xfs.push(null);
2231
+ continue;
2232
+ }
2233
+ xfs.push(buildCellFormat(font, fill, border, numFmtParsed, align));
2234
+ }
2235
+ return xfs;
2236
+ }
2237
+ };
2238
+ function parseNumFmts(xml) {
2239
+ const map = {};
2240
+ for (const nf of els(el(xml, "numFmts"), "numFmt")) {
2241
+ map[parseInt(nf.attrs["numFmtId"] ?? "0", 10)] = nf.attrs["formatCode"] ?? "";
2242
+ }
2243
+ return map;
2244
+ }
2245
+ function parseFonts(xml) {
2246
+ return els(el(xml, "fonts"), "font").map((f) => {
2247
+ const sz = el(f, "sz");
2248
+ const color = el(f, "color");
2249
+ return {
2250
+ bold: !!el(f, "b"),
2251
+ italic: !!el(f, "i"),
2252
+ size: sz ? Math.trunc(Number(at(sz, "val"))) : 11,
2253
+ color: color && at(color, "rgb") !== void 0 ? argbToHex(at(color, "rgb")) : null
2254
+ };
2255
+ });
2256
+ }
2257
+ function parseFills(xml) {
2258
+ return els(el(xml, "fills"), "fill").map((fill) => {
2259
+ const pf = el(fill, "patternFill");
2260
+ const pattern = pf ? at(pf, "patternType") ?? "" : "none";
2261
+ if (pattern !== "solid") return null;
2262
+ const fg = el(pf, "fgColor");
2263
+ return fg && at(fg, "rgb") !== void 0 ? argbToHex(at(fg, "rgb")) : null;
2264
+ });
2265
+ }
2266
+ function parseBorders(xml) {
2267
+ return els(el(xml, "borders"), "border").map((b) => {
2268
+ const rec = { top: null, right: null, bottom: null, left: null };
2269
+ for (const side of ["top", "right", "bottom", "left"]) {
2270
+ const sideEl = el(b, side);
2271
+ const color = el(sideEl, "color");
2272
+ if (sideEl && at(sideEl, "style") !== void 0 && at(sideEl, "style") !== "" && color && at(color, "rgb") !== void 0) {
2273
+ rec[side] = argbToHex(at(color, "rgb"));
2274
+ }
2275
+ }
2276
+ return rec;
2277
+ });
2278
+ }
2279
+ function buildCellFormat(font, fill, border, numFmt, align) {
2280
+ return new CellFormat({
2281
+ bold: font?.bold ?? false,
2282
+ italic: font?.italic ?? false,
2283
+ textAlign: align,
2284
+ displayFormat: numFmt?.displayFormat ?? null,
2285
+ decimals: numFmt?.decimals ?? null,
2286
+ color: font?.color ?? null,
2287
+ backgroundColor: fill,
2288
+ fontSize: font && font.size !== 11 ? font.size : null,
2289
+ borderTop: border?.top ?? null,
2290
+ borderRight: border?.right ?? null,
2291
+ borderBottom: border?.bottom ?? null,
2292
+ borderLeft: border?.left ?? null,
2293
+ currency: numFmt?.currency ?? null
2294
+ });
2295
+ }
2296
+ function argbToHex(argb) {
2297
+ const a = argb.toUpperCase();
2298
+ if (a.length === 8) return "#" + a.slice(2);
2299
+ if (a.length === 6) return "#" + a;
2300
+ return "#000000";
2301
+ }
2302
+
2303
+ // src/reader/worksheet-parser.ts
2304
+ var WorksheetParser = {
2305
+ parse(worksheetXml, name, stylesIndex, comments = {}, sharedStrings = []) {
2306
+ const xml = parseXml(worksheetXml);
2307
+ if (!xml) return new Sheet(name);
2308
+ const cells = parseCells(xml, stylesIndex, comments, sharedStrings);
2309
+ const merges = parseMerges(xml);
2310
+ const columnWidths = parseColumnWidths(xml);
2311
+ const [frozenRows, frozenCols] = parseFrozen(xml);
2312
+ return new Sheet(name, cells, merges, columnWidths, frozenRows, frozenCols);
2313
+ }
2314
+ };
2315
+ function parseCells(xml, stylesIndex, comments, sharedStrings) {
2316
+ const cells = /* @__PURE__ */ new Map();
2317
+ const sheetData = el(xml, "sheetData");
2318
+ if (!sheetData) return cells;
2319
+ for (const row of els(sheetData, "row")) {
2320
+ for (const c of els(row, "c")) {
2321
+ const address = c.attrs["r"] ?? "";
2322
+ if (address === "") continue;
2323
+ const type = c.attrs["t"] ?? "n";
2324
+ const styleIdx = c.attrs["s"] !== void 0 ? parseInt(c.attrs["s"], 10) : 0;
2325
+ const format = stylesIndex[styleIdx] ?? null;
2326
+ let formula = null;
2327
+ let cachedValue = null;
2328
+ let value = null;
2329
+ const f = el(c, "f");
2330
+ const v = el(c, "v");
2331
+ if (f) {
2332
+ formula = f.text;
2333
+ cachedValue = v ? coerceValue(v.text, type) : null;
2334
+ } else if (type === "inlineStr" && el(c, "is") && el(el(c, "is"), "t")) {
2335
+ value = el(el(c, "is"), "t").text;
2336
+ } else if (type === "s" && v) {
2337
+ const idx = parseInt(v.text, 10);
2338
+ value = sharedStrings[idx] ?? "";
2339
+ } else if (v) {
2340
+ value = coerceValue(v.text, type);
2341
+ }
2342
+ const comment = comments[address] ?? null;
2343
+ cells.set(address, new Cell(address, value, formula, format, comment, cachedValue));
2344
+ }
2345
+ }
2346
+ return cells;
2347
+ }
2348
+ function coerceValue(raw, type) {
2349
+ if (type === "b") return raw === "1" || raw === "true" || raw === "TRUE";
2350
+ if (type === "str") return raw;
2351
+ if (type === "inlineStr") return raw;
2352
+ if (raw === "") return null;
2353
+ if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
2354
+ if (/^\s*[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/.test(raw)) return parseFloat(raw);
2355
+ return raw;
2356
+ }
2357
+ function parseMerges(xml) {
2358
+ const out = [];
2359
+ for (const m of els(el(xml, "mergeCells"), "mergeCell")) {
2360
+ const ref = m.attrs["ref"] ?? "";
2361
+ if (ref.includes(":")) {
2362
+ const [start, end] = ref.split(":", 2);
2363
+ out.push(new MergedRegion(start, end));
2364
+ }
2365
+ }
2366
+ return out;
2367
+ }
2368
+ function parseColumnWidths(xml) {
2369
+ const out = /* @__PURE__ */ new Map();
2370
+ for (const col of els(el(xml, "cols"), "col")) {
2371
+ if (col.attrs["customWidth"] === void 0) continue;
2372
+ const min = parseInt(col.attrs["min"] ?? "0", 10);
2373
+ const max = parseInt(col.attrs["max"] ?? "0", 10);
2374
+ const excelWidth = Number(col.attrs["width"] ?? "0");
2375
+ const px = excelWidth * 7 + 5;
2376
+ for (let i = min; i <= max; i++) out.set(i - 1, Math.round(px));
2377
+ }
2378
+ return out;
2379
+ }
2380
+ function parseFrozen(xml) {
2381
+ for (const view of els(el(xml, "sheetViews"), "sheetView")) {
2382
+ const pane = el(view, "pane");
2383
+ if (!pane) continue;
2384
+ const rows = at(pane, "ySplit") !== void 0 ? parseInt(at(pane, "ySplit"), 10) : 0;
2385
+ const cols = at(pane, "xSplit") !== void 0 ? parseInt(at(pane, "xSplit"), 10) : 0;
2386
+ return [rows, cols];
2387
+ }
2388
+ return [0, 0];
2389
+ }
2390
+
2391
+ // src/reader/xlsx-reader.ts
2392
+ var decoder2 = new TextDecoder();
2393
+ var XlsxReader = class {
2394
+ describe(bytes) {
2395
+ return this.workbookToSchema(this.readWorkbook(bytes));
2396
+ }
2397
+ readWorkbook(bytes) {
2398
+ const files = unzipSync(bytes);
2399
+ const getText = (name) => files[name] ? decoder2.decode(files[name]) : null;
2400
+ const stylesXml = getText("xl/styles.xml");
2401
+ const stylesIndex = stylesXml ? StylesParser.parse(stylesXml) : [];
2402
+ const sharedStringsXml = getText("xl/sharedStrings.xml");
2403
+ const sharedStrings = sharedStringsXml ? SharedStringsParser.parse(sharedStringsXml) : [];
2404
+ const workbookXml = getText("xl/workbook.xml");
2405
+ if (workbookXml === null) throw new Error("[holy-sheet] missing xl/workbook.xml");
2406
+ const workbookRelsXml = getText("xl/_rels/workbook.xml.rels");
2407
+ const workbookRels = workbookRelsXml ? RelsParser.parse(workbookRelsXml) : {};
2408
+ const workbookDoc = parseXml(workbookXml);
2409
+ if (!workbookDoc) throw new Error("[holy-sheet] failed to parse xl/workbook.xml");
2410
+ const sheets = [];
2411
+ let i = 0;
2412
+ for (const sheetEl of els(el(workbookDoc, "sheets"), "sheet")) {
2413
+ const name = at(sheetEl, "name") ?? "";
2414
+ const rId = at(sheetEl, "id") ?? "";
2415
+ const target = workbookRels[rId]?.Target;
2416
+ if (target == null) continue;
2417
+ const sheetPath = "xl/" + target.replace(/^\//, "");
2418
+ const worksheetXml = getText(sheetPath);
2419
+ if (worksheetXml === null) continue;
2420
+ const sheetNum = i + 1;
2421
+ const sheetRelsXml = getText(`xl/worksheets/_rels/sheet${sheetNum}.xml.rels`);
2422
+ let comments = {};
2423
+ if (sheetRelsXml) {
2424
+ const sheetRels = RelsParser.parse(sheetRelsXml);
2425
+ for (const cr of Object.values(RelsParser.byType(sheetRels, "/comments"))) {
2426
+ const commentsPath = resolveRelativePath(sheetPath, cr.Target);
2427
+ const commentsXml = getText(commentsPath);
2428
+ if (commentsXml) comments = { ...comments, ...CommentsParser.parse(commentsXml) };
2429
+ }
2430
+ }
2431
+ sheets.push(WorksheetParser.parse(worksheetXml, name, stylesIndex, comments, sharedStrings));
2432
+ i++;
2433
+ }
2434
+ const meta = this.parseDocProps(getText("docProps/core.xml"));
2435
+ return new Workbook(sheets, meta);
2436
+ }
2437
+ parseDocProps(coreXml) {
2438
+ if (coreXml === null) return {};
2439
+ const core = parseXml(coreXml);
2440
+ if (!core) return {};
2441
+ const meta = {};
2442
+ const creator = el(core, "creator");
2443
+ if (creator) meta["creator"] = creator.text;
2444
+ const created = el(core, "created");
2445
+ if (created) meta["created"] = created.text;
2446
+ return meta;
2447
+ }
2448
+ workbookToSchema(workbook) {
2449
+ const schema = { sheets: workbook.sheets.map((s) => this.sheetToSchema(s)) };
2450
+ if (Object.keys(workbook.meta).length > 0) schema["meta"] = workbook.meta;
2451
+ return schema;
2452
+ }
2453
+ sheetToSchema(sheet) {
2454
+ const cells = {};
2455
+ for (const [address, cell] of sheet.cells) {
2456
+ const cellSchema = this.cellToSchema(cell);
2457
+ if (cellSchema === null) continue;
2458
+ cells[address] = cellSchema;
2459
+ }
2460
+ const out = { name: sheet.name, cells };
2461
+ if (sheet.mergedRegions.length > 0) {
2462
+ out["mergedRegions"] = sheet.mergedRegions.map((m) => ({ start: m.start, end: m.end }));
2463
+ }
2464
+ if (sheet.columnWidths.size > 0) {
2465
+ const widths = {};
2466
+ for (const [k, v] of sheet.columnWidths) widths[k] = v;
2467
+ out["columnWidths"] = widths;
2468
+ }
2469
+ if (sheet.frozenRows > 0) out["frozenRows"] = sheet.frozenRows;
2470
+ if (sheet.frozenCols > 0) out["frozenCols"] = sheet.frozenCols;
2471
+ return out;
2472
+ }
2473
+ cellToSchema(cell) {
2474
+ let value = cell.value;
2475
+ const format = cell.format;
2476
+ if (format !== null && (format.displayFormat === "date" || format.displayFormat === "datetime") && isNumericString(value)) {
2477
+ value = DateInverter.toIso(Number(value), format.displayFormat === "datetime");
2478
+ }
2479
+ const out = { value };
2480
+ if (cell.formula !== null) out["formula"] = cell.formula;
2481
+ if (cell.cachedValue !== null) out["computedValue"] = cell.cachedValue;
2482
+ if (format !== null && !format.isEmpty()) out["format"] = formatToObject(format);
2483
+ if (cell.comment !== null) out["comment"] = commentToObject(cell.comment);
2484
+ if (Object.keys(out).length === 1 && out["value"] === null) return null;
2485
+ return out;
2486
+ }
2487
+ };
2488
+ function formatToObject(f) {
2489
+ const out = {};
2490
+ if (f.bold) out["bold"] = true;
2491
+ if (f.italic) out["italic"] = true;
2492
+ if (f.textAlign !== null) out["textAlign"] = f.textAlign;
2493
+ if (f.displayFormat !== null) out["displayFormat"] = f.displayFormat;
2494
+ if (f.decimals !== null) out["decimals"] = f.decimals;
2495
+ if (f.color !== null) out["color"] = f.color;
2496
+ if (f.backgroundColor !== null) out["backgroundColor"] = f.backgroundColor;
2497
+ if (f.fontSize !== null) out["fontSize"] = f.fontSize;
2498
+ if (f.borderTop !== null) out["borderTop"] = f.borderTop;
2499
+ if (f.borderRight !== null) out["borderRight"] = f.borderRight;
2500
+ if (f.borderBottom !== null) out["borderBottom"] = f.borderBottom;
2501
+ if (f.borderLeft !== null) out["borderLeft"] = f.borderLeft;
2502
+ if (f.currency !== null) out["currency"] = f.currency;
2503
+ return out;
2504
+ }
2505
+ function commentToObject(c) {
2506
+ const out = { text: c.text };
2507
+ if (c.author !== null) out["author"] = c.author;
2508
+ if (c.color !== null) out["color"] = c.color;
2509
+ return out;
2510
+ }
2511
+ function resolveRelativePath(base, target) {
2512
+ const baseDir = base.includes("/") ? base.slice(0, base.lastIndexOf("/")) : ".";
2513
+ const combined = baseDir + "/" + target;
2514
+ const parts = [];
2515
+ for (const segment of combined.split("/")) {
2516
+ if (segment === "..") parts.pop();
2517
+ else if (segment !== "" && segment !== ".") parts.push(segment);
2518
+ }
2519
+ return parts.join("/");
2520
+ }
2521
+
2522
+ // src/xml.ts
2523
+ var CONTROL_CHARS = new RegExp("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "g");
2524
+ function xmlEscape(s) {
2525
+ return s.replace(CONTROL_CHARS, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2526
+ }
2527
+
2528
+ // src/writer/format/num-fmt-builder.ts
2529
+ var NumFmtBuilder = {
2530
+ build(format) {
2531
+ const df = format.displayFormat;
2532
+ const decimals = format.decimals;
2533
+ if (df === null || df === "auto" || df === "text") return null;
2534
+ switch (df) {
2535
+ case "number":
2536
+ return numberFormat(decimals);
2537
+ case "percentage":
2538
+ return percentFormat(decimals);
2539
+ case "currency":
2540
+ return currencyFormat(format.currency, decimals);
2541
+ case "date":
2542
+ return "yyyy-mm-dd";
2543
+ case "datetime":
2544
+ return "yyyy-mm-dd hh:mm:ss";
2545
+ default:
2546
+ return null;
2547
+ }
2548
+ }
2549
+ };
2550
+ function numberFormat(decimals) {
2551
+ if (decimals === null || decimals <= 0) return "#,##0";
2552
+ return "#,##0." + "0".repeat(decimals);
2553
+ }
2554
+ function percentFormat(decimals) {
2555
+ const d = decimals ?? 1;
2556
+ if (d <= 0) return "0%";
2557
+ return "0." + "0".repeat(d) + "%";
2558
+ }
2559
+ function currencyFormat(currency, decimals) {
2560
+ const symbol = currencySymbol(currency ?? "USD");
2561
+ const d = decimals ?? 2;
2562
+ const body = d <= 0 ? "#,##0" : "#,##0." + "0".repeat(d);
2563
+ return `"${symbol}"${body};-"${symbol}"${body}`;
2564
+ }
2565
+ function currencySymbol(iso) {
2566
+ switch (iso.toUpperCase()) {
2567
+ case "USD":
2568
+ return "$";
2569
+ case "EUR":
2570
+ return "\u20AC";
2571
+ case "GBP":
2572
+ return "\xA3";
2573
+ case "JPY":
2574
+ return "\xA5";
2575
+ case "CNY":
2576
+ return "\xA5";
2577
+ case "INR":
2578
+ return "\u20B9";
2579
+ case "AUD":
2580
+ return "A$";
2581
+ case "CAD":
2582
+ return "C$";
2583
+ case "CHF":
2584
+ return "CHF";
2585
+ case "KRW":
2586
+ return "\u20A9";
2587
+ default:
2588
+ return iso + " ";
2589
+ }
2590
+ }
2591
+
2592
+ // src/writer/styles-registry.ts
2593
+ var StylesRegistry = class {
2594
+ constructor() {
2595
+ this.xfIndex = /* @__PURE__ */ new Map([["__default__", 0]]);
2596
+ this.xfs = [{ fontId: 0, fillId: 0, borderId: 0, numFmtId: 0, align: null }];
2597
+ this.fontIndex = /* @__PURE__ */ new Map([["__default__", 0]]);
2598
+ this.fonts = [{ size: 11, name: "Calibri", color: null, bold: false, italic: false }];
2599
+ this.fillIndex = /* @__PURE__ */ new Map([
2600
+ ["__none__", 0],
2601
+ ["__gray125__", 1]
2602
+ ]);
2603
+ this.fills = [
2604
+ { type: "none" },
2605
+ { type: "gray125" }
2606
+ ];
2607
+ this.borderIndex = /* @__PURE__ */ new Map([["__default__", 0]]);
2608
+ this.borders = [{ top: null, right: null, bottom: null, left: null }];
2609
+ this.numFmtIndex = /* @__PURE__ */ new Map();
2610
+ this.numFmts = /* @__PURE__ */ new Map();
2611
+ this.nextNumFmtId = 164;
2612
+ }
2613
+ register(format) {
2614
+ if (format === null || format.isEmpty()) return 0;
2615
+ const key = format.key();
2616
+ const existing = this.xfIndex.get(key);
2617
+ if (existing !== void 0) return existing;
2618
+ const fontId = this.fontFor(format);
2619
+ const fillId = this.fillFor(format);
2620
+ const borderId = this.borderFor(format);
2621
+ const numFmtId = this.numFmtFor(format);
2622
+ this.xfs.push({ fontId, fillId, borderId, numFmtId, align: format.textAlign });
2623
+ const idx = this.xfs.length - 1;
2624
+ this.xfIndex.set(key, idx);
2625
+ return idx;
2626
+ }
2627
+ fontFor(f) {
2628
+ const rec = {
2629
+ size: f.fontSize ?? 11,
2630
+ name: "Calibri",
2631
+ color: f.color,
2632
+ bold: f.bold,
2633
+ italic: f.italic
2634
+ };
2635
+ const key = JSON.stringify(rec);
2636
+ const existing = this.fontIndex.get(key);
2637
+ if (existing !== void 0) return existing;
2638
+ this.fonts.push(rec);
2639
+ const idx = this.fonts.length - 1;
2640
+ this.fontIndex.set(key, idx);
2641
+ return idx;
2642
+ }
2643
+ fillFor(f) {
2644
+ if (f.backgroundColor === null) return 0;
2645
+ const key = f.backgroundColor.toUpperCase();
2646
+ const existing = this.fillIndex.get(key);
2647
+ if (existing !== void 0) return existing;
2648
+ this.fills.push({ type: "solid", fg: key });
2649
+ const idx = this.fills.length - 1;
2650
+ this.fillIndex.set(key, idx);
2651
+ return idx;
2652
+ }
2653
+ borderFor(f) {
2654
+ if (!f.borderTop && !f.borderRight && !f.borderBottom && !f.borderLeft) return 0;
2655
+ const rec = {
2656
+ top: f.borderTop,
2657
+ right: f.borderRight,
2658
+ bottom: f.borderBottom,
2659
+ left: f.borderLeft
2660
+ };
2661
+ const key = JSON.stringify(rec);
2662
+ const existing = this.borderIndex.get(key);
2663
+ if (existing !== void 0) return existing;
2664
+ this.borders.push(rec);
2665
+ const idx = this.borders.length - 1;
2666
+ this.borderIndex.set(key, idx);
2667
+ return idx;
2668
+ }
2669
+ numFmtFor(f) {
2670
+ const code = NumFmtBuilder.build(f);
2671
+ if (code === null) return 0;
2672
+ const existing = this.numFmtIndex.get(code);
2673
+ if (existing !== void 0) return existing;
2674
+ const id = this.nextNumFmtId++;
2675
+ this.numFmts.set(id, code);
2676
+ this.numFmtIndex.set(code, id);
2677
+ return id;
2678
+ }
2679
+ toXml() {
2680
+ let numFmtsXml = "";
2681
+ if (this.numFmts.size > 0) {
2682
+ let records = "";
2683
+ for (const [id, code] of this.numFmts) {
2684
+ records += `<numFmt numFmtId="${id}" formatCode="${xmlEscape(code)}"/>`;
2685
+ }
2686
+ numFmtsXml = `<numFmts count="${this.numFmts.size}">${records}</numFmts>`;
2687
+ }
2688
+ let fontsXml = `<fonts count="${this.fonts.length}">`;
2689
+ for (const f of this.fonts) {
2690
+ fontsXml += "<font>";
2691
+ fontsXml += `<sz val="${Number(f.size)}"/>`;
2692
+ if (f.bold) fontsXml += "<b/>";
2693
+ if (f.italic) fontsXml += "<i/>";
2694
+ if (f.color) fontsXml += `<color rgb="${hexToArgb(f.color)}"/>`;
2695
+ fontsXml += `<name val="${xmlEscape(f.name)}"/>`;
2696
+ fontsXml += "</font>";
2697
+ }
2698
+ fontsXml += "</fonts>";
2699
+ let fillsXml = `<fills count="${this.fills.length}">`;
2700
+ for (const fill of this.fills) {
2701
+ if (fill.type === "none") {
2702
+ fillsXml += '<fill><patternFill patternType="none"/></fill>';
2703
+ } else if (fill.type === "gray125") {
2704
+ fillsXml += '<fill><patternFill patternType="gray125"/></fill>';
2705
+ } else {
2706
+ fillsXml += `<fill><patternFill patternType="solid"><fgColor rgb="${hexToArgb(fill.fg)}"/></patternFill></fill>`;
2707
+ }
2708
+ }
2709
+ fillsXml += "</fills>";
2710
+ let bordersXml = `<borders count="${this.borders.length}">`;
2711
+ for (const b of this.borders) {
2712
+ bordersXml += "<border>";
2713
+ for (const side of ["left", "right", "top", "bottom"]) {
2714
+ const color = b[side];
2715
+ if (!color) {
2716
+ bordersXml += `<${side}/>`;
2717
+ } else {
2718
+ bordersXml += `<${side} style="thin"><color rgb="${hexToArgb(color)}"/></${side}>`;
2719
+ }
2720
+ }
2721
+ bordersXml += "<diagonal/></border>";
2722
+ }
2723
+ bordersXml += "</borders>";
2724
+ let cellXfsXml = `<cellXfs count="${this.xfs.length}">`;
2725
+ for (const xf of this.xfs) {
2726
+ let apply = "";
2727
+ if (xf.fontId > 0) apply += ' applyFont="1"';
2728
+ if (xf.fillId > 0) apply += ' applyFill="1"';
2729
+ if (xf.borderId > 0) apply += ' applyBorder="1"';
2730
+ if (xf.numFmtId > 0) apply += ' applyNumberFormat="1"';
2731
+ if (xf.align) apply += ' applyAlignment="1"';
2732
+ cellXfsXml += `<xf numFmtId="${xf.numFmtId}" fontId="${xf.fontId}" fillId="${xf.fillId}" borderId="${xf.borderId}" xfId="0"${apply}>`;
2733
+ if (xf.align) cellXfsXml += `<alignment horizontal="${xmlEscape(xf.align)}"/>`;
2734
+ cellXfsXml += "</xf>";
2735
+ }
2736
+ cellXfsXml += "</cellXfs>";
2737
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' + numFmtsXml + fontsXml + fillsXml + bordersXml + '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>' + cellXfsXml + '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles><dxfs count="0"/><tableStyles count="0" defaultTableStyle="TableStyleMedium2" defaultPivotStyle="PivotStyleLight16"/></styleSheet>';
2738
+ }
2739
+ };
2740
+ function hexToArgb(hex) {
2741
+ const h = hex.replace(/^#/, "");
2742
+ if (h.length === 6) return "FF" + h.toUpperCase();
2743
+ if (h.length === 8) return h.toUpperCase();
2744
+ return "FF000000";
2745
+ }
2746
+
2747
+ // src/writer/xlsx-writer.ts
2748
+ var encoder2 = new TextEncoder();
2749
+ var XlsxWriter = class {
2750
+ toBytes(workbook) {
2751
+ const styles = new StylesRegistry();
2752
+ const sheetXmls = workbook.sheets.map((sheet) => this.sheetXml(sheet, styles));
2753
+ const sheetsWithComments = [];
2754
+ workbook.sheets.forEach((sheet, i) => {
2755
+ if (sheet.hasComments()) sheetsWithComments.push(i);
2756
+ });
2757
+ const files = [];
2758
+ const add = (name, xml) => {
2759
+ files.push({ name, data: encoder2.encode(xml) });
2760
+ };
2761
+ add("[Content_Types].xml", this.contentTypesXml(workbook, sheetsWithComments));
2762
+ add("_rels/.rels", this.rootRelsXml());
2763
+ add("xl/workbook.xml", this.workbookXml(workbook));
2764
+ add("xl/_rels/workbook.xml.rels", this.workbookRelsXml(workbook));
2765
+ add("xl/styles.xml", styles.toXml());
2766
+ add("docProps/core.xml", this.coreXml(workbook));
2767
+ add("docProps/app.xml", this.appXml());
2768
+ workbook.sheets.forEach((sheet, i) => {
2769
+ const n = i + 1;
2770
+ add(`xl/worksheets/sheet${n}.xml`, sheetXmls[i]);
2771
+ if (sheet.hasComments()) {
2772
+ add(`xl/worksheets/_rels/sheet${n}.xml.rels`, this.sheetRelsXml(n));
2773
+ add(`xl/comments${n}.xml`, this.commentsXml(sheet));
2774
+ add(`xl/drawings/vmlDrawing${n}.vml`, this.vmlDrawingXml(sheet));
2775
+ }
2776
+ });
2777
+ return zipSync(files);
2778
+ }
2779
+ contentTypesXml(workbook, sheetsWithComments) {
2780
+ let overrides = '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>';
2781
+ workbook.sheets.forEach((_, i) => {
2782
+ const n = i + 1;
2783
+ overrides += `<Override PartName="/xl/worksheets/sheet${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
2784
+ });
2785
+ for (const i of sheetsWithComments) {
2786
+ const n = i + 1;
2787
+ overrides += `<Override PartName="/xl/comments${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>`;
2788
+ }
2789
+ let defaults = '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>';
2790
+ if (sheetsWithComments.length > 0) {
2791
+ defaults += '<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>';
2792
+ }
2793
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' + defaults + overrides + "</Types>";
2794
+ }
2795
+ rootRelsXml() {
2796
+ 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/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>';
2797
+ }
2798
+ workbookXml(workbook) {
2799
+ let sheets = "";
2800
+ workbook.sheets.forEach((sheet, i) => {
2801
+ const name = this.escape(sheet.name);
2802
+ const sheetId = i + 1;
2803
+ const rId = "rId" + (i + 1);
2804
+ sheets += `<sheet name="${name}" sheetId="${sheetId}" r:id="${rId}"/>`;
2805
+ });
2806
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>${sheets}</sheets></workbook>`;
2807
+ }
2808
+ workbookRelsXml(workbook) {
2809
+ let rels = "";
2810
+ workbook.sheets.forEach((_, i) => {
2811
+ const rId = "rId" + (i + 1);
2812
+ rels += `<Relationship Id="${rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`;
2813
+ });
2814
+ rels += `<Relationship Id="rId${workbook.sheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
2815
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
2816
+ }
2817
+ sheetRelsXml(sheetNum) {
2818
+ 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>`;
2819
+ }
2820
+ sheetXml(sheet, styles) {
2821
+ let sheetViewsXml = "";
2822
+ if (sheet.frozenRows > 0 || sheet.frozenCols > 0) {
2823
+ const topLeft = CellAddress.letter(sheet.frozenCols) + (sheet.frozenRows + 1);
2824
+ const pane = "<pane" + (sheet.frozenCols > 0 ? ` xSplit="${sheet.frozenCols}"` : "") + (sheet.frozenRows > 0 ? ` ySplit="${sheet.frozenRows}"` : "") + ` topLeftCell="${topLeft}" activePane="bottomRight" state="frozen"/>`;
2825
+ sheetViewsXml = `<sheetViews><sheetView workbookViewId="0">${pane}</sheetView></sheetViews>`;
2826
+ }
2827
+ let colsXml = "";
2828
+ if (sheet.columnWidths.size > 0) {
2829
+ colsXml = "<cols>";
2830
+ for (const [colIdx, px] of sheet.columnWidths) {
2831
+ const excelWidth = Math.max(1, (px - 5) / 7);
2832
+ const colNum = colIdx + 1;
2833
+ colsXml += `<col min="${colNum}" max="${colNum}" width="${excelWidth.toFixed(4)}" customWidth="1"/>`;
2834
+ }
2835
+ colsXml += "</cols>";
2836
+ }
2837
+ let rowsXml = "";
2838
+ for (const [rowIndex, row] of sheet.rows()) {
2839
+ let cellsXml = "";
2840
+ const sortedCols = [...row.keys()].sort();
2841
+ for (const col of sortedCols) {
2842
+ cellsXml += this.cellXml(row.get(col), styles);
2843
+ }
2844
+ rowsXml += `<row r="${rowIndex}">${cellsXml}</row>`;
2845
+ }
2846
+ let mergesXml = "";
2847
+ if (sheet.mergedRegions.length > 0) {
2848
+ mergesXml = `<mergeCells count="${sheet.mergedRegions.length}">`;
2849
+ for (const merge of sheet.mergedRegions) mergesXml += `<mergeCell ref="${merge.ref()}"/>`;
2850
+ mergesXml += "</mergeCells>";
2851
+ }
2852
+ const legacyDrawing = sheet.hasComments() ? '<legacyDrawing r:id="rId1"/>' : "";
2853
+ 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>";
2854
+ }
2855
+ cellXml(cell, styles) {
2856
+ const ref = cell.address;
2857
+ const type = cell.excelType();
2858
+ const styleIdx = styles.register(cell.format);
2859
+ const sAttr = styleIdx > 0 ? ` s="${styleIdx}"` : "";
2860
+ if (cell.formula !== null) {
2861
+ const f = this.escape(cell.formula.replace(/^=+/, ""));
2862
+ const cached = cell.cachedValue ?? cell.value;
2863
+ const v2 = cached !== null ? `<v>${this.escape(String(cached))}</v>` : "";
2864
+ return `<c r="${ref}"${sAttr}><f>${f}</f>${v2}</c>`;
2865
+ }
2866
+ if (type === "inlineStr") {
2867
+ const t = this.escape(String(cell.value));
2868
+ return `<c r="${ref}"${sAttr} t="inlineStr"><is><t xml:space="preserve">${t}</t></is></c>`;
2869
+ }
2870
+ if (type === "b") {
2871
+ const v2 = cell.value === true ? "1" : "0";
2872
+ return `<c r="${ref}"${sAttr} t="b"><v>${v2}</v></c>`;
2873
+ }
2874
+ if (cell.value === null) {
2875
+ return `<c r="${ref}"${sAttr}/>`;
2876
+ }
2877
+ let v;
2878
+ if (typeof cell.value === "number") {
2879
+ v = Number.isInteger(cell.value) ? String(cell.value) : formatFloat(cell.value);
2880
+ } else {
2881
+ v = String(cell.value);
2882
+ }
2883
+ if (v === "") v = "0";
2884
+ return `<c r="${ref}"${sAttr}><v>${v}</v></c>`;
2885
+ }
2886
+ commentsXml(sheet) {
2887
+ const authors = [];
2888
+ let commentList = "";
2889
+ for (const entry of sheet.comments()) {
2890
+ const author = entry.comment.author ?? "Author";
2891
+ if (!authors.includes(author)) authors.push(author);
2892
+ const authorIdx = authors.indexOf(author);
2893
+ const text = this.escape(entry.comment.text);
2894
+ commentList += `<comment ref="${entry.address}" authorId="${authorIdx}"><text><r><t xml:space="preserve">${text}</t></r></text></comment>`;
2895
+ }
2896
+ let authorsXml = "<authors>";
2897
+ for (const a of authors) authorsXml += `<author>${this.escape(a)}</author>`;
2898
+ authorsXml += "</authors>";
2899
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' + authorsXml + `<commentList>${commentList}</commentList></comments>`;
2900
+ }
2901
+ vmlDrawingXml(sheet) {
2902
+ let shapes = "";
2903
+ let shapeId = 1024;
2904
+ for (const entry of sheet.comments()) {
2905
+ const m = /^([A-Z]+)(\d+)$/.exec(entry.address);
2906
+ const col = CellAddress.index(m[1]);
2907
+ const row = parseInt(m[2], 10) - 1;
2908
+ shapeId++;
2909
+ shapes += `<v:shape id="_x0000_s${shapeId}" type="#_x0000_t202" style="position:absolute;margin-left:60pt;margin-top:5pt;width:108pt;height:60pt;z-index:1;visibility:hidden" fillcolor="#ffffe1" o:insetmode="auto"><v:fill color2="#ffffe1"/><v:shadow on="t" color="black" obscured="t"/><v:path o:connecttype="none"/><v:textbox><div style="text-align:left"></div></v:textbox><x:ClientData ObjectType="Note"><x:MoveWithCells/><x:SizeWithCells/><x:Anchor>${col + 1}, 15, ${row}, 10, ${col + 3}, 31, ${row + 4}, 18</x:Anchor><x:AutoFill>False</x:AutoFill><x:Row>${row}</x:Row><x:Column>${col}</x:Column></x:ClientData></v:shape>`;
2910
+ }
2911
+ return '<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout><v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>' + shapes + "</xml>";
2912
+ }
2913
+ coreXml(workbook) {
2914
+ const now = workbook.meta["created"] ?? gmDateZ();
2915
+ const creator = this.escape(String(workbook.meta["creator"] ?? "Holy Sheet"));
2916
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:creator>${creator}</dc:creator><cp:lastModifiedBy>${creator}</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`;
2917
+ }
2918
+ appXml() {
2919
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>Holy Sheet</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged><AppVersion>1.0</AppVersion></Properties>';
2920
+ }
2921
+ escape(s) {
2922
+ return xmlEscape(s);
2923
+ }
2924
+ };
2925
+ function gmDateZ() {
2926
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2927
+ }
2928
+
2929
+ // src/holy-sheet.schema.json
2930
+ var holy_sheet_schema_default = {
2931
+ $schema: "http://json-schema.org/draft-07/schema#",
2932
+ $id: "https://github.com/Particle-Academy/holy-sheet/skills/holy-sheet.schema.json",
2933
+ title: "Holy Sheet workbook schema",
2934
+ description: "Input format for HolySheet\\Agent::write(). Describes a multi-sheet xlsx workbook agentically. v0.2.0 supports scalar values, formulas (cached), and multiple sheets. Styles, formats, comments, merges land in subsequent minors.",
2935
+ type: "object",
2936
+ required: ["sheets"],
2937
+ additionalProperties: false,
2938
+ properties: {
2939
+ sheets: {
2940
+ type: "array",
2941
+ minItems: 1,
2942
+ description: "One or more sheets. The first sheet is shown when the file opens.",
2943
+ items: { $ref: "#/definitions/Sheet" }
2944
+ },
2945
+ meta: {
2946
+ type: "object",
2947
+ description: "Optional workbook metadata surfaced in Excel's File \u2192 Info pane.",
2948
+ properties: {
2949
+ creator: { type: "string" },
2950
+ created: { type: "string", description: "ISO-8601 datetime. Defaults to now()." }
2951
+ },
2952
+ additionalProperties: true
2953
+ }
2954
+ },
2955
+ definitions: {
2956
+ Sheet: {
2957
+ type: "object",
2958
+ required: ["name"],
2959
+ additionalProperties: false,
2960
+ oneOf: [
2961
+ { required: ["columns", "rows"], description: "Row-oriented (agent-ergonomic)." },
2962
+ { required: ["cells"], description: "Sparse A1-keyed map (fancy-sheets passthrough)." }
2963
+ ],
2964
+ properties: {
2965
+ name: {
2966
+ type: "string",
2967
+ minLength: 1,
2968
+ maxLength: 31,
2969
+ description: "Tab label in Excel. Excel limits to 31 chars and forbids /\\?*[]:"
2970
+ },
2971
+ columns: {
2972
+ type: "array",
2973
+ items: { $ref: "#/definitions/Column" },
2974
+ description: "When present, the first row is the header row. Column types are applied to every row in that column."
2975
+ },
2976
+ rows: {
2977
+ type: "array",
2978
+ items: {
2979
+ type: "array",
2980
+ description: "One row of cell values, in column order. Use null for missing values.",
2981
+ items: { $ref: "#/definitions/CellValue" }
2982
+ }
2983
+ },
2984
+ cells: {
2985
+ type: "object",
2986
+ description: "Sparse A1-keyed map. Use this shape when exporting from <Spreadsheet>.",
2987
+ additionalProperties: { $ref: "#/definitions/CellData" },
2988
+ patternProperties: {
2989
+ "^[A-Z]+[1-9][0-9]*$": { $ref: "#/definitions/CellData" }
2990
+ }
2991
+ },
2992
+ totals: {
2993
+ type: "object",
2994
+ description: "Symbolic aggregations appended below the data. Key = column header; value = aggregation. Lands in 0.7.",
2995
+ additionalProperties: {
2996
+ type: "string",
2997
+ enum: ["sum", "avg", "count", "min", "max"]
2998
+ }
2999
+ },
3000
+ theme: {
3001
+ type: "string",
3002
+ enum: ["default", "minimal", "plain", "business"],
3003
+ default: "default",
3004
+ description: "Pre-baked style preset. Lands in 0.3."
3005
+ },
3006
+ mergedRegions: {
3007
+ type: "array",
3008
+ items: {
3009
+ type: "object",
3010
+ required: ["start", "end"],
3011
+ properties: {
3012
+ start: { type: "string", description: "A1 ref of top-left." },
3013
+ end: { type: "string", description: "A1 ref of bottom-right." }
3014
+ }
3015
+ },
3016
+ description: "Cells to merge. Lands in 0.5."
3017
+ },
3018
+ columnWidths: {
3019
+ type: "object",
3020
+ description: "Override widths in pixels. Keys are 0-based column indexes. Lands in 0.5.",
3021
+ additionalProperties: { type: "number", minimum: 0 }
3022
+ },
3023
+ frozenRows: { type: "integer", minimum: 0, description: "Lands in 0.5." },
3024
+ frozenCols: { type: "integer", minimum: 0, description: "Lands in 0.5." }
3025
+ }
3026
+ },
3027
+ Column: {
3028
+ type: "object",
3029
+ required: ["header"],
3030
+ additionalProperties: false,
3031
+ properties: {
3032
+ header: { type: "string" },
3033
+ type: {
3034
+ type: "string",
3035
+ enum: ["auto", "string", "number", "integer", "boolean", "date", "datetime", "currency", "percent", "formula"],
3036
+ default: "auto",
3037
+ description: "Cell type applied to every value in this column. 'auto' infers per cell."
3038
+ },
3039
+ currency: {
3040
+ type: "string",
3041
+ description: "ISO-4217 code (USD, EUR, JPY, ...) when type=currency. Defaults to config('holy-sheet.locale')."
3042
+ },
3043
+ decimals: {
3044
+ type: "integer",
3045
+ minimum: 0,
3046
+ maximum: 10,
3047
+ description: "Display decimals for number/currency/percent. Default 2 for currency, 1 for percent, 0 for integer, none for number."
3048
+ },
3049
+ format: {
3050
+ type: "string",
3051
+ description: "Custom Excel number format string. Overrides type-derived format. Power-user escape hatch."
3052
+ },
3053
+ width: { type: "number", minimum: 0, description: "Column width in pixels. Same as columnWidths but per-column." }
3054
+ }
3055
+ },
3056
+ CellValue: {
3057
+ description: "A primitive value, null, or an object {value, formula, format, comment} for per-cell control.",
3058
+ oneOf: [
3059
+ { type: "string" },
3060
+ { type: "number" },
3061
+ { type: "boolean" },
3062
+ { type: "null" },
3063
+ { $ref: "#/definitions/CellData" }
3064
+ ]
3065
+ },
3066
+ CellData: {
3067
+ type: "object",
3068
+ additionalProperties: false,
3069
+ properties: {
3070
+ value: {
3071
+ description: "Raw cell value.",
3072
+ oneOf: [
3073
+ { type: "string" },
3074
+ { type: "number" },
3075
+ { type: "boolean" },
3076
+ { type: "null" }
3077
+ ]
3078
+ },
3079
+ formula: {
3080
+ type: "string",
3081
+ description: "Formula without leading '='. e.g. 'SUM(A1:A10)' or 'Sheet2!B5+1'."
3082
+ },
3083
+ computedValue: {
3084
+ description: "Cached value from a previous evaluation. When present, Excel shows it without recomputing on open.",
3085
+ oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }, { type: "null" }]
3086
+ },
3087
+ format: { $ref: "#/definitions/CellFormat" },
3088
+ comment: { $ref: "#/definitions/CellComment" }
3089
+ }
3090
+ },
3091
+ CellFormat: {
3092
+ type: "object",
3093
+ description: "Per-cell format. Lands in 0.3.",
3094
+ additionalProperties: false,
3095
+ properties: {
3096
+ bold: { type: "boolean" },
3097
+ italic: { type: "boolean" },
3098
+ textAlign: { type: "string", enum: ["left", "center", "right"] },
3099
+ displayFormat: {
3100
+ type: "string",
3101
+ enum: ["auto", "text", "number", "date", "datetime", "percentage", "currency"]
3102
+ },
3103
+ decimals: { type: "integer", minimum: 0, maximum: 10 },
3104
+ color: { type: "string", description: "Hex color (#RRGGBB) for text." },
3105
+ backgroundColor: { type: "string", description: "Hex color for cell fill." },
3106
+ fontSize: { type: "integer", minimum: 6, maximum: 72 },
3107
+ borderTop: { type: "string" },
3108
+ borderRight: { type: "string" },
3109
+ borderBottom: { type: "string" },
3110
+ borderLeft: { type: "string" }
3111
+ }
3112
+ },
3113
+ CellComment: {
3114
+ type: "object",
3115
+ description: "Lands in 0.6.",
3116
+ required: ["text"],
3117
+ additionalProperties: false,
3118
+ properties: {
3119
+ text: { type: "string" },
3120
+ author: { type: "string" },
3121
+ color: { type: "string", description: "Hex color for the corner triangle indicator." }
3122
+ }
3123
+ }
3124
+ }
3125
+ };
3126
+
3127
+ // src/agent.ts
3128
+ var VERSION = "1.0.0";
3129
+ function toU8(input) {
3130
+ return input instanceof Uint8Array ? input : new Uint8Array(input);
3131
+ }
3132
+ var Agent = {
3133
+ /** Validate a schema without writing. Empty array = valid. */
3134
+ validate(schema) {
3135
+ return new Validator().validate(schema);
3136
+ },
3137
+ /** xlsx bytes without touching disk. Universal. Throws SchemaException if invalid. */
3138
+ toBytes(schema) {
3139
+ new Validator().assert(schema);
3140
+ const workbook = new Normalizer().normalize(schema);
3141
+ return new XlsxWriter().toBytes(workbook);
3142
+ },
3143
+ /** Write a workbook to disk (Node only). Throws SchemaException if invalid. */
3144
+ async write(schema, path) {
3145
+ new Validator().assert(schema);
3146
+ const workbook = new Normalizer().normalize(schema);
3147
+ const bytes = new XlsxWriter().toBytes(workbook);
3148
+ const fs = await import('fs');
3149
+ fs.writeFileSync(path, bytes);
3150
+ return { path, bytes: bytes.length, sheets: workbook.sheets.length };
3151
+ },
3152
+ /** JSON Schema for LLM tool-use. */
3153
+ toolDefinition() {
3154
+ return holy_sheet_schema_default;
3155
+ },
3156
+ /** Round-trip xlsx bytes back to a schema. Universal. */
3157
+ read(input) {
3158
+ return new XlsxReader().describe(toU8(input));
3159
+ },
3160
+ /** Round-trip an xlsx file on disk back to a schema (Node only). */
3161
+ async describe(path) {
3162
+ const fs = await import('fs');
3163
+ if (!fs.existsSync(path)) return { error: "not_found", path };
3164
+ const bytes = fs.readFileSync(path);
3165
+ return new XlsxReader().describe(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength));
3166
+ },
3167
+ /** Validate + conservative repairs in one call. */
3168
+ validateAndRepair(schema) {
3169
+ return new Validator().validateAndRepair(schema);
3170
+ },
3171
+ /** Build a schema from a flat array of rows (+ optional headers). */
3172
+ fromArray(rows, headers = null, sheetName = "Sheet 1", options = {}) {
3173
+ return ArrayBuilder.build(rows, headers, sheetName, options);
3174
+ },
3175
+ /** Build a schema from CSV content (read files yourself in Node). */
3176
+ fromCsv(csv, options = {}) {
3177
+ return CsvBuilder.build(csv, options);
3178
+ },
3179
+ /** Evaluate every formula and report Excel-style errors. Empty array = clean. */
3180
+ lint(schema) {
3181
+ return new FormulaLinter().lint(schema);
3182
+ },
3183
+ version() {
3184
+ return VERSION;
3185
+ }
3186
+ };
3187
+
3188
+ // src/holy-sheet.ts
3189
+ var HolySheet = class {
3190
+ validate(schema) {
3191
+ return Agent.validate(schema);
3192
+ }
3193
+ toBytes(schema) {
3194
+ return Agent.toBytes(schema);
3195
+ }
3196
+ write(schema, path) {
3197
+ return Agent.write(schema, path);
3198
+ }
3199
+ toolDefinition() {
3200
+ return Agent.toolDefinition();
3201
+ }
3202
+ read(input) {
3203
+ return Agent.read(input);
3204
+ }
3205
+ describe(path) {
3206
+ return Agent.describe(path);
3207
+ }
3208
+ validateAndRepair(schema) {
3209
+ return Agent.validateAndRepair(schema);
3210
+ }
3211
+ fromArray(rows, headers = null, sheetName = "Sheet 1", options = {}) {
3212
+ return Agent.fromArray(rows, headers, sheetName, options);
3213
+ }
3214
+ fromCsv(csv, options = {}) {
3215
+ return Agent.fromCsv(csv, options);
3216
+ }
3217
+ lint(schema) {
3218
+ return Agent.lint(schema);
3219
+ }
3220
+ getVersion() {
3221
+ return VERSION;
3222
+ }
3223
+ static version() {
3224
+ return VERSION;
3225
+ }
3226
+ };
3227
+ HolySheet.VERSION = VERSION;
3228
+
3229
+ exports.Agent = Agent;
3230
+ exports.ArrayBuilder = ArrayBuilder;
3231
+ exports.CellAddress = CellAddress;
3232
+ exports.CsvBuilder = CsvBuilder;
3233
+ exports.FormulaLinter = FormulaLinter;
3234
+ exports.HolySheet = HolySheet;
3235
+ exports.Inference = Inference;
3236
+ exports.Normalizer = Normalizer;
3237
+ exports.Repairer = Repairer;
3238
+ exports.SchemaException = SchemaException;
3239
+ exports.Theme = Theme;
3240
+ exports.VERSION = VERSION;
3241
+ exports.Validator = Validator;
3242
+ exports.XlsxReader = XlsxReader;
3243
+ exports.XlsxWriter = XlsxWriter;
3244
+ exports.unzipSync = unzipSync;
3245
+ exports.zipSync = zipSync;
3246
+ //# sourceMappingURL=index.cjs.map
3247
+ //# sourceMappingURL=index.cjs.map