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