werkmap 0.2.0 → 0.3.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/README.md +3 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +661 -430
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -57,6 +57,66 @@ const DOS_DATE = (1 << 5) | 1;
|
|
|
57
57
|
const STORED = 0;
|
|
58
58
|
const DEFLATED = 8;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {{ method: number, sum: number, packed: Uint8Array, raw: Uint8Array, name: Uint8Array }} Packed
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/** @param {Packed} entry */
|
|
65
|
+
const localHeader = ({ method, sum, packed, raw, name }) => {
|
|
66
|
+
const head = new DataView(new ArrayBuffer(30));
|
|
67
|
+
head.setUint32(0, 0x04034b50, true);
|
|
68
|
+
head.setUint16(4, 20, true);
|
|
69
|
+
head.setUint16(6, 0x0800, true); // the name is UTF-8
|
|
70
|
+
head.setUint16(8, method, true);
|
|
71
|
+
head.setUint16(10, DOS_TIME, true);
|
|
72
|
+
head.setUint16(12, DOS_DATE, true);
|
|
73
|
+
head.setUint32(14, sum, true);
|
|
74
|
+
head.setUint32(18, packed.length, true);
|
|
75
|
+
head.setUint32(22, raw.length, true);
|
|
76
|
+
head.setUint16(26, name.length, true);
|
|
77
|
+
return new Uint8Array(head.buffer);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** @param {Packed} entry @param {number} offset */
|
|
81
|
+
const centralRecord = ({ method, sum, packed, raw, name }, offset) => {
|
|
82
|
+
const record = new DataView(new ArrayBuffer(46));
|
|
83
|
+
record.setUint32(0, 0x02014b50, true);
|
|
84
|
+
record.setUint16(4, 20, true);
|
|
85
|
+
record.setUint16(6, 20, true);
|
|
86
|
+
record.setUint16(8, 0x0800, true);
|
|
87
|
+
record.setUint16(10, method, true);
|
|
88
|
+
record.setUint16(12, DOS_TIME, true);
|
|
89
|
+
record.setUint16(14, DOS_DATE, true);
|
|
90
|
+
record.setUint32(16, sum, true);
|
|
91
|
+
record.setUint32(20, packed.length, true);
|
|
92
|
+
record.setUint32(24, raw.length, true);
|
|
93
|
+
record.setUint16(28, name.length, true);
|
|
94
|
+
record.setUint32(42, offset, true);
|
|
95
|
+
return new Uint8Array(record.buffer);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** @param {number} count @param {number} size @param {number} offset */
|
|
99
|
+
const endRecord = (count, size, offset) => {
|
|
100
|
+
const end = new DataView(new ArrayBuffer(22));
|
|
101
|
+
end.setUint32(0, 0x06054b50, true);
|
|
102
|
+
end.setUint16(8, count, true);
|
|
103
|
+
end.setUint16(10, count, true);
|
|
104
|
+
end.setUint32(12, size, true);
|
|
105
|
+
end.setUint32(16, offset, true);
|
|
106
|
+
return new Uint8Array(end.buffer);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** @param {ReadonlyArray<Uint8Array>} chunks */
|
|
110
|
+
const concat = (chunks) => {
|
|
111
|
+
const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
112
|
+
let at = 0;
|
|
113
|
+
for (const chunk of chunks) {
|
|
114
|
+
bytes.set(chunk, at);
|
|
115
|
+
at += chunk.length;
|
|
116
|
+
}
|
|
117
|
+
return bytes;
|
|
118
|
+
};
|
|
119
|
+
|
|
60
120
|
/**
|
|
61
121
|
* @param {ReadonlyArray<{ name: string, body: Uint8Array, store?: boolean }>} entries
|
|
62
122
|
* @returns {Promise<Uint8Array>}
|
|
@@ -68,58 +128,16 @@ const zip = async (entries) => {
|
|
|
68
128
|
|
|
69
129
|
for (const entry of entries) {
|
|
70
130
|
const raw = entry.body;
|
|
71
|
-
const packed = entry.store ? raw : await deflate(raw);
|
|
72
|
-
const method = entry.store ? STORED : DEFLATED;
|
|
131
|
+
const [packed, method] = entry.store ? [raw, STORED] : [await deflate(raw), DEFLATED];
|
|
73
132
|
const name = utf8(entry.name);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
head.setUint32(0, 0x04034b50, true);
|
|
78
|
-
head.setUint16(4, 20, true);
|
|
79
|
-
head.setUint16(6, 0x0800, true); // the name is UTF-8
|
|
80
|
-
head.setUint16(8, method, true);
|
|
81
|
-
head.setUint16(10, DOS_TIME, true);
|
|
82
|
-
head.setUint16(12, DOS_DATE, true);
|
|
83
|
-
head.setUint32(14, sum, true);
|
|
84
|
-
head.setUint32(18, packed.length, true);
|
|
85
|
-
head.setUint32(22, raw.length, true);
|
|
86
|
-
head.setUint16(26, name.length, true);
|
|
87
|
-
local.push(new Uint8Array(head.buffer), name, packed);
|
|
88
|
-
|
|
89
|
-
const record = new DataView(new ArrayBuffer(46));
|
|
90
|
-
record.setUint32(0, 0x02014b50, true);
|
|
91
|
-
record.setUint16(4, 20, true);
|
|
92
|
-
record.setUint16(6, 20, true);
|
|
93
|
-
record.setUint16(8, 0x0800, true);
|
|
94
|
-
record.setUint16(10, method, true);
|
|
95
|
-
record.setUint16(12, DOS_TIME, true);
|
|
96
|
-
record.setUint16(14, DOS_DATE, true);
|
|
97
|
-
record.setUint32(16, sum, true);
|
|
98
|
-
record.setUint32(20, packed.length, true);
|
|
99
|
-
record.setUint32(24, raw.length, true);
|
|
100
|
-
record.setUint16(28, name.length, true);
|
|
101
|
-
record.setUint32(42, offset, true);
|
|
102
|
-
central.push(new Uint8Array(record.buffer), name);
|
|
103
|
-
|
|
133
|
+
const item = { method, sum: crc32(raw), packed, raw, name };
|
|
134
|
+
local.push(localHeader(item), name, packed);
|
|
135
|
+
central.push(centralRecord(item, offset), name);
|
|
104
136
|
offset += 30 + name.length + packed.length;
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
const size = central.reduce((total, chunk) => total + chunk.length, 0);
|
|
108
|
-
|
|
109
|
-
end.setUint32(0, 0x06054b50, true);
|
|
110
|
-
end.setUint16(8, entries.length, true);
|
|
111
|
-
end.setUint16(10, entries.length, true);
|
|
112
|
-
end.setUint32(12, size, true);
|
|
113
|
-
end.setUint32(16, offset, true);
|
|
114
|
-
|
|
115
|
-
const all = [...local, ...central, new Uint8Array(end.buffer)];
|
|
116
|
-
const bytes = new Uint8Array(all.reduce((total, chunk) => total + chunk.length, 0));
|
|
117
|
-
let at = 0;
|
|
118
|
-
for (const chunk of all) {
|
|
119
|
-
bytes.set(chunk, at);
|
|
120
|
-
at += chunk.length;
|
|
121
|
-
}
|
|
122
|
-
return bytes;
|
|
140
|
+
return concat([...local, ...central, endRecord(entries.length, size, offset)]);
|
|
123
141
|
};
|
|
124
142
|
|
|
125
143
|
// ---------------------------------------------------------------- XML ----
|
|
@@ -163,6 +181,70 @@ const esc = (text) =>
|
|
|
163
181
|
/** @param {string} text */
|
|
164
182
|
const textNode = (text) => `<t xml:space="preserve">${esc(text)}</t>`;
|
|
165
183
|
|
|
184
|
+
// ---------------------------------------------------------- validation ----
|
|
185
|
+
|
|
186
|
+
// Absent means the caller said nothing: `undefined` and `null` read the same
|
|
187
|
+
// everywhere on this surface.
|
|
188
|
+
/** @param {unknown} value */
|
|
189
|
+
const absent = (value) => value === undefined || value === null;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {unknown} value
|
|
193
|
+
* @param {string} where
|
|
194
|
+
* @param {string} expected what the message names when the value is not an object
|
|
195
|
+
* @returns {Record<string, unknown>}
|
|
196
|
+
*/
|
|
197
|
+
const requireRecord = (value, where, expected) => {
|
|
198
|
+
if (value === null || typeof value !== "object")
|
|
199
|
+
throw TypeError(`${where}: expected ${expected}, got ${JSON.stringify(value)}`);
|
|
200
|
+
return /** @type {Record<string, unknown>} */ (value);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {unknown} value
|
|
205
|
+
* @param {string} where
|
|
206
|
+
*/
|
|
207
|
+
const requireString = (value, where) => {
|
|
208
|
+
if (typeof value !== "string")
|
|
209
|
+
throw TypeError(`${where}: expected a string, got ${JSON.stringify(value)}`);
|
|
210
|
+
return value;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {unknown} value
|
|
215
|
+
* @param {string} where
|
|
216
|
+
* @param {string} fallback
|
|
217
|
+
*/
|
|
218
|
+
const requireText = (value, where, fallback) =>
|
|
219
|
+
value === undefined ? fallback : requireString(value, where);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {unknown} value
|
|
223
|
+
* @param {string} where
|
|
224
|
+
*/
|
|
225
|
+
const requireNumber = (value, where) => {
|
|
226
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
227
|
+
throw RangeError(`${where}: expected a finite number, got ${JSON.stringify(value)}`);
|
|
228
|
+
return value;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** @param {unknown} value @param {number} limit */
|
|
232
|
+
const isIndex = (value, limit) =>
|
|
233
|
+
typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= limit;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {unknown} value
|
|
237
|
+
* @param {number} limit
|
|
238
|
+
* @param {string} where
|
|
239
|
+
*/
|
|
240
|
+
const requireIndex = (value, limit, where) => {
|
|
241
|
+
if (!isIndex(value, limit))
|
|
242
|
+
throw RangeError(
|
|
243
|
+
`${where}: expected an integer between 1 and ${limit}, got ${JSON.stringify(value)}`,
|
|
244
|
+
);
|
|
245
|
+
return /** @type {number} */ (value);
|
|
246
|
+
};
|
|
247
|
+
|
|
166
248
|
/**
|
|
167
249
|
* One run of rich text, written in full: an OOXML run inherits nothing from
|
|
168
250
|
* the cell font.
|
|
@@ -170,17 +252,12 @@ const textNode = (text) => `<t xml:space="preserve">${esc(text)}</t>`;
|
|
|
170
252
|
* @param {string} at
|
|
171
253
|
*/
|
|
172
254
|
const richRun = (run, at) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const properties =
|
|
180
|
-
font === undefined || font === null
|
|
181
|
-
? ""
|
|
182
|
-
: `<rPr>${runFontXml(/** @type {Record<string, unknown>} */ (font), `${at}.font`)}</rPr>`;
|
|
183
|
-
return `<r>${properties}${textNode(text)}</r>`;
|
|
255
|
+
const { text, font } = requireRecord(run, at, "a { text, font } run");
|
|
256
|
+
const body = textNode(requireString(text, `${at}.text`));
|
|
257
|
+
const properties = absent(font)
|
|
258
|
+
? ""
|
|
259
|
+
: `<rPr>${runFontXml(/** @type {Record<string, unknown>} */ (font), `${at}.font`)}</rPr>`;
|
|
260
|
+
return `<r>${properties}${body}</r>`;
|
|
184
261
|
};
|
|
185
262
|
|
|
186
263
|
/**
|
|
@@ -336,6 +413,15 @@ const table = (initial) => {
|
|
|
336
413
|
|
|
337
414
|
// ------------------------------------------------------------- styles ----
|
|
338
415
|
|
|
416
|
+
// The boolean half of a font, in the order `CT_Font` writes them.
|
|
417
|
+
/** @type {ReadonlyArray<[string, string]>} */
|
|
418
|
+
const FONT_FLAGS = [
|
|
419
|
+
["bold", "<b/>"],
|
|
420
|
+
["italic", "<i/>"],
|
|
421
|
+
["strikethrough", "<strike/>"],
|
|
422
|
+
["underline", "<u/>"],
|
|
423
|
+
];
|
|
424
|
+
|
|
339
425
|
/**
|
|
340
426
|
* The font half of a style, written in the SDK's `CT_Font` sequence:
|
|
341
427
|
* b i strike u sz color name.
|
|
@@ -343,14 +429,14 @@ const table = (initial) => {
|
|
|
343
429
|
* @param {string} where
|
|
344
430
|
*/
|
|
345
431
|
const fontXml = (font, where) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return
|
|
432
|
+
const flags = FONT_FLAGS.filter(([key]) => font[key])
|
|
433
|
+
.map(([, tag]) => tag)
|
|
434
|
+
.join("");
|
|
435
|
+
const size = font.size === undefined ? 11 : requireNumber(font.size, `${where}.size`);
|
|
436
|
+
const colour =
|
|
437
|
+
font.color === undefined ? "" : `<color rgb="${rgb(font.color, `${where}.color`)}"/>`;
|
|
438
|
+
const name = esc(requireText(font.name, `${where}.name`, "Calibri"));
|
|
439
|
+
return `${flags}<sz val="${size}"/>${colour}<name val="${name}"/>`;
|
|
354
440
|
};
|
|
355
441
|
|
|
356
442
|
/**
|
|
@@ -368,93 +454,92 @@ const SIDES = ["left", "right", "top", "bottom"];
|
|
|
368
454
|
* @param {Record<string, unknown>} border
|
|
369
455
|
* @param {string} where
|
|
370
456
|
*/
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const at = `${where}.${side}`;
|
|
380
|
-
const style = /** @type {Record<string, unknown>} */ (edge).style;
|
|
381
|
-
if (typeof style !== "string" || !BORDER_STYLES.has(style))
|
|
382
|
-
throw RangeError(
|
|
383
|
-
`${at}.style: expected thin, dashed or dotted, got ${JSON.stringify(style)}`,
|
|
384
|
-
);
|
|
385
|
-
const colour = /** @type {Record<string, unknown>} */ (edge).color;
|
|
386
|
-
out +=
|
|
387
|
-
colour === undefined
|
|
388
|
-
? `<${side} style="${style}"/>`
|
|
389
|
-
: `<${side} style="${style}"><color rgb="${rgb(colour, `${at}.color`)}"/></${side}>`;
|
|
390
|
-
}
|
|
391
|
-
return out + "<diagonal/>";
|
|
457
|
+
/**
|
|
458
|
+
* @param {unknown} style
|
|
459
|
+
* @param {string} at
|
|
460
|
+
*/
|
|
461
|
+
const edgeStyle = (style, at) => {
|
|
462
|
+
if (typeof style !== "string" || !BORDER_STYLES.has(style))
|
|
463
|
+
throw RangeError(`${at}.style: expected thin, dashed or dotted, got ${JSON.stringify(style)}`);
|
|
464
|
+
return style;
|
|
392
465
|
};
|
|
393
466
|
|
|
394
|
-
|
|
395
|
-
|
|
467
|
+
/**
|
|
468
|
+
* One side of a border. An absent side is written empty rather than skipped,
|
|
469
|
+
* because `CT_Border` lists all four in a fixed order.
|
|
470
|
+
* @param {string} side
|
|
471
|
+
* @param {unknown} edge
|
|
472
|
+
* @param {string} at
|
|
473
|
+
*/
|
|
474
|
+
const sideXml = (side, edge, at) => {
|
|
475
|
+
if (absent(edge)) return `<${side}/>`;
|
|
476
|
+
const style = edgeStyle(/** @type {Record<string, unknown>} */ (edge).style, at);
|
|
477
|
+
const colour = /** @type {Record<string, unknown>} */ (edge).color;
|
|
478
|
+
return colour === undefined
|
|
479
|
+
? `<${side} style="${style}"/>`
|
|
480
|
+
: `<${side} style="${style}"><color rgb="${rgb(colour, `${at}.color`)}"/></${side}>`;
|
|
481
|
+
};
|
|
396
482
|
|
|
397
483
|
/**
|
|
398
|
-
* @param {Record<string, unknown>}
|
|
484
|
+
* @param {Record<string, unknown>} border
|
|
399
485
|
* @param {string} where
|
|
400
486
|
*/
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
throw RangeError(`${where}.horizontal: unknown alignment ${JSON.stringify(horizontal)}`);
|
|
407
|
-
out += ` horizontal="${horizontal}"`;
|
|
408
|
-
}
|
|
409
|
-
if (alignment.vertical !== undefined) {
|
|
410
|
-
const vertical = /** @type {string} */ (alignment.vertical);
|
|
411
|
-
if (!VERTICAL.has(vertical))
|
|
412
|
-
throw RangeError(`${where}.vertical: unknown alignment ${JSON.stringify(vertical)}`);
|
|
413
|
-
out += ` vertical="${vertical}"`;
|
|
414
|
-
}
|
|
415
|
-
if (alignment.wrapText) out += ' wrapText="1"';
|
|
416
|
-
return out;
|
|
417
|
-
};
|
|
487
|
+
const borderXml = (border, where) =>
|
|
488
|
+
SIDES.map((side) => sideXml(side, border[side], `${where}.${side}`)).join("") + "<diagonal/>";
|
|
489
|
+
|
|
490
|
+
const HORIZONTAL = new Set(["left", "center", "right", "justify", "fill"]);
|
|
491
|
+
const VERTICAL = new Set(["top", "center", "bottom", "justify"]);
|
|
418
492
|
|
|
419
493
|
/**
|
|
494
|
+
* One alignment axis as its attribute, or nothing when the caller left it out.
|
|
420
495
|
* @param {unknown} value
|
|
496
|
+
* @param {ReadonlySet<string>} known
|
|
497
|
+
* @param {string} axis
|
|
421
498
|
* @param {string} where
|
|
422
|
-
* @param {string} fallback
|
|
423
499
|
*/
|
|
424
|
-
const
|
|
425
|
-
if (value === undefined) return
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
500
|
+
const axisXml = (value, known, axis, where) => {
|
|
501
|
+
if (value === undefined) return "";
|
|
502
|
+
const named = /** @type {string} */ (value);
|
|
503
|
+
if (!known.has(named))
|
|
504
|
+
throw RangeError(`${where}.${axis}: unknown alignment ${JSON.stringify(named)}`);
|
|
505
|
+
return ` ${axis}="${named}"`;
|
|
429
506
|
};
|
|
430
507
|
|
|
431
508
|
/**
|
|
432
|
-
* @param {unknown}
|
|
509
|
+
* @param {Record<string, unknown>} alignment
|
|
433
510
|
* @param {string} where
|
|
434
511
|
*/
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
};
|
|
512
|
+
const alignmentXml = (alignment, where) =>
|
|
513
|
+
axisXml(alignment.horizontal, HORIZONTAL, "horizontal", where) +
|
|
514
|
+
axisXml(alignment.vertical, VERTICAL, "vertical", where) +
|
|
515
|
+
(alignment.wrapText ? ' wrapText="1"' : "");
|
|
440
516
|
|
|
441
517
|
/**
|
|
442
518
|
* @typedef {{ numFmtId: number, fontId: number, fillId: number, borderId: number, alignment: string }} Xf
|
|
443
519
|
*/
|
|
444
520
|
|
|
521
|
+
// Which `apply*` flag each table id switches on, in the order `CT_Xf` lists
|
|
522
|
+
// them. Alignment is not a table, so it is written apart.
|
|
523
|
+
/** @type {ReadonlyArray<["numFmtId" | "fontId" | "fillId" | "borderId", string]>} */
|
|
524
|
+
const APPLIES = [
|
|
525
|
+
["numFmtId", "applyNumberFormat"],
|
|
526
|
+
["fontId", "applyFont"],
|
|
527
|
+
["fillId", "applyFill"],
|
|
528
|
+
["borderId", "applyBorder"],
|
|
529
|
+
];
|
|
530
|
+
|
|
445
531
|
/**
|
|
446
532
|
* One `xf`. Each `apply*` flag says which table this format actually reaches
|
|
447
533
|
* into; a reader ignores a table entry the flag does not claim.
|
|
448
534
|
* @param {Xf} xf
|
|
449
535
|
*/
|
|
450
536
|
const xfXml = (xf) => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (xf.
|
|
456
|
-
|
|
457
|
-
return xf.alignment === "" ? out + "/>" : `${out}><alignment${xf.alignment}/></xf>`;
|
|
537
|
+
const ids = `numFmtId="${xf.numFmtId}" fontId="${xf.fontId}" fillId="${xf.fillId}" borderId="${xf.borderId}"`;
|
|
538
|
+
const applies = APPLIES.filter(([key]) => xf[key] !== 0)
|
|
539
|
+
.map(([, flag]) => ` ${flag}="1"`)
|
|
540
|
+
.join("");
|
|
541
|
+
if (xf.alignment === "") return `<xf ${ids} xfId="0"${applies}/>`;
|
|
542
|
+
return `<xf ${ids} xfId="0"${applies} applyAlignment="1"><alignment${xf.alignment}/></xf>`;
|
|
458
543
|
};
|
|
459
544
|
|
|
460
545
|
/**
|
|
@@ -483,11 +568,14 @@ const stylesheet = () => {
|
|
|
483
568
|
* @param {(value: Record<string, unknown>) => [string, () => T]} describe
|
|
484
569
|
*/
|
|
485
570
|
const optional = (part, into, describe) => {
|
|
486
|
-
if (part
|
|
571
|
+
if (absent(part)) return 0;
|
|
487
572
|
const [key, make] = describe(/** @type {Record<string, unknown>} */ (part));
|
|
488
573
|
return into.intern(key, make);
|
|
489
574
|
};
|
|
490
575
|
|
|
576
|
+
/** @param {string} code */
|
|
577
|
+
const knownFormat = (code) => BUILT_IN.get(code) ?? numFmts.get(code);
|
|
578
|
+
|
|
491
579
|
/**
|
|
492
580
|
* A format code a reader knows by id keeps that id; anything else is
|
|
493
581
|
* interned from 164 up, in first-seen order.
|
|
@@ -495,12 +583,10 @@ const stylesheet = () => {
|
|
|
495
583
|
* @param {string} where
|
|
496
584
|
*/
|
|
497
585
|
const formatId = (code, where) => {
|
|
498
|
-
if (code
|
|
586
|
+
if (absent(code)) return 0;
|
|
499
587
|
if (typeof code !== "string")
|
|
500
588
|
throw TypeError(`${where}: expected a format code, got ${JSON.stringify(code)}`);
|
|
501
|
-
const
|
|
502
|
-
if (builtIn !== undefined) return builtIn;
|
|
503
|
-
const seen = numFmts.get(code);
|
|
589
|
+
const seen = knownFormat(code);
|
|
504
590
|
if (seen !== undefined) return seen;
|
|
505
591
|
const id = FIRST_CUSTOM + numFmts.size;
|
|
506
592
|
numFmts.set(code, id);
|
|
@@ -514,7 +600,7 @@ const stylesheet = () => {
|
|
|
514
600
|
* @returns {number}
|
|
515
601
|
*/
|
|
516
602
|
intern(style, where) {
|
|
517
|
-
if (style
|
|
603
|
+
if (absent(style)) return 0;
|
|
518
604
|
if (typeof style !== "object")
|
|
519
605
|
throw TypeError(`${where}: expected a style object, got ${JSON.stringify(style)}`);
|
|
520
606
|
|
|
@@ -536,13 +622,12 @@ const stylesheet = () => {
|
|
|
536
622
|
JSON.stringify(border),
|
|
537
623
|
() => `<border>${borderXml(border, `${where}.border`)}</border>`,
|
|
538
624
|
]),
|
|
539
|
-
alignment:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
),
|
|
625
|
+
alignment: absent(style.alignment)
|
|
626
|
+
? ""
|
|
627
|
+
: alignmentXml(
|
|
628
|
+
/** @type {Record<string, unknown>} */ (style.alignment),
|
|
629
|
+
`${where}.alignment`,
|
|
630
|
+
),
|
|
546
631
|
};
|
|
547
632
|
return formats.intern(JSON.stringify(record), () => record);
|
|
548
633
|
},
|
|
@@ -622,24 +707,33 @@ const FURNITURE = 0.3;
|
|
|
622
707
|
* two this surface does not take keep Excel's own gap for a header and a
|
|
623
708
|
* footer.
|
|
624
709
|
*
|
|
625
|
-
* @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean } | null} setup
|
|
710
|
+
* @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean, titles?: number } | null} setup
|
|
626
711
|
*/
|
|
627
712
|
const printXml = (setup) => {
|
|
628
713
|
if (setup === null) return "";
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
714
|
+
const attributes = setupAttributes(setup);
|
|
715
|
+
return marginsXml(setup.margin) + (attributes === "" ? "" : `<pageSetup${attributes}/>`);
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
/** @param {number | undefined} margin in points */
|
|
719
|
+
const marginsXml = (margin) => {
|
|
720
|
+
if (margin === undefined) return "";
|
|
721
|
+
const inches = (margin / POINTS_PER_INCH).toFixed(3);
|
|
722
|
+
return (
|
|
723
|
+
`<pageMargins left="${inches}" right="${inches}" top="${inches}" bottom="${inches}"` +
|
|
724
|
+
` header="${FURNITURE}" footer="${FURNITURE}"/>`
|
|
725
|
+
);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
/** @param {{ size?: string, orientation?: string, fit?: boolean }} setup */
|
|
729
|
+
const setupAttributes = (setup) => {
|
|
636
730
|
let attributes = "";
|
|
637
731
|
if (setup.size !== undefined) attributes += ` paperSize="${PAPER.get(setup.size)}"`;
|
|
638
732
|
if (setup.orientation !== undefined) attributes += ` orientation="${setup.orientation}"`;
|
|
639
733
|
// `fitToHeight="0"` is what makes it *width* the document fits to: one page
|
|
640
734
|
// across, as many down as it takes.
|
|
641
735
|
if (setup.fit === true) attributes += ' fitToWidth="1" fitToHeight="0"';
|
|
642
|
-
return attributes
|
|
736
|
+
return attributes;
|
|
643
737
|
};
|
|
644
738
|
|
|
645
739
|
// -------------------------------------------------------------- sheet ----
|
|
@@ -652,21 +746,170 @@ const NAME_LIMIT = 31;
|
|
|
652
746
|
const MAX_ROW = 1_048_576;
|
|
653
747
|
const MAX_COLUMN = 16_384;
|
|
654
748
|
|
|
749
|
+
// EMU per CSS pixel at 96 dpi.
|
|
750
|
+
const EMU = 9525;
|
|
751
|
+
|
|
655
752
|
/**
|
|
753
|
+
* A `Date` as the serial Excel stores. An invalid one has no serial to give.
|
|
754
|
+
* @param {Date} date
|
|
755
|
+
* @param {string} at
|
|
756
|
+
*/
|
|
757
|
+
const dateSerial = (date, at) => {
|
|
758
|
+
if (Number.isNaN(date.getTime()))
|
|
759
|
+
throw RangeError(`${at}: expected a valid Date, got an invalid one`);
|
|
760
|
+
return serial(date);
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The shared-string index of a cell's text, plain or rich.
|
|
765
|
+
* @param {ReturnType<typeof strings>} sst
|
|
766
|
+
* @param {unknown} value
|
|
767
|
+
* @param {string} at
|
|
768
|
+
*/
|
|
769
|
+
const textIndex = (sst, value, at) => {
|
|
770
|
+
if (typeof value === "string")
|
|
771
|
+
return sst.intern("s" + value, () => `<si>${textNode(value)}</si>`);
|
|
772
|
+
if (Array.isArray(value)) return sst.intern(richKey(value, at), () => richXml(value, at));
|
|
773
|
+
throw TypeError(`${at}: unsupported cell value ${JSON.stringify(value)}`);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* The type attribute and `<v>` of a present cell value.
|
|
778
|
+
* @param {ReturnType<typeof strings>} sst
|
|
656
779
|
* @param {unknown} value
|
|
657
|
-
* @param {number} limit
|
|
658
780
|
* @param {string} where
|
|
781
|
+
* @returns {[string, string | number]}
|
|
659
782
|
*/
|
|
660
|
-
const
|
|
661
|
-
|
|
783
|
+
const cellBody = (sst, value, where) => {
|
|
784
|
+
const at = `${where}.value`;
|
|
785
|
+
if (typeof value === "number") return ["", number(requireNumber(value, at))];
|
|
786
|
+
if (typeof value === "boolean") return [' t="b"', Number(value)];
|
|
787
|
+
if (value instanceof Date) return ["", number(dateSerial(value, at))];
|
|
788
|
+
return [' t="s"', textIndex(sst, value, at)];
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* @param {ReturnType<typeof strings>} sst
|
|
793
|
+
* @param {unknown} value
|
|
794
|
+
* @param {number} styleId
|
|
795
|
+
* @param {string} ref
|
|
796
|
+
* @param {string} where
|
|
797
|
+
*/
|
|
798
|
+
const cellXml = (sst, value, styleId, ref, where) => {
|
|
799
|
+
const s = styleId === 0 ? "" : ` s="${styleId}"`;
|
|
800
|
+
if (absent(value)) return s === "" ? "" : `<c r="${ref}"${s}/>`;
|
|
801
|
+
const [type, v] = cellBody(sst, value, where);
|
|
802
|
+
return `<c r="${ref}"${s}${type}><v>${v}</v></c>`;
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// A style that is not an object passes through untouched, so the interning
|
|
806
|
+
// below is still the one place that refuses it.
|
|
807
|
+
/** @param {unknown} declared */
|
|
808
|
+
const usable = (declared) => absent(declared) || typeof declared === "object";
|
|
809
|
+
|
|
810
|
+
// A declared style that names no number format of its own.
|
|
811
|
+
/** @param {Record<string, unknown> | null | undefined} declared */
|
|
812
|
+
const undated = (declared) => usable(declared) && absent(declared?.numberFormat);
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* A date with no format of its own gets the short-date built-in, so a `Date`
|
|
816
|
+
* reads back as a day rather than as the number underneath it.
|
|
817
|
+
* @param {unknown} value
|
|
818
|
+
* @param {Record<string, unknown> | null | undefined} declared
|
|
819
|
+
*/
|
|
820
|
+
const cellStyle = (value, declared) =>
|
|
821
|
+
value instanceof Date && undated(declared) ? { ...declared, numberFormat: SHORT_DATE } : declared;
|
|
822
|
+
|
|
823
|
+
/** @param {number} frozen */
|
|
824
|
+
const paneXml = (frozen) =>
|
|
825
|
+
frozen === 0
|
|
826
|
+
? '<sheetView workbookViewId="0"/>'
|
|
827
|
+
: `<sheetView workbookViewId="0"><pane ySplit="${frozen}" topLeftCell="A${frozen + 1}" activePane="bottomLeft" state="frozen"/><selection pane="bottomLeft" activeCell="A${frozen + 1}" sqref="A${frozen + 1}"/></sheetView>`;
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* @typedef {{ top: number, left: number, bottom: number, right: number }} Range
|
|
831
|
+
*/
|
|
832
|
+
|
|
833
|
+
/** @param {Range} range */
|
|
834
|
+
const rangeRef = (range) =>
|
|
835
|
+
`${letters(range.left)}${range.top}:${letters(range.right)}${range.bottom}`;
|
|
836
|
+
|
|
837
|
+
/** @param {Range} a @param {Range} b */
|
|
838
|
+
const overlaps = (a, b) =>
|
|
839
|
+
a.top <= b.bottom && a.bottom >= b.top && a.left <= b.right && a.right >= b.left;
|
|
840
|
+
|
|
841
|
+
/** @param {ReadonlyArray<Range>} merges */
|
|
842
|
+
const mergesXml = (merges) =>
|
|
843
|
+
merges.length === 0
|
|
844
|
+
? ""
|
|
845
|
+
: `<mergeCells count="${merges.length}">` +
|
|
846
|
+
merges.map((range) => `<mergeCell ref="${rangeRef(range)}"/>`).join("") +
|
|
847
|
+
`</mergeCells>`;
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* The last column a merge reaches, once its width is known to be a count of
|
|
851
|
+
* two or more that stays on the sheet.
|
|
852
|
+
* @param {unknown} width
|
|
853
|
+
* @param {number} at
|
|
854
|
+
*/
|
|
855
|
+
const mergeEnd = (width, at) => {
|
|
856
|
+
if (!Number.isInteger(width) || /** @type {number} */ (width) < 2)
|
|
857
|
+
throw RangeError(`merge: expected a width of 2 or more, got ${JSON.stringify(width)}`);
|
|
858
|
+
const right = at + /** @type {number} */ (width) - 1;
|
|
859
|
+
if (right > MAX_COLUMN) throw RangeError(`merge: the range ends past column ${MAX_COLUMN}`);
|
|
860
|
+
return right;
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
/** @param {unknown} margin */
|
|
864
|
+
const requireMargin = (margin) => {
|
|
865
|
+
const points = requireNumber(margin, "print: margin");
|
|
866
|
+
if (points < 0) throw RangeError(`print: margin cannot be negative, got ${points}`);
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
/** @param {unknown} size */
|
|
870
|
+
const requirePaper = (size) => {
|
|
871
|
+
if (!PAPER.has(/** @type {string} */ (size)))
|
|
662
872
|
throw RangeError(
|
|
663
|
-
|
|
873
|
+
`print: unknown paper size ${JSON.stringify(size)} -- known: ${[...PAPER.keys()].join(", ")}`,
|
|
664
874
|
);
|
|
665
|
-
return value;
|
|
666
875
|
};
|
|
667
876
|
|
|
668
|
-
//
|
|
669
|
-
|
|
877
|
+
// Zero clears, exactly as it does for a freeze, so a caller that set a count
|
|
878
|
+
// has a way back from it. That is why this does not reach for `requireIndex`:
|
|
879
|
+
// its message names 1 as the floor, and here the floor is 0.
|
|
880
|
+
/** @param {unknown} rows */
|
|
881
|
+
const requireTitles = (rows) => {
|
|
882
|
+
if (rows === 0) return;
|
|
883
|
+
if (!isIndex(rows, MAX_ROW))
|
|
884
|
+
throw RangeError(
|
|
885
|
+
`print: titles expected 0, or a row count between 1 and ${MAX_ROW}, got ${JSON.stringify(rows)}`,
|
|
886
|
+
);
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
/** @param {unknown} orientation */
|
|
890
|
+
const requireOrientation = (orientation) => {
|
|
891
|
+
if (!ORIENTATION.has(/** @type {string} */ (orientation)))
|
|
892
|
+
throw RangeError(`print: expected portrait or landscape, got ${JSON.stringify(orientation)}`);
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
/** @param {number} width @param {number} height */
|
|
896
|
+
const requireSize = (width, height) => {
|
|
897
|
+
if (width <= 0 || height <= 0)
|
|
898
|
+
throw RangeError(`place: expected a positive size, got ${width} by ${height}`);
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// Every print key this surface checks, and the check it gets. The order is the
|
|
902
|
+
// order a caller hears about a mistake, so it is this object's key order and
|
|
903
|
+
// not a detail. A table rather than a run of guards because a guard apiece
|
|
904
|
+
// puts `print` over the cyclomatic ceiling this package holds every function
|
|
905
|
+
// to; the table keeps the branch count flat as keys are added. `fit` is absent
|
|
906
|
+
// because only `true` writes anything, so no value of it is a mistake.
|
|
907
|
+
const PRINT_CHECKS = {
|
|
908
|
+
margin: requireMargin,
|
|
909
|
+
size: requirePaper,
|
|
910
|
+
orientation: requireOrientation,
|
|
911
|
+
titles: requireTitles,
|
|
912
|
+
};
|
|
670
913
|
|
|
671
914
|
/**
|
|
672
915
|
* One worksheet. Rows accumulate in call order; everything else is bookkeeping
|
|
@@ -688,46 +931,24 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
688
931
|
let widest = 1;
|
|
689
932
|
// What `print` was told, or null. Nothing reaches the file until a caller
|
|
690
933
|
// asks: a reader's own print defaults are better than this writer guessing.
|
|
691
|
-
/** @type {{ margin?: number, size?: string, orientation?: string, fit?: boolean } | null} */
|
|
934
|
+
/** @type {{ margin?: number, size?: string, orientation?: string, fit?: boolean, titles?: number } | null} */
|
|
692
935
|
let printing = null;
|
|
693
936
|
|
|
694
937
|
/**
|
|
695
|
-
*
|
|
696
|
-
* @param {
|
|
697
|
-
* @param {
|
|
698
|
-
* @param {
|
|
938
|
+
* One cell of a row: the empty string for a gap, else its `<c>`.
|
|
939
|
+
* @param {unknown} cell
|
|
940
|
+
* @param {number} index 0-based column
|
|
941
|
+
* @param {number} at 1-based row
|
|
699
942
|
*/
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (typeof value === "boolean") return `<c r="${ref}"${s} t="b"><v>${value ? 1 : 0}</v></c>`;
|
|
711
|
-
|
|
712
|
-
if (value instanceof Date) {
|
|
713
|
-
if (Number.isNaN(value.getTime()))
|
|
714
|
-
throw RangeError(`${where}.value: expected a valid Date, got an invalid one`);
|
|
715
|
-
return `<c r="${ref}"${s}><v>${number(serial(value))}</v></c>`;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (typeof value === "string") {
|
|
719
|
-
const at = sst.intern("s" + value, () => `<si>${textNode(value)}</si>`);
|
|
720
|
-
return `<c r="${ref}"${s} t="s"><v>${at}</v></c>`;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (Array.isArray(value)) {
|
|
724
|
-
const at = sst.intern(richKey(value, `${where}.value`), () =>
|
|
725
|
-
richXml(value, `${where}.value`),
|
|
726
|
-
);
|
|
727
|
-
return `<c r="${ref}"${s} t="s"><v>${at}</v></c>`;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
throw TypeError(`${where}.value: unsupported cell value ${JSON.stringify(value)}`);
|
|
943
|
+
const cellAt = (cell, index, at) => {
|
|
944
|
+
if (absent(cell)) return "";
|
|
945
|
+
if (typeof cell !== "object")
|
|
946
|
+
throw TypeError(`row: cell ${index + 1} is not a { value, style } object`);
|
|
947
|
+
const where = `row ${at}, cell ${index + 1}`;
|
|
948
|
+
const { value, style } = /** @type {Record<string, unknown>} */ (cell);
|
|
949
|
+
const declared = /** @type {Record<string, unknown> | null | undefined} */ (style);
|
|
950
|
+
const styleId = styles.intern(cellStyle(value, declared), `${where}.style`);
|
|
951
|
+
return cellXml(sst, value, styleId, letters(index + 1) + at, where);
|
|
731
952
|
};
|
|
732
953
|
|
|
733
954
|
return {
|
|
@@ -742,35 +963,8 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
742
963
|
throw RangeError(`row: a sheet holds at most ${MAX_COLUMN} columns`);
|
|
743
964
|
|
|
744
965
|
let body = "";
|
|
745
|
-
for (let index = 0; index < cells.length; index++)
|
|
746
|
-
|
|
747
|
-
if (cell === undefined || cell === null) continue;
|
|
748
|
-
if (typeof cell !== "object")
|
|
749
|
-
throw TypeError(`row: cell ${index + 1} is not a { value, style } object`);
|
|
750
|
-
const where = `row ${at}, cell ${index + 1}`;
|
|
751
|
-
const declared = /** @type {Record<string, unknown> | null | undefined} */ (
|
|
752
|
-
/** @type {Record<string, unknown>} */ (cell).style
|
|
753
|
-
);
|
|
754
|
-
// A date with no format of its own gets the short-date built-in, so a
|
|
755
|
-
// `Date` reads back as a day rather than as the number underneath it.
|
|
756
|
-
// A style that is not an object passes through untouched, so the
|
|
757
|
-
// interning below is still the one place that refuses it.
|
|
758
|
-
const usable = declared === null || declared === undefined || typeof declared === "object";
|
|
759
|
-
const style =
|
|
760
|
-
/** @type {Record<string, unknown>} */ (cell).value instanceof Date &&
|
|
761
|
-
usable &&
|
|
762
|
-
(declared?.numberFormat ?? null) === null
|
|
763
|
-
? { ...declared, numberFormat: SHORT_DATE }
|
|
764
|
-
: /** @type {Record<string, unknown> | null | undefined} */ (declared);
|
|
765
|
-
const styleId = styles.intern(style, `${where}.style`);
|
|
766
|
-
body += cellXml(
|
|
767
|
-
/** @type {Record<string, unknown>} */ (cell).value,
|
|
768
|
-
styleId,
|
|
769
|
-
letters(index + 1) + at,
|
|
770
|
-
where,
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
if (cells.length > widest) widest = cells.length;
|
|
966
|
+
for (let index = 0; index < cells.length; index++) body += cellAt(cells[index], index, at);
|
|
967
|
+
widest = Math.max(widest, cells.length);
|
|
774
968
|
rows.push(`<row r="${at}">${body}</row>`);
|
|
775
969
|
return at;
|
|
776
970
|
},
|
|
@@ -785,18 +979,12 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
785
979
|
requireIndex(at, MAX_COLUMN, "merge: at");
|
|
786
980
|
if (row > rows.length)
|
|
787
981
|
throw RangeError(`merge: row ${row} does not exist yet (the sheet has ${rows.length})`);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
throw RangeError(
|
|
795
|
-
`merge: the range ${letters(at)}${row}:${letters(right)}${row} overlaps ` +
|
|
796
|
-
`${letters(other.left)}${other.top}:${letters(other.right)}${other.bottom}`,
|
|
797
|
-
);
|
|
798
|
-
merges.push({ top: row, left: at, bottom: row, right });
|
|
799
|
-
if (right > widest) widest = right;
|
|
982
|
+
const range = { top: row, left: at, bottom: row, right: mergeEnd(width, at) };
|
|
983
|
+
const clash = merges.find((other) => overlaps(range, other));
|
|
984
|
+
if (clash !== undefined)
|
|
985
|
+
throw RangeError(`merge: the range ${rangeRef(range)} overlaps ${rangeRef(clash)}`);
|
|
986
|
+
merges.push(range);
|
|
987
|
+
widest = Math.max(widest, range.right);
|
|
800
988
|
},
|
|
801
989
|
|
|
802
990
|
/**
|
|
@@ -805,27 +993,24 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
805
993
|
* are better than a guess, and writing one would put this writer's opinion
|
|
806
994
|
* in every file.
|
|
807
995
|
*
|
|
808
|
-
* @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean }} setup
|
|
996
|
+
* @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean, titles?: number }} setup
|
|
809
997
|
*/
|
|
810
998
|
print(setup) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
throw RangeError(`print: margin cannot be negative, got ${setup.margin}`);
|
|
815
|
-
if (setup.size !== undefined && !PAPER.has(setup.size))
|
|
816
|
-
throw RangeError(
|
|
817
|
-
`print: unknown paper size ${JSON.stringify(setup.size)} -- known: ${[...PAPER.keys()].join(", ")}`,
|
|
818
|
-
);
|
|
819
|
-
if (setup.orientation !== undefined && !ORIENTATION.has(setup.orientation))
|
|
820
|
-
throw RangeError(
|
|
821
|
-
`print: expected portrait or landscape, got ${JSON.stringify(setup.orientation)}`,
|
|
822
|
-
);
|
|
999
|
+
const declared = requireRecord(setup, "print", "a setup object");
|
|
1000
|
+
for (const [key, check] of Object.entries(PRINT_CHECKS))
|
|
1001
|
+
if (declared[key] !== undefined) check(declared[key]);
|
|
823
1002
|
printing = { ...printing, ...setup };
|
|
824
1003
|
},
|
|
825
1004
|
|
|
1005
|
+
// Read by the workbook part rather than by this sheet's own: a print title
|
|
1006
|
+
// is a defined name, and defined names live in `xl/workbook.xml`.
|
|
1007
|
+
get titles() {
|
|
1008
|
+
return printing?.titles ?? 0;
|
|
1009
|
+
},
|
|
1010
|
+
|
|
826
1011
|
/** @param {number} count */
|
|
827
1012
|
freeze(count) {
|
|
828
|
-
if (
|
|
1013
|
+
if (!Number.isInteger(count) || count < 0 || count >= MAX_ROW)
|
|
829
1014
|
throw RangeError(`freeze: expected a row count of 0 or more, got ${JSON.stringify(count)}`);
|
|
830
1015
|
frozen = count;
|
|
831
1016
|
},
|
|
@@ -836,14 +1021,12 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
836
1021
|
*/
|
|
837
1022
|
place(id, at) {
|
|
838
1023
|
if (!knows(id)) throw TypeError(`place: no image with id ${JSON.stringify(id)}`);
|
|
839
|
-
|
|
840
|
-
throw TypeError(`place: expected { row, col, width, height }, got ${JSON.stringify(at)}`);
|
|
1024
|
+
requireRecord(at, "place", "{ row, col, width, height }");
|
|
841
1025
|
const row = requireIndex(at.row, MAX_ROW, "place: row");
|
|
842
1026
|
const col = requireIndex(at.col === undefined ? 1 : at.col, MAX_COLUMN, "place: col");
|
|
843
1027
|
const width = requireNumber(at.width, "place: width");
|
|
844
1028
|
const height = requireNumber(at.height, "place: height");
|
|
845
|
-
|
|
846
|
-
throw RangeError(`place: expected a positive size, got ${width} by ${height}`);
|
|
1029
|
+
requireSize(width, height);
|
|
847
1030
|
pictures.push({ id: /** @type {number} */ (id), row, col, width, height });
|
|
848
1031
|
},
|
|
849
1032
|
|
|
@@ -854,21 +1037,6 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
854
1037
|
/** @param {number | null} drawing the relationship id of this sheet's drawing part */
|
|
855
1038
|
xml(drawing) {
|
|
856
1039
|
const dimension = `A1:${letters(widest)}${Math.max(rows.length, 1)}`;
|
|
857
|
-
const pane =
|
|
858
|
-
frozen === 0
|
|
859
|
-
? '<sheetView workbookViewId="0"/>'
|
|
860
|
-
: `<sheetView workbookViewId="0"><pane ySplit="${frozen}" topLeftCell="A${frozen + 1}" activePane="bottomLeft" state="frozen"/><selection pane="bottomLeft" activeCell="A${frozen + 1}" sqref="A${frozen + 1}"/></sheetView>`;
|
|
861
|
-
const merged =
|
|
862
|
-
merges.length === 0
|
|
863
|
-
? ""
|
|
864
|
-
: `<mergeCells count="${merges.length}">` +
|
|
865
|
-
merges
|
|
866
|
-
.map(
|
|
867
|
-
(range) =>
|
|
868
|
-
`<mergeCell ref="${letters(range.left)}${range.top}:${letters(range.right)}${range.bottom}"/>`,
|
|
869
|
-
)
|
|
870
|
-
.join("") +
|
|
871
|
-
`</mergeCells>`;
|
|
872
1040
|
|
|
873
1041
|
// `fitToPage` lives on `sheetPr`, which the schema puts before every
|
|
874
1042
|
// other child of a worksheet.
|
|
@@ -880,9 +1048,9 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
880
1048
|
`<worksheet xmlns="${NS}" xmlns:r="${NS_R}">` +
|
|
881
1049
|
properties +
|
|
882
1050
|
`<dimension ref="${dimension}"/>` +
|
|
883
|
-
`<sheetViews>${
|
|
1051
|
+
`<sheetViews>${paneXml(frozen)}</sheetViews>` +
|
|
884
1052
|
`<sheetData>${rows.join("")}</sheetData>` +
|
|
885
|
-
|
|
1053
|
+
mergesXml(merges) +
|
|
886
1054
|
printXml(printing) +
|
|
887
1055
|
(drawing === null ? "" : `<drawing r:id="rId${drawing}"/>`) +
|
|
888
1056
|
`</worksheet>`
|
|
@@ -935,26 +1103,215 @@ const FORMATS = new Map([
|
|
|
935
1103
|
// "the same input" as "the same input at the same wall time".
|
|
936
1104
|
const CREATED = "1970-01-01T00:00:00Z";
|
|
937
1105
|
|
|
938
|
-
/** @param {Uint8Array} a @param {Uint8Array} b */
|
|
939
|
-
const
|
|
940
|
-
// The same array twice is the common case, and the one the byte loop is
|
|
941
|
-
// worst at: equal bytes never exit early, so it reads the whole image every
|
|
942
|
-
// time. A caller placing one logo on a thousand rows pays 54 ms for that,
|
|
943
|
-
// and nothing for this.
|
|
944
|
-
if (a === b) return true;
|
|
945
|
-
if (a.length !== b.length) return false;
|
|
1106
|
+
/** @param {Uint8Array} a @param {Uint8Array} b of the same length */
|
|
1107
|
+
const sameBytes = (a, b) => {
|
|
946
1108
|
for (let at = 0; at < a.length; at++) if (a[at] !== b[at]) return false;
|
|
947
1109
|
return true;
|
|
948
1110
|
};
|
|
949
1111
|
|
|
1112
|
+
// The same array twice is the common case, and the one the byte loop is worst
|
|
1113
|
+
// at: equal bytes never exit early, so it reads the whole image every time. A
|
|
1114
|
+
// caller placing one logo on a thousand rows pays 54 ms for that, and nothing
|
|
1115
|
+
// for the identity check.
|
|
1116
|
+
/** @param {Uint8Array} a @param {Uint8Array} b */
|
|
1117
|
+
const same = (a, b) => a === b || (a.length === b.length && sameBytes(a, b));
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* @param {unknown} name
|
|
1121
|
+
* @returns {string}
|
|
1122
|
+
*/
|
|
1123
|
+
const requireSheetName = (name) => {
|
|
1124
|
+
if (typeof name !== "string" || name === "")
|
|
1125
|
+
throw RangeError(`sheet: expected a name, got ${JSON.stringify(name)}`);
|
|
1126
|
+
if (name.length > NAME_LIMIT)
|
|
1127
|
+
throw RangeError(`sheet: a name is at most ${NAME_LIMIT} characters, got ${name.length}`);
|
|
1128
|
+
return name;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* @param {number} sheets how many worksheets the workbook holds
|
|
1133
|
+
* @param {number} drawings how many of them carry a drawing part
|
|
1134
|
+
* @param {ReadonlyArray<{ format: string }>} media
|
|
1135
|
+
*/
|
|
1136
|
+
const contentTypesXml = (sheets, drawings, media) => {
|
|
1137
|
+
const overrides = Array.from(
|
|
1138
|
+
{ length: sheets },
|
|
1139
|
+
(_sheet, index) =>
|
|
1140
|
+
`<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="${CT}.spreadsheetml.worksheet+xml"/>`,
|
|
1141
|
+
).join("");
|
|
1142
|
+
const drawingOverrides = Array.from(
|
|
1143
|
+
{ length: drawings },
|
|
1144
|
+
(_each, at) =>
|
|
1145
|
+
`<Override PartName="/xl/drawings/drawing${at + 1}.xml" ContentType="${CT}.drawing+xml"/>`,
|
|
1146
|
+
).join("");
|
|
1147
|
+
const defaults = [...new Set(media.map((each) => each.format))]
|
|
1148
|
+
.map((format) => `<Default Extension="${format}" ContentType="${FORMATS.get(format)}"/>`)
|
|
1149
|
+
.join("");
|
|
1150
|
+
|
|
1151
|
+
return (
|
|
1152
|
+
DECLARATION +
|
|
1153
|
+
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
|
1154
|
+
`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
|
|
1155
|
+
`<Default Extension="xml" ContentType="application/xml"/>` +
|
|
1156
|
+
defaults +
|
|
1157
|
+
`<Override PartName="/xl/workbook.xml" ContentType="${CT}.spreadsheetml.sheet.main+xml"/>` +
|
|
1158
|
+
overrides +
|
|
1159
|
+
`<Override PartName="/xl/styles.xml" ContentType="${CT}.spreadsheetml.styles+xml"/>` +
|
|
1160
|
+
`<Override PartName="/xl/sharedStrings.xml" ContentType="${CT}.spreadsheetml.sharedStrings+xml"/>` +
|
|
1161
|
+
drawingOverrides +
|
|
1162
|
+
`<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>` +
|
|
1163
|
+
`<Override PartName="/docProps/app.xml" ContentType="${CT}.extended-properties+xml"/>` +
|
|
1164
|
+
`</Types>`
|
|
1165
|
+
);
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const ROOT_RELS =
|
|
1169
|
+
DECLARATION +
|
|
1170
|
+
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1171
|
+
`<Relationship Id="rId1" Type="${REL}/officeDocument" Target="xl/workbook.xml"/>` +
|
|
1172
|
+
`<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>` +
|
|
1173
|
+
`<Relationship Id="rId3" Type="${REL}/extended-properties" Target="docProps/app.xml"/>` +
|
|
1174
|
+
`</Relationships>`;
|
|
1175
|
+
|
|
1176
|
+
const APP_XML =
|
|
1177
|
+
DECLARATION +
|
|
1178
|
+
`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>werkmap</Application></Properties>`;
|
|
1179
|
+
|
|
1180
|
+
/** @param {string} name @param {string | undefined} value */
|
|
1181
|
+
const tag = (name, value) => (value === undefined ? "" : `<${name}>${esc(value)}</${name}>`);
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* @param {{ title?: string, creator?: string, subject?: string, description?: string }} meta
|
|
1185
|
+
*/
|
|
1186
|
+
const coreXml = (meta) =>
|
|
1187
|
+
DECLARATION +
|
|
1188
|
+
`<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">` +
|
|
1189
|
+
tag("dc:title", meta.title) +
|
|
1190
|
+
tag("dc:subject", meta.subject) +
|
|
1191
|
+
tag("dc:creator", meta.creator) +
|
|
1192
|
+
tag("dc:description", meta.description) +
|
|
1193
|
+
`<dcterms:created xsi:type="dcterms:W3CDTF">${CREATED}</dcterms:created>` +
|
|
1194
|
+
`<dcterms:modified xsi:type="dcterms:W3CDTF">${CREATED}</dcterms:modified>` +
|
|
1195
|
+
`</cp:coreProperties>`;
|
|
1196
|
+
|
|
1197
|
+
// A sheet reference inside a defined name is always quoted, so a name holding
|
|
1198
|
+
// a space or a symbol needs no special case. An apostrophe in the name closes
|
|
1199
|
+
// the quote early unless it doubles, and the whole reference is XML-escaped
|
|
1200
|
+
// after that like every other name this writer emits.
|
|
1201
|
+
/** @param {string} name @param {number} rows */
|
|
1202
|
+
const titlesRef = (name, rows) => `${esc(`'${name.replace(/'/g, "''")}'`)}!$1:$${rows}`;
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* The built-in name that makes the top rows of a sheet repeat on every printed
|
|
1206
|
+
* page. `localSheetId` is the sheet's 0-based position in `<sheets>`, which is
|
|
1207
|
+
* what scopes the name to that one worksheet -- a workbook-scoped name would
|
|
1208
|
+
* make one range serve them all.
|
|
1209
|
+
*
|
|
1210
|
+
* @param {ReadonlyArray<{ name: string, titles: number }>} sheets
|
|
1211
|
+
*/
|
|
1212
|
+
const definedNamesXml = (sheets) => {
|
|
1213
|
+
const names = sheets
|
|
1214
|
+
.map((sheet, index) =>
|
|
1215
|
+
sheet.titles === 0
|
|
1216
|
+
? ""
|
|
1217
|
+
: `<definedName name="_xlnm.Print_Titles" localSheetId="${index}">${titlesRef(sheet.name, sheet.titles)}</definedName>`,
|
|
1218
|
+
)
|
|
1219
|
+
.join("");
|
|
1220
|
+
return names === "" ? "" : `<definedNames>${names}</definedNames>`;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
/** @param {ReadonlyArray<{ name: string, titles: number }>} sheets */
|
|
1224
|
+
const workbookXml = (sheets) =>
|
|
1225
|
+
DECLARATION +
|
|
1226
|
+
`<workbook xmlns="${NS}" xmlns:r="${NS_R}"><sheets>` +
|
|
1227
|
+
sheets
|
|
1228
|
+
.map(
|
|
1229
|
+
(sheet, index) =>
|
|
1230
|
+
`<sheet name="${esc(sheet.name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`,
|
|
1231
|
+
)
|
|
1232
|
+
.join("") +
|
|
1233
|
+
`</sheets>` +
|
|
1234
|
+
definedNamesXml(sheets) +
|
|
1235
|
+
`</workbook>`;
|
|
1236
|
+
|
|
1237
|
+
/** @param {number} sheets how many worksheets the workbook holds */
|
|
1238
|
+
const workbookRelsXml = (sheets) =>
|
|
1239
|
+
DECLARATION +
|
|
1240
|
+
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1241
|
+
Array.from(
|
|
1242
|
+
{ length: sheets },
|
|
1243
|
+
(_sheet, index) =>
|
|
1244
|
+
`<Relationship Id="rId${index + 1}" Type="${REL}/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`,
|
|
1245
|
+
).join("") +
|
|
1246
|
+
`<Relationship Id="rId${sheets + 1}" Type="${REL}/styles" Target="styles.xml"/>` +
|
|
1247
|
+
`<Relationship Id="rId${sheets + 2}" Type="${REL}/sharedStrings" Target="sharedStrings.xml"/>` +
|
|
1248
|
+
`</Relationships>`;
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* @typedef {(name: string, body: string | Uint8Array, store?: boolean) => void} Part
|
|
1252
|
+
* @typedef {{ sheet: ReturnType<typeof worksheet>, index: number }} Drawing
|
|
1253
|
+
*/
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Every worksheet part, and the relationship part of each sheet that carries
|
|
1257
|
+
* a drawing.
|
|
1258
|
+
* @param {Part} part
|
|
1259
|
+
* @param {ReadonlyArray<ReturnType<typeof worksheet>>} sheets
|
|
1260
|
+
* @param {ReadonlyArray<Drawing>} drawings
|
|
1261
|
+
*/
|
|
1262
|
+
const sheetParts = (part, sheets, drawings) => {
|
|
1263
|
+
for (const [index, sheet] of sheets.entries()) {
|
|
1264
|
+
const at = drawings.findIndex((each) => each.index === index);
|
|
1265
|
+
part(`xl/worksheets/sheet${index + 1}.xml`, sheet.xml(at === -1 ? null : 1));
|
|
1266
|
+
if (at !== -1)
|
|
1267
|
+
part(
|
|
1268
|
+
`xl/worksheets/_rels/sheet${index + 1}.xml.rels`,
|
|
1269
|
+
DECLARATION +
|
|
1270
|
+
`<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${REL}/drawing" Target="../drawings/drawing${at + 1}.xml"/></Relationships>`,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Each drawing part and its relationships. A drawing numbers its own
|
|
1277
|
+
* relationships from one, in the order its pictures were placed, so they line
|
|
1278
|
+
* up with its own part rather than with the workbook-wide media list.
|
|
1279
|
+
* @param {Part} part
|
|
1280
|
+
* @param {ReadonlyArray<Drawing>} drawings
|
|
1281
|
+
* @param {ReadonlyArray<{ format: string }>} media
|
|
1282
|
+
*/
|
|
1283
|
+
const drawingParts = (part, drawings, media) => {
|
|
1284
|
+
for (const [at, each] of drawings.entries()) {
|
|
1285
|
+
const used = [...new Set(each.sheet.pictures.map((picture) => picture.id))];
|
|
1286
|
+
part(
|
|
1287
|
+
`xl/drawings/drawing${at + 1}.xml`,
|
|
1288
|
+
drawingXml(
|
|
1289
|
+
each.sheet.pictures,
|
|
1290
|
+
each.sheet.pictures.map((picture) => used.indexOf(picture.id)),
|
|
1291
|
+
),
|
|
1292
|
+
);
|
|
1293
|
+
part(
|
|
1294
|
+
`xl/drawings/_rels/drawing${at + 1}.xml.rels`,
|
|
1295
|
+
DECLARATION +
|
|
1296
|
+
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1297
|
+
used
|
|
1298
|
+
.map(
|
|
1299
|
+
(id, index) =>
|
|
1300
|
+
`<Relationship Id="rId${index + 1}" Type="${REL}/image" Target="../media/image${id + 1}.${media[id].format}"/>`,
|
|
1301
|
+
)
|
|
1302
|
+
.join("") +
|
|
1303
|
+
`</Relationships>`,
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
|
|
950
1308
|
/**
|
|
951
1309
|
* Open a workbook.
|
|
952
1310
|
*
|
|
953
1311
|
* @param {{ title?: string, creator?: string, subject?: string, description?: string }} [meta]
|
|
954
1312
|
*/
|
|
955
1313
|
export const workbook = (meta = {}) => {
|
|
956
|
-
|
|
957
|
-
throw TypeError(`workbook: expected a metadata object, got ${JSON.stringify(meta)}`);
|
|
1314
|
+
requireRecord(meta, "workbook", "a metadata object");
|
|
958
1315
|
|
|
959
1316
|
const styles = stylesheet();
|
|
960
1317
|
const sst = strings();
|
|
@@ -972,10 +1329,18 @@ export const workbook = (meta = {}) => {
|
|
|
972
1329
|
return value;
|
|
973
1330
|
};
|
|
974
1331
|
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1332
|
+
const properties = {
|
|
1333
|
+
title: property("title"),
|
|
1334
|
+
creator: property("creator"),
|
|
1335
|
+
subject: property("subject"),
|
|
1336
|
+
description: property("description"),
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
/** @param {unknown} id */
|
|
1340
|
+
const knows = (id) =>
|
|
1341
|
+
Number.isInteger(id) &&
|
|
1342
|
+
/** @type {number} */ (id) >= 0 &&
|
|
1343
|
+
/** @type {number} */ (id) < media.length;
|
|
979
1344
|
|
|
980
1345
|
return {
|
|
981
1346
|
/**
|
|
@@ -989,30 +1354,22 @@ export const workbook = (meta = {}) => {
|
|
|
989
1354
|
throw TypeError(`image: expected png or jpeg, got ${JSON.stringify(format)}`);
|
|
990
1355
|
// Identical bytes deduplicate: a caller embedding the same logo on every
|
|
991
1356
|
// sheet pays for it once, and does so without keeping a tally of its own.
|
|
992
|
-
|
|
993
|
-
|
|
1357
|
+
const seen = media.findIndex((each) => each.format === format && same(each.bytes, bytes));
|
|
1358
|
+
if (seen !== -1) return seen;
|
|
994
1359
|
media.push({ bytes, format });
|
|
995
1360
|
return media.length - 1;
|
|
996
1361
|
},
|
|
997
1362
|
|
|
998
1363
|
/** @param {string} name */
|
|
999
1364
|
sheet(name) {
|
|
1000
|
-
|
|
1001
|
-
throw RangeError(`sheet: expected a name, got ${JSON.stringify(name)}`);
|
|
1002
|
-
if (name.length > NAME_LIMIT)
|
|
1003
|
-
throw RangeError(`sheet: a name is at most ${NAME_LIMIT} characters, got ${name.length}`);
|
|
1365
|
+
requireSheetName(name);
|
|
1004
1366
|
if (FORBIDDEN_IN_NAME.test(name))
|
|
1005
1367
|
throw RangeError(
|
|
1006
1368
|
`sheet: a name cannot contain : \\ / ? * [ ], got ${JSON.stringify(name)}`,
|
|
1007
1369
|
);
|
|
1008
1370
|
if (sheets.some((each) => each.name === name))
|
|
1009
1371
|
throw RangeError(`sheet: a sheet named ${JSON.stringify(name)} already exists`);
|
|
1010
|
-
const made = worksheet(
|
|
1011
|
-
name,
|
|
1012
|
-
styles,
|
|
1013
|
-
sst,
|
|
1014
|
-
(id) => typeof id === "number" && Number.isInteger(id) && id >= 0 && id < media.length,
|
|
1015
|
-
);
|
|
1372
|
+
const made = worksheet(name, styles, sst, knows);
|
|
1016
1373
|
sheets.push(made);
|
|
1017
1374
|
return made;
|
|
1018
1375
|
},
|
|
@@ -1022,11 +1379,7 @@ export const workbook = (meta = {}) => {
|
|
|
1022
1379
|
|
|
1023
1380
|
/** @type {Array<{ name: string, body: Uint8Array, store?: boolean }>} */
|
|
1024
1381
|
const entries = [];
|
|
1025
|
-
/**
|
|
1026
|
-
* @param {string} name
|
|
1027
|
-
* @param {string | Uint8Array} body
|
|
1028
|
-
* @param {boolean} [store]
|
|
1029
|
-
*/
|
|
1382
|
+
/** @type {Part} */
|
|
1030
1383
|
const part = (name, body, store) =>
|
|
1031
1384
|
entries.push({
|
|
1032
1385
|
name,
|
|
@@ -1034,144 +1387,22 @@ export const workbook = (meta = {}) => {
|
|
|
1034
1387
|
store,
|
|
1035
1388
|
});
|
|
1036
1389
|
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
// the workbook-wide media list.
|
|
1390
|
+
// The sheets that carry a drawing part, in sheet order: drawing parts
|
|
1391
|
+
// are numbered by this list, not by sheet index.
|
|
1040
1392
|
const drawings = sheets
|
|
1041
1393
|
.map((sheet, index) => ({ sheet, index }))
|
|
1042
1394
|
.filter((each) => each.sheet.pictures.length > 0);
|
|
1043
1395
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
.map(
|
|
1052
|
-
(_each, at) =>
|
|
1053
|
-
`<Override PartName="/xl/drawings/drawing${at + 1}.xml" ContentType="${CT}.drawing+xml"/>`,
|
|
1054
|
-
)
|
|
1055
|
-
.join("");
|
|
1056
|
-
const defaults = [...new Set(media.map((each) => each.format))]
|
|
1057
|
-
.map((format) => `<Default Extension="${format}" ContentType="${FORMATS.get(format)}"/>`)
|
|
1058
|
-
.join("");
|
|
1059
|
-
|
|
1060
|
-
part(
|
|
1061
|
-
"[Content_Types].xml",
|
|
1062
|
-
DECLARATION +
|
|
1063
|
-
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
|
1064
|
-
`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
|
|
1065
|
-
`<Default Extension="xml" ContentType="application/xml"/>` +
|
|
1066
|
-
defaults +
|
|
1067
|
-
`<Override PartName="/xl/workbook.xml" ContentType="${CT}.spreadsheetml.sheet.main+xml"/>` +
|
|
1068
|
-
overrides +
|
|
1069
|
-
`<Override PartName="/xl/styles.xml" ContentType="${CT}.spreadsheetml.styles+xml"/>` +
|
|
1070
|
-
`<Override PartName="/xl/sharedStrings.xml" ContentType="${CT}.spreadsheetml.sharedStrings+xml"/>` +
|
|
1071
|
-
drawingOverrides +
|
|
1072
|
-
`<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>` +
|
|
1073
|
-
`<Override PartName="/docProps/app.xml" ContentType="${CT}.extended-properties+xml"/>` +
|
|
1074
|
-
`</Types>`,
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
part(
|
|
1078
|
-
"_rels/.rels",
|
|
1079
|
-
DECLARATION +
|
|
1080
|
-
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1081
|
-
`<Relationship Id="rId1" Type="${REL}/officeDocument" Target="xl/workbook.xml"/>` +
|
|
1082
|
-
`<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>` +
|
|
1083
|
-
`<Relationship Id="rId3" Type="${REL}/extended-properties" Target="docProps/app.xml"/>` +
|
|
1084
|
-
`</Relationships>`,
|
|
1085
|
-
);
|
|
1086
|
-
|
|
1087
|
-
/** @param {string} name @param {string | undefined} value */
|
|
1088
|
-
const tag = (name, value) => (value === undefined ? "" : `<${name}>${esc(value)}</${name}>`);
|
|
1089
|
-
part(
|
|
1090
|
-
"docProps/core.xml",
|
|
1091
|
-
DECLARATION +
|
|
1092
|
-
`<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">` +
|
|
1093
|
-
tag("dc:title", title) +
|
|
1094
|
-
tag("dc:subject", subject) +
|
|
1095
|
-
tag("dc:creator", creator) +
|
|
1096
|
-
tag("dc:description", description) +
|
|
1097
|
-
`<dcterms:created xsi:type="dcterms:W3CDTF">${CREATED}</dcterms:created>` +
|
|
1098
|
-
`<dcterms:modified xsi:type="dcterms:W3CDTF">${CREATED}</dcterms:modified>` +
|
|
1099
|
-
`</cp:coreProperties>`,
|
|
1100
|
-
);
|
|
1101
|
-
|
|
1102
|
-
part(
|
|
1103
|
-
"docProps/app.xml",
|
|
1104
|
-
DECLARATION +
|
|
1105
|
-
`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>werkmap</Application></Properties>`,
|
|
1106
|
-
);
|
|
1107
|
-
|
|
1108
|
-
part(
|
|
1109
|
-
"xl/workbook.xml",
|
|
1110
|
-
DECLARATION +
|
|
1111
|
-
`<workbook xmlns="${NS}" xmlns:r="${NS_R}"><sheets>` +
|
|
1112
|
-
sheets
|
|
1113
|
-
.map(
|
|
1114
|
-
(sheet, index) =>
|
|
1115
|
-
`<sheet name="${esc(sheet.name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`,
|
|
1116
|
-
)
|
|
1117
|
-
.join("") +
|
|
1118
|
-
`</sheets></workbook>`,
|
|
1119
|
-
);
|
|
1120
|
-
|
|
1121
|
-
part(
|
|
1122
|
-
"xl/_rels/workbook.xml.rels",
|
|
1123
|
-
DECLARATION +
|
|
1124
|
-
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1125
|
-
sheets
|
|
1126
|
-
.map(
|
|
1127
|
-
(_sheet, index) =>
|
|
1128
|
-
`<Relationship Id="rId${index + 1}" Type="${REL}/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`,
|
|
1129
|
-
)
|
|
1130
|
-
.join("") +
|
|
1131
|
-
`<Relationship Id="rId${sheets.length + 1}" Type="${REL}/styles" Target="styles.xml"/>` +
|
|
1132
|
-
`<Relationship Id="rId${sheets.length + 2}" Type="${REL}/sharedStrings" Target="sharedStrings.xml"/>` +
|
|
1133
|
-
`</Relationships>`,
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
for (const [index, sheet] of sheets.entries()) {
|
|
1137
|
-
const at = drawings.findIndex((each) => each.index === index);
|
|
1138
|
-
part(`xl/worksheets/sheet${index + 1}.xml`, sheet.xml(at === -1 ? null : 1));
|
|
1139
|
-
if (at !== -1)
|
|
1140
|
-
part(
|
|
1141
|
-
`xl/worksheets/_rels/sheet${index + 1}.xml.rels`,
|
|
1142
|
-
DECLARATION +
|
|
1143
|
-
`<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${REL}/drawing" Target="../drawings/drawing${at + 1}.xml"/></Relationships>`,
|
|
1144
|
-
);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1396
|
+
part("[Content_Types].xml", contentTypesXml(sheets.length, drawings.length, media));
|
|
1397
|
+
part("_rels/.rels", ROOT_RELS);
|
|
1398
|
+
part("docProps/core.xml", coreXml(properties));
|
|
1399
|
+
part("docProps/app.xml", APP_XML);
|
|
1400
|
+
part("xl/workbook.xml", workbookXml(sheets));
|
|
1401
|
+
part("xl/_rels/workbook.xml.rels", workbookRelsXml(sheets.length));
|
|
1402
|
+
sheetParts(part, sheets, drawings);
|
|
1147
1403
|
part("xl/styles.xml", styles.xml());
|
|
1148
1404
|
part("xl/sharedStrings.xml", sst.xml());
|
|
1149
|
-
|
|
1150
|
-
for (const [at, each] of drawings.entries()) {
|
|
1151
|
-
// Each drawing numbers its own relationships from one, in the order
|
|
1152
|
-
// its pictures were placed.
|
|
1153
|
-
const used = [...new Set(each.sheet.pictures.map((picture) => picture.id))];
|
|
1154
|
-
part(
|
|
1155
|
-
`xl/drawings/drawing${at + 1}.xml`,
|
|
1156
|
-
drawingXml(
|
|
1157
|
-
each.sheet.pictures,
|
|
1158
|
-
each.sheet.pictures.map((picture) => used.indexOf(picture.id)),
|
|
1159
|
-
),
|
|
1160
|
-
);
|
|
1161
|
-
part(
|
|
1162
|
-
`xl/drawings/_rels/drawing${at + 1}.xml.rels`,
|
|
1163
|
-
DECLARATION +
|
|
1164
|
-
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1165
|
-
used
|
|
1166
|
-
.map(
|
|
1167
|
-
(id, index) =>
|
|
1168
|
-
`<Relationship Id="rId${index + 1}" Type="${REL}/image" Target="../media/image${id + 1}.${media[id].format}"/>`,
|
|
1169
|
-
)
|
|
1170
|
-
.join("") +
|
|
1171
|
-
`</Relationships>`,
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1405
|
+
drawingParts(part, drawings, media);
|
|
1175
1406
|
for (const [index, each] of media.entries())
|
|
1176
1407
|
part(`xl/media/image${index + 1}.${each.format}`, each.bytes, true);
|
|
1177
1408
|
|