@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/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