@openjsxl/core 0.3.0 → 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/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;
@@ -100,6 +104,23 @@ function decodeCell(raw, ctx) {
100
104
  function isWhitespace(ch) {
101
105
  return ch === " " || ch === " " || ch === "\n" || ch === "\r";
102
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
+ }
103
124
 
104
125
  // src/utils/xml-names.ts
105
126
  function localName(name) {
@@ -433,50 +454,327 @@ function isBuiltinDateId(id) {
433
454
  return id >= 14 && id <= 22 || id >= 27 && id <= 36 || id >= 45 && id <= 47 || id >= 50 && id <= 58;
434
455
  }
435
456
  var ELAPSED_TIME = /\[(?:h+|m+|s+)\]/i;
436
- var NON_TOKEN = /\[[^\]]*\]|"[^"]*"|\\./g;
457
+ var LITERALS = /"[^"]*"|\\.|[_*]./g;
458
+ var BRACKETS = /\[[^\]]*\]/g;
437
459
  var DATE_TOKEN = /[dmyhs]/i;
438
460
  function isDateFormatCode(formatCode) {
439
- if (ELAPSED_TIME.test(formatCode)) return true;
440
- return DATE_TOKEN.test(formatCode.replace(NON_TOKEN, ""));
461
+ const withoutLiterals = formatCode.replace(LITERALS, "");
462
+ if (ELAPSED_TIME.test(withoutLiterals)) return true;
463
+ return DATE_TOKEN.test(withoutLiterals.replace(BRACKETS, ""));
441
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;
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;
442
564
  function parseStyles(xml) {
443
565
  const customFormats = /* @__PURE__ */ new Map();
444
- const cellFormatIds = [];
566
+ const xfs = [];
567
+ const fonts = [];
568
+ const fills = [];
569
+ const borders = [];
445
570
  let inNumFmts = false;
446
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
+ };
447
591
  for (const token of tokenize(xml)) {
448
592
  if (token.kind === "text") continue;
449
593
  const name = localName(token.name);
450
594
  if (token.kind === "open") {
451
595
  if (name === "numFmts") {
452
596
  if (!token.selfClosing) inNumFmts = true;
453
- } else if (name === "cellXfs") {
454
- if (!token.selfClosing) inCellXfs = true;
455
597
  } else if (name === "numFmt" && inNumFmts) {
456
598
  const id = Number(token.attrs.numFmtId);
457
599
  const code = token.attrs.formatCode;
458
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;
459
669
  } else if (name === "xf" && inCellXfs) {
460
- const id = Number(token.attrs.numFmtId ?? "0");
461
- cellFormatIds.push(Number.isInteger(id) ? id : 0);
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
+ }
462
688
  }
463
689
  } else if (token.kind === "close") {
464
690
  if (name === "numFmts") inNumFmts = false;
465
- else if (name === "cellXfs") inCellXfs = false;
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;
466
734
  }
467
735
  }
468
736
  function isDateStyle(styleIndex) {
469
- const numFmtId = cellFormatIds[styleIndex ?? 0];
737
+ const numFmtId = xfs[styleIndex ?? 0]?.numFmtId;
470
738
  if (numFmtId === void 0) return false;
471
739
  const custom = customFormats.get(numFmtId);
472
740
  return custom !== void 0 ? isDateFormatCode(custom) : isBuiltinDateId(numFmtId);
473
741
  }
474
742
  function formatCode(styleIndex) {
475
- const numFmtId = cellFormatIds[styleIndex ?? 0];
743
+ const numFmtId = xfs[styleIndex ?? 0]?.numFmtId;
476
744
  if (numFmtId === void 0) return void 0;
477
745
  return customFormats.get(numFmtId) ?? BUILTIN_FORMATS[numFmtId];
478
746
  }
479
- return { isDateStyle, formatCode };
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 };
480
778
  }
481
779
 
482
780
  // src/ooxml/workbook.ts
@@ -493,8 +791,9 @@ function parseWorkbook(xml) {
493
791
  const name = token.attrs.name;
494
792
  const rid = relationshipId(token.attrs);
495
793
  if (name === void 0 || rid === void 0) continue;
496
- const state = token.attrs.state;
497
- sheets.push({ name, rid, visible: state !== "hidden" && state !== "veryHidden" });
794
+ const raw = token.attrs.state;
795
+ const state = raw === "hidden" || raw === "veryHidden" ? raw : "visible";
796
+ sheets.push({ name, rid, visible: state === "visible", state });
498
797
  }
499
798
  }
500
799
  return { sheets, date1904 };
@@ -1012,7 +1311,10 @@ function parseHyperlinks(xml, rels) {
1012
1311
  const display = token.attrs.display;
1013
1312
  links.push({
1014
1313
  ref,
1015
- ...target !== void 0 ? { target } : {},
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 } : {},
1016
1318
  ...location !== void 0 && location !== "" ? { location } : {},
1017
1319
  ...tooltip !== void 0 ? { tooltip } : {},
1018
1320
  ...display !== void 0 ? { display } : {}
@@ -1034,6 +1336,85 @@ async function* streamRows(chunks, ctx) {
1034
1336
  for (const token of xml.flush()) yield* assembler.push(token);
1035
1337
  yield* assembler.flush();
1036
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
+ }
1037
1418
 
1038
1419
  // src/reader/workbook.ts
1039
1420
  var decoder = new TextDecoder();
@@ -1071,6 +1452,10 @@ var Worksheet = class {
1071
1452
  #dimension;
1072
1453
  #dimensionRead = false;
1073
1454
  #comments;
1455
+ #columns;
1456
+ #rowProps;
1457
+ #freeze;
1458
+ #freezeRead = false;
1074
1459
  constructor(info, xml, context, rels, commentsXml) {
1075
1460
  this.name = info.name;
1076
1461
  this.#info = info;
@@ -1087,6 +1472,10 @@ var Worksheet = class {
1087
1472
  get visible() {
1088
1473
  return this.#info.visible;
1089
1474
  }
1475
+ /** The tab's visibility state (F4.6): `"visible"`, `"hidden"`, or `"veryHidden"`. */
1476
+ get state() {
1477
+ return this.#info.state;
1478
+ }
1090
1479
  /**
1091
1480
  * Merged-cell ranges in A1 notation (e.g. `['A1:B1', 'A2:A4']`), in document order. Only the
1092
1481
  * top-left cell of a merge holds a value; the rest read as `empty`. Empty when none.
@@ -1115,6 +1504,17 @@ var Worksheet = class {
1115
1504
  numberFormat(ref) {
1116
1505
  return this.#context.styles?.formatCode(this.#cellStyleMap().get(ref));
1117
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
+ }
1118
1518
  /**
1119
1519
  * The sheet's declared used range in A1 notation (e.g. `"A1:E10"`, or a single cell), from
1120
1520
  * the worksheet's `<dimension>`. `undefined` when the producer omits it — it is an optional
@@ -1137,6 +1537,34 @@ var Worksheet = class {
1137
1537
  }
1138
1538
  return this.#comments;
1139
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
+ }
1140
1568
  #cellStyleMap() {
1141
1569
  if (this.#cellStyles === void 0) this.#cellStyles = parseCellStyles(this.#xml);
1142
1570
  return this.#cellStyles;
@@ -1218,7 +1646,10 @@ async function loadWorkbook(source, options) {
1218
1646
  if (rel === void 0 || rel.targetMode === "External") continue;
1219
1647
  const path = resolveTarget(workbookDir, rel.target);
1220
1648
  if (!zip.has(path)) continue;
1221
- sheets.push({ info: { name: entry.name, path, visible: entry.visible }, path });
1649
+ sheets.push({
1650
+ info: { name: entry.name, path, visible: entry.visible, state: entry.state },
1651
+ path
1652
+ });
1222
1653
  }
1223
1654
  return { zip, context, sheets };
1224
1655
  }
@@ -1302,26 +1733,62 @@ async function deflateRaw(data) {
1302
1733
  }
1303
1734
 
1304
1735
  // src/writer/from-workbook.ts
1305
- function cellToValue(cell) {
1306
- return cell.value;
1736
+ function cellToInput(worksheet, cell) {
1737
+ const style = worksheet.style(cell.ref);
1738
+ return style === void 0 ? cell.value : { value: cell.value, style };
1307
1739
  }
1308
1740
  async function workbookToInput(workbook) {
1309
1741
  const sheets = [];
1310
1742
  for (const info of workbook.sheets) {
1311
1743
  const worksheet = workbook.sheet(info.name);
1312
1744
  const rows = [];
1745
+ const occupied = /* @__PURE__ */ new Map();
1313
1746
  for await (const row of worksheet.rows()) {
1314
1747
  for (const cell of row.cells) {
1315
- const { col, row: rowNum } = parseRef(cell.ref);
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;
1316
1771
  let rowArr = rows[rowNum - 1];
1317
1772
  if (rowArr === void 0) {
1318
1773
  rowArr = [];
1319
1774
  rows[rowNum - 1] = rowArr;
1320
1775
  }
1321
- rowArr[col - 1] = cellToValue(cell);
1776
+ rowArr[col - 1] = cellToInput(worksheet, cell);
1322
1777
  }
1323
1778
  }
1324
- sheets.push({ name: info.name, rows });
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);
1325
1792
  }
1326
1793
  return { sheets };
1327
1794
  }
@@ -1331,24 +1798,7 @@ function escapeText(s) {
1331
1798
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1332
1799
  }
1333
1800
  function escapeAttr(s) {
1334
- return escapeText(s).replace(/"/g, "&quot;");
1335
- }
1336
- function isXmlSafe(s) {
1337
- for (let i = 0; i < s.length; i++) {
1338
- const c = s.charCodeAt(i);
1339
- if (c < 32) {
1340
- if (c !== 9 && c !== 10 && c !== 13) return false;
1341
- } else if (c === 65534 || c === 65535) {
1342
- return false;
1343
- } else if (c >= 55296 && c <= 56319) {
1344
- const next = s.charCodeAt(i + 1);
1345
- if (!(next >= 56320 && next <= 57343)) return false;
1346
- i++;
1347
- } else if (c >= 56320 && c <= 57343) {
1348
- return false;
1349
- }
1350
- }
1351
- return true;
1801
+ return escapeText(s).replace(/"/g, "&quot;").replace(/\t/g, "&#9;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;");
1352
1802
  }
1353
1803
  function needsPreserve(s) {
1354
1804
  return s !== s.trim();
@@ -1360,13 +1810,56 @@ function preserveAttr(s) {
1360
1810
  // src/writer/sheet.ts
1361
1811
  var XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
1362
1812
  var NS_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
1363
- var DATE_STYLE_INDEX = 1;
1813
+ var NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
1364
1814
  function numberToXml(n) {
1365
1815
  return String(n);
1366
1816
  }
1367
- function renderCell(col, row, value, date1904) {
1368
- if (value === null || value === void 0) return void 0;
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);
1369
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
+ }
1370
1863
  if (typeof value === "string") {
1371
1864
  if (!isXmlSafe(value)) {
1372
1865
  throw new XlsxError(
@@ -1374,39 +1867,257 @@ function renderCell(col, row, value, date1904) {
1374
1867
  `cell ${ref}: string contains a character not allowed in XML (a control character or lone surrogate)`
1375
1868
  );
1376
1869
  }
1377
- return {
1378
- xml: `<c r="${ref}" t="inlineStr"><is><t${preserveAttr(value)}>${escapeText(value)}</t></is></c>`,
1379
- isDate: false
1380
- };
1870
+ return `<c r="${ref}"${sAttr} t="inlineStr"><is><t${preserveAttr(value)}>${escapeText(value)}</t></is></c>`;
1381
1871
  }
1382
1872
  if (typeof value === "boolean") {
1383
- return { xml: `<c r="${ref}" t="b"><v>${value ? 1 : 0}</v></c>`, isDate: false };
1873
+ return `<c r="${ref}"${sAttr} t="b"><v>${value ? 1 : 0}</v></c>`;
1384
1874
  }
1385
1875
  if (typeof value === "number") {
1386
1876
  if (!Number.isFinite(value)) {
1387
1877
  throw new XlsxError("invalid-input", `cell ${ref}: ${value} is not a finite number`);
1388
1878
  }
1389
- return { xml: `<c r="${ref}"><v>${numberToXml(value)}</v></c>`, isDate: false };
1879
+ return `<c r="${ref}"${sAttr}><v>${numberToXml(value)}</v></c>`;
1390
1880
  }
1391
1881
  if (value instanceof Date) {
1392
1882
  const serial = dateToSerial(value, date1904);
1393
1883
  if (!Number.isFinite(serial)) {
1394
1884
  throw new XlsxError("invalid-input", `cell ${ref}: invalid Date`);
1395
1885
  }
1396
- return {
1397
- xml: `<c r="${ref}" s="${DATE_STYLE_INDEX}"><v>${numberToXml(serial)}</v></c>`,
1398
- isDate: true
1399
- };
1886
+ return `<c r="${ref}"${sAttr}><v>${numberToXml(serial)}</v></c>`;
1400
1887
  }
1401
1888
  throw new XlsxError("invalid-input", `cell ${ref}: unsupported cell value type`);
1402
1889
  }
1403
- function worksheetXml(rows, date1904) {
1404
- let usesDate = false;
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);
1405
2113
  let minRow = 0;
1406
2114
  let maxRow = 0;
1407
2115
  let minCol = 0;
1408
2116
  let maxCol = 0;
1409
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
+ }
1410
2121
  for (let r = 0; r < rows.length; r++) {
1411
2122
  const cells = rows[r];
1412
2123
  if (cells === void 0) continue;
@@ -1417,29 +2128,455 @@ function worksheetXml(rows, date1904) {
1417
2128
  );
1418
2129
  }
1419
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
+ }
1420
2137
  const rowNum = r + 1;
1421
2138
  const cellXmls = [];
1422
2139
  for (let c = 0; c < cells.length; c++) {
1423
2140
  const colNum = c + 1;
1424
- const rendered = renderCell(colNum, rowNum, cells[c], date1904);
2141
+ const rendered = renderCell(colNum, rowNum, cells[c], date1904, styles);
1425
2142
  if (rendered === void 0) continue;
1426
- if (rendered.isDate) usesDate = true;
1427
2143
  if (minRow === 0 || rowNum < minRow) minRow = rowNum;
1428
2144
  if (rowNum > maxRow) maxRow = rowNum;
1429
2145
  if (minCol === 0 || colNum < minCol) minCol = colNum;
1430
2146
  if (colNum > maxCol) maxCol = colNum;
1431
- cellXmls.push(rendered.xml);
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>`]);
1432
2153
  }
1433
- if (cellXmls.length > 0) rowXmls.push(`<row r="${rowNum}">${cellXmls.join("")}</row>`);
1434
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]);
1435
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}"` : "";
1436
2161
  const xml = `${XML_DECL}
1437
- <worksheet xmlns="${NS_MAIN}"><dimension ref="${dimension}"/><sheetData>${rowXmls.join(
1438
- ""
1439
- )}</sheetData></worksheet>`;
1440
- return { xml, usesDate };
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
+ };
1441
2575
  }
1442
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="&#xFF2D;&#xFF33; &#xFF30;&#x30B4;&#x30B7;&#x30C3;&#x30AF;"/><a:font script="Hang" typeface="&#xB9D1;&#xC740; &#xACE0;&#xB515;"/><a:font script="Hans" typeface="&#x5B8B;&#x4F53;"/><a:font script="Hant" typeface="&#x65B0;&#x7D30;&#x660E;&#x9AD4;"/><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="&#xFF2D;&#xFF33; &#xFF30;&#x30B4;&#x30B7;&#x30C3;&#x30AF;"/><a:font script="Hang" typeface="&#xB9D1;&#xC740; &#xACE0;&#xB515;"/><a:font script="Hans" typeface="&#x5B8B;&#x4F53;"/><a:font script="Hant" typeface="&#x65B0;&#x7D30;&#x660E;&#x9AD4;"/><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
+
1443
2580
  // src/writer/zip.ts
1444
2581
  var encoder = new TextEncoder();
1445
2582
  var SIG_LOCAL2 = 67324752;
@@ -1583,23 +2720,23 @@ async function writeZip(entries) {
1583
2720
 
1584
2721
  // src/writer/workbook.ts
1585
2722
  var encoder2 = new TextEncoder();
1586
- var XML_DECL2 = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
1587
- var NS_MAIN2 = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
1588
- var NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
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";
1589
2726
  var NS_PKG_REL = "http://schemas.openxmlformats.org/package/2006/relationships";
1590
2727
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
1591
2728
  var CT_BASE = "application/vnd.openxmlformats-officedocument.spreadsheetml";
1592
2729
  var CT_RELS = "application/vnd.openxmlformats-package.relationships+xml";
1593
2730
  var MAX_SHEET_NAME = 31;
1594
2731
  var FORBIDDEN_SHEET_NAME = /[\\/?*[\]:]/;
1595
- var STYLES_XML = `${XML_DECL2}
1596
- <styleSheet xmlns="${NS_MAIN2}"><fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border/></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="14" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
1597
2732
  function validate(workbook) {
1598
2733
  const sheets = workbook?.sheets;
1599
2734
  if (!Array.isArray(sheets) || sheets.length === 0) {
1600
2735
  throw new XlsxError("invalid-input", "a workbook needs at least one sheet");
1601
2736
  }
1602
2737
  const seen = /* @__PURE__ */ new Set();
2738
+ const states = [];
2739
+ let anyVisible = false;
1603
2740
  for (const sheet of sheets) {
1604
2741
  const name = sheet?.name;
1605
2742
  if (typeof name !== "string" || name.length === 0) {
@@ -1634,14 +2771,29 @@ function validate(workbook) {
1634
2771
  if (!Array.isArray(sheet.rows)) {
1635
2772
  throw new XlsxError("invalid-input", `sheet "${name}": rows must be an array`);
1636
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;
1637
2783
  }
2784
+ if (!anyVisible) {
2785
+ throw new XlsxError("invalid-input", "at least one sheet must be visible");
2786
+ }
2787
+ return states;
1638
2788
  }
1639
2789
  async function writeXlsx(workbook, options) {
1640
- validate(workbook);
2790
+ const states = validate(workbook);
1641
2791
  const date1904 = options?.date1904 === true;
1642
2792
  const sheets = workbook.sheets;
1643
- const worksheets = sheets.map((sheet) => worksheetXml(sheet.rows, date1904));
1644
- const anyDate = worksheets.some((w) => w.usesDate);
2793
+ const styles = createStyleRegistry();
2794
+ const worksheets = sheets.map((sheet) => worksheetXml(sheet, date1904, styles));
2795
+ const needStyles = styles.needed();
2796
+ const needTheme = styles.usesTheme();
1645
2797
  const parts = [];
1646
2798
  const add = (name, xml) => {
1647
2799
  parts.push({ name, data: encoder2.encode(xml) });
@@ -1651,43 +2803,63 @@ async function writeXlsx(workbook, options) {
1651
2803
  ...sheets.map(
1652
2804
  (_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="${CT_BASE}.worksheet+xml"/>`
1653
2805
  ),
1654
- ...anyDate ? [`<Override PartName="/xl/styles.xml" ContentType="${CT_BASE}.styles+xml"/>`] : []
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
+ ] : []
1655
2810
  ].join("");
1656
2811
  add(
1657
2812
  "[Content_Types].xml",
1658
- `${XML_DECL2}
2813
+ `${XML_DECL3}
1659
2814
  <Types xmlns="${NS_CT}"><Default Extension="rels" ContentType="${CT_RELS}"/><Default Extension="xml" ContentType="application/xml"/>${overrides}</Types>`
1660
2815
  );
1661
2816
  add(
1662
2817
  "_rels/.rels",
1663
- `${XML_DECL2}
1664
- <Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_REL}/officeDocument" Target="xl/workbook.xml"/></Relationships>`
2818
+ `${XML_DECL3}
2819
+ <Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_REL2}/officeDocument" Target="xl/workbook.xml"/></Relationships>`
1665
2820
  );
1666
2821
  const workbookPr = date1904 ? '<workbookPr date1904="1"/>' : "";
1667
- const sheetsXml = sheets.map(
1668
- (sheet, i) => `<sheet name="${escapeAttr(sheet.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`
1669
- ).join("");
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("");
1670
2828
  add(
1671
2829
  "xl/workbook.xml",
1672
- `${XML_DECL2}
1673
- <workbook xmlns="${NS_MAIN2}" xmlns:r="${NS_REL}">${workbookPr}<sheets>${sheetsXml}</sheets></workbook>`
2830
+ `${XML_DECL3}
2831
+ <workbook xmlns="${NS_MAIN3}" xmlns:r="${NS_REL2}">${workbookPr}${bookViews}<sheets>${sheetsXml}</sheets></workbook>`
1674
2832
  );
1675
2833
  const relItems = [
1676
2834
  ...sheets.map(
1677
- (_, i) => `<Relationship Id="rId${i + 1}" Type="${NS_REL}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`
2835
+ (_, i) => `<Relationship Id="rId${i + 1}" Type="${NS_REL2}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`
1678
2836
  ),
1679
- ...anyDate ? [
1680
- `<Relationship Id="rId${sheets.length + 1}" Type="${NS_REL}/styles" Target="styles.xml"/>`
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"/>`
1681
2842
  ] : []
1682
2843
  ].join("");
1683
2844
  add(
1684
2845
  "xl/_rels/workbook.xml.rels",
1685
- `${XML_DECL2}
2846
+ `${XML_DECL3}
1686
2847
  <Relationships xmlns="${NS_PKG_REL}">${relItems}</Relationships>`
1687
2848
  );
1688
- if (anyDate) add("xl/styles.xml", STYLES_XML);
2849
+ if (needStyles) add("xl/styles.xml", styles.stylesXml());
2850
+ if (needTheme) add("xl/theme/theme1.xml", DEFAULT_THEME_XML);
1689
2851
  worksheets.forEach((w, i) => {
1690
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
+ }
1691
2863
  });
1692
2864
  return writeZip(parts);
1693
2865
  }