@particle-academy/holy-sheet 2.2.1 → 2.3.1
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 +1292 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -8
- package/dist/index.d.ts +70 -8
- package/dist/index.js +1290 -277
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
290
|
+
constructor(text, author = null, color2 = null) {
|
|
291
291
|
this.text = text;
|
|
292
292
|
this.author = author;
|
|
293
|
-
this.color =
|
|
293
|
+
this.color = color2;
|
|
294
294
|
}
|
|
295
295
|
};
|
|
296
296
|
|
|
@@ -719,6 +719,7 @@ var ERR_REF = "#REF!";
|
|
|
719
719
|
var ERR_NAME = "#NAME?";
|
|
720
720
|
var ERR_DIV0 = "#DIV/0!";
|
|
721
721
|
var ERR_CIRC = "#CIRC!";
|
|
722
|
+
var SHEET_QUALIFIER = "('(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
|
|
722
723
|
var LinterError = class extends Error {
|
|
723
724
|
constructor(errorCode) {
|
|
724
725
|
super(errorCode);
|
|
@@ -729,9 +730,16 @@ var FormulaLinter = class {
|
|
|
729
730
|
constructor() {
|
|
730
731
|
this.index = /* @__PURE__ */ new Map();
|
|
731
732
|
this.cache = /* @__PURE__ */ new Map();
|
|
733
|
+
/**
|
|
734
|
+
* Every sheet name, keyed by its lower-cased form. Excel matches sheet names
|
|
735
|
+
* without regard to case, and a reference to a sheet that is not here is
|
|
736
|
+
* `#REF!` rather than a range of blanks.
|
|
737
|
+
*/
|
|
738
|
+
this.sheetNames = /* @__PURE__ */ new Map();
|
|
732
739
|
}
|
|
733
740
|
lint(schema) {
|
|
734
741
|
const workbook = new Normalizer().normalize(schema);
|
|
742
|
+
this.sheetNames = new Map(workbook.sheets.map((sheet) => [sheet.name.toLowerCase(), sheet.name]));
|
|
735
743
|
this.index = this.buildIndex(workbook);
|
|
736
744
|
this.cache = /* @__PURE__ */ new Map();
|
|
737
745
|
const issues = [];
|
|
@@ -807,6 +815,24 @@ var FormulaLinter = class {
|
|
|
807
815
|
tokens.push({ type: "STRING", value });
|
|
808
816
|
continue;
|
|
809
817
|
}
|
|
818
|
+
if (ch === "'") {
|
|
819
|
+
const start = i;
|
|
820
|
+
i++;
|
|
821
|
+
for (; ; ) {
|
|
822
|
+
if (i >= len) throw new LinterError(ERR_NAME);
|
|
823
|
+
if (src[i] === "'") {
|
|
824
|
+
if (i + 1 < len && src[i + 1] === "'") {
|
|
825
|
+
i += 2;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
i++;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
i++;
|
|
832
|
+
}
|
|
833
|
+
tokens.push({ type: "SHEET", value: src.slice(start, i) });
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
810
836
|
if (isAlpha(ch) || ch === "_" || ch === "$") {
|
|
811
837
|
const start = i;
|
|
812
838
|
while (i < len && (isAlnum(src[i]) || src[i] === "_" || src[i] === "$" || src[i] === ".")) i++;
|
|
@@ -924,16 +950,18 @@ var FormulaLinter = class {
|
|
|
924
950
|
c.pos++;
|
|
925
951
|
return tok.value;
|
|
926
952
|
}
|
|
927
|
-
if (tok.type === "IDENT") {
|
|
953
|
+
if (tok.type === "SHEET" || tok.type === "IDENT") {
|
|
928
954
|
const next = c.tokens[c.pos + 1];
|
|
929
955
|
const next2 = c.tokens[c.pos + 2];
|
|
930
956
|
if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
|
|
931
|
-
const sheetName = this.cleanSheetName(tok.value);
|
|
957
|
+
const sheetName = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
|
|
958
|
+
if (sheetName === void 0) throw new LinterError(ERR_REF);
|
|
932
959
|
c.pos += 2;
|
|
933
960
|
const startTok = c.tokens[c.pos].value;
|
|
934
961
|
c.pos++;
|
|
935
962
|
return this.resolveRefOrRange(startTok, sheetName, c, stack);
|
|
936
963
|
}
|
|
964
|
+
if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
|
|
937
965
|
if (next && next.type === "OP" && next.value === "(") {
|
|
938
966
|
const name = tok.value.toUpperCase();
|
|
939
967
|
c.pos += 2;
|
|
@@ -978,9 +1006,13 @@ var FormulaLinter = class {
|
|
|
978
1006
|
const r = ref.replace(/\$/g, "").toUpperCase();
|
|
979
1007
|
return /^[A-Z]+\d+$/.test(r) ? r : null;
|
|
980
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Unwrap the single quotes Excel puts around a sheet name that needs them,
|
|
1011
|
+
* and turn its doubled quote back into one: 'Q3 ''Final''' is Q3 'Final'.
|
|
1012
|
+
*/
|
|
981
1013
|
cleanSheetName(name) {
|
|
982
1014
|
if (name.length >= 2 && name[0] === "'" && name[name.length - 1] === "'") {
|
|
983
|
-
return name.slice(1, -1);
|
|
1015
|
+
return name.slice(1, -1).split("''").join("'");
|
|
984
1016
|
}
|
|
985
1017
|
return name;
|
|
986
1018
|
}
|
|
@@ -1148,7 +1180,7 @@ var FormulaLinter = class {
|
|
|
1148
1180
|
case ERR_VALUE:
|
|
1149
1181
|
return this.hintValue(formula, sheet);
|
|
1150
1182
|
case ERR_REF:
|
|
1151
|
-
return
|
|
1183
|
+
return this.hintRef(formula);
|
|
1152
1184
|
case ERR_NAME:
|
|
1153
1185
|
return "The formula references an unknown function or has a syntax error. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.";
|
|
1154
1186
|
case ERR_DIV0:
|
|
@@ -1159,11 +1191,20 @@ var FormulaLinter = class {
|
|
|
1159
1191
|
return "Formula evaluation failed.";
|
|
1160
1192
|
}
|
|
1161
1193
|
}
|
|
1194
|
+
hintRef(formula) {
|
|
1195
|
+
for (const m of formula.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
|
|
1196
|
+
const name = this.cleanSheetName(m[1]);
|
|
1197
|
+
if (!this.sheetNames.has(name.toLowerCase())) {
|
|
1198
|
+
return `The formula refers to a sheet named '${name}', and this workbook has no such sheet. Its sheets are: ${[...this.sheetNames.values()].join(", ")}. Quote a name that contains spaces or punctuation: 'My Sheet'!A1.`;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
|
|
1202
|
+
}
|
|
1162
1203
|
hintValue(formula, sheet) {
|
|
1163
|
-
const re =
|
|
1204
|
+
const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
|
|
1164
1205
|
const offenders = [];
|
|
1165
1206
|
for (const m of formula.matchAll(re)) {
|
|
1166
|
-
const sheetName = m[1] ? m[1] : sheet;
|
|
1207
|
+
const sheetName = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
|
|
1167
1208
|
const a1 = m[2].toUpperCase() + m[3];
|
|
1168
1209
|
const key = sheetName + "!" + a1;
|
|
1169
1210
|
const cell = this.index.get(key);
|
|
@@ -1212,10 +1253,33 @@ function summarize(errors) {
|
|
|
1212
1253
|
const e = errors[0];
|
|
1213
1254
|
return `[holy-sheet] schema invalid at ${e.path}: expected ${e.expected}, got ${e.got}`;
|
|
1214
1255
|
}
|
|
1215
|
-
const
|
|
1256
|
+
const first2 = errors[0];
|
|
1216
1257
|
const rest = errors.length - 1;
|
|
1217
|
-
return `[holy-sheet] schema invalid at ${
|
|
1258
|
+
return `[holy-sheet] schema invalid at ${first2.path}: expected ${first2.expected}, got ${first2.got} (+${rest} more)`;
|
|
1218
1259
|
}
|
|
1260
|
+
var UnsupportedFormatException = class _UnsupportedFormatException extends Error {
|
|
1261
|
+
constructor(message, path = null, mimetype = null) {
|
|
1262
|
+
super(message);
|
|
1263
|
+
this.name = "UnsupportedFormatException";
|
|
1264
|
+
this.path = path;
|
|
1265
|
+
this.mimetype = mimetype;
|
|
1266
|
+
Object.setPrototypeOf(this, _UnsupportedFormatException.prototype);
|
|
1267
|
+
}
|
|
1268
|
+
static notAZip(path = null) {
|
|
1269
|
+
return new _UnsupportedFormatException(
|
|
1270
|
+
`[holy-sheet] cannot read ${path ?? "this file"}: it is not a zip archive, and xlsx and ods both are`,
|
|
1271
|
+
path
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
static unknownPackage(path, mimetype) {
|
|
1275
|
+
const what = mimetype !== null && mimetype !== "" ? `its mimetype is ${mimetype}` : "it has neither xl/workbook.xml nor an OpenDocument mimetype";
|
|
1276
|
+
return new _UnsupportedFormatException(
|
|
1277
|
+
`[holy-sheet] cannot read ${path ?? "this file"}: ${what}. Supported: xlsx, ods`,
|
|
1278
|
+
path,
|
|
1279
|
+
mimetype !== "" ? mimetype : null
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1219
1283
|
|
|
1220
1284
|
// src/schema/repairer.ts
|
|
1221
1285
|
var VALID_THEMES = ["default", "minimal", "plain", "business"];
|
|
@@ -1444,7 +1508,8 @@ var Validator = class {
|
|
|
1444
1508
|
}
|
|
1445
1509
|
}
|
|
1446
1510
|
if (sheet.cells != null) {
|
|
1447
|
-
|
|
1511
|
+
const emptyList = Array.isArray(sheet.cells) && sheet.cells.length === 0;
|
|
1512
|
+
if (!isPlainObject(sheet.cells) && !emptyList) {
|
|
1448
1513
|
errors.push(
|
|
1449
1514
|
error(
|
|
1450
1515
|
`${path}.cells`,
|
|
@@ -1589,13 +1654,13 @@ function zipSync(files) {
|
|
|
1589
1654
|
edv.setUint16(20, 0, true);
|
|
1590
1655
|
const out = new Uint8Array(localSize + centralSize + 22);
|
|
1591
1656
|
let p = 0;
|
|
1592
|
-
for (const
|
|
1593
|
-
out.set(
|
|
1594
|
-
p +=
|
|
1657
|
+
for (const part2 of localParts) {
|
|
1658
|
+
out.set(part2, p);
|
|
1659
|
+
p += part2.length;
|
|
1595
1660
|
}
|
|
1596
|
-
for (const
|
|
1597
|
-
out.set(
|
|
1598
|
-
p +=
|
|
1661
|
+
for (const part2 of centralParts) {
|
|
1662
|
+
out.set(part2, p);
|
|
1663
|
+
p += part2.length;
|
|
1599
1664
|
}
|
|
1600
1665
|
out.set(eocd, p);
|
|
1601
1666
|
return out;
|
|
@@ -1955,7 +2020,638 @@ function unzipSync(data) {
|
|
|
1955
2020
|
return result;
|
|
1956
2021
|
}
|
|
1957
2022
|
|
|
2023
|
+
// src/reader/ods/ns.ts
|
|
2024
|
+
var NS = {
|
|
2025
|
+
OFFICE: "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
|
2026
|
+
TABLE: "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
|
|
2027
|
+
TEXT: "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
|
2028
|
+
STYLE: "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
|
2029
|
+
FO: "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
|
2030
|
+
NUMBER: "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
|
|
2031
|
+
DC: "http://purl.org/dc/elements/1.1/",
|
|
2032
|
+
META: "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
|
|
2033
|
+
/** LibreOffice's extension namespace; carries `value-type="error"` for a formula error. */
|
|
2034
|
+
CALCEXT: "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0"
|
|
2035
|
+
};
|
|
2036
|
+
function child(node, ns, name) {
|
|
2037
|
+
return node?.children.find((c) => c.ns === ns && c.name === name);
|
|
2038
|
+
}
|
|
2039
|
+
function childrenIn(node, ns) {
|
|
2040
|
+
return node ? node.children.filter((c) => c.ns === ns) : [];
|
|
2041
|
+
}
|
|
2042
|
+
function attr(node, ns, name) {
|
|
2043
|
+
return node?.nsAttrs?.[`{${ns}}${name}`];
|
|
2044
|
+
}
|
|
2045
|
+
function ownText(node) {
|
|
2046
|
+
return node ? node.text : "";
|
|
2047
|
+
}
|
|
2048
|
+
function phpTrim(s) {
|
|
2049
|
+
return s.replace(/^[ \t\n\r\0\x0B]+|[ \t\n\r\0\x0B]+$/g, "");
|
|
2050
|
+
}
|
|
2051
|
+
function asciiLower(s) {
|
|
2052
|
+
return s.replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
2053
|
+
}
|
|
2054
|
+
function phpInt(s) {
|
|
2055
|
+
const m = /^[ \t\n\r\v\f]*([+-]?\d+)/.exec(s);
|
|
2056
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
2057
|
+
}
|
|
2058
|
+
function phpIsNumeric(s) {
|
|
2059
|
+
return /^[ \t\n\r\v\f]*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[ \t\n\r\v\f]*$/.test(s);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/reader/format-sniffer.ts
|
|
2063
|
+
var ODS_MIMETYPES = /* @__PURE__ */ new Set([
|
|
2064
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
2065
|
+
"application/vnd.oasis.opendocument.spreadsheet-template"
|
|
2066
|
+
]);
|
|
2067
|
+
var decoder2 = new TextDecoder();
|
|
2068
|
+
var FormatSniffer = {
|
|
2069
|
+
sniff(bytes, path = null) {
|
|
2070
|
+
let files;
|
|
2071
|
+
try {
|
|
2072
|
+
files = unzipSync(bytes);
|
|
2073
|
+
} catch (e) {
|
|
2074
|
+
if (e instanceof Error && e.message.includes("unsupported zip method")) throw e;
|
|
2075
|
+
throw UnsupportedFormatException.notAZip(path);
|
|
2076
|
+
}
|
|
2077
|
+
const mimetype = files["mimetype"] ? phpTrim(decoder2.decode(files["mimetype"])) : null;
|
|
2078
|
+
if (mimetype !== null && ODS_MIMETYPES.has(mimetype)) return { format: "ods", files };
|
|
2079
|
+
if (files["xl/workbook.xml"]) return { format: "xlsx", files };
|
|
2080
|
+
throw UnsupportedFormatException.unknownPackage(path, mimetype);
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
// src/reader/ods/ods-formula.ts
|
|
2085
|
+
function odsFormulaToA1(formula) {
|
|
2086
|
+
let grammar = "of";
|
|
2087
|
+
const m = /^(of|oooc|msoxl):/i.exec(formula);
|
|
2088
|
+
if (m) {
|
|
2089
|
+
grammar = m[1].toLowerCase();
|
|
2090
|
+
formula = formula.slice(m[0].length);
|
|
2091
|
+
}
|
|
2092
|
+
if (formula.startsWith("=")) formula = formula.slice(1);
|
|
2093
|
+
return grammar === "msoxl" ? formula : translate(formula);
|
|
2094
|
+
}
|
|
2095
|
+
function translate(s) {
|
|
2096
|
+
let out = "";
|
|
2097
|
+
const length = s.length;
|
|
2098
|
+
let arrayDepth = 0;
|
|
2099
|
+
for (let i = 0; i < length; i++) {
|
|
2100
|
+
let c = s[i];
|
|
2101
|
+
if (c === '"') {
|
|
2102
|
+
let end = i + 1;
|
|
2103
|
+
while (end < length) {
|
|
2104
|
+
if (s[end] === '"') {
|
|
2105
|
+
if (end + 1 < length && s[end + 1] === '"') {
|
|
2106
|
+
end += 2;
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
break;
|
|
2110
|
+
}
|
|
2111
|
+
end++;
|
|
2112
|
+
}
|
|
2113
|
+
out += s.slice(i, end + 1);
|
|
2114
|
+
i = end;
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
if (c === "[") {
|
|
2118
|
+
let end = i + 1;
|
|
2119
|
+
let quoted = false;
|
|
2120
|
+
while (end < length && (quoted || s[end] !== "]")) {
|
|
2121
|
+
if (s[end] === "'") quoted = !quoted;
|
|
2122
|
+
end++;
|
|
2123
|
+
}
|
|
2124
|
+
out += reference(s.slice(i + 1, end));
|
|
2125
|
+
i = end;
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (c === "{") {
|
|
2129
|
+
arrayDepth++;
|
|
2130
|
+
} else if (c === "}") {
|
|
2131
|
+
arrayDepth = Math.max(0, arrayDepth - 1);
|
|
2132
|
+
} else if (c === ";") {
|
|
2133
|
+
c = ",";
|
|
2134
|
+
} else if (c === "|" && arrayDepth > 0) {
|
|
2135
|
+
c = ";";
|
|
2136
|
+
}
|
|
2137
|
+
out += c;
|
|
2138
|
+
}
|
|
2139
|
+
return out;
|
|
2140
|
+
}
|
|
2141
|
+
function reference(ref) {
|
|
2142
|
+
const parts = [];
|
|
2143
|
+
let start = 0;
|
|
2144
|
+
let quoted = false;
|
|
2145
|
+
for (let i = 0; i < ref.length; i++) {
|
|
2146
|
+
if (ref[i] === "'") {
|
|
2147
|
+
quoted = !quoted;
|
|
2148
|
+
} else if (ref[i] === ":" && !quoted) {
|
|
2149
|
+
parts.push(ref.slice(start, i));
|
|
2150
|
+
start = i + 1;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
parts.push(ref.slice(start));
|
|
2154
|
+
const parsed = parts.length <= 2 ? parts.map(part) : [null];
|
|
2155
|
+
if (parsed.some((p) => p === null)) {
|
|
2156
|
+
return ref.includes("#REF!") ? "#REF!" : `[${ref}]`;
|
|
2157
|
+
}
|
|
2158
|
+
const [sheet, address] = parsed[0];
|
|
2159
|
+
if (parsed.length === 1) return sheetPrefix(sheet) + address;
|
|
2160
|
+
const [sheet2, address2] = parsed[1];
|
|
2161
|
+
if (sheet2 === null || sheet2 === sheet) return sheetPrefix(sheet) + address + ":" + address2;
|
|
2162
|
+
if (sheet === null) return address + ":" + sheetPrefix(sheet2) + address2;
|
|
2163
|
+
const both = needsQuotes(sheet) || needsQuotes(sheet2) ? "'" + sheet.replace(/'/g, "''") + ":" + sheet2.replace(/'/g, "''") + "'" : sheet + ":" + sheet2;
|
|
2164
|
+
return both + "!" + address + ":" + address2;
|
|
2165
|
+
}
|
|
2166
|
+
function part(p) {
|
|
2167
|
+
const length = p.length;
|
|
2168
|
+
let i = 0;
|
|
2169
|
+
if (i < length && p[i] === "$") i++;
|
|
2170
|
+
let sheet = null;
|
|
2171
|
+
if (i < length && p[i] === "'") {
|
|
2172
|
+
sheet = "";
|
|
2173
|
+
i++;
|
|
2174
|
+
for (; ; ) {
|
|
2175
|
+
if (i >= length) return null;
|
|
2176
|
+
if (p[i] === "'") {
|
|
2177
|
+
if (i + 1 < length && p[i + 1] === "'") {
|
|
2178
|
+
sheet += "'";
|
|
2179
|
+
i += 2;
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
i++;
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
sheet += p[i++];
|
|
2186
|
+
}
|
|
2187
|
+
if (i >= length || p[i] !== ".") return null;
|
|
2188
|
+
i++;
|
|
2189
|
+
} else {
|
|
2190
|
+
const dot = p.indexOf(".", i);
|
|
2191
|
+
if (dot < 0) return null;
|
|
2192
|
+
if (dot > i) sheet = p.slice(i, dot);
|
|
2193
|
+
i = dot + 1;
|
|
2194
|
+
}
|
|
2195
|
+
const address = p.slice(i);
|
|
2196
|
+
if (!/^(\$?[A-Za-z]{1,3}\$?[0-9]+|\$?[A-Za-z]{1,3}|\$?[0-9]+)$/.test(address)) return null;
|
|
2197
|
+
return [sheet, address];
|
|
2198
|
+
}
|
|
2199
|
+
function sheetPrefix(sheet) {
|
|
2200
|
+
if (sheet === null) return "";
|
|
2201
|
+
return (needsQuotes(sheet) ? "'" + sheet.replace(/'/g, "''") + "'" : sheet) + "!";
|
|
2202
|
+
}
|
|
2203
|
+
function needsQuotes(sheet) {
|
|
2204
|
+
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);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/reader/format/num-fmt-parser.ts
|
|
2208
|
+
var BUILTIN = {
|
|
2209
|
+
0: "General",
|
|
2210
|
+
1: "0",
|
|
2211
|
+
2: "0.00",
|
|
2212
|
+
3: "#,##0",
|
|
2213
|
+
4: "#,##0.00",
|
|
2214
|
+
9: "0%",
|
|
2215
|
+
10: "0.00%",
|
|
2216
|
+
11: "0.00E+00",
|
|
2217
|
+
12: "# ?/?",
|
|
2218
|
+
13: "# ??/??",
|
|
2219
|
+
14: "mm-dd-yy",
|
|
2220
|
+
15: "d-mmm-yy",
|
|
2221
|
+
16: "d-mmm",
|
|
2222
|
+
17: "mmm-yy",
|
|
2223
|
+
18: "h:mm AM/PM",
|
|
2224
|
+
19: "h:mm:ss AM/PM",
|
|
2225
|
+
20: "h:mm",
|
|
2226
|
+
21: "h:mm:ss",
|
|
2227
|
+
22: "m/d/yy h:mm",
|
|
2228
|
+
37: "#,##0 ;(#,##0)",
|
|
2229
|
+
38: "#,##0 ;[Red](#,##0)",
|
|
2230
|
+
39: "#,##0.00;(#,##0.00)",
|
|
2231
|
+
40: "#,##0.00;[Red](#,##0.00)",
|
|
2232
|
+
45: "mm:ss",
|
|
2233
|
+
46: "[h]:mm:ss",
|
|
2234
|
+
47: "mmss.0",
|
|
2235
|
+
48: "##0.0E+0",
|
|
2236
|
+
49: "@"
|
|
2237
|
+
};
|
|
2238
|
+
var SYMBOL_TO_ISO = {
|
|
2239
|
+
$: "USD",
|
|
2240
|
+
"\u20AC": "EUR",
|
|
2241
|
+
"\xA3": "GBP",
|
|
2242
|
+
"\xA5": "JPY",
|
|
2243
|
+
"\u20B9": "INR",
|
|
2244
|
+
"\u20A9": "KRW"
|
|
2245
|
+
};
|
|
2246
|
+
var NumFmtParser = {
|
|
2247
|
+
parse(formatCode) {
|
|
2248
|
+
if (formatCode === null) return null;
|
|
2249
|
+
const code = formatCode;
|
|
2250
|
+
if (code === "" || code === "General" || code === "@") {
|
|
2251
|
+
return { displayFormat: code === "@" ? "text" : "auto" };
|
|
2252
|
+
}
|
|
2253
|
+
if (looksLikeDate(code)) {
|
|
2254
|
+
return { displayFormat: hasTimeComponent(code) ? "datetime" : "date" };
|
|
2255
|
+
}
|
|
2256
|
+
if (code.includes("%")) {
|
|
2257
|
+
return { displayFormat: "percentage", decimals: decimalsAfter(code) };
|
|
2258
|
+
}
|
|
2259
|
+
const m = /^"([^"]+)"/u.exec(code);
|
|
2260
|
+
if (m) {
|
|
2261
|
+
const iso = SYMBOL_TO_ISO[m[1]];
|
|
2262
|
+
const result = { displayFormat: "currency", decimals: decimalsAfter(code) };
|
|
2263
|
+
if (iso !== void 0) result.currency = iso;
|
|
2264
|
+
return result;
|
|
2265
|
+
}
|
|
2266
|
+
if (/^#?,?#?#?0(\.0+)?$/.test(code.split(";")[0])) {
|
|
2267
|
+
return { displayFormat: "number", decimals: decimalsAfter(code) };
|
|
2268
|
+
}
|
|
2269
|
+
return null;
|
|
2270
|
+
},
|
|
2271
|
+
parseBuiltin(id) {
|
|
2272
|
+
const code = BUILTIN[id];
|
|
2273
|
+
if (code === void 0) return null;
|
|
2274
|
+
return NumFmtParser.parse(code);
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
function looksLikeDate(code) {
|
|
2278
|
+
const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
|
|
2279
|
+
return /[ymd]/i.test(stripped);
|
|
2280
|
+
}
|
|
2281
|
+
function hasTimeComponent(code) {
|
|
2282
|
+
const stripped = code.replace(/"[^"]*"|\[[^\]]*\]/gu, "");
|
|
2283
|
+
return /[hHsS]/.test(stripped) || stripped.toLowerCase().includes("mm:");
|
|
2284
|
+
}
|
|
2285
|
+
function decimalsAfter(code) {
|
|
2286
|
+
const section = code.split(";")[0];
|
|
2287
|
+
const m = /0\.(0+)/.exec(section);
|
|
2288
|
+
return m ? m[1].length : 0;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// src/reader/ods/ods-styles.ts
|
|
2292
|
+
var OdsStyles = class {
|
|
2293
|
+
constructor(content, styles) {
|
|
2294
|
+
this.automatic = /* @__PURE__ */ new Map();
|
|
2295
|
+
this.common = /* @__PURE__ */ new Map();
|
|
2296
|
+
this.defaultStyle = null;
|
|
2297
|
+
this.dataStyles = /* @__PURE__ */ new Map();
|
|
2298
|
+
this.resolved = /* @__PURE__ */ new Map();
|
|
2299
|
+
if (content) this.index(child(content, NS.OFFICE, "automatic-styles"), this.automatic);
|
|
2300
|
+
if (styles) {
|
|
2301
|
+
this.index(child(styles, NS.OFFICE, "styles"), this.common);
|
|
2302
|
+
this.index(child(styles, NS.OFFICE, "automatic-styles"), this.common);
|
|
2303
|
+
}
|
|
2304
|
+
this.defaultFontSize = this.properties("Default").fontSize;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* The format of one cell.
|
|
2308
|
+
*
|
|
2309
|
+
* @param styleName the cell's effective style (cell, row default, column default, or "Default")
|
|
2310
|
+
* @param valueType office:value-type
|
|
2311
|
+
* @param currency office:currency, the ISO code a currency value carries
|
|
2312
|
+
* @param dateHasTime whether an office:date-value carries a time of day
|
|
2313
|
+
*/
|
|
2314
|
+
cellFormat(styleName, valueType, currency, dateHasTime) {
|
|
2315
|
+
const p = this.properties(styleName);
|
|
2316
|
+
let data = p.data;
|
|
2317
|
+
const display = data?.displayFormat ?? null;
|
|
2318
|
+
switch (valueType) {
|
|
2319
|
+
case "date":
|
|
2320
|
+
data = { displayFormat: display === "date" || display === "datetime" ? display : dateHasTime ? "datetime" : "date" };
|
|
2321
|
+
break;
|
|
2322
|
+
case "time":
|
|
2323
|
+
data = { displayFormat: "datetime" };
|
|
2324
|
+
break;
|
|
2325
|
+
case "percentage":
|
|
2326
|
+
if (display !== "percentage") {
|
|
2327
|
+
data = { displayFormat: "percentage", ...data?.decimals !== void 0 ? { decimals: data.decimals } : {} };
|
|
2328
|
+
}
|
|
2329
|
+
break;
|
|
2330
|
+
case "currency":
|
|
2331
|
+
if (display !== "currency") {
|
|
2332
|
+
data = { displayFormat: "currency", ...data?.decimals !== void 0 ? { decimals: data.decimals } : {} };
|
|
2333
|
+
} else {
|
|
2334
|
+
data = { ...data };
|
|
2335
|
+
}
|
|
2336
|
+
if (currency !== null && currency !== "") data.currency = currency;
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
const format = new CellFormat({
|
|
2340
|
+
bold: p.bold,
|
|
2341
|
+
italic: p.italic,
|
|
2342
|
+
textAlign: p.textAlign,
|
|
2343
|
+
displayFormat: data?.displayFormat ?? null,
|
|
2344
|
+
decimals: data?.decimals ?? null,
|
|
2345
|
+
color: p.color,
|
|
2346
|
+
backgroundColor: p.backgroundColor,
|
|
2347
|
+
fontSize: p.fontSize !== null && p.fontSize !== this.defaultFontSize ? p.fontSize : null,
|
|
2348
|
+
borderTop: p.borderTop,
|
|
2349
|
+
borderRight: p.borderRight,
|
|
2350
|
+
borderBottom: p.borderBottom,
|
|
2351
|
+
borderLeft: p.borderLeft,
|
|
2352
|
+
currency: data?.currency ?? null
|
|
2353
|
+
});
|
|
2354
|
+
return format.isEmpty() ? null : format;
|
|
2355
|
+
}
|
|
2356
|
+
index(container, into) {
|
|
2357
|
+
if (!container) return;
|
|
2358
|
+
for (const el2 of childrenIn(container, NS.STYLE)) {
|
|
2359
|
+
const family = attr(el2, NS.STYLE, "family") ?? "";
|
|
2360
|
+
if (el2.name === "default-style" && family === "table-cell") {
|
|
2361
|
+
this.defaultStyle ?? (this.defaultStyle = el2);
|
|
2362
|
+
} else if (el2.name === "style" && family === "table-cell") {
|
|
2363
|
+
const name = attr(el2, NS.STYLE, "name") ?? "";
|
|
2364
|
+
if (!into.has(name)) into.set(name, el2);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
for (const el2 of childrenIn(container, NS.NUMBER)) {
|
|
2368
|
+
const name = attr(el2, NS.STYLE, "name") ?? "";
|
|
2369
|
+
if (!this.dataStyles.has(name)) this.dataStyles.set(name, el2);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/** The style and its ancestors, nearest first, ending with the default style. */
|
|
2373
|
+
chain(name) {
|
|
2374
|
+
const chain = [];
|
|
2375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2376
|
+
let el2 = this.automatic.get(name) ?? this.common.get(name);
|
|
2377
|
+
while (el2 !== void 0 && !seen.has(name)) {
|
|
2378
|
+
seen.add(name);
|
|
2379
|
+
chain.push(el2);
|
|
2380
|
+
name = attr(el2, NS.STYLE, "parent-style-name") ?? "";
|
|
2381
|
+
el2 = name === "" ? void 0 : this.common.get(name) ?? this.automatic.get(name);
|
|
2382
|
+
}
|
|
2383
|
+
if (this.defaultStyle) chain.push(this.defaultStyle);
|
|
2384
|
+
return chain;
|
|
2385
|
+
}
|
|
2386
|
+
properties(name) {
|
|
2387
|
+
const cached = this.resolved.get(name);
|
|
2388
|
+
if (cached) return cached;
|
|
2389
|
+
const chain = this.chain(name);
|
|
2390
|
+
const weight = first(chain, "text-properties", NS.FO, "font-weight");
|
|
2391
|
+
const style = first(chain, "text-properties", NS.FO, "font-style");
|
|
2392
|
+
const size = first(chain, "text-properties", NS.FO, "font-size");
|
|
2393
|
+
let dataStyle = null;
|
|
2394
|
+
for (const el2 of chain) {
|
|
2395
|
+
const candidate = attr(el2, NS.STYLE, "data-style-name") ?? "";
|
|
2396
|
+
if (candidate !== "") {
|
|
2397
|
+
dataStyle = candidate;
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
const sizeMatch = size !== null ? /^([0-9]*\.?[0-9]+)pt$/.exec(size) : null;
|
|
2402
|
+
const resolved = {
|
|
2403
|
+
bold: weight !== null && (weight === "bold" || phpIsNumeric(weight) && phpInt(weight) >= 600),
|
|
2404
|
+
italic: style === "italic" || style === "oblique",
|
|
2405
|
+
textAlign: textAlign(chain),
|
|
2406
|
+
color: color(chain),
|
|
2407
|
+
backgroundColor: background(chain),
|
|
2408
|
+
fontSize: sizeMatch ? Math.trunc(parseFloat(sizeMatch[1])) : null,
|
|
2409
|
+
borderTop: border(chain, "top"),
|
|
2410
|
+
borderRight: border(chain, "right"),
|
|
2411
|
+
borderBottom: border(chain, "bottom"),
|
|
2412
|
+
borderLeft: border(chain, "left"),
|
|
2413
|
+
data: this.dataFormat(dataStyle, 0)
|
|
2414
|
+
};
|
|
2415
|
+
this.resolved.set(name, resolved);
|
|
2416
|
+
return resolved;
|
|
2417
|
+
}
|
|
2418
|
+
/** A data style -> displayFormat, decimals, currency. */
|
|
2419
|
+
dataFormat(name, depth) {
|
|
2420
|
+
if (name === null || depth > 4) return null;
|
|
2421
|
+
const el2 = this.dataStyles.get(name);
|
|
2422
|
+
if (!el2) return null;
|
|
2423
|
+
for (const map of childrenIn(el2, NS.STYLE)) {
|
|
2424
|
+
if (map.name !== "map") continue;
|
|
2425
|
+
const condition = (attr(map, NS.STYLE, "condition") ?? "").replace(/[ \t\n\r\f\v]+/g, "");
|
|
2426
|
+
if (condition === "value()>=0" || condition === "value()>0") {
|
|
2427
|
+
const mapped = this.dataFormat(attr(map, NS.STYLE, "apply-style-name") ?? "", depth + 1);
|
|
2428
|
+
if (mapped !== null) return mapped;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
const parts = childrenIn(el2, NS.NUMBER);
|
|
2432
|
+
const has = (local) => parts.some((p) => p.name === local);
|
|
2433
|
+
const number = parts.find((p) => p.name === "number");
|
|
2434
|
+
let decimals = null;
|
|
2435
|
+
if (number) {
|
|
2436
|
+
const places = attr(number, NS.NUMBER, "decimal-places") ?? attr(number, NS.NUMBER, "min-decimal-places");
|
|
2437
|
+
decimals = places !== void 0 ? phpInt(places) : null;
|
|
2438
|
+
}
|
|
2439
|
+
switch (el2.name) {
|
|
2440
|
+
case "number-style": {
|
|
2441
|
+
if (has("scientific-number") || has("fraction")) return null;
|
|
2442
|
+
for (const text of parts.filter((p) => p.name === "text")) {
|
|
2443
|
+
const iso = currencyCode(ownText(text).replace(/[-()"\u00A0]/g, ""));
|
|
2444
|
+
if (iso !== null) return { displayFormat: "currency", decimals: decimals ?? 0, currency: iso };
|
|
2445
|
+
}
|
|
2446
|
+
if (!number) return null;
|
|
2447
|
+
return decimals === null ? { displayFormat: "auto" } : { displayFormat: "number", decimals };
|
|
2448
|
+
}
|
|
2449
|
+
case "percentage-style":
|
|
2450
|
+
return { displayFormat: "percentage", decimals: decimals ?? 0 };
|
|
2451
|
+
case "currency-style": {
|
|
2452
|
+
const out = { displayFormat: "currency", decimals: decimals ?? 0 };
|
|
2453
|
+
const symbol = parts.find((p) => p.name === "currency-symbol");
|
|
2454
|
+
const iso = symbol ? currencyCode(ownText(symbol)) : null;
|
|
2455
|
+
if (iso !== null) out.currency = iso;
|
|
2456
|
+
return out;
|
|
2457
|
+
}
|
|
2458
|
+
case "date-style":
|
|
2459
|
+
return ["hours", "minutes", "seconds", "am-pm"].some(has) ? { displayFormat: "datetime" } : { displayFormat: "date" };
|
|
2460
|
+
case "time-style":
|
|
2461
|
+
return { displayFormat: "datetime" };
|
|
2462
|
+
case "text-style":
|
|
2463
|
+
return { displayFormat: "text" };
|
|
2464
|
+
default:
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
function first(chain, properties, ns, name) {
|
|
2470
|
+
for (const el2 of chain) {
|
|
2471
|
+
const props = child(el2, NS.STYLE, properties);
|
|
2472
|
+
const value = attr(props, ns, name);
|
|
2473
|
+
if (value !== void 0) return phpTrim(value);
|
|
2474
|
+
}
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
function color(chain) {
|
|
2478
|
+
for (const el2 of chain) {
|
|
2479
|
+
const props = child(el2, NS.STYLE, "text-properties");
|
|
2480
|
+
if (!props) continue;
|
|
2481
|
+
if (attr(props, NS.STYLE, "use-window-font-color") === "true") return null;
|
|
2482
|
+
const value = attr(props, NS.FO, "color");
|
|
2483
|
+
if (value !== void 0) return hex(value);
|
|
2484
|
+
}
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
function background(chain) {
|
|
2488
|
+
const value = first(chain, "table-cell-properties", NS.FO, "background-color");
|
|
2489
|
+
return value === null || value === "transparent" ? null : hex(value);
|
|
2490
|
+
}
|
|
2491
|
+
function border(chain, side) {
|
|
2492
|
+
for (const el2 of chain) {
|
|
2493
|
+
const props = child(el2, NS.STYLE, "table-cell-properties");
|
|
2494
|
+
if (!props) continue;
|
|
2495
|
+
const raw = attr(props, NS.FO, `border-${side}`) ?? attr(props, NS.FO, "border");
|
|
2496
|
+
if (raw === void 0) continue;
|
|
2497
|
+
const value = asciiLower(phpTrim(raw));
|
|
2498
|
+
if (value === "" || /\b(none|hidden)\b/.test(value)) return null;
|
|
2499
|
+
const m = /#([0-9a-f]{6})\b/.exec(value);
|
|
2500
|
+
return m ? "#" + m[1].toUpperCase() : "#000000";
|
|
2501
|
+
}
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
function textAlign(chain) {
|
|
2505
|
+
if (first(chain, "table-cell-properties", NS.STYLE, "text-align-source") === "value-type") return null;
|
|
2506
|
+
const align = first(chain, "paragraph-properties", NS.FO, "text-align");
|
|
2507
|
+
if (align === null || align === "") return null;
|
|
2508
|
+
if (align === "start") return "left";
|
|
2509
|
+
if (align === "end") return "right";
|
|
2510
|
+
return align;
|
|
2511
|
+
}
|
|
2512
|
+
function hex(value) {
|
|
2513
|
+
const m = /^#([0-9a-fA-F]{6})$/.exec(phpTrim(value));
|
|
2514
|
+
return m ? "#" + m[1].toUpperCase() : null;
|
|
2515
|
+
}
|
|
2516
|
+
function currencyCode(symbol) {
|
|
2517
|
+
const s = phpTrim(symbol);
|
|
2518
|
+
if (s === "") return null;
|
|
2519
|
+
if (/^[A-Z]{3}$/.test(s)) return s;
|
|
2520
|
+
return SYMBOL_TO_ISO[s] ?? null;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// src/reader/ods/ods-text.ts
|
|
2524
|
+
var SKIPPED = /* @__PURE__ */ new Set(["annotation", "note"]);
|
|
2525
|
+
function paragraphs(node) {
|
|
2526
|
+
const lines = [];
|
|
2527
|
+
for (const c of node.children) {
|
|
2528
|
+
if (c.ns === NS.TEXT && (c.name === "p" || c.name === "h")) lines.push(paragraph(c));
|
|
2529
|
+
}
|
|
2530
|
+
return lines.length === 0 ? null : lines.join("\n");
|
|
2531
|
+
}
|
|
2532
|
+
function paragraph(p) {
|
|
2533
|
+
const state = { out: "", ignoreSpace: true };
|
|
2534
|
+
walk(p, state);
|
|
2535
|
+
return state.out;
|
|
2536
|
+
}
|
|
2537
|
+
function walk(node, state) {
|
|
2538
|
+
for (const item of node.content ?? []) {
|
|
2539
|
+
if (typeof item === "string") {
|
|
2540
|
+
appendCharacters(state, item);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (SKIPPED.has(item.name)) continue;
|
|
2544
|
+
if (item.name === "s") {
|
|
2545
|
+
const c = item.attrs["c"];
|
|
2546
|
+
const count = c !== void 0 && /^\d+$/.test(c) ? parseInt(c, 10) : 1;
|
|
2547
|
+
state.out += " ".repeat(Math.max(1, count));
|
|
2548
|
+
state.ignoreSpace = false;
|
|
2549
|
+
} else if (item.name === "tab") {
|
|
2550
|
+
state.out += " ";
|
|
2551
|
+
state.ignoreSpace = false;
|
|
2552
|
+
} else if (item.name === "line-break") {
|
|
2553
|
+
state.out += "\n";
|
|
2554
|
+
state.ignoreSpace = false;
|
|
2555
|
+
}
|
|
2556
|
+
walk(item, state);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
function appendCharacters(state, chars) {
|
|
2560
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2561
|
+
const c = chars[i];
|
|
2562
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
2563
|
+
if (!state.ignoreSpace) {
|
|
2564
|
+
state.out += " ";
|
|
2565
|
+
state.ignoreSpace = true;
|
|
2566
|
+
}
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
state.out += c;
|
|
2570
|
+
state.ignoreSpace = false;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// src/reader/format/date-inverter.ts
|
|
2575
|
+
var DateInverter = {
|
|
2576
|
+
toIso(serial2, includeTime = false) {
|
|
2577
|
+
const epoch = Date.UTC(1899, 11, 30, 0, 0, 0);
|
|
2578
|
+
const ms = epoch + Math.round(serial2 * 86400) * 1e3;
|
|
2579
|
+
const d = new Date(ms);
|
|
2580
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2581
|
+
const date = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
2582
|
+
if (!includeTime) return date;
|
|
2583
|
+
return `${date}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
|
|
2587
|
+
// src/reader/workbook-schema.ts
|
|
2588
|
+
var WorkbookSchema = {
|
|
2589
|
+
fromWorkbook(workbook) {
|
|
2590
|
+
const schema = { sheets: workbook.sheets.map(sheetToSchema) };
|
|
2591
|
+
if (Object.keys(workbook.meta).length > 0) schema["meta"] = workbook.meta;
|
|
2592
|
+
return schema;
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
function sheetToSchema(sheet) {
|
|
2596
|
+
const cells = {};
|
|
2597
|
+
for (const [address, cell] of sheet.cells) {
|
|
2598
|
+
const cellSchema = cellToSchema(cell);
|
|
2599
|
+
if (cellSchema === null) continue;
|
|
2600
|
+
cells[address] = cellSchema;
|
|
2601
|
+
}
|
|
2602
|
+
const out = { name: sheet.name, cells };
|
|
2603
|
+
if (sheet.mergedRegions.length > 0) {
|
|
2604
|
+
out["mergedRegions"] = sheet.mergedRegions.map((m) => ({ start: m.start, end: m.end }));
|
|
2605
|
+
}
|
|
2606
|
+
if (sheet.columnWidths.size > 0) {
|
|
2607
|
+
const widths = {};
|
|
2608
|
+
for (const [k, v] of sheet.columnWidths) widths[k] = v;
|
|
2609
|
+
out["columnWidths"] = widths;
|
|
2610
|
+
}
|
|
2611
|
+
if (sheet.frozenRows > 0) out["frozenRows"] = sheet.frozenRows;
|
|
2612
|
+
if (sheet.frozenCols > 0) out["frozenCols"] = sheet.frozenCols;
|
|
2613
|
+
return out;
|
|
2614
|
+
}
|
|
2615
|
+
function cellToSchema(cell) {
|
|
2616
|
+
let value = cell.value;
|
|
2617
|
+
const format = cell.format;
|
|
2618
|
+
if (format !== null && (format.displayFormat === "date" || format.displayFormat === "datetime") && isNumericString(value)) {
|
|
2619
|
+
value = DateInverter.toIso(Number(value), format.displayFormat === "datetime");
|
|
2620
|
+
}
|
|
2621
|
+
const out = { value };
|
|
2622
|
+
if (cell.formula !== null) out["formula"] = cell.formula;
|
|
2623
|
+
if (cell.cachedValue !== null) out["computedValue"] = cell.cachedValue;
|
|
2624
|
+
if (format !== null && !format.isEmpty()) out["format"] = formatToObject(format);
|
|
2625
|
+
if (cell.comment !== null) out["comment"] = commentToObject(cell.comment);
|
|
2626
|
+
if (Object.keys(out).length === 1 && out["value"] === null) return null;
|
|
2627
|
+
return out;
|
|
2628
|
+
}
|
|
2629
|
+
function formatToObject(f) {
|
|
2630
|
+
const out = {};
|
|
2631
|
+
if (f.bold) out["bold"] = true;
|
|
2632
|
+
if (f.italic) out["italic"] = true;
|
|
2633
|
+
if (f.textAlign !== null) out["textAlign"] = f.textAlign;
|
|
2634
|
+
if (f.displayFormat !== null) out["displayFormat"] = f.displayFormat;
|
|
2635
|
+
if (f.decimals !== null) out["decimals"] = f.decimals;
|
|
2636
|
+
if (f.color !== null) out["color"] = f.color;
|
|
2637
|
+
if (f.backgroundColor !== null) out["backgroundColor"] = f.backgroundColor;
|
|
2638
|
+
if (f.fontSize !== null) out["fontSize"] = f.fontSize;
|
|
2639
|
+
if (f.borderTop !== null) out["borderTop"] = f.borderTop;
|
|
2640
|
+
if (f.borderRight !== null) out["borderRight"] = f.borderRight;
|
|
2641
|
+
if (f.borderBottom !== null) out["borderBottom"] = f.borderBottom;
|
|
2642
|
+
if (f.borderLeft !== null) out["borderLeft"] = f.borderLeft;
|
|
2643
|
+
if (f.currency !== null) out["currency"] = f.currency;
|
|
2644
|
+
return out;
|
|
2645
|
+
}
|
|
2646
|
+
function commentToObject(c) {
|
|
2647
|
+
const out = { text: c.text };
|
|
2648
|
+
if (c.author !== null) out["author"] = c.author;
|
|
2649
|
+
if (c.color !== null) out["color"] = c.color;
|
|
2650
|
+
return out;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
1958
2653
|
// src/reader/xml.ts
|
|
2654
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
1959
2655
|
function localName(name) {
|
|
1960
2656
|
const idx = name.indexOf(":");
|
|
1961
2657
|
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
@@ -2004,19 +2700,28 @@ function parseTag(inner) {
|
|
|
2004
2700
|
while (i < trimmed.length && !/\s/.test(trimmed[i])) i++;
|
|
2005
2701
|
const name = trimmed.slice(0, i);
|
|
2006
2702
|
const attrs = {};
|
|
2703
|
+
const raw = [];
|
|
2007
2704
|
const rest = trimmed.slice(i);
|
|
2008
2705
|
for (const m of rest.matchAll(ATTR_RE)) {
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
|
|
2706
|
+
const rawValue = m[3] ?? m[4] ?? "";
|
|
2707
|
+
attrs[localName(m[1])] = unescapeXml(rawValue);
|
|
2708
|
+
raw.push([m[1], rawValue]);
|
|
2012
2709
|
}
|
|
2013
|
-
return { name, attrs };
|
|
2710
|
+
return { name, attrs, raw };
|
|
2014
2711
|
}
|
|
2015
|
-
function parseXml(src) {
|
|
2712
|
+
function parseXml(src, options = {}) {
|
|
2713
|
+
const namespaces = options.namespaces === true;
|
|
2016
2714
|
let i = 0;
|
|
2017
2715
|
const n = src.length;
|
|
2018
2716
|
const root = { name: "#root", attrs: {}, children: [], text: "" };
|
|
2717
|
+
if (namespaces) root.content = [];
|
|
2019
2718
|
const stack = [root];
|
|
2719
|
+
const scopes = [{ xml: XML_NS }];
|
|
2720
|
+
const appendText = (text) => {
|
|
2721
|
+
const top = stack[stack.length - 1];
|
|
2722
|
+
top.text += text;
|
|
2723
|
+
if (namespaces && text !== "") top.content.push(text);
|
|
2724
|
+
};
|
|
2020
2725
|
while (i < n) {
|
|
2021
2726
|
if (src[i] === "<") {
|
|
2022
2727
|
if (src.startsWith("<!--", i)) {
|
|
@@ -2026,7 +2731,7 @@ function parseXml(src) {
|
|
|
2026
2731
|
}
|
|
2027
2732
|
if (src.startsWith("<![CDATA[", i)) {
|
|
2028
2733
|
const end2 = src.indexOf("]]>", i);
|
|
2029
|
-
|
|
2734
|
+
appendText(src.slice(i + 9, end2 < 0 ? n : end2));
|
|
2030
2735
|
i = end2 < 0 ? n : end2 + 3;
|
|
2031
2736
|
continue;
|
|
2032
2737
|
}
|
|
@@ -2042,27 +2747,64 @@ function parseXml(src) {
|
|
|
2042
2747
|
}
|
|
2043
2748
|
if (src[i + 1] === "/") {
|
|
2044
2749
|
const end2 = src.indexOf(">", i);
|
|
2045
|
-
if (stack.length > 1)
|
|
2750
|
+
if (stack.length > 1) {
|
|
2751
|
+
stack.pop();
|
|
2752
|
+
if (namespaces) scopes.pop();
|
|
2753
|
+
}
|
|
2046
2754
|
i = end2 < 0 ? n : end2 + 1;
|
|
2047
2755
|
continue;
|
|
2048
2756
|
}
|
|
2049
2757
|
const end = findTagEnd(src, i + 1);
|
|
2050
2758
|
const tagContent = src.slice(i + 1, end);
|
|
2051
2759
|
const selfClosing = tagContent.endsWith("/");
|
|
2052
|
-
const { name, attrs } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
|
|
2760
|
+
const { name, attrs, raw } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
|
|
2053
2761
|
const node = { name: localName(name), attrs, children: [], text: "" };
|
|
2054
|
-
stack[stack.length - 1]
|
|
2055
|
-
|
|
2762
|
+
const parent = stack[stack.length - 1];
|
|
2763
|
+
parent.children.push(node);
|
|
2764
|
+
let scope;
|
|
2765
|
+
if (namespaces) {
|
|
2766
|
+
scope = resolveNamespaces(node, name, raw, scopes[scopes.length - 1]);
|
|
2767
|
+
parent.content.push(node);
|
|
2768
|
+
}
|
|
2769
|
+
if (!selfClosing) {
|
|
2770
|
+
stack.push(node);
|
|
2771
|
+
if (scope) scopes.push(scope);
|
|
2772
|
+
}
|
|
2056
2773
|
i = end + 1;
|
|
2057
2774
|
} else {
|
|
2058
2775
|
const next = src.indexOf("<", i);
|
|
2059
2776
|
const stop = next < 0 ? n : next;
|
|
2060
|
-
|
|
2777
|
+
appendText(unescapeXml(src.slice(i, stop)));
|
|
2061
2778
|
i = stop;
|
|
2062
2779
|
}
|
|
2063
2780
|
}
|
|
2064
2781
|
return root.children[0] ?? null;
|
|
2065
2782
|
}
|
|
2783
|
+
function resolveNamespaces(node, qname, raw, parentScope) {
|
|
2784
|
+
let scope = parentScope;
|
|
2785
|
+
for (const [key, value] of raw) {
|
|
2786
|
+
if (key === "xmlns" || key.startsWith("xmlns:")) {
|
|
2787
|
+
if (scope === parentScope) scope = { ...parentScope };
|
|
2788
|
+
scope[key === "xmlns" ? "" : key.slice(6)] = unescapeXml(value);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const colon = qname.indexOf(":");
|
|
2792
|
+
node.ns = scope[colon >= 0 ? qname.slice(0, colon) : ""] ?? "";
|
|
2793
|
+
node.content = [];
|
|
2794
|
+
const nsAttrs = {};
|
|
2795
|
+
for (const [key, value] of raw) {
|
|
2796
|
+
if (key === "xmlns" || key.startsWith("xmlns:")) continue;
|
|
2797
|
+
const idx = key.indexOf(":");
|
|
2798
|
+
const normalised = unescapeXml(value.replace(/[\t\n\r]/g, " "));
|
|
2799
|
+
if (idx < 0) {
|
|
2800
|
+
nsAttrs[key] = normalised;
|
|
2801
|
+
} else {
|
|
2802
|
+
nsAttrs[`{${scope[key.slice(0, idx)] ?? ""}}${key.slice(idx + 1)}`] = normalised;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
node.nsAttrs = nsAttrs;
|
|
2806
|
+
return scope;
|
|
2807
|
+
}
|
|
2066
2808
|
function el(node, name) {
|
|
2067
2809
|
return node?.children.find((c) => c.name === name);
|
|
2068
2810
|
}
|
|
@@ -2073,6 +2815,259 @@ function at(node, name) {
|
|
|
2073
2815
|
return node?.attrs[name];
|
|
2074
2816
|
}
|
|
2075
2817
|
|
|
2818
|
+
// src/reader/ods-reader.ts
|
|
2819
|
+
var MAX_ROWS = 1048576;
|
|
2820
|
+
var MAX_COLUMNS = 16384;
|
|
2821
|
+
var UNIX_EPOCH_SERIAL = 25569;
|
|
2822
|
+
var decoder3 = new TextDecoder();
|
|
2823
|
+
var OdsReader = class {
|
|
2824
|
+
describe(bytes) {
|
|
2825
|
+
return WorkbookSchema.fromWorkbook(this.readWorkbook(bytes));
|
|
2826
|
+
}
|
|
2827
|
+
readWorkbook(bytes) {
|
|
2828
|
+
return this.readFiles(unzipSync(bytes));
|
|
2829
|
+
}
|
|
2830
|
+
/** Read an already-unzipped package (name -> bytes). */
|
|
2831
|
+
readFiles(files) {
|
|
2832
|
+
const getText = (name) => files[name] ? decoder3.decode(files[name]) : null;
|
|
2833
|
+
const content = getText("content.xml");
|
|
2834
|
+
if (content === null) throw new Error("[holy-sheet] missing content.xml");
|
|
2835
|
+
const contentDoc = parseXml(content, { namespaces: true });
|
|
2836
|
+
if (!contentDoc) throw new Error("[holy-sheet] failed to parse content.xml");
|
|
2837
|
+
const stylesText = getText("styles.xml");
|
|
2838
|
+
const stylesDoc = stylesText === null ? null : parseXml(stylesText, { namespaces: true });
|
|
2839
|
+
const styles = new OdsStyles(contentDoc, stylesDoc);
|
|
2840
|
+
const sheets = [];
|
|
2841
|
+
const spreadsheet = child(child(contentDoc, NS.OFFICE, "body"), NS.OFFICE, "spreadsheet");
|
|
2842
|
+
for (const table of spreadsheet?.children ?? []) {
|
|
2843
|
+
if (table.ns === NS.TABLE && table.name === "table") sheets.push(this.readSheet(table, styles));
|
|
2844
|
+
}
|
|
2845
|
+
const meta = getText("meta.xml");
|
|
2846
|
+
return new Workbook(sheets, meta === null ? {} : readMeta(meta));
|
|
2847
|
+
}
|
|
2848
|
+
readSheet(table, styles) {
|
|
2849
|
+
const columns = [];
|
|
2850
|
+
collectColumns(table, columns, { next: 0 });
|
|
2851
|
+
const state = { row: 0, cells: /* @__PURE__ */ new Map(), merges: [] };
|
|
2852
|
+
this.walkRows(table, styles, columns, state);
|
|
2853
|
+
return new Sheet(attr(table, NS.TABLE, "name") ?? "", state.cells, state.merges);
|
|
2854
|
+
}
|
|
2855
|
+
walkRows(container, styles, columns, state) {
|
|
2856
|
+
for (const el2 of container.children) {
|
|
2857
|
+
if (state.row >= MAX_ROWS) return;
|
|
2858
|
+
if (el2.ns !== NS.TABLE) continue;
|
|
2859
|
+
if (el2.name === "table-row") {
|
|
2860
|
+
this.readRow(el2, styles, columns, state);
|
|
2861
|
+
} else if (el2.name === "table-header-rows" || el2.name === "table-rows" || el2.name === "table-row-group") {
|
|
2862
|
+
this.walkRows(el2, styles, columns, state);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
readRow(row, styles, columns, state) {
|
|
2867
|
+
const repeat = Math.max(1, phpInt(attr(row, NS.TABLE, "number-rows-repeated") ?? "1"));
|
|
2868
|
+
const rowStyle = attr(row, NS.TABLE, "default-cell-style-name") ?? "";
|
|
2869
|
+
const entries = [];
|
|
2870
|
+
let column = 0;
|
|
2871
|
+
for (const cell of row.children) {
|
|
2872
|
+
if (cell.ns !== NS.TABLE || cell.name !== "table-cell" && cell.name !== "covered-table-cell") continue;
|
|
2873
|
+
const span = cell.name === "table-cell" ? [
|
|
2874
|
+
Math.max(1, phpInt(attr(cell, NS.TABLE, "number-columns-spanned") ?? "1")),
|
|
2875
|
+
Math.max(1, phpInt(attr(cell, NS.TABLE, "number-rows-spanned") ?? "1"))
|
|
2876
|
+
] : [1, 1];
|
|
2877
|
+
const read = hasContent(cell) ? readCell(cell) : null;
|
|
2878
|
+
const count = Math.max(1, phpInt(attr(cell, NS.TABLE, "number-columns-repeated") ?? "1"));
|
|
2879
|
+
if (read !== null || span[0] !== 1 || span[1] !== 1) {
|
|
2880
|
+
const style = attr(cell, NS.TABLE, "style-name") ?? "";
|
|
2881
|
+
for (let i = 0; i < count && column + i < MAX_COLUMNS; i++) {
|
|
2882
|
+
entries.push([column + i, read, span, style]);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
column += count;
|
|
2886
|
+
if (column >= MAX_COLUMNS) break;
|
|
2887
|
+
}
|
|
2888
|
+
if (entries.length === 0) {
|
|
2889
|
+
state.row += repeat;
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
for (let r = 0; r < repeat && state.row < MAX_ROWS; r++) {
|
|
2893
|
+
const rowNumber = state.row + 1;
|
|
2894
|
+
for (const [col, read, span, style] of entries) {
|
|
2895
|
+
const address = CellAddress.letter(col) + rowNumber;
|
|
2896
|
+
if (span[0] !== 1 || span[1] !== 1) {
|
|
2897
|
+
state.merges.push(
|
|
2898
|
+
new MergedRegion(
|
|
2899
|
+
address,
|
|
2900
|
+
CellAddress.letter(Math.min(col + span[0], MAX_COLUMNS) - 1) + Math.min(rowNumber + span[1] - 1, MAX_ROWS)
|
|
2901
|
+
)
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
if (read === null) continue;
|
|
2905
|
+
const styleName = style !== "" ? style : rowStyle !== "" ? rowStyle : columnStyle(columns, col);
|
|
2906
|
+
const format = styles.cellFormat(styleName, read.type, read.currency, read.dateHasTime);
|
|
2907
|
+
let value = read.value;
|
|
2908
|
+
if (read.seconds !== null) {
|
|
2909
|
+
value = isoFromSeconds(read.seconds, format?.displayFormat !== "date");
|
|
2910
|
+
}
|
|
2911
|
+
state.cells.set(address, new Cell(address, value, read.formula, format, read.comment, read.cached));
|
|
2912
|
+
}
|
|
2913
|
+
state.row++;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
function collectColumns(container, columns, cursor) {
|
|
2918
|
+
for (const el2 of container.children) {
|
|
2919
|
+
if (el2.ns === NS.TABLE && el2.name === "table-column") {
|
|
2920
|
+
const repeat = Math.max(1, phpInt(attr(el2, NS.TABLE, "number-columns-repeated") ?? "1"));
|
|
2921
|
+
const style = attr(el2, NS.TABLE, "default-cell-style-name") ?? "";
|
|
2922
|
+
if (style !== "") columns.push([cursor.next, cursor.next + repeat - 1, style]);
|
|
2923
|
+
cursor.next += repeat;
|
|
2924
|
+
} else if (el2.ns === NS.TABLE && (el2.name === "table-columns" || el2.name === "table-header-columns" || el2.name === "table-column-group")) {
|
|
2925
|
+
collectColumns(el2, columns, cursor);
|
|
2926
|
+
}
|
|
2927
|
+
if (cursor.next >= MAX_COLUMNS) return;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
function columnStyle(columns, column) {
|
|
2931
|
+
for (const [first2, last, style] of columns) {
|
|
2932
|
+
if (column >= first2 && column <= last) return style;
|
|
2933
|
+
}
|
|
2934
|
+
return "Default";
|
|
2935
|
+
}
|
|
2936
|
+
function hasContent(cell) {
|
|
2937
|
+
if (attr(cell, NS.OFFICE, "value-type") !== void 0) return true;
|
|
2938
|
+
if (attr(cell, NS.TABLE, "formula") !== void 0) return true;
|
|
2939
|
+
if (child(cell, NS.OFFICE, "annotation") !== void 0) return true;
|
|
2940
|
+
return (paragraphs(cell) ?? "") !== "";
|
|
2941
|
+
}
|
|
2942
|
+
function readCell(cell) {
|
|
2943
|
+
const type = attr(cell, NS.OFFICE, "value-type") ?? null;
|
|
2944
|
+
const text = paragraphs(cell);
|
|
2945
|
+
let value = null;
|
|
2946
|
+
let seconds = null;
|
|
2947
|
+
let dateHasTime = false;
|
|
2948
|
+
switch (type) {
|
|
2949
|
+
case "float":
|
|
2950
|
+
case "percentage":
|
|
2951
|
+
case "currency":
|
|
2952
|
+
value = toNumber(attr(cell, NS.OFFICE, "value") ?? "");
|
|
2953
|
+
break;
|
|
2954
|
+
case "boolean": {
|
|
2955
|
+
const b = asciiLower(phpTrim(attr(cell, NS.OFFICE, "boolean-value") ?? ""));
|
|
2956
|
+
value = b === "true" || b === "1";
|
|
2957
|
+
break;
|
|
2958
|
+
}
|
|
2959
|
+
case "date": {
|
|
2960
|
+
const raw = phpTrim(attr(cell, NS.OFFICE, "date-value") ?? "");
|
|
2961
|
+
seconds = dateSeconds(raw);
|
|
2962
|
+
dateHasTime = raw.includes("T");
|
|
2963
|
+
break;
|
|
2964
|
+
}
|
|
2965
|
+
case "time": {
|
|
2966
|
+
const duration = durationSeconds(attr(cell, NS.OFFICE, "time-value") ?? "");
|
|
2967
|
+
seconds = duration === null ? null : duration - UNIX_EPOCH_SERIAL * 86400;
|
|
2968
|
+
break;
|
|
2969
|
+
}
|
|
2970
|
+
case "string": {
|
|
2971
|
+
const error2 = attr(cell, NS.CALCEXT, "value-type") === "error";
|
|
2972
|
+
const stored = attr(cell, NS.OFFICE, "string-value");
|
|
2973
|
+
value = stored !== void 0 && !error2 ? stored : text ?? "";
|
|
2974
|
+
break;
|
|
2975
|
+
}
|
|
2976
|
+
default:
|
|
2977
|
+
value = text !== null && text !== "" ? text : null;
|
|
2978
|
+
}
|
|
2979
|
+
let formula = null;
|
|
2980
|
+
let cached = null;
|
|
2981
|
+
const formulaAttr = attr(cell, NS.TABLE, "formula");
|
|
2982
|
+
if (formulaAttr !== void 0) {
|
|
2983
|
+
const translated = odsFormulaToA1(formulaAttr);
|
|
2984
|
+
const literalBoolean = type === "boolean" && /^[ \t\n\r\f\v]*(TRUE|FALSE)\(\)[ \t\n\r\f\v]*$/i.test(translated);
|
|
2985
|
+
if (!literalBoolean) {
|
|
2986
|
+
formula = translated;
|
|
2987
|
+
cached = seconds !== null ? serial(seconds) : value;
|
|
2988
|
+
value = null;
|
|
2989
|
+
seconds = null;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
let comment = null;
|
|
2993
|
+
const annotation = child(cell, NS.OFFICE, "annotation");
|
|
2994
|
+
if (annotation) {
|
|
2995
|
+
const creator = child(annotation, NS.DC, "creator");
|
|
2996
|
+
const author = creator ? phpTrim(creator.text) : "";
|
|
2997
|
+
comment = new CellComment(paragraphs(annotation) ?? "", author === "" ? null : author);
|
|
2998
|
+
}
|
|
2999
|
+
const currency = attr(cell, NS.OFFICE, "currency");
|
|
3000
|
+
return {
|
|
3001
|
+
value,
|
|
3002
|
+
formula,
|
|
3003
|
+
cached,
|
|
3004
|
+
type,
|
|
3005
|
+
currency: currency !== void 0 ? phpTrim(currency) : null,
|
|
3006
|
+
dateHasTime,
|
|
3007
|
+
comment,
|
|
3008
|
+
seconds
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
function toNumber(raw) {
|
|
3012
|
+
const s = phpTrim(raw);
|
|
3013
|
+
if (s === "") return null;
|
|
3014
|
+
if (/^-?\d+$/.test(s)) return parseInt(s, 10);
|
|
3015
|
+
return phpIsNumeric(s) ? Number(s) : null;
|
|
3016
|
+
}
|
|
3017
|
+
function dateSeconds(raw) {
|
|
3018
|
+
const m = /^(-?\d{4,})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?)?(Z|[+-]\d{2}:?\d{2})?$/.exec(raw);
|
|
3019
|
+
if (!m) return null;
|
|
3020
|
+
const days = daysFromCivil(parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10));
|
|
3021
|
+
let seconds = days * 86400 + phpInt(m[4] ?? "0") * 3600 + phpInt(m[5] ?? "0") * 60 + phpInt(m[6] ?? "0");
|
|
3022
|
+
const fraction = m[7] !== void 0 && m[7] !== "" ? Number("0" + m[7]) : 0;
|
|
3023
|
+
const zone = m[8] ?? "";
|
|
3024
|
+
if (zone !== "" && zone !== "Z") {
|
|
3025
|
+
const digits = zone.slice(1).replace(":", "");
|
|
3026
|
+
const offset = phpInt(digits.slice(0, 2)) * 3600 + phpInt(digits.slice(2, 4)) * 60;
|
|
3027
|
+
seconds -= zone[0] === "-" ? -offset : offset;
|
|
3028
|
+
}
|
|
3029
|
+
return seconds + Math.round(fraction);
|
|
3030
|
+
}
|
|
3031
|
+
function durationSeconds(raw) {
|
|
3032
|
+
const m = /^(-)?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(phpTrim(raw));
|
|
3033
|
+
if (!m) return null;
|
|
3034
|
+
const total = phpInt(m[2] ?? "0") * 86400 + phpInt(m[3] ?? "0") * 3600 + phpInt(m[4] ?? "0") * 60 + (m[5] ? Number(m[5]) : 0);
|
|
3035
|
+
const seconds = Math.round(total);
|
|
3036
|
+
return m[1] === "-" ? -seconds : seconds;
|
|
3037
|
+
}
|
|
3038
|
+
function daysFromCivil(year, month, day) {
|
|
3039
|
+
year -= month <= 2 ? 1 : 0;
|
|
3040
|
+
const era = Math.trunc((year >= 0 ? year : year - 399) / 400);
|
|
3041
|
+
const yoe = year - era * 400;
|
|
3042
|
+
const doy = Math.trunc((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
|
|
3043
|
+
const doe = yoe * 365 + Math.trunc(yoe / 4) - Math.trunc(yoe / 100) + doy;
|
|
3044
|
+
return era * 146097 + doe - 719468;
|
|
3045
|
+
}
|
|
3046
|
+
function serial(seconds) {
|
|
3047
|
+
return (seconds + UNIX_EPOCH_SERIAL * 86400) / 86400;
|
|
3048
|
+
}
|
|
3049
|
+
function isoFromSeconds(seconds, withTime) {
|
|
3050
|
+
const d = new Date(seconds * 1e3);
|
|
3051
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
3052
|
+
const date = `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
3053
|
+
if (!withTime) return date;
|
|
3054
|
+
return `${date}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
3055
|
+
}
|
|
3056
|
+
function readMeta(xml) {
|
|
3057
|
+
const doc = parseXml(xml, { namespaces: true });
|
|
3058
|
+
const meta = child(doc, NS.OFFICE, "meta");
|
|
3059
|
+
if (!meta) return {};
|
|
3060
|
+
const out = {};
|
|
3061
|
+
const creator = child(meta, NS.META, "initial-creator") ?? child(meta, NS.DC, "creator");
|
|
3062
|
+
if (creator && phpTrim(creator.text) !== "") out["creator"] = phpTrim(creator.text);
|
|
3063
|
+
const created = child(meta, NS.META, "creation-date");
|
|
3064
|
+
if (created && phpTrim(created.text) !== "") {
|
|
3065
|
+
const value = phpTrim(created.text);
|
|
3066
|
+
out["created"] = /(Z|[+-]\d{2}:?\d{2})$/.test(value) ? value : value + "Z";
|
|
3067
|
+
}
|
|
3068
|
+
return out;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2076
3071
|
// src/reader/comments-parser.ts
|
|
2077
3072
|
var CommentsParser = {
|
|
2078
3073
|
parse(commentsXml) {
|
|
@@ -2097,19 +3092,6 @@ function extractText(textNode) {
|
|
|
2097
3092
|
return parts.join("");
|
|
2098
3093
|
}
|
|
2099
3094
|
|
|
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
3095
|
// src/reader/rels-parser.ts
|
|
2114
3096
|
var RelsParser = {
|
|
2115
3097
|
parse(relsXml) {
|
|
@@ -2145,90 +3127,6 @@ function renderSi(si) {
|
|
|
2145
3127
|
return els(si, "r").map((run) => el(run, "t")?.text ?? "").join("");
|
|
2146
3128
|
}
|
|
2147
3129
|
|
|
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
3130
|
// src/reader/styles-parser.ts
|
|
2233
3131
|
var StylesParser = {
|
|
2234
3132
|
parse(stylesXml) {
|
|
@@ -2248,14 +3146,14 @@ var StylesParser = {
|
|
|
2248
3146
|
const align = alignment && alignment.attrs["horizontal"] !== void 0 ? alignment.attrs["horizontal"] : null;
|
|
2249
3147
|
const font = fonts[fontId] ?? null;
|
|
2250
3148
|
const fill = fills[fillId] ?? null;
|
|
2251
|
-
const
|
|
3149
|
+
const border2 = borders[borderId] ?? null;
|
|
2252
3150
|
const customCode = numFmts[numFmtId];
|
|
2253
3151
|
const numFmtParsed = customCode !== void 0 ? NumFmtParser.parse(customCode) : NumFmtParser.parseBuiltin(numFmtId);
|
|
2254
|
-
if (xfs.length === 0 && font === null && fill === null &&
|
|
3152
|
+
if (xfs.length === 0 && font === null && fill === null && border2 === null && !numFmtParsed && align === null) {
|
|
2255
3153
|
xfs.push(null);
|
|
2256
3154
|
continue;
|
|
2257
3155
|
}
|
|
2258
|
-
xfs.push(buildCellFormat(font, fill,
|
|
3156
|
+
xfs.push(buildCellFormat(font, fill, border2, numFmtParsed, align));
|
|
2259
3157
|
}
|
|
2260
3158
|
return xfs;
|
|
2261
3159
|
}
|
|
@@ -2270,12 +3168,12 @@ function parseNumFmts(xml) {
|
|
|
2270
3168
|
function parseFonts(xml) {
|
|
2271
3169
|
return els(el(xml, "fonts"), "font").map((f) => {
|
|
2272
3170
|
const sz = el(f, "sz");
|
|
2273
|
-
const
|
|
3171
|
+
const color2 = el(f, "color");
|
|
2274
3172
|
return {
|
|
2275
3173
|
bold: !!el(f, "b"),
|
|
2276
3174
|
italic: !!el(f, "i"),
|
|
2277
3175
|
size: sz ? Math.trunc(Number(at(sz, "val"))) : 11,
|
|
2278
|
-
color:
|
|
3176
|
+
color: color2 && at(color2, "rgb") !== void 0 ? argbToHex(at(color2, "rgb")) : null
|
|
2279
3177
|
};
|
|
2280
3178
|
});
|
|
2281
3179
|
}
|
|
@@ -2293,15 +3191,15 @@ function parseBorders(xml) {
|
|
|
2293
3191
|
const rec = { top: null, right: null, bottom: null, left: null };
|
|
2294
3192
|
for (const side of ["top", "right", "bottom", "left"]) {
|
|
2295
3193
|
const sideEl = el(b, side);
|
|
2296
|
-
const
|
|
2297
|
-
if (sideEl && at(sideEl, "style") !== void 0 && at(sideEl, "style") !== "" &&
|
|
2298
|
-
rec[side] = argbToHex(at(
|
|
3194
|
+
const color2 = el(sideEl, "color");
|
|
3195
|
+
if (sideEl && at(sideEl, "style") !== void 0 && at(sideEl, "style") !== "" && color2 && at(color2, "rgb") !== void 0) {
|
|
3196
|
+
rec[side] = argbToHex(at(color2, "rgb"));
|
|
2299
3197
|
}
|
|
2300
3198
|
}
|
|
2301
3199
|
return rec;
|
|
2302
3200
|
});
|
|
2303
3201
|
}
|
|
2304
|
-
function buildCellFormat(font, fill,
|
|
3202
|
+
function buildCellFormat(font, fill, border2, numFmt, align) {
|
|
2305
3203
|
return new CellFormat({
|
|
2306
3204
|
bold: font?.bold ?? false,
|
|
2307
3205
|
italic: font?.italic ?? false,
|
|
@@ -2311,10 +3209,10 @@ function buildCellFormat(font, fill, border, numFmt, align) {
|
|
|
2311
3209
|
color: font?.color ?? null,
|
|
2312
3210
|
backgroundColor: fill,
|
|
2313
3211
|
fontSize: font && font.size !== 11 ? font.size : null,
|
|
2314
|
-
borderTop:
|
|
2315
|
-
borderRight:
|
|
2316
|
-
borderBottom:
|
|
2317
|
-
borderLeft:
|
|
3212
|
+
borderTop: border2?.top ?? null,
|
|
3213
|
+
borderRight: border2?.right ?? null,
|
|
3214
|
+
borderBottom: border2?.bottom ?? null,
|
|
3215
|
+
borderLeft: border2?.left ?? null,
|
|
2318
3216
|
currency: numFmt?.currency ?? null
|
|
2319
3217
|
});
|
|
2320
3218
|
}
|
|
@@ -2414,14 +3312,17 @@ function parseFrozen(xml) {
|
|
|
2414
3312
|
}
|
|
2415
3313
|
|
|
2416
3314
|
// src/reader/xlsx-reader.ts
|
|
2417
|
-
var
|
|
3315
|
+
var decoder4 = new TextDecoder();
|
|
2418
3316
|
var XlsxReader = class {
|
|
2419
3317
|
describe(bytes) {
|
|
2420
|
-
return
|
|
3318
|
+
return WorkbookSchema.fromWorkbook(this.readWorkbook(bytes));
|
|
2421
3319
|
}
|
|
2422
3320
|
readWorkbook(bytes) {
|
|
2423
|
-
|
|
2424
|
-
|
|
3321
|
+
return this.readFiles(unzipSync(bytes));
|
|
3322
|
+
}
|
|
3323
|
+
/** Read an already-unzipped package (name -> bytes), so a caller that sniffed it does not unzip twice. */
|
|
3324
|
+
readFiles(files) {
|
|
3325
|
+
const getText = (name) => files[name] ? decoder4.decode(files[name]) : null;
|
|
2425
3326
|
const stylesXml = getText("xl/styles.xml");
|
|
2426
3327
|
const stylesIndex = stylesXml ? StylesParser.parse(stylesXml) : [];
|
|
2427
3328
|
const sharedStringsXml = getText("xl/sharedStrings.xml");
|
|
@@ -2470,69 +3371,7 @@ var XlsxReader = class {
|
|
|
2470
3371
|
if (created) meta["created"] = created.text;
|
|
2471
3372
|
return meta;
|
|
2472
3373
|
}
|
|
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
3374
|
};
|
|
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
3375
|
function resolveRelativePath(base, target) {
|
|
2537
3376
|
const baseDir = base.includes("/") ? base.slice(0, base.lastIndexOf("/")) : ".";
|
|
2538
3377
|
const combined = baseDir + "/" + target;
|
|
@@ -2736,11 +3575,11 @@ var StylesRegistry = class {
|
|
|
2736
3575
|
for (const b of this.borders) {
|
|
2737
3576
|
bordersXml += "<border>";
|
|
2738
3577
|
for (const side of ["left", "right", "top", "bottom"]) {
|
|
2739
|
-
const
|
|
2740
|
-
if (!
|
|
3578
|
+
const color2 = b[side];
|
|
3579
|
+
if (!color2) {
|
|
2741
3580
|
bordersXml += `<${side}/>`;
|
|
2742
3581
|
} else {
|
|
2743
|
-
bordersXml += `<${side} style="thin"><color rgb="${hexToArgb(
|
|
3582
|
+
bordersXml += `<${side} style="thin"><color rgb="${hexToArgb(color2)}"/></${side}>`;
|
|
2744
3583
|
}
|
|
2745
3584
|
}
|
|
2746
3585
|
bordersXml += "<diagonal/></border>";
|
|
@@ -2762,8 +3601,8 @@ var StylesRegistry = class {
|
|
|
2762
3601
|
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
3602
|
}
|
|
2764
3603
|
};
|
|
2765
|
-
function hexToArgb(
|
|
2766
|
-
const h =
|
|
3604
|
+
function hexToArgb(hex2) {
|
|
3605
|
+
const h = hex2.replace(/^#/, "");
|
|
2767
3606
|
if (h.length === 6) return "FF" + h.toUpperCase();
|
|
2768
3607
|
if (h.length === 8) return h.toUpperCase();
|
|
2769
3608
|
return "FF000000";
|
|
@@ -2956,23 +3795,32 @@ var holy_sheet_schema_default = {
|
|
|
2956
3795
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2957
3796
|
$id: "https://github.com/Particle-Academy/holy-sheet/skills/holy-sheet.schema.json",
|
|
2958
3797
|
title: "Holy Sheet workbook schema",
|
|
2959
|
-
description: "Input format for HolySheet\\Agent::write(). Describes a multi-sheet xlsx workbook agentically.
|
|
3798
|
+
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
3799
|
type: "object",
|
|
2961
|
-
required: [
|
|
3800
|
+
required: [
|
|
3801
|
+
"sheets"
|
|
3802
|
+
],
|
|
2962
3803
|
additionalProperties: false,
|
|
2963
3804
|
properties: {
|
|
2964
3805
|
sheets: {
|
|
2965
3806
|
type: "array",
|
|
2966
3807
|
minItems: 1,
|
|
2967
3808
|
description: "One or more sheets. The first sheet is shown when the file opens.",
|
|
2968
|
-
items: {
|
|
3809
|
+
items: {
|
|
3810
|
+
$ref: "#/definitions/Sheet"
|
|
3811
|
+
}
|
|
2969
3812
|
},
|
|
2970
3813
|
meta: {
|
|
2971
3814
|
type: "object",
|
|
2972
3815
|
description: "Optional workbook metadata surfaced in Excel's File \u2192 Info pane.",
|
|
2973
3816
|
properties: {
|
|
2974
|
-
creator: {
|
|
2975
|
-
|
|
3817
|
+
creator: {
|
|
3818
|
+
type: "string"
|
|
3819
|
+
},
|
|
3820
|
+
created: {
|
|
3821
|
+
type: "string",
|
|
3822
|
+
description: "ISO-8601 datetime. Defaults to now()."
|
|
3823
|
+
}
|
|
2976
3824
|
},
|
|
2977
3825
|
additionalProperties: true
|
|
2978
3826
|
}
|
|
@@ -2980,11 +3828,24 @@ var holy_sheet_schema_default = {
|
|
|
2980
3828
|
definitions: {
|
|
2981
3829
|
Sheet: {
|
|
2982
3830
|
type: "object",
|
|
2983
|
-
required: [
|
|
3831
|
+
required: [
|
|
3832
|
+
"name"
|
|
3833
|
+
],
|
|
2984
3834
|
additionalProperties: false,
|
|
2985
3835
|
oneOf: [
|
|
2986
|
-
{
|
|
2987
|
-
|
|
3836
|
+
{
|
|
3837
|
+
required: [
|
|
3838
|
+
"columns",
|
|
3839
|
+
"rows"
|
|
3840
|
+
],
|
|
3841
|
+
description: "Row-oriented (agent-ergonomic)."
|
|
3842
|
+
},
|
|
3843
|
+
{
|
|
3844
|
+
required: [
|
|
3845
|
+
"cells"
|
|
3846
|
+
],
|
|
3847
|
+
description: "Sparse A1-keyed map (fancy-sheets passthrough)."
|
|
3848
|
+
}
|
|
2988
3849
|
],
|
|
2989
3850
|
properties: {
|
|
2990
3851
|
name: {
|
|
@@ -2995,7 +3856,9 @@ var holy_sheet_schema_default = {
|
|
|
2995
3856
|
},
|
|
2996
3857
|
columns: {
|
|
2997
3858
|
type: "array",
|
|
2998
|
-
items: {
|
|
3859
|
+
items: {
|
|
3860
|
+
$ref: "#/definitions/Column"
|
|
3861
|
+
},
|
|
2999
3862
|
description: "When present, the first row is the header row. Column types are applied to every row in that column."
|
|
3000
3863
|
},
|
|
3001
3864
|
rows: {
|
|
@@ -3003,89 +3866,155 @@ var holy_sheet_schema_default = {
|
|
|
3003
3866
|
items: {
|
|
3004
3867
|
type: "array",
|
|
3005
3868
|
description: "One row of cell values, in column order. Use null for missing values.",
|
|
3006
|
-
items: {
|
|
3869
|
+
items: {
|
|
3870
|
+
$ref: "#/definitions/CellValue"
|
|
3871
|
+
}
|
|
3007
3872
|
}
|
|
3008
3873
|
},
|
|
3009
3874
|
cells: {
|
|
3010
3875
|
type: "object",
|
|
3011
|
-
description: "Sparse
|
|
3012
|
-
additionalProperties: {
|
|
3876
|
+
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.",
|
|
3877
|
+
additionalProperties: {
|
|
3878
|
+
$ref: "#/definitions/CellData"
|
|
3879
|
+
},
|
|
3013
3880
|
patternProperties: {
|
|
3014
|
-
"^[A-Z]+[1-9][0-9]*$": {
|
|
3881
|
+
"^[A-Z]+[1-9][0-9]*$": {
|
|
3882
|
+
$ref: "#/definitions/CellData"
|
|
3883
|
+
}
|
|
3015
3884
|
}
|
|
3016
3885
|
},
|
|
3017
3886
|
totals: {
|
|
3018
3887
|
type: "object",
|
|
3019
|
-
description: "Symbolic aggregations appended below the data. Key = column header; value =
|
|
3888
|
+
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
3889
|
additionalProperties: {
|
|
3021
3890
|
type: "string",
|
|
3022
|
-
enum: [
|
|
3891
|
+
enum: [
|
|
3892
|
+
"sum",
|
|
3893
|
+
"avg",
|
|
3894
|
+
"count",
|
|
3895
|
+
"min",
|
|
3896
|
+
"max"
|
|
3897
|
+
]
|
|
3023
3898
|
}
|
|
3024
3899
|
},
|
|
3025
3900
|
theme: {
|
|
3026
3901
|
type: "string",
|
|
3027
|
-
enum: [
|
|
3902
|
+
enum: [
|
|
3903
|
+
"default",
|
|
3904
|
+
"minimal",
|
|
3905
|
+
"plain",
|
|
3906
|
+
"business"
|
|
3907
|
+
],
|
|
3028
3908
|
default: "default",
|
|
3029
|
-
description: "Pre-baked style preset.
|
|
3909
|
+
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
3910
|
},
|
|
3031
3911
|
mergedRegions: {
|
|
3032
3912
|
type: "array",
|
|
3033
3913
|
items: {
|
|
3034
3914
|
type: "object",
|
|
3035
|
-
required: [
|
|
3915
|
+
required: [
|
|
3916
|
+
"start",
|
|
3917
|
+
"end"
|
|
3918
|
+
],
|
|
3036
3919
|
properties: {
|
|
3037
|
-
start: {
|
|
3038
|
-
|
|
3920
|
+
start: {
|
|
3921
|
+
type: "string",
|
|
3922
|
+
description: "A1 ref of top-left."
|
|
3923
|
+
},
|
|
3924
|
+
end: {
|
|
3925
|
+
type: "string",
|
|
3926
|
+
description: "A1 ref of bottom-right."
|
|
3927
|
+
}
|
|
3039
3928
|
}
|
|
3040
3929
|
},
|
|
3041
|
-
description:
|
|
3930
|
+
description: 'Cells to merge, as [{start, end}] \u2014 e.g. [{"start": "A1", "end": "D1"}]. Use it for a title band spanning the table.'
|
|
3042
3931
|
},
|
|
3043
3932
|
columnWidths: {
|
|
3044
3933
|
type: "object",
|
|
3045
|
-
description: "Override widths in pixels. Keys are 0-based column indexes.
|
|
3046
|
-
additionalProperties: {
|
|
3934
|
+
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 ####.",
|
|
3935
|
+
additionalProperties: {
|
|
3936
|
+
type: "number",
|
|
3937
|
+
minimum: 0
|
|
3938
|
+
}
|
|
3939
|
+
},
|
|
3940
|
+
frozenRows: {
|
|
3941
|
+
type: "integer",
|
|
3942
|
+
minimum: 0,
|
|
3943
|
+
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
3944
|
},
|
|
3048
|
-
|
|
3049
|
-
|
|
3945
|
+
frozenCols: {
|
|
3946
|
+
type: "integer",
|
|
3947
|
+
minimum: 0,
|
|
3948
|
+
description: "How many columns stay visible while the sheet scrolls sideways. Set to 1 when the first column is the label for the row."
|
|
3949
|
+
}
|
|
3050
3950
|
}
|
|
3051
3951
|
},
|
|
3052
3952
|
Column: {
|
|
3053
3953
|
type: "object",
|
|
3054
|
-
required: [
|
|
3954
|
+
required: [
|
|
3955
|
+
"header"
|
|
3956
|
+
],
|
|
3055
3957
|
additionalProperties: false,
|
|
3056
3958
|
properties: {
|
|
3057
|
-
header: {
|
|
3959
|
+
header: {
|
|
3960
|
+
type: "string"
|
|
3961
|
+
},
|
|
3058
3962
|
type: {
|
|
3059
3963
|
type: "string",
|
|
3060
|
-
enum: [
|
|
3964
|
+
enum: [
|
|
3965
|
+
"auto",
|
|
3966
|
+
"string",
|
|
3967
|
+
"number",
|
|
3968
|
+
"integer",
|
|
3969
|
+
"boolean",
|
|
3970
|
+
"date",
|
|
3971
|
+
"datetime",
|
|
3972
|
+
"currency",
|
|
3973
|
+
"percent",
|
|
3974
|
+
"formula"
|
|
3975
|
+
],
|
|
3061
3976
|
default: "auto",
|
|
3062
|
-
description: "Cell type applied to every value in this column. 'auto' infers
|
|
3977
|
+
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
3978
|
},
|
|
3064
3979
|
currency: {
|
|
3065
3980
|
type: "string",
|
|
3066
|
-
description: "ISO
|
|
3981
|
+
description: "ISO currency code for a 'currency' column, e.g. USD. Pair it with decimals: 2."
|
|
3067
3982
|
},
|
|
3068
3983
|
decimals: {
|
|
3069
3984
|
type: "integer",
|
|
3070
3985
|
minimum: 0,
|
|
3071
3986
|
maximum: 10,
|
|
3072
|
-
description: "
|
|
3987
|
+
description: "Decimal places for a numeric, currency or percent column. Money is normally 2; a growth rate is normally 1."
|
|
3073
3988
|
},
|
|
3074
3989
|
format: {
|
|
3075
3990
|
type: "string",
|
|
3076
3991
|
description: "Custom Excel number format string. Overrides type-derived format. Power-user escape hatch."
|
|
3077
3992
|
},
|
|
3078
|
-
width: {
|
|
3993
|
+
width: {
|
|
3994
|
+
type: "number",
|
|
3995
|
+
minimum: 0,
|
|
3996
|
+
description: "Column width in pixels. Same as columnWidths but per-column."
|
|
3997
|
+
}
|
|
3079
3998
|
}
|
|
3080
3999
|
},
|
|
3081
4000
|
CellValue: {
|
|
3082
4001
|
description: "A primitive value, null, or an object {value, formula, format, comment} for per-cell control.",
|
|
3083
4002
|
oneOf: [
|
|
3084
|
-
{
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
{
|
|
3088
|
-
|
|
4003
|
+
{
|
|
4004
|
+
type: "string"
|
|
4005
|
+
},
|
|
4006
|
+
{
|
|
4007
|
+
type: "number"
|
|
4008
|
+
},
|
|
4009
|
+
{
|
|
4010
|
+
type: "boolean"
|
|
4011
|
+
},
|
|
4012
|
+
{
|
|
4013
|
+
type: "null"
|
|
4014
|
+
},
|
|
4015
|
+
{
|
|
4016
|
+
$ref: "#/definitions/CellData"
|
|
4017
|
+
}
|
|
3089
4018
|
]
|
|
3090
4019
|
},
|
|
3091
4020
|
CellData: {
|
|
@@ -3095,10 +4024,18 @@ var holy_sheet_schema_default = {
|
|
|
3095
4024
|
value: {
|
|
3096
4025
|
description: "Raw cell value.",
|
|
3097
4026
|
oneOf: [
|
|
3098
|
-
{
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
{
|
|
4027
|
+
{
|
|
4028
|
+
type: "string"
|
|
4029
|
+
},
|
|
4030
|
+
{
|
|
4031
|
+
type: "number"
|
|
4032
|
+
},
|
|
4033
|
+
{
|
|
4034
|
+
type: "boolean"
|
|
4035
|
+
},
|
|
4036
|
+
{
|
|
4037
|
+
type: "null"
|
|
4038
|
+
}
|
|
3102
4039
|
]
|
|
3103
4040
|
},
|
|
3104
4041
|
formula: {
|
|
@@ -3107,53 +4044,125 @@ var holy_sheet_schema_default = {
|
|
|
3107
4044
|
},
|
|
3108
4045
|
computedValue: {
|
|
3109
4046
|
description: "Cached value from a previous evaluation. When present, Excel shows it without recomputing on open.",
|
|
3110
|
-
oneOf: [
|
|
4047
|
+
oneOf: [
|
|
4048
|
+
{
|
|
4049
|
+
type: "string"
|
|
4050
|
+
},
|
|
4051
|
+
{
|
|
4052
|
+
type: "number"
|
|
4053
|
+
},
|
|
4054
|
+
{
|
|
4055
|
+
type: "boolean"
|
|
4056
|
+
},
|
|
4057
|
+
{
|
|
4058
|
+
type: "null"
|
|
4059
|
+
}
|
|
4060
|
+
]
|
|
4061
|
+
},
|
|
4062
|
+
format: {
|
|
4063
|
+
$ref: "#/definitions/CellFormat"
|
|
3111
4064
|
},
|
|
3112
|
-
|
|
3113
|
-
|
|
4065
|
+
comment: {
|
|
4066
|
+
$ref: "#/definitions/CellComment"
|
|
4067
|
+
}
|
|
3114
4068
|
}
|
|
3115
4069
|
},
|
|
3116
4070
|
CellFormat: {
|
|
3117
4071
|
type: "object",
|
|
3118
|
-
description: "Per-cell format.
|
|
4072
|
+
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
4073
|
additionalProperties: false,
|
|
3120
4074
|
properties: {
|
|
3121
|
-
bold: {
|
|
3122
|
-
|
|
3123
|
-
|
|
4075
|
+
bold: {
|
|
4076
|
+
type: "boolean"
|
|
4077
|
+
},
|
|
4078
|
+
italic: {
|
|
4079
|
+
type: "boolean"
|
|
4080
|
+
},
|
|
4081
|
+
textAlign: {
|
|
4082
|
+
type: "string",
|
|
4083
|
+
enum: [
|
|
4084
|
+
"left",
|
|
4085
|
+
"center",
|
|
4086
|
+
"right"
|
|
4087
|
+
]
|
|
4088
|
+
},
|
|
3124
4089
|
displayFormat: {
|
|
3125
4090
|
type: "string",
|
|
3126
|
-
enum: [
|
|
4091
|
+
enum: [
|
|
4092
|
+
"auto",
|
|
4093
|
+
"text",
|
|
4094
|
+
"number",
|
|
4095
|
+
"date",
|
|
4096
|
+
"datetime",
|
|
4097
|
+
"percentage",
|
|
4098
|
+
"currency"
|
|
4099
|
+
]
|
|
4100
|
+
},
|
|
4101
|
+
decimals: {
|
|
4102
|
+
type: "integer",
|
|
4103
|
+
minimum: 0,
|
|
4104
|
+
maximum: 10
|
|
3127
4105
|
},
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
4106
|
+
color: {
|
|
4107
|
+
type: "string",
|
|
4108
|
+
description: "Hex color (#RRGGBB) for text."
|
|
4109
|
+
},
|
|
4110
|
+
backgroundColor: {
|
|
4111
|
+
type: "string",
|
|
4112
|
+
description: "Hex color for cell fill."
|
|
4113
|
+
},
|
|
4114
|
+
fontSize: {
|
|
4115
|
+
type: "integer",
|
|
4116
|
+
minimum: 6,
|
|
4117
|
+
maximum: 72
|
|
4118
|
+
},
|
|
4119
|
+
borderTop: {
|
|
4120
|
+
type: "string"
|
|
4121
|
+
},
|
|
4122
|
+
borderRight: {
|
|
4123
|
+
type: "string"
|
|
4124
|
+
},
|
|
4125
|
+
borderBottom: {
|
|
4126
|
+
type: "string"
|
|
4127
|
+
},
|
|
4128
|
+
borderLeft: {
|
|
4129
|
+
type: "string"
|
|
4130
|
+
}
|
|
3136
4131
|
}
|
|
3137
4132
|
},
|
|
3138
4133
|
CellComment: {
|
|
3139
4134
|
type: "object",
|
|
3140
|
-
description: "
|
|
3141
|
-
required: [
|
|
4135
|
+
description: "A reviewer note attached to a cell. Accepts a plain string, or an object with text/author/color.",
|
|
4136
|
+
required: [
|
|
4137
|
+
"text"
|
|
4138
|
+
],
|
|
3142
4139
|
additionalProperties: false,
|
|
3143
4140
|
properties: {
|
|
3144
|
-
text: {
|
|
3145
|
-
|
|
3146
|
-
|
|
4141
|
+
text: {
|
|
4142
|
+
type: "string"
|
|
4143
|
+
},
|
|
4144
|
+
author: {
|
|
4145
|
+
type: "string"
|
|
4146
|
+
},
|
|
4147
|
+
color: {
|
|
4148
|
+
type: "string",
|
|
4149
|
+
description: "Hex color for the corner triangle indicator."
|
|
4150
|
+
}
|
|
3147
4151
|
}
|
|
3148
4152
|
}
|
|
3149
4153
|
}
|
|
3150
4154
|
};
|
|
3151
4155
|
|
|
3152
4156
|
// src/agent.ts
|
|
3153
|
-
var VERSION = "2.
|
|
4157
|
+
var VERSION = "2.3.1";
|
|
3154
4158
|
function toU8(input) {
|
|
3155
4159
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
3156
4160
|
}
|
|
4161
|
+
function readSpreadsheet(bytes, path) {
|
|
4162
|
+
const { format, files } = FormatSniffer.sniff(bytes, path);
|
|
4163
|
+
const workbook = format === "ods" ? new OdsReader().readFiles(files) : new XlsxReader().readFiles(files);
|
|
4164
|
+
return WorkbookSchema.fromWorkbook(workbook);
|
|
4165
|
+
}
|
|
3157
4166
|
var Agent = {
|
|
3158
4167
|
/** Validate a schema without writing. Empty array = valid. */
|
|
3159
4168
|
validate(schema) {
|
|
@@ -3178,16 +4187,20 @@ var Agent = {
|
|
|
3178
4187
|
toolDefinition() {
|
|
3179
4188
|
return holy_sheet_schema_default;
|
|
3180
4189
|
},
|
|
3181
|
-
/**
|
|
4190
|
+
/**
|
|
4191
|
+
* Round-trip xlsx or ods bytes back to a schema. Universal. The format is
|
|
4192
|
+
* told apart by content, never by name, and both describe to the same
|
|
4193
|
+
* schema. Throws `UnsupportedFormatException` for anything else.
|
|
4194
|
+
*/
|
|
3182
4195
|
read(input) {
|
|
3183
|
-
return
|
|
4196
|
+
return readSpreadsheet(toU8(input), null);
|
|
3184
4197
|
},
|
|
3185
|
-
/** Round-trip an xlsx file on disk back to a schema (Node only). */
|
|
4198
|
+
/** Round-trip an xlsx or ods file on disk back to a schema (Node only). */
|
|
3186
4199
|
async describe(path) {
|
|
3187
4200
|
const fs = await import('fs');
|
|
3188
4201
|
if (!fs.existsSync(path)) return { error: "not_found", path };
|
|
3189
4202
|
const bytes = fs.readFileSync(path);
|
|
3190
|
-
return
|
|
4203
|
+
return readSpreadsheet(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), path);
|
|
3191
4204
|
},
|
|
3192
4205
|
/** Validate + conservative repairs in one call. */
|
|
3193
4206
|
validateAndRepair(schema) {
|
|
@@ -3251,6 +4264,6 @@ var HolySheet = class {
|
|
|
3251
4264
|
};
|
|
3252
4265
|
HolySheet.VERSION = VERSION;
|
|
3253
4266
|
|
|
3254
|
-
export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormulaLinter, HolySheet, Inference, Normalizer, Repairer, SchemaException, Theme, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
|
|
4267
|
+
export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, HolySheet, Inference, Normalizer, OdsReader, Repairer, SchemaException, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
|
|
3255
4268
|
//# sourceMappingURL=index.js.map
|
|
3256
4269
|
//# sourceMappingURL=index.js.map
|