@dou-ui/sheet-xlsx 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.js +94 -0
- package/dist/messages.js.map +1 -0
- package/dist/read-styles.js +301 -0
- package/dist/read-styles.js.map +1 -0
- package/dist/read.js +1099 -0
- package/dist/read.js.map +1 -0
- package/dist/styles.js +124 -0
- package/dist/styles.js.map +1 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/messages.d.ts +20 -0
- package/dist/types/read-styles.d.ts +28 -0
- package/dist/types/read.d.ts +36 -0
- package/dist/types/styles.d.ts +33 -0
- package/dist/types/write.d.ts +13 -0
- package/dist/types/xml-read.d.ts +45 -0
- package/dist/types/xml.d.ts +8 -0
- package/dist/types/zip.d.ts +32 -0
- package/dist/write.js +799 -0
- package/dist/write.js.map +1 -0
- package/dist/xml-read.js +166 -0
- package/dist/xml-read.js.map +1 -0
- package/dist/xml.js +20 -0
- package/dist/xml.js.map +1 -0
- package/dist/zip.js +168 -0
- package/dist/zip.js.map +1 -0
- package/package.json +44 -4
package/dist/read.js
ADDED
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
import { mergeXlsxMessages, sheetXlsxMessagesFor } from "./messages.js";
|
|
2
|
+
import { XmlReader, attrsOf, childOf, childrenOf, isNotOff, isOff, isOn, parseXml } from "./xml-read.js";
|
|
3
|
+
import { colorOf, readStyles, themeColors } from "./read-styles.js";
|
|
4
|
+
import { pause, zipFiles } from "./zip.js";
|
|
5
|
+
import { SheetFileError, boundsOf, columnName, linkTarget, parseCell, parseRange, shiftFormula } from "@dou-ui/sheet/core";
|
|
6
|
+
import { dateText } from "@dou-ui/sheet/formula";
|
|
7
|
+
// src/read.ts
|
|
8
|
+
var ERRORS = {
|
|
9
|
+
notXlsx: "[@dou-ui/sheet-xlsx] The file isn't an .xlsx file.",
|
|
10
|
+
damaged: "[@dou-ui/sheet-xlsx] The file is damaged and could not be read.",
|
|
11
|
+
locked: "[@dou-ui/sheet-xlsx] The file is encrypted with a password."
|
|
12
|
+
};
|
|
13
|
+
var decoder = new TextDecoder();
|
|
14
|
+
/** Column widths: the characters the file counts in to px, as the file format works it out (padding included). */
|
|
15
|
+
var pxOfWidth = (width) => Math.trunc((256 * width + Math.trunc(128 / 7)) / 256 * 7);
|
|
16
|
+
/** Row heights: points to px. */
|
|
17
|
+
var pxOfHeight = (points) => Math.round(points / .75);
|
|
18
|
+
/** A path a relationship points to, from the part it is in. */
|
|
19
|
+
function resolvePath(from, target) {
|
|
20
|
+
if (target.startsWith("/")) return target.slice(1);
|
|
21
|
+
const parts = from.split("/").slice(0, -1);
|
|
22
|
+
for (const piece of target.split("/")) if (piece === "..") parts.pop();
|
|
23
|
+
else if (piece !== ".") parts.push(piece);
|
|
24
|
+
return parts.join("/");
|
|
25
|
+
}
|
|
26
|
+
/** Where a part's relationships are: `xl/workbook.xml` keeps them in `xl/_rels/workbook.xml.rels`. */
|
|
27
|
+
var relsPath = (part) => {
|
|
28
|
+
const slash = part.lastIndexOf("/");
|
|
29
|
+
return `${part.slice(0, slash + 1)}_rels/${part.slice(slash + 1)}.rels`;
|
|
30
|
+
};
|
|
31
|
+
/** Text of a run of text: `<t>`, or the `<t>` of each `<r>`. Phonetic guides are left out. */
|
|
32
|
+
function textOf(node) {
|
|
33
|
+
let out = "";
|
|
34
|
+
for (const child of node.children) if (child.name === "t") out += child.text;
|
|
35
|
+
else if (child.name === "r") out += textOf(child);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Text the file escapes as `_x000D_`, back to its characters. */
|
|
39
|
+
var unescapeX = (text) => text.includes("_x") ? text.replace(/_x([0-9a-f]{4})_/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))) : text;
|
|
40
|
+
/** A formula as the sheet writes it: with `=`, without the prefixes of newer functions. */
|
|
41
|
+
var formulaOf = (text) => `=${text.replace(/_xl(?:fn|ws)\./g, "")}`;
|
|
42
|
+
/** A value written in a rule's formula: a number, text in quotes, `TRUE` or `FALSE`; `undefined` otherwise. */
|
|
43
|
+
function literalOf(formula) {
|
|
44
|
+
if (formula == null) return void 0;
|
|
45
|
+
const text = formula.trim();
|
|
46
|
+
if (/^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text)) return Number(text);
|
|
47
|
+
if (/^"(?:[^"]|"")*"$/.test(text)) return text.slice(1, -1).replaceAll("\"\"", "\"");
|
|
48
|
+
if (/^true$/i.test(text)) return true;
|
|
49
|
+
if (/^false$/i.test(text)) return false;
|
|
50
|
+
}
|
|
51
|
+
/** A rule's cells as the sheet writes them: one range, or several. */
|
|
52
|
+
function rangesOf(sqref) {
|
|
53
|
+
const list = (sqref ?? "").trim().split(/\s+/).filter(Boolean);
|
|
54
|
+
if (!list.length) return null;
|
|
55
|
+
return list.length === 1 ? list[0] : list;
|
|
56
|
+
}
|
|
57
|
+
/** The top left cell of a rule's first range, which its formula is written for. */
|
|
58
|
+
function anchorOf(range) {
|
|
59
|
+
return (Array.isArray(range) ? range[0] : range).split(":")[0].replaceAll("$", "");
|
|
60
|
+
}
|
|
61
|
+
var CELL_IS = {
|
|
62
|
+
equal: "eq",
|
|
63
|
+
notEqual: "ne",
|
|
64
|
+
greaterThan: "gt",
|
|
65
|
+
greaterThanOrEqual: "ge",
|
|
66
|
+
lessThan: "lt",
|
|
67
|
+
lessThanOrEqual: "le",
|
|
68
|
+
between: "between",
|
|
69
|
+
notBetween: "notBetween"
|
|
70
|
+
};
|
|
71
|
+
var COMPARE = {
|
|
72
|
+
eq: "=",
|
|
73
|
+
ne: "<>",
|
|
74
|
+
gt: ">",
|
|
75
|
+
ge: ">=",
|
|
76
|
+
lt: "<",
|
|
77
|
+
le: "<="
|
|
78
|
+
};
|
|
79
|
+
/** Where the first relationship of a kind points. */
|
|
80
|
+
/** Days from 1900's day 0 to 1904's, as the two ways of counting dates differ. */
|
|
81
|
+
var DAYS_1904 = 1462;
|
|
82
|
+
/** Whether an older-style container holds an encrypted workbook, by its `EncryptionInfo` stream's name. */
|
|
83
|
+
function hasEncryptionInfo(bytes) {
|
|
84
|
+
const name = [..."EncryptionInfo"].flatMap((ch) => [ch.charCodeAt(0), 0]);
|
|
85
|
+
for (let i = bytes.indexOf(name[0]); i !== -1; i = bytes.indexOf(name[0], i + 1)) {
|
|
86
|
+
let j = 1;
|
|
87
|
+
while (j < name.length && bytes[i + j] === name[j]) j++;
|
|
88
|
+
if (j === name.length) return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/** What sets a run of text apart from another, leaving out its typeface. */
|
|
93
|
+
function runLook(run) {
|
|
94
|
+
const props = childOf(run, "rPr");
|
|
95
|
+
if (!props) return "";
|
|
96
|
+
return [
|
|
97
|
+
"b",
|
|
98
|
+
"i",
|
|
99
|
+
"u",
|
|
100
|
+
"strike",
|
|
101
|
+
"color",
|
|
102
|
+
"sz"
|
|
103
|
+
].map((part) => {
|
|
104
|
+
const node = childOf(props, part);
|
|
105
|
+
return node ? `${part}${JSON.stringify(node.attrs)}` : "";
|
|
106
|
+
}).join("");
|
|
107
|
+
}
|
|
108
|
+
/** Whether a string's runs differ in more than their typeface, as text mixing scripts is split by font. */
|
|
109
|
+
function hasRichRuns(si) {
|
|
110
|
+
const runs = childrenOf(si, "r");
|
|
111
|
+
return runs.length > 1 && new Set(runs.map(runLook)).size > 1;
|
|
112
|
+
}
|
|
113
|
+
var ofType = (rels, type) => [...rels.values()].find((rel) => rel.type.endsWith(`/${type}`))?.path;
|
|
114
|
+
/** Reads the file's warnings up, by what was lost and on which sheet. */
|
|
115
|
+
var Warnings = class {
|
|
116
|
+
messages;
|
|
117
|
+
list = /* @__PURE__ */ new Map();
|
|
118
|
+
constructor(messages) {
|
|
119
|
+
this.messages = messages;
|
|
120
|
+
}
|
|
121
|
+
add(code, sheet, count = 1) {
|
|
122
|
+
if (count <= 0) return;
|
|
123
|
+
const key = `${code}\u0000${sheet ?? ""}`;
|
|
124
|
+
const warning = this.list.get(key);
|
|
125
|
+
if (warning) warning.count = (warning.count ?? 1) + count;
|
|
126
|
+
else this.list.set(key, {
|
|
127
|
+
code,
|
|
128
|
+
message: this.messages.warnings[code],
|
|
129
|
+
count,
|
|
130
|
+
...sheet == null ? {} : { sheet }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
get all() {
|
|
134
|
+
return [...this.list.values()];
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Reads an .xlsx file as a workbook: values, formulas, number formats,
|
|
139
|
+
* styles, merges, frozen panes, sizes, hidden rows and columns, groups,
|
|
140
|
+
* notes, data validations, conditional formats and filters. What it can't
|
|
141
|
+
* bring over, such as borders or charts, is listed in `warnings`.
|
|
142
|
+
*/
|
|
143
|
+
async function readXlsx(file, options = {}) {
|
|
144
|
+
const messages = mergeXlsxMessages(sheetXlsxMessagesFor(options.locale), options.messages);
|
|
145
|
+
/** Why the file can't be read: for developers in `message`, for people in `reason`. */
|
|
146
|
+
const failed = (code, cause) => new SheetFileError(ERRORS[code], code, messages[code], cause ? { cause } : void 0);
|
|
147
|
+
const { signal } = options;
|
|
148
|
+
const bytes = file instanceof Uint8Array ? file : new Uint8Array(file instanceof ArrayBuffer ? file : await file.arrayBuffer());
|
|
149
|
+
signal?.throwIfAborted();
|
|
150
|
+
if (bytes[0] === 208 && bytes[1] === 207 && bytes[2] === 17 && bytes[3] === 224) throw failed(hasEncryptionInfo(bytes) ? "locked" : "notXlsx");
|
|
151
|
+
if (bytes[0] !== 80 || bytes[1] !== 75) throw failed("notXlsx");
|
|
152
|
+
let files;
|
|
153
|
+
try {
|
|
154
|
+
files = zipFiles(bytes);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw failed("damaged", error);
|
|
157
|
+
}
|
|
158
|
+
const pacing = {
|
|
159
|
+
signal,
|
|
160
|
+
lastPause: { at: performance.now() }
|
|
161
|
+
};
|
|
162
|
+
const readText = async (path) => {
|
|
163
|
+
const entry = files.get(path);
|
|
164
|
+
if (!entry) return void 0;
|
|
165
|
+
try {
|
|
166
|
+
return decoder.decode(await entry.read(pacing));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (signal?.aborted || String(error).includes("CompressionStream")) throw error;
|
|
169
|
+
throw failed("damaged", error);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const readNode = async (path) => {
|
|
173
|
+
const text = path ? await readText(path) : void 0;
|
|
174
|
+
return text == null ? void 0 : parseXml(text);
|
|
175
|
+
};
|
|
176
|
+
/** A part's relationships by id: where each points and what kind it is. */
|
|
177
|
+
const relsOf = async (part) => {
|
|
178
|
+
const out = /* @__PURE__ */ new Map();
|
|
179
|
+
const rels = await readNode(relsPath(part));
|
|
180
|
+
for (const rel of childrenOf(rels, "Relationship")) {
|
|
181
|
+
const external = rel.attrs.TargetMode === "External";
|
|
182
|
+
out.set(rel.attrs.Id ?? "", {
|
|
183
|
+
path: external ? rel.attrs.Target ?? "" : resolvePath(part, rel.attrs.Target ?? ""),
|
|
184
|
+
type: rel.attrs.Type ?? "",
|
|
185
|
+
external
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
};
|
|
190
|
+
const workbookPath = [...(await relsOf("")).values()].find((rel) => rel.type.endsWith("/officeDocument"))?.path ?? "xl/workbook.xml";
|
|
191
|
+
const workbook = await readNode(workbookPath);
|
|
192
|
+
if (!workbook || workbook.name !== "workbook") throw failed("notXlsx");
|
|
193
|
+
const workbookRels = await relsOf(workbookPath);
|
|
194
|
+
const warnings = new Warnings(messages);
|
|
195
|
+
const theme = themeColors(await readNode(ofType(workbookRels, "theme")));
|
|
196
|
+
const { looks, differentials } = readStyles(await readNode(ofType(workbookRels, "styles")), theme);
|
|
197
|
+
const strings = [];
|
|
198
|
+
const stringsXML = await readNode(ofType(workbookRels, "sharedStrings"));
|
|
199
|
+
const richText = /* @__PURE__ */ new Set();
|
|
200
|
+
for (const [i, si] of childrenOf(stringsXML, "si").entries()) {
|
|
201
|
+
strings.push(unescapeX(textOf(si)));
|
|
202
|
+
if (hasRichRuns(si)) richText.add(i);
|
|
203
|
+
}
|
|
204
|
+
for (const path of files.keys()) {
|
|
205
|
+
if (/^xl\/vbaProject\.bin$/i.test(path)) warnings.add("macro");
|
|
206
|
+
if (/^xl\/pivotTables\/[^/]+\.xml$/i.test(path)) warnings.add("pivot");
|
|
207
|
+
}
|
|
208
|
+
for (const name of childrenOf(childOf(workbook, "definedNames"), "definedName")) if (!name.attrs.name?.startsWith("_xlnm.")) warnings.add("name");
|
|
209
|
+
const date1904 = isOn(childOf(workbook, "workbookPr")?.attrs.date1904);
|
|
210
|
+
const tableNames = [];
|
|
211
|
+
for (const path of files.keys()) {
|
|
212
|
+
if (!/^xl\/tables\/[^/]+\.xml$/i.test(path)) continue;
|
|
213
|
+
const table = await readNode(path);
|
|
214
|
+
for (const key of ["name", "displayName"]) {
|
|
215
|
+
const tableName = table?.attrs[key];
|
|
216
|
+
if (tableName && !tableNames.includes(tableName)) tableNames.push(tableName);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const tableRef = tableNames.length ? new RegExp(`(?:^|[^\\w.])(?:${tableNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\[`, "i") : null;
|
|
220
|
+
const styleNames = /* @__PURE__ */ new Map();
|
|
221
|
+
const styles = {};
|
|
222
|
+
const styleName = (look) => {
|
|
223
|
+
if (!look.key) return void 0;
|
|
224
|
+
let name = styleNames.get(look.key);
|
|
225
|
+
if (!name) {
|
|
226
|
+
name = `style${styleNames.size + 1}`;
|
|
227
|
+
styleNames.set(look.key, name);
|
|
228
|
+
styles[name] = look.style;
|
|
229
|
+
}
|
|
230
|
+
return name;
|
|
231
|
+
};
|
|
232
|
+
const sheets = [];
|
|
233
|
+
const keepFormulas = options.formulas !== false;
|
|
234
|
+
for (const sheetNode of childrenOf(childOf(workbook, "sheets"), "sheet")) {
|
|
235
|
+
signal?.throwIfAborted();
|
|
236
|
+
const name = sheetNode.attrs.name ?? `Sheet${sheets.length + 1}`;
|
|
237
|
+
const rel = workbookRels.get(sheetNode.attrs.id ?? "");
|
|
238
|
+
if (!rel || !rel.type.endsWith("/worksheet")) {
|
|
239
|
+
if (rel?.type.endsWith("/chartsheet")) warnings.add("chart", name);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (sheetNode.attrs.state === "hidden" || sheetNode.attrs.state === "veryHidden") warnings.add("hiddenSheet", name);
|
|
243
|
+
const xml = await readText(rel.path);
|
|
244
|
+
if (xml == null) throw failed("damaged");
|
|
245
|
+
const sheetRels = await relsOf(rel.path);
|
|
246
|
+
const { data, links, passwords } = await readSheet(xml, name, {
|
|
247
|
+
strings,
|
|
248
|
+
richText,
|
|
249
|
+
looks,
|
|
250
|
+
differentials,
|
|
251
|
+
styleName,
|
|
252
|
+
keepFormulas,
|
|
253
|
+
warnings,
|
|
254
|
+
pacing,
|
|
255
|
+
date1904,
|
|
256
|
+
tableRef
|
|
257
|
+
});
|
|
258
|
+
warnings.add("hyperlink", name, addLinks(data, links, sheetRels));
|
|
259
|
+
warnings.add("password", name, passwords);
|
|
260
|
+
const comments = await readNode(ofType(sheetRels, "comments"));
|
|
261
|
+
if (comments) addNotes(data, comments);
|
|
262
|
+
for (const { type, path } of sheetRels.values()) {
|
|
263
|
+
if (type.endsWith("/drawing")) {
|
|
264
|
+
const drawing = await relsOf(path);
|
|
265
|
+
for (const { type: kind } of drawing.values()) if (kind.endsWith("/chart")) warnings.add("chart", name);
|
|
266
|
+
else if (kind.endsWith("/image")) warnings.add("image", name);
|
|
267
|
+
}
|
|
268
|
+
if (type.endsWith("/pivotTable")) warnings.add("pivot", name);
|
|
269
|
+
if (type.endsWith("/table")) warnings.add("table", name);
|
|
270
|
+
}
|
|
271
|
+
sheets.push(data);
|
|
272
|
+
}
|
|
273
|
+
if (!sheets.length) sheets.push({
|
|
274
|
+
name: "Sheet1",
|
|
275
|
+
cells: []
|
|
276
|
+
});
|
|
277
|
+
const book = {
|
|
278
|
+
version: 1,
|
|
279
|
+
sheets
|
|
280
|
+
};
|
|
281
|
+
if (styleNames.size) book.styles = styles;
|
|
282
|
+
const lock = childOf(workbook, "workbookProtection");
|
|
283
|
+
if (lock && isOn(lock.attrs.lockStructure)) {
|
|
284
|
+
book.protect = true;
|
|
285
|
+
const a = lock.attrs;
|
|
286
|
+
if (a.workbookPassword || a.workbookHashValue) warnings.add("password");
|
|
287
|
+
}
|
|
288
|
+
signal?.throwIfAborted();
|
|
289
|
+
return {
|
|
290
|
+
book,
|
|
291
|
+
warnings: warnings.all
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** One sheet's part, read a piece at a time. */
|
|
295
|
+
async function readSheet(xml, name, context) {
|
|
296
|
+
const { strings, richText, looks, differentials, styleName, keepFormulas, warnings, pacing } = context;
|
|
297
|
+
const { date1904, tableRef } = context;
|
|
298
|
+
const dateOf = (serial) => dateText(date1904 ? serial + DAYS_1904 : serial);
|
|
299
|
+
const data = {
|
|
300
|
+
name,
|
|
301
|
+
cells: []
|
|
302
|
+
};
|
|
303
|
+
const cells = data.cells;
|
|
304
|
+
const rowSizes = {};
|
|
305
|
+
const hiddenRows = /* @__PURE__ */ new Set();
|
|
306
|
+
const rowLevels = /* @__PURE__ */ new Map();
|
|
307
|
+
const colSizes = {};
|
|
308
|
+
const hiddenCols = /* @__PURE__ */ new Set();
|
|
309
|
+
const colLevels = /* @__PURE__ */ new Map();
|
|
310
|
+
const merges = [];
|
|
311
|
+
let summaryAfter = true;
|
|
312
|
+
let borders = 0;
|
|
313
|
+
let fonts = 0;
|
|
314
|
+
let rich = 0;
|
|
315
|
+
let external = 0;
|
|
316
|
+
let tableFormulas = 0;
|
|
317
|
+
const links = [];
|
|
318
|
+
let passwords = 0;
|
|
319
|
+
const dates = /* @__PURE__ */ new Map();
|
|
320
|
+
const times = /* @__PURE__ */ new Map();
|
|
321
|
+
const sharedFormulas = /* @__PURE__ */ new Map();
|
|
322
|
+
let row = -1;
|
|
323
|
+
let col = -1;
|
|
324
|
+
let kind = "";
|
|
325
|
+
let look;
|
|
326
|
+
let value = "";
|
|
327
|
+
let inlineText = "";
|
|
328
|
+
let formula = null;
|
|
329
|
+
let formulaAttrs = null;
|
|
330
|
+
let reading = null;
|
|
331
|
+
let inInline = false;
|
|
332
|
+
let inPhonetic = false;
|
|
333
|
+
let skip = 0;
|
|
334
|
+
let filter = null;
|
|
335
|
+
let filterCol = null;
|
|
336
|
+
const conditional = [];
|
|
337
|
+
let cfRange = null;
|
|
338
|
+
let cfRule = null;
|
|
339
|
+
const validations = [];
|
|
340
|
+
let validation = null;
|
|
341
|
+
let text = "";
|
|
342
|
+
const finishCell = () => {
|
|
343
|
+
let content = null;
|
|
344
|
+
if (kind === "s") {
|
|
345
|
+
const index = +value;
|
|
346
|
+
content = strings[index] ?? "";
|
|
347
|
+
if (richText.has(index)) rich++;
|
|
348
|
+
} else if (kind === "inlineStr") content = unescapeX(inlineText);
|
|
349
|
+
else if (kind === "str") content = unescapeX(value);
|
|
350
|
+
else if (kind === "b") content = isOn(value);
|
|
351
|
+
else if (kind === "e") content = value;
|
|
352
|
+
else if (kind === "d") {
|
|
353
|
+
const date = /^(\d{4}-\d{2}-\d{2})(?:T(\d{2}:\d{2}(?::\d{2})?))?/.exec(value);
|
|
354
|
+
content = date ? date[2] && date[2] !== "00:00:00" ? `${date[1]} ${date[2].replace(/:00$/, "")}` : date[1] : value;
|
|
355
|
+
} else if (value !== "") {
|
|
356
|
+
const number = Number(value);
|
|
357
|
+
content = Number.isFinite(number) ? number : value;
|
|
358
|
+
}
|
|
359
|
+
if (typeof content === "string" && content.startsWith("=") && !formula) content = `'${content}`;
|
|
360
|
+
let format = look?.format;
|
|
361
|
+
if (typeof content === "number" && look && look.kind !== "number") {
|
|
362
|
+
const serial = content;
|
|
363
|
+
const time = look.kind === "time" && serial < 1;
|
|
364
|
+
let shown = (time ? times : dates).get(serial);
|
|
365
|
+
if (shown == null) {
|
|
366
|
+
shown = time ? timeText(serial) : dateOf(serial);
|
|
367
|
+
if (dates.size + times.size < 1e5) (time ? times : dates).set(serial, shown);
|
|
368
|
+
}
|
|
369
|
+
content = shown;
|
|
370
|
+
if (format && sameAsPlain(format, content)) format = void 0;
|
|
371
|
+
}
|
|
372
|
+
if (kind === "b" && formula != null && /^(TRUE|FALSE)\(\)$/i.test(formula)) formula = null;
|
|
373
|
+
if (formula != null && keepFormulas) {
|
|
374
|
+
if (/\[\d+\]/.test(formula)) external++;
|
|
375
|
+
else if (tableRef?.test(formula)) tableFormulas++;
|
|
376
|
+
else content = formulaOf(formula);
|
|
377
|
+
}
|
|
378
|
+
if (look?.border) borders++;
|
|
379
|
+
if (look?.font) fonts++;
|
|
380
|
+
const style = look ? styleName(look) : void 0;
|
|
381
|
+
if (content === null && !style && !format) return;
|
|
382
|
+
const line = cells[row] ??= [];
|
|
383
|
+
line[col] = style || format ? compact({
|
|
384
|
+
value: content,
|
|
385
|
+
style,
|
|
386
|
+
format
|
|
387
|
+
}) : content;
|
|
388
|
+
};
|
|
389
|
+
const handler = {
|
|
390
|
+
open(tag, raw, empty) {
|
|
391
|
+
if (skip) {
|
|
392
|
+
if (!empty) skip++;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
switch (tag) {
|
|
396
|
+
case "extLst":
|
|
397
|
+
if (!empty) skip = 1;
|
|
398
|
+
return;
|
|
399
|
+
case "row": {
|
|
400
|
+
const a = attrsOf(raw);
|
|
401
|
+
row = a.r ? +a.r - 1 : row + 1;
|
|
402
|
+
col = -1;
|
|
403
|
+
if (isOn(a.customHeight) && a.ht) rowSizes[row] = pxOfHeight(+a.ht);
|
|
404
|
+
if (isOn(a.hidden)) hiddenRows.add(row);
|
|
405
|
+
if (a.outlineLevel && +a.outlineLevel > 0) rowLevels.set(row, +a.outlineLevel);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case "c": {
|
|
409
|
+
const a = attrsOf(raw);
|
|
410
|
+
if (a.r) {
|
|
411
|
+
const point = parseCell(a.r);
|
|
412
|
+
if (point) {
|
|
413
|
+
row = point.row;
|
|
414
|
+
col = point.col;
|
|
415
|
+
} else col++;
|
|
416
|
+
} else col++;
|
|
417
|
+
kind = a.t ?? "n";
|
|
418
|
+
look = a.s ? looks[+a.s] : looks[0];
|
|
419
|
+
if (look && !look.style && !look.format && !look.border && !look.font && !look.quote) look = void 0;
|
|
420
|
+
value = "";
|
|
421
|
+
inlineText = "";
|
|
422
|
+
formula = null;
|
|
423
|
+
formulaAttrs = null;
|
|
424
|
+
if (empty) finishCell();
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case "v":
|
|
428
|
+
reading = "v";
|
|
429
|
+
value = "";
|
|
430
|
+
break;
|
|
431
|
+
case "f":
|
|
432
|
+
formulaAttrs = attrsOf(raw);
|
|
433
|
+
reading = "f";
|
|
434
|
+
formula = "";
|
|
435
|
+
if (empty) closeFormula();
|
|
436
|
+
break;
|
|
437
|
+
case "is":
|
|
438
|
+
inInline = true;
|
|
439
|
+
break;
|
|
440
|
+
case "rPh":
|
|
441
|
+
inPhonetic = true;
|
|
442
|
+
break;
|
|
443
|
+
case "t":
|
|
444
|
+
if (inInline && !inPhonetic) reading = "t";
|
|
445
|
+
else text = "";
|
|
446
|
+
break;
|
|
447
|
+
case "outlinePr": {
|
|
448
|
+
const a = attrsOf(raw);
|
|
449
|
+
summaryAfter = isNotOff(a.summaryBelow);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "pane": {
|
|
453
|
+
const a = attrsOf(raw);
|
|
454
|
+
if (a.state === "frozen" || a.state === "frozenSplit") {
|
|
455
|
+
const rows = Math.round(+(a.ySplit ?? 0));
|
|
456
|
+
const cols = Math.round(+(a.xSplit ?? 0));
|
|
457
|
+
if (rows || cols) data.freeze = {
|
|
458
|
+
...rows ? { rows } : {},
|
|
459
|
+
...cols ? { cols } : {}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "col": {
|
|
465
|
+
const a = attrsOf(raw);
|
|
466
|
+
const min = +(a.min ?? 1) - 1;
|
|
467
|
+
const max = Math.min(+(a.max ?? a.min ?? 1) - 1, 16383);
|
|
468
|
+
const hidden = isOn(a.hidden);
|
|
469
|
+
const level = a.outlineLevel ? +a.outlineLevel : 0;
|
|
470
|
+
const sized = isOn(a.customWidth) && a.width != null && max - min < 256;
|
|
471
|
+
for (let c = min; c <= max; c++) {
|
|
472
|
+
if (sized) colSizes[c] = pxOfWidth(+a.width);
|
|
473
|
+
if (hidden) hiddenCols.add(c);
|
|
474
|
+
if (level > 0) colLevels.set(c, level);
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "mergeCell": {
|
|
479
|
+
const ref = attrsOf(raw).ref;
|
|
480
|
+
if (ref) merges.push(ref);
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case "autoFilter": {
|
|
484
|
+
const ref = attrsOf(raw).ref;
|
|
485
|
+
if (ref) filter = {
|
|
486
|
+
range: ref,
|
|
487
|
+
left: parseCell(ref.split(":")[0])?.col ?? 0,
|
|
488
|
+
columns: {}
|
|
489
|
+
};
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case "filterColumn":
|
|
493
|
+
filterCol = {
|
|
494
|
+
id: +(attrsOf(raw).colId ?? 0),
|
|
495
|
+
values: [],
|
|
496
|
+
blank: false,
|
|
497
|
+
custom: [],
|
|
498
|
+
and: false,
|
|
499
|
+
other: false
|
|
500
|
+
};
|
|
501
|
+
break;
|
|
502
|
+
case "filters":
|
|
503
|
+
if (filterCol) filterCol.blank = isOn(attrsOf(raw).blank);
|
|
504
|
+
break;
|
|
505
|
+
case "filter":
|
|
506
|
+
filterCol?.values.push(attrsOf(raw).val ?? "");
|
|
507
|
+
break;
|
|
508
|
+
case "customFilters":
|
|
509
|
+
if (filterCol) filterCol.and = isOn(attrsOf(raw).and);
|
|
510
|
+
break;
|
|
511
|
+
case "customFilter": {
|
|
512
|
+
const a = attrsOf(raw);
|
|
513
|
+
filterCol?.custom.push({
|
|
514
|
+
op: a.operator,
|
|
515
|
+
val: a.val ?? ""
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "top10":
|
|
520
|
+
case "dynamicFilter":
|
|
521
|
+
case "colorFilter":
|
|
522
|
+
case "iconFilter":
|
|
523
|
+
case "dateGroupItem":
|
|
524
|
+
if (filterCol) filterCol.other = true;
|
|
525
|
+
break;
|
|
526
|
+
case "conditionalFormatting":
|
|
527
|
+
cfRange = rangesOf(attrsOf(raw).sqref);
|
|
528
|
+
break;
|
|
529
|
+
case "cfRule":
|
|
530
|
+
cfRule = {
|
|
531
|
+
attrs: attrsOf(raw),
|
|
532
|
+
formulas: [],
|
|
533
|
+
colors: []
|
|
534
|
+
};
|
|
535
|
+
if (empty) closeRule();
|
|
536
|
+
break;
|
|
537
|
+
case "formula":
|
|
538
|
+
case "formula1":
|
|
539
|
+
case "formula2":
|
|
540
|
+
text = "";
|
|
541
|
+
break;
|
|
542
|
+
case "color":
|
|
543
|
+
if (cfRule) cfRule.colors.push(colorOf({
|
|
544
|
+
name: "color",
|
|
545
|
+
attrs: attrsOf(raw),
|
|
546
|
+
children: [],
|
|
547
|
+
text: ""
|
|
548
|
+
}, []));
|
|
549
|
+
break;
|
|
550
|
+
case "dataValidation":
|
|
551
|
+
validation = { attrs: attrsOf(raw) };
|
|
552
|
+
if (empty) closeValidation();
|
|
553
|
+
break;
|
|
554
|
+
case "hyperlink":
|
|
555
|
+
links.push(attrsOf(raw));
|
|
556
|
+
break;
|
|
557
|
+
case "sheetProtection": {
|
|
558
|
+
const a = attrsOf(raw);
|
|
559
|
+
if (!isOn(a.sheet)) break;
|
|
560
|
+
const allow = {};
|
|
561
|
+
for (const [key, part] of PROTECTION_PARTS) if (isOff(a[key])) allow[part] = true;
|
|
562
|
+
data.protect = Object.keys(allow).length ? allow : true;
|
|
563
|
+
if (a.password || a.hashValue) passwords++;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
close(tag) {
|
|
569
|
+
if (skip) {
|
|
570
|
+
skip--;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
switch (tag) {
|
|
574
|
+
case "c":
|
|
575
|
+
finishCell();
|
|
576
|
+
break;
|
|
577
|
+
case "v":
|
|
578
|
+
case "t":
|
|
579
|
+
if (reading === "v" || reading === "t") reading = null;
|
|
580
|
+
break;
|
|
581
|
+
case "f":
|
|
582
|
+
closeFormula();
|
|
583
|
+
break;
|
|
584
|
+
case "is":
|
|
585
|
+
inInline = false;
|
|
586
|
+
break;
|
|
587
|
+
case "rPh":
|
|
588
|
+
inPhonetic = false;
|
|
589
|
+
break;
|
|
590
|
+
case "formula":
|
|
591
|
+
cfRule?.formulas.push(text);
|
|
592
|
+
break;
|
|
593
|
+
case "formula1":
|
|
594
|
+
if (validation) validation.formula1 = text;
|
|
595
|
+
break;
|
|
596
|
+
case "formula2":
|
|
597
|
+
if (validation) validation.formula2 = text;
|
|
598
|
+
break;
|
|
599
|
+
case "filterColumn":
|
|
600
|
+
closeFilterColumn();
|
|
601
|
+
break;
|
|
602
|
+
case "cfRule":
|
|
603
|
+
closeRule();
|
|
604
|
+
break;
|
|
605
|
+
case "dataValidation": closeValidation();
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
text(chunk) {
|
|
609
|
+
if (skip) return;
|
|
610
|
+
if (reading === "v") value += chunk;
|
|
611
|
+
else if (reading === "f") formula += chunk;
|
|
612
|
+
else if (reading === "t") inlineText += chunk;
|
|
613
|
+
else text += chunk;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
function closeFormula() {
|
|
617
|
+
reading = null;
|
|
618
|
+
const a = formulaAttrs ?? {};
|
|
619
|
+
let written = formula ?? "";
|
|
620
|
+
if (a.t === "shared" && a.si != null) {
|
|
621
|
+
const master = sharedFormulas.get(a.si);
|
|
622
|
+
if (written) sharedFormulas.set(a.si, {
|
|
623
|
+
formula: written,
|
|
624
|
+
row,
|
|
625
|
+
col
|
|
626
|
+
});
|
|
627
|
+
else if (master) written = shiftFormula(`=${master.formula}`, row - master.row, col - master.col).slice(1);
|
|
628
|
+
}
|
|
629
|
+
formula = written && a.t !== "dataTable" ? written : null;
|
|
630
|
+
}
|
|
631
|
+
function closeFilterColumn() {
|
|
632
|
+
const column = filterCol;
|
|
633
|
+
filterCol = null;
|
|
634
|
+
if (!filter || !column) return;
|
|
635
|
+
const at = filter.left + column.id;
|
|
636
|
+
if (column.other) {
|
|
637
|
+
warnings.add("filter", name);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (column.values.length || column.blank) {
|
|
641
|
+
filter.columns[at] = { values: [...column.values, ...column.blank ? [""] : []] };
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const condition = filterCondition(column.custom, column.and);
|
|
645
|
+
if (condition) filter.columns[at] = { condition };
|
|
646
|
+
else if (column.custom.length) warnings.add("filter", name);
|
|
647
|
+
}
|
|
648
|
+
function closeRule() {
|
|
649
|
+
const rule = cfRule;
|
|
650
|
+
cfRule = null;
|
|
651
|
+
if (!rule || !cfRange) return;
|
|
652
|
+
const made = conditionalOf(rule, cfRange, differentials);
|
|
653
|
+
if (made) conditional.push({
|
|
654
|
+
priority: +(rule.attrs.priority ?? 0),
|
|
655
|
+
rule: made
|
|
656
|
+
});
|
|
657
|
+
else warnings.add("conditional", name);
|
|
658
|
+
}
|
|
659
|
+
function closeValidation() {
|
|
660
|
+
const rule = validation;
|
|
661
|
+
validation = null;
|
|
662
|
+
if (!rule) return;
|
|
663
|
+
const made = validationOf(rule, dateOf);
|
|
664
|
+
if (made) validations.push(made);
|
|
665
|
+
else if (rule.attrs.type && rule.attrs.type !== "none") warnings.add("validation", name);
|
|
666
|
+
}
|
|
667
|
+
const reader = new XmlReader(xml, handler);
|
|
668
|
+
while (reader.step(2e4)) if (performance.now() - pacing.lastPause.at > 50) {
|
|
669
|
+
await pause();
|
|
670
|
+
pacing.signal?.throwIfAborted();
|
|
671
|
+
pacing.lastPause.at = performance.now();
|
|
672
|
+
}
|
|
673
|
+
const filtered = filter && Object.keys(filter.columns).length ? filter : null;
|
|
674
|
+
const filterRows = filtered ? rowSpan(filtered.range) : null;
|
|
675
|
+
const rowGroups = groupsOf(rowLevels, hiddenRows, (a, b) => `${a + 1}:${b + 1}`);
|
|
676
|
+
const colGroups = groupsOf(colLevels, hiddenCols, (a, b) => `${columnName(a)}:${columnName(b)}`);
|
|
677
|
+
for (const r of hiddenRows) {
|
|
678
|
+
if (rowGroups.folded.has(r)) continue;
|
|
679
|
+
if (filterRows && r > filterRows[0] && r <= filterRows[1]) continue;
|
|
680
|
+
rowSizes[r] = 0;
|
|
681
|
+
}
|
|
682
|
+
for (const c of hiddenCols) if (!colGroups.folded.has(c)) colSizes[c] = 0;
|
|
683
|
+
if (Object.keys(rowSizes).length) data.rowSizes = rowSizes;
|
|
684
|
+
if (Object.keys(colSizes).length) data.colSizes = colSizes;
|
|
685
|
+
if (merges.length) data.merges = merges;
|
|
686
|
+
const groups = [...rowGroups.groups, ...colGroups.groups];
|
|
687
|
+
if (groups.length) {
|
|
688
|
+
data.groups = groups;
|
|
689
|
+
if (!summaryAfter) data.groupSummary = "before";
|
|
690
|
+
else data.groupSummary = "after";
|
|
691
|
+
}
|
|
692
|
+
if (filter) data.filter = Object.keys(filter.columns).length ? {
|
|
693
|
+
range: filter.range,
|
|
694
|
+
columns: filter.columns
|
|
695
|
+
} : { range: filter.range };
|
|
696
|
+
if (conditional.length) data.conditionalFormats = conditional.sort((a, b) => a.priority - b.priority).map((c) => c.rule);
|
|
697
|
+
if (validations.length) data.validations = validations;
|
|
698
|
+
warnings.add("border", name, borders);
|
|
699
|
+
warnings.add("font", name, fonts);
|
|
700
|
+
warnings.add("richText", name, rich);
|
|
701
|
+
warnings.add("external", name, external);
|
|
702
|
+
warnings.add("table", name, tableFormulas);
|
|
703
|
+
return {
|
|
704
|
+
data,
|
|
705
|
+
links,
|
|
706
|
+
passwords
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/** The parts of a sheet's protection in the file, with the names the sheet gives them. */
|
|
710
|
+
var PROTECTION_PARTS = [
|
|
711
|
+
["formatCells", "formatCells"],
|
|
712
|
+
["formatColumns", "formatColumns"],
|
|
713
|
+
["formatRows", "formatRows"],
|
|
714
|
+
["insertColumns", "insertColumns"],
|
|
715
|
+
["insertRows", "insertRows"],
|
|
716
|
+
["deleteColumns", "deleteColumns"],
|
|
717
|
+
["deleteRows", "deleteRows"],
|
|
718
|
+
["autoFilter", "filter"]
|
|
719
|
+
];
|
|
720
|
+
/**
|
|
721
|
+
* Puts a sheet's links on their cells: a web address from the sheet's
|
|
722
|
+
* relationships, a place in the workbook as `#` and its reference. Gives
|
|
723
|
+
* how many couldn't be kept, such as links to other files.
|
|
724
|
+
*/
|
|
725
|
+
function addLinks(data, links, rels) {
|
|
726
|
+
let dropped = 0;
|
|
727
|
+
for (const a of links) {
|
|
728
|
+
const rel = a.id ? rels.get(a.id) : void 0;
|
|
729
|
+
const place = a.location ? `#${a.location}` : "";
|
|
730
|
+
const link = rel ? rel.external ? rel.path + place : "" : place;
|
|
731
|
+
const range = a.ref ? parseRange(a.ref) : null;
|
|
732
|
+
if (!range || !link || !linkTarget(link)) {
|
|
733
|
+
dropped++;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const b = boundsOf(range);
|
|
737
|
+
const bottom = Math.min(b.bottom, b.top + 999);
|
|
738
|
+
for (let r = b.top; r <= bottom; r++) for (let c = b.left; c <= b.right && c <= b.left + 99; c++) {
|
|
739
|
+
const line = data.cells[r] ??= [];
|
|
740
|
+
const cell = line[c];
|
|
741
|
+
line[c] = cell != null && typeof cell === "object" ? {
|
|
742
|
+
...cell,
|
|
743
|
+
link
|
|
744
|
+
} : cell == null || cell === "" ? { link } : {
|
|
745
|
+
value: cell,
|
|
746
|
+
link
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return dropped;
|
|
751
|
+
}
|
|
752
|
+
/** A cell with only what it has. */
|
|
753
|
+
function compact(cell) {
|
|
754
|
+
const out = {};
|
|
755
|
+
if (cell.value != null) out.value = cell.value;
|
|
756
|
+
if (cell.style) out.style = cell.style;
|
|
757
|
+
if (cell.format) out.format = cell.format;
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
760
|
+
/** Whether a date's format is the one the sheet would show it with anyway. */
|
|
761
|
+
function sameAsPlain(format, text) {
|
|
762
|
+
const plain = text.length === 10 ? "yyyy-mm-dd" : text.length === 5 ? "hh:mm" : text.length === 8 ? "hh:mm:ss" : text.length === 16 ? "yyyy-mm-dd hh:mm" : "yyyy-mm-dd hh:mm:ss";
|
|
763
|
+
return format.toLowerCase() === plain;
|
|
764
|
+
}
|
|
765
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
766
|
+
/** A part of a day as `hh:mm`, or `hh:mm:ss` with seconds. */
|
|
767
|
+
function timeText(fraction) {
|
|
768
|
+
const seconds = Math.round(fraction * 86400) % 86400;
|
|
769
|
+
const time = `${pad(Math.floor(seconds / 3600))}:${pad(Math.floor(seconds % 3600 / 60))}`;
|
|
770
|
+
return seconds % 60 ? `${time}:${pad(seconds % 60)}` : time;
|
|
771
|
+
}
|
|
772
|
+
/** The first and last row of a range such as `A1:E40`. */
|
|
773
|
+
function rowSpan(range) {
|
|
774
|
+
const [from, to] = range.split(":");
|
|
775
|
+
const start = from ? parseCell(from) : null;
|
|
776
|
+
const end = to ? parseCell(to) : start;
|
|
777
|
+
return start && end ? [start.row, end.row] : null;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* The groups of lines with outline levels: each run of lines at a level or
|
|
781
|
+
* deeper is a group. A group whose lines are all hidden is folded.
|
|
782
|
+
*/
|
|
783
|
+
function groupsOf(levels, hidden, rangeOf) {
|
|
784
|
+
const groups = [];
|
|
785
|
+
const folded = /* @__PURE__ */ new Set();
|
|
786
|
+
if (!levels.size) return {
|
|
787
|
+
groups,
|
|
788
|
+
folded
|
|
789
|
+
};
|
|
790
|
+
const lines = [...levels.keys()].sort((a, b) => a - b);
|
|
791
|
+
let deepest = 0;
|
|
792
|
+
for (const level of levels.values()) if (level > deepest) deepest = level;
|
|
793
|
+
for (let level = 1; level <= deepest; level++) {
|
|
794
|
+
let start = -1;
|
|
795
|
+
let prev = -2;
|
|
796
|
+
const close = () => {
|
|
797
|
+
if (start < 0) return;
|
|
798
|
+
let all = true;
|
|
799
|
+
for (let i = start; i <= prev; i++) if (!hidden.has(i)) all = false;
|
|
800
|
+
if (all) for (let i = start; i <= prev; i++) folded.add(i);
|
|
801
|
+
groups.push(all ? {
|
|
802
|
+
range: rangeOf(start, prev),
|
|
803
|
+
collapsed: true
|
|
804
|
+
} : { range: rangeOf(start, prev) });
|
|
805
|
+
};
|
|
806
|
+
for (const line of lines) {
|
|
807
|
+
if (levels.get(line) < level) continue;
|
|
808
|
+
if (line !== prev + 1) {
|
|
809
|
+
close();
|
|
810
|
+
start = line;
|
|
811
|
+
}
|
|
812
|
+
prev = line;
|
|
813
|
+
}
|
|
814
|
+
close();
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
groups,
|
|
818
|
+
folded
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** A value typed in a filter or a list: a number when it reads as one. */
|
|
822
|
+
var typedValue = (val) => /^-?(\d+\.?\d*|\.\d+)$/.test(val.trim()) ? Number(val) : val;
|
|
823
|
+
/** A filter's custom conditions as one condition, `null` when the sheet has none like them. */
|
|
824
|
+
function filterCondition(custom, and) {
|
|
825
|
+
if (custom.length === 1) {
|
|
826
|
+
const { op = "equal", val } = custom[0];
|
|
827
|
+
const wild = /^\*(.*)\*$/.exec(val);
|
|
828
|
+
if (op === "equal" || op === "notEqual") {
|
|
829
|
+
const not = op === "notEqual";
|
|
830
|
+
if (not && val === " ") return { op: "notBlank" };
|
|
831
|
+
if (wild && !wild[1].includes("*")) return {
|
|
832
|
+
op: not ? "notContains" : "contains",
|
|
833
|
+
value: wild[1]
|
|
834
|
+
};
|
|
835
|
+
if (!not && /^[^*]+\*$/.test(val)) return {
|
|
836
|
+
op: "begins",
|
|
837
|
+
value: val.slice(0, -1)
|
|
838
|
+
};
|
|
839
|
+
if (!not && /^\*[^*]+$/.test(val)) return {
|
|
840
|
+
op: "ends",
|
|
841
|
+
value: val.slice(1)
|
|
842
|
+
};
|
|
843
|
+
if (val.includes("*") || val.includes("?")) return null;
|
|
844
|
+
return {
|
|
845
|
+
op: not ? "ne" : "eq",
|
|
846
|
+
value: typedValue(val)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const ops = {
|
|
850
|
+
greaterThan: "gt",
|
|
851
|
+
greaterThanOrEqual: "ge",
|
|
852
|
+
lessThan: "lt",
|
|
853
|
+
lessThanOrEqual: "le"
|
|
854
|
+
};
|
|
855
|
+
return ops[op] ? {
|
|
856
|
+
op: ops[op],
|
|
857
|
+
value: typedValue(val)
|
|
858
|
+
} : null;
|
|
859
|
+
}
|
|
860
|
+
if (custom.length === 2) {
|
|
861
|
+
const [a, b] = custom;
|
|
862
|
+
if (and && a.op === "greaterThanOrEqual" && b.op === "lessThanOrEqual") return {
|
|
863
|
+
op: "between",
|
|
864
|
+
value: typedValue(a.val),
|
|
865
|
+
to: typedValue(b.val)
|
|
866
|
+
};
|
|
867
|
+
if (!and && a.op === "lessThan" && b.op === "greaterThan") return {
|
|
868
|
+
op: "notBetween",
|
|
869
|
+
value: typedValue(a.val),
|
|
870
|
+
to: typedValue(b.val)
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
/** A conditional format's rule as the sheet's, `null` for one it has none like. */
|
|
876
|
+
function conditionalOf(rule, range, differentials) {
|
|
877
|
+
const { attrs, formulas } = rule;
|
|
878
|
+
const style = differentials[+(attrs.dxfId ?? -1)] ?? {};
|
|
879
|
+
const anchor = anchorOf(range);
|
|
880
|
+
switch (attrs.type) {
|
|
881
|
+
case "cellIs": {
|
|
882
|
+
const op = CELL_IS[attrs.operator ?? "between"];
|
|
883
|
+
if (!op) return null;
|
|
884
|
+
const value = literalOf(formulas[0]);
|
|
885
|
+
const to = literalOf(formulas[1]);
|
|
886
|
+
const two = op === "between" || op === "notBetween";
|
|
887
|
+
if (value !== void 0 && (!two || to !== void 0)) return {
|
|
888
|
+
range,
|
|
889
|
+
when: two ? {
|
|
890
|
+
op,
|
|
891
|
+
value,
|
|
892
|
+
to
|
|
893
|
+
} : {
|
|
894
|
+
op,
|
|
895
|
+
value
|
|
896
|
+
},
|
|
897
|
+
style
|
|
898
|
+
};
|
|
899
|
+
if (!formulas[0]) return null;
|
|
900
|
+
if (two) {
|
|
901
|
+
if (!formulas[1]) return null;
|
|
902
|
+
const inside = `AND(${anchor}>=${formulas[0]},${anchor}<=${formulas[1]})`;
|
|
903
|
+
return {
|
|
904
|
+
range,
|
|
905
|
+
formula: `=${op === "between" ? inside : `NOT(${inside})`}`,
|
|
906
|
+
style
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
range,
|
|
911
|
+
formula: `=${anchor}${COMPARE[op]}${formulas[0]}`,
|
|
912
|
+
style
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
case "containsText":
|
|
916
|
+
case "notContainsText":
|
|
917
|
+
case "beginsWith":
|
|
918
|
+
case "endsWith": {
|
|
919
|
+
const ops = {
|
|
920
|
+
containsText: "contains",
|
|
921
|
+
notContainsText: "notContains",
|
|
922
|
+
beginsWith: "begins",
|
|
923
|
+
endsWith: "ends"
|
|
924
|
+
};
|
|
925
|
+
if (attrs.text == null) break;
|
|
926
|
+
return {
|
|
927
|
+
range,
|
|
928
|
+
when: {
|
|
929
|
+
op: ops[attrs.type],
|
|
930
|
+
value: attrs.text
|
|
931
|
+
},
|
|
932
|
+
style
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
case "containsBlanks": return {
|
|
936
|
+
range,
|
|
937
|
+
when: { op: "blank" },
|
|
938
|
+
style
|
|
939
|
+
};
|
|
940
|
+
case "notContainsBlanks": return {
|
|
941
|
+
range,
|
|
942
|
+
when: { op: "notBlank" },
|
|
943
|
+
style
|
|
944
|
+
};
|
|
945
|
+
case "expression": return formulas[0] ? {
|
|
946
|
+
range,
|
|
947
|
+
formula: formulaOf(formulas[0]),
|
|
948
|
+
style
|
|
949
|
+
} : null;
|
|
950
|
+
case "top10": {
|
|
951
|
+
const rank = +(attrs.rank ?? 10);
|
|
952
|
+
const percent = isOn(attrs.percent) ? { percent: true } : {};
|
|
953
|
+
return isOn(attrs.bottom) ? {
|
|
954
|
+
range,
|
|
955
|
+
bottom: rank,
|
|
956
|
+
...percent,
|
|
957
|
+
style
|
|
958
|
+
} : {
|
|
959
|
+
range,
|
|
960
|
+
top: rank,
|
|
961
|
+
...percent,
|
|
962
|
+
style
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
case "aboveAverage":
|
|
966
|
+
if (attrs.stdDev != null) return null;
|
|
967
|
+
return {
|
|
968
|
+
range,
|
|
969
|
+
average: !isNotOff(attrs.aboveAverage) ? "below" : "above",
|
|
970
|
+
style
|
|
971
|
+
};
|
|
972
|
+
case "duplicateValues": return {
|
|
973
|
+
range,
|
|
974
|
+
duplicates: true,
|
|
975
|
+
style
|
|
976
|
+
};
|
|
977
|
+
case "uniqueValues": return {
|
|
978
|
+
range,
|
|
979
|
+
unique: true,
|
|
980
|
+
style
|
|
981
|
+
};
|
|
982
|
+
case "colorScale": {
|
|
983
|
+
const colors = rule.colors.map((c) => c ?? "#ffffff");
|
|
984
|
+
if (colors.length === 2) return {
|
|
985
|
+
range,
|
|
986
|
+
scale: [colors[0], colors[1]]
|
|
987
|
+
};
|
|
988
|
+
if (colors.length === 3) return {
|
|
989
|
+
range,
|
|
990
|
+
scale: [
|
|
991
|
+
colors[0],
|
|
992
|
+
colors[1],
|
|
993
|
+
colors[2]
|
|
994
|
+
]
|
|
995
|
+
};
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
case "dataBar": return {
|
|
999
|
+
range,
|
|
1000
|
+
bar: rule.colors[0] ?? true
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
return formulas[0] ? {
|
|
1004
|
+
range,
|
|
1005
|
+
formula: formulaOf(formulas[0]),
|
|
1006
|
+
style
|
|
1007
|
+
} : null;
|
|
1008
|
+
}
|
|
1009
|
+
/** A data validation as the sheet's, `null` for one it has none like. */
|
|
1010
|
+
function validationOf(rule, dateOf) {
|
|
1011
|
+
const { attrs, formula1, formula2 } = rule;
|
|
1012
|
+
const range = rangesOf(attrs.sqref);
|
|
1013
|
+
if (!range || formula1 == null) return null;
|
|
1014
|
+
const common = {
|
|
1015
|
+
range,
|
|
1016
|
+
...attrs.errorStyle === "warning" || attrs.errorStyle === "information" ? { strict: false } : {},
|
|
1017
|
+
...attrs.error ? { message: attrs.error } : {},
|
|
1018
|
+
...attrs.prompt ? { prompt: attrs.prompt } : {}
|
|
1019
|
+
};
|
|
1020
|
+
switch (attrs.type) {
|
|
1021
|
+
case "list": {
|
|
1022
|
+
const inline = /^"(.*)"$/s.exec(formula1.trim());
|
|
1023
|
+
if (!inline) return {
|
|
1024
|
+
...common,
|
|
1025
|
+
list: formulaOf(formula1)
|
|
1026
|
+
};
|
|
1027
|
+
const list = inline[1].split(",").map(typedValue);
|
|
1028
|
+
return {
|
|
1029
|
+
...common,
|
|
1030
|
+
list
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
case "custom": return {
|
|
1034
|
+
...common,
|
|
1035
|
+
formula: formulaOf(formula1)
|
|
1036
|
+
};
|
|
1037
|
+
case "whole":
|
|
1038
|
+
case "decimal":
|
|
1039
|
+
case "date":
|
|
1040
|
+
case "time": {
|
|
1041
|
+
const op = CELL_IS[attrs.operator ?? "between"];
|
|
1042
|
+
const date = attrs.type === "date" || attrs.type === "time";
|
|
1043
|
+
const valueOf = (formula) => {
|
|
1044
|
+
const value = literalOf(formula);
|
|
1045
|
+
if (typeof value !== "number") return void 0;
|
|
1046
|
+
return date ? attrs.type === "time" ? timeText(value) : dateOf(value) : value;
|
|
1047
|
+
};
|
|
1048
|
+
const value = valueOf(formula1);
|
|
1049
|
+
const to = valueOf(formula2);
|
|
1050
|
+
if (!op || value === void 0) return null;
|
|
1051
|
+
if (op === "between" || op === "notBetween") {
|
|
1052
|
+
if (to === void 0) return null;
|
|
1053
|
+
return {
|
|
1054
|
+
...common,
|
|
1055
|
+
when: {
|
|
1056
|
+
op,
|
|
1057
|
+
value,
|
|
1058
|
+
to
|
|
1059
|
+
},
|
|
1060
|
+
...attrs.type === "whole" ? { integer: true } : {}
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
...common,
|
|
1065
|
+
when: {
|
|
1066
|
+
op,
|
|
1067
|
+
value
|
|
1068
|
+
},
|
|
1069
|
+
...attrs.type === "whole" ? { integer: true } : {}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
/** Adds a sheet's notes to its cells. */
|
|
1076
|
+
function addNotes(data, comments) {
|
|
1077
|
+
const authors = childrenOf(childOf(comments, "authors"), "author").map((a) => a.text);
|
|
1078
|
+
for (const comment of childrenOf(childOf(comments, "commentList"), "comment")) {
|
|
1079
|
+
const point = parseCell(comment.attrs.ref ?? "");
|
|
1080
|
+
const body = childOf(comment, "text");
|
|
1081
|
+
if (!point || !body) continue;
|
|
1082
|
+
let note = unescapeX(textOf(body));
|
|
1083
|
+
const author = authors[+(comment.attrs.authorId ?? -1)];
|
|
1084
|
+
if (author && note.startsWith(`${author}:`)) note = note.slice(author.length + 1).replace(/^\r?\n/, "");
|
|
1085
|
+
if (!note) continue;
|
|
1086
|
+
const line = data.cells[point.row] ??= [];
|
|
1087
|
+
const input = line[point.col];
|
|
1088
|
+
line[point.col] = input != null && typeof input === "object" ? {
|
|
1089
|
+
...input,
|
|
1090
|
+
note
|
|
1091
|
+
} : {
|
|
1092
|
+
...input == null ? {} : { value: input },
|
|
1093
|
+
note
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
export { readXlsx };
|
|
1098
|
+
|
|
1099
|
+
//# sourceMappingURL=read.js.map
|