@ricsam/excel-to-formula-engine 0.0.0 → 0.1.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 +179 -45
- package/dist/index.cjs +1611 -0
- package/dist/index.cjs.map +19 -0
- package/dist/index.mjs +1571 -0
- package/dist/index.mjs.map +19 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/src/a1.d.ts +29 -0
- package/dist/types/src/convert.d.ts +23 -0
- package/dist/types/src/translate/conditional-format.d.ts +33 -0
- package/dist/types/src/translate/dates.d.ts +9 -0
- package/dist/types/src/translate/formula.d.ts +51 -0
- package/dist/types/src/types.d.ts +119 -0
- package/dist/types/src/xlsx/package.d.ts +26 -0
- package/dist/types/src/xlsx/styles.d.ts +22 -0
- package/dist/types/src/xlsx/workbook.d.ts +42 -0
- package/dist/types/src/xlsx/worksheet.d.ts +72 -0
- package/package.json +58 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1611 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// index.ts
|
|
40
|
+
var exports_excel_to_formula_engine = {};
|
|
41
|
+
__export(exports_excel_to_formula_engine, {
|
|
42
|
+
columnIndexToLabel: () => columnIndexToLabel,
|
|
43
|
+
columnLabelToIndex: () => columnLabelToIndex,
|
|
44
|
+
convertWorkbook: () => convertWorkbook,
|
|
45
|
+
excelSerialToIso: () => excelSerialToIso,
|
|
46
|
+
excelToFormulaEngine: () => excelToFormulaEngine,
|
|
47
|
+
excelToFormulaEngineDetailed: () => excelToFormulaEngineDetailed,
|
|
48
|
+
findUnsupportedFunctions: () => findUnsupportedFunctions,
|
|
49
|
+
offsetFormulaReferences: () => offsetFormulaReferences,
|
|
50
|
+
parseA1: () => parseA1,
|
|
51
|
+
parseA1Range: () => parseA1Range,
|
|
52
|
+
toA1: () => toA1,
|
|
53
|
+
translateFormula: () => translateFormula
|
|
54
|
+
});
|
|
55
|
+
module.exports = __toCommonJS(exports_excel_to_formula_engine);
|
|
56
|
+
|
|
57
|
+
// src/xlsx/package.ts
|
|
58
|
+
var import_fflate = require("fflate");
|
|
59
|
+
var import_fast_xml_parser = require("fast-xml-parser");
|
|
60
|
+
var parser = new import_fast_xml_parser.XMLParser({
|
|
61
|
+
ignoreAttributes: false,
|
|
62
|
+
attributeNamePrefix: "@",
|
|
63
|
+
parseTagValue: false,
|
|
64
|
+
parseAttributeValue: false,
|
|
65
|
+
trimValues: false,
|
|
66
|
+
processEntities: true
|
|
67
|
+
});
|
|
68
|
+
async function readXlsxPackage(source) {
|
|
69
|
+
const bytes = await toBytes(source);
|
|
70
|
+
let unzipped;
|
|
71
|
+
try {
|
|
72
|
+
unzipped = import_fflate.unzipSync(bytes);
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
throw new Error("Could not read the file as an .xlsx package. Legacy .xls files and " + "password-protected workbooks are not supported.", { cause });
|
|
75
|
+
}
|
|
76
|
+
const files = new Map;
|
|
77
|
+
for (const [path, content] of Object.entries(unzipped)) {
|
|
78
|
+
files.set(normalizePath(path), content);
|
|
79
|
+
}
|
|
80
|
+
if (!files.has("xl/workbook.xml")) {
|
|
81
|
+
throw new Error("The file is a zip archive but does not contain xl/workbook.xml, so it " + "is not an Excel workbook.");
|
|
82
|
+
}
|
|
83
|
+
const xmlCache = new Map;
|
|
84
|
+
return {
|
|
85
|
+
files,
|
|
86
|
+
text(path) {
|
|
87
|
+
const content = files.get(normalizePath(path));
|
|
88
|
+
return content ? import_fflate.strFromU8(content) : undefined;
|
|
89
|
+
},
|
|
90
|
+
xml(path) {
|
|
91
|
+
const key = normalizePath(path);
|
|
92
|
+
if (xmlCache.has(key)) {
|
|
93
|
+
return xmlCache.get(key);
|
|
94
|
+
}
|
|
95
|
+
const content = files.get(key);
|
|
96
|
+
const parsed = content ? parser.parse(import_fflate.strFromU8(content)) : undefined;
|
|
97
|
+
xmlCache.set(key, parsed);
|
|
98
|
+
return parsed;
|
|
99
|
+
},
|
|
100
|
+
has(path) {
|
|
101
|
+
return files.has(normalizePath(path));
|
|
102
|
+
},
|
|
103
|
+
list(prefix) {
|
|
104
|
+
const normalized = normalizePath(prefix);
|
|
105
|
+
return Array.from(files.keys()).filter((path) => path.startsWith(normalized));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function normalizePath(path) {
|
|
110
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
111
|
+
}
|
|
112
|
+
async function toBytes(source) {
|
|
113
|
+
if (source instanceof Uint8Array) {
|
|
114
|
+
return source;
|
|
115
|
+
}
|
|
116
|
+
if (source instanceof ArrayBuffer) {
|
|
117
|
+
return new Uint8Array(source);
|
|
118
|
+
}
|
|
119
|
+
return new Uint8Array(await source.arrayBuffer());
|
|
120
|
+
}
|
|
121
|
+
function asArray(value) {
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return Array.isArray(value) ? value : [value];
|
|
126
|
+
}
|
|
127
|
+
function attr(node, name) {
|
|
128
|
+
if (!node || typeof node !== "object") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const value = node[`@${name}`];
|
|
132
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
133
|
+
}
|
|
134
|
+
function numAttr(node, name) {
|
|
135
|
+
const raw = attr(node, name);
|
|
136
|
+
if (raw === undefined) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const parsed = Number(raw);
|
|
140
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
141
|
+
}
|
|
142
|
+
function boolAttr(node, name, fallback = false) {
|
|
143
|
+
const raw = attr(node, name);
|
|
144
|
+
if (raw === undefined) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
return raw === "1" || raw.toLowerCase() === "true";
|
|
148
|
+
}
|
|
149
|
+
function textOf(node) {
|
|
150
|
+
if (node === undefined || node === null) {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
if (typeof node === "string") {
|
|
154
|
+
return node;
|
|
155
|
+
}
|
|
156
|
+
if (typeof node === "number" || typeof node === "boolean") {
|
|
157
|
+
return String(node);
|
|
158
|
+
}
|
|
159
|
+
if (typeof node === "object") {
|
|
160
|
+
const value = node["#text"];
|
|
161
|
+
return value === undefined || value === null ? "" : String(value);
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/a1.ts
|
|
167
|
+
var A1_CELL = /^\$?([A-Za-z]+)\$?(\d+)$/;
|
|
168
|
+
function columnLabelToIndex(label) {
|
|
169
|
+
let index = 0;
|
|
170
|
+
for (let i = 0;i < label.length; i++) {
|
|
171
|
+
index = index * 26 + (label.toUpperCase().charCodeAt(i) - 64);
|
|
172
|
+
}
|
|
173
|
+
return index - 1;
|
|
174
|
+
}
|
|
175
|
+
function columnIndexToLabel(index) {
|
|
176
|
+
let label = "";
|
|
177
|
+
let remaining = index + 1;
|
|
178
|
+
while (remaining > 0) {
|
|
179
|
+
const rest = (remaining - 1) % 26;
|
|
180
|
+
label = String.fromCharCode(65 + rest) + label;
|
|
181
|
+
remaining = Math.floor((remaining - rest) / 26);
|
|
182
|
+
}
|
|
183
|
+
return label;
|
|
184
|
+
}
|
|
185
|
+
function parseA1(reference) {
|
|
186
|
+
const match = A1_CELL.exec(reference.trim());
|
|
187
|
+
if (!match || match[1] === undefined || match[2] === undefined) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
colIndex: columnLabelToIndex(match[1]),
|
|
192
|
+
rowIndex: Number.parseInt(match[2], 10) - 1
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function toA1({ colIndex, rowIndex }) {
|
|
196
|
+
return `${columnIndexToLabel(colIndex)}${rowIndex + 1}`;
|
|
197
|
+
}
|
|
198
|
+
var EXCEL_MAX_ROW_INDEX = 1048575;
|
|
199
|
+
var EXCEL_MAX_COL_INDEX = 16383;
|
|
200
|
+
function parseA1Range(reference) {
|
|
201
|
+
const trimmed = reference.trim().replace(/\$/g, "");
|
|
202
|
+
const [rawStart, rawEnd] = trimmed.split(":");
|
|
203
|
+
if (rawStart === undefined) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (rawEnd === undefined) {
|
|
207
|
+
const cell = parseA1(rawStart);
|
|
208
|
+
return cell ? { start: cell, end: cell } : undefined;
|
|
209
|
+
}
|
|
210
|
+
const startCell = parseA1(rawStart);
|
|
211
|
+
const endCell = parseA1(rawEnd);
|
|
212
|
+
if (startCell && endCell) {
|
|
213
|
+
return { start: startCell, end: endCell };
|
|
214
|
+
}
|
|
215
|
+
if (/^[A-Za-z]+$/.test(rawStart) && /^[A-Za-z]+$/.test(rawEnd)) {
|
|
216
|
+
return {
|
|
217
|
+
start: { colIndex: columnLabelToIndex(rawStart), rowIndex: 0 },
|
|
218
|
+
end: {
|
|
219
|
+
colIndex: columnLabelToIndex(rawEnd),
|
|
220
|
+
rowIndex: EXCEL_MAX_ROW_INDEX
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (/^\d+$/.test(rawStart) && /^\d+$/.test(rawEnd)) {
|
|
225
|
+
return {
|
|
226
|
+
start: { colIndex: 0, rowIndex: Number.parseInt(rawStart, 10) - 1 },
|
|
227
|
+
end: {
|
|
228
|
+
colIndex: EXCEL_MAX_COL_INDEX,
|
|
229
|
+
rowIndex: Number.parseInt(rawEnd, 10) - 1
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
function toSpreadsheetRange(range) {
|
|
236
|
+
return {
|
|
237
|
+
start: { col: range.start.colIndex, row: range.start.rowIndex },
|
|
238
|
+
end: {
|
|
239
|
+
col: { type: "number", value: range.end.colIndex },
|
|
240
|
+
row: { type: "number", value: range.end.rowIndex }
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function rangeReferenceToSpreadsheetRange(reference) {
|
|
245
|
+
const parsed = parseA1Range(reference);
|
|
246
|
+
return parsed ? toSpreadsheetRange(parsed) : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/xlsx/worksheet.ts
|
|
250
|
+
function parseWorksheet(pkg, partPath, sharedStrings) {
|
|
251
|
+
const root = pkg.xml(partPath);
|
|
252
|
+
const worksheet = root?.["worksheet"];
|
|
253
|
+
const result = {
|
|
254
|
+
cells: [],
|
|
255
|
+
merges: [],
|
|
256
|
+
columnWidths: {},
|
|
257
|
+
rowHeights: {},
|
|
258
|
+
conditionalFormats: [],
|
|
259
|
+
hyperlinks: {}
|
|
260
|
+
};
|
|
261
|
+
if (!worksheet) {
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
parseCells(worksheet, sharedStrings, result);
|
|
265
|
+
parseColumns(worksheet, result);
|
|
266
|
+
parsePanes(worksheet, result);
|
|
267
|
+
parseMerges(worksheet, result);
|
|
268
|
+
parseConditionalFormats(worksheet, result);
|
|
269
|
+
parseHyperlinks(pkg, partPath, worksheet, result);
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
function parseCells(worksheet, sharedStrings, result) {
|
|
273
|
+
const sheetData = worksheet["sheetData"];
|
|
274
|
+
const shared = new Map;
|
|
275
|
+
for (const rawRow of asArray(sheetData?.["row"])) {
|
|
276
|
+
const row = rawRow;
|
|
277
|
+
const rowNumber = numAttr(row, "r");
|
|
278
|
+
const height = numAttr(row, "ht");
|
|
279
|
+
if (rowNumber !== undefined && height !== undefined) {
|
|
280
|
+
result.rowHeights[rowNumber - 1] = height;
|
|
281
|
+
}
|
|
282
|
+
for (const rawCell of asArray(row["c"])) {
|
|
283
|
+
const cell = rawCell;
|
|
284
|
+
const reference = attr(cell, "r");
|
|
285
|
+
if (!reference) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const address = parseA1(reference);
|
|
289
|
+
if (!address) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const styleIndex = numAttr(cell, "s");
|
|
293
|
+
const type = attr(cell, "t") ?? "n";
|
|
294
|
+
const formula = readFormula(cell, address, shared);
|
|
295
|
+
const { kind, value } = readValue(cell, type, sharedStrings);
|
|
296
|
+
if (kind === "empty" && formula === undefined && styleIndex === undefined) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
result.cells.push({
|
|
300
|
+
reference,
|
|
301
|
+
colIndex: address.colIndex,
|
|
302
|
+
rowIndex: address.rowIndex,
|
|
303
|
+
kind,
|
|
304
|
+
value,
|
|
305
|
+
formula: formula?.formula,
|
|
306
|
+
formulaOffset: formula?.offset,
|
|
307
|
+
styleIndex
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function readFormula(cell, address, shared) {
|
|
313
|
+
const node = cell["f"];
|
|
314
|
+
if (node === undefined || node === null) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const body = textOf(node);
|
|
318
|
+
const type = attr(node, "t");
|
|
319
|
+
const si = attr(node, "si");
|
|
320
|
+
if (type === "shared" && si !== undefined) {
|
|
321
|
+
if (body) {
|
|
322
|
+
shared.set(si, {
|
|
323
|
+
formula: body,
|
|
324
|
+
colIndex: address.colIndex,
|
|
325
|
+
rowIndex: address.rowIndex
|
|
326
|
+
});
|
|
327
|
+
return { formula: body };
|
|
328
|
+
}
|
|
329
|
+
const master = shared.get(si);
|
|
330
|
+
if (!master) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
formula: master.formula,
|
|
335
|
+
offset: {
|
|
336
|
+
cols: address.colIndex - master.colIndex,
|
|
337
|
+
rows: address.rowIndex - master.rowIndex
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return body ? { formula: body } : undefined;
|
|
342
|
+
}
|
|
343
|
+
function readValue(cell, type, sharedStrings) {
|
|
344
|
+
if (type === "inlineStr") {
|
|
345
|
+
const inline = cell["is"];
|
|
346
|
+
return { kind: "string", value: readRichText(inline) };
|
|
347
|
+
}
|
|
348
|
+
const raw = cell["v"];
|
|
349
|
+
if (raw === undefined || raw === null) {
|
|
350
|
+
return { kind: "empty", value: undefined };
|
|
351
|
+
}
|
|
352
|
+
const text = textOf(raw);
|
|
353
|
+
switch (type) {
|
|
354
|
+
case "s": {
|
|
355
|
+
const index = Number.parseInt(text, 10);
|
|
356
|
+
return { kind: "string", value: sharedStrings[index] ?? "" };
|
|
357
|
+
}
|
|
358
|
+
case "str":
|
|
359
|
+
return { kind: "string", value: text };
|
|
360
|
+
case "b":
|
|
361
|
+
return { kind: "boolean", value: text === "1" };
|
|
362
|
+
case "e":
|
|
363
|
+
return { kind: "error", value: text };
|
|
364
|
+
case "d":
|
|
365
|
+
return { kind: "date", value: text };
|
|
366
|
+
default: {
|
|
367
|
+
const parsed = Number(text);
|
|
368
|
+
return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "string", value: text };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function readRichText(node) {
|
|
373
|
+
if (node === undefined || node === null) {
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
if (typeof node === "string") {
|
|
377
|
+
return node;
|
|
378
|
+
}
|
|
379
|
+
const record = node;
|
|
380
|
+
const runs = asArray(record["r"]);
|
|
381
|
+
if (runs.length > 0) {
|
|
382
|
+
return runs.map((run) => textOf(run["t"])).join("");
|
|
383
|
+
}
|
|
384
|
+
if (record["t"] !== undefined) {
|
|
385
|
+
return textOf(record["t"]);
|
|
386
|
+
}
|
|
387
|
+
return textOf(record);
|
|
388
|
+
}
|
|
389
|
+
function parseColumns(worksheet, result) {
|
|
390
|
+
for (const rawCol of asArray(worksheet["cols"]?.["col"])) {
|
|
391
|
+
const col = rawCol;
|
|
392
|
+
const min = numAttr(col, "min");
|
|
393
|
+
const max = numAttr(col, "max");
|
|
394
|
+
const width = numAttr(col, "width");
|
|
395
|
+
if (min === undefined || max === undefined || width === undefined) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (max - min > 1024) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
for (let index = min;index <= max; index++) {
|
|
402
|
+
result.columnWidths[index - 1] = width;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function parsePanes(worksheet, result) {
|
|
407
|
+
const sheetViews = worksheet["sheetViews"];
|
|
408
|
+
const sheetView = asArray(sheetViews?.["sheetView"])[0];
|
|
409
|
+
const pane = sheetView?.["pane"];
|
|
410
|
+
if (!pane) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (attr(pane, "state") !== "frozen") {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
result.frozen = {
|
|
417
|
+
cols: numAttr(pane, "xSplit") ?? 0,
|
|
418
|
+
rows: numAttr(pane, "ySplit") ?? 0
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function parseMerges(worksheet, result) {
|
|
422
|
+
for (const merge of asArray(worksheet["mergeCells"]?.["mergeCell"])) {
|
|
423
|
+
const ref = attr(merge, "ref");
|
|
424
|
+
if (ref) {
|
|
425
|
+
result.merges.push(ref);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function parseConditionalFormats(worksheet, result) {
|
|
430
|
+
for (const rawBlock of asArray(worksheet["conditionalFormatting"])) {
|
|
431
|
+
const block = rawBlock;
|
|
432
|
+
const sqref = attr(block, "sqref");
|
|
433
|
+
if (!sqref) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const ranges = sqref.split(/\s+/).filter(Boolean);
|
|
437
|
+
for (const rawRule of asArray(block["cfRule"])) {
|
|
438
|
+
const rule = rawRule;
|
|
439
|
+
const type = attr(rule, "type");
|
|
440
|
+
if (!type) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const colorScaleNode = rule["colorScale"];
|
|
444
|
+
result.conditionalFormats.push({
|
|
445
|
+
ranges,
|
|
446
|
+
type,
|
|
447
|
+
operator: attr(rule, "operator"),
|
|
448
|
+
formulas: asArray(rule["formula"]).map((formula) => textOf(formula)),
|
|
449
|
+
text: attr(rule, "text"),
|
|
450
|
+
dxfId: numAttr(rule, "dxfId"),
|
|
451
|
+
priority: numAttr(rule, "priority") ?? 0,
|
|
452
|
+
rank: numAttr(rule, "rank"),
|
|
453
|
+
bottom: boolAttr(rule, "bottom"),
|
|
454
|
+
percent: boolAttr(rule, "percent"),
|
|
455
|
+
stopIfTrue: boolAttr(rule, "stopIfTrue"),
|
|
456
|
+
colorScale: colorScaleNode ? {
|
|
457
|
+
cfvo: asArray(colorScaleNode["cfvo"]).map((cfvo) => ({
|
|
458
|
+
type: attr(cfvo, "type") ?? "min",
|
|
459
|
+
val: attr(cfvo, "val")
|
|
460
|
+
})),
|
|
461
|
+
colors: asArray(colorScaleNode["color"]).map((color) => attr(color, "rgb"))
|
|
462
|
+
} : undefined
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function parseHyperlinks(pkg, partPath, worksheet, result) {
|
|
468
|
+
const links = asArray(worksheet["hyperlinks"]?.["hyperlink"]);
|
|
469
|
+
if (links.length === 0) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const relationships = readRelationships(pkg, partPath);
|
|
473
|
+
for (const rawLink of links) {
|
|
474
|
+
const link = rawLink;
|
|
475
|
+
const ref = attr(link, "ref");
|
|
476
|
+
if (!ref) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const id = attr(link, "r:id") ?? attr(link, "id");
|
|
480
|
+
const location = attr(link, "location");
|
|
481
|
+
const target = id ? relationships.get(id) : undefined;
|
|
482
|
+
const href = target ? location ? `${target}#${location}` : target : location;
|
|
483
|
+
if (!href) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const start = parseA1(ref.split(":")[0] ?? ref);
|
|
487
|
+
if (start) {
|
|
488
|
+
result.hyperlinks[toA1(start)] = href;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function readRelationships(pkg, partPath) {
|
|
493
|
+
const slash = partPath.lastIndexOf("/");
|
|
494
|
+
const dir = slash === -1 ? "" : partPath.slice(0, slash);
|
|
495
|
+
const file = slash === -1 ? partPath : partPath.slice(slash + 1);
|
|
496
|
+
const relsPath = `${dir}/_rels/${file}.rels`;
|
|
497
|
+
const map = new Map;
|
|
498
|
+
const root = pkg.xml(relsPath);
|
|
499
|
+
const container = root?.["Relationships"];
|
|
500
|
+
if (!container) {
|
|
501
|
+
return map;
|
|
502
|
+
}
|
|
503
|
+
for (const relationship of asArray(container["Relationship"])) {
|
|
504
|
+
const id = attr(relationship, "Id");
|
|
505
|
+
const target = attr(relationship, "Target");
|
|
506
|
+
if (id && target) {
|
|
507
|
+
map.set(id, target);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return map;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/xlsx/workbook.ts
|
|
514
|
+
function parseSharedStrings(pkg) {
|
|
515
|
+
const root = pkg.xml("xl/sharedStrings.xml");
|
|
516
|
+
const sst = root?.["sst"];
|
|
517
|
+
if (!sst) {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
return asArray(sst["si"]).map((entry) => readRichText(entry));
|
|
521
|
+
}
|
|
522
|
+
function parseWorkbook(pkg) {
|
|
523
|
+
const root = pkg.xml("xl/workbook.xml");
|
|
524
|
+
const workbook = root?.["workbook"];
|
|
525
|
+
if (!workbook) {
|
|
526
|
+
return { sheets: [], definedNames: [], date1904: false };
|
|
527
|
+
}
|
|
528
|
+
const relationships = readRelationships(pkg, "xl/workbook.xml");
|
|
529
|
+
const sheets = [];
|
|
530
|
+
for (const rawSheet of asArray(workbook["sheets"]?.["sheet"])) {
|
|
531
|
+
const sheet = rawSheet;
|
|
532
|
+
const name = attr(sheet, "name");
|
|
533
|
+
const id = attr(sheet, "r:id") ?? attr(sheet, "id");
|
|
534
|
+
if (!name || !id) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const target = relationships.get(id);
|
|
538
|
+
if (!target) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
sheets.push({
|
|
542
|
+
name,
|
|
543
|
+
partPath: resolvePart(target),
|
|
544
|
+
state: attr(sheet, "state") ?? "visible"
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
const definedNames = [];
|
|
548
|
+
for (const rawName of asArray(workbook["definedNames"]?.["definedName"])) {
|
|
549
|
+
const node = rawName;
|
|
550
|
+
const name = attr(node, "name");
|
|
551
|
+
if (!name) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
definedNames.push({
|
|
555
|
+
name,
|
|
556
|
+
formula: textContent(node),
|
|
557
|
+
localSheetId: numAttr(node, "localSheetId"),
|
|
558
|
+
hidden: attr(node, "hidden") === "1"
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const pr = workbook["workbookPr"];
|
|
562
|
+
const date1904 = attr(pr, "date1904") === "1" || attr(pr, "date1904") === "true";
|
|
563
|
+
return { sheets, definedNames, date1904 };
|
|
564
|
+
}
|
|
565
|
+
function parseTables(pkg, worksheetPartPath) {
|
|
566
|
+
const relationships = readRelationships(pkg, worksheetPartPath);
|
|
567
|
+
const tables = [];
|
|
568
|
+
for (const target of relationships.values()) {
|
|
569
|
+
if (!target.includes("tables/")) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const partPath = resolvePart(target, worksheetPartPath);
|
|
573
|
+
const root = pkg.xml(partPath);
|
|
574
|
+
const table = root?.["table"];
|
|
575
|
+
if (!table) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const ref = attr(table, "ref");
|
|
579
|
+
const name = attr(table, "name") ?? attr(table, "displayName");
|
|
580
|
+
if (!ref || !name) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
tables.push({
|
|
584
|
+
name,
|
|
585
|
+
displayName: attr(table, "displayName") ?? name,
|
|
586
|
+
ref,
|
|
587
|
+
headerRowCount: numAttr(table, "headerRowCount") ?? 1,
|
|
588
|
+
totalsRowCount: numAttr(table, "totalsRowCount") ?? 0,
|
|
589
|
+
columnNames: asArray(table["tableColumns"]?.["tableColumn"]).map((column) => attr(column, "name") ?? "")
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return tables;
|
|
593
|
+
}
|
|
594
|
+
function textContent(node) {
|
|
595
|
+
const value = node["#text"];
|
|
596
|
+
if (value === undefined || value === null) {
|
|
597
|
+
return "";
|
|
598
|
+
}
|
|
599
|
+
return String(value);
|
|
600
|
+
}
|
|
601
|
+
function resolvePart(target, fromPart = "xl/workbook.xml") {
|
|
602
|
+
if (target.startsWith("/")) {
|
|
603
|
+
return target.slice(1);
|
|
604
|
+
}
|
|
605
|
+
const slash = fromPart.lastIndexOf("/");
|
|
606
|
+
const baseDir = slash === -1 ? "" : fromPart.slice(0, slash);
|
|
607
|
+
const segments = baseDir ? baseDir.split("/") : [];
|
|
608
|
+
for (const segment of target.split("/")) {
|
|
609
|
+
if (segment === "." || segment === "") {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (segment === "..") {
|
|
613
|
+
segments.pop();
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
segments.push(segment);
|
|
617
|
+
}
|
|
618
|
+
return segments.join("/");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/xlsx/styles.ts
|
|
622
|
+
var BUILTIN_FORMATS = {
|
|
623
|
+
0: "General",
|
|
624
|
+
1: "0",
|
|
625
|
+
2: "0.00",
|
|
626
|
+
3: "#,##0",
|
|
627
|
+
4: "#,##0.00",
|
|
628
|
+
9: "0%",
|
|
629
|
+
10: "0.00%",
|
|
630
|
+
11: "0.00E+00",
|
|
631
|
+
12: "# ?/?",
|
|
632
|
+
13: "# ??/??",
|
|
633
|
+
14: "mm-dd-yy",
|
|
634
|
+
15: "d-mmm-yy",
|
|
635
|
+
16: "d-mmm",
|
|
636
|
+
17: "mmm-yy",
|
|
637
|
+
18: "h:mm AM/PM",
|
|
638
|
+
19: "h:mm:ss AM/PM",
|
|
639
|
+
20: "h:mm",
|
|
640
|
+
21: "h:mm:ss",
|
|
641
|
+
22: "m/d/yy h:mm",
|
|
642
|
+
37: "#,##0 ;(#,##0)",
|
|
643
|
+
38: "#,##0 ;[Red](#,##0)",
|
|
644
|
+
39: "#,##0.00;(#,##0.00)",
|
|
645
|
+
40: "#,##0.00;[Red](#,##0.00)",
|
|
646
|
+
45: "mm:ss",
|
|
647
|
+
46: "[h]:mm:ss",
|
|
648
|
+
47: "mmss.0",
|
|
649
|
+
48: "##0.0E+0",
|
|
650
|
+
49: "@"
|
|
651
|
+
};
|
|
652
|
+
var BUILTIN_DATE_FORMAT_IDS = new Set([
|
|
653
|
+
14,
|
|
654
|
+
15,
|
|
655
|
+
16,
|
|
656
|
+
17,
|
|
657
|
+
18,
|
|
658
|
+
19,
|
|
659
|
+
20,
|
|
660
|
+
21,
|
|
661
|
+
22,
|
|
662
|
+
45,
|
|
663
|
+
46,
|
|
664
|
+
47
|
|
665
|
+
]);
|
|
666
|
+
function parseStyles(pkg) {
|
|
667
|
+
const root = pkg.xml("xl/styles.xml");
|
|
668
|
+
const styleSheet = root?.["styleSheet"];
|
|
669
|
+
if (!styleSheet) {
|
|
670
|
+
return { formats: [], differentialFormats: [] };
|
|
671
|
+
}
|
|
672
|
+
const theme = parseThemeColors(pkg);
|
|
673
|
+
const indexedColors = parseIndexedColors(styleSheet);
|
|
674
|
+
const numberFormats = new Map;
|
|
675
|
+
for (const numFmt of asArray(styleSheet["numFmts"]?.["numFmt"])) {
|
|
676
|
+
const id = numAttr(numFmt, "numFmtId");
|
|
677
|
+
const code = attr(numFmt, "formatCode");
|
|
678
|
+
if (id !== undefined && code !== undefined) {
|
|
679
|
+
numberFormats.set(id, code);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const fonts = asArray(styleSheet["fonts"]?.["font"]);
|
|
683
|
+
const fills = asArray(styleSheet["fills"]?.["fill"]);
|
|
684
|
+
const borders = asArray(styleSheet["borders"]?.["border"]);
|
|
685
|
+
const cellXfs = asArray(styleSheet["cellXfs"]?.["xf"]);
|
|
686
|
+
const formats = cellXfs.map((rawXf) => {
|
|
687
|
+
const xf = rawXf;
|
|
688
|
+
const style = {};
|
|
689
|
+
if (boolAttr(xf, "applyFont", true)) {
|
|
690
|
+
const font = fonts[numAttr(xf, "fontId") ?? -1];
|
|
691
|
+
if (font) {
|
|
692
|
+
applyFont(style, font, theme, indexedColors);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (boolAttr(xf, "applyFill", true)) {
|
|
696
|
+
const fill = fills[numAttr(xf, "fillId") ?? -1];
|
|
697
|
+
if (fill) {
|
|
698
|
+
applyFill(style, fill, theme, indexedColors);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (boolAttr(xf, "applyBorder", true)) {
|
|
702
|
+
const border = borders[numAttr(xf, "borderId") ?? -1];
|
|
703
|
+
if (border) {
|
|
704
|
+
applyBorder(style, border, theme, indexedColors);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (boolAttr(xf, "applyAlignment", true)) {
|
|
708
|
+
const alignment = xf["alignment"];
|
|
709
|
+
if (alignment && boolAttr(alignment, "wrapText")) {
|
|
710
|
+
style.wrapText = true;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const numFmtId = numAttr(xf, "numFmtId") ?? 0;
|
|
714
|
+
const numberFormat = numberFormats.get(numFmtId) ?? BUILTIN_FORMATS[numFmtId];
|
|
715
|
+
return {
|
|
716
|
+
style,
|
|
717
|
+
numberFormat: numberFormat === "General" ? undefined : numberFormat,
|
|
718
|
+
isDateFormat: isDateFormat(numFmtId, numberFormat)
|
|
719
|
+
};
|
|
720
|
+
});
|
|
721
|
+
const differentialFormats = asArray(styleSheet["dxfs"]?.["dxf"]).map((dxf) => {
|
|
722
|
+
const style = {};
|
|
723
|
+
const node = dxf;
|
|
724
|
+
if (node["font"]) {
|
|
725
|
+
applyFont(style, node["font"], theme, indexedColors);
|
|
726
|
+
}
|
|
727
|
+
if (node["fill"]) {
|
|
728
|
+
applyFill(style, node["fill"], theme, indexedColors);
|
|
729
|
+
}
|
|
730
|
+
if (node["border"]) {
|
|
731
|
+
applyBorder(style, node["border"], theme, indexedColors);
|
|
732
|
+
}
|
|
733
|
+
return style;
|
|
734
|
+
});
|
|
735
|
+
return { formats, differentialFormats };
|
|
736
|
+
}
|
|
737
|
+
function applyFont(style, font, theme, indexedColors) {
|
|
738
|
+
if (font["b"] !== undefined && isOn(font["b"])) {
|
|
739
|
+
style.bold = true;
|
|
740
|
+
}
|
|
741
|
+
if (font["i"] !== undefined && isOn(font["i"])) {
|
|
742
|
+
style.italic = true;
|
|
743
|
+
}
|
|
744
|
+
if (font["u"] !== undefined && isOn(font["u"])) {
|
|
745
|
+
style.underline = true;
|
|
746
|
+
}
|
|
747
|
+
const size = numAttr(font["sz"], "val");
|
|
748
|
+
if (size !== undefined) {
|
|
749
|
+
style.fontSize = Math.round(size * (96 / 72));
|
|
750
|
+
}
|
|
751
|
+
const color = resolveColor(font["color"], theme, indexedColors);
|
|
752
|
+
if (color) {
|
|
753
|
+
style.color = color;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function applyFill(style, fill, theme, indexedColors) {
|
|
757
|
+
const pattern = fill["patternFill"];
|
|
758
|
+
if (!pattern) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const patternType = attr(pattern, "patternType");
|
|
762
|
+
if (patternType === "none" || patternType === undefined) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const color = resolveColor(pattern["fgColor"], theme, indexedColors) ?? resolveColor(pattern["bgColor"], theme, indexedColors);
|
|
766
|
+
if (color) {
|
|
767
|
+
style.backgroundColor = color;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
var BORDER_SIDES = ["top", "right", "bottom", "left"];
|
|
771
|
+
function applyBorder(style, border, theme, indexedColors) {
|
|
772
|
+
const sides = {};
|
|
773
|
+
let color;
|
|
774
|
+
let any = false;
|
|
775
|
+
for (const side of BORDER_SIDES) {
|
|
776
|
+
const node = border[side];
|
|
777
|
+
const borderStyle = node ? attr(node, "style") : undefined;
|
|
778
|
+
if (!node || borderStyle === undefined || borderStyle === "none") {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
sides[side] = true;
|
|
782
|
+
any = true;
|
|
783
|
+
color ??= resolveColor(node["color"], theme, indexedColors);
|
|
784
|
+
}
|
|
785
|
+
if (any) {
|
|
786
|
+
style.borderSides = sides;
|
|
787
|
+
style.borderColor = color ?? "#000000";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function isOn(node) {
|
|
791
|
+
const val = attr(node, "val");
|
|
792
|
+
return val === undefined || val === "1" || val.toLowerCase() === "true";
|
|
793
|
+
}
|
|
794
|
+
function isDateFormat(numFmtId, code) {
|
|
795
|
+
if (BUILTIN_DATE_FORMAT_IDS.has(numFmtId)) {
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
if (!code) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
const stripped = code.replace(/\[[^\]]*\]/g, "").replace(/"[^"]*"/g, "").replace(/\\./g, "");
|
|
802
|
+
return /[dmyhs]/i.test(stripped) && !/^[^dmyhs]*$/i.test(stripped);
|
|
803
|
+
}
|
|
804
|
+
function resolveColor(node, theme, indexedColors) {
|
|
805
|
+
if (!node || typeof node !== "object") {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const rgb = attr(node, "rgb");
|
|
809
|
+
if (rgb) {
|
|
810
|
+
return applyTint(argbToHex(rgb), numAttr(node, "tint"));
|
|
811
|
+
}
|
|
812
|
+
const themeIndex = numAttr(node, "theme");
|
|
813
|
+
if (themeIndex !== undefined) {
|
|
814
|
+
const base = theme[themeIndex];
|
|
815
|
+
if (base) {
|
|
816
|
+
return applyTint(base, numAttr(node, "tint"));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const indexed = numAttr(node, "indexed");
|
|
820
|
+
if (indexed !== undefined) {
|
|
821
|
+
const base = indexedColors[indexed];
|
|
822
|
+
if (base) {
|
|
823
|
+
return applyTint(base, numAttr(node, "tint"));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
function argbToHex(argb) {
|
|
829
|
+
const value = argb.replace(/^#/, "");
|
|
830
|
+
const rgb = value.length === 8 ? value.slice(2) : value;
|
|
831
|
+
return `#${rgb.toLowerCase()}`;
|
|
832
|
+
}
|
|
833
|
+
function applyTint(hex, tint) {
|
|
834
|
+
if (!tint) {
|
|
835
|
+
return hex;
|
|
836
|
+
}
|
|
837
|
+
const value = hex.replace("#", "");
|
|
838
|
+
const channels = [0, 2, 4].map((offset) => Number.parseInt(value.slice(offset, offset + 2), 16));
|
|
839
|
+
const tinted = channels.map((channel) => {
|
|
840
|
+
const next = tint < 0 ? channel * (1 + tint) : channel * (1 - tint) + 255 * tint;
|
|
841
|
+
return Math.max(0, Math.min(255, Math.round(next)));
|
|
842
|
+
});
|
|
843
|
+
return `#${tinted.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
844
|
+
}
|
|
845
|
+
function parseThemeColors(pkg) {
|
|
846
|
+
const themePath = pkg.list("xl/theme/").find((path) => path.endsWith(".xml"));
|
|
847
|
+
if (!themePath) {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
const text = pkg.text(themePath);
|
|
851
|
+
if (!text) {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
const scheme = /<a:clrScheme[\s\S]*?<\/a:clrScheme>/.exec(text)?.[0];
|
|
855
|
+
if (!scheme) {
|
|
856
|
+
return [];
|
|
857
|
+
}
|
|
858
|
+
const colors = [];
|
|
859
|
+
const entry = /<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)>([\s\S]*?)<\/a:\1>/g;
|
|
860
|
+
let match;
|
|
861
|
+
while ((match = entry.exec(scheme)) !== null) {
|
|
862
|
+
const body = match[2] ?? "";
|
|
863
|
+
const srgb = /<a:srgbClr val="([0-9A-Fa-f]{6})"/.exec(body)?.[1];
|
|
864
|
+
const sys = /<a:sysClr[^>]*lastClr="([0-9A-Fa-f]{6})"/.exec(body)?.[1];
|
|
865
|
+
colors.push(`#${(srgb ?? sys ?? "000000").toLowerCase()}`);
|
|
866
|
+
}
|
|
867
|
+
if (colors.length >= 4) {
|
|
868
|
+
const [dk1, lt1, dk2, lt2, ...rest] = colors;
|
|
869
|
+
return [lt1, dk1, lt2, dk2, ...rest];
|
|
870
|
+
}
|
|
871
|
+
return colors;
|
|
872
|
+
}
|
|
873
|
+
function parseIndexedColors(styleSheet) {
|
|
874
|
+
const colors = asArray(styleSheet["colors"]?.["indexedColors"]?.["rgbColor"]);
|
|
875
|
+
if (colors.length > 0) {
|
|
876
|
+
return colors.map((color) => argbToHex(attr(color, "rgb") ?? "FF000000"));
|
|
877
|
+
}
|
|
878
|
+
return DEFAULT_INDEXED_COLORS;
|
|
879
|
+
}
|
|
880
|
+
var DEFAULT_INDEXED_COLORS = [
|
|
881
|
+
"#000000",
|
|
882
|
+
"#ffffff",
|
|
883
|
+
"#ff0000",
|
|
884
|
+
"#00ff00",
|
|
885
|
+
"#0000ff",
|
|
886
|
+
"#ffff00",
|
|
887
|
+
"#ff00ff",
|
|
888
|
+
"#00ffff",
|
|
889
|
+
"#000000",
|
|
890
|
+
"#ffffff",
|
|
891
|
+
"#ff0000",
|
|
892
|
+
"#00ff00",
|
|
893
|
+
"#0000ff",
|
|
894
|
+
"#ffff00",
|
|
895
|
+
"#ff00ff",
|
|
896
|
+
"#00ffff",
|
|
897
|
+
"#800000",
|
|
898
|
+
"#008000",
|
|
899
|
+
"#000080",
|
|
900
|
+
"#808000",
|
|
901
|
+
"#800080",
|
|
902
|
+
"#008080",
|
|
903
|
+
"#c0c0c0",
|
|
904
|
+
"#808080",
|
|
905
|
+
"#9999ff",
|
|
906
|
+
"#993366",
|
|
907
|
+
"#ffffcc",
|
|
908
|
+
"#ccffff",
|
|
909
|
+
"#660066",
|
|
910
|
+
"#ff8080",
|
|
911
|
+
"#0066cc",
|
|
912
|
+
"#ccccff",
|
|
913
|
+
"#000080",
|
|
914
|
+
"#ff00ff",
|
|
915
|
+
"#ffff00",
|
|
916
|
+
"#00ffff",
|
|
917
|
+
"#800080",
|
|
918
|
+
"#800000",
|
|
919
|
+
"#008080",
|
|
920
|
+
"#0000ff",
|
|
921
|
+
"#00ccff",
|
|
922
|
+
"#ccffff",
|
|
923
|
+
"#ccffcc",
|
|
924
|
+
"#ffff99",
|
|
925
|
+
"#99ccff",
|
|
926
|
+
"#ff99cc",
|
|
927
|
+
"#cc99ff",
|
|
928
|
+
"#ffcc99",
|
|
929
|
+
"#3366ff",
|
|
930
|
+
"#33cccc",
|
|
931
|
+
"#99cc00",
|
|
932
|
+
"#ffcc00",
|
|
933
|
+
"#ff9900",
|
|
934
|
+
"#ff6600",
|
|
935
|
+
"#666699",
|
|
936
|
+
"#969696",
|
|
937
|
+
"#003366",
|
|
938
|
+
"#339966",
|
|
939
|
+
"#003300",
|
|
940
|
+
"#333300",
|
|
941
|
+
"#993300",
|
|
942
|
+
"#993366",
|
|
943
|
+
"#333399",
|
|
944
|
+
"#333333"
|
|
945
|
+
];
|
|
946
|
+
|
|
947
|
+
// src/translate/formula.ts
|
|
948
|
+
var import_formula_engine = require("@ricsam/formula-engine");
|
|
949
|
+
var supportedFunctionNames;
|
|
950
|
+
function getSupportedFunctions() {
|
|
951
|
+
if (!supportedFunctionNames) {
|
|
952
|
+
const names = new Set;
|
|
953
|
+
for (const descriptor of import_formula_engine.getFormulaFunctionCatalog()) {
|
|
954
|
+
names.add(descriptor.name.toUpperCase());
|
|
955
|
+
for (const alias of descriptor.aliases ?? []) {
|
|
956
|
+
names.add(alias.toUpperCase());
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
supportedFunctionNames = names;
|
|
960
|
+
}
|
|
961
|
+
return supportedFunctionNames;
|
|
962
|
+
}
|
|
963
|
+
var XLFN_PREFIX = /_xlfn\.(_xlws\.)?/g;
|
|
964
|
+
var IMPLICIT_INTERSECTION = /(?<![\w$"'\]\[])@(?=[A-Za-z_\\])/g;
|
|
965
|
+
var THIS_ROW_REFERENCE = /(?:'((?:[^']|'')+)'|([A-Za-z_\\][\w.\\]*))?\[\[#This ?Row\],\s*\[([^\]]*)\](?:\s*:\s*\[([^\]]*)\])?\]/gi;
|
|
966
|
+
function translateFormula(rawFormula, options = {}) {
|
|
967
|
+
const diagnostics = [];
|
|
968
|
+
let body = rawFormula.startsWith("=") ? rawFormula.slice(1) : rawFormula;
|
|
969
|
+
body = body.replace(XLFN_PREFIX, "");
|
|
970
|
+
body = body.replace(IMPLICIT_INTERSECTION, "");
|
|
971
|
+
body = contractThisRowReferences(body, options.containingTableName);
|
|
972
|
+
for (const name of findUnsupportedFunctions(body)) {
|
|
973
|
+
diagnostics.push({
|
|
974
|
+
severity: "warning",
|
|
975
|
+
code: "unsupported-function",
|
|
976
|
+
message: `The engine does not implement ${name}(). The formula was imported ` + `unchanged and will evaluate to an error.`,
|
|
977
|
+
sheetName: options.sheetName,
|
|
978
|
+
cellReference: options.cellReference,
|
|
979
|
+
source: rawFormula
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
return { formula: `=${body}`, diagnostics };
|
|
983
|
+
}
|
|
984
|
+
function findUnsupportedFunctions(formulaBody) {
|
|
985
|
+
const supported = getSupportedFunctions();
|
|
986
|
+
const found = new Set;
|
|
987
|
+
let inString = false;
|
|
988
|
+
let identifier = "";
|
|
989
|
+
for (let i = 0;i < formulaBody.length; i++) {
|
|
990
|
+
const char = formulaBody[i];
|
|
991
|
+
if (inString) {
|
|
992
|
+
if (char === '"') {
|
|
993
|
+
if (formulaBody[i + 1] === '"') {
|
|
994
|
+
i++;
|
|
995
|
+
} else {
|
|
996
|
+
inString = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (char === '"') {
|
|
1002
|
+
inString = true;
|
|
1003
|
+
identifier = "";
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (/[A-Za-z0-9_.]/.test(char)) {
|
|
1007
|
+
identifier += char;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (char === "(" && identifier.length > 0) {
|
|
1011
|
+
const name = identifier.toUpperCase();
|
|
1012
|
+
if (/^[A-Z_][A-Z0-9_.]*$/.test(name) && !supported.has(name)) {
|
|
1013
|
+
found.add(name);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
identifier = "";
|
|
1017
|
+
}
|
|
1018
|
+
return Array.from(found);
|
|
1019
|
+
}
|
|
1020
|
+
function offsetFormulaReferences(formulaBody, deltaCols, deltaRows) {
|
|
1021
|
+
if (deltaCols === 0 && deltaRows === 0) {
|
|
1022
|
+
return formulaBody;
|
|
1023
|
+
}
|
|
1024
|
+
const reference = /(\$?)([A-Za-z]{1,3})(\$?)(\d{1,7})(?![\w(])/g;
|
|
1025
|
+
let result = "";
|
|
1026
|
+
let lastIndex = 0;
|
|
1027
|
+
let inString = false;
|
|
1028
|
+
for (let i = 0;i < formulaBody.length; i++) {
|
|
1029
|
+
if (formulaBody[i] === '"') {
|
|
1030
|
+
if (inString && formulaBody[i + 1] === '"') {
|
|
1031
|
+
i++;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
inString = !inString;
|
|
1035
|
+
}
|
|
1036
|
+
if (inString) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
reference.lastIndex = i;
|
|
1040
|
+
const match = reference.exec(formulaBody);
|
|
1041
|
+
if (!match || match.index !== i) {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const [whole, colAbs, colLabel, rowAbs, rowDigits] = match;
|
|
1045
|
+
const before = i > 0 ? formulaBody[i - 1] : "";
|
|
1046
|
+
if (/[A-Za-z0-9_.]/.test(before)) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
result += formulaBody.slice(lastIndex, i);
|
|
1050
|
+
result += shiftReference(colAbs, colLabel, rowAbs, rowDigits, deltaCols, deltaRows);
|
|
1051
|
+
lastIndex = i + whole.length;
|
|
1052
|
+
i = lastIndex - 1;
|
|
1053
|
+
}
|
|
1054
|
+
result += formulaBody.slice(lastIndex);
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|
|
1057
|
+
function shiftReference(colAbs, colLabel, rowAbs, rowDigits, deltaCols, deltaRows) {
|
|
1058
|
+
const colIndex = columnLabelToIndex2(colLabel);
|
|
1059
|
+
const rowIndex = Number.parseInt(rowDigits, 10) - 1;
|
|
1060
|
+
const newColIndex = colAbs ? colIndex : colIndex + deltaCols;
|
|
1061
|
+
const newRowIndex = rowAbs ? rowIndex : rowIndex + deltaRows;
|
|
1062
|
+
if (newColIndex < 0 || newRowIndex < 0) {
|
|
1063
|
+
return "#REF!";
|
|
1064
|
+
}
|
|
1065
|
+
return `${colAbs}${columnIndexToLabel2(newColIndex)}${rowAbs}${newRowIndex + 1}`;
|
|
1066
|
+
}
|
|
1067
|
+
function columnLabelToIndex2(label) {
|
|
1068
|
+
let index = 0;
|
|
1069
|
+
for (let i = 0;i < label.length; i++) {
|
|
1070
|
+
index = index * 26 + (label.toUpperCase().charCodeAt(i) - 64);
|
|
1071
|
+
}
|
|
1072
|
+
return index - 1;
|
|
1073
|
+
}
|
|
1074
|
+
function columnIndexToLabel2(index) {
|
|
1075
|
+
let label = "";
|
|
1076
|
+
let remaining = index + 1;
|
|
1077
|
+
while (remaining > 0) {
|
|
1078
|
+
const rest = (remaining - 1) % 26;
|
|
1079
|
+
label = String.fromCharCode(65 + rest) + label;
|
|
1080
|
+
remaining = Math.floor((remaining - rest) / 26);
|
|
1081
|
+
}
|
|
1082
|
+
return label;
|
|
1083
|
+
}
|
|
1084
|
+
function contractThisRowReferences(formulaBody, containingTableName) {
|
|
1085
|
+
return formulaBody.replace(THIS_ROW_REFERENCE, (whole, quotedTable, bareTable, startCol, endCol) => {
|
|
1086
|
+
const tableName = quotedTable ? String(quotedTable).replace(/''/g, "'") : bareTable ? String(bareTable) : undefined;
|
|
1087
|
+
const columns = endCol === undefined ? `[${startCol}]` : `[${startCol}]:[${endCol}]`;
|
|
1088
|
+
const isPlainSingleColumn = endCol === undefined && /^[^[\]@#'"]+$/.test(String(startCol));
|
|
1089
|
+
const reference = isPlainSingleColumn ? `@${startCol}` : `@${columns}`;
|
|
1090
|
+
if (tableName && containingTableName && tableName.toLowerCase() === containingTableName.toLowerCase()) {
|
|
1091
|
+
return `[${reference}]`;
|
|
1092
|
+
}
|
|
1093
|
+
if (!tableName) {
|
|
1094
|
+
return `[${reference}]`;
|
|
1095
|
+
}
|
|
1096
|
+
const quoted = quotedTable ? `'${String(quotedTable)}'` : tableName;
|
|
1097
|
+
return `${quoted}[${reference}]`;
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/translate/conditional-format.ts
|
|
1102
|
+
var import_formula_engine2 = require("@ricsam/formula-engine");
|
|
1103
|
+
function translateConditionalFormat(rule, options) {
|
|
1104
|
+
const diagnostics = [];
|
|
1105
|
+
const areas = [];
|
|
1106
|
+
for (const reference of rule.ranges) {
|
|
1107
|
+
const range = rangeReferenceToSpreadsheetRange(reference);
|
|
1108
|
+
if (range) {
|
|
1109
|
+
areas.push({ sheetName: options.sheetName, range });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (areas.length === 0) {
|
|
1113
|
+
return { diagnostics };
|
|
1114
|
+
}
|
|
1115
|
+
const anchor = rule.ranges[0]?.split(":")[0]?.replace(/\$/g, "") ?? "A1";
|
|
1116
|
+
if (rule.type === "colorScale") {
|
|
1117
|
+
return translateColorScale(rule, areas, options, diagnostics);
|
|
1118
|
+
}
|
|
1119
|
+
const condition = buildFormulaCondition(rule, anchor, options, diagnostics);
|
|
1120
|
+
if (!condition) {
|
|
1121
|
+
return { diagnostics };
|
|
1122
|
+
}
|
|
1123
|
+
return { result: { areas, condition }, diagnostics };
|
|
1124
|
+
}
|
|
1125
|
+
function buildFormulaCondition(rule, anchor, options, diagnostics) {
|
|
1126
|
+
const color = ruleColor(rule, options);
|
|
1127
|
+
if (!color) {
|
|
1128
|
+
diagnostics.push({
|
|
1129
|
+
severity: "warning",
|
|
1130
|
+
code: "unsupported-conditional-format",
|
|
1131
|
+
message: `A ${rule.type} rule was skipped because its format has no fill or ` + `font colour the engine can apply.`,
|
|
1132
|
+
sheetName: options.sheetName
|
|
1133
|
+
});
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const expression = buildExpression(rule, anchor, options, diagnostics);
|
|
1137
|
+
if (!expression) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const translated = translateFormula(expression, {
|
|
1141
|
+
sheetName: options.sheetName,
|
|
1142
|
+
cellReference: anchor
|
|
1143
|
+
});
|
|
1144
|
+
diagnostics.push(...translated.diagnostics);
|
|
1145
|
+
return { type: "formula", formula: translated.formula, color };
|
|
1146
|
+
}
|
|
1147
|
+
function buildExpression(rule, anchor, options, diagnostics) {
|
|
1148
|
+
const [first, second] = rule.formulas;
|
|
1149
|
+
switch (rule.type) {
|
|
1150
|
+
case "expression":
|
|
1151
|
+
return first ? `=${first}` : undefined;
|
|
1152
|
+
case "cellIs":
|
|
1153
|
+
return cellIsExpression(rule.operator, anchor, first, second);
|
|
1154
|
+
case "containsText":
|
|
1155
|
+
return rule.text === undefined ? undefined : `=IFERROR(FIND(${quote(rule.text)},${anchor})>0,FALSE)`;
|
|
1156
|
+
case "notContainsText":
|
|
1157
|
+
return rule.text === undefined ? undefined : `=IFERROR(FIND(${quote(rule.text)},${anchor})>0,FALSE)=FALSE`;
|
|
1158
|
+
case "beginsWith":
|
|
1159
|
+
return rule.text === undefined ? undefined : `=EXACT(LEFT(${anchor},${rule.text.length}),${quote(rule.text)})`;
|
|
1160
|
+
case "endsWith":
|
|
1161
|
+
return rule.text === undefined ? undefined : `=EXACT(RIGHT(${anchor},${rule.text.length}),${quote(rule.text)})`;
|
|
1162
|
+
case "containsBlanks":
|
|
1163
|
+
return `=LEN(${anchor})=0`;
|
|
1164
|
+
case "notContainsBlanks":
|
|
1165
|
+
return `=LEN(${anchor})>0`;
|
|
1166
|
+
default:
|
|
1167
|
+
diagnostics.push({
|
|
1168
|
+
severity: "warning",
|
|
1169
|
+
code: "unsupported-conditional-format",
|
|
1170
|
+
message: `The engine cannot express a "${rule.type}" conditional formatting ` + `rule, so it was skipped.`,
|
|
1171
|
+
sheetName: options.sheetName
|
|
1172
|
+
});
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function cellIsExpression(operator, anchor, first, second) {
|
|
1177
|
+
if (first === undefined) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
switch (operator) {
|
|
1181
|
+
case "equal":
|
|
1182
|
+
return `=${anchor}=${first}`;
|
|
1183
|
+
case "notEqual":
|
|
1184
|
+
return `=${anchor}<>${first}`;
|
|
1185
|
+
case "greaterThan":
|
|
1186
|
+
return `=${anchor}>${first}`;
|
|
1187
|
+
case "greaterThanOrEqual":
|
|
1188
|
+
return `=${anchor}>=${first}`;
|
|
1189
|
+
case "lessThan":
|
|
1190
|
+
return `=${anchor}<${first}`;
|
|
1191
|
+
case "lessThanOrEqual":
|
|
1192
|
+
return `=${anchor}<=${first}`;
|
|
1193
|
+
case "between":
|
|
1194
|
+
return second === undefined ? undefined : `=AND(${anchor}>=${first},${anchor}<=${second})`;
|
|
1195
|
+
case "notBetween":
|
|
1196
|
+
return second === undefined ? undefined : `=OR(${anchor}<${first},${anchor}>${second})`;
|
|
1197
|
+
default:
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function translateColorScale(rule, areas, options, diagnostics) {
|
|
1202
|
+
const scale = rule.colorScale;
|
|
1203
|
+
if (!scale || scale.cfvo.length < 2 || scale.colors.length < 2) {
|
|
1204
|
+
return { diagnostics };
|
|
1205
|
+
}
|
|
1206
|
+
if (scale.cfvo.length > 2) {
|
|
1207
|
+
diagnostics.push({
|
|
1208
|
+
severity: "info",
|
|
1209
|
+
code: "unsupported-conditional-format",
|
|
1210
|
+
message: "A three-colour scale was imported as a two-colour gradient between " + "its lowest and highest stops; the midpoint colour was dropped.",
|
|
1211
|
+
sheetName: options.sheetName
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
const firstStop = scale.cfvo[0];
|
|
1215
|
+
const lastStop = scale.cfvo[scale.cfvo.length - 1];
|
|
1216
|
+
const firstColor = toLch(scale.colors[0]);
|
|
1217
|
+
const lastColor = toLch(scale.colors[scale.colors.length - 1]);
|
|
1218
|
+
if (!firstColor || !lastColor) {
|
|
1219
|
+
return { diagnostics };
|
|
1220
|
+
}
|
|
1221
|
+
const min = firstStop.type === "num" && firstStop.val !== undefined ? {
|
|
1222
|
+
type: "number",
|
|
1223
|
+
color: firstColor,
|
|
1224
|
+
valueFormula: `=${firstStop.val}`
|
|
1225
|
+
} : { type: "lowest_value", color: firstColor };
|
|
1226
|
+
const max = lastStop.type === "num" && lastStop.val !== undefined ? {
|
|
1227
|
+
type: "number",
|
|
1228
|
+
color: lastColor,
|
|
1229
|
+
valueFormula: `=${lastStop.val}`
|
|
1230
|
+
} : { type: "highest_value", color: lastColor };
|
|
1231
|
+
if (firstStop.type !== "num" && firstStop.type !== "min" || lastStop.type !== "num" && lastStop.type !== "max") {
|
|
1232
|
+
diagnostics.push({
|
|
1233
|
+
severity: "info",
|
|
1234
|
+
code: "unsupported-conditional-format",
|
|
1235
|
+
message: `A colour scale using "${firstStop.type}"/"${lastStop.type}" stops was ` + "imported as a gradient across the range's own lowest and highest " + "values.",
|
|
1236
|
+
sheetName: options.sheetName
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
result: { areas, condition: { type: "gradient", min, max } },
|
|
1241
|
+
diagnostics
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function ruleColor(rule, options) {
|
|
1245
|
+
if (rule.dxfId === undefined) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const format = options.differentialFormats[rule.dxfId];
|
|
1249
|
+
if (!format) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const hex = format.backgroundColor ?? format.color;
|
|
1253
|
+
return hex ? import_formula_engine2.hexToLch(hex) : undefined;
|
|
1254
|
+
}
|
|
1255
|
+
function toLch(argb) {
|
|
1256
|
+
if (!argb) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const value = argb.replace(/^#/, "");
|
|
1260
|
+
const rgb = value.length === 8 ? value.slice(2) : value;
|
|
1261
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(rgb)) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
return import_formula_engine2.hexToLch(`#${rgb.toLowerCase()}`);
|
|
1265
|
+
}
|
|
1266
|
+
function quote(text) {
|
|
1267
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/translate/dates.ts
|
|
1271
|
+
var DAY_MS = 86400000;
|
|
1272
|
+
var EPOCH_1900_UTC = Date.UTC(1899, 11, 30);
|
|
1273
|
+
var EPOCH_1904_UTC = Date.UTC(1904, 0, 1);
|
|
1274
|
+
function excelSerialToIso(serial, date1904 = false) {
|
|
1275
|
+
if (!Number.isFinite(serial)) {
|
|
1276
|
+
return String(serial);
|
|
1277
|
+
}
|
|
1278
|
+
const epoch = date1904 ? EPOCH_1904_UTC : EPOCH_1900_UTC;
|
|
1279
|
+
const adjusted = !date1904 && serial < 60 && serial >= 1 ? serial + 1 : serial;
|
|
1280
|
+
const wholeDays = Math.floor(adjusted);
|
|
1281
|
+
const fraction = adjusted - wholeDays;
|
|
1282
|
+
const ms = epoch + wholeDays * DAY_MS + Math.round(fraction * DAY_MS);
|
|
1283
|
+
const date = new Date(ms);
|
|
1284
|
+
if (Number.isNaN(date.getTime())) {
|
|
1285
|
+
return String(serial);
|
|
1286
|
+
}
|
|
1287
|
+
const iso = date.toISOString();
|
|
1288
|
+
if (fraction === 0) {
|
|
1289
|
+
return iso.slice(0, 10);
|
|
1290
|
+
}
|
|
1291
|
+
if (wholeDays === 0) {
|
|
1292
|
+
return iso.slice(11, 19);
|
|
1293
|
+
}
|
|
1294
|
+
return iso.slice(0, 19).replace("T", " ");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/convert.ts
|
|
1298
|
+
var BUILTIN_DEFINED_NAMES = new Set([
|
|
1299
|
+
"_xlnm.print_area",
|
|
1300
|
+
"_xlnm.print_titles",
|
|
1301
|
+
"_xlnm._filterdatabase",
|
|
1302
|
+
"_xlnm.criteria",
|
|
1303
|
+
"_xlnm.extract",
|
|
1304
|
+
"_xlnm.database",
|
|
1305
|
+
"_xlnm.sheet_title"
|
|
1306
|
+
]);
|
|
1307
|
+
async function convertWorkbook(source, options = {}) {
|
|
1308
|
+
const {
|
|
1309
|
+
styles: importStyles = true,
|
|
1310
|
+
conditionalFormatting: importConditionalFormatting = true,
|
|
1311
|
+
tables: importTables = true,
|
|
1312
|
+
definedNames: importDefinedNames = true,
|
|
1313
|
+
cellMetadata: importCellMetadata = true,
|
|
1314
|
+
convertDates = true
|
|
1315
|
+
} = options;
|
|
1316
|
+
const diagnostics = [];
|
|
1317
|
+
const pkg = await readXlsxPackage(source);
|
|
1318
|
+
const sharedStrings = parseSharedStrings(pkg);
|
|
1319
|
+
const { sheets: sheetEntries, definedNames, date1904 } = parseWorkbook(pkg);
|
|
1320
|
+
const styleTable = parseStyles(pkg);
|
|
1321
|
+
const selected = options.sheets ? sheetEntries.filter((entry) => options.sheets.includes(entry.name)) : sheetEntries;
|
|
1322
|
+
const sheets = [];
|
|
1323
|
+
const tables = [];
|
|
1324
|
+
const cellStyles = [];
|
|
1325
|
+
const conditionalStyles = [];
|
|
1326
|
+
const importedSheetNames = new Set;
|
|
1327
|
+
for (const entry of selected) {
|
|
1328
|
+
const worksheet = parseWorksheet(pkg, entry.partPath, sharedStrings);
|
|
1329
|
+
importedSheetNames.add(entry.name);
|
|
1330
|
+
const sheetTables = importTables ? parseTables(pkg, entry.partPath) : [];
|
|
1331
|
+
const tableRanges = sheetTables.map((table) => ({ name: table.displayName, range: parseA1Range(table.ref) })).filter((entryTable) => entryTable.range !== undefined);
|
|
1332
|
+
const tableAt = (colIndex, rowIndex) => tableRanges.find(({ range }) => colIndex >= range.start.colIndex && colIndex <= range.end.colIndex && rowIndex >= range.start.rowIndex && rowIndex <= range.end.rowIndex)?.name;
|
|
1333
|
+
const content = new Map;
|
|
1334
|
+
const cellMetadata = new Map;
|
|
1335
|
+
const styleGroups = new Map;
|
|
1336
|
+
for (const cell of worksheet.cells) {
|
|
1337
|
+
const format = cell.styleIndex === undefined ? undefined : styleTable.formats[cell.styleIndex];
|
|
1338
|
+
const value = cellValue(cell, format, {
|
|
1339
|
+
convertDates,
|
|
1340
|
+
date1904,
|
|
1341
|
+
sheetName: entry.name,
|
|
1342
|
+
diagnostics,
|
|
1343
|
+
containingTableName: tableAt(cell.colIndex, cell.rowIndex)
|
|
1344
|
+
});
|
|
1345
|
+
if (value !== undefined) {
|
|
1346
|
+
content.set(cell.reference, value);
|
|
1347
|
+
}
|
|
1348
|
+
if (importStyles && cell.styleIndex !== undefined && format) {
|
|
1349
|
+
if (Object.keys(format.style).length > 0) {
|
|
1350
|
+
const group = styleGroups.get(cell.styleIndex);
|
|
1351
|
+
if (group) {
|
|
1352
|
+
group.push(cell);
|
|
1353
|
+
} else {
|
|
1354
|
+
styleGroups.set(cell.styleIndex, [cell]);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (importCellMetadata) {
|
|
1359
|
+
const metadata = {};
|
|
1360
|
+
if (format?.numberFormat) {
|
|
1361
|
+
metadata.numberFormat = format.numberFormat;
|
|
1362
|
+
}
|
|
1363
|
+
const hyperlink = worksheet.hyperlinks[cell.reference];
|
|
1364
|
+
if (hyperlink) {
|
|
1365
|
+
metadata.hyperlink = hyperlink;
|
|
1366
|
+
}
|
|
1367
|
+
if (Object.keys(metadata).length > 0) {
|
|
1368
|
+
cellMetadata.set(cell.reference, metadata);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (importStyles) {
|
|
1373
|
+
for (const [styleIndex, cells] of styleGroups) {
|
|
1374
|
+
const format = styleTable.formats[styleIndex];
|
|
1375
|
+
if (!format) {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
cellStyles.push({
|
|
1379
|
+
areas: coalesceCellsIntoAreas(entry.name, cells),
|
|
1380
|
+
style: format.style
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
if (importConditionalFormatting) {
|
|
1385
|
+
const ordered = [...worksheet.conditionalFormats].sort((a, b) => b.priority - a.priority);
|
|
1386
|
+
for (const rule of ordered) {
|
|
1387
|
+
const { result, diagnostics: ruleDiagnostics } = translateConditionalFormat(rule, {
|
|
1388
|
+
sheetName: entry.name,
|
|
1389
|
+
differentialFormats: styleTable.differentialFormats
|
|
1390
|
+
});
|
|
1391
|
+
diagnostics.push(...ruleDiagnostics);
|
|
1392
|
+
if (result) {
|
|
1393
|
+
conditionalStyles.push(result);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (importTables) {
|
|
1398
|
+
for (const table of sheetTables) {
|
|
1399
|
+
const converted = convertTable(table, entry.name, diagnostics);
|
|
1400
|
+
if (converted) {
|
|
1401
|
+
tables.push(converted);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const sheetMetadata = {};
|
|
1406
|
+
if (Object.keys(worksheet.columnWidths).length > 0) {
|
|
1407
|
+
sheetMetadata.columnWidths = worksheet.columnWidths;
|
|
1408
|
+
}
|
|
1409
|
+
if (Object.keys(worksheet.rowHeights).length > 0) {
|
|
1410
|
+
sheetMetadata.rowHeights = worksheet.rowHeights;
|
|
1411
|
+
}
|
|
1412
|
+
if (worksheet.merges.length > 0) {
|
|
1413
|
+
sheetMetadata.merges = worksheet.merges;
|
|
1414
|
+
}
|
|
1415
|
+
if (worksheet.frozen) {
|
|
1416
|
+
sheetMetadata.frozen = worksheet.frozen;
|
|
1417
|
+
}
|
|
1418
|
+
if (entry.state !== "visible") {
|
|
1419
|
+
sheetMetadata.visible = false;
|
|
1420
|
+
}
|
|
1421
|
+
sheets.push({
|
|
1422
|
+
name: entry.name,
|
|
1423
|
+
content,
|
|
1424
|
+
cellMetadata: cellMetadata.size > 0 ? cellMetadata : undefined,
|
|
1425
|
+
sheetMetadata: Object.keys(sheetMetadata).length > 0 ? sheetMetadata : undefined
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
const namedExpressions = importDefinedNames ? convertDefinedNames(definedNames, sheetEntries.map((entry) => entry.name), importedSheetNames, diagnostics) : [];
|
|
1429
|
+
const data = {
|
|
1430
|
+
sheets,
|
|
1431
|
+
tables: tables.length > 0 ? tables : undefined,
|
|
1432
|
+
namedExpressions: namedExpressions.length > 0 ? namedExpressions : undefined,
|
|
1433
|
+
cellStyles: cellStyles.length > 0 ? cellStyles : undefined,
|
|
1434
|
+
conditionalStyles: conditionalStyles.length > 0 ? conditionalStyles : undefined,
|
|
1435
|
+
workbookMetadata: {
|
|
1436
|
+
fileName: options.fileName,
|
|
1437
|
+
sheetOrder: sheetEntries.map((entry) => entry.name),
|
|
1438
|
+
diagnostics: diagnostics.length > 0 ? diagnostics : undefined
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
return { data, diagnostics };
|
|
1442
|
+
}
|
|
1443
|
+
function cellValue(cell, format, context) {
|
|
1444
|
+
if (cell.formula !== undefined) {
|
|
1445
|
+
const body = cell.formulaOffset ? offsetFormulaReferences(cell.formula, cell.formulaOffset.cols, cell.formulaOffset.rows) : cell.formula;
|
|
1446
|
+
const translated = translateFormula(body, {
|
|
1447
|
+
sheetName: context.sheetName,
|
|
1448
|
+
cellReference: cell.reference,
|
|
1449
|
+
containingTableName: context.containingTableName
|
|
1450
|
+
});
|
|
1451
|
+
context.diagnostics.push(...translated.diagnostics);
|
|
1452
|
+
return translated.formula;
|
|
1453
|
+
}
|
|
1454
|
+
switch (cell.kind) {
|
|
1455
|
+
case "number": {
|
|
1456
|
+
const value = cell.value;
|
|
1457
|
+
if (context.convertDates && format?.isDateFormat) {
|
|
1458
|
+
return excelSerialToIso(value, context.date1904);
|
|
1459
|
+
}
|
|
1460
|
+
return value;
|
|
1461
|
+
}
|
|
1462
|
+
case "boolean":
|
|
1463
|
+
return cell.value;
|
|
1464
|
+
case "string":
|
|
1465
|
+
return cell.value;
|
|
1466
|
+
case "date":
|
|
1467
|
+
return cell.value;
|
|
1468
|
+
case "error":
|
|
1469
|
+
return cell.value;
|
|
1470
|
+
case "empty":
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function convertTable(table, sheetName, diagnostics) {
|
|
1475
|
+
const parsed = parseA1Range(table.ref);
|
|
1476
|
+
if (!parsed) {
|
|
1477
|
+
diagnostics.push({
|
|
1478
|
+
severity: "warning",
|
|
1479
|
+
code: "skipped-table",
|
|
1480
|
+
message: `Table "${table.displayName}" has an unreadable range (${table.ref}).`,
|
|
1481
|
+
sheetName
|
|
1482
|
+
});
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (table.headerRowCount === 0) {
|
|
1486
|
+
diagnostics.push({
|
|
1487
|
+
severity: "warning",
|
|
1488
|
+
code: "skipped-table",
|
|
1489
|
+
message: `Table "${table.displayName}" has no header row, which the engine ` + `requires for structured references, so it was skipped.`,
|
|
1490
|
+
sheetName
|
|
1491
|
+
});
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const numCols = parsed.end.colIndex - parsed.start.colIndex + 1;
|
|
1495
|
+
const dataRows = parsed.end.rowIndex - parsed.start.rowIndex + 1 - table.headerRowCount - table.totalsRowCount;
|
|
1496
|
+
if (numCols <= 0 || dataRows < 0) {
|
|
1497
|
+
diagnostics.push({
|
|
1498
|
+
severity: "warning",
|
|
1499
|
+
code: "skipped-table",
|
|
1500
|
+
message: `Table "${table.displayName}" has an empty range (${table.ref}).`,
|
|
1501
|
+
sheetName
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
name: table.displayName,
|
|
1507
|
+
sheetName,
|
|
1508
|
+
start: toA1(parsed.start),
|
|
1509
|
+
numRows: { type: "number", value: dataRows },
|
|
1510
|
+
numCols
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
function convertDefinedNames(definedNames, sheetNames, importedSheetNames, diagnostics) {
|
|
1514
|
+
const result = [];
|
|
1515
|
+
for (const definedName of definedNames) {
|
|
1516
|
+
if (BUILTIN_DEFINED_NAMES.has(definedName.name.toLowerCase())) {
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (definedName.hidden || definedName.formula.trim() === "") {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (definedName.formula.includes("#REF!")) {
|
|
1523
|
+
diagnostics.push({
|
|
1524
|
+
severity: "warning",
|
|
1525
|
+
code: "skipped-defined-name",
|
|
1526
|
+
message: `Defined name "${definedName.name}" refers to a deleted range.`,
|
|
1527
|
+
source: definedName.formula
|
|
1528
|
+
});
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
const sheetName = definedName.localSheetId === undefined ? undefined : sheetNames[definedName.localSheetId];
|
|
1532
|
+
if (sheetName !== undefined && !importedSheetNames.has(sheetName)) {
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const translated = translateFormula(definedName.formula);
|
|
1536
|
+
diagnostics.push(...translated.diagnostics);
|
|
1537
|
+
result.push({
|
|
1538
|
+
name: definedName.name,
|
|
1539
|
+
expression: translated.formula.slice(1),
|
|
1540
|
+
sheetName
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
return result;
|
|
1544
|
+
}
|
|
1545
|
+
function coalesceCellsIntoAreas(sheetName, cells) {
|
|
1546
|
+
const byRow = new Map;
|
|
1547
|
+
for (const cell of cells) {
|
|
1548
|
+
const row = byRow.get(cell.rowIndex);
|
|
1549
|
+
if (row) {
|
|
1550
|
+
row.push(cell.colIndex);
|
|
1551
|
+
} else {
|
|
1552
|
+
byRow.set(cell.rowIndex, [cell.colIndex]);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const runs = [];
|
|
1556
|
+
for (const [rowIndex, columns] of byRow) {
|
|
1557
|
+
columns.sort((a, b) => a - b);
|
|
1558
|
+
let startCol = columns[0];
|
|
1559
|
+
let previous = startCol;
|
|
1560
|
+
for (let i = 1;i < columns.length; i++) {
|
|
1561
|
+
const current = columns[i];
|
|
1562
|
+
if (current === previous + 1) {
|
|
1563
|
+
previous = current;
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
runs.push({ rowIndex, startCol, endCol: previous });
|
|
1567
|
+
startCol = current;
|
|
1568
|
+
previous = current;
|
|
1569
|
+
}
|
|
1570
|
+
runs.push({ rowIndex, startCol, endCol: previous });
|
|
1571
|
+
}
|
|
1572
|
+
runs.sort((a, b) => a.startCol - b.startCol || a.endCol - b.endCol || a.rowIndex - b.rowIndex);
|
|
1573
|
+
const areas = [];
|
|
1574
|
+
let index = 0;
|
|
1575
|
+
while (index < runs.length) {
|
|
1576
|
+
const first = runs[index];
|
|
1577
|
+
let lastRow = first.rowIndex;
|
|
1578
|
+
let next = index + 1;
|
|
1579
|
+
while (next < runs.length && runs[next].startCol === first.startCol && runs[next].endCol === first.endCol && runs[next].rowIndex === lastRow + 1) {
|
|
1580
|
+
lastRow = runs[next].rowIndex;
|
|
1581
|
+
next++;
|
|
1582
|
+
}
|
|
1583
|
+
areas.push({
|
|
1584
|
+
sheetName,
|
|
1585
|
+
range: toSpreadsheetRange({
|
|
1586
|
+
start: { colIndex: first.startCol, rowIndex: first.rowIndex },
|
|
1587
|
+
end: { colIndex: first.endCol, rowIndex: lastRow }
|
|
1588
|
+
})
|
|
1589
|
+
});
|
|
1590
|
+
index = next;
|
|
1591
|
+
}
|
|
1592
|
+
return areas;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// index.ts
|
|
1596
|
+
async function excelToFormulaEngine(source, options) {
|
|
1597
|
+
const { data } = await convertWorkbook(source, withFileName(source, options));
|
|
1598
|
+
return data;
|
|
1599
|
+
}
|
|
1600
|
+
async function excelToFormulaEngineDetailed(source, options) {
|
|
1601
|
+
return convertWorkbook(source, withFileName(source, options));
|
|
1602
|
+
}
|
|
1603
|
+
function withFileName(source, options) {
|
|
1604
|
+
if (options?.fileName !== undefined) {
|
|
1605
|
+
return options;
|
|
1606
|
+
}
|
|
1607
|
+
const name = typeof File !== "undefined" && source instanceof File ? source.name : undefined;
|
|
1608
|
+
return name ? { ...options, fileName: name } : { ...options };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
//# debugId=B8C6694D236575E064756E2164756E21
|