@openjsxl/core 0.2.1 → 0.4.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 +32 -3
- package/dist/index.d.ts +251 -11
- package/dist/index.js +1625 -18
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ var XlsxError = class extends Error {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/ooxml/a1.ts
|
|
13
|
+
var MAX_ROW = 1048576;
|
|
14
|
+
var MAX_COL = 16384;
|
|
15
|
+
var MAX_COL_WIDTH = 255;
|
|
16
|
+
var MAX_ROW_HEIGHT = 409.5;
|
|
13
17
|
var CODE_UPPER_A = 65;
|
|
14
18
|
var CODE_UPPER_Z = 90;
|
|
15
19
|
var CODE_LOWER_A = 97;
|
|
@@ -62,6 +66,10 @@ function serialToDate(serial, date1904 = false) {
|
|
|
62
66
|
const epoch = date1904 ? EPOCH_1904_UTC : EPOCH_1900_UTC;
|
|
63
67
|
return new Date(epoch + Math.round(serial * MS_PER_DAY));
|
|
64
68
|
}
|
|
69
|
+
function dateToSerial(date, date1904 = false) {
|
|
70
|
+
const epoch = date1904 ? EPOCH_1904_UTC : EPOCH_1900_UTC;
|
|
71
|
+
return (date.getTime() - epoch) / MS_PER_DAY;
|
|
72
|
+
}
|
|
65
73
|
|
|
66
74
|
// src/ooxml/cell.ts
|
|
67
75
|
function decodeCell(raw, ctx) {
|
|
@@ -84,7 +92,8 @@ function decodeCell(raw, ctx) {
|
|
|
84
92
|
const num = Number(value);
|
|
85
93
|
if (!Number.isFinite(num)) return { ref, type: "empty", value: null };
|
|
86
94
|
if (ctx.styles?.isDateStyle(raw.style)) {
|
|
87
|
-
|
|
95
|
+
const date = serialToDate(num, ctx.date1904 ?? false);
|
|
96
|
+
if (!Number.isNaN(date.getTime())) return { ref, type: "date", value: date };
|
|
88
97
|
}
|
|
89
98
|
return { ref, type: "number", value: num };
|
|
90
99
|
}
|
|
@@ -95,6 +104,23 @@ function decodeCell(raw, ctx) {
|
|
|
95
104
|
function isWhitespace(ch) {
|
|
96
105
|
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
97
106
|
}
|
|
107
|
+
function isXmlSafe(s) {
|
|
108
|
+
for (let i = 0; i < s.length; i++) {
|
|
109
|
+
const c = s.charCodeAt(i);
|
|
110
|
+
if (c < 32) {
|
|
111
|
+
if (c !== 9 && c !== 10 && c !== 13) return false;
|
|
112
|
+
} else if (c === 65534 || c === 65535) {
|
|
113
|
+
return false;
|
|
114
|
+
} else if (c >= 55296 && c <= 56319) {
|
|
115
|
+
const next = s.charCodeAt(i + 1);
|
|
116
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
117
|
+
i++;
|
|
118
|
+
} else if (c >= 56320 && c <= 57343) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
98
124
|
|
|
99
125
|
// src/utils/xml-names.ts
|
|
100
126
|
function localName(name) {
|
|
@@ -428,50 +454,327 @@ function isBuiltinDateId(id) {
|
|
|
428
454
|
return id >= 14 && id <= 22 || id >= 27 && id <= 36 || id >= 45 && id <= 47 || id >= 50 && id <= 58;
|
|
429
455
|
}
|
|
430
456
|
var ELAPSED_TIME = /\[(?:h+|m+|s+)\]/i;
|
|
431
|
-
var
|
|
457
|
+
var LITERALS = /"[^"]*"|\\.|[_*]./g;
|
|
458
|
+
var BRACKETS = /\[[^\]]*\]/g;
|
|
432
459
|
var DATE_TOKEN = /[dmyhs]/i;
|
|
433
460
|
function isDateFormatCode(formatCode) {
|
|
434
|
-
|
|
435
|
-
|
|
461
|
+
const withoutLiterals = formatCode.replace(LITERALS, "");
|
|
462
|
+
if (ELAPSED_TIME.test(withoutLiterals)) return true;
|
|
463
|
+
return DATE_TOKEN.test(withoutLiterals.replace(BRACKETS, ""));
|
|
464
|
+
}
|
|
465
|
+
function boolAttr(val) {
|
|
466
|
+
return val !== "0" && val !== "false";
|
|
467
|
+
}
|
|
468
|
+
var HEX_COLOR = /^(?:[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
469
|
+
var MAX_COLOR_INDEX = 4294967295;
|
|
470
|
+
var MAX_INDENT = 250;
|
|
471
|
+
function parseColor(attrs) {
|
|
472
|
+
if (attrs.rgb !== void 0) {
|
|
473
|
+
return HEX_COLOR.test(attrs.rgb) ? { rgb: attrs.rgb } : void 0;
|
|
474
|
+
}
|
|
475
|
+
if (attrs.theme !== void 0) {
|
|
476
|
+
const theme = Number(attrs.theme);
|
|
477
|
+
if (!Number.isInteger(theme) || theme < 0 || theme > MAX_COLOR_INDEX) return void 0;
|
|
478
|
+
const tint = attrs.tint === void 0 ? void 0 : Number(attrs.tint);
|
|
479
|
+
if (tint !== void 0 && Number.isFinite(tint)) return { theme, tint };
|
|
480
|
+
return { theme };
|
|
481
|
+
}
|
|
482
|
+
if (attrs.indexed !== void 0) {
|
|
483
|
+
const indexed = Number(attrs.indexed);
|
|
484
|
+
return Number.isInteger(indexed) && indexed >= 0 && indexed <= MAX_COLOR_INDEX ? { indexed } : void 0;
|
|
485
|
+
}
|
|
486
|
+
if (attrs.auto !== void 0 && boolAttr(attrs.auto)) return { auto: true };
|
|
487
|
+
return void 0;
|
|
436
488
|
}
|
|
489
|
+
var PATTERN_TYPES = /* @__PURE__ */ new Set([
|
|
490
|
+
"none",
|
|
491
|
+
"solid",
|
|
492
|
+
"mediumGray",
|
|
493
|
+
"darkGray",
|
|
494
|
+
"lightGray",
|
|
495
|
+
"darkHorizontal",
|
|
496
|
+
"darkVertical",
|
|
497
|
+
"darkDown",
|
|
498
|
+
"darkUp",
|
|
499
|
+
"darkGrid",
|
|
500
|
+
"darkTrellis",
|
|
501
|
+
"lightHorizontal",
|
|
502
|
+
"lightVertical",
|
|
503
|
+
"lightDown",
|
|
504
|
+
"lightUp",
|
|
505
|
+
"lightGrid",
|
|
506
|
+
"lightTrellis",
|
|
507
|
+
"gray125",
|
|
508
|
+
"gray0625"
|
|
509
|
+
]);
|
|
510
|
+
var BORDER_LINE_STYLES = /* @__PURE__ */ new Set([
|
|
511
|
+
"thin",
|
|
512
|
+
"medium",
|
|
513
|
+
"thick",
|
|
514
|
+
"dashed",
|
|
515
|
+
"dotted",
|
|
516
|
+
"double",
|
|
517
|
+
"hair",
|
|
518
|
+
"mediumDashed",
|
|
519
|
+
"dashDot",
|
|
520
|
+
"mediumDashDot",
|
|
521
|
+
"dashDotDot",
|
|
522
|
+
"mediumDashDotDot",
|
|
523
|
+
"slantDashDot"
|
|
524
|
+
]);
|
|
525
|
+
var H_ALIGNMENTS = /* @__PURE__ */ new Set([
|
|
526
|
+
"left",
|
|
527
|
+
"center",
|
|
528
|
+
"right",
|
|
529
|
+
"justify",
|
|
530
|
+
"fill",
|
|
531
|
+
"centerContinuous",
|
|
532
|
+
"distributed"
|
|
533
|
+
]);
|
|
534
|
+
var V_ALIGNMENTS = /* @__PURE__ */ new Set([
|
|
535
|
+
"top",
|
|
536
|
+
"center",
|
|
537
|
+
"bottom",
|
|
538
|
+
"justify",
|
|
539
|
+
"distributed"
|
|
540
|
+
]);
|
|
541
|
+
var FONT_CHILDREN = /* @__PURE__ */ new Set(["name", "sz", "b", "i", "u", "strike", "color"]);
|
|
542
|
+
function parseAlignment(attrs) {
|
|
543
|
+
const out = {};
|
|
544
|
+
if (attrs.horizontal !== void 0 && H_ALIGNMENTS.has(attrs.horizontal)) {
|
|
545
|
+
out.horizontal = attrs.horizontal;
|
|
546
|
+
}
|
|
547
|
+
if (attrs.vertical !== void 0 && V_ALIGNMENTS.has(attrs.vertical)) {
|
|
548
|
+
out.vertical = attrs.vertical;
|
|
549
|
+
}
|
|
550
|
+
if (attrs.wrapText !== void 0 && boolAttr(attrs.wrapText)) out.wrapText = true;
|
|
551
|
+
if (attrs.shrinkToFit !== void 0 && boolAttr(attrs.shrinkToFit)) out.shrinkToFit = true;
|
|
552
|
+
if (attrs.indent !== void 0) {
|
|
553
|
+
const indent = Number(attrs.indent);
|
|
554
|
+
if (Number.isInteger(indent) && indent > 0 && indent <= MAX_INDENT) out.indent = indent;
|
|
555
|
+
}
|
|
556
|
+
if (attrs.textRotation !== void 0) {
|
|
557
|
+
const rotation = Number(attrs.textRotation);
|
|
558
|
+
if (Number.isInteger(rotation) && rotation > 0 && rotation <= 180)
|
|
559
|
+
out.textRotation = rotation;
|
|
560
|
+
}
|
|
561
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
562
|
+
}
|
|
563
|
+
var hasKeys = (o) => Object.keys(o).length > 0;
|
|
437
564
|
function parseStyles(xml) {
|
|
438
565
|
const customFormats = /* @__PURE__ */ new Map();
|
|
439
|
-
const
|
|
566
|
+
const xfs = [];
|
|
567
|
+
const fonts = [];
|
|
568
|
+
const fills = [];
|
|
569
|
+
const borders = [];
|
|
440
570
|
let inNumFmts = false;
|
|
441
571
|
let inCellXfs = false;
|
|
572
|
+
let inXf = false;
|
|
573
|
+
let inFonts = false;
|
|
574
|
+
let font;
|
|
575
|
+
let inFills = false;
|
|
576
|
+
let inFill = false;
|
|
577
|
+
let fill;
|
|
578
|
+
let inBorders = false;
|
|
579
|
+
let border;
|
|
580
|
+
let edgeName;
|
|
581
|
+
let edgeStyle;
|
|
582
|
+
let edgeColor;
|
|
583
|
+
const commitEdge = () => {
|
|
584
|
+
if (border !== void 0 && edgeName !== void 0 && edgeStyle !== void 0) {
|
|
585
|
+
border[edgeName] = edgeColor !== void 0 ? { style: edgeStyle, color: edgeColor } : { style: edgeStyle };
|
|
586
|
+
}
|
|
587
|
+
edgeName = void 0;
|
|
588
|
+
edgeStyle = void 0;
|
|
589
|
+
edgeColor = void 0;
|
|
590
|
+
};
|
|
442
591
|
for (const token of tokenize(xml)) {
|
|
443
592
|
if (token.kind === "text") continue;
|
|
444
593
|
const name = localName(token.name);
|
|
445
594
|
if (token.kind === "open") {
|
|
446
595
|
if (name === "numFmts") {
|
|
447
596
|
if (!token.selfClosing) inNumFmts = true;
|
|
448
|
-
} else if (name === "cellXfs") {
|
|
449
|
-
if (!token.selfClosing) inCellXfs = true;
|
|
450
597
|
} else if (name === "numFmt" && inNumFmts) {
|
|
451
598
|
const id = Number(token.attrs.numFmtId);
|
|
452
599
|
const code = token.attrs.formatCode;
|
|
453
600
|
if (Number.isInteger(id) && code !== void 0) customFormats.set(id, code);
|
|
601
|
+
} else if (name === "fonts") {
|
|
602
|
+
if (!token.selfClosing) inFonts = true;
|
|
603
|
+
} else if (name === "font" && inFonts) {
|
|
604
|
+
font = {};
|
|
605
|
+
if (token.selfClosing) {
|
|
606
|
+
fonts.push({});
|
|
607
|
+
font = void 0;
|
|
608
|
+
}
|
|
609
|
+
} else if (font !== void 0 && FONT_CHILDREN.has(name)) {
|
|
610
|
+
if (name === "name" && token.attrs.val !== void 0) {
|
|
611
|
+
const val = token.attrs.val;
|
|
612
|
+
if (val !== "" && isXmlSafe(val)) font.name = val;
|
|
613
|
+
} else if (name === "sz") {
|
|
614
|
+
const size = Number(token.attrs.val);
|
|
615
|
+
if (Number.isFinite(size) && size > 0) font.size = size;
|
|
616
|
+
} else if (name === "b") {
|
|
617
|
+
if (boolAttr(token.attrs.val)) font.bold = true;
|
|
618
|
+
} else if (name === "i") {
|
|
619
|
+
if (boolAttr(token.attrs.val)) font.italic = true;
|
|
620
|
+
} else if (name === "u") {
|
|
621
|
+
const val = token.attrs.val ?? "single";
|
|
622
|
+
if (val === "single" || val === "double") font.underline = val;
|
|
623
|
+
} else if (name === "strike") {
|
|
624
|
+
if (boolAttr(token.attrs.val)) font.strike = true;
|
|
625
|
+
} else if (name === "color") {
|
|
626
|
+
const color = parseColor(token.attrs);
|
|
627
|
+
if (color !== void 0) font.color = color;
|
|
628
|
+
}
|
|
629
|
+
} else if (name === "fills") {
|
|
630
|
+
if (!token.selfClosing) inFills = true;
|
|
631
|
+
} else if (name === "fill" && inFills) {
|
|
632
|
+
if (token.selfClosing) fills.push(void 0);
|
|
633
|
+
else {
|
|
634
|
+
inFill = true;
|
|
635
|
+
fill = void 0;
|
|
636
|
+
}
|
|
637
|
+
} else if (name === "patternFill" && inFill) {
|
|
638
|
+
const patternType = token.attrs.patternType;
|
|
639
|
+
fill = {
|
|
640
|
+
patternType: patternType !== void 0 && PATTERN_TYPES.has(patternType) ? patternType : "none"
|
|
641
|
+
};
|
|
642
|
+
} else if (name === "fgColor" && fill !== void 0) {
|
|
643
|
+
const color = parseColor(token.attrs);
|
|
644
|
+
if (color !== void 0) fill.fgColor = color;
|
|
645
|
+
} else if (name === "bgColor" && fill !== void 0) {
|
|
646
|
+
const color = parseColor(token.attrs);
|
|
647
|
+
if (color !== void 0) fill.bgColor = color;
|
|
648
|
+
} else if (name === "borders") {
|
|
649
|
+
if (!token.selfClosing) inBorders = true;
|
|
650
|
+
} else if (name === "border" && inBorders) {
|
|
651
|
+
if (token.selfClosing) borders.push({});
|
|
652
|
+
else border = {};
|
|
653
|
+
} else if (border !== void 0 && (name === "left" || name === "right" || name === "top" || name === "bottom")) {
|
|
654
|
+
const style = token.attrs.style;
|
|
655
|
+
const lineStyle = style !== void 0 && BORDER_LINE_STYLES.has(style) ? style : void 0;
|
|
656
|
+
if (token.selfClosing) {
|
|
657
|
+
if (lineStyle !== void 0 && border !== void 0)
|
|
658
|
+
border[name] = { style: lineStyle };
|
|
659
|
+
} else {
|
|
660
|
+
edgeName = name;
|
|
661
|
+
edgeStyle = lineStyle;
|
|
662
|
+
edgeColor = void 0;
|
|
663
|
+
}
|
|
664
|
+
} else if (name === "color" && edgeName !== void 0) {
|
|
665
|
+
const color = parseColor(token.attrs);
|
|
666
|
+
if (color !== void 0) edgeColor = color;
|
|
667
|
+
} else if (name === "cellXfs") {
|
|
668
|
+
if (!token.selfClosing) inCellXfs = true;
|
|
454
669
|
} else if (name === "xf" && inCellXfs) {
|
|
455
|
-
const
|
|
456
|
-
|
|
670
|
+
const numFmtId = Number(token.attrs.numFmtId ?? "0");
|
|
671
|
+
const fontId = Number(token.attrs.fontId ?? "0");
|
|
672
|
+
const fillId = Number(token.attrs.fillId ?? "0");
|
|
673
|
+
const borderId = Number(token.attrs.borderId ?? "0");
|
|
674
|
+
xfs.push({
|
|
675
|
+
numFmtId: Number.isInteger(numFmtId) ? numFmtId : 0,
|
|
676
|
+
fontId: Number.isInteger(fontId) ? fontId : 0,
|
|
677
|
+
fillId: Number.isInteger(fillId) ? fillId : 0,
|
|
678
|
+
borderId: Number.isInteger(borderId) ? borderId : 0,
|
|
679
|
+
alignment: void 0
|
|
680
|
+
});
|
|
681
|
+
if (!token.selfClosing) inXf = true;
|
|
682
|
+
} else if (name === "alignment" && inXf) {
|
|
683
|
+
const alignment = parseAlignment(token.attrs);
|
|
684
|
+
if (alignment !== void 0 && xfs.length > 0) {
|
|
685
|
+
const last = xfs[xfs.length - 1];
|
|
686
|
+
xfs[xfs.length - 1] = { ...last, alignment };
|
|
687
|
+
}
|
|
457
688
|
}
|
|
458
689
|
} else if (token.kind === "close") {
|
|
459
690
|
if (name === "numFmts") inNumFmts = false;
|
|
460
|
-
else if (name === "
|
|
691
|
+
else if (name === "fonts") {
|
|
692
|
+
inFonts = false;
|
|
693
|
+
if (font !== void 0) {
|
|
694
|
+
fonts.push(font);
|
|
695
|
+
font = void 0;
|
|
696
|
+
}
|
|
697
|
+
} else if (name === "font") {
|
|
698
|
+
if (font !== void 0) {
|
|
699
|
+
fonts.push(font);
|
|
700
|
+
font = void 0;
|
|
701
|
+
}
|
|
702
|
+
} else if (name === "fills") {
|
|
703
|
+
inFills = false;
|
|
704
|
+
if (inFill) {
|
|
705
|
+
fills.push(fill);
|
|
706
|
+
fill = void 0;
|
|
707
|
+
inFill = false;
|
|
708
|
+
}
|
|
709
|
+
} else if (name === "fill") {
|
|
710
|
+
if (inFill) {
|
|
711
|
+
fills.push(fill);
|
|
712
|
+
fill = void 0;
|
|
713
|
+
inFill = false;
|
|
714
|
+
}
|
|
715
|
+
} else if (name === "borders") {
|
|
716
|
+
inBorders = false;
|
|
717
|
+
if (border !== void 0) {
|
|
718
|
+
commitEdge();
|
|
719
|
+
borders.push(border);
|
|
720
|
+
border = void 0;
|
|
721
|
+
}
|
|
722
|
+
} else if (name === "border") {
|
|
723
|
+
if (border !== void 0) {
|
|
724
|
+
commitEdge();
|
|
725
|
+
borders.push(border);
|
|
726
|
+
border = void 0;
|
|
727
|
+
}
|
|
728
|
+
} else if (name === "left" || name === "right" || name === "top" || name === "bottom") {
|
|
729
|
+
if (edgeName === name) commitEdge();
|
|
730
|
+
} else if (name === "cellXfs") {
|
|
731
|
+
inCellXfs = false;
|
|
732
|
+
inXf = false;
|
|
733
|
+
} else if (name === "xf") inXf = false;
|
|
461
734
|
}
|
|
462
735
|
}
|
|
463
736
|
function isDateStyle(styleIndex) {
|
|
464
|
-
const numFmtId =
|
|
737
|
+
const numFmtId = xfs[styleIndex ?? 0]?.numFmtId;
|
|
465
738
|
if (numFmtId === void 0) return false;
|
|
466
739
|
const custom = customFormats.get(numFmtId);
|
|
467
740
|
return custom !== void 0 ? isDateFormatCode(custom) : isBuiltinDateId(numFmtId);
|
|
468
741
|
}
|
|
469
742
|
function formatCode(styleIndex) {
|
|
470
|
-
const numFmtId =
|
|
743
|
+
const numFmtId = xfs[styleIndex ?? 0]?.numFmtId;
|
|
471
744
|
if (numFmtId === void 0) return void 0;
|
|
472
745
|
return customFormats.get(numFmtId) ?? BUILTIN_FORMATS[numFmtId];
|
|
473
746
|
}
|
|
474
|
-
|
|
747
|
+
const styleCache = /* @__PURE__ */ new Map();
|
|
748
|
+
function cellStyle(styleIndex) {
|
|
749
|
+
const index = styleIndex ?? 0;
|
|
750
|
+
const cached = styleCache.get(index);
|
|
751
|
+
if (cached !== void 0 || styleCache.has(index)) return cached;
|
|
752
|
+
const xf = xfs[index];
|
|
753
|
+
let result;
|
|
754
|
+
if (xf !== void 0) {
|
|
755
|
+
const style = {};
|
|
756
|
+
if (xf.numFmtId !== 0) {
|
|
757
|
+
const code = customFormats.get(xf.numFmtId) ?? BUILTIN_FORMATS[xf.numFmtId];
|
|
758
|
+
if (code !== void 0 && code !== "" && code !== "General" && isXmlSafe(code)) {
|
|
759
|
+
style.numberFormat = code;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (xf.fontId !== 0) {
|
|
763
|
+
const fontRecord = fonts[xf.fontId];
|
|
764
|
+
if (fontRecord !== void 0 && hasKeys(fontRecord)) style.font = fontRecord;
|
|
765
|
+
}
|
|
766
|
+
const fillRecord = fills[xf.fillId];
|
|
767
|
+
if (fillRecord !== void 0 && fillRecord.patternType !== "none")
|
|
768
|
+
style.fill = fillRecord;
|
|
769
|
+
const borderRecord = borders[xf.borderId];
|
|
770
|
+
if (borderRecord !== void 0 && hasKeys(borderRecord)) style.border = borderRecord;
|
|
771
|
+
if (xf.alignment !== void 0) style.alignment = xf.alignment;
|
|
772
|
+
result = hasKeys(style) ? style : void 0;
|
|
773
|
+
}
|
|
774
|
+
styleCache.set(index, result);
|
|
775
|
+
return result;
|
|
776
|
+
}
|
|
777
|
+
return { isDateStyle, formatCode, cellStyle };
|
|
475
778
|
}
|
|
476
779
|
|
|
477
780
|
// src/ooxml/workbook.ts
|
|
@@ -488,8 +791,9 @@ function parseWorkbook(xml) {
|
|
|
488
791
|
const name = token.attrs.name;
|
|
489
792
|
const rid = relationshipId(token.attrs);
|
|
490
793
|
if (name === void 0 || rid === void 0) continue;
|
|
491
|
-
const
|
|
492
|
-
|
|
794
|
+
const raw = token.attrs.state;
|
|
795
|
+
const state = raw === "hidden" || raw === "veryHidden" ? raw : "visible";
|
|
796
|
+
sheets.push({ name, rid, visible: state === "visible", state });
|
|
493
797
|
}
|
|
494
798
|
}
|
|
495
799
|
return { sheets, date1904 };
|
|
@@ -1007,7 +1311,10 @@ function parseHyperlinks(xml, rels) {
|
|
|
1007
1311
|
const display = token.attrs.display;
|
|
1008
1312
|
links.push({
|
|
1009
1313
|
ref,
|
|
1010
|
-
|
|
1314
|
+
// An empty external target is no destination — gate it exactly like an empty location,
|
|
1315
|
+
// so a degenerate `Target=""` rel doesn't surface as a target the writer then melts
|
|
1316
|
+
// away (which would make read→write→read lossy instead of lossless-or-typed).
|
|
1317
|
+
...target !== void 0 && target !== "" ? { target } : {},
|
|
1011
1318
|
...location !== void 0 && location !== "" ? { location } : {},
|
|
1012
1319
|
...tooltip !== void 0 ? { tooltip } : {},
|
|
1013
1320
|
...display !== void 0 ? { display } : {}
|
|
@@ -1029,6 +1336,85 @@ async function* streamRows(chunks, ctx) {
|
|
|
1029
1336
|
for (const token of xml.flush()) yield* assembler.push(token);
|
|
1030
1337
|
yield* assembler.flush();
|
|
1031
1338
|
}
|
|
1339
|
+
var boolFlag = (val) => val === "1" || val === "true";
|
|
1340
|
+
function parseColumnProps(xml) {
|
|
1341
|
+
const out = [];
|
|
1342
|
+
for (const token of tokenize(xml)) {
|
|
1343
|
+
if (token.kind !== "open") continue;
|
|
1344
|
+
const name = localName(token.name);
|
|
1345
|
+
if (name === "sheetData") break;
|
|
1346
|
+
if (name !== "col") continue;
|
|
1347
|
+
const min = Number(token.attrs.min);
|
|
1348
|
+
const max = Number(token.attrs.max);
|
|
1349
|
+
if (!Number.isInteger(min) || !Number.isInteger(max)) continue;
|
|
1350
|
+
if (min < 1 || max < min || max > MAX_COL) continue;
|
|
1351
|
+
const props = { min, max };
|
|
1352
|
+
if (token.attrs.width !== void 0) {
|
|
1353
|
+
const width = Number(token.attrs.width);
|
|
1354
|
+
if (Number.isFinite(width) && width > 0 && width <= MAX_COL_WIDTH) props.width = width;
|
|
1355
|
+
}
|
|
1356
|
+
if (boolFlag(token.attrs.hidden)) props.hidden = true;
|
|
1357
|
+
if (props.width !== void 0 || props.hidden) out.push(props);
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1360
|
+
}
|
|
1361
|
+
function parseRowProperties(xml) {
|
|
1362
|
+
const out = /* @__PURE__ */ new Map();
|
|
1363
|
+
let lastRow = 0;
|
|
1364
|
+
let inSheetData = false;
|
|
1365
|
+
for (const token of tokenize(xml)) {
|
|
1366
|
+
if (token.kind === "text") continue;
|
|
1367
|
+
const name = localName(token.name);
|
|
1368
|
+
if (token.kind === "close") {
|
|
1369
|
+
if (name === "sheetData") inSheetData = false;
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (name === "sheetData") {
|
|
1373
|
+
if (!token.selfClosing) inSheetData = true;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (!inSheetData || name !== "row") continue;
|
|
1377
|
+
const rAttr = token.attrs.r;
|
|
1378
|
+
const parsed = rAttr !== void 0 ? Number.parseInt(rAttr, 10) : Number.NaN;
|
|
1379
|
+
const rowIndex = Number.isInteger(parsed) && parsed > 0 ? parsed : lastRow + 1;
|
|
1380
|
+
lastRow = rowIndex;
|
|
1381
|
+
if (rowIndex > MAX_ROW) continue;
|
|
1382
|
+
const props = {};
|
|
1383
|
+
if (token.attrs.ht !== void 0) {
|
|
1384
|
+
const height = Number(token.attrs.ht);
|
|
1385
|
+
if (Number.isFinite(height) && height > 0 && height <= MAX_ROW_HEIGHT) {
|
|
1386
|
+
props.height = height;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (boolFlag(token.attrs.hidden)) props.hidden = true;
|
|
1390
|
+
if (props.height !== void 0 || props.hidden) out.set(rowIndex, props);
|
|
1391
|
+
}
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
function parseFreezePane(xml) {
|
|
1395
|
+
let inSheetViews = false;
|
|
1396
|
+
for (const token of tokenize(xml)) {
|
|
1397
|
+
if (token.kind === "text") continue;
|
|
1398
|
+
const name = localName(token.name);
|
|
1399
|
+
if (name === "sheetViews") {
|
|
1400
|
+
if (token.kind === "close" || token.selfClosing) return void 0;
|
|
1401
|
+
inSheetViews = true;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (!inSheetViews || token.kind !== "open" || name !== "pane") continue;
|
|
1405
|
+
if (token.attrs.state !== "frozen") return void 0;
|
|
1406
|
+
const x = token.attrs.xSplit === void 0 ? 0 : Number(token.attrs.xSplit);
|
|
1407
|
+
const y = token.attrs.ySplit === void 0 ? 0 : Number(token.attrs.ySplit);
|
|
1408
|
+
const cols = Number.isInteger(x) && x > 0 && x < MAX_COL ? x : 0;
|
|
1409
|
+
const rows = Number.isInteger(y) && y > 0 && y < MAX_ROW ? y : 0;
|
|
1410
|
+
if (cols === 0 && rows === 0) return void 0;
|
|
1411
|
+
const out = {};
|
|
1412
|
+
if (rows > 0) out.rows = rows;
|
|
1413
|
+
if (cols > 0) out.cols = cols;
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1416
|
+
return void 0;
|
|
1417
|
+
}
|
|
1032
1418
|
|
|
1033
1419
|
// src/reader/workbook.ts
|
|
1034
1420
|
var decoder = new TextDecoder();
|
|
@@ -1066,6 +1452,10 @@ var Worksheet = class {
|
|
|
1066
1452
|
#dimension;
|
|
1067
1453
|
#dimensionRead = false;
|
|
1068
1454
|
#comments;
|
|
1455
|
+
#columns;
|
|
1456
|
+
#rowProps;
|
|
1457
|
+
#freeze;
|
|
1458
|
+
#freezeRead = false;
|
|
1069
1459
|
constructor(info, xml, context, rels, commentsXml) {
|
|
1070
1460
|
this.name = info.name;
|
|
1071
1461
|
this.#info = info;
|
|
@@ -1082,6 +1472,10 @@ var Worksheet = class {
|
|
|
1082
1472
|
get visible() {
|
|
1083
1473
|
return this.#info.visible;
|
|
1084
1474
|
}
|
|
1475
|
+
/** The tab's visibility state (F4.6): `"visible"`, `"hidden"`, or `"veryHidden"`. */
|
|
1476
|
+
get state() {
|
|
1477
|
+
return this.#info.state;
|
|
1478
|
+
}
|
|
1085
1479
|
/**
|
|
1086
1480
|
* Merged-cell ranges in A1 notation (e.g. `['A1:B1', 'A2:A4']`), in document order. Only the
|
|
1087
1481
|
* top-left cell of a merge holds a value; the rest read as `empty`. Empty when none.
|
|
@@ -1110,6 +1504,17 @@ var Worksheet = class {
|
|
|
1110
1504
|
numberFormat(ref) {
|
|
1111
1505
|
return this.#context.styles?.formatCode(this.#cellStyleMap().get(ref));
|
|
1112
1506
|
}
|
|
1507
|
+
/**
|
|
1508
|
+
* The resolved style of the cell at `ref` — number format code, font, fill, border, and
|
|
1509
|
+
* alignment (F4.1). Resolution shares the same effective-style map as {@link numberFormat}
|
|
1510
|
+
* (cell `s` → row `customFormat` default → column default), so the two always agree.
|
|
1511
|
+
* `undefined` for an unstyled cell, an absent cell, or a workbook with no style table —
|
|
1512
|
+
* "no style" and "the default style" are deliberately the same answer. Objects are cached
|
|
1513
|
+
* per distinct format record: two cells sharing a format return the same object.
|
|
1514
|
+
*/
|
|
1515
|
+
style(ref) {
|
|
1516
|
+
return this.#context.styles?.cellStyle(this.#cellStyleMap().get(ref));
|
|
1517
|
+
}
|
|
1113
1518
|
/**
|
|
1114
1519
|
* The sheet's declared used range in A1 notation (e.g. `"A1:E10"`, or a single cell), from
|
|
1115
1520
|
* the worksheet's `<dimension>`. `undefined` when the producer omits it — it is an optional
|
|
@@ -1132,6 +1537,34 @@ var Worksheet = class {
|
|
|
1132
1537
|
}
|
|
1133
1538
|
return this.#comments;
|
|
1134
1539
|
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Column width/visibility declarations (`<cols>`), in document order (F4.5). Entries carrying
|
|
1542
|
+
* only a column-default STYLE are not geometry and are omitted — `style(ref)` already resolves
|
|
1543
|
+
* them. Empty when the sheet declares none.
|
|
1544
|
+
*/
|
|
1545
|
+
get columns() {
|
|
1546
|
+
if (this.#columns === void 0) this.#columns = parseColumnProps(this.#xml);
|
|
1547
|
+
return this.#columns;
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Per-row height/visibility, keyed by 1-based row index (F4.5). Only rows that declare a
|
|
1551
|
+
* height or hidden flag appear. Empty map when none do.
|
|
1552
|
+
*/
|
|
1553
|
+
get rowProperties() {
|
|
1554
|
+
if (this.#rowProps === void 0) this.#rowProps = parseRowProperties(this.#xml);
|
|
1555
|
+
return this.#rowProps;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* The sheet's frozen pane, or `undefined` when nothing is frozen (F4.5). Split (non-frozen)
|
|
1559
|
+
* panes are not modelled and read as `undefined`.
|
|
1560
|
+
*/
|
|
1561
|
+
get freeze() {
|
|
1562
|
+
if (!this.#freezeRead) {
|
|
1563
|
+
this.#freeze = parseFreezePane(this.#xml);
|
|
1564
|
+
this.#freezeRead = true;
|
|
1565
|
+
}
|
|
1566
|
+
return this.#freeze;
|
|
1567
|
+
}
|
|
1135
1568
|
#cellStyleMap() {
|
|
1136
1569
|
if (this.#cellStyles === void 0) this.#cellStyles = parseCellStyles(this.#xml);
|
|
1137
1570
|
return this.#cellStyles;
|
|
@@ -1213,7 +1646,10 @@ async function loadWorkbook(source, options) {
|
|
|
1213
1646
|
if (rel === void 0 || rel.targetMode === "External") continue;
|
|
1214
1647
|
const path = resolveTarget(workbookDir, rel.target);
|
|
1215
1648
|
if (!zip.has(path)) continue;
|
|
1216
|
-
sheets.push({
|
|
1649
|
+
sheets.push({
|
|
1650
|
+
info: { name: entry.name, path, visible: entry.visible, state: entry.state },
|
|
1651
|
+
path
|
|
1652
|
+
});
|
|
1217
1653
|
}
|
|
1218
1654
|
return { zip, context, sheets };
|
|
1219
1655
|
}
|
|
@@ -1257,4 +1693,1175 @@ async function* streamSheetRows(source, sheetName, options) {
|
|
|
1257
1693
|
yield* streamRows(zip.readStream(path), context);
|
|
1258
1694
|
}
|
|
1259
1695
|
|
|
1260
|
-
|
|
1696
|
+
// src/writer/crc32.ts
|
|
1697
|
+
var CRC_TABLE = (() => {
|
|
1698
|
+
const table = new Uint32Array(256);
|
|
1699
|
+
for (let n = 0; n < 256; n++) {
|
|
1700
|
+
let c = n;
|
|
1701
|
+
for (let k = 0; k < 8; k++) c = (c & 1) !== 0 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
1702
|
+
table[n] = c >>> 0;
|
|
1703
|
+
}
|
|
1704
|
+
return table;
|
|
1705
|
+
})();
|
|
1706
|
+
function crc32(bytes) {
|
|
1707
|
+
let crc = 4294967295;
|
|
1708
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1709
|
+
crc = (CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8) >>> 0;
|
|
1710
|
+
}
|
|
1711
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/writer/deflate.ts
|
|
1715
|
+
async function deflateRaw(data) {
|
|
1716
|
+
const blob = new Blob([data]);
|
|
1717
|
+
const reader = blob.stream().pipeThrough(new CompressionStream("deflate-raw")).getReader();
|
|
1718
|
+
const chunks = [];
|
|
1719
|
+
let total = 0;
|
|
1720
|
+
for (; ; ) {
|
|
1721
|
+
const { done, value } = await reader.read();
|
|
1722
|
+
if (done) break;
|
|
1723
|
+
total += value.byteLength;
|
|
1724
|
+
chunks.push(value);
|
|
1725
|
+
}
|
|
1726
|
+
const out = new Uint8Array(total);
|
|
1727
|
+
let offset = 0;
|
|
1728
|
+
for (const chunk of chunks) {
|
|
1729
|
+
out.set(chunk, offset);
|
|
1730
|
+
offset += chunk.byteLength;
|
|
1731
|
+
}
|
|
1732
|
+
return out;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/writer/from-workbook.ts
|
|
1736
|
+
function cellToInput(worksheet, cell) {
|
|
1737
|
+
const style = worksheet.style(cell.ref);
|
|
1738
|
+
return style === void 0 ? cell.value : { value: cell.value, style };
|
|
1739
|
+
}
|
|
1740
|
+
async function workbookToInput(workbook) {
|
|
1741
|
+
const sheets = [];
|
|
1742
|
+
for (const info of workbook.sheets) {
|
|
1743
|
+
const worksheet = workbook.sheet(info.name);
|
|
1744
|
+
const rows = [];
|
|
1745
|
+
const occupied = /* @__PURE__ */ new Map();
|
|
1746
|
+
for await (const row of worksheet.rows()) {
|
|
1747
|
+
for (const cell of row.cells) {
|
|
1748
|
+
let placed;
|
|
1749
|
+
try {
|
|
1750
|
+
placed = parseRef(cell.ref);
|
|
1751
|
+
} catch {
|
|
1752
|
+
placed = void 0;
|
|
1753
|
+
}
|
|
1754
|
+
if (placed === void 0 || placed.row > MAX_ROW || placed.col > MAX_COL) {
|
|
1755
|
+
const shown = cell.ref.length > 24 ? `${cell.ref.slice(0, 24)}\u2026` : cell.ref;
|
|
1756
|
+
throw new XlsxError(
|
|
1757
|
+
"invalid-input",
|
|
1758
|
+
`sheet "${info.name}": cell reference "${shown}" has no writable grid position`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
const canonical = formatRef(placed);
|
|
1762
|
+
const prior = occupied.get(canonical);
|
|
1763
|
+
if (prior !== void 0 && prior !== cell.ref) {
|
|
1764
|
+
throw new XlsxError(
|
|
1765
|
+
"invalid-input",
|
|
1766
|
+
`sheet "${info.name}": cells "${prior}" and "${cell.ref}" are distinct to the reader but occupy one grid position (${canonical})`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
occupied.set(canonical, cell.ref);
|
|
1770
|
+
const { col, row: rowNum } = placed;
|
|
1771
|
+
let rowArr = rows[rowNum - 1];
|
|
1772
|
+
if (rowArr === void 0) {
|
|
1773
|
+
rowArr = [];
|
|
1774
|
+
rows[rowNum - 1] = rowArr;
|
|
1775
|
+
}
|
|
1776
|
+
rowArr[col - 1] = cellToInput(worksheet, cell);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
const sheet = { name: info.name, rows };
|
|
1780
|
+
const columns = worksheet.columns;
|
|
1781
|
+
if (columns.length > 0) sheet.columns = columns;
|
|
1782
|
+
const rowProperties = worksheet.rowProperties;
|
|
1783
|
+
if (rowProperties.size > 0) sheet.rowProperties = Object.fromEntries(rowProperties);
|
|
1784
|
+
const freeze = worksheet.freeze;
|
|
1785
|
+
if (freeze !== void 0) sheet.freeze = freeze;
|
|
1786
|
+
const merges = worksheet.mergedCells;
|
|
1787
|
+
if (merges.length > 0) sheet.merges = merges;
|
|
1788
|
+
const hyperlinks = worksheet.hyperlinks;
|
|
1789
|
+
if (hyperlinks.length > 0) sheet.hyperlinks = hyperlinks;
|
|
1790
|
+
if (info.state !== "visible") sheet.state = info.state;
|
|
1791
|
+
sheets.push(sheet);
|
|
1792
|
+
}
|
|
1793
|
+
return { sheets };
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// src/writer/xml.ts
|
|
1797
|
+
function escapeText(s) {
|
|
1798
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1799
|
+
}
|
|
1800
|
+
function escapeAttr(s) {
|
|
1801
|
+
return escapeText(s).replace(/"/g, """).replace(/\t/g, "	").replace(/\n/g, " ").replace(/\r/g, " ");
|
|
1802
|
+
}
|
|
1803
|
+
function needsPreserve(s) {
|
|
1804
|
+
return s !== s.trim();
|
|
1805
|
+
}
|
|
1806
|
+
function preserveAttr(s) {
|
|
1807
|
+
return needsPreserve(s) ? ' xml:space="preserve"' : "";
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/writer/sheet.ts
|
|
1811
|
+
var XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1812
|
+
var NS_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
1813
|
+
var NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
1814
|
+
function numberToXml(n) {
|
|
1815
|
+
return String(n);
|
|
1816
|
+
}
|
|
1817
|
+
function splitInput(col, row, input) {
|
|
1818
|
+
if (input === null || input === void 0) return { value: input, styled: void 0 };
|
|
1819
|
+
if (typeof input !== "object" || input instanceof Date) {
|
|
1820
|
+
return { value: input, styled: void 0 };
|
|
1821
|
+
}
|
|
1822
|
+
const ref = formatRef({ col, row });
|
|
1823
|
+
if (Array.isArray(input)) {
|
|
1824
|
+
throw new XlsxError("invalid-input", `cell ${ref}: an array is not a cell value`);
|
|
1825
|
+
}
|
|
1826
|
+
const record = input;
|
|
1827
|
+
for (const key of Object.keys(record)) {
|
|
1828
|
+
if (key !== "value" && key !== "style") {
|
|
1829
|
+
throw new XlsxError(
|
|
1830
|
+
"invalid-input",
|
|
1831
|
+
`cell ${ref}: a styled cell allows only "value" and "style" (got "${key}")`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (!("value" in record)) {
|
|
1836
|
+
throw new XlsxError(
|
|
1837
|
+
"invalid-input",
|
|
1838
|
+
`cell ${ref}: an object cell must be { value, style? } \u2014 did you mean a StyledCell?`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const value = record.value;
|
|
1842
|
+
if (value !== null && value !== void 0 && typeof value === "object" && !(value instanceof Date)) {
|
|
1843
|
+
throw new XlsxError(
|
|
1844
|
+
"invalid-input",
|
|
1845
|
+
`cell ${ref}: a styled cell's value cannot be an object`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
return { value, styled: input };
|
|
1849
|
+
}
|
|
1850
|
+
function renderCell(col, row, input, date1904, styles) {
|
|
1851
|
+
const { value, styled } = splitInput(col, row, input);
|
|
1852
|
+
const ref = formatRef({ col, row });
|
|
1853
|
+
let xf = 0;
|
|
1854
|
+
if (value instanceof Date) {
|
|
1855
|
+
xf = styles.xfIndexFor(styled?.style, true, ref);
|
|
1856
|
+
} else if (styled?.style !== void 0) {
|
|
1857
|
+
xf = styles.xfIndexFor(styled.style, false, ref);
|
|
1858
|
+
}
|
|
1859
|
+
const sAttr = xf === 0 ? "" : ` s="${xf}"`;
|
|
1860
|
+
if (value === null || value === void 0) {
|
|
1861
|
+
return xf === 0 ? void 0 : `<c r="${ref}"${sAttr}/>`;
|
|
1862
|
+
}
|
|
1863
|
+
if (typeof value === "string") {
|
|
1864
|
+
if (!isXmlSafe(value)) {
|
|
1865
|
+
throw new XlsxError(
|
|
1866
|
+
"invalid-input",
|
|
1867
|
+
`cell ${ref}: string contains a character not allowed in XML (a control character or lone surrogate)`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
return `<c r="${ref}"${sAttr} t="inlineStr"><is><t${preserveAttr(value)}>${escapeText(value)}</t></is></c>`;
|
|
1871
|
+
}
|
|
1872
|
+
if (typeof value === "boolean") {
|
|
1873
|
+
return `<c r="${ref}"${sAttr} t="b"><v>${value ? 1 : 0}</v></c>`;
|
|
1874
|
+
}
|
|
1875
|
+
if (typeof value === "number") {
|
|
1876
|
+
if (!Number.isFinite(value)) {
|
|
1877
|
+
throw new XlsxError("invalid-input", `cell ${ref}: ${value} is not a finite number`);
|
|
1878
|
+
}
|
|
1879
|
+
return `<c r="${ref}"${sAttr}><v>${numberToXml(value)}</v></c>`;
|
|
1880
|
+
}
|
|
1881
|
+
if (value instanceof Date) {
|
|
1882
|
+
const serial = dateToSerial(value, date1904);
|
|
1883
|
+
if (!Number.isFinite(serial)) {
|
|
1884
|
+
throw new XlsxError("invalid-input", `cell ${ref}: invalid Date`);
|
|
1885
|
+
}
|
|
1886
|
+
return `<c r="${ref}"${sAttr}><v>${numberToXml(serial)}</v></c>`;
|
|
1887
|
+
}
|
|
1888
|
+
throw new XlsxError("invalid-input", `cell ${ref}: unsupported cell value type`);
|
|
1889
|
+
}
|
|
1890
|
+
function sheetInvalid(sheetName, message) {
|
|
1891
|
+
throw new XlsxError("invalid-input", `sheet "${sheetName}": ${message}`);
|
|
1892
|
+
}
|
|
1893
|
+
function isPlainRecord(value) {
|
|
1894
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1895
|
+
const proto = Object.getPrototypeOf(value);
|
|
1896
|
+
return proto === null || proto === Object.prototype;
|
|
1897
|
+
}
|
|
1898
|
+
function checkKeys(sheetName, what, obj, allowed) {
|
|
1899
|
+
for (const key of Object.keys(obj)) {
|
|
1900
|
+
if (!allowed.includes(key))
|
|
1901
|
+
sheetInvalid(sheetName, `${what} has an unknown property "${key}"`);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function colsXml(sheetName, columns) {
|
|
1905
|
+
if (columns === void 0) return "";
|
|
1906
|
+
if (!Array.isArray(columns)) sheetInvalid(sheetName, "columns must be an array");
|
|
1907
|
+
const entries = [];
|
|
1908
|
+
for (let i = 0; i < columns.length; i++) {
|
|
1909
|
+
const raw = columns[i];
|
|
1910
|
+
const what = `columns[${i}]`;
|
|
1911
|
+
if (!isPlainRecord(raw)) sheetInvalid(sheetName, `${what} must be an object`);
|
|
1912
|
+
checkKeys(sheetName, what, raw, ["min", "max", "width", "hidden"]);
|
|
1913
|
+
const min = raw.min;
|
|
1914
|
+
const max = raw.max;
|
|
1915
|
+
if (typeof min !== "number" || !Number.isInteger(min) || typeof max !== "number" || !Number.isInteger(max) || min < 1 || max < min || max > MAX_COL) {
|
|
1916
|
+
sheetInvalid(
|
|
1917
|
+
sheetName,
|
|
1918
|
+
`${what} needs integer 1-based min \u2264 max within Excel's ${MAX_COL} columns`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
let attrs = ` min="${min}" max="${max}"`;
|
|
1922
|
+
const width = raw.width;
|
|
1923
|
+
if (width !== void 0) {
|
|
1924
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width <= 0 || width > MAX_COL_WIDTH) {
|
|
1925
|
+
sheetInvalid(sheetName, `${what}.width must be a number in (0, ${MAX_COL_WIDTH}]`);
|
|
1926
|
+
}
|
|
1927
|
+
attrs += ` width="${String(width)}" customWidth="1"`;
|
|
1928
|
+
}
|
|
1929
|
+
const hidden = raw.hidden;
|
|
1930
|
+
if (hidden !== void 0 && typeof hidden !== "boolean") {
|
|
1931
|
+
sheetInvalid(sheetName, `${what}.hidden must be a boolean`);
|
|
1932
|
+
}
|
|
1933
|
+
if (hidden === true) attrs += ' hidden="1"';
|
|
1934
|
+
if (width !== void 0 || hidden === true) entries.push(`<col${attrs}/>`);
|
|
1935
|
+
}
|
|
1936
|
+
return entries.length > 0 ? `<cols>${entries.join("")}</cols>` : "";
|
|
1937
|
+
}
|
|
1938
|
+
function rowAttrsMap(sheetName, rowProperties) {
|
|
1939
|
+
const out = /* @__PURE__ */ new Map();
|
|
1940
|
+
if (rowProperties === void 0) return out;
|
|
1941
|
+
if (!isPlainRecord(rowProperties)) sheetInvalid(sheetName, "rowProperties must be an object");
|
|
1942
|
+
for (const key of Object.keys(rowProperties)) {
|
|
1943
|
+
const rowNum = Number(key);
|
|
1944
|
+
if (!Number.isInteger(rowNum) || rowNum < 1 || rowNum > MAX_ROW) {
|
|
1945
|
+
sheetInvalid(
|
|
1946
|
+
sheetName,
|
|
1947
|
+
`rowProperties key "${key}" is not a row number within Excel's grid`
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
const raw = rowProperties[key];
|
|
1951
|
+
const what = `rowProperties[${key}]`;
|
|
1952
|
+
if (!isPlainRecord(raw)) sheetInvalid(sheetName, `${what} must be an object`);
|
|
1953
|
+
checkKeys(sheetName, what, raw, ["height", "hidden"]);
|
|
1954
|
+
let attrs = "";
|
|
1955
|
+
const height = raw.height;
|
|
1956
|
+
if (height !== void 0) {
|
|
1957
|
+
if (typeof height !== "number" || !Number.isFinite(height) || height <= 0 || height > MAX_ROW_HEIGHT) {
|
|
1958
|
+
sheetInvalid(sheetName, `${what}.height must be a number in (0, ${MAX_ROW_HEIGHT}]`);
|
|
1959
|
+
}
|
|
1960
|
+
attrs += ` ht="${String(height)}" customHeight="1"`;
|
|
1961
|
+
}
|
|
1962
|
+
const hidden = raw.hidden;
|
|
1963
|
+
if (hidden !== void 0 && typeof hidden !== "boolean") {
|
|
1964
|
+
sheetInvalid(sheetName, `${what}.hidden must be a boolean`);
|
|
1965
|
+
}
|
|
1966
|
+
if (hidden === true) attrs += ' hidden="1"';
|
|
1967
|
+
if (attrs !== "") out.set(rowNum, attrs);
|
|
1968
|
+
}
|
|
1969
|
+
return out;
|
|
1970
|
+
}
|
|
1971
|
+
function sheetViewsXml(sheetName, freeze) {
|
|
1972
|
+
if (freeze === void 0) return "";
|
|
1973
|
+
if (!isPlainRecord(freeze)) sheetInvalid(sheetName, "freeze must be an object");
|
|
1974
|
+
checkKeys(sheetName, "freeze", freeze, ["rows", "cols"]);
|
|
1975
|
+
const validate2 = (value, what, limit) => {
|
|
1976
|
+
if (value === void 0) return 0;
|
|
1977
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value >= limit) {
|
|
1978
|
+
sheetInvalid(sheetName, `freeze.${what} must be an integer in [0, ${limit})`);
|
|
1979
|
+
}
|
|
1980
|
+
return value;
|
|
1981
|
+
};
|
|
1982
|
+
const rows = validate2(freeze.rows, "rows", MAX_ROW);
|
|
1983
|
+
const cols = validate2(freeze.cols, "cols", MAX_COL);
|
|
1984
|
+
if (rows === 0 && cols === 0) return "";
|
|
1985
|
+
const splits = (cols > 0 ? ` xSplit="${cols}"` : "") + (rows > 0 ? ` ySplit="${rows}"` : "");
|
|
1986
|
+
const topLeft = formatRef({ col: cols + 1, row: rows + 1 });
|
|
1987
|
+
const activePane = rows > 0 && cols > 0 ? "bottomRight" : rows > 0 ? "bottomLeft" : "topRight";
|
|
1988
|
+
return `<sheetViews><sheetView workbookViewId="0"><pane${splits} topLeftCell="${topLeft}" activePane="${activePane}" state="frozen"/></sheetView></sheetViews>`;
|
|
1989
|
+
}
|
|
1990
|
+
var CANONICAL_CELL = /^[A-Z]{1,3}[1-9][0-9]*$/;
|
|
1991
|
+
function parseCanonicalRef(ref) {
|
|
1992
|
+
if (!CANONICAL_CELL.test(ref)) return void 0;
|
|
1993
|
+
const parsed = parseRef(ref);
|
|
1994
|
+
return parsed.col <= MAX_COL && parsed.row <= MAX_ROW ? parsed : void 0;
|
|
1995
|
+
}
|
|
1996
|
+
var shortened = (s) => s.length > 24 ? `${s.slice(0, 24)}\u2026` : s;
|
|
1997
|
+
function mergeCellsXml(sheetName, merges) {
|
|
1998
|
+
if (merges === void 0) return "";
|
|
1999
|
+
if (!Array.isArray(merges)) sheetInvalid(sheetName, "merges must be an array");
|
|
2000
|
+
const rects = [];
|
|
2001
|
+
for (let i = 0; i < merges.length; i++) {
|
|
2002
|
+
const ref = merges[i];
|
|
2003
|
+
const what = `merges[${i}]`;
|
|
2004
|
+
if (typeof ref !== "string") sheetInvalid(sheetName, `${what} must be a string`);
|
|
2005
|
+
const colon = ref.indexOf(":");
|
|
2006
|
+
const from = colon === -1 ? void 0 : parseCanonicalRef(ref.slice(0, colon));
|
|
2007
|
+
const to = colon === -1 ? void 0 : parseCanonicalRef(ref.slice(colon + 1));
|
|
2008
|
+
if (from === void 0 || to === void 0) {
|
|
2009
|
+
sheetInvalid(
|
|
2010
|
+
sheetName,
|
|
2011
|
+
`${what} "${shortened(ref)}" is not a canonical A1 range like "A1:B2" within Excel's grid`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
if (to.col < from.col || to.row < from.row) {
|
|
2015
|
+
sheetInvalid(sheetName, `${what} "${shortened(ref)}" must run top-left to bottom-right`);
|
|
2016
|
+
}
|
|
2017
|
+
if (to.col === from.col && to.row === from.row) {
|
|
2018
|
+
sheetInvalid(sheetName, `${what} "${shortened(ref)}" merges a single cell`);
|
|
2019
|
+
}
|
|
2020
|
+
rects.push({ ref, c1: from.col, r1: from.row, c2: to.col, r2: to.row });
|
|
2021
|
+
}
|
|
2022
|
+
if (rects.length === 0) return "";
|
|
2023
|
+
const sorted = [...rects].sort((a, b) => a.r1 - b.r1);
|
|
2024
|
+
const active = [];
|
|
2025
|
+
for (const rect of sorted) {
|
|
2026
|
+
let kept = 0;
|
|
2027
|
+
for (const a of active) {
|
|
2028
|
+
if (a.r2 < rect.r1) continue;
|
|
2029
|
+
active[kept++] = a;
|
|
2030
|
+
if (a.c1 <= rect.c2 && rect.c1 <= a.c2) {
|
|
2031
|
+
sheetInvalid(
|
|
2032
|
+
sheetName,
|
|
2033
|
+
`merges "${a.ref}" and "${rect.ref}" overlap \u2014 Excel repairs overlapping merges`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
active.length = kept;
|
|
2038
|
+
active.push(rect);
|
|
2039
|
+
}
|
|
2040
|
+
return `<mergeCells count="${rects.length}">${rects.map((r) => `<mergeCell ref="${r.ref}"/>`).join("")}</mergeCells>`;
|
|
2041
|
+
}
|
|
2042
|
+
function hyperlinksXml(sheetName, hyperlinks) {
|
|
2043
|
+
if (hyperlinks === void 0) return { xml: "", targets: [] };
|
|
2044
|
+
if (!Array.isArray(hyperlinks)) sheetInvalid(sheetName, "hyperlinks must be an array");
|
|
2045
|
+
const targets = [];
|
|
2046
|
+
const entries = [];
|
|
2047
|
+
for (let i = 0; i < hyperlinks.length; i++) {
|
|
2048
|
+
const raw = hyperlinks[i];
|
|
2049
|
+
const what = `hyperlinks[${i}]`;
|
|
2050
|
+
if (!isPlainRecord(raw)) sheetInvalid(sheetName, `${what} must be an object`);
|
|
2051
|
+
checkKeys(sheetName, what, raw, ["ref", "target", "location", "tooltip", "display"]);
|
|
2052
|
+
const ref = raw.ref;
|
|
2053
|
+
if (typeof ref !== "string") sheetInvalid(sheetName, `${what}.ref must be a string`);
|
|
2054
|
+
const colon = ref.indexOf(":");
|
|
2055
|
+
const from = parseCanonicalRef(colon === -1 ? ref : ref.slice(0, colon));
|
|
2056
|
+
const to = colon === -1 ? from : parseCanonicalRef(ref.slice(colon + 1));
|
|
2057
|
+
if (from === void 0 || to === void 0) {
|
|
2058
|
+
sheetInvalid(
|
|
2059
|
+
sheetName,
|
|
2060
|
+
`${what}.ref "${shortened(ref)}" is not a canonical A1 cell or range within Excel's grid`
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
if (to.col < from.col || to.row < from.row) {
|
|
2064
|
+
sheetInvalid(
|
|
2065
|
+
sheetName,
|
|
2066
|
+
`${what}.ref "${shortened(ref)}" must run top-left to bottom-right`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
const str = (key, value) => {
|
|
2070
|
+
if (value === void 0) return void 0;
|
|
2071
|
+
if (typeof value !== "string")
|
|
2072
|
+
sheetInvalid(sheetName, `${what}.${key} must be a string`);
|
|
2073
|
+
if (!isXmlSafe(value)) {
|
|
2074
|
+
sheetInvalid(
|
|
2075
|
+
sheetName,
|
|
2076
|
+
`${what}.${key} contains a character not allowed in XML (a control character or lone surrogate)`
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
return value;
|
|
2080
|
+
};
|
|
2081
|
+
const rawTarget = str("target", raw.target);
|
|
2082
|
+
const rawLocation = str("location", raw.location);
|
|
2083
|
+
const target = rawTarget === "" ? void 0 : rawTarget;
|
|
2084
|
+
const location = rawLocation === "" ? void 0 : rawLocation;
|
|
2085
|
+
if (target === void 0 && location === void 0) {
|
|
2086
|
+
sheetInvalid(
|
|
2087
|
+
sheetName,
|
|
2088
|
+
`${what} needs a target (external) and/or a location (in-workbook)`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
const tooltip = str("tooltip", raw.tooltip);
|
|
2092
|
+
const display = str("display", raw.display);
|
|
2093
|
+
let attrs = ` ref="${ref}"`;
|
|
2094
|
+
if (target !== void 0) {
|
|
2095
|
+
targets.push(target);
|
|
2096
|
+
attrs += ` r:id="rId${targets.length}"`;
|
|
2097
|
+
}
|
|
2098
|
+
if (location !== void 0) attrs += ` location="${escapeAttr(location)}"`;
|
|
2099
|
+
if (tooltip !== void 0) attrs += ` tooltip="${escapeAttr(tooltip)}"`;
|
|
2100
|
+
if (display !== void 0) attrs += ` display="${escapeAttr(display)}"`;
|
|
2101
|
+
entries.push(`<hyperlink${attrs}/>`);
|
|
2102
|
+
}
|
|
2103
|
+
if (entries.length === 0) return { xml: "", targets: [] };
|
|
2104
|
+
return { xml: `<hyperlinks>${entries.join("")}</hyperlinks>`, targets };
|
|
2105
|
+
}
|
|
2106
|
+
function worksheetXml(sheet, date1904, styles) {
|
|
2107
|
+
const rows = sheet.rows;
|
|
2108
|
+
const cols = colsXml(sheet.name, sheet.columns);
|
|
2109
|
+
const rowAttrs = rowAttrsMap(sheet.name, sheet.rowProperties);
|
|
2110
|
+
const sheetViews = sheetViewsXml(sheet.name, sheet.freeze);
|
|
2111
|
+
const mergeCells = mergeCellsXml(sheet.name, sheet.merges);
|
|
2112
|
+
const links = hyperlinksXml(sheet.name, sheet.hyperlinks);
|
|
2113
|
+
let minRow = 0;
|
|
2114
|
+
let maxRow = 0;
|
|
2115
|
+
let minCol = 0;
|
|
2116
|
+
let maxCol = 0;
|
|
2117
|
+
const rowXmls = [];
|
|
2118
|
+
if (rows.length > MAX_ROW) {
|
|
2119
|
+
throw new XlsxError("invalid-input", `a sheet cannot have more than ${MAX_ROW} rows`);
|
|
2120
|
+
}
|
|
2121
|
+
for (let r = 0; r < rows.length; r++) {
|
|
2122
|
+
const cells = rows[r];
|
|
2123
|
+
if (cells === void 0) continue;
|
|
2124
|
+
if (!Array.isArray(cells)) {
|
|
2125
|
+
throw new XlsxError(
|
|
2126
|
+
"invalid-input",
|
|
2127
|
+
`sheet row ${r + 1}: a row must be an array of cell values`
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
if (cells.length === 0) continue;
|
|
2131
|
+
if (cells.length > MAX_COL) {
|
|
2132
|
+
throw new XlsxError(
|
|
2133
|
+
"invalid-input",
|
|
2134
|
+
`sheet row ${r + 1}: a row cannot have more than ${MAX_COL} cells`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
const rowNum = r + 1;
|
|
2138
|
+
const cellXmls = [];
|
|
2139
|
+
for (let c = 0; c < cells.length; c++) {
|
|
2140
|
+
const colNum = c + 1;
|
|
2141
|
+
const rendered = renderCell(colNum, rowNum, cells[c], date1904, styles);
|
|
2142
|
+
if (rendered === void 0) continue;
|
|
2143
|
+
if (minRow === 0 || rowNum < minRow) minRow = rowNum;
|
|
2144
|
+
if (rowNum > maxRow) maxRow = rowNum;
|
|
2145
|
+
if (minCol === 0 || colNum < minCol) minCol = colNum;
|
|
2146
|
+
if (colNum > maxCol) maxCol = colNum;
|
|
2147
|
+
cellXmls.push(rendered);
|
|
2148
|
+
}
|
|
2149
|
+
if (cellXmls.length > 0) {
|
|
2150
|
+
const attrs = rowAttrs.get(rowNum) ?? "";
|
|
2151
|
+
rowAttrs.delete(rowNum);
|
|
2152
|
+
rowXmls.push([rowNum, `<row r="${rowNum}"${attrs}>${cellXmls.join("")}</row>`]);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
for (const [rowNum, attrs] of rowAttrs) {
|
|
2156
|
+
rowXmls.push([rowNum, `<row r="${rowNum}"${attrs}/>`]);
|
|
2157
|
+
}
|
|
2158
|
+
rowXmls.sort((a, b) => a[0] - b[0]);
|
|
2159
|
+
const dimension = minRow === 0 ? "A1" : minRow === maxRow && minCol === maxCol ? formatRef({ col: minCol, row: minRow }) : `${formatRef({ col: minCol, row: minRow })}:${formatRef({ col: maxCol, row: maxRow })}`;
|
|
2160
|
+
const nsR = links.targets.length > 0 ? ` xmlns:r="${NS_REL}"` : "";
|
|
2161
|
+
const xml = `${XML_DECL}
|
|
2162
|
+
<worksheet xmlns="${NS_MAIN}"${nsR}><dimension ref="${dimension}"/>${sheetViews}${cols}<sheetData>${rowXmls.map(([, x]) => x).join("")}</sheetData>${mergeCells}${links.xml}</worksheet>`;
|
|
2163
|
+
return { xml, hyperlinkTargets: links.targets };
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/writer/styles.ts
|
|
2167
|
+
var XML_DECL2 = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
2168
|
+
var NS_MAIN2 = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
2169
|
+
var DATE_NUMFMT_ID = 14;
|
|
2170
|
+
var BUILTIN_CODE_TO_ID = (() => {
|
|
2171
|
+
const map = /* @__PURE__ */ new Map();
|
|
2172
|
+
for (const [id, code] of Object.entries(BUILTIN_FORMATS)) {
|
|
2173
|
+
if (!map.has(code)) map.set(code, Number(id));
|
|
2174
|
+
}
|
|
2175
|
+
return map;
|
|
2176
|
+
})();
|
|
2177
|
+
var CUSTOM_NUMFMT_BASE = 164;
|
|
2178
|
+
function invalid(ref, message) {
|
|
2179
|
+
throw new XlsxError("invalid-input", `cell ${ref}: ${message}`);
|
|
2180
|
+
}
|
|
2181
|
+
function isPlainObject(value) {
|
|
2182
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2183
|
+
const proto = Object.getPrototypeOf(value);
|
|
2184
|
+
return proto === null || proto === Object.prototype;
|
|
2185
|
+
}
|
|
2186
|
+
function checkKeys2(ref, what, obj, allowed) {
|
|
2187
|
+
for (const key of Object.keys(obj)) {
|
|
2188
|
+
if (!allowed.includes(key)) invalid(ref, `${what} has an unknown property "${key}"`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
function validateColor(ref, what, raw) {
|
|
2192
|
+
if (!isPlainObject(raw)) invalid(ref, `${what} must be a color object`);
|
|
2193
|
+
if ("rgb" in raw) {
|
|
2194
|
+
checkKeys2(ref, what, raw, ["rgb"]);
|
|
2195
|
+
const rgb = raw.rgb;
|
|
2196
|
+
if (typeof rgb !== "string" || !HEX_COLOR.test(rgb)) {
|
|
2197
|
+
invalid(ref, `${what}.rgb must be 6- or 8-digit hex (got ${JSON.stringify(rgb)})`);
|
|
2198
|
+
}
|
|
2199
|
+
return { rgb };
|
|
2200
|
+
}
|
|
2201
|
+
if ("theme" in raw) {
|
|
2202
|
+
checkKeys2(ref, what, raw, ["theme", "tint"]);
|
|
2203
|
+
const theme = raw.theme;
|
|
2204
|
+
if (typeof theme !== "number" || !Number.isInteger(theme) || theme < 0 || theme > MAX_COLOR_INDEX) {
|
|
2205
|
+
invalid(ref, `${what}.theme must be an integer between 0 and ${MAX_COLOR_INDEX}`);
|
|
2206
|
+
}
|
|
2207
|
+
const tint = raw.tint;
|
|
2208
|
+
if (tint === void 0) return { theme };
|
|
2209
|
+
if (typeof tint !== "number" || !Number.isFinite(tint)) {
|
|
2210
|
+
invalid(ref, `${what}.tint must be a finite number`);
|
|
2211
|
+
}
|
|
2212
|
+
return { theme, tint };
|
|
2213
|
+
}
|
|
2214
|
+
if ("indexed" in raw) {
|
|
2215
|
+
checkKeys2(ref, what, raw, ["indexed"]);
|
|
2216
|
+
const indexed = raw.indexed;
|
|
2217
|
+
if (typeof indexed !== "number" || !Number.isInteger(indexed) || indexed < 0 || indexed > MAX_COLOR_INDEX) {
|
|
2218
|
+
invalid(ref, `${what}.indexed must be an integer between 0 and ${MAX_COLOR_INDEX}`);
|
|
2219
|
+
}
|
|
2220
|
+
return { indexed };
|
|
2221
|
+
}
|
|
2222
|
+
if ("auto" in raw) {
|
|
2223
|
+
checkKeys2(ref, what, raw, ["auto"]);
|
|
2224
|
+
if (raw.auto !== true) invalid(ref, `${what}.auto must be true`);
|
|
2225
|
+
return { auto: true };
|
|
2226
|
+
}
|
|
2227
|
+
invalid(ref, `${what} needs one of rgb / theme / indexed / auto`);
|
|
2228
|
+
}
|
|
2229
|
+
function validateFont(ref, raw) {
|
|
2230
|
+
if (!isPlainObject(raw)) invalid(ref, "style.font must be an object");
|
|
2231
|
+
checkKeys2(ref, "style.font", raw, [
|
|
2232
|
+
"name",
|
|
2233
|
+
"size",
|
|
2234
|
+
"bold",
|
|
2235
|
+
"italic",
|
|
2236
|
+
"underline",
|
|
2237
|
+
"strike",
|
|
2238
|
+
"color"
|
|
2239
|
+
]);
|
|
2240
|
+
const out = {};
|
|
2241
|
+
const name = raw.name;
|
|
2242
|
+
if (name !== void 0) {
|
|
2243
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
2244
|
+
invalid(ref, "style.font.name must be a non-empty string");
|
|
2245
|
+
}
|
|
2246
|
+
if (!isXmlSafe(name)) {
|
|
2247
|
+
invalid(
|
|
2248
|
+
ref,
|
|
2249
|
+
"style.font.name contains a character not allowed in XML (a control character or lone surrogate)"
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
out.name = name;
|
|
2253
|
+
}
|
|
2254
|
+
const size = raw.size;
|
|
2255
|
+
if (size !== void 0) {
|
|
2256
|
+
if (typeof size !== "number" || !Number.isFinite(size) || size <= 0) {
|
|
2257
|
+
invalid(ref, "style.font.size must be a positive number");
|
|
2258
|
+
}
|
|
2259
|
+
out.size = size;
|
|
2260
|
+
}
|
|
2261
|
+
for (const flag of ["bold", "italic", "strike"]) {
|
|
2262
|
+
const value = raw[flag];
|
|
2263
|
+
if (value !== void 0) {
|
|
2264
|
+
if (typeof value !== "boolean") invalid(ref, `style.font.${flag} must be a boolean`);
|
|
2265
|
+
if (value) out[flag] = true;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
const underline = raw.underline;
|
|
2269
|
+
if (underline !== void 0) {
|
|
2270
|
+
if (underline !== "single" && underline !== "double") {
|
|
2271
|
+
invalid(
|
|
2272
|
+
ref,
|
|
2273
|
+
`style.font.underline must be "single" or "double" (accounting variants are not supported)`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
out.underline = underline;
|
|
2277
|
+
}
|
|
2278
|
+
const color = raw.color;
|
|
2279
|
+
if (color !== void 0) out.color = validateColor(ref, "style.font.color", color);
|
|
2280
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2281
|
+
}
|
|
2282
|
+
function validateFill(ref, raw) {
|
|
2283
|
+
if (!isPlainObject(raw)) invalid(ref, "style.fill must be an object");
|
|
2284
|
+
checkKeys2(ref, "style.fill", raw, ["patternType", "fgColor", "bgColor"]);
|
|
2285
|
+
const patternType = raw.patternType;
|
|
2286
|
+
if (typeof patternType !== "string" || !PATTERN_TYPES.has(patternType)) {
|
|
2287
|
+
invalid(ref, `style.fill.patternType must be one of the OOXML pattern types`);
|
|
2288
|
+
}
|
|
2289
|
+
const rawFg = raw.fgColor;
|
|
2290
|
+
const fgColor = rawFg === void 0 ? void 0 : validateColor(ref, "style.fill.fgColor", rawFg);
|
|
2291
|
+
const rawBg = raw.bgColor;
|
|
2292
|
+
const bgColor = rawBg === void 0 ? void 0 : validateColor(ref, "style.fill.bgColor", rawBg);
|
|
2293
|
+
if (patternType === "none") {
|
|
2294
|
+
if (fgColor !== void 0 || bgColor !== void 0) {
|
|
2295
|
+
invalid(ref, 'style.fill with patternType "none" cannot carry colors');
|
|
2296
|
+
}
|
|
2297
|
+
return void 0;
|
|
2298
|
+
}
|
|
2299
|
+
const out = {
|
|
2300
|
+
patternType
|
|
2301
|
+
};
|
|
2302
|
+
if (fgColor !== void 0) out.fgColor = fgColor;
|
|
2303
|
+
if (bgColor !== void 0) out.bgColor = bgColor;
|
|
2304
|
+
return out;
|
|
2305
|
+
}
|
|
2306
|
+
function validateEdge(ref, what, raw) {
|
|
2307
|
+
if (!isPlainObject(raw)) invalid(ref, `${what} must be an object`);
|
|
2308
|
+
checkKeys2(ref, what, raw, ["style", "color"]);
|
|
2309
|
+
const style = raw.style;
|
|
2310
|
+
if (typeof style !== "string" || !BORDER_LINE_STYLES.has(style)) {
|
|
2311
|
+
invalid(ref, `${what}.style must be one of the OOXML border line styles`);
|
|
2312
|
+
}
|
|
2313
|
+
const color = raw.color;
|
|
2314
|
+
if (color === void 0) return { style };
|
|
2315
|
+
return {
|
|
2316
|
+
style,
|
|
2317
|
+
color: validateColor(ref, `${what}.color`, color)
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
function validateBorder(ref, raw) {
|
|
2321
|
+
if (!isPlainObject(raw)) invalid(ref, "style.border must be an object");
|
|
2322
|
+
checkKeys2(ref, "style.border", raw, ["top", "right", "bottom", "left"]);
|
|
2323
|
+
const out = {};
|
|
2324
|
+
for (const edge of ["top", "right", "bottom", "left"]) {
|
|
2325
|
+
const value = raw[edge];
|
|
2326
|
+
if (value !== void 0) out[edge] = validateEdge(ref, `style.border.${edge}`, value);
|
|
2327
|
+
}
|
|
2328
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2329
|
+
}
|
|
2330
|
+
function validateAlignment(ref, raw) {
|
|
2331
|
+
if (!isPlainObject(raw)) invalid(ref, "style.alignment must be an object");
|
|
2332
|
+
checkKeys2(ref, "style.alignment", raw, [
|
|
2333
|
+
"horizontal",
|
|
2334
|
+
"vertical",
|
|
2335
|
+
"wrapText",
|
|
2336
|
+
"shrinkToFit",
|
|
2337
|
+
"indent",
|
|
2338
|
+
"textRotation"
|
|
2339
|
+
]);
|
|
2340
|
+
const out = {};
|
|
2341
|
+
const horizontal = raw.horizontal;
|
|
2342
|
+
if (horizontal !== void 0) {
|
|
2343
|
+
if (typeof horizontal !== "string" || !H_ALIGNMENTS.has(horizontal)) {
|
|
2344
|
+
invalid(ref, "style.alignment.horizontal is not a valid value");
|
|
2345
|
+
}
|
|
2346
|
+
out.horizontal = horizontal;
|
|
2347
|
+
}
|
|
2348
|
+
const vertical = raw.vertical;
|
|
2349
|
+
if (vertical !== void 0) {
|
|
2350
|
+
if (typeof vertical !== "string" || !V_ALIGNMENTS.has(vertical)) {
|
|
2351
|
+
invalid(ref, "style.alignment.vertical is not a valid value");
|
|
2352
|
+
}
|
|
2353
|
+
out.vertical = vertical;
|
|
2354
|
+
}
|
|
2355
|
+
for (const flag of ["wrapText", "shrinkToFit"]) {
|
|
2356
|
+
const value = raw[flag];
|
|
2357
|
+
if (value !== void 0) {
|
|
2358
|
+
if (typeof value !== "boolean")
|
|
2359
|
+
invalid(ref, `style.alignment.${flag} must be a boolean`);
|
|
2360
|
+
if (value) out[flag] = true;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
const indent = raw.indent;
|
|
2364
|
+
if (indent !== void 0) {
|
|
2365
|
+
if (typeof indent !== "number" || !Number.isInteger(indent) || indent < 0 || indent > MAX_INDENT) {
|
|
2366
|
+
invalid(ref, `style.alignment.indent must be an integer between 0 and ${MAX_INDENT}`);
|
|
2367
|
+
}
|
|
2368
|
+
if (indent > 0) out.indent = indent;
|
|
2369
|
+
}
|
|
2370
|
+
const textRotation = raw.textRotation;
|
|
2371
|
+
if (textRotation !== void 0) {
|
|
2372
|
+
if (typeof textRotation !== "number" || !Number.isInteger(textRotation) || textRotation < 0 || textRotation > 180) {
|
|
2373
|
+
invalid(ref, "style.alignment.textRotation must be an integer between 0 and 180");
|
|
2374
|
+
}
|
|
2375
|
+
if (textRotation > 0) out.textRotation = textRotation;
|
|
2376
|
+
}
|
|
2377
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2378
|
+
}
|
|
2379
|
+
var keyOf = (o) => o === void 0 ? "" : JSON.stringify(o);
|
|
2380
|
+
function colorXml(tag, color) {
|
|
2381
|
+
if ("rgb" in color) return `<${tag} rgb="${color.rgb}"/>`;
|
|
2382
|
+
if ("theme" in color) {
|
|
2383
|
+
const tint = color.tint !== void 0 ? ` tint="${String(color.tint)}"` : "";
|
|
2384
|
+
return `<${tag} theme="${color.theme}"${tint}/>`;
|
|
2385
|
+
}
|
|
2386
|
+
if ("indexed" in color) return `<${tag} indexed="${color.indexed}"/>`;
|
|
2387
|
+
return `<${tag} auto="1"/>`;
|
|
2388
|
+
}
|
|
2389
|
+
function fontXml(font) {
|
|
2390
|
+
let out = "<font>";
|
|
2391
|
+
if (font.bold) out += "<b/>";
|
|
2392
|
+
if (font.italic) out += "<i/>";
|
|
2393
|
+
if (font.strike) out += "<strike/>";
|
|
2394
|
+
if (font.underline !== void 0) {
|
|
2395
|
+
out += font.underline === "single" ? "<u/>" : '<u val="double"/>';
|
|
2396
|
+
}
|
|
2397
|
+
if (font.size !== void 0) out += `<sz val="${String(font.size)}"/>`;
|
|
2398
|
+
if (font.color !== void 0) out += colorXml("color", font.color);
|
|
2399
|
+
if (font.name !== void 0) out += `<name val="${escapeAttr(font.name)}"/>`;
|
|
2400
|
+
return `${out}</font>`;
|
|
2401
|
+
}
|
|
2402
|
+
function fillXml(fill) {
|
|
2403
|
+
const fg = fill.fgColor !== void 0 ? colorXml("fgColor", fill.fgColor) : "";
|
|
2404
|
+
const bg = fill.bgColor !== void 0 ? colorXml("bgColor", fill.bgColor) : "";
|
|
2405
|
+
if (fg === "" && bg === "") {
|
|
2406
|
+
return `<fill><patternFill patternType="${fill.patternType}"/></fill>`;
|
|
2407
|
+
}
|
|
2408
|
+
return `<fill><patternFill patternType="${fill.patternType}">${fg}${bg}</patternFill></fill>`;
|
|
2409
|
+
}
|
|
2410
|
+
function borderXml(border) {
|
|
2411
|
+
const edge = (tag, e) => {
|
|
2412
|
+
if (e === void 0) return "";
|
|
2413
|
+
if (e.color === void 0) return `<${tag} style="${e.style}"/>`;
|
|
2414
|
+
return `<${tag} style="${e.style}">${colorXml("color", e.color)}</${tag}>`;
|
|
2415
|
+
};
|
|
2416
|
+
const inner = edge("left", border.left) + edge("right", border.right) + edge("top", border.top) + edge("bottom", border.bottom);
|
|
2417
|
+
return inner === "" ? "<border/>" : `<border>${inner}</border>`;
|
|
2418
|
+
}
|
|
2419
|
+
function alignmentXml(alignment) {
|
|
2420
|
+
let attrs = "";
|
|
2421
|
+
if (alignment.horizontal !== void 0) attrs += ` horizontal="${alignment.horizontal}"`;
|
|
2422
|
+
if (alignment.vertical !== void 0) attrs += ` vertical="${alignment.vertical}"`;
|
|
2423
|
+
if (alignment.textRotation !== void 0) attrs += ` textRotation="${alignment.textRotation}"`;
|
|
2424
|
+
if (alignment.wrapText) attrs += ' wrapText="1"';
|
|
2425
|
+
if (alignment.shrinkToFit) attrs += ' shrinkToFit="1"';
|
|
2426
|
+
if (alignment.indent !== void 0) attrs += ` indent="${alignment.indent}"`;
|
|
2427
|
+
return `<alignment${attrs}/>`;
|
|
2428
|
+
}
|
|
2429
|
+
function colorUsesTheme(color) {
|
|
2430
|
+
return color !== void 0 && "theme" in color;
|
|
2431
|
+
}
|
|
2432
|
+
function createStyleRegistry() {
|
|
2433
|
+
const fonts = ['<font><sz val="11"/><name val="Calibri"/></font>'];
|
|
2434
|
+
const fontIndex = /* @__PURE__ */ new Map([
|
|
2435
|
+
[keyOf({ name: "Calibri", size: 11 }), 0],
|
|
2436
|
+
["", 0]
|
|
2437
|
+
]);
|
|
2438
|
+
const fills = [
|
|
2439
|
+
'<fill><patternFill patternType="none"/></fill>',
|
|
2440
|
+
'<fill><patternFill patternType="gray125"/></fill>'
|
|
2441
|
+
];
|
|
2442
|
+
const fillIndex = /* @__PURE__ */ new Map([
|
|
2443
|
+
["", 0],
|
|
2444
|
+
[keyOf({ patternType: "gray125" }), 1]
|
|
2445
|
+
]);
|
|
2446
|
+
const borders = ["<border/>"];
|
|
2447
|
+
const borderIndex = /* @__PURE__ */ new Map([["", 0]]);
|
|
2448
|
+
const xfs = [
|
|
2449
|
+
{ numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, alignment: void 0 }
|
|
2450
|
+
];
|
|
2451
|
+
const xfIndex = /* @__PURE__ */ new Map([["0/0/0/0/", 0]]);
|
|
2452
|
+
const customFormats = /* @__PURE__ */ new Map();
|
|
2453
|
+
let themeUsed = false;
|
|
2454
|
+
const numFmtIdFor = (code) => {
|
|
2455
|
+
const builtin = BUILTIN_CODE_TO_ID.get(code);
|
|
2456
|
+
if (builtin !== void 0) return builtin;
|
|
2457
|
+
let id = customFormats.get(code);
|
|
2458
|
+
if (id === void 0) {
|
|
2459
|
+
id = CUSTOM_NUMFMT_BASE + customFormats.size;
|
|
2460
|
+
customFormats.set(code, id);
|
|
2461
|
+
}
|
|
2462
|
+
return id;
|
|
2463
|
+
};
|
|
2464
|
+
const internFont = (font) => {
|
|
2465
|
+
if (font === void 0) return 0;
|
|
2466
|
+
const key = keyOf(font);
|
|
2467
|
+
let index = fontIndex.get(key);
|
|
2468
|
+
if (index === void 0) {
|
|
2469
|
+
index = fonts.length;
|
|
2470
|
+
fonts.push(fontXml(font));
|
|
2471
|
+
fontIndex.set(key, index);
|
|
2472
|
+
}
|
|
2473
|
+
if (colorUsesTheme(font.color)) themeUsed = true;
|
|
2474
|
+
return index;
|
|
2475
|
+
};
|
|
2476
|
+
const internFill = (fill) => {
|
|
2477
|
+
if (fill === void 0) return 0;
|
|
2478
|
+
const key = keyOf(fill);
|
|
2479
|
+
let index = fillIndex.get(key);
|
|
2480
|
+
if (index === void 0) {
|
|
2481
|
+
index = fills.length;
|
|
2482
|
+
fills.push(fillXml(fill));
|
|
2483
|
+
fillIndex.set(key, index);
|
|
2484
|
+
}
|
|
2485
|
+
if (colorUsesTheme(fill.fgColor) || colorUsesTheme(fill.bgColor)) themeUsed = true;
|
|
2486
|
+
return index;
|
|
2487
|
+
};
|
|
2488
|
+
const internBorder = (border) => {
|
|
2489
|
+
if (border === void 0) return 0;
|
|
2490
|
+
const key = keyOf(border);
|
|
2491
|
+
let index = borderIndex.get(key);
|
|
2492
|
+
if (index === void 0) {
|
|
2493
|
+
index = borders.length;
|
|
2494
|
+
borders.push(borderXml(border));
|
|
2495
|
+
borderIndex.set(key, index);
|
|
2496
|
+
}
|
|
2497
|
+
for (const edge of [border.top, border.right, border.bottom, border.left]) {
|
|
2498
|
+
if (edge !== void 0 && colorUsesTheme(edge.color)) themeUsed = true;
|
|
2499
|
+
}
|
|
2500
|
+
return index;
|
|
2501
|
+
};
|
|
2502
|
+
function xfIndexFor(style, isDate, ref) {
|
|
2503
|
+
let fontId = 0;
|
|
2504
|
+
let fillId = 0;
|
|
2505
|
+
let borderId = 0;
|
|
2506
|
+
let alignment;
|
|
2507
|
+
let numFmtCode;
|
|
2508
|
+
if (style !== void 0) {
|
|
2509
|
+
if (!isPlainObject(style)) invalid(ref, "style must be an object");
|
|
2510
|
+
checkKeys2(ref, "style", style, [
|
|
2511
|
+
"font",
|
|
2512
|
+
"fill",
|
|
2513
|
+
"border",
|
|
2514
|
+
"alignment",
|
|
2515
|
+
"numberFormat"
|
|
2516
|
+
]);
|
|
2517
|
+
const rawCode = style.numberFormat;
|
|
2518
|
+
if (rawCode !== void 0) {
|
|
2519
|
+
if (typeof rawCode !== "string" || rawCode.length === 0) {
|
|
2520
|
+
invalid(ref, "style.numberFormat must be a non-empty format code string");
|
|
2521
|
+
}
|
|
2522
|
+
if (!isXmlSafe(rawCode)) {
|
|
2523
|
+
invalid(
|
|
2524
|
+
ref,
|
|
2525
|
+
"style.numberFormat contains a character not allowed in XML (a control character or lone surrogate)"
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
numFmtCode = rawCode;
|
|
2529
|
+
}
|
|
2530
|
+
const font = style.font;
|
|
2531
|
+
fontId = internFont(font === void 0 ? void 0 : validateFont(ref, font));
|
|
2532
|
+
const fill = style.fill;
|
|
2533
|
+
fillId = internFill(fill === void 0 ? void 0 : validateFill(ref, fill));
|
|
2534
|
+
const border = style.border;
|
|
2535
|
+
borderId = internBorder(border === void 0 ? void 0 : validateBorder(ref, border));
|
|
2536
|
+
const align = style.alignment;
|
|
2537
|
+
alignment = align === void 0 ? void 0 : validateAlignment(ref, align);
|
|
2538
|
+
}
|
|
2539
|
+
let numFmtId = 0;
|
|
2540
|
+
if (numFmtCode !== void 0) numFmtId = numFmtIdFor(numFmtCode);
|
|
2541
|
+
else if (isDate) numFmtId = DATE_NUMFMT_ID;
|
|
2542
|
+
const key = `${numFmtId}/${fontId}/${fillId}/${borderId}/${keyOf(alignment)}`;
|
|
2543
|
+
let index = xfIndex.get(key);
|
|
2544
|
+
if (index === void 0) {
|
|
2545
|
+
index = xfs.length;
|
|
2546
|
+
xfs.push({ numFmtId, fontId, fillId, borderId, alignment });
|
|
2547
|
+
xfIndex.set(key, index);
|
|
2548
|
+
}
|
|
2549
|
+
return index;
|
|
2550
|
+
}
|
|
2551
|
+
function xfXml(xf) {
|
|
2552
|
+
let attrs = `numFmtId="${xf.numFmtId}" fontId="${xf.fontId}" fillId="${xf.fillId}" borderId="${xf.borderId}" xfId="0"`;
|
|
2553
|
+
if (xf.numFmtId !== 0) attrs += ' applyNumberFormat="1"';
|
|
2554
|
+
if (xf.fontId !== 0) attrs += ' applyFont="1"';
|
|
2555
|
+
if (xf.fillId >= 2) attrs += ' applyFill="1"';
|
|
2556
|
+
if (xf.borderId !== 0) attrs += ' applyBorder="1"';
|
|
2557
|
+
if (xf.alignment !== void 0) {
|
|
2558
|
+
return `<xf ${attrs} applyAlignment="1">${alignmentXml(xf.alignment)}</xf>`;
|
|
2559
|
+
}
|
|
2560
|
+
return `<xf ${attrs}/>`;
|
|
2561
|
+
}
|
|
2562
|
+
function stylesXml() {
|
|
2563
|
+
const numFmts = customFormats.size === 0 ? "" : `<numFmts count="${customFormats.size}">${[...customFormats].map(
|
|
2564
|
+
([code, id]) => `<numFmt numFmtId="${id}" formatCode="${escapeAttr(code)}"/>`
|
|
2565
|
+
).join("")}</numFmts>`;
|
|
2566
|
+
return `${XML_DECL2}
|
|
2567
|
+
<styleSheet xmlns="${NS_MAIN2}">${numFmts}<fonts count="${fonts.length}">${fonts.join("")}</fonts><fills count="${fills.length}">${fills.join("")}</fills><borders count="${borders.length}">${borders.join("")}</borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${xfs.length}">${xfs.map(xfXml).join("")}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
|
|
2568
|
+
}
|
|
2569
|
+
return {
|
|
2570
|
+
xfIndexFor,
|
|
2571
|
+
needed: () => xfs.length > 1,
|
|
2572
|
+
usesTheme: () => themeUsed,
|
|
2573
|
+
stylesXml
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// src/writer/theme.ts
|
|
2578
|
+
var DEFAULT_THEME_XML = '<?xml version="1.0"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="EEECE1"/></a:lt2><a:accent1><a:srgbClr val="4F81BD"/></a:accent1><a:accent2><a:srgbClr val="C0504D"/></a:accent2><a:accent3><a:srgbClr val="9BBB59"/></a:accent3><a:accent4><a:srgbClr val="8064A2"/></a:accent4><a:accent5><a:srgbClr val="4BACC6"/></a:accent5><a:accent6><a:srgbClr val="F79646"/></a:accent6><a:hlink><a:srgbClr val="0000FF"/></a:hlink><a:folHlink><a:srgbClr val="800080"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Cambria"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="MS Pゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="MS Pゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="94000"/><a:satMod val="135000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst><a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>';
|
|
2579
|
+
|
|
2580
|
+
// src/writer/zip.ts
|
|
2581
|
+
var encoder = new TextEncoder();
|
|
2582
|
+
var SIG_LOCAL2 = 67324752;
|
|
2583
|
+
var SIG_CENTRAL2 = 33639248;
|
|
2584
|
+
var SIG_EOCD2 = 101010256;
|
|
2585
|
+
var METHOD_STORE = 0;
|
|
2586
|
+
var METHOD_DEFLATE = 8;
|
|
2587
|
+
var VERSION = 20;
|
|
2588
|
+
var U32_CEILING = 4294967295;
|
|
2589
|
+
var MAX_ENTRIES = 65535;
|
|
2590
|
+
var DOS_TIME = 0;
|
|
2591
|
+
var DOS_DATE = 33;
|
|
2592
|
+
var u16 = (n) => Uint8Array.from([n & 255, n >>> 8 & 255]);
|
|
2593
|
+
var u32 = (n) => Uint8Array.from([n & 255, n >>> 8 & 255, n >>> 16 & 255, n >>> 24 & 255]);
|
|
2594
|
+
function concat(parts) {
|
|
2595
|
+
let total = 0;
|
|
2596
|
+
for (const part of parts) total += part.length;
|
|
2597
|
+
const out = new Uint8Array(total);
|
|
2598
|
+
let offset = 0;
|
|
2599
|
+
for (const part of parts) {
|
|
2600
|
+
out.set(part, offset);
|
|
2601
|
+
offset += part.length;
|
|
2602
|
+
}
|
|
2603
|
+
return out;
|
|
2604
|
+
}
|
|
2605
|
+
async function writeZip(entries) {
|
|
2606
|
+
if (entries.length >= MAX_ENTRIES) {
|
|
2607
|
+
throw new XlsxError(
|
|
2608
|
+
"unsupported",
|
|
2609
|
+
`too many zip entries (${entries.length}); would require ZIP64, which is not supported`
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2613
|
+
const local = [];
|
|
2614
|
+
const central = [];
|
|
2615
|
+
let offset = 0;
|
|
2616
|
+
for (const entry of entries) {
|
|
2617
|
+
if (entry.name.endsWith("/")) {
|
|
2618
|
+
throw new Error(`invalid zip entry name (directory placeholder): ${entry.name}`);
|
|
2619
|
+
}
|
|
2620
|
+
if (seen.has(entry.name)) throw new Error(`duplicate zip entry name: ${entry.name}`);
|
|
2621
|
+
seen.add(entry.name);
|
|
2622
|
+
const name = encoder.encode(entry.name);
|
|
2623
|
+
if (name.length > 65535) {
|
|
2624
|
+
throw new XlsxError(
|
|
2625
|
+
"unsupported",
|
|
2626
|
+
`zip entry name too long (${name.length} bytes); exceeds the classic-ZIP name-length field`
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
const data = entry.data;
|
|
2630
|
+
const crc = crc32(data);
|
|
2631
|
+
const uncompressedSize = data.length;
|
|
2632
|
+
const deflated = await deflateRaw(data);
|
|
2633
|
+
const useDeflate = deflated.length < data.length;
|
|
2634
|
+
const method = useDeflate ? METHOD_DEFLATE : METHOD_STORE;
|
|
2635
|
+
const payload = useDeflate ? deflated : data;
|
|
2636
|
+
const compressedSize = payload.length;
|
|
2637
|
+
if (uncompressedSize >= U32_CEILING || compressedSize >= U32_CEILING || offset >= U32_CEILING) {
|
|
2638
|
+
throw new XlsxError(
|
|
2639
|
+
"unsupported",
|
|
2640
|
+
`zip entry ${entry.name} too large for a classic (ZIP64-free) archive`
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
const header = concat([
|
|
2644
|
+
u32(SIG_LOCAL2),
|
|
2645
|
+
u16(VERSION),
|
|
2646
|
+
// version needed to extract
|
|
2647
|
+
u16(0),
|
|
2648
|
+
// general-purpose flags: none (sizes/CRC are in the header, no data descriptor)
|
|
2649
|
+
u16(method),
|
|
2650
|
+
u16(DOS_TIME),
|
|
2651
|
+
u16(DOS_DATE),
|
|
2652
|
+
u32(crc),
|
|
2653
|
+
u32(compressedSize),
|
|
2654
|
+
u32(uncompressedSize),
|
|
2655
|
+
u16(name.length),
|
|
2656
|
+
u16(0),
|
|
2657
|
+
// extra field length
|
|
2658
|
+
name
|
|
2659
|
+
]);
|
|
2660
|
+
local.push(header, payload);
|
|
2661
|
+
central.push(
|
|
2662
|
+
concat([
|
|
2663
|
+
u32(SIG_CENTRAL2),
|
|
2664
|
+
u16(VERSION),
|
|
2665
|
+
// version made by
|
|
2666
|
+
u16(VERSION),
|
|
2667
|
+
// version needed to extract
|
|
2668
|
+
u16(0),
|
|
2669
|
+
// flags
|
|
2670
|
+
u16(method),
|
|
2671
|
+
u16(DOS_TIME),
|
|
2672
|
+
u16(DOS_DATE),
|
|
2673
|
+
u32(crc),
|
|
2674
|
+
u32(compressedSize),
|
|
2675
|
+
u32(uncompressedSize),
|
|
2676
|
+
u16(name.length),
|
|
2677
|
+
u16(0),
|
|
2678
|
+
// extra field length
|
|
2679
|
+
u16(0),
|
|
2680
|
+
// file comment length
|
|
2681
|
+
u16(0),
|
|
2682
|
+
// disk number start
|
|
2683
|
+
u16(0),
|
|
2684
|
+
// internal file attributes
|
|
2685
|
+
u32(0),
|
|
2686
|
+
// external file attributes
|
|
2687
|
+
u32(offset),
|
|
2688
|
+
// relative offset of the local header
|
|
2689
|
+
name
|
|
2690
|
+
])
|
|
2691
|
+
);
|
|
2692
|
+
offset += header.length + payload.length;
|
|
2693
|
+
}
|
|
2694
|
+
const directory = concat(central);
|
|
2695
|
+
if (offset >= U32_CEILING || directory.length >= U32_CEILING) {
|
|
2696
|
+
throw new XlsxError(
|
|
2697
|
+
"unsupported",
|
|
2698
|
+
"zip archive too large for a classic (ZIP64-free) archive"
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
const eocd = concat([
|
|
2702
|
+
u32(SIG_EOCD2),
|
|
2703
|
+
u16(0),
|
|
2704
|
+
// number of this disk
|
|
2705
|
+
u16(0),
|
|
2706
|
+
// disk where the central directory starts
|
|
2707
|
+
u16(entries.length),
|
|
2708
|
+
// central-directory entries on this disk
|
|
2709
|
+
u16(entries.length),
|
|
2710
|
+
// total central-directory entries
|
|
2711
|
+
u32(directory.length),
|
|
2712
|
+
// size of the central directory
|
|
2713
|
+
u32(offset),
|
|
2714
|
+
// offset of the central directory from the start of the archive
|
|
2715
|
+
u16(0)
|
|
2716
|
+
// comment length
|
|
2717
|
+
]);
|
|
2718
|
+
return concat([...local, directory, eocd]);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// src/writer/workbook.ts
|
|
2722
|
+
var encoder2 = new TextEncoder();
|
|
2723
|
+
var XML_DECL3 = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
2724
|
+
var NS_MAIN3 = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
2725
|
+
var NS_REL2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
2726
|
+
var NS_PKG_REL = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
2727
|
+
var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
2728
|
+
var CT_BASE = "application/vnd.openxmlformats-officedocument.spreadsheetml";
|
|
2729
|
+
var CT_RELS = "application/vnd.openxmlformats-package.relationships+xml";
|
|
2730
|
+
var MAX_SHEET_NAME = 31;
|
|
2731
|
+
var FORBIDDEN_SHEET_NAME = /[\\/?*[\]:]/;
|
|
2732
|
+
function validate(workbook) {
|
|
2733
|
+
const sheets = workbook?.sheets;
|
|
2734
|
+
if (!Array.isArray(sheets) || sheets.length === 0) {
|
|
2735
|
+
throw new XlsxError("invalid-input", "a workbook needs at least one sheet");
|
|
2736
|
+
}
|
|
2737
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2738
|
+
const states = [];
|
|
2739
|
+
let anyVisible = false;
|
|
2740
|
+
for (const sheet of sheets) {
|
|
2741
|
+
const name = sheet?.name;
|
|
2742
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
2743
|
+
throw new XlsxError("invalid-input", "a sheet name must be a non-empty string");
|
|
2744
|
+
}
|
|
2745
|
+
if (name.length > MAX_SHEET_NAME) {
|
|
2746
|
+
throw new XlsxError(
|
|
2747
|
+
"invalid-input",
|
|
2748
|
+
`sheet name "${name}" exceeds ${MAX_SHEET_NAME} characters`
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (FORBIDDEN_SHEET_NAME.test(name)) {
|
|
2752
|
+
throw new XlsxError(
|
|
2753
|
+
"invalid-input",
|
|
2754
|
+
`sheet name "${name}" contains a forbidden character (\\ / ? * [ ] :)`
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
if (!isXmlSafe(name)) {
|
|
2758
|
+
throw new XlsxError(
|
|
2759
|
+
"invalid-input",
|
|
2760
|
+
`sheet name "${name}" contains a character not allowed in XML`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
const key = name.toLowerCase();
|
|
2764
|
+
if (seen.has(key)) {
|
|
2765
|
+
throw new XlsxError(
|
|
2766
|
+
"invalid-input",
|
|
2767
|
+
`duplicate sheet name "${name}" (case-insensitive)`
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
seen.add(key);
|
|
2771
|
+
if (!Array.isArray(sheet.rows)) {
|
|
2772
|
+
throw new XlsxError("invalid-input", `sheet "${name}": rows must be an array`);
|
|
2773
|
+
}
|
|
2774
|
+
const state = sheet.state ?? "visible";
|
|
2775
|
+
if (state !== "visible" && state !== "hidden" && state !== "veryHidden") {
|
|
2776
|
+
throw new XlsxError(
|
|
2777
|
+
"invalid-input",
|
|
2778
|
+
`sheet "${name}": state must be "visible", "hidden", or "veryHidden"`
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
states.push(state);
|
|
2782
|
+
if (state === "visible") anyVisible = true;
|
|
2783
|
+
}
|
|
2784
|
+
if (!anyVisible) {
|
|
2785
|
+
throw new XlsxError("invalid-input", "at least one sheet must be visible");
|
|
2786
|
+
}
|
|
2787
|
+
return states;
|
|
2788
|
+
}
|
|
2789
|
+
async function writeXlsx(workbook, options) {
|
|
2790
|
+
const states = validate(workbook);
|
|
2791
|
+
const date1904 = options?.date1904 === true;
|
|
2792
|
+
const sheets = workbook.sheets;
|
|
2793
|
+
const styles = createStyleRegistry();
|
|
2794
|
+
const worksheets = sheets.map((sheet) => worksheetXml(sheet, date1904, styles));
|
|
2795
|
+
const needStyles = styles.needed();
|
|
2796
|
+
const needTheme = styles.usesTheme();
|
|
2797
|
+
const parts = [];
|
|
2798
|
+
const add = (name, xml) => {
|
|
2799
|
+
parts.push({ name, data: encoder2.encode(xml) });
|
|
2800
|
+
};
|
|
2801
|
+
const overrides = [
|
|
2802
|
+
`<Override PartName="/xl/workbook.xml" ContentType="${CT_BASE}.sheet.main+xml"/>`,
|
|
2803
|
+
...sheets.map(
|
|
2804
|
+
(_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="${CT_BASE}.worksheet+xml"/>`
|
|
2805
|
+
),
|
|
2806
|
+
...needStyles ? [`<Override PartName="/xl/styles.xml" ContentType="${CT_BASE}.styles+xml"/>`] : [],
|
|
2807
|
+
...needTheme ? [
|
|
2808
|
+
'<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>'
|
|
2809
|
+
] : []
|
|
2810
|
+
].join("");
|
|
2811
|
+
add(
|
|
2812
|
+
"[Content_Types].xml",
|
|
2813
|
+
`${XML_DECL3}
|
|
2814
|
+
<Types xmlns="${NS_CT}"><Default Extension="rels" ContentType="${CT_RELS}"/><Default Extension="xml" ContentType="application/xml"/>${overrides}</Types>`
|
|
2815
|
+
);
|
|
2816
|
+
add(
|
|
2817
|
+
"_rels/.rels",
|
|
2818
|
+
`${XML_DECL3}
|
|
2819
|
+
<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_REL2}/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
2820
|
+
);
|
|
2821
|
+
const workbookPr = date1904 ? '<workbookPr date1904="1"/>' : "";
|
|
2822
|
+
const firstVisible = states.indexOf("visible");
|
|
2823
|
+
const bookViews = firstVisible > 0 ? `<bookViews><workbookView activeTab="${firstVisible}"/></bookViews>` : "";
|
|
2824
|
+
const sheetsXml = sheets.map((sheet, i) => {
|
|
2825
|
+
const state = states[i] === "visible" ? "" : ` state="${states[i]}"`;
|
|
2826
|
+
return `<sheet name="${escapeAttr(sheet.name)}" sheetId="${i + 1}"${state} r:id="rId${i + 1}"/>`;
|
|
2827
|
+
}).join("");
|
|
2828
|
+
add(
|
|
2829
|
+
"xl/workbook.xml",
|
|
2830
|
+
`${XML_DECL3}
|
|
2831
|
+
<workbook xmlns="${NS_MAIN3}" xmlns:r="${NS_REL2}">${workbookPr}${bookViews}<sheets>${sheetsXml}</sheets></workbook>`
|
|
2832
|
+
);
|
|
2833
|
+
const relItems = [
|
|
2834
|
+
...sheets.map(
|
|
2835
|
+
(_, i) => `<Relationship Id="rId${i + 1}" Type="${NS_REL2}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`
|
|
2836
|
+
),
|
|
2837
|
+
...needStyles ? [
|
|
2838
|
+
`<Relationship Id="rId${sheets.length + 1}" Type="${NS_REL2}/styles" Target="styles.xml"/>`
|
|
2839
|
+
] : [],
|
|
2840
|
+
...needTheme ? [
|
|
2841
|
+
`<Relationship Id="rId${sheets.length + (needStyles ? 2 : 1)}" Type="${NS_REL2}/theme" Target="theme/theme1.xml"/>`
|
|
2842
|
+
] : []
|
|
2843
|
+
].join("");
|
|
2844
|
+
add(
|
|
2845
|
+
"xl/_rels/workbook.xml.rels",
|
|
2846
|
+
`${XML_DECL3}
|
|
2847
|
+
<Relationships xmlns="${NS_PKG_REL}">${relItems}</Relationships>`
|
|
2848
|
+
);
|
|
2849
|
+
if (needStyles) add("xl/styles.xml", styles.stylesXml());
|
|
2850
|
+
if (needTheme) add("xl/theme/theme1.xml", DEFAULT_THEME_XML);
|
|
2851
|
+
worksheets.forEach((w, i) => {
|
|
2852
|
+
add(`xl/worksheets/sheet${i + 1}.xml`, w.xml);
|
|
2853
|
+
if (w.hyperlinkTargets.length > 0) {
|
|
2854
|
+
const rels = w.hyperlinkTargets.map(
|
|
2855
|
+
(target, j) => `<Relationship Id="rId${j + 1}" Type="${NS_REL2}/hyperlink" Target="${escapeAttr(target)}" TargetMode="External"/>`
|
|
2856
|
+
).join("");
|
|
2857
|
+
add(
|
|
2858
|
+
`xl/worksheets/_rels/sheet${i + 1}.xml.rels`,
|
|
2859
|
+
`${XML_DECL3}
|
|
2860
|
+
<Relationships xmlns="${NS_PKG_REL}">${rels}</Relationships>`
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
return writeZip(parts);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
export { Workbook, Worksheet, XlsxError, columnToIndex, dateToSerial, formatRef, indexToColumn, openXlsx, parseRef, serialToDate, streamSheetRows, workbookToInput, writeXlsx };
|