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