@particle-academy/holy-sheet 2.2.0 → 2.3.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.js CHANGED
@@ -287,10 +287,10 @@ var Cell = class {
287
287
 
288
288
  // src/workbook/cell-comment.ts
289
289
  var CellComment = class {
290
- constructor(text, author = null, color = null) {
290
+ constructor(text, author = null, color2 = null) {
291
291
  this.text = text;
292
292
  this.author = author;
293
- this.color = color;
293
+ this.color = color2;
294
294
  }
295
295
  };
296
296
 
@@ -1212,10 +1212,33 @@ function summarize(errors) {
1212
1212
  const e = errors[0];
1213
1213
  return `[holy-sheet] schema invalid at ${e.path}: expected ${e.expected}, got ${e.got}`;
1214
1214
  }
1215
- const first = errors[0];
1215
+ const first2 = errors[0];
1216
1216
  const rest = errors.length - 1;
1217
- return `[holy-sheet] schema invalid at ${first.path}: expected ${first.expected}, got ${first.got} (+${rest} more)`;
1217
+ return `[holy-sheet] schema invalid at ${first2.path}: expected ${first2.expected}, got ${first2.got} (+${rest} more)`;
1218
1218
  }
1219
+ var UnsupportedFormatException = class _UnsupportedFormatException extends Error {
1220
+ constructor(message, path = null, mimetype = null) {
1221
+ super(message);
1222
+ this.name = "UnsupportedFormatException";
1223
+ this.path = path;
1224
+ this.mimetype = mimetype;
1225
+ Object.setPrototypeOf(this, _UnsupportedFormatException.prototype);
1226
+ }
1227
+ static notAZip(path = null) {
1228
+ return new _UnsupportedFormatException(
1229
+ `[holy-sheet] cannot read ${path ?? "this file"}: it is not a zip archive, and xlsx and ods both are`,
1230
+ path
1231
+ );
1232
+ }
1233
+ static unknownPackage(path, mimetype) {
1234
+ const what = mimetype !== null && mimetype !== "" ? `its mimetype is ${mimetype}` : "it has neither xl/workbook.xml nor an OpenDocument mimetype";
1235
+ return new _UnsupportedFormatException(
1236
+ `[holy-sheet] cannot read ${path ?? "this file"}: ${what}. Supported: xlsx, ods`,
1237
+ path,
1238
+ mimetype !== "" ? mimetype : null
1239
+ );
1240
+ }
1241
+ };
1219
1242
 
1220
1243
  // src/schema/repairer.ts
1221
1244
  var VALID_THEMES = ["default", "minimal", "plain", "business"];
@@ -1444,7 +1467,8 @@ var Validator = class {
1444
1467
  }
1445
1468
  }
1446
1469
  if (sheet.cells != null) {
1447
- if (!isPlainObject(sheet.cells)) {
1470
+ const emptyList = Array.isArray(sheet.cells) && sheet.cells.length === 0;
1471
+ if (!isPlainObject(sheet.cells) && !emptyList) {
1448
1472
  errors.push(
1449
1473
  error(
1450
1474
  `${path}.cells`,
@@ -1589,13 +1613,13 @@ function zipSync(files) {
1589
1613
  edv.setUint16(20, 0, true);
1590
1614
  const out = new Uint8Array(localSize + centralSize + 22);
1591
1615
  let p = 0;
1592
- for (const part of localParts) {
1593
- out.set(part, p);
1594
- p += part.length;
1616
+ for (const part2 of localParts) {
1617
+ out.set(part2, p);
1618
+ p += part2.length;
1595
1619
  }
1596
- for (const part of centralParts) {
1597
- out.set(part, p);
1598
- p += part.length;
1620
+ for (const part2 of centralParts) {
1621
+ out.set(part2, p);
1622
+ p += part2.length;
1599
1623
  }
1600
1624
  out.set(eocd, p);
1601
1625
  return out;
@@ -1955,7 +1979,638 @@ function unzipSync(data) {
1955
1979
  return result;
1956
1980
  }
1957
1981
 
1982
+ // src/reader/ods/ns.ts
1983
+ var NS = {
1984
+ OFFICE: "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
1985
+ TABLE: "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
1986
+ TEXT: "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
1987
+ STYLE: "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
1988
+ FO: "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
1989
+ NUMBER: "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
1990
+ DC: "http://purl.org/dc/elements/1.1/",
1991
+ META: "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
1992
+ /** LibreOffice's extension namespace; carries `value-type="error"` for a formula error. */
1993
+ CALCEXT: "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0"
1994
+ };
1995
+ function child(node, ns, name) {
1996
+ return node?.children.find((c) => c.ns === ns && c.name === name);
1997
+ }
1998
+ function childrenIn(node, ns) {
1999
+ return node ? node.children.filter((c) => c.ns === ns) : [];
2000
+ }
2001
+ function attr(node, ns, name) {
2002
+ return node?.nsAttrs?.[`{${ns}}${name}`];
2003
+ }
2004
+ function ownText(node) {
2005
+ return node ? node.text : "";
2006
+ }
2007
+ function phpTrim(s) {
2008
+ return s.replace(/^[ \t\n\r\0\x0B]+|[ \t\n\r\0\x0B]+$/g, "");
2009
+ }
2010
+ function asciiLower(s) {
2011
+ return s.replace(/[A-Z]/g, (c) => c.toLowerCase());
2012
+ }
2013
+ function phpInt(s) {
2014
+ const m = /^[ \t\n\r\v\f]*([+-]?\d+)/.exec(s);
2015
+ return m ? parseInt(m[1], 10) : 0;
2016
+ }
2017
+ function phpIsNumeric(s) {
2018
+ return /^[ \t\n\r\v\f]*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[ \t\n\r\v\f]*$/.test(s);
2019
+ }
2020
+
2021
+ // src/reader/format-sniffer.ts
2022
+ var ODS_MIMETYPES = /* @__PURE__ */ new Set([
2023
+ "application/vnd.oasis.opendocument.spreadsheet",
2024
+ "application/vnd.oasis.opendocument.spreadsheet-template"
2025
+ ]);
2026
+ var decoder2 = new TextDecoder();
2027
+ var FormatSniffer = {
2028
+ sniff(bytes, path = null) {
2029
+ let files;
2030
+ try {
2031
+ files = unzipSync(bytes);
2032
+ } catch (e) {
2033
+ if (e instanceof Error && e.message.includes("unsupported zip method")) throw e;
2034
+ throw UnsupportedFormatException.notAZip(path);
2035
+ }
2036
+ const mimetype = files["mimetype"] ? phpTrim(decoder2.decode(files["mimetype"])) : null;
2037
+ if (mimetype !== null && ODS_MIMETYPES.has(mimetype)) return { format: "ods", files };
2038
+ if (files["xl/workbook.xml"]) return { format: "xlsx", files };
2039
+ throw UnsupportedFormatException.unknownPackage(path, mimetype);
2040
+ }
2041
+ };
2042
+
2043
+ // src/reader/ods/ods-formula.ts
2044
+ function odsFormulaToA1(formula) {
2045
+ let grammar = "of";
2046
+ const m = /^(of|oooc|msoxl):/i.exec(formula);
2047
+ if (m) {
2048
+ grammar = m[1].toLowerCase();
2049
+ formula = formula.slice(m[0].length);
2050
+ }
2051
+ if (formula.startsWith("=")) formula = formula.slice(1);
2052
+ return grammar === "msoxl" ? formula : translate(formula);
2053
+ }
2054
+ function translate(s) {
2055
+ let out = "";
2056
+ const length = s.length;
2057
+ let arrayDepth = 0;
2058
+ for (let i = 0; i < length; i++) {
2059
+ let c = s[i];
2060
+ if (c === '"') {
2061
+ let end = i + 1;
2062
+ while (end < length) {
2063
+ if (s[end] === '"') {
2064
+ if (end + 1 < length && s[end + 1] === '"') {
2065
+ end += 2;
2066
+ continue;
2067
+ }
2068
+ break;
2069
+ }
2070
+ end++;
2071
+ }
2072
+ out += s.slice(i, end + 1);
2073
+ i = end;
2074
+ continue;
2075
+ }
2076
+ if (c === "[") {
2077
+ let end = i + 1;
2078
+ let quoted = false;
2079
+ while (end < length && (quoted || s[end] !== "]")) {
2080
+ if (s[end] === "'") quoted = !quoted;
2081
+ end++;
2082
+ }
2083
+ out += reference(s.slice(i + 1, end));
2084
+ i = end;
2085
+ continue;
2086
+ }
2087
+ if (c === "{") {
2088
+ arrayDepth++;
2089
+ } else if (c === "}") {
2090
+ arrayDepth = Math.max(0, arrayDepth - 1);
2091
+ } else if (c === ";") {
2092
+ c = ",";
2093
+ } else if (c === "|" && arrayDepth > 0) {
2094
+ c = ";";
2095
+ }
2096
+ out += c;
2097
+ }
2098
+ return out;
2099
+ }
2100
+ function reference(ref) {
2101
+ const parts = [];
2102
+ let start = 0;
2103
+ let quoted = false;
2104
+ for (let i = 0; i < ref.length; i++) {
2105
+ if (ref[i] === "'") {
2106
+ quoted = !quoted;
2107
+ } else if (ref[i] === ":" && !quoted) {
2108
+ parts.push(ref.slice(start, i));
2109
+ start = i + 1;
2110
+ }
2111
+ }
2112
+ parts.push(ref.slice(start));
2113
+ const parsed = parts.length <= 2 ? parts.map(part) : [null];
2114
+ if (parsed.some((p) => p === null)) {
2115
+ return ref.includes("#REF!") ? "#REF!" : `[${ref}]`;
2116
+ }
2117
+ const [sheet, address] = parsed[0];
2118
+ if (parsed.length === 1) return sheetPrefix(sheet) + address;
2119
+ const [sheet2, address2] = parsed[1];
2120
+ if (sheet2 === null || sheet2 === sheet) return sheetPrefix(sheet) + address + ":" + address2;
2121
+ if (sheet === null) return address + ":" + sheetPrefix(sheet2) + address2;
2122
+ const both = needsQuotes(sheet) || needsQuotes(sheet2) ? "'" + sheet.replace(/'/g, "''") + ":" + sheet2.replace(/'/g, "''") + "'" : sheet + ":" + sheet2;
2123
+ return both + "!" + address + ":" + address2;
2124
+ }
2125
+ function part(p) {
2126
+ const length = p.length;
2127
+ let i = 0;
2128
+ if (i < length && p[i] === "$") i++;
2129
+ let sheet = null;
2130
+ if (i < length && p[i] === "'") {
2131
+ sheet = "";
2132
+ i++;
2133
+ for (; ; ) {
2134
+ if (i >= length) return null;
2135
+ if (p[i] === "'") {
2136
+ if (i + 1 < length && p[i + 1] === "'") {
2137
+ sheet += "'";
2138
+ i += 2;
2139
+ continue;
2140
+ }
2141
+ i++;
2142
+ break;
2143
+ }
2144
+ sheet += p[i++];
2145
+ }
2146
+ if (i >= length || p[i] !== ".") return null;
2147
+ i++;
2148
+ } else {
2149
+ const dot = p.indexOf(".", i);
2150
+ if (dot < 0) return null;
2151
+ if (dot > i) sheet = p.slice(i, dot);
2152
+ i = dot + 1;
2153
+ }
2154
+ const address = p.slice(i);
2155
+ if (!/^(\$?[A-Za-z]{1,3}\$?[0-9]+|\$?[A-Za-z]{1,3}|\$?[0-9]+)$/.test(address)) return null;
2156
+ return [sheet, address];
2157
+ }
2158
+ function sheetPrefix(sheet) {
2159
+ if (sheet === null) return "";
2160
+ return (needsQuotes(sheet) ? "'" + sheet.replace(/'/g, "''") + "'" : sheet) + "!";
2161
+ }
2162
+ function needsQuotes(sheet) {
2163
+ return !/^[A-Za-z_][A-Za-z0-9_.]*$/.test(sheet) || /^[A-Za-z]{1,3}[0-9]+$/.test(sheet) || /^[Rr]([0-9]+)?[Cc]([0-9]+)?$/.test(sheet);
2164
+ }
2165
+
2166
+ // src/reader/format/num-fmt-parser.ts
2167
+ var BUILTIN = {
2168
+ 0: "General",
2169
+ 1: "0",
2170
+ 2: "0.00",
2171
+ 3: "#,##0",
2172
+ 4: "#,##0.00",
2173
+ 9: "0%",
2174
+ 10: "0.00%",
2175
+ 11: "0.00E+00",
2176
+ 12: "# ?/?",
2177
+ 13: "# ??/??",
2178
+ 14: "mm-dd-yy",
2179
+ 15: "d-mmm-yy",
2180
+ 16: "d-mmm",
2181
+ 17: "mmm-yy",
2182
+ 18: "h:mm AM/PM",
2183
+ 19: "h:mm:ss AM/PM",
2184
+ 20: "h:mm",
2185
+ 21: "h:mm:ss",
2186
+ 22: "m/d/yy h:mm",
2187
+ 37: "#,##0 ;(#,##0)",
2188
+ 38: "#,##0 ;[Red](#,##0)",
2189
+ 39: "#,##0.00;(#,##0.00)",
2190
+ 40: "#,##0.00;[Red](#,##0.00)",
2191
+ 45: "mm:ss",
2192
+ 46: "[h]:mm:ss",
2193
+ 47: "mmss.0",
2194
+ 48: "##0.0E+0",
2195
+ 49: "@"
2196
+ };
2197
+ var SYMBOL_TO_ISO = {
2198
+ $: "USD",
2199
+ "\u20AC": "EUR",
2200
+ "\xA3": "GBP",
2201
+ "\xA5": "JPY",
2202
+ "\u20B9": "INR",
2203
+ "\u20A9": "KRW"
2204
+ };
2205
+ var NumFmtParser = {
2206
+ parse(formatCode) {
2207
+ if (formatCode === null) return null;
2208
+ const code = formatCode;
2209
+ if (code === "" || code === "General" || code === "@") {
2210
+ return { displayFormat: code === "@" ? "text" : "auto" };
2211
+ }
2212
+ if (looksLikeDate(code)) {
2213
+ return { displayFormat: hasTimeComponent(code) ? "datetime" : "date" };
2214
+ }
2215
+ if (code.includes("%")) {
2216
+ return { displayFormat: "percentage", decimals: decimalsAfter(code) };
2217
+ }
2218
+ const m = /^"([^"]+)"/u.exec(code);
2219
+ if (m) {
2220
+ const iso = SYMBOL_TO_ISO[m[1]];
2221
+ const result = { displayFormat: "currency", decimals: decimalsAfter(code) };
2222
+ if (iso !== void 0) result.currency = iso;
2223
+ return result;
2224
+ }
2225
+ if (/^#?,?#?#?0(\.0+)?$/.test(code.split(";")[0])) {
2226
+ return { displayFormat: "number", decimals: decimalsAfter(code) };
2227
+ }
2228
+ return null;
2229
+ },
2230
+ parseBuiltin(id) {
2231
+ const code = BUILTIN[id];
2232
+ if (code === void 0) return null;
2233
+ return NumFmtParser.parse(code);
2234
+ }
2235
+ };
2236
+ function looksLikeDate(code) {
2237
+ const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
2238
+ return /[ymd]/i.test(stripped);
2239
+ }
2240
+ function hasTimeComponent(code) {
2241
+ const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
2242
+ return /[hHsS]/.test(stripped) || stripped.toLowerCase().includes("mm:");
2243
+ }
2244
+ function decimalsAfter(code) {
2245
+ const section = code.split(";")[0];
2246
+ const m = /0\.(0+)/.exec(section);
2247
+ return m ? m[1].length : 0;
2248
+ }
2249
+
2250
+ // src/reader/ods/ods-styles.ts
2251
+ var OdsStyles = class {
2252
+ constructor(content, styles) {
2253
+ this.automatic = /* @__PURE__ */ new Map();
2254
+ this.common = /* @__PURE__ */ new Map();
2255
+ this.defaultStyle = null;
2256
+ this.dataStyles = /* @__PURE__ */ new Map();
2257
+ this.resolved = /* @__PURE__ */ new Map();
2258
+ if (content) this.index(child(content, NS.OFFICE, "automatic-styles"), this.automatic);
2259
+ if (styles) {
2260
+ this.index(child(styles, NS.OFFICE, "styles"), this.common);
2261
+ this.index(child(styles, NS.OFFICE, "automatic-styles"), this.common);
2262
+ }
2263
+ this.defaultFontSize = this.properties("Default").fontSize;
2264
+ }
2265
+ /**
2266
+ * The format of one cell.
2267
+ *
2268
+ * @param styleName the cell's effective style (cell, row default, column default, or "Default")
2269
+ * @param valueType office:value-type
2270
+ * @param currency office:currency, the ISO code a currency value carries
2271
+ * @param dateHasTime whether an office:date-value carries a time of day
2272
+ */
2273
+ cellFormat(styleName, valueType, currency, dateHasTime) {
2274
+ const p = this.properties(styleName);
2275
+ let data = p.data;
2276
+ const display = data?.displayFormat ?? null;
2277
+ switch (valueType) {
2278
+ case "date":
2279
+ data = { displayFormat: display === "date" || display === "datetime" ? display : dateHasTime ? "datetime" : "date" };
2280
+ break;
2281
+ case "time":
2282
+ data = { displayFormat: "datetime" };
2283
+ break;
2284
+ case "percentage":
2285
+ if (display !== "percentage") {
2286
+ data = { displayFormat: "percentage", ...data?.decimals !== void 0 ? { decimals: data.decimals } : {} };
2287
+ }
2288
+ break;
2289
+ case "currency":
2290
+ if (display !== "currency") {
2291
+ data = { displayFormat: "currency", ...data?.decimals !== void 0 ? { decimals: data.decimals } : {} };
2292
+ } else {
2293
+ data = { ...data };
2294
+ }
2295
+ if (currency !== null && currency !== "") data.currency = currency;
2296
+ break;
2297
+ }
2298
+ const format = new CellFormat({
2299
+ bold: p.bold,
2300
+ italic: p.italic,
2301
+ textAlign: p.textAlign,
2302
+ displayFormat: data?.displayFormat ?? null,
2303
+ decimals: data?.decimals ?? null,
2304
+ color: p.color,
2305
+ backgroundColor: p.backgroundColor,
2306
+ fontSize: p.fontSize !== null && p.fontSize !== this.defaultFontSize ? p.fontSize : null,
2307
+ borderTop: p.borderTop,
2308
+ borderRight: p.borderRight,
2309
+ borderBottom: p.borderBottom,
2310
+ borderLeft: p.borderLeft,
2311
+ currency: data?.currency ?? null
2312
+ });
2313
+ return format.isEmpty() ? null : format;
2314
+ }
2315
+ index(container, into) {
2316
+ if (!container) return;
2317
+ for (const el2 of childrenIn(container, NS.STYLE)) {
2318
+ const family = attr(el2, NS.STYLE, "family") ?? "";
2319
+ if (el2.name === "default-style" && family === "table-cell") {
2320
+ this.defaultStyle ?? (this.defaultStyle = el2);
2321
+ } else if (el2.name === "style" && family === "table-cell") {
2322
+ const name = attr(el2, NS.STYLE, "name") ?? "";
2323
+ if (!into.has(name)) into.set(name, el2);
2324
+ }
2325
+ }
2326
+ for (const el2 of childrenIn(container, NS.NUMBER)) {
2327
+ const name = attr(el2, NS.STYLE, "name") ?? "";
2328
+ if (!this.dataStyles.has(name)) this.dataStyles.set(name, el2);
2329
+ }
2330
+ }
2331
+ /** The style and its ancestors, nearest first, ending with the default style. */
2332
+ chain(name) {
2333
+ const chain = [];
2334
+ const seen = /* @__PURE__ */ new Set();
2335
+ let el2 = this.automatic.get(name) ?? this.common.get(name);
2336
+ while (el2 !== void 0 && !seen.has(name)) {
2337
+ seen.add(name);
2338
+ chain.push(el2);
2339
+ name = attr(el2, NS.STYLE, "parent-style-name") ?? "";
2340
+ el2 = name === "" ? void 0 : this.common.get(name) ?? this.automatic.get(name);
2341
+ }
2342
+ if (this.defaultStyle) chain.push(this.defaultStyle);
2343
+ return chain;
2344
+ }
2345
+ properties(name) {
2346
+ const cached = this.resolved.get(name);
2347
+ if (cached) return cached;
2348
+ const chain = this.chain(name);
2349
+ const weight = first(chain, "text-properties", NS.FO, "font-weight");
2350
+ const style = first(chain, "text-properties", NS.FO, "font-style");
2351
+ const size = first(chain, "text-properties", NS.FO, "font-size");
2352
+ let dataStyle = null;
2353
+ for (const el2 of chain) {
2354
+ const candidate = attr(el2, NS.STYLE, "data-style-name") ?? "";
2355
+ if (candidate !== "") {
2356
+ dataStyle = candidate;
2357
+ break;
2358
+ }
2359
+ }
2360
+ const sizeMatch = size !== null ? /^([0-9]*\.?[0-9]+)pt$/.exec(size) : null;
2361
+ const resolved = {
2362
+ bold: weight !== null && (weight === "bold" || phpIsNumeric(weight) && phpInt(weight) >= 600),
2363
+ italic: style === "italic" || style === "oblique",
2364
+ textAlign: textAlign(chain),
2365
+ color: color(chain),
2366
+ backgroundColor: background(chain),
2367
+ fontSize: sizeMatch ? Math.trunc(parseFloat(sizeMatch[1])) : null,
2368
+ borderTop: border(chain, "top"),
2369
+ borderRight: border(chain, "right"),
2370
+ borderBottom: border(chain, "bottom"),
2371
+ borderLeft: border(chain, "left"),
2372
+ data: this.dataFormat(dataStyle, 0)
2373
+ };
2374
+ this.resolved.set(name, resolved);
2375
+ return resolved;
2376
+ }
2377
+ /** A data style -> displayFormat, decimals, currency. */
2378
+ dataFormat(name, depth) {
2379
+ if (name === null || depth > 4) return null;
2380
+ const el2 = this.dataStyles.get(name);
2381
+ if (!el2) return null;
2382
+ for (const map of childrenIn(el2, NS.STYLE)) {
2383
+ if (map.name !== "map") continue;
2384
+ const condition = (attr(map, NS.STYLE, "condition") ?? "").replace(/[ \t\n\r\f\v]+/g, "");
2385
+ if (condition === "value()>=0" || condition === "value()>0") {
2386
+ const mapped = this.dataFormat(attr(map, NS.STYLE, "apply-style-name") ?? "", depth + 1);
2387
+ if (mapped !== null) return mapped;
2388
+ }
2389
+ }
2390
+ const parts = childrenIn(el2, NS.NUMBER);
2391
+ const has = (local) => parts.some((p) => p.name === local);
2392
+ const number = parts.find((p) => p.name === "number");
2393
+ let decimals = null;
2394
+ if (number) {
2395
+ const places = attr(number, NS.NUMBER, "decimal-places") ?? attr(number, NS.NUMBER, "min-decimal-places");
2396
+ decimals = places !== void 0 ? phpInt(places) : null;
2397
+ }
2398
+ switch (el2.name) {
2399
+ case "number-style": {
2400
+ if (has("scientific-number") || has("fraction")) return null;
2401
+ for (const text of parts.filter((p) => p.name === "text")) {
2402
+ const iso = currencyCode(ownText(text).replace(/[-()"\u00A0]/g, ""));
2403
+ if (iso !== null) return { displayFormat: "currency", decimals: decimals ?? 0, currency: iso };
2404
+ }
2405
+ if (!number) return null;
2406
+ return decimals === null ? { displayFormat: "auto" } : { displayFormat: "number", decimals };
2407
+ }
2408
+ case "percentage-style":
2409
+ return { displayFormat: "percentage", decimals: decimals ?? 0 };
2410
+ case "currency-style": {
2411
+ const out = { displayFormat: "currency", decimals: decimals ?? 0 };
2412
+ const symbol = parts.find((p) => p.name === "currency-symbol");
2413
+ const iso = symbol ? currencyCode(ownText(symbol)) : null;
2414
+ if (iso !== null) out.currency = iso;
2415
+ return out;
2416
+ }
2417
+ case "date-style":
2418
+ return ["hours", "minutes", "seconds", "am-pm"].some(has) ? { displayFormat: "datetime" } : { displayFormat: "date" };
2419
+ case "time-style":
2420
+ return { displayFormat: "datetime" };
2421
+ case "text-style":
2422
+ return { displayFormat: "text" };
2423
+ default:
2424
+ return null;
2425
+ }
2426
+ }
2427
+ };
2428
+ function first(chain, properties, ns, name) {
2429
+ for (const el2 of chain) {
2430
+ const props = child(el2, NS.STYLE, properties);
2431
+ const value = attr(props, ns, name);
2432
+ if (value !== void 0) return phpTrim(value);
2433
+ }
2434
+ return null;
2435
+ }
2436
+ function color(chain) {
2437
+ for (const el2 of chain) {
2438
+ const props = child(el2, NS.STYLE, "text-properties");
2439
+ if (!props) continue;
2440
+ if (attr(props, NS.STYLE, "use-window-font-color") === "true") return null;
2441
+ const value = attr(props, NS.FO, "color");
2442
+ if (value !== void 0) return hex(value);
2443
+ }
2444
+ return null;
2445
+ }
2446
+ function background(chain) {
2447
+ const value = first(chain, "table-cell-properties", NS.FO, "background-color");
2448
+ return value === null || value === "transparent" ? null : hex(value);
2449
+ }
2450
+ function border(chain, side) {
2451
+ for (const el2 of chain) {
2452
+ const props = child(el2, NS.STYLE, "table-cell-properties");
2453
+ if (!props) continue;
2454
+ const raw = attr(props, NS.FO, `border-${side}`) ?? attr(props, NS.FO, "border");
2455
+ if (raw === void 0) continue;
2456
+ const value = asciiLower(phpTrim(raw));
2457
+ if (value === "" || /\b(none|hidden)\b/.test(value)) return null;
2458
+ const m = /#([0-9a-f]{6})\b/.exec(value);
2459
+ return m ? "#" + m[1].toUpperCase() : "#000000";
2460
+ }
2461
+ return null;
2462
+ }
2463
+ function textAlign(chain) {
2464
+ if (first(chain, "table-cell-properties", NS.STYLE, "text-align-source") === "value-type") return null;
2465
+ const align = first(chain, "paragraph-properties", NS.FO, "text-align");
2466
+ if (align === null || align === "") return null;
2467
+ if (align === "start") return "left";
2468
+ if (align === "end") return "right";
2469
+ return align;
2470
+ }
2471
+ function hex(value) {
2472
+ const m = /^#([0-9a-fA-F]{6})$/.exec(phpTrim(value));
2473
+ return m ? "#" + m[1].toUpperCase() : null;
2474
+ }
2475
+ function currencyCode(symbol) {
2476
+ const s = phpTrim(symbol);
2477
+ if (s === "") return null;
2478
+ if (/^[A-Z]{3}$/.test(s)) return s;
2479
+ return SYMBOL_TO_ISO[s] ?? null;
2480
+ }
2481
+
2482
+ // src/reader/ods/ods-text.ts
2483
+ var SKIPPED = /* @__PURE__ */ new Set(["annotation", "note"]);
2484
+ function paragraphs(node) {
2485
+ const lines = [];
2486
+ for (const c of node.children) {
2487
+ if (c.ns === NS.TEXT && (c.name === "p" || c.name === "h")) lines.push(paragraph(c));
2488
+ }
2489
+ return lines.length === 0 ? null : lines.join("\n");
2490
+ }
2491
+ function paragraph(p) {
2492
+ const state = { out: "", ignoreSpace: true };
2493
+ walk(p, state);
2494
+ return state.out;
2495
+ }
2496
+ function walk(node, state) {
2497
+ for (const item of node.content ?? []) {
2498
+ if (typeof item === "string") {
2499
+ appendCharacters(state, item);
2500
+ continue;
2501
+ }
2502
+ if (SKIPPED.has(item.name)) continue;
2503
+ if (item.name === "s") {
2504
+ const c = item.attrs["c"];
2505
+ const count = c !== void 0 && /^\d+$/.test(c) ? parseInt(c, 10) : 1;
2506
+ state.out += " ".repeat(Math.max(1, count));
2507
+ state.ignoreSpace = false;
2508
+ } else if (item.name === "tab") {
2509
+ state.out += " ";
2510
+ state.ignoreSpace = false;
2511
+ } else if (item.name === "line-break") {
2512
+ state.out += "\n";
2513
+ state.ignoreSpace = false;
2514
+ }
2515
+ walk(item, state);
2516
+ }
2517
+ }
2518
+ function appendCharacters(state, chars) {
2519
+ for (let i = 0; i < chars.length; i++) {
2520
+ const c = chars[i];
2521
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
2522
+ if (!state.ignoreSpace) {
2523
+ state.out += " ";
2524
+ state.ignoreSpace = true;
2525
+ }
2526
+ continue;
2527
+ }
2528
+ state.out += c;
2529
+ state.ignoreSpace = false;
2530
+ }
2531
+ }
2532
+
2533
+ // src/reader/format/date-inverter.ts
2534
+ var DateInverter = {
2535
+ toIso(serial2, includeTime = false) {
2536
+ const epoch = Date.UTC(1899, 11, 30, 0, 0, 0);
2537
+ const ms = epoch + Math.round(serial2 * 86400) * 1e3;
2538
+ const d = new Date(ms);
2539
+ const pad = (n) => String(n).padStart(2, "0");
2540
+ const date = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
2541
+ if (!includeTime) return date;
2542
+ return `${date}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
2543
+ }
2544
+ };
2545
+
2546
+ // src/reader/workbook-schema.ts
2547
+ var WorkbookSchema = {
2548
+ fromWorkbook(workbook) {
2549
+ const schema = { sheets: workbook.sheets.map(sheetToSchema) };
2550
+ if (Object.keys(workbook.meta).length > 0) schema["meta"] = workbook.meta;
2551
+ return schema;
2552
+ }
2553
+ };
2554
+ function sheetToSchema(sheet) {
2555
+ const cells = {};
2556
+ for (const [address, cell] of sheet.cells) {
2557
+ const cellSchema = cellToSchema(cell);
2558
+ if (cellSchema === null) continue;
2559
+ cells[address] = cellSchema;
2560
+ }
2561
+ const out = { name: sheet.name, cells };
2562
+ if (sheet.mergedRegions.length > 0) {
2563
+ out["mergedRegions"] = sheet.mergedRegions.map((m) => ({ start: m.start, end: m.end }));
2564
+ }
2565
+ if (sheet.columnWidths.size > 0) {
2566
+ const widths = {};
2567
+ for (const [k, v] of sheet.columnWidths) widths[k] = v;
2568
+ out["columnWidths"] = widths;
2569
+ }
2570
+ if (sheet.frozenRows > 0) out["frozenRows"] = sheet.frozenRows;
2571
+ if (sheet.frozenCols > 0) out["frozenCols"] = sheet.frozenCols;
2572
+ return out;
2573
+ }
2574
+ function cellToSchema(cell) {
2575
+ let value = cell.value;
2576
+ const format = cell.format;
2577
+ if (format !== null && (format.displayFormat === "date" || format.displayFormat === "datetime") && isNumericString(value)) {
2578
+ value = DateInverter.toIso(Number(value), format.displayFormat === "datetime");
2579
+ }
2580
+ const out = { value };
2581
+ if (cell.formula !== null) out["formula"] = cell.formula;
2582
+ if (cell.cachedValue !== null) out["computedValue"] = cell.cachedValue;
2583
+ if (format !== null && !format.isEmpty()) out["format"] = formatToObject(format);
2584
+ if (cell.comment !== null) out["comment"] = commentToObject(cell.comment);
2585
+ if (Object.keys(out).length === 1 && out["value"] === null) return null;
2586
+ return out;
2587
+ }
2588
+ function formatToObject(f) {
2589
+ const out = {};
2590
+ if (f.bold) out["bold"] = true;
2591
+ if (f.italic) out["italic"] = true;
2592
+ if (f.textAlign !== null) out["textAlign"] = f.textAlign;
2593
+ if (f.displayFormat !== null) out["displayFormat"] = f.displayFormat;
2594
+ if (f.decimals !== null) out["decimals"] = f.decimals;
2595
+ if (f.color !== null) out["color"] = f.color;
2596
+ if (f.backgroundColor !== null) out["backgroundColor"] = f.backgroundColor;
2597
+ if (f.fontSize !== null) out["fontSize"] = f.fontSize;
2598
+ if (f.borderTop !== null) out["borderTop"] = f.borderTop;
2599
+ if (f.borderRight !== null) out["borderRight"] = f.borderRight;
2600
+ if (f.borderBottom !== null) out["borderBottom"] = f.borderBottom;
2601
+ if (f.borderLeft !== null) out["borderLeft"] = f.borderLeft;
2602
+ if (f.currency !== null) out["currency"] = f.currency;
2603
+ return out;
2604
+ }
2605
+ function commentToObject(c) {
2606
+ const out = { text: c.text };
2607
+ if (c.author !== null) out["author"] = c.author;
2608
+ if (c.color !== null) out["color"] = c.color;
2609
+ return out;
2610
+ }
2611
+
1958
2612
  // src/reader/xml.ts
2613
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
1959
2614
  function localName(name) {
1960
2615
  const idx = name.indexOf(":");
1961
2616
  return idx >= 0 ? name.slice(idx + 1) : name;
@@ -2004,19 +2659,28 @@ function parseTag(inner) {
2004
2659
  while (i < trimmed.length && !/\s/.test(trimmed[i])) i++;
2005
2660
  const name = trimmed.slice(0, i);
2006
2661
  const attrs = {};
2662
+ const raw = [];
2007
2663
  const rest = trimmed.slice(i);
2008
2664
  for (const m of rest.matchAll(ATTR_RE)) {
2009
- const key = localName(m[1]);
2010
- const value = unescapeXml(m[3] ?? m[4] ?? "");
2011
- attrs[key] = value;
2665
+ const rawValue = m[3] ?? m[4] ?? "";
2666
+ attrs[localName(m[1])] = unescapeXml(rawValue);
2667
+ raw.push([m[1], rawValue]);
2012
2668
  }
2013
- return { name, attrs };
2669
+ return { name, attrs, raw };
2014
2670
  }
2015
- function parseXml(src) {
2671
+ function parseXml(src, options = {}) {
2672
+ const namespaces = options.namespaces === true;
2016
2673
  let i = 0;
2017
2674
  const n = src.length;
2018
2675
  const root = { name: "#root", attrs: {}, children: [], text: "" };
2676
+ if (namespaces) root.content = [];
2019
2677
  const stack = [root];
2678
+ const scopes = [{ xml: XML_NS }];
2679
+ const appendText = (text) => {
2680
+ const top = stack[stack.length - 1];
2681
+ top.text += text;
2682
+ if (namespaces && text !== "") top.content.push(text);
2683
+ };
2020
2684
  while (i < n) {
2021
2685
  if (src[i] === "<") {
2022
2686
  if (src.startsWith("<!--", i)) {
@@ -2026,7 +2690,7 @@ function parseXml(src) {
2026
2690
  }
2027
2691
  if (src.startsWith("<![CDATA[", i)) {
2028
2692
  const end2 = src.indexOf("]]>", i);
2029
- stack[stack.length - 1].text += src.slice(i + 9, end2 < 0 ? n : end2);
2693
+ appendText(src.slice(i + 9, end2 < 0 ? n : end2));
2030
2694
  i = end2 < 0 ? n : end2 + 3;
2031
2695
  continue;
2032
2696
  }
@@ -2042,27 +2706,64 @@ function parseXml(src) {
2042
2706
  }
2043
2707
  if (src[i + 1] === "/") {
2044
2708
  const end2 = src.indexOf(">", i);
2045
- if (stack.length > 1) stack.pop();
2709
+ if (stack.length > 1) {
2710
+ stack.pop();
2711
+ if (namespaces) scopes.pop();
2712
+ }
2046
2713
  i = end2 < 0 ? n : end2 + 1;
2047
2714
  continue;
2048
2715
  }
2049
2716
  const end = findTagEnd(src, i + 1);
2050
2717
  const tagContent = src.slice(i + 1, end);
2051
2718
  const selfClosing = tagContent.endsWith("/");
2052
- const { name, attrs } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
2719
+ const { name, attrs, raw } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
2053
2720
  const node = { name: localName(name), attrs, children: [], text: "" };
2054
- stack[stack.length - 1].children.push(node);
2055
- if (!selfClosing) stack.push(node);
2721
+ const parent = stack[stack.length - 1];
2722
+ parent.children.push(node);
2723
+ let scope;
2724
+ if (namespaces) {
2725
+ scope = resolveNamespaces(node, name, raw, scopes[scopes.length - 1]);
2726
+ parent.content.push(node);
2727
+ }
2728
+ if (!selfClosing) {
2729
+ stack.push(node);
2730
+ if (scope) scopes.push(scope);
2731
+ }
2056
2732
  i = end + 1;
2057
2733
  } else {
2058
2734
  const next = src.indexOf("<", i);
2059
2735
  const stop = next < 0 ? n : next;
2060
- stack[stack.length - 1].text += unescapeXml(src.slice(i, stop));
2736
+ appendText(unescapeXml(src.slice(i, stop)));
2061
2737
  i = stop;
2062
2738
  }
2063
2739
  }
2064
2740
  return root.children[0] ?? null;
2065
2741
  }
2742
+ function resolveNamespaces(node, qname, raw, parentScope) {
2743
+ let scope = parentScope;
2744
+ for (const [key, value] of raw) {
2745
+ if (key === "xmlns" || key.startsWith("xmlns:")) {
2746
+ if (scope === parentScope) scope = { ...parentScope };
2747
+ scope[key === "xmlns" ? "" : key.slice(6)] = unescapeXml(value);
2748
+ }
2749
+ }
2750
+ const colon = qname.indexOf(":");
2751
+ node.ns = scope[colon >= 0 ? qname.slice(0, colon) : ""] ?? "";
2752
+ node.content = [];
2753
+ const nsAttrs = {};
2754
+ for (const [key, value] of raw) {
2755
+ if (key === "xmlns" || key.startsWith("xmlns:")) continue;
2756
+ const idx = key.indexOf(":");
2757
+ const normalised = unescapeXml(value.replace(/[\t\n\r]/g, " "));
2758
+ if (idx < 0) {
2759
+ nsAttrs[key] = normalised;
2760
+ } else {
2761
+ nsAttrs[`{${scope[key.slice(0, idx)] ?? ""}}${key.slice(idx + 1)}`] = normalised;
2762
+ }
2763
+ }
2764
+ node.nsAttrs = nsAttrs;
2765
+ return scope;
2766
+ }
2066
2767
  function el(node, name) {
2067
2768
  return node?.children.find((c) => c.name === name);
2068
2769
  }
@@ -2073,6 +2774,259 @@ function at(node, name) {
2073
2774
  return node?.attrs[name];
2074
2775
  }
2075
2776
 
2777
+ // src/reader/ods-reader.ts
2778
+ var MAX_ROWS = 1048576;
2779
+ var MAX_COLUMNS = 16384;
2780
+ var UNIX_EPOCH_SERIAL = 25569;
2781
+ var decoder3 = new TextDecoder();
2782
+ var OdsReader = class {
2783
+ describe(bytes) {
2784
+ return WorkbookSchema.fromWorkbook(this.readWorkbook(bytes));
2785
+ }
2786
+ readWorkbook(bytes) {
2787
+ return this.readFiles(unzipSync(bytes));
2788
+ }
2789
+ /** Read an already-unzipped package (name -> bytes). */
2790
+ readFiles(files) {
2791
+ const getText = (name) => files[name] ? decoder3.decode(files[name]) : null;
2792
+ const content = getText("content.xml");
2793
+ if (content === null) throw new Error("[holy-sheet] missing content.xml");
2794
+ const contentDoc = parseXml(content, { namespaces: true });
2795
+ if (!contentDoc) throw new Error("[holy-sheet] failed to parse content.xml");
2796
+ const stylesText = getText("styles.xml");
2797
+ const stylesDoc = stylesText === null ? null : parseXml(stylesText, { namespaces: true });
2798
+ const styles = new OdsStyles(contentDoc, stylesDoc);
2799
+ const sheets = [];
2800
+ const spreadsheet = child(child(contentDoc, NS.OFFICE, "body"), NS.OFFICE, "spreadsheet");
2801
+ for (const table of spreadsheet?.children ?? []) {
2802
+ if (table.ns === NS.TABLE && table.name === "table") sheets.push(this.readSheet(table, styles));
2803
+ }
2804
+ const meta = getText("meta.xml");
2805
+ return new Workbook(sheets, meta === null ? {} : readMeta(meta));
2806
+ }
2807
+ readSheet(table, styles) {
2808
+ const columns = [];
2809
+ collectColumns(table, columns, { next: 0 });
2810
+ const state = { row: 0, cells: /* @__PURE__ */ new Map(), merges: [] };
2811
+ this.walkRows(table, styles, columns, state);
2812
+ return new Sheet(attr(table, NS.TABLE, "name") ?? "", state.cells, state.merges);
2813
+ }
2814
+ walkRows(container, styles, columns, state) {
2815
+ for (const el2 of container.children) {
2816
+ if (state.row >= MAX_ROWS) return;
2817
+ if (el2.ns !== NS.TABLE) continue;
2818
+ if (el2.name === "table-row") {
2819
+ this.readRow(el2, styles, columns, state);
2820
+ } else if (el2.name === "table-header-rows" || el2.name === "table-rows" || el2.name === "table-row-group") {
2821
+ this.walkRows(el2, styles, columns, state);
2822
+ }
2823
+ }
2824
+ }
2825
+ readRow(row, styles, columns, state) {
2826
+ const repeat = Math.max(1, phpInt(attr(row, NS.TABLE, "number-rows-repeated") ?? "1"));
2827
+ const rowStyle = attr(row, NS.TABLE, "default-cell-style-name") ?? "";
2828
+ const entries = [];
2829
+ let column = 0;
2830
+ for (const cell of row.children) {
2831
+ if (cell.ns !== NS.TABLE || cell.name !== "table-cell" && cell.name !== "covered-table-cell") continue;
2832
+ const span = cell.name === "table-cell" ? [
2833
+ Math.max(1, phpInt(attr(cell, NS.TABLE, "number-columns-spanned") ?? "1")),
2834
+ Math.max(1, phpInt(attr(cell, NS.TABLE, "number-rows-spanned") ?? "1"))
2835
+ ] : [1, 1];
2836
+ const read = hasContent(cell) ? readCell(cell) : null;
2837
+ const count = Math.max(1, phpInt(attr(cell, NS.TABLE, "number-columns-repeated") ?? "1"));
2838
+ if (read !== null || span[0] !== 1 || span[1] !== 1) {
2839
+ const style = attr(cell, NS.TABLE, "style-name") ?? "";
2840
+ for (let i = 0; i < count && column + i < MAX_COLUMNS; i++) {
2841
+ entries.push([column + i, read, span, style]);
2842
+ }
2843
+ }
2844
+ column += count;
2845
+ if (column >= MAX_COLUMNS) break;
2846
+ }
2847
+ if (entries.length === 0) {
2848
+ state.row += repeat;
2849
+ return;
2850
+ }
2851
+ for (let r = 0; r < repeat && state.row < MAX_ROWS; r++) {
2852
+ const rowNumber = state.row + 1;
2853
+ for (const [col, read, span, style] of entries) {
2854
+ const address = CellAddress.letter(col) + rowNumber;
2855
+ if (span[0] !== 1 || span[1] !== 1) {
2856
+ state.merges.push(
2857
+ new MergedRegion(
2858
+ address,
2859
+ CellAddress.letter(Math.min(col + span[0], MAX_COLUMNS) - 1) + Math.min(rowNumber + span[1] - 1, MAX_ROWS)
2860
+ )
2861
+ );
2862
+ }
2863
+ if (read === null) continue;
2864
+ const styleName = style !== "" ? style : rowStyle !== "" ? rowStyle : columnStyle(columns, col);
2865
+ const format = styles.cellFormat(styleName, read.type, read.currency, read.dateHasTime);
2866
+ let value = read.value;
2867
+ if (read.seconds !== null) {
2868
+ value = isoFromSeconds(read.seconds, format?.displayFormat !== "date");
2869
+ }
2870
+ state.cells.set(address, new Cell(address, value, read.formula, format, read.comment, read.cached));
2871
+ }
2872
+ state.row++;
2873
+ }
2874
+ }
2875
+ };
2876
+ function collectColumns(container, columns, cursor) {
2877
+ for (const el2 of container.children) {
2878
+ if (el2.ns === NS.TABLE && el2.name === "table-column") {
2879
+ const repeat = Math.max(1, phpInt(attr(el2, NS.TABLE, "number-columns-repeated") ?? "1"));
2880
+ const style = attr(el2, NS.TABLE, "default-cell-style-name") ?? "";
2881
+ if (style !== "") columns.push([cursor.next, cursor.next + repeat - 1, style]);
2882
+ cursor.next += repeat;
2883
+ } else if (el2.ns === NS.TABLE && (el2.name === "table-columns" || el2.name === "table-header-columns" || el2.name === "table-column-group")) {
2884
+ collectColumns(el2, columns, cursor);
2885
+ }
2886
+ if (cursor.next >= MAX_COLUMNS) return;
2887
+ }
2888
+ }
2889
+ function columnStyle(columns, column) {
2890
+ for (const [first2, last, style] of columns) {
2891
+ if (column >= first2 && column <= last) return style;
2892
+ }
2893
+ return "Default";
2894
+ }
2895
+ function hasContent(cell) {
2896
+ if (attr(cell, NS.OFFICE, "value-type") !== void 0) return true;
2897
+ if (attr(cell, NS.TABLE, "formula") !== void 0) return true;
2898
+ if (child(cell, NS.OFFICE, "annotation") !== void 0) return true;
2899
+ return (paragraphs(cell) ?? "") !== "";
2900
+ }
2901
+ function readCell(cell) {
2902
+ const type = attr(cell, NS.OFFICE, "value-type") ?? null;
2903
+ const text = paragraphs(cell);
2904
+ let value = null;
2905
+ let seconds = null;
2906
+ let dateHasTime = false;
2907
+ switch (type) {
2908
+ case "float":
2909
+ case "percentage":
2910
+ case "currency":
2911
+ value = toNumber(attr(cell, NS.OFFICE, "value") ?? "");
2912
+ break;
2913
+ case "boolean": {
2914
+ const b = asciiLower(phpTrim(attr(cell, NS.OFFICE, "boolean-value") ?? ""));
2915
+ value = b === "true" || b === "1";
2916
+ break;
2917
+ }
2918
+ case "date": {
2919
+ const raw = phpTrim(attr(cell, NS.OFFICE, "date-value") ?? "");
2920
+ seconds = dateSeconds(raw);
2921
+ dateHasTime = raw.includes("T");
2922
+ break;
2923
+ }
2924
+ case "time": {
2925
+ const duration = durationSeconds(attr(cell, NS.OFFICE, "time-value") ?? "");
2926
+ seconds = duration === null ? null : duration - UNIX_EPOCH_SERIAL * 86400;
2927
+ break;
2928
+ }
2929
+ case "string": {
2930
+ const error2 = attr(cell, NS.CALCEXT, "value-type") === "error";
2931
+ const stored = attr(cell, NS.OFFICE, "string-value");
2932
+ value = stored !== void 0 && !error2 ? stored : text ?? "";
2933
+ break;
2934
+ }
2935
+ default:
2936
+ value = text !== null && text !== "" ? text : null;
2937
+ }
2938
+ let formula = null;
2939
+ let cached = null;
2940
+ const formulaAttr = attr(cell, NS.TABLE, "formula");
2941
+ if (formulaAttr !== void 0) {
2942
+ const translated = odsFormulaToA1(formulaAttr);
2943
+ const literalBoolean = type === "boolean" && /^[ \t\n\r\f\v]*(TRUE|FALSE)\(\)[ \t\n\r\f\v]*$/i.test(translated);
2944
+ if (!literalBoolean) {
2945
+ formula = translated;
2946
+ cached = seconds !== null ? serial(seconds) : value;
2947
+ value = null;
2948
+ seconds = null;
2949
+ }
2950
+ }
2951
+ let comment = null;
2952
+ const annotation = child(cell, NS.OFFICE, "annotation");
2953
+ if (annotation) {
2954
+ const creator = child(annotation, NS.DC, "creator");
2955
+ const author = creator ? phpTrim(creator.text) : "";
2956
+ comment = new CellComment(paragraphs(annotation) ?? "", author === "" ? null : author);
2957
+ }
2958
+ const currency = attr(cell, NS.OFFICE, "currency");
2959
+ return {
2960
+ value,
2961
+ formula,
2962
+ cached,
2963
+ type,
2964
+ currency: currency !== void 0 ? phpTrim(currency) : null,
2965
+ dateHasTime,
2966
+ comment,
2967
+ seconds
2968
+ };
2969
+ }
2970
+ function toNumber(raw) {
2971
+ const s = phpTrim(raw);
2972
+ if (s === "") return null;
2973
+ if (/^-?\d+$/.test(s)) return parseInt(s, 10);
2974
+ return phpIsNumeric(s) ? Number(s) : null;
2975
+ }
2976
+ function dateSeconds(raw) {
2977
+ const m = /^(-?\d{4,})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?)?(Z|[+-]\d{2}:?\d{2})?$/.exec(raw);
2978
+ if (!m) return null;
2979
+ const days = daysFromCivil(parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10));
2980
+ let seconds = days * 86400 + phpInt(m[4] ?? "0") * 3600 + phpInt(m[5] ?? "0") * 60 + phpInt(m[6] ?? "0");
2981
+ const fraction = m[7] !== void 0 && m[7] !== "" ? Number("0" + m[7]) : 0;
2982
+ const zone = m[8] ?? "";
2983
+ if (zone !== "" && zone !== "Z") {
2984
+ const digits = zone.slice(1).replace(":", "");
2985
+ const offset = phpInt(digits.slice(0, 2)) * 3600 + phpInt(digits.slice(2, 4)) * 60;
2986
+ seconds -= zone[0] === "-" ? -offset : offset;
2987
+ }
2988
+ return seconds + Math.round(fraction);
2989
+ }
2990
+ function durationSeconds(raw) {
2991
+ const m = /^(-)?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(phpTrim(raw));
2992
+ if (!m) return null;
2993
+ const total = phpInt(m[2] ?? "0") * 86400 + phpInt(m[3] ?? "0") * 3600 + phpInt(m[4] ?? "0") * 60 + (m[5] ? Number(m[5]) : 0);
2994
+ const seconds = Math.round(total);
2995
+ return m[1] === "-" ? -seconds : seconds;
2996
+ }
2997
+ function daysFromCivil(year, month, day) {
2998
+ year -= month <= 2 ? 1 : 0;
2999
+ const era = Math.trunc((year >= 0 ? year : year - 399) / 400);
3000
+ const yoe = year - era * 400;
3001
+ const doy = Math.trunc((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
3002
+ const doe = yoe * 365 + Math.trunc(yoe / 4) - Math.trunc(yoe / 100) + doy;
3003
+ return era * 146097 + doe - 719468;
3004
+ }
3005
+ function serial(seconds) {
3006
+ return (seconds + UNIX_EPOCH_SERIAL * 86400) / 86400;
3007
+ }
3008
+ function isoFromSeconds(seconds, withTime) {
3009
+ const d = new Date(seconds * 1e3);
3010
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
3011
+ const date = `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
3012
+ if (!withTime) return date;
3013
+ return `${date}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
3014
+ }
3015
+ function readMeta(xml) {
3016
+ const doc = parseXml(xml, { namespaces: true });
3017
+ const meta = child(doc, NS.OFFICE, "meta");
3018
+ if (!meta) return {};
3019
+ const out = {};
3020
+ const creator = child(meta, NS.META, "initial-creator") ?? child(meta, NS.DC, "creator");
3021
+ if (creator && phpTrim(creator.text) !== "") out["creator"] = phpTrim(creator.text);
3022
+ const created = child(meta, NS.META, "creation-date");
3023
+ if (created && phpTrim(created.text) !== "") {
3024
+ const value = phpTrim(created.text);
3025
+ out["created"] = /(Z|[+-]\d{2}:?\d{2})$/.test(value) ? value : value + "Z";
3026
+ }
3027
+ return out;
3028
+ }
3029
+
2076
3030
  // src/reader/comments-parser.ts
2077
3031
  var CommentsParser = {
2078
3032
  parse(commentsXml) {
@@ -2097,19 +3051,6 @@ function extractText(textNode) {
2097
3051
  return parts.join("");
2098
3052
  }
2099
3053
 
2100
- // src/reader/format/date-inverter.ts
2101
- var DateInverter = {
2102
- toIso(serial, includeTime = false) {
2103
- const epoch = Date.UTC(1899, 11, 30, 0, 0, 0);
2104
- const ms = epoch + Math.round(serial * 86400) * 1e3;
2105
- const d = new Date(ms);
2106
- const pad = (n) => String(n).padStart(2, "0");
2107
- const date = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
2108
- if (!includeTime) return date;
2109
- return `${date}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
2110
- }
2111
- };
2112
-
2113
3054
  // src/reader/rels-parser.ts
2114
3055
  var RelsParser = {
2115
3056
  parse(relsXml) {
@@ -2145,90 +3086,6 @@ function renderSi(si) {
2145
3086
  return els(si, "r").map((run) => el(run, "t")?.text ?? "").join("");
2146
3087
  }
2147
3088
 
2148
- // src/reader/format/num-fmt-parser.ts
2149
- var BUILTIN = {
2150
- 0: "General",
2151
- 1: "0",
2152
- 2: "0.00",
2153
- 3: "#,##0",
2154
- 4: "#,##0.00",
2155
- 9: "0%",
2156
- 10: "0.00%",
2157
- 11: "0.00E+00",
2158
- 12: "# ?/?",
2159
- 13: "# ??/??",
2160
- 14: "mm-dd-yy",
2161
- 15: "d-mmm-yy",
2162
- 16: "d-mmm",
2163
- 17: "mmm-yy",
2164
- 18: "h:mm AM/PM",
2165
- 19: "h:mm:ss AM/PM",
2166
- 20: "h:mm",
2167
- 21: "h:mm:ss",
2168
- 22: "m/d/yy h:mm",
2169
- 37: "#,##0 ;(#,##0)",
2170
- 38: "#,##0 ;[Red](#,##0)",
2171
- 39: "#,##0.00;(#,##0.00)",
2172
- 40: "#,##0.00;[Red](#,##0.00)",
2173
- 45: "mm:ss",
2174
- 46: "[h]:mm:ss",
2175
- 47: "mmss.0",
2176
- 48: "##0.0E+0",
2177
- 49: "@"
2178
- };
2179
- var SYMBOL_TO_ISO = {
2180
- $: "USD",
2181
- "\u20AC": "EUR",
2182
- "\xA3": "GBP",
2183
- "\xA5": "JPY",
2184
- "\u20B9": "INR",
2185
- "\u20A9": "KRW"
2186
- };
2187
- var NumFmtParser = {
2188
- parse(formatCode) {
2189
- if (formatCode === null) return null;
2190
- const code = formatCode;
2191
- if (code === "" || code === "General" || code === "@") {
2192
- return { displayFormat: code === "@" ? "text" : "auto" };
2193
- }
2194
- if (looksLikeDate(code)) {
2195
- return { displayFormat: hasTimeComponent(code) ? "datetime" : "date" };
2196
- }
2197
- if (code.includes("%")) {
2198
- return { displayFormat: "percentage", decimals: decimalsAfter(code) };
2199
- }
2200
- const m = /^"([^"]+)"/u.exec(code);
2201
- if (m) {
2202
- const iso = SYMBOL_TO_ISO[m[1]];
2203
- const result = { displayFormat: "currency", decimals: decimalsAfter(code) };
2204
- if (iso !== void 0) result.currency = iso;
2205
- return result;
2206
- }
2207
- if (/^#?,?#?#?0(\.0+)?$/.test(code.split(";")[0])) {
2208
- return { displayFormat: "number", decimals: decimalsAfter(code) };
2209
- }
2210
- return null;
2211
- },
2212
- parseBuiltin(id) {
2213
- const code = BUILTIN[id];
2214
- if (code === void 0) return null;
2215
- return NumFmtParser.parse(code);
2216
- }
2217
- };
2218
- function looksLikeDate(code) {
2219
- const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
2220
- return /[ymd]/i.test(stripped);
2221
- }
2222
- function hasTimeComponent(code) {
2223
- const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
2224
- return /[hHsS]/.test(stripped) || stripped.toLowerCase().includes("mm:");
2225
- }
2226
- function decimalsAfter(code) {
2227
- const section = code.split(";")[0];
2228
- const m = /0\.(0+)/.exec(section);
2229
- return m ? m[1].length : 0;
2230
- }
2231
-
2232
3089
  // src/reader/styles-parser.ts
2233
3090
  var StylesParser = {
2234
3091
  parse(stylesXml) {
@@ -2248,14 +3105,14 @@ var StylesParser = {
2248
3105
  const align = alignment && alignment.attrs["horizontal"] !== void 0 ? alignment.attrs["horizontal"] : null;
2249
3106
  const font = fonts[fontId] ?? null;
2250
3107
  const fill = fills[fillId] ?? null;
2251
- const border = borders[borderId] ?? null;
3108
+ const border2 = borders[borderId] ?? null;
2252
3109
  const customCode = numFmts[numFmtId];
2253
3110
  const numFmtParsed = customCode !== void 0 ? NumFmtParser.parse(customCode) : NumFmtParser.parseBuiltin(numFmtId);
2254
- if (xfs.length === 0 && font === null && fill === null && border === null && !numFmtParsed && align === null) {
3111
+ if (xfs.length === 0 && font === null && fill === null && border2 === null && !numFmtParsed && align === null) {
2255
3112
  xfs.push(null);
2256
3113
  continue;
2257
3114
  }
2258
- xfs.push(buildCellFormat(font, fill, border, numFmtParsed, align));
3115
+ xfs.push(buildCellFormat(font, fill, border2, numFmtParsed, align));
2259
3116
  }
2260
3117
  return xfs;
2261
3118
  }
@@ -2270,12 +3127,12 @@ function parseNumFmts(xml) {
2270
3127
  function parseFonts(xml) {
2271
3128
  return els(el(xml, "fonts"), "font").map((f) => {
2272
3129
  const sz = el(f, "sz");
2273
- const color = el(f, "color");
3130
+ const color2 = el(f, "color");
2274
3131
  return {
2275
3132
  bold: !!el(f, "b"),
2276
3133
  italic: !!el(f, "i"),
2277
3134
  size: sz ? Math.trunc(Number(at(sz, "val"))) : 11,
2278
- color: color && at(color, "rgb") !== void 0 ? argbToHex(at(color, "rgb")) : null
3135
+ color: color2 && at(color2, "rgb") !== void 0 ? argbToHex(at(color2, "rgb")) : null
2279
3136
  };
2280
3137
  });
2281
3138
  }
@@ -2293,15 +3150,15 @@ function parseBorders(xml) {
2293
3150
  const rec = { top: null, right: null, bottom: null, left: null };
2294
3151
  for (const side of ["top", "right", "bottom", "left"]) {
2295
3152
  const sideEl = el(b, side);
2296
- const color = el(sideEl, "color");
2297
- if (sideEl && at(sideEl, "style") !== void 0 && at(sideEl, "style") !== "" && color && at(color, "rgb") !== void 0) {
2298
- rec[side] = argbToHex(at(color, "rgb"));
3153
+ const color2 = el(sideEl, "color");
3154
+ if (sideEl && at(sideEl, "style") !== void 0 && at(sideEl, "style") !== "" && color2 && at(color2, "rgb") !== void 0) {
3155
+ rec[side] = argbToHex(at(color2, "rgb"));
2299
3156
  }
2300
3157
  }
2301
3158
  return rec;
2302
3159
  });
2303
3160
  }
2304
- function buildCellFormat(font, fill, border, numFmt, align) {
3161
+ function buildCellFormat(font, fill, border2, numFmt, align) {
2305
3162
  return new CellFormat({
2306
3163
  bold: font?.bold ?? false,
2307
3164
  italic: font?.italic ?? false,
@@ -2311,10 +3168,10 @@ function buildCellFormat(font, fill, border, numFmt, align) {
2311
3168
  color: font?.color ?? null,
2312
3169
  backgroundColor: fill,
2313
3170
  fontSize: font && font.size !== 11 ? font.size : null,
2314
- borderTop: border?.top ?? null,
2315
- borderRight: border?.right ?? null,
2316
- borderBottom: border?.bottom ?? null,
2317
- borderLeft: border?.left ?? null,
3171
+ borderTop: border2?.top ?? null,
3172
+ borderRight: border2?.right ?? null,
3173
+ borderBottom: border2?.bottom ?? null,
3174
+ borderLeft: border2?.left ?? null,
2318
3175
  currency: numFmt?.currency ?? null
2319
3176
  });
2320
3177
  }
@@ -2414,14 +3271,17 @@ function parseFrozen(xml) {
2414
3271
  }
2415
3272
 
2416
3273
  // src/reader/xlsx-reader.ts
2417
- var decoder2 = new TextDecoder();
3274
+ var decoder4 = new TextDecoder();
2418
3275
  var XlsxReader = class {
2419
3276
  describe(bytes) {
2420
- return this.workbookToSchema(this.readWorkbook(bytes));
3277
+ return WorkbookSchema.fromWorkbook(this.readWorkbook(bytes));
2421
3278
  }
2422
3279
  readWorkbook(bytes) {
2423
- const files = unzipSync(bytes);
2424
- const getText = (name) => files[name] ? decoder2.decode(files[name]) : null;
3280
+ return this.readFiles(unzipSync(bytes));
3281
+ }
3282
+ /** Read an already-unzipped package (name -> bytes), so a caller that sniffed it does not unzip twice. */
3283
+ readFiles(files) {
3284
+ const getText = (name) => files[name] ? decoder4.decode(files[name]) : null;
2425
3285
  const stylesXml = getText("xl/styles.xml");
2426
3286
  const stylesIndex = stylesXml ? StylesParser.parse(stylesXml) : [];
2427
3287
  const sharedStringsXml = getText("xl/sharedStrings.xml");
@@ -2470,69 +3330,7 @@ var XlsxReader = class {
2470
3330
  if (created) meta["created"] = created.text;
2471
3331
  return meta;
2472
3332
  }
2473
- workbookToSchema(workbook) {
2474
- const schema = { sheets: workbook.sheets.map((s) => this.sheetToSchema(s)) };
2475
- if (Object.keys(workbook.meta).length > 0) schema["meta"] = workbook.meta;
2476
- return schema;
2477
- }
2478
- sheetToSchema(sheet) {
2479
- const cells = {};
2480
- for (const [address, cell] of sheet.cells) {
2481
- const cellSchema = this.cellToSchema(cell);
2482
- if (cellSchema === null) continue;
2483
- cells[address] = cellSchema;
2484
- }
2485
- const out = { name: sheet.name, cells };
2486
- if (sheet.mergedRegions.length > 0) {
2487
- out["mergedRegions"] = sheet.mergedRegions.map((m) => ({ start: m.start, end: m.end }));
2488
- }
2489
- if (sheet.columnWidths.size > 0) {
2490
- const widths = {};
2491
- for (const [k, v] of sheet.columnWidths) widths[k] = v;
2492
- out["columnWidths"] = widths;
2493
- }
2494
- if (sheet.frozenRows > 0) out["frozenRows"] = sheet.frozenRows;
2495
- if (sheet.frozenCols > 0) out["frozenCols"] = sheet.frozenCols;
2496
- return out;
2497
- }
2498
- cellToSchema(cell) {
2499
- let value = cell.value;
2500
- const format = cell.format;
2501
- if (format !== null && (format.displayFormat === "date" || format.displayFormat === "datetime") && isNumericString(value)) {
2502
- value = DateInverter.toIso(Number(value), format.displayFormat === "datetime");
2503
- }
2504
- const out = { value };
2505
- if (cell.formula !== null) out["formula"] = cell.formula;
2506
- if (cell.cachedValue !== null) out["computedValue"] = cell.cachedValue;
2507
- if (format !== null && !format.isEmpty()) out["format"] = formatToObject(format);
2508
- if (cell.comment !== null) out["comment"] = commentToObject(cell.comment);
2509
- if (Object.keys(out).length === 1 && out["value"] === null) return null;
2510
- return out;
2511
- }
2512
3333
  };
2513
- function formatToObject(f) {
2514
- const out = {};
2515
- if (f.bold) out["bold"] = true;
2516
- if (f.italic) out["italic"] = true;
2517
- if (f.textAlign !== null) out["textAlign"] = f.textAlign;
2518
- if (f.displayFormat !== null) out["displayFormat"] = f.displayFormat;
2519
- if (f.decimals !== null) out["decimals"] = f.decimals;
2520
- if (f.color !== null) out["color"] = f.color;
2521
- if (f.backgroundColor !== null) out["backgroundColor"] = f.backgroundColor;
2522
- if (f.fontSize !== null) out["fontSize"] = f.fontSize;
2523
- if (f.borderTop !== null) out["borderTop"] = f.borderTop;
2524
- if (f.borderRight !== null) out["borderRight"] = f.borderRight;
2525
- if (f.borderBottom !== null) out["borderBottom"] = f.borderBottom;
2526
- if (f.borderLeft !== null) out["borderLeft"] = f.borderLeft;
2527
- if (f.currency !== null) out["currency"] = f.currency;
2528
- return out;
2529
- }
2530
- function commentToObject(c) {
2531
- const out = { text: c.text };
2532
- if (c.author !== null) out["author"] = c.author;
2533
- if (c.color !== null) out["color"] = c.color;
2534
- return out;
2535
- }
2536
3334
  function resolveRelativePath(base, target) {
2537
3335
  const baseDir = base.includes("/") ? base.slice(0, base.lastIndexOf("/")) : ".";
2538
3336
  const combined = baseDir + "/" + target;
@@ -2736,11 +3534,11 @@ var StylesRegistry = class {
2736
3534
  for (const b of this.borders) {
2737
3535
  bordersXml += "<border>";
2738
3536
  for (const side of ["left", "right", "top", "bottom"]) {
2739
- const color = b[side];
2740
- if (!color) {
3537
+ const color2 = b[side];
3538
+ if (!color2) {
2741
3539
  bordersXml += `<${side}/>`;
2742
3540
  } else {
2743
- bordersXml += `<${side} style="thin"><color rgb="${hexToArgb(color)}"/></${side}>`;
3541
+ bordersXml += `<${side} style="thin"><color rgb="${hexToArgb(color2)}"/></${side}>`;
2744
3542
  }
2745
3543
  }
2746
3544
  bordersXml += "<diagonal/></border>";
@@ -2762,8 +3560,8 @@ var StylesRegistry = class {
2762
3560
  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>';
2763
3561
  }
2764
3562
  };
2765
- function hexToArgb(hex) {
2766
- const h = hex.replace(/^#/, "");
3563
+ function hexToArgb(hex2) {
3564
+ const h = hex2.replace(/^#/, "");
2767
3565
  if (h.length === 6) return "FF" + h.toUpperCase();
2768
3566
  if (h.length === 8) return h.toUpperCase();
2769
3567
  return "FF000000";
@@ -2956,23 +3754,32 @@ var holy_sheet_schema_default = {
2956
3754
  $schema: "http://json-schema.org/draft-07/schema#",
2957
3755
  $id: "https://github.com/Particle-Academy/holy-sheet/skills/holy-sheet.schema.json",
2958
3756
  title: "Holy Sheet workbook schema",
2959
- 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.",
3757
+ description: "Input format for HolySheet\\Agent::write(). Describes a multi-sheet xlsx workbook agentically. Everything in this schema is SUPPORTED NOW: values, formulas, per-column types and number formats, per-cell styling, themes, merges, frozen panes, column widths, totals and comments. A workbook is judged by how it READS, not only by whether the numbers are right \u2014 money should render as money and a header row should look like one, so prefer a typed column over a raw number and set the presentation the document needs.",
2960
3758
  type: "object",
2961
- required: ["sheets"],
3759
+ required: [
3760
+ "sheets"
3761
+ ],
2962
3762
  additionalProperties: false,
2963
3763
  properties: {
2964
3764
  sheets: {
2965
3765
  type: "array",
2966
3766
  minItems: 1,
2967
3767
  description: "One or more sheets. The first sheet is shown when the file opens.",
2968
- items: { $ref: "#/definitions/Sheet" }
3768
+ items: {
3769
+ $ref: "#/definitions/Sheet"
3770
+ }
2969
3771
  },
2970
3772
  meta: {
2971
3773
  type: "object",
2972
3774
  description: "Optional workbook metadata surfaced in Excel's File \u2192 Info pane.",
2973
3775
  properties: {
2974
- creator: { type: "string" },
2975
- created: { type: "string", description: "ISO-8601 datetime. Defaults to now()." }
3776
+ creator: {
3777
+ type: "string"
3778
+ },
3779
+ created: {
3780
+ type: "string",
3781
+ description: "ISO-8601 datetime. Defaults to now()."
3782
+ }
2976
3783
  },
2977
3784
  additionalProperties: true
2978
3785
  }
@@ -2980,11 +3787,24 @@ var holy_sheet_schema_default = {
2980
3787
  definitions: {
2981
3788
  Sheet: {
2982
3789
  type: "object",
2983
- required: ["name"],
3790
+ required: [
3791
+ "name"
3792
+ ],
2984
3793
  additionalProperties: false,
2985
3794
  oneOf: [
2986
- { required: ["columns", "rows"], description: "Row-oriented (agent-ergonomic)." },
2987
- { required: ["cells"], description: "Sparse A1-keyed map (fancy-sheets passthrough)." }
3795
+ {
3796
+ required: [
3797
+ "columns",
3798
+ "rows"
3799
+ ],
3800
+ description: "Row-oriented (agent-ergonomic)."
3801
+ },
3802
+ {
3803
+ required: [
3804
+ "cells"
3805
+ ],
3806
+ description: "Sparse A1-keyed map (fancy-sheets passthrough)."
3807
+ }
2988
3808
  ],
2989
3809
  properties: {
2990
3810
  name: {
@@ -2995,7 +3815,9 @@ var holy_sheet_schema_default = {
2995
3815
  },
2996
3816
  columns: {
2997
3817
  type: "array",
2998
- items: { $ref: "#/definitions/Column" },
3818
+ items: {
3819
+ $ref: "#/definitions/Column"
3820
+ },
2999
3821
  description: "When present, the first row is the header row. Column types are applied to every row in that column."
3000
3822
  },
3001
3823
  rows: {
@@ -3003,89 +3825,155 @@ var holy_sheet_schema_default = {
3003
3825
  items: {
3004
3826
  type: "array",
3005
3827
  description: "One row of cell values, in column order. Use null for missing values.",
3006
- items: { $ref: "#/definitions/CellValue" }
3828
+ items: {
3829
+ $ref: "#/definitions/CellValue"
3830
+ }
3007
3831
  }
3008
3832
  },
3009
3833
  cells: {
3010
3834
  type: "object",
3011
- description: "Sparse A1-keyed map. Use this shape when exporting from <Spreadsheet>.",
3012
- additionalProperties: { $ref: "#/definitions/CellData" },
3835
+ description: "Sparse cells keyed by A1 address, for anything the row/column model does not cover \u2014 a title band, a callout, a note. May be used ALONGSIDE columns/rows: the explicit cell wins at its address and inherits that column's format for anything it does not set.",
3836
+ additionalProperties: {
3837
+ $ref: "#/definitions/CellData"
3838
+ },
3013
3839
  patternProperties: {
3014
- "^[A-Z]+[1-9][0-9]*$": { $ref: "#/definitions/CellData" }
3840
+ "^[A-Z]+[1-9][0-9]*$": {
3841
+ $ref: "#/definitions/CellData"
3842
+ }
3015
3843
  }
3016
3844
  },
3017
3845
  totals: {
3018
3846
  type: "object",
3019
- description: "Symbolic aggregations appended below the data. Key = column header; value = aggregation. Lands in 0.7.",
3847
+ description: "Symbolic aggregations appended below the data. Key = column header; value = one of sum, avg, count, min, max. These become real FORMULAS, so a total updates when a figure above it is edited \u2014 a row of typed numbers looks identical and is wrong the moment anyone changes anything.",
3020
3848
  additionalProperties: {
3021
3849
  type: "string",
3022
- enum: ["sum", "avg", "count", "min", "max"]
3850
+ enum: [
3851
+ "sum",
3852
+ "avg",
3853
+ "count",
3854
+ "min",
3855
+ "max"
3856
+ ]
3023
3857
  }
3024
3858
  },
3025
3859
  theme: {
3026
3860
  type: "string",
3027
- enum: ["default", "minimal", "plain", "business"],
3861
+ enum: [
3862
+ "default",
3863
+ "minimal",
3864
+ "plain",
3865
+ "business"
3866
+ ],
3028
3867
  default: "default",
3029
- description: "Pre-baked style preset. Lands in 0.3."
3868
+ description: "Pre-baked style preset applied to headers, banding and totals. The cheapest way to make a sheet look deliberate: set it unless you are styling every cell yourself."
3030
3869
  },
3031
3870
  mergedRegions: {
3032
3871
  type: "array",
3033
3872
  items: {
3034
3873
  type: "object",
3035
- required: ["start", "end"],
3874
+ required: [
3875
+ "start",
3876
+ "end"
3877
+ ],
3036
3878
  properties: {
3037
- start: { type: "string", description: "A1 ref of top-left." },
3038
- end: { type: "string", description: "A1 ref of bottom-right." }
3879
+ start: {
3880
+ type: "string",
3881
+ description: "A1 ref of top-left."
3882
+ },
3883
+ end: {
3884
+ type: "string",
3885
+ description: "A1 ref of bottom-right."
3886
+ }
3039
3887
  }
3040
3888
  },
3041
- description: "Cells to merge. Lands in 0.5."
3889
+ description: 'Cells to merge, as [{start, end}] \u2014 e.g. [{"start": "A1", "end": "D1"}]. Use it for a title band spanning the table.'
3042
3890
  },
3043
3891
  columnWidths: {
3044
3892
  type: "object",
3045
- description: "Override widths in pixels. Keys are 0-based column indexes. Lands in 0.5.",
3046
- additionalProperties: { type: "number", minimum: 0 }
3893
+ description: "Override widths in pixels. Keys are 0-based column indexes. Set these when a header or a formatted number would otherwise be cut off or shown as ####.",
3894
+ additionalProperties: {
3895
+ type: "number",
3896
+ minimum: 0
3897
+ }
3898
+ },
3899
+ frozenRows: {
3900
+ type: "integer",
3901
+ minimum: 0,
3902
+ description: "How many rows stay visible while the sheet scrolls. Set to 1 whenever there is a header row, so the reader can still see what the columns are further down."
3047
3903
  },
3048
- frozenRows: { type: "integer", minimum: 0, description: "Lands in 0.5." },
3049
- frozenCols: { type: "integer", minimum: 0, description: "Lands in 0.5." }
3904
+ frozenCols: {
3905
+ type: "integer",
3906
+ minimum: 0,
3907
+ description: "How many columns stay visible while the sheet scrolls sideways. Set to 1 when the first column is the label for the row."
3908
+ }
3050
3909
  }
3051
3910
  },
3052
3911
  Column: {
3053
3912
  type: "object",
3054
- required: ["header"],
3913
+ required: [
3914
+ "header"
3915
+ ],
3055
3916
  additionalProperties: false,
3056
3917
  properties: {
3057
- header: { type: "string" },
3918
+ header: {
3919
+ type: "string"
3920
+ },
3058
3921
  type: {
3059
3922
  type: "string",
3060
- enum: ["auto", "string", "number", "integer", "boolean", "date", "datetime", "currency", "percent", "formula"],
3923
+ enum: [
3924
+ "auto",
3925
+ "string",
3926
+ "number",
3927
+ "integer",
3928
+ "boolean",
3929
+ "date",
3930
+ "datetime",
3931
+ "currency",
3932
+ "percent",
3933
+ "formula"
3934
+ ],
3061
3935
  default: "auto",
3062
- description: "Cell type applied to every value in this column. 'auto' infers per cell."
3936
+ description: "Cell type applied to every value in this column, and the main thing deciding whether the sheet reads as a document or as a dump. Use 'currency' for money, 'percent' for rates and 'date' for dates \u2014 'auto' infers a plain number and leaves 1250000.5 looking like 1250000.5."
3063
3937
  },
3064
3938
  currency: {
3065
3939
  type: "string",
3066
- description: "ISO-4217 code (USD, EUR, JPY, ...) when type=currency. Defaults to config('holy-sheet.locale')."
3940
+ description: "ISO currency code for a 'currency' column, e.g. USD. Pair it with decimals: 2."
3067
3941
  },
3068
3942
  decimals: {
3069
3943
  type: "integer",
3070
3944
  minimum: 0,
3071
3945
  maximum: 10,
3072
- description: "Display decimals for number/currency/percent. Default 2 for currency, 1 for percent, 0 for integer, none for number."
3946
+ description: "Decimal places for a numeric, currency or percent column. Money is normally 2; a growth rate is normally 1."
3073
3947
  },
3074
3948
  format: {
3075
3949
  type: "string",
3076
3950
  description: "Custom Excel number format string. Overrides type-derived format. Power-user escape hatch."
3077
3951
  },
3078
- width: { type: "number", minimum: 0, description: "Column width in pixels. Same as columnWidths but per-column." }
3952
+ width: {
3953
+ type: "number",
3954
+ minimum: 0,
3955
+ description: "Column width in pixels. Same as columnWidths but per-column."
3956
+ }
3079
3957
  }
3080
3958
  },
3081
3959
  CellValue: {
3082
3960
  description: "A primitive value, null, or an object {value, formula, format, comment} for per-cell control.",
3083
3961
  oneOf: [
3084
- { type: "string" },
3085
- { type: "number" },
3086
- { type: "boolean" },
3087
- { type: "null" },
3088
- { $ref: "#/definitions/CellData" }
3962
+ {
3963
+ type: "string"
3964
+ },
3965
+ {
3966
+ type: "number"
3967
+ },
3968
+ {
3969
+ type: "boolean"
3970
+ },
3971
+ {
3972
+ type: "null"
3973
+ },
3974
+ {
3975
+ $ref: "#/definitions/CellData"
3976
+ }
3089
3977
  ]
3090
3978
  },
3091
3979
  CellData: {
@@ -3095,10 +3983,18 @@ var holy_sheet_schema_default = {
3095
3983
  value: {
3096
3984
  description: "Raw cell value.",
3097
3985
  oneOf: [
3098
- { type: "string" },
3099
- { type: "number" },
3100
- { type: "boolean" },
3101
- { type: "null" }
3986
+ {
3987
+ type: "string"
3988
+ },
3989
+ {
3990
+ type: "number"
3991
+ },
3992
+ {
3993
+ type: "boolean"
3994
+ },
3995
+ {
3996
+ type: "null"
3997
+ }
3102
3998
  ]
3103
3999
  },
3104
4000
  formula: {
@@ -3107,53 +4003,125 @@ var holy_sheet_schema_default = {
3107
4003
  },
3108
4004
  computedValue: {
3109
4005
  description: "Cached value from a previous evaluation. When present, Excel shows it without recomputing on open.",
3110
- oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }, { type: "null" }]
4006
+ oneOf: [
4007
+ {
4008
+ type: "string"
4009
+ },
4010
+ {
4011
+ type: "number"
4012
+ },
4013
+ {
4014
+ type: "boolean"
4015
+ },
4016
+ {
4017
+ type: "null"
4018
+ }
4019
+ ]
4020
+ },
4021
+ format: {
4022
+ $ref: "#/definitions/CellFormat"
3111
4023
  },
3112
- format: { $ref: "#/definitions/CellFormat" },
3113
- comment: { $ref: "#/definitions/CellComment" }
4024
+ comment: {
4025
+ $ref: "#/definitions/CellComment"
4026
+ }
3114
4027
  }
3115
4028
  },
3116
4029
  CellFormat: {
3117
4030
  type: "object",
3118
- description: "Per-cell format. Lands in 0.3.",
4031
+ description: "Per-cell presentation: bold, italic, font size, text and background colour, alignment, borders and number format. Use it for the things a column type cannot express \u2014 a title band, a highlighted figure, a ruled callout.",
3119
4032
  additionalProperties: false,
3120
4033
  properties: {
3121
- bold: { type: "boolean" },
3122
- italic: { type: "boolean" },
3123
- textAlign: { type: "string", enum: ["left", "center", "right"] },
4034
+ bold: {
4035
+ type: "boolean"
4036
+ },
4037
+ italic: {
4038
+ type: "boolean"
4039
+ },
4040
+ textAlign: {
4041
+ type: "string",
4042
+ enum: [
4043
+ "left",
4044
+ "center",
4045
+ "right"
4046
+ ]
4047
+ },
3124
4048
  displayFormat: {
3125
4049
  type: "string",
3126
- enum: ["auto", "text", "number", "date", "datetime", "percentage", "currency"]
4050
+ enum: [
4051
+ "auto",
4052
+ "text",
4053
+ "number",
4054
+ "date",
4055
+ "datetime",
4056
+ "percentage",
4057
+ "currency"
4058
+ ]
4059
+ },
4060
+ decimals: {
4061
+ type: "integer",
4062
+ minimum: 0,
4063
+ maximum: 10
4064
+ },
4065
+ color: {
4066
+ type: "string",
4067
+ description: "Hex color (#RRGGBB) for text."
4068
+ },
4069
+ backgroundColor: {
4070
+ type: "string",
4071
+ description: "Hex color for cell fill."
4072
+ },
4073
+ fontSize: {
4074
+ type: "integer",
4075
+ minimum: 6,
4076
+ maximum: 72
4077
+ },
4078
+ borderTop: {
4079
+ type: "string"
4080
+ },
4081
+ borderRight: {
4082
+ type: "string"
4083
+ },
4084
+ borderBottom: {
4085
+ type: "string"
3127
4086
  },
3128
- decimals: { type: "integer", minimum: 0, maximum: 10 },
3129
- color: { type: "string", description: "Hex color (#RRGGBB) for text." },
3130
- backgroundColor: { type: "string", description: "Hex color for cell fill." },
3131
- fontSize: { type: "integer", minimum: 6, maximum: 72 },
3132
- borderTop: { type: "string" },
3133
- borderRight: { type: "string" },
3134
- borderBottom: { type: "string" },
3135
- borderLeft: { type: "string" }
4087
+ borderLeft: {
4088
+ type: "string"
4089
+ }
3136
4090
  }
3137
4091
  },
3138
4092
  CellComment: {
3139
4093
  type: "object",
3140
- description: "Lands in 0.6.",
3141
- required: ["text"],
4094
+ description: "A reviewer note attached to a cell. Accepts a plain string, or an object with text/author/color.",
4095
+ required: [
4096
+ "text"
4097
+ ],
3142
4098
  additionalProperties: false,
3143
4099
  properties: {
3144
- text: { type: "string" },
3145
- author: { type: "string" },
3146
- color: { type: "string", description: "Hex color for the corner triangle indicator." }
4100
+ text: {
4101
+ type: "string"
4102
+ },
4103
+ author: {
4104
+ type: "string"
4105
+ },
4106
+ color: {
4107
+ type: "string",
4108
+ description: "Hex color for the corner triangle indicator."
4109
+ }
3147
4110
  }
3148
4111
  }
3149
4112
  }
3150
4113
  };
3151
4114
 
3152
4115
  // src/agent.ts
3153
- var VERSION = "1.0.0";
4116
+ var VERSION = "2.3.0";
3154
4117
  function toU8(input) {
3155
4118
  return input instanceof Uint8Array ? input : new Uint8Array(input);
3156
4119
  }
4120
+ function readSpreadsheet(bytes, path) {
4121
+ const { format, files } = FormatSniffer.sniff(bytes, path);
4122
+ const workbook = format === "ods" ? new OdsReader().readFiles(files) : new XlsxReader().readFiles(files);
4123
+ return WorkbookSchema.fromWorkbook(workbook);
4124
+ }
3157
4125
  var Agent = {
3158
4126
  /** Validate a schema without writing. Empty array = valid. */
3159
4127
  validate(schema) {
@@ -3178,16 +4146,20 @@ var Agent = {
3178
4146
  toolDefinition() {
3179
4147
  return holy_sheet_schema_default;
3180
4148
  },
3181
- /** Round-trip xlsx bytes back to a schema. Universal. */
4149
+ /**
4150
+ * Round-trip xlsx or ods bytes back to a schema. Universal. The format is
4151
+ * told apart by content, never by name, and both describe to the same
4152
+ * schema. Throws `UnsupportedFormatException` for anything else.
4153
+ */
3182
4154
  read(input) {
3183
- return new XlsxReader().describe(toU8(input));
4155
+ return readSpreadsheet(toU8(input), null);
3184
4156
  },
3185
- /** Round-trip an xlsx file on disk back to a schema (Node only). */
4157
+ /** Round-trip an xlsx or ods file on disk back to a schema (Node only). */
3186
4158
  async describe(path) {
3187
4159
  const fs = await import('fs');
3188
4160
  if (!fs.existsSync(path)) return { error: "not_found", path };
3189
4161
  const bytes = fs.readFileSync(path);
3190
- return new XlsxReader().describe(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength));
4162
+ return readSpreadsheet(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), path);
3191
4163
  },
3192
4164
  /** Validate + conservative repairs in one call. */
3193
4165
  validateAndRepair(schema) {
@@ -3251,6 +4223,6 @@ var HolySheet = class {
3251
4223
  };
3252
4224
  HolySheet.VERSION = VERSION;
3253
4225
 
3254
- export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormulaLinter, HolySheet, Inference, Normalizer, Repairer, SchemaException, Theme, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
4226
+ export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, HolySheet, Inference, Normalizer, OdsReader, Repairer, SchemaException, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
3255
4227
  //# sourceMappingURL=index.js.map
3256
4228
  //# sourceMappingURL=index.js.map