@dou-ui/sheet-xlsx 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/write.js
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { pause, zip } from "./zip.js";
|
|
2
|
+
import { HEAD, NS, NS_PKG, NS_R, attrs, esc } from "./xml.js";
|
|
3
|
+
import { StyleSheet, argb } from "./styles.js";
|
|
4
|
+
import { boundsOf, columnName, lightThemeColor, linkTarget, meetsRule, parseRange, plainStyle, quoteSheet, renameReferencesAll, textCollator } from "@dou-ui/sheet/core";
|
|
5
|
+
import { isSheetError, serialOf } from "@dou-ui/sheet/formula";
|
|
6
|
+
// src/write.ts
|
|
7
|
+
/** An error as the file can hold it: a formula that depends on itself as `#VALUE!`. */
|
|
8
|
+
var errorCode = (code) => esc(code === "#CYCLE!" ? "#VALUE!" : code);
|
|
9
|
+
var encoder = new TextEncoder();
|
|
10
|
+
/** The deepest outline level, `undefined` for none. */
|
|
11
|
+
function maxLevel(levels) {
|
|
12
|
+
let max = 0;
|
|
13
|
+
for (const level of levels.values()) if (level > max) max = level;
|
|
14
|
+
return max || void 0;
|
|
15
|
+
}
|
|
16
|
+
var DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
17
|
+
var DATE_TIME = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$/;
|
|
18
|
+
var TIME = /^\d{2}:\d{2}(:\d{2})?$/;
|
|
19
|
+
/** The number format of a date or time written as text, when the cell has none. */
|
|
20
|
+
function dateFormat(text) {
|
|
21
|
+
if (DATE.test(text)) return "yyyy-mm-dd";
|
|
22
|
+
if (DATE_TIME.test(text)) return text.length > 16 ? "yyyy-mm-dd hh:mm:ss" : "yyyy-mm-dd hh:mm";
|
|
23
|
+
if (TIME.test(text)) return text.length > 5 ? "hh:mm:ss" : "hh:mm";
|
|
24
|
+
}
|
|
25
|
+
/** Column widths: px to the characters the file counts in, which the file format turns back into these px (its padding included). */
|
|
26
|
+
var widthOf = (px) => Math.max(0, Math.round(px * 256 / 7) / 256);
|
|
27
|
+
/** Row heights: px to points. */
|
|
28
|
+
var heightOf = (px) => Math.round(px * .75 * 100) / 100;
|
|
29
|
+
var cellName = (row, col) => `${columnName(col)}${row + 1}`;
|
|
30
|
+
var rangeText = (b) => b.top === b.bottom && b.left === b.right ? cellName(b.top, b.left) : `${cellName(b.top, b.left)}:${cellName(b.bottom, b.right)}`;
|
|
31
|
+
var ILLEGAL = /[[\]:*?/\\]/g;
|
|
32
|
+
/** Whether other programs take a sheet name as it is. */
|
|
33
|
+
var legalName = (name) => name.length > 0 && name.length <= 31 && !/[[\]:*?/\\]/.test(name) && !name.startsWith("'") && !name.endsWith("'");
|
|
34
|
+
/**
|
|
35
|
+
* Sheet names other programs take: at most 31 characters, none of
|
|
36
|
+
* `[]:*?/\`, no `'` at either end, each different ignoring case. Names
|
|
37
|
+
* that are fine stay as they are; the others are changed, with a number
|
|
38
|
+
* after them when that makes them the same as another.
|
|
39
|
+
*/
|
|
40
|
+
function sheetNames(names) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const taken = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const [i, name] of names.entries()) if (legalName(name) && !taken.has(name.toLowerCase())) {
|
|
44
|
+
out[i] = name;
|
|
45
|
+
taken.add(name.toLowerCase());
|
|
46
|
+
}
|
|
47
|
+
for (const [i, name] of names.entries()) {
|
|
48
|
+
if (out[i] != null) continue;
|
|
49
|
+
const base = name.replace(ILLEGAL, "_").replace(/^'|'$/g, "_").slice(0, 31) || "Sheet";
|
|
50
|
+
let next = base;
|
|
51
|
+
for (let n = 2; taken.has(next.toLowerCase()); n++) {
|
|
52
|
+
const tail = ` (${n})`;
|
|
53
|
+
next = base.slice(0, 31 - tail.length) + tail;
|
|
54
|
+
}
|
|
55
|
+
taken.add(next.toLowerCase());
|
|
56
|
+
out[i] = next;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A list of values written in the rule itself, `"a,b"`, or `null` when it
|
|
62
|
+
* can't be: a value with a comma or a quote would split or end it, and
|
|
63
|
+
* the file holds at most 255 characters.
|
|
64
|
+
*/
|
|
65
|
+
function inlineList(options) {
|
|
66
|
+
const values = options.map((o) => String(typeof o === "object" && o !== null ? o.value : o));
|
|
67
|
+
if (values.some((v) => v.includes(",") || v.includes("\""))) return null;
|
|
68
|
+
const list = `"${values.join(",")}"`;
|
|
69
|
+
return list.length <= 255 ? list : null;
|
|
70
|
+
}
|
|
71
|
+
/** The ranges of a rule, as the file lists them. */
|
|
72
|
+
var sqref = (range) => (Array.isArray(range) ? range : [range]).map((r) => r.replaceAll("$", "").trim()).join(" ");
|
|
73
|
+
/** The top left cell of a rule's first range, which its formula is written for. */
|
|
74
|
+
function anchorOf(range) {
|
|
75
|
+
const first = Array.isArray(range) ? range[0] : range;
|
|
76
|
+
const parsed = first ? parseRange(first.replaceAll("$", "")) : null;
|
|
77
|
+
if (!parsed) return "A1";
|
|
78
|
+
const b = boundsOf(parsed);
|
|
79
|
+
return cellName(Number.isFinite(b.top) ? b.top : 0, Number.isFinite(b.left) ? b.left : 0);
|
|
80
|
+
}
|
|
81
|
+
/** A value in a formula: numbers as they are, dates as day numbers, text in quotes. */
|
|
82
|
+
function literal(value) {
|
|
83
|
+
if (value == null) return "\"\"";
|
|
84
|
+
if (typeof value === "number") return String(value);
|
|
85
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
86
|
+
const serial = dateFormat(value) ? serialOf(value) : null;
|
|
87
|
+
if (serial != null) return String(serial);
|
|
88
|
+
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
89
|
+
}
|
|
90
|
+
var formulaText = (formula) => formula.startsWith("=") ? formula.slice(1) : formula;
|
|
91
|
+
var CELL_IS = {
|
|
92
|
+
eq: "equal",
|
|
93
|
+
ne: "notEqual",
|
|
94
|
+
gt: "greaterThan",
|
|
95
|
+
ge: "greaterThanOrEqual",
|
|
96
|
+
lt: "lessThan",
|
|
97
|
+
le: "lessThanOrEqual",
|
|
98
|
+
between: "between",
|
|
99
|
+
notBetween: "notBetween"
|
|
100
|
+
};
|
|
101
|
+
/** A tone's soft fill or plain color as the light theme paints it, for scales and bars. */
|
|
102
|
+
function toneColor(color, share) {
|
|
103
|
+
if (![
|
|
104
|
+
"neutral",
|
|
105
|
+
"primary",
|
|
106
|
+
"success",
|
|
107
|
+
"warning",
|
|
108
|
+
"danger"
|
|
109
|
+
].includes(color)) return lightThemeColor(color);
|
|
110
|
+
const base = color === "neutral" ? "var(--dou-color-text)" : `var(--dou-color-${color})`;
|
|
111
|
+
return lightThemeColor(`color-mix(in oklab, ${base} ${share}%, var(--dou-color-bg-surface))`);
|
|
112
|
+
}
|
|
113
|
+
/** Writes the cells a source exports as an .xlsx file. */
|
|
114
|
+
async function writeXlsx(source, options = {}) {
|
|
115
|
+
const book = source.book;
|
|
116
|
+
if (!source.sheets.length) throw new Error("[@dou-ui/sheet-xlsx] a workbook needs at least one sheet to be written");
|
|
117
|
+
const styles = new StyleSheet();
|
|
118
|
+
const strings = [];
|
|
119
|
+
const stringIds = /* @__PURE__ */ new Map();
|
|
120
|
+
let stringCount = 0;
|
|
121
|
+
const shared = (text) => {
|
|
122
|
+
stringCount++;
|
|
123
|
+
let id = stringIds.get(text);
|
|
124
|
+
if (id == null) {
|
|
125
|
+
id = strings.push(text) - 1;
|
|
126
|
+
stringIds.set(text, id);
|
|
127
|
+
}
|
|
128
|
+
return id;
|
|
129
|
+
};
|
|
130
|
+
const namedStyles = /* @__PURE__ */ new Map();
|
|
131
|
+
const styleNamed = (name) => {
|
|
132
|
+
if (!name) return void 0;
|
|
133
|
+
if (!namedStyles.has(name)) {
|
|
134
|
+
const style = book.styles?.[name];
|
|
135
|
+
namedStyles.set(name, style ? plainStyle(style) : void 0);
|
|
136
|
+
}
|
|
137
|
+
return namedStyles.get(name);
|
|
138
|
+
};
|
|
139
|
+
const lookStyle = (style) => typeof style === "string" ? styleNamed(style) : plainStyle(style);
|
|
140
|
+
const looks = /* @__PURE__ */ new Map();
|
|
141
|
+
const lookOf = (name, format, quote) => {
|
|
142
|
+
let byFormat = looks.get(name);
|
|
143
|
+
if (!byFormat) looks.set(name, byFormat = /* @__PURE__ */ new Map());
|
|
144
|
+
const key = (quote ? "'" : "") + (format ?? "");
|
|
145
|
+
let id = byFormat.get(key);
|
|
146
|
+
if (id == null) {
|
|
147
|
+
id = styles.look({
|
|
148
|
+
style: styleNamed(name),
|
|
149
|
+
format,
|
|
150
|
+
quote
|
|
151
|
+
});
|
|
152
|
+
byFormat.set(key, id);
|
|
153
|
+
}
|
|
154
|
+
return id;
|
|
155
|
+
};
|
|
156
|
+
const dates = /* @__PURE__ */ new Map();
|
|
157
|
+
const dateOf = (text) => {
|
|
158
|
+
let date = dates.get(text);
|
|
159
|
+
if (date === void 0) {
|
|
160
|
+
const format = dateFormat(text);
|
|
161
|
+
const serial = format ? serialOf(text) : null;
|
|
162
|
+
date = format && serial != null ? {
|
|
163
|
+
serial,
|
|
164
|
+
format
|
|
165
|
+
} : null;
|
|
166
|
+
if (dates.size < 1e5) dates.set(text, date);
|
|
167
|
+
}
|
|
168
|
+
return date;
|
|
169
|
+
};
|
|
170
|
+
let plainKind;
|
|
171
|
+
let plainFormat;
|
|
172
|
+
let plainQuote = false;
|
|
173
|
+
/** A value's content; its type, format and quoting go to `plainKind`, `plainFormat` and `plainQuote`. */
|
|
174
|
+
function plainCell(value, format) {
|
|
175
|
+
plainKind = void 0;
|
|
176
|
+
plainFormat = format;
|
|
177
|
+
plainQuote = false;
|
|
178
|
+
if (value == null || value === "") return "";
|
|
179
|
+
if (typeof value === "number") {
|
|
180
|
+
if (Number.isFinite(value)) return `<v>${value}</v>`;
|
|
181
|
+
plainKind = "e";
|
|
182
|
+
return "<v>#NUM!</v>";
|
|
183
|
+
}
|
|
184
|
+
if (typeof value === "boolean") {
|
|
185
|
+
plainKind = "b";
|
|
186
|
+
return `<v>${value ? 1 : 0}</v>`;
|
|
187
|
+
}
|
|
188
|
+
const first = value.charCodeAt(0);
|
|
189
|
+
const date = first >= 48 && first <= 57 ? dateOf(value) : null;
|
|
190
|
+
if (date) {
|
|
191
|
+
plainFormat = format ?? date.format;
|
|
192
|
+
return `<v>${date.serial}</v>`;
|
|
193
|
+
}
|
|
194
|
+
plainKind = "s";
|
|
195
|
+
if (value.startsWith("'=")) {
|
|
196
|
+
plainQuote = true;
|
|
197
|
+
return `<v>${shared(value.slice(1))}</v>`;
|
|
198
|
+
}
|
|
199
|
+
return `<v>${shared(value)}</v>`;
|
|
200
|
+
}
|
|
201
|
+
const selection = source.scope === "selection";
|
|
202
|
+
const keepFormulas = !selection && options.formulas !== false;
|
|
203
|
+
const { colWidth, rowHeight } = source;
|
|
204
|
+
const names = sheetNames(source.sheets.map((s) => s.data.name));
|
|
205
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const [i, { data }] of source.sheets.entries()) if (names[i] !== data.name || quoteSheet(data.name).startsWith("'")) renamed.set(data.name, names[i]);
|
|
207
|
+
const rename = (formula) => renameReferencesAll(formula, renamed);
|
|
208
|
+
const entries = [];
|
|
209
|
+
const sheetFiles = [];
|
|
210
|
+
const pacing = {
|
|
211
|
+
signal: source.signal,
|
|
212
|
+
lastPause: { at: performance.now() }
|
|
213
|
+
};
|
|
214
|
+
for (const [n, { index, data, bounds }] of source.sheets.entries()) {
|
|
215
|
+
source.signal?.throwIfAborted();
|
|
216
|
+
const top = selection && bounds ? bounds.top : 0;
|
|
217
|
+
const left = selection && bounds ? bounds.left : 0;
|
|
218
|
+
const last = bounds ? {
|
|
219
|
+
row: bounds.bottom,
|
|
220
|
+
col: bounds.right
|
|
221
|
+
} : {
|
|
222
|
+
row: -1,
|
|
223
|
+
col: -1
|
|
224
|
+
};
|
|
225
|
+
const letters = [];
|
|
226
|
+
let rowName = "";
|
|
227
|
+
const at = (_row, col) => (letters[col] ??= columnName(col - left)) + rowName;
|
|
228
|
+
const rowLevels = /* @__PURE__ */ new Map();
|
|
229
|
+
const colLevels = /* @__PURE__ */ new Map();
|
|
230
|
+
const hiddenRows = /* @__PURE__ */ new Set();
|
|
231
|
+
const hiddenCols = /* @__PURE__ */ new Set();
|
|
232
|
+
const collapsedRows = /* @__PURE__ */ new Set();
|
|
233
|
+
const collapsedCols = /* @__PURE__ */ new Set();
|
|
234
|
+
const after = data.groupSummary === "after";
|
|
235
|
+
if (!selection) for (const group of data.groups ?? []) {
|
|
236
|
+
const parsed = parseRange(group.range);
|
|
237
|
+
if (!parsed) continue;
|
|
238
|
+
const b = boundsOf(parsed);
|
|
239
|
+
const rows = b.left === 0 && b.right === Infinity;
|
|
240
|
+
const [start, end] = rows ? [b.top, b.bottom] : [b.left, b.right];
|
|
241
|
+
if (!Number.isFinite(end)) continue;
|
|
242
|
+
const levels = rows ? rowLevels : colLevels;
|
|
243
|
+
for (let i = start; i <= end; i++) levels.set(i, Math.min(7, (levels.get(i) ?? 0) + 1));
|
|
244
|
+
if (group.collapsed) {
|
|
245
|
+
const hidden = rows ? hiddenRows : hiddenCols;
|
|
246
|
+
for (let i = start; i <= end; i++) hidden.add(i);
|
|
247
|
+
(rows ? collapsedRows : collapsedCols).add(after ? end + 1 : start - 1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const [key, size] of Object.entries(data.rowSizes ?? {})) if (size === 0) hiddenRows.add(+key);
|
|
251
|
+
if (!selection) for (const row of filteredOut(source, index, data, last.row)) hiddenRows.add(row);
|
|
252
|
+
for (const [key, size] of Object.entries(data.colSizes ?? {})) if (size === 0) hiddenCols.add(+key);
|
|
253
|
+
const colList = /* @__PURE__ */ new Set();
|
|
254
|
+
for (const key of Object.keys(data.colSizes ?? {})) colList.add(+key);
|
|
255
|
+
for (const col of colLevels.keys()) colList.add(col);
|
|
256
|
+
for (const col of hiddenCols) colList.add(col);
|
|
257
|
+
const cols = [...colList].filter((col) => col >= left && (!selection || col <= last.col)).sort((a, b) => a - b).map((col) => {
|
|
258
|
+
const size = data.colSizes?.[col];
|
|
259
|
+
const hidden = hiddenCols.has(col);
|
|
260
|
+
return `<col${attrs({
|
|
261
|
+
min: col - left + 1,
|
|
262
|
+
max: col - left + 1,
|
|
263
|
+
width: widthOf(size || colWidth),
|
|
264
|
+
customWidth: size != null && size > 0 ? 1 : void 0,
|
|
265
|
+
hidden,
|
|
266
|
+
outlineLevel: colLevels.get(col),
|
|
267
|
+
collapsed: collapsedCols.has(col) ? 1 : void 0
|
|
268
|
+
})}/>`;
|
|
269
|
+
});
|
|
270
|
+
const lists = /* @__PURE__ */ new Map();
|
|
271
|
+
const notes = [];
|
|
272
|
+
const links = [];
|
|
273
|
+
const rows = [];
|
|
274
|
+
let nextCol = left;
|
|
275
|
+
let lastRow = last.row;
|
|
276
|
+
if (!selection) {
|
|
277
|
+
for (const row of rowLevels.keys()) if (row > lastRow) lastRow = row;
|
|
278
|
+
for (const row of hiddenRows) if (row > lastRow) lastRow = row;
|
|
279
|
+
for (const key of Object.keys(data.rowSizes ?? {})) if (+key > lastRow) lastRow = +key;
|
|
280
|
+
}
|
|
281
|
+
const chunks = [];
|
|
282
|
+
const flush = () => {
|
|
283
|
+
if (rows.length) chunks.push(encoder.encode(rows.join("")));
|
|
284
|
+
rows.length = 0;
|
|
285
|
+
};
|
|
286
|
+
for (let row = top; row <= lastRow; row++) {
|
|
287
|
+
const line = data.cells[row];
|
|
288
|
+
rowName = String(row - top + 1);
|
|
289
|
+
let xml = "";
|
|
290
|
+
nextCol = left;
|
|
291
|
+
if (line && row <= last.row) for (let col = left; col <= last.col && col < line.length; col++) {
|
|
292
|
+
const input = line[col];
|
|
293
|
+
if (input == null) continue;
|
|
294
|
+
xml += cellXML(row, col, input);
|
|
295
|
+
}
|
|
296
|
+
const size = data.rowSizes?.[row];
|
|
297
|
+
const level = rowLevels.get(row);
|
|
298
|
+
const hidden = hiddenRows.has(row);
|
|
299
|
+
if (!xml && size == null && !level && !hidden) continue;
|
|
300
|
+
if (rows.length >= 1e3) {
|
|
301
|
+
flush();
|
|
302
|
+
if (performance.now() - pacing.lastPause.at > 50) {
|
|
303
|
+
await pause();
|
|
304
|
+
source.signal?.throwIfAborted();
|
|
305
|
+
pacing.lastPause.at = performance.now();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
rows.push(`<row${attrs({
|
|
309
|
+
r: row - top + 1,
|
|
310
|
+
ht: size ? heightOf(size) : void 0,
|
|
311
|
+
customHeight: size ? 1 : void 0,
|
|
312
|
+
hidden,
|
|
313
|
+
outlineLevel: level,
|
|
314
|
+
collapsed: collapsedRows.has(row) ? 1 : void 0
|
|
315
|
+
})}${xml ? `>${xml}</row>` : "/>"}`);
|
|
316
|
+
}
|
|
317
|
+
function cellXML(row, col, input) {
|
|
318
|
+
const cell = typeof input === "object" && input !== null ? input : null;
|
|
319
|
+
if (cell && typeof cell.note === "string" && cell.note) notes.push({
|
|
320
|
+
ref: at(row, col),
|
|
321
|
+
row: row - top,
|
|
322
|
+
col: col - left,
|
|
323
|
+
text: cell.note
|
|
324
|
+
});
|
|
325
|
+
if (cell && typeof cell.link === "string") {
|
|
326
|
+
const target = linkTarget(cell.link);
|
|
327
|
+
if (target) links.push({
|
|
328
|
+
ref: at(row, col),
|
|
329
|
+
target,
|
|
330
|
+
link: cell.link
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const value = source.value(index, row, col);
|
|
334
|
+
const typed = source.cellType(index, row, col);
|
|
335
|
+
let format = cell && typeof cell.format === "string" ? cell.format : void 0;
|
|
336
|
+
const styleName = cell && typeof cell.style === "string" ? cell.style : void 0;
|
|
337
|
+
if (typed?.name === "select" && Array.isArray(typed.setup.options)) {
|
|
338
|
+
const key = cell?.type;
|
|
339
|
+
let list = lists.get(key);
|
|
340
|
+
if (list === void 0) {
|
|
341
|
+
const values = inlineList(typed.setup.options);
|
|
342
|
+
list = values == null ? null : {
|
|
343
|
+
values,
|
|
344
|
+
cells: []
|
|
345
|
+
};
|
|
346
|
+
lists.set(key, list);
|
|
347
|
+
}
|
|
348
|
+
list?.cells.push(at(row, col));
|
|
349
|
+
}
|
|
350
|
+
let body = "";
|
|
351
|
+
let kind;
|
|
352
|
+
let quote = false;
|
|
353
|
+
if (typed?.definition.plain) {
|
|
354
|
+
body = plainCell(source.plain(index, row, col), format);
|
|
355
|
+
kind = plainKind;
|
|
356
|
+
format = plainFormat;
|
|
357
|
+
} else if (typeof value === "string" && value.startsWith("=") && value.length > 1) {
|
|
358
|
+
const result = source.result(index, row, col);
|
|
359
|
+
if (isSheetError(result) && result.code === "#ERROR!") {
|
|
360
|
+
kind = "s";
|
|
361
|
+
body = `<v>${shared(value)}</v>`;
|
|
362
|
+
} else if (keepFormulas) {
|
|
363
|
+
body = `<f>${esc(formulaText(rename(value)))}</f>`;
|
|
364
|
+
if (isSheetError(result)) {
|
|
365
|
+
kind = "e";
|
|
366
|
+
body += `<v>${errorCode(result.code)}</v>`;
|
|
367
|
+
} else if (typeof result === "string") {
|
|
368
|
+
const first = result.charCodeAt(0);
|
|
369
|
+
const date = first >= 48 && first <= 57 ? dateOf(result) : null;
|
|
370
|
+
if (date) {
|
|
371
|
+
body += `<v>${date.serial}</v>`;
|
|
372
|
+
format ??= date.format;
|
|
373
|
+
} else {
|
|
374
|
+
kind = "str";
|
|
375
|
+
body += `<v>${esc(result)}</v>`;
|
|
376
|
+
}
|
|
377
|
+
} else if (result != null) {
|
|
378
|
+
body += plainCell(result, format);
|
|
379
|
+
kind = plainKind;
|
|
380
|
+
}
|
|
381
|
+
} else if (isSheetError(result)) {
|
|
382
|
+
kind = "e";
|
|
383
|
+
body = `<v>${errorCode(result.code)}</v>`;
|
|
384
|
+
} else {
|
|
385
|
+
body = plainCell(source.plain(index, row, col), format);
|
|
386
|
+
kind = plainKind;
|
|
387
|
+
format = plainFormat;
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
body = plainCell(value, format);
|
|
391
|
+
kind = plainKind;
|
|
392
|
+
format = plainFormat;
|
|
393
|
+
quote = plainQuote;
|
|
394
|
+
}
|
|
395
|
+
const s = styleName || format || quote ? lookOf(styleName, format, quote) : 0;
|
|
396
|
+
if (!body && !s) return "";
|
|
397
|
+
const named = col !== nextCol;
|
|
398
|
+
nextCol = col + 1;
|
|
399
|
+
return `<c${named ? ` r="${at(row, col)}"` : ""}${s ? ` s="${s}"` : ""}${kind ? ` t="${kind}"` : ""}${body ? `>${body}</c>` : "/>"}`;
|
|
400
|
+
}
|
|
401
|
+
const merges = [];
|
|
402
|
+
for (const merge of data.merges ?? []) {
|
|
403
|
+
const parsed = parseRange(merge);
|
|
404
|
+
if (!parsed) continue;
|
|
405
|
+
const b = boundsOf(parsed);
|
|
406
|
+
if (selection && bounds) {
|
|
407
|
+
if (b.top < top || b.left < left || b.bottom > bounds.bottom || b.right > bounds.right) continue;
|
|
408
|
+
}
|
|
409
|
+
merges.push(`<mergeCell ref="${rangeText({
|
|
410
|
+
top: b.top - top,
|
|
411
|
+
left: b.left - left,
|
|
412
|
+
bottom: b.bottom - top,
|
|
413
|
+
right: b.right - left
|
|
414
|
+
})}"/>`);
|
|
415
|
+
}
|
|
416
|
+
const extra = selection ? null : sheetRules(data);
|
|
417
|
+
const freeze = !selection && data.freeze && (data.freeze.rows || data.freeze.cols) ? data.freeze : null;
|
|
418
|
+
const pane = freeze ? `<pane${attrs({
|
|
419
|
+
xSplit: freeze.cols || void 0,
|
|
420
|
+
ySplit: freeze.rows || void 0,
|
|
421
|
+
topLeftCell: cellName(freeze.rows ?? 0, freeze.cols ?? 0),
|
|
422
|
+
activePane: freeze.rows && freeze.cols ? "bottomRight" : freeze.rows ? "bottomLeft" : "topRight",
|
|
423
|
+
state: "frozen"
|
|
424
|
+
})}/>` : "";
|
|
425
|
+
const hasLevels = rowLevels.size || colLevels.size;
|
|
426
|
+
const validations = [...[...lists.values()].filter((list) => list != null).map(({ values, cells }) => `<dataValidation type="list" allowBlank="1" showErrorMessage="1" sqref="${esc(compactRefs(cells))}"><formula1>${esc(values)}</formula1></dataValidation>`), ...extra?.validations ?? []];
|
|
427
|
+
const relsList = [];
|
|
428
|
+
let legacy = "";
|
|
429
|
+
if (notes.length) {
|
|
430
|
+
const id = n + 1;
|
|
431
|
+
entries.push({
|
|
432
|
+
path: `xl/comments${id}.xml`,
|
|
433
|
+
data: commentsXML(notes)
|
|
434
|
+
});
|
|
435
|
+
entries.push({
|
|
436
|
+
path: `xl/drawings/vmlDrawing${id}.vml`,
|
|
437
|
+
data: vmlXML(notes)
|
|
438
|
+
});
|
|
439
|
+
relsList.push(`<Relationship Id="rId1" Type="${NS_R}/comments" Target="../comments${id}.xml"/>`, `<Relationship Id="rId2" Type="${NS_R}/vmlDrawing" Target="../drawings/vmlDrawing${id}.vml"/>`);
|
|
440
|
+
legacy = "<legacyDrawing r:id=\"rId2\"/>";
|
|
441
|
+
}
|
|
442
|
+
const hyperlinks = links.map(({ ref, target, link }) => {
|
|
443
|
+
if (target.kind === "cell") return `<hyperlink${attrs({
|
|
444
|
+
ref,
|
|
445
|
+
location: link.slice(1)
|
|
446
|
+
})}/>`;
|
|
447
|
+
const id = `rId${relsList.length + 1}`;
|
|
448
|
+
relsList.push(`<Relationship Id="${id}" Type="${NS_R}/hyperlink" Target="${esc(target.href)}" TargetMode="External"/>`);
|
|
449
|
+
return `<hyperlink${attrs({
|
|
450
|
+
ref,
|
|
451
|
+
"r:id": id
|
|
452
|
+
})}/>`;
|
|
453
|
+
});
|
|
454
|
+
const protect = !selection && data.protect ? protectionXML(data.protect) : "";
|
|
455
|
+
const lastCell = last.row >= top && last.col >= left ? rangeText({
|
|
456
|
+
top: 0,
|
|
457
|
+
left: 0,
|
|
458
|
+
bottom: last.row - top,
|
|
459
|
+
right: last.col - left
|
|
460
|
+
}) : "A1";
|
|
461
|
+
flush();
|
|
462
|
+
const head = `${HEAD}<worksheet xmlns="${NS}" xmlns:r="${NS_R}">` + (hasLevels ? `<sheetPr><outlinePr${attrs({
|
|
463
|
+
summaryBelow: after ? 1 : 0,
|
|
464
|
+
summaryRight: after ? 1 : 0
|
|
465
|
+
})}/></sheetPr>` : "") + `<dimension ref="${lastCell}"/><sheetViews><sheetView${attrs({
|
|
466
|
+
workbookViewId: 0,
|
|
467
|
+
tabSelected: n === 0 ? 1 : void 0
|
|
468
|
+
})}>${pane}</sheetView></sheetViews><sheetFormatPr${attrs({
|
|
469
|
+
defaultRowHeight: heightOf(rowHeight),
|
|
470
|
+
defaultColWidth: widthOf(colWidth),
|
|
471
|
+
customHeight: 1,
|
|
472
|
+
outlineLevelRow: maxLevel(rowLevels),
|
|
473
|
+
outlineLevelCol: maxLevel(colLevels)
|
|
474
|
+
})}/>` + (cols.length ? `<cols>${cols.join("")}</cols>` : "") + "<sheetData>";
|
|
475
|
+
const tail = "</sheetData>" + protect + (extra?.filter ?? "") + (merges.length ? `<mergeCells count="${merges.length}">${merges.join("")}</mergeCells>` : "") + (extra?.conditional ?? "") + (validations.length ? `<dataValidations count="${validations.length}">${validations.join("")}</dataValidations>` : "") + (hyperlinks.length ? `<hyperlinks>${hyperlinks.join("")}</hyperlinks>` : "") + legacy + "</worksheet>";
|
|
476
|
+
entries.push({
|
|
477
|
+
path: `xl/worksheets/sheet${n + 1}.xml`,
|
|
478
|
+
data: [
|
|
479
|
+
encoder.encode(head),
|
|
480
|
+
...chunks,
|
|
481
|
+
encoder.encode(tail)
|
|
482
|
+
]
|
|
483
|
+
});
|
|
484
|
+
if (relsList.length) entries.push({
|
|
485
|
+
path: `xl/worksheets/_rels/sheet${n + 1}.xml.rels`,
|
|
486
|
+
data: `${HEAD}<Relationships xmlns="${NS_PKG}">${relsList.join("")}</Relationships>`
|
|
487
|
+
});
|
|
488
|
+
sheetFiles.push({
|
|
489
|
+
name: names[n],
|
|
490
|
+
hidden: false,
|
|
491
|
+
filter: !selection && data.filter ? data.filter.range : void 0
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/** A sheet's filter, conditional formats and validations. */
|
|
495
|
+
function sheetRules(data) {
|
|
496
|
+
let filter = "";
|
|
497
|
+
if (data.filter) {
|
|
498
|
+
const parsed = parseRange(data.filter.range);
|
|
499
|
+
const columns = Object.entries(data.filter.columns ?? {}).map(([col, rule]) => {
|
|
500
|
+
const id = +col - (parsed ? boundsOf(parsed).left : 0);
|
|
501
|
+
if ("values" in rule) {
|
|
502
|
+
const blank = rule.values.includes("");
|
|
503
|
+
const values = rule.values.filter((v) => v !== "").map((v) => `<filter val="${esc(v)}"/>`).join("");
|
|
504
|
+
return `<filterColumn colId="${id}"><filters${attrs({ blank })}>${values}</filters></filterColumn>`;
|
|
505
|
+
}
|
|
506
|
+
const custom = customFilter(rule.condition);
|
|
507
|
+
return custom ? `<filterColumn colId="${id}">${custom}</filterColumn>` : "";
|
|
508
|
+
}).join("");
|
|
509
|
+
filter = `<autoFilter ref="${esc(data.filter.range.replaceAll("$", ""))}"${columns ? `>${columns}</autoFilter>` : "/>"}`;
|
|
510
|
+
}
|
|
511
|
+
let priority = 0;
|
|
512
|
+
const conditional = (data.conditionalFormats ?? []).map((rule) => {
|
|
513
|
+
const xml = conditionalRule(rule, ++priority);
|
|
514
|
+
return xml ? `<conditionalFormatting sqref="${esc(sqref(rule.range))}">${xml}</conditionalFormatting>` : "";
|
|
515
|
+
}).join("");
|
|
516
|
+
const validations = (data.validations ?? []).map((rule) => validationXML(rule, rename)).filter(Boolean);
|
|
517
|
+
return {
|
|
518
|
+
filter,
|
|
519
|
+
conditional,
|
|
520
|
+
validations
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function conditionalRule(rule, priority) {
|
|
524
|
+
const dxf = () => {
|
|
525
|
+
const look = "style" in rule && rule.style != null ? lookStyle(rule.style) : void 0;
|
|
526
|
+
return look ? styles.differential(look) : void 0;
|
|
527
|
+
};
|
|
528
|
+
const anchor = anchorOf(rule.range);
|
|
529
|
+
if ("when" in rule) {
|
|
530
|
+
const { op, value, to } = rule.when;
|
|
531
|
+
const cellIs = CELL_IS[op];
|
|
532
|
+
if (cellIs) return `<cfRule${attrs({
|
|
533
|
+
type: "cellIs",
|
|
534
|
+
dxfId: dxf(),
|
|
535
|
+
priority,
|
|
536
|
+
operator: cellIs
|
|
537
|
+
})}><formula>${esc(literal(value))}</formula>${op === "between" || op === "notBetween" ? `<formula>${esc(literal(to))}</formula>` : ""}</cfRule>`;
|
|
538
|
+
const text = String(value ?? "");
|
|
539
|
+
const quoted = literal(text);
|
|
540
|
+
const textRule = (type, operator, formula) => `<cfRule${attrs({
|
|
541
|
+
type,
|
|
542
|
+
dxfId: dxf(),
|
|
543
|
+
priority,
|
|
544
|
+
operator,
|
|
545
|
+
text: operator ? text : void 0
|
|
546
|
+
})}><formula>${esc(formula)}</formula></cfRule>`;
|
|
547
|
+
if (op === "contains") return textRule("containsText", "containsText", `NOT(ISERROR(SEARCH(${quoted},${anchor})))`);
|
|
548
|
+
if (op === "notContains") return textRule("notContainsText", "notContains", `ISERROR(SEARCH(${quoted},${anchor}))`);
|
|
549
|
+
if (op === "begins") return textRule("beginsWith", "beginsWith", `LEFT(${anchor},LEN(${quoted}))=${quoted}`);
|
|
550
|
+
if (op === "ends") return textRule("endsWith", "endsWith", `RIGHT(${anchor},LEN(${quoted}))=${quoted}`);
|
|
551
|
+
if (op === "blank") return textRule("containsBlanks", void 0, `LEN(TRIM(${anchor}))=0`);
|
|
552
|
+
if (op === "notBlank") return textRule("notContainsBlanks", void 0, `LEN(TRIM(${anchor}))>0`);
|
|
553
|
+
return "";
|
|
554
|
+
}
|
|
555
|
+
if ("formula" in rule) return `<cfRule${attrs({
|
|
556
|
+
type: "expression",
|
|
557
|
+
dxfId: dxf(),
|
|
558
|
+
priority
|
|
559
|
+
})}><formula>${esc(formulaText(rename(rule.formula)))}</formula></cfRule>`;
|
|
560
|
+
if ("top" in rule || "bottom" in rule) {
|
|
561
|
+
const rank = rule.top ?? rule.bottom ?? 10;
|
|
562
|
+
return `<cfRule${attrs({
|
|
563
|
+
type: "top10",
|
|
564
|
+
dxfId: dxf(),
|
|
565
|
+
priority,
|
|
566
|
+
percent: rule.percent,
|
|
567
|
+
bottom: rule.bottom != null,
|
|
568
|
+
rank
|
|
569
|
+
})}/>`;
|
|
570
|
+
}
|
|
571
|
+
if ("average" in rule) return `<cfRule${attrs({
|
|
572
|
+
type: "aboveAverage",
|
|
573
|
+
dxfId: dxf(),
|
|
574
|
+
priority,
|
|
575
|
+
aboveAverage: rule.average === "above" ? void 0 : "0"
|
|
576
|
+
})}/>`;
|
|
577
|
+
if ("duplicates" in rule) return `<cfRule${attrs({
|
|
578
|
+
type: "duplicateValues",
|
|
579
|
+
dxfId: dxf(),
|
|
580
|
+
priority
|
|
581
|
+
})}/>`;
|
|
582
|
+
if ("unique" in rule) return `<cfRule${attrs({
|
|
583
|
+
type: "uniqueValues",
|
|
584
|
+
dxfId: dxf(),
|
|
585
|
+
priority
|
|
586
|
+
})}/>`;
|
|
587
|
+
if ("scale" in rule) {
|
|
588
|
+
const colors = rule.scale.map((c) => argb(toneColor(c, 32)) ?? "FFFFFFFF");
|
|
589
|
+
const points = colors.length === 3 ? "<cfvo type=\"min\"/><cfvo type=\"percentile\" val=\"50\"/><cfvo type=\"max\"/>" : "<cfvo type=\"min\"/><cfvo type=\"max\"/>";
|
|
590
|
+
return `<cfRule${attrs({
|
|
591
|
+
type: "colorScale",
|
|
592
|
+
priority
|
|
593
|
+
})}><colorScale>${points}${colors.map((c) => `<color rgb="${c}"/>`).join("")}</colorScale></cfRule>`;
|
|
594
|
+
}
|
|
595
|
+
if ("bar" in rule) {
|
|
596
|
+
const color = argb(toneColor(rule.bar === true ? "neutral" : rule.bar, 30)) ?? "FF638EC6";
|
|
597
|
+
return `<cfRule${attrs({
|
|
598
|
+
type: "dataBar",
|
|
599
|
+
priority
|
|
600
|
+
})}><dataBar><cfvo type="min"/><cfvo type="max"/><color rgb="${color}"/></dataBar></cfRule>`;
|
|
601
|
+
}
|
|
602
|
+
return "";
|
|
603
|
+
}
|
|
604
|
+
const sheetsXML = sheetFiles.map((s, i) => `<sheet${attrs({
|
|
605
|
+
name: s.name,
|
|
606
|
+
sheetId: i + 1,
|
|
607
|
+
"r:id": `rId${i + 1}`
|
|
608
|
+
})}/>`).join("");
|
|
609
|
+
const defined = sheetFiles.map((s, i) => s.filter ? `<definedName name="_xlnm._FilterDatabase" localSheetId="${i}" hidden="1">${esc(`${quoteSheet(s.name)}!${absolute(s.filter)}`)}</definedName>` : "").join("");
|
|
610
|
+
entries.push({
|
|
611
|
+
path: "xl/workbook.xml",
|
|
612
|
+
data: `${HEAD}<workbook xmlns="${NS}" xmlns:r="${NS_R}">` + (book.protect ? "<workbookProtection lockStructure=\"1\"/>" : "") + `<bookViews><workbookView activeTab="0"/></bookViews><sheets>${sheetsXML}</sheets>` + (defined ? `<definedNames>${defined}</definedNames>` : "") + "<calcPr calcId=\"191029\" fullCalcOnLoad=\"1\"/></workbook>"
|
|
613
|
+
});
|
|
614
|
+
const count = sheetFiles.length;
|
|
615
|
+
entries.push({
|
|
616
|
+
path: "xl/_rels/workbook.xml.rels",
|
|
617
|
+
data: `${HEAD}<Relationships xmlns="${NS_PKG}">` + sheetFiles.map((_, i) => `<Relationship Id="rId${i + 1}" Type="${NS_R}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`).join("") + `<Relationship Id="rId${count + 1}" Type="${NS_R}/styles" Target="styles.xml"/><Relationship Id="rId${count + 2}" Type="${NS_R}/sharedStrings" Target="sharedStrings.xml"/></Relationships>`
|
|
618
|
+
});
|
|
619
|
+
entries.push({
|
|
620
|
+
path: "xl/styles.xml",
|
|
621
|
+
data: styles.toXML()
|
|
622
|
+
});
|
|
623
|
+
entries.push({
|
|
624
|
+
path: "xl/sharedStrings.xml",
|
|
625
|
+
data: `${HEAD}<sst xmlns="${NS}" count="${stringCount}" uniqueCount="${strings.length}">` + strings.map((t) => `<si><t${/^\s|\s$|\n/.test(t) ? " xml:space=\"preserve\"" : ""}>${esc(t)}</t></si>`).join("") + "</sst>"
|
|
626
|
+
});
|
|
627
|
+
entries.push({
|
|
628
|
+
path: "_rels/.rels",
|
|
629
|
+
data: `${HEAD}<Relationships xmlns="${NS_PKG}"><Relationship Id="rId1" Type="${NS_R}/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
630
|
+
});
|
|
631
|
+
const hasNotes = entries.some((e) => e.path.startsWith("xl/comments"));
|
|
632
|
+
entries.push({
|
|
633
|
+
path: "[Content_Types].xml",
|
|
634
|
+
data: `${HEAD}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>` + (hasNotes ? "<Default Extension=\"vml\" ContentType=\"application/vnd.openxmlformats-officedocument.vmlDrawing\"/>" : "") + "<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>" + sheetFiles.map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join("") + entries.filter((e) => e.path.startsWith("xl/comments")).map((e) => `<Override PartName="/${e.path}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>`).join("") + "<Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/><Override PartName=\"/xl/sharedStrings.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\"/></Types>"
|
|
635
|
+
});
|
|
636
|
+
entries.unshift(entries.pop());
|
|
637
|
+
source.signal?.throwIfAborted();
|
|
638
|
+
return zip(entries, pacing);
|
|
639
|
+
}
|
|
640
|
+
/** The rows of a sheet's filter that its rules hide, as the grid hides them. */
|
|
641
|
+
function filteredOut(source, index, data, lastRow) {
|
|
642
|
+
const out = /* @__PURE__ */ new Set();
|
|
643
|
+
const parsed = data.filter?.columns && parseRange(data.filter.range);
|
|
644
|
+
if (!parsed) return out;
|
|
645
|
+
const b = boundsOf(parsed);
|
|
646
|
+
const rules = Object.entries(data.filter.columns).map(([key, rule]) => ({
|
|
647
|
+
col: Number(key),
|
|
648
|
+
rule
|
|
649
|
+
})).filter(({ col }) => col >= b.left && col <= b.right);
|
|
650
|
+
const collator = textCollator(source.locale);
|
|
651
|
+
for (let row = b.top + 1; row <= Math.min(b.bottom, lastRow); row++) for (const { col, rule } of rules) {
|
|
652
|
+
const result = source.result(index, row, col);
|
|
653
|
+
const value = isSheetError(result) ? result.code : result;
|
|
654
|
+
const text = value == null || value === "" ? "" : source.text(index, row, col);
|
|
655
|
+
if (!meetsRule(rule, value, text, collator)) {
|
|
656
|
+
out.add(row);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
/** A part of a sheet's protection as the file writes it: `0` for what people may still do. */
|
|
663
|
+
var kept = (allowed) => allowed ? 0 : void 0;
|
|
664
|
+
/**
|
|
665
|
+
* A sheet's protection. In the file each part says what is kept from
|
|
666
|
+
* people (`1`, the default for most), so what the sheet allows is `0`.
|
|
667
|
+
*/
|
|
668
|
+
function protectionXML(protect) {
|
|
669
|
+
const allow = protect === true ? {} : protect;
|
|
670
|
+
return `<sheetProtection${attrs({
|
|
671
|
+
sheet: 1,
|
|
672
|
+
objects: 1,
|
|
673
|
+
scenarios: 1,
|
|
674
|
+
formatCells: kept(allow.formatCells),
|
|
675
|
+
formatColumns: kept(allow.formatColumns),
|
|
676
|
+
formatRows: kept(allow.formatRows),
|
|
677
|
+
insertColumns: kept(allow.insertColumns),
|
|
678
|
+
insertRows: kept(allow.insertRows),
|
|
679
|
+
deleteColumns: kept(allow.deleteColumns),
|
|
680
|
+
deleteRows: kept(allow.deleteRows),
|
|
681
|
+
autoFilter: kept(allow.filter)
|
|
682
|
+
})}/>`;
|
|
683
|
+
}
|
|
684
|
+
/** Cell names, with runs down a column written as ranges. */
|
|
685
|
+
function compactRefs(refs) {
|
|
686
|
+
const byCol = /* @__PURE__ */ new Map();
|
|
687
|
+
for (const ref of refs) {
|
|
688
|
+
const [, col, row] = /^([A-Z]+)(\d+)$/.exec(ref);
|
|
689
|
+
const list = byCol.get(col) ?? [];
|
|
690
|
+
list.push(+row);
|
|
691
|
+
byCol.set(col, list);
|
|
692
|
+
}
|
|
693
|
+
const out = [];
|
|
694
|
+
for (const [col, list] of byCol) {
|
|
695
|
+
list.sort((a, b) => a - b);
|
|
696
|
+
let start = list[0];
|
|
697
|
+
let prev = start;
|
|
698
|
+
for (const row of [...list.slice(1), Infinity]) {
|
|
699
|
+
if (row === prev + 1) {
|
|
700
|
+
prev = row;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
out.push(start === prev ? `${col}${start}` : `${col}${start}:${col}${prev}`);
|
|
704
|
+
start = prev = row;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return out.join(" ");
|
|
708
|
+
}
|
|
709
|
+
/** A validation as the file writes it, `''` for one it can't hold. */
|
|
710
|
+
function validationXML(rule, rename) {
|
|
711
|
+
const common = {
|
|
712
|
+
allowBlank: 1,
|
|
713
|
+
showErrorMessage: 1,
|
|
714
|
+
showInputMessage: rule.prompt ? 1 : void 0,
|
|
715
|
+
errorStyle: rule.strict === false ? "warning" : void 0,
|
|
716
|
+
error: rule.message,
|
|
717
|
+
prompt: rule.prompt,
|
|
718
|
+
sqref: sqref(rule.range)
|
|
719
|
+
};
|
|
720
|
+
if ("list" in rule) {
|
|
721
|
+
const list = typeof rule.list === "string" ? formulaText(rename(rule.list)) : inlineList(rule.list);
|
|
722
|
+
if (list == null || list.length > 255) return "";
|
|
723
|
+
return `<dataValidation${attrs({
|
|
724
|
+
type: "list",
|
|
725
|
+
...common
|
|
726
|
+
})}>${body(list)}</dataValidation>`;
|
|
727
|
+
}
|
|
728
|
+
if ("formula" in rule) return `<dataValidation${attrs({
|
|
729
|
+
type: "custom",
|
|
730
|
+
...common
|
|
731
|
+
})}>${body(formulaText(rename(rule.formula)))}</dataValidation>`;
|
|
732
|
+
const { op, value, to } = rule.when;
|
|
733
|
+
const operator = CELL_IS[op];
|
|
734
|
+
const numeric = typeof value === "number" || typeof value === "string" && !!dateFormat(value);
|
|
735
|
+
if (operator && numeric) {
|
|
736
|
+
const type = typeof value === "string" ? "date" : rule.integer ? "whole" : "decimal";
|
|
737
|
+
const formulas = op === "between" || op === "notBetween" ? [literal(value), literal(to)] : [literal(value)];
|
|
738
|
+
return `<dataValidation${attrs({
|
|
739
|
+
type,
|
|
740
|
+
operator,
|
|
741
|
+
...common
|
|
742
|
+
})}>${body(...formulas)}</dataValidation>`;
|
|
743
|
+
}
|
|
744
|
+
const anchor = anchorOf(rule.range);
|
|
745
|
+
const quoted = literal(String(value ?? ""));
|
|
746
|
+
const custom = {
|
|
747
|
+
eq: `EXACT(LOWER(${anchor}),LOWER(${quoted}))`,
|
|
748
|
+
ne: `NOT(EXACT(LOWER(${anchor}),LOWER(${quoted})))`,
|
|
749
|
+
contains: `NOT(ISERROR(SEARCH(${quoted},${anchor})))`,
|
|
750
|
+
notContains: `ISERROR(SEARCH(${quoted},${anchor}))`,
|
|
751
|
+
begins: `LEFT(LOWER(${anchor}),LEN(${quoted}))=LOWER(${quoted})`,
|
|
752
|
+
ends: `RIGHT(LOWER(${anchor}),LEN(${quoted}))=LOWER(${quoted})`,
|
|
753
|
+
notBlank: `LEN(TRIM(${anchor}))>0`
|
|
754
|
+
}[op];
|
|
755
|
+
return custom ? `<dataValidation${attrs({
|
|
756
|
+
type: "custom",
|
|
757
|
+
...common
|
|
758
|
+
})}>${body(custom)}</dataValidation>` : "";
|
|
759
|
+
}
|
|
760
|
+
/** A validation's formulas. */
|
|
761
|
+
var body = (...formulas) => formulas.map((f, i) => `<formula${i + 1}>${esc(f)}</formula${i + 1}>`).join("");
|
|
762
|
+
/** One condition of a custom filter. */
|
|
763
|
+
var one = (operator, val) => `<customFilter${attrs({
|
|
764
|
+
operator,
|
|
765
|
+
val
|
|
766
|
+
})}/>`;
|
|
767
|
+
/** A filter's condition as the file's custom filter, `''` when it has none. */
|
|
768
|
+
function customFilter(condition) {
|
|
769
|
+
const value = condition.value == null ? "" : String(condition.value);
|
|
770
|
+
switch (condition.op) {
|
|
771
|
+
case "eq": return `<customFilters>${one(void 0, value)}</customFilters>`;
|
|
772
|
+
case "ne": return `<customFilters>${one("notEqual", value)}</customFilters>`;
|
|
773
|
+
case "gt": return `<customFilters>${one("greaterThan", value)}</customFilters>`;
|
|
774
|
+
case "ge": return `<customFilters>${one("greaterThanOrEqual", value)}</customFilters>`;
|
|
775
|
+
case "lt": return `<customFilters>${one("lessThan", value)}</customFilters>`;
|
|
776
|
+
case "le": return `<customFilters>${one("lessThanOrEqual", value)}</customFilters>`;
|
|
777
|
+
case "between": return `<customFilters and="1">${one("greaterThanOrEqual", value)}${one("lessThanOrEqual", String(condition.to ?? ""))}</customFilters>`;
|
|
778
|
+
case "notBetween": return `<customFilters>${one("lessThan", value)}${one("greaterThan", String(condition.to ?? ""))}</customFilters>`;
|
|
779
|
+
case "contains": return `<customFilters>${one(void 0, `*${value}*`)}</customFilters>`;
|
|
780
|
+
case "notContains": return `<customFilters>${one("notEqual", `*${value}*`)}</customFilters>`;
|
|
781
|
+
case "begins": return `<customFilters>${one(void 0, `${value}*`)}</customFilters>`;
|
|
782
|
+
case "ends": return `<customFilters>${one(void 0, `*${value}`)}</customFilters>`;
|
|
783
|
+
case "blank": return "<filters blank=\"1\"/>";
|
|
784
|
+
case "notBlank": return `<customFilters>${one("notEqual", " ")}</customFilters>`;
|
|
785
|
+
default: return "";
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
var absolute = (range) => range.replaceAll("$", "").split(":").map((ref) => ref.replace(/^([A-Z]+)(\d+)$/, "$$$1$$$2")).join(":");
|
|
789
|
+
/** The notes of a sheet as the file's comments. */
|
|
790
|
+
function commentsXML(notes) {
|
|
791
|
+
return `${HEAD}<comments xmlns="${NS}"><authors><author></author></authors><commentList>` + notes.map((note) => `<comment ref="${note.ref}" authorId="0"><text><t xml:space="preserve">${esc(note.text)}</t></text></comment>`).join("") + "</commentList></comments>";
|
|
792
|
+
}
|
|
793
|
+
/** The boxes notes show in, which other programs need to show them. */
|
|
794
|
+
function vmlXML(notes) {
|
|
795
|
+
return "<xml xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:x=\"urn:schemas-microsoft-com:office:excel\"><o:shapelayout v:ext=\"edit\"><o:idmap v:ext=\"edit\" data=\"1\"/></o:shapelayout><v:shapetype id=\"_x0000_t202\" coordsize=\"21600,21600\" o:spt=\"202\" path=\"m,l,21600r21600,l21600,xe\"><v:stroke joinstyle=\"miter\"/><v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/></v:shapetype>" + notes.map((note, i) => `<v:shape id="_x0000_s${1025 + i}" type="#_x0000_t202" style="position:absolute;margin-left:80pt;margin-top:2pt;width:120pt;height:60pt;z-index:${i + 1};visibility:hidden" fillcolor="#ffffe1" o:insetmode="auto"><v:fill color2="#ffffe1"/><v:shadow on="t" color="black" obscured="t"/><v:path o:connecttype="none"/><v:textbox style="mso-direction-alt:auto"><div style="text-align:left"></div></v:textbox><x:ClientData ObjectType="Note"><x:MoveWithCells/><x:SizeWithCells/><x:AutoFill>False</x:AutoFill><x:Row>${note.row}</x:Row><x:Column>${note.col}</x:Column></x:ClientData></v:shape>`).join("") + "</xml>";
|
|
796
|
+
}
|
|
797
|
+
export { writeXlsx };
|
|
798
|
+
|
|
799
|
+
//# sourceMappingURL=write.js.map
|