@cj-tech-master/excelts 5.1.11 → 5.1.12

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.
Files changed (33) hide show
  1. package/dist/browser/modules/excel/stream/worksheet-writer.js +13 -5
  2. package/dist/browser/modules/excel/utils/cell-format.js +4 -37
  3. package/dist/browser/modules/excel/utils/merge-borders.d.ts +44 -0
  4. package/dist/browser/modules/excel/utils/merge-borders.js +105 -0
  5. package/dist/browser/modules/excel/worksheet.js +15 -5
  6. package/dist/browser/utils/utils.base.d.ts +8 -0
  7. package/dist/browser/utils/utils.base.js +51 -7
  8. package/dist/browser/utils/utils.browser.d.ts +1 -1
  9. package/dist/browser/utils/utils.browser.js +1 -1
  10. package/dist/browser/utils/utils.d.ts +1 -1
  11. package/dist/browser/utils/utils.js +1 -1
  12. package/dist/cjs/modules/excel/stream/worksheet-writer.js +13 -5
  13. package/dist/cjs/modules/excel/utils/cell-format.js +3 -36
  14. package/dist/cjs/modules/excel/utils/merge-borders.js +109 -0
  15. package/dist/cjs/modules/excel/worksheet.js +15 -5
  16. package/dist/cjs/utils/utils.base.js +52 -7
  17. package/dist/cjs/utils/utils.browser.js +2 -1
  18. package/dist/cjs/utils/utils.js +2 -1
  19. package/dist/esm/modules/excel/stream/worksheet-writer.js +13 -5
  20. package/dist/esm/modules/excel/utils/cell-format.js +4 -37
  21. package/dist/esm/modules/excel/utils/merge-borders.js +105 -0
  22. package/dist/esm/modules/excel/worksheet.js +15 -5
  23. package/dist/esm/utils/utils.base.js +51 -7
  24. package/dist/esm/utils/utils.browser.js +1 -1
  25. package/dist/esm/utils/utils.js +1 -1
  26. package/dist/iife/excelts.iife.js +140 -38
  27. package/dist/iife/excelts.iife.js.map +1 -1
  28. package/dist/iife/excelts.iife.min.js +23 -23
  29. package/dist/types/modules/excel/utils/merge-borders.d.ts +44 -0
  30. package/dist/types/utils/utils.base.d.ts +8 -0
  31. package/dist/types/utils/utils.browser.d.ts +1 -1
  32. package/dist/types/utils/utils.d.ts +1 -1
  33. package/package.json +1 -1
@@ -14,6 +14,7 @@ const encryptor_1 = require("./utils/encryptor.js");
14
14
  const utils_1 = require("../../utils/utils.js");
15
15
  const pivot_table_1 = require("./pivot-table.js");
16
16
  const copy_style_1 = require("./utils/copy-style.js");
17
+ const merge_borders_1 = require("./utils/merge-borders.js");
17
18
  // Worksheet requirements
18
19
  // Operate as sheet inside workbook or standalone
19
20
  // Load and Save from file and stream
@@ -737,16 +738,25 @@ class Worksheet {
737
738
  throw new Error("Cannot merge already merged cells");
738
739
  }
739
740
  });
740
- // apply merge
741
+ const { top, left, bottom, right } = dimensions;
742
+ // Collect perimeter borders BEFORE merge overwrites slave styles
743
+ const collected = ignoreStyle
744
+ ? undefined
745
+ : (0, merge_borders_1.collectMergeBorders)(top, left, bottom, right, (r, c) => this.findCell(r, c));
746
+ // Apply merge — slave cells inherit the master's full style
741
747
  const master = this.getCell(dimensions.top, dimensions.left);
742
- for (let i = dimensions.top; i <= dimensions.bottom; i++) {
743
- for (let j = dimensions.left; j <= dimensions.right; j++) {
744
- // merge all but the master cell
745
- if (i > dimensions.top || j > dimensions.left) {
748
+ for (let i = top; i <= bottom; i++) {
749
+ for (let j = left; j <= right; j++) {
750
+ if (i > top || j > left) {
746
751
  this.getCell(i, j).merge(master, ignoreStyle);
747
752
  }
748
753
  }
749
754
  }
755
+ // Reconstruct position-aware borders (like Excel):
756
+ // outer borders survive, inner borders are cleared.
757
+ if (collected) {
758
+ (0, merge_borders_1.applyMergeBorders)(top, left, bottom, right, collected, (r, c) => this.getCell(r, c));
759
+ }
750
760
  // index merge
751
761
  this._merges[master.address] = dimensions;
752
762
  }
@@ -13,6 +13,7 @@ exports.parseOoxmlDate = parseOoxmlDate;
13
13
  exports.xmlDecode = xmlDecode;
14
14
  exports.xmlEncode = xmlEncode;
15
15
  exports.validInt = validInt;
16
+ exports.splitFormatSections = splitFormatSections;
16
17
  exports.isDateFmt = isDateFmt;
17
18
  exports.parseBoolean = parseBoolean;
18
19
  exports.range = range;
@@ -155,19 +156,63 @@ function validInt(value) {
155
156
  const i = typeof value === "number" ? value : parseInt(value, 10);
156
157
  return Number.isNaN(i) ? 0 : i;
157
158
  }
159
+ /**
160
+ * Split an Excel numFmt string by semicolons, respecting quoted strings and brackets.
161
+ *
162
+ * Excel numFmt can have up to 4 sections: `positive ; negative ; zero ; text`.
163
+ * Semicolons inside `"..."` (literal text) or `[...]` (locale/color tags) must NOT
164
+ * be treated as section separators.
165
+ */
166
+ function splitFormatSections(fmt) {
167
+ const sections = [];
168
+ let current = "";
169
+ let inQuote = false;
170
+ let inBracket = false;
171
+ for (let i = 0; i < fmt.length; i++) {
172
+ const char = fmt[i];
173
+ if (char === '"' && !inBracket) {
174
+ inQuote = !inQuote;
175
+ current += char;
176
+ }
177
+ else if (char === "[" && !inQuote) {
178
+ inBracket = true;
179
+ current += char;
180
+ }
181
+ else if (char === "]" && !inQuote) {
182
+ inBracket = false;
183
+ current += char;
184
+ }
185
+ else if (char === ";" && !inQuote && !inBracket) {
186
+ sections.push(current);
187
+ current = "";
188
+ }
189
+ else {
190
+ current += char;
191
+ }
192
+ }
193
+ sections.push(current);
194
+ return sections;
195
+ }
196
+ /** Reusable regex — no capture groups, so safe for `test()`. */
197
+ const DATE_FMT_RE = /[ymdhMsb]/;
198
+ /** Strips bracket expressions `[...]` and quoted literals `"..."` from a format string. */
199
+ const STRIP_BRACKETS_QUOTES_RE = /\[[^\]]*\]|"[^"]*"/g;
158
200
  function isDateFmt(fmt) {
159
201
  if (!fmt) {
160
202
  return false;
161
203
  }
162
- // must not be a string fmt
163
- if (fmt.indexOf("@") > -1) {
204
+ // Only the first section (used for positive numbers / dates) determines
205
+ // whether the format represents a date. The "@" text placeholder may
206
+ // legitimately appear in later sections as a text fallback (e.g. "mm/dd/yyyy;@").
207
+ const firstSection = splitFormatSections(fmt)[0];
208
+ // Strip bracket expressions [...] (locale/color tags) and quoted literals "..."
209
+ // before any further checks so that characters inside them are ignored.
210
+ const clean = firstSection.replace(STRIP_BRACKETS_QUOTES_RE, "");
211
+ // "@" in the cleaned section means it's a text format, not a date format.
212
+ if (clean.indexOf("@") > -1) {
164
213
  return false;
165
214
  }
166
- // must remove all chars inside quotes and []
167
- let cleanFmt = fmt.replace(/\[[^\]]*\]/g, "");
168
- cleanFmt = cleanFmt.replace(/"[^"]*"/g, "");
169
- // then check for date formatting chars
170
- return cleanFmt.match(/[ymdhMsb]+/) !== null;
215
+ return DATE_FMT_RE.test(clean);
171
216
  }
172
217
  function parseBoolean(value) {
173
218
  return value === true || value === "true" || value === 1 || value === "1";
@@ -4,7 +4,7 @@
4
4
  * Re-exports shared utilities and adds browser-specific implementations
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.stringToUtf16Le = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.bufferToString = exports.toSortedArray = exports.range = exports.parseBoolean = exports.isDateFmt = exports.validInt = exports.xmlEncode = exports.xmlDecode = exports.parseOoxmlDate = exports.excelToDate = exports.dateToExcel = exports.delay = void 0;
7
+ exports.stringToUtf16Le = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.bufferToString = exports.toSortedArray = exports.range = exports.parseBoolean = exports.splitFormatSections = exports.isDateFmt = exports.validInt = exports.xmlEncode = exports.xmlDecode = exports.parseOoxmlDate = exports.excelToDate = exports.dateToExcel = exports.delay = void 0;
8
8
  exports.fileExists = fileExists;
9
9
  // Re-export all shared utilities
10
10
  var utils_base_1 = require("./utils.base.js");
@@ -16,6 +16,7 @@ Object.defineProperty(exports, "xmlDecode", { enumerable: true, get: function ()
16
16
  Object.defineProperty(exports, "xmlEncode", { enumerable: true, get: function () { return utils_base_1.xmlEncode; } });
17
17
  Object.defineProperty(exports, "validInt", { enumerable: true, get: function () { return utils_base_1.validInt; } });
18
18
  Object.defineProperty(exports, "isDateFmt", { enumerable: true, get: function () { return utils_base_1.isDateFmt; } });
19
+ Object.defineProperty(exports, "splitFormatSections", { enumerable: true, get: function () { return utils_base_1.splitFormatSections; } });
19
20
  Object.defineProperty(exports, "parseBoolean", { enumerable: true, get: function () { return utils_base_1.parseBoolean; } });
20
21
  Object.defineProperty(exports, "range", { enumerable: true, get: function () { return utils_base_1.range; } });
21
22
  Object.defineProperty(exports, "toSortedArray", { enumerable: true, get: function () { return utils_base_1.toSortedArray; } });
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.stringToUtf16Le = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.bufferToString = exports.toSortedArray = exports.range = exports.parseBoolean = exports.isDateFmt = exports.validInt = exports.xmlEncode = exports.xmlDecode = exports.parseOoxmlDate = exports.excelToDate = exports.dateToExcel = exports.delay = void 0;
10
+ exports.stringToUtf16Le = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.bufferToString = exports.toSortedArray = exports.range = exports.parseBoolean = exports.splitFormatSections = exports.isDateFmt = exports.validInt = exports.xmlEncode = exports.xmlDecode = exports.parseOoxmlDate = exports.excelToDate = exports.dateToExcel = exports.delay = void 0;
11
11
  exports.fileExists = fileExists;
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  // Re-export all shared utilities
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "xmlDecode", { enumerable: true, get: function ()
20
20
  Object.defineProperty(exports, "xmlEncode", { enumerable: true, get: function () { return utils_base_1.xmlEncode; } });
21
21
  Object.defineProperty(exports, "validInt", { enumerable: true, get: function () { return utils_base_1.validInt; } });
22
22
  Object.defineProperty(exports, "isDateFmt", { enumerable: true, get: function () { return utils_base_1.isDateFmt; } });
23
+ Object.defineProperty(exports, "splitFormatSections", { enumerable: true, get: function () { return utils_base_1.splitFormatSections; } });
23
24
  Object.defineProperty(exports, "parseBoolean", { enumerable: true, get: function () { return utils_base_1.parseBoolean; } });
24
25
  Object.defineProperty(exports, "range", { enumerable: true, get: function () { return utils_base_1.range; } });
25
26
  Object.defineProperty(exports, "toSortedArray", { enumerable: true, get: function () { return utils_base_1.toSortedArray; } });
@@ -9,6 +9,7 @@ import { Column } from "../column.js";
9
9
  import { SheetRelsWriter } from "./sheet-rels-writer.js";
10
10
  import { SheetCommentsWriter } from "./sheet-comments-writer.js";
11
11
  import { DataValidations } from "../data-validations.js";
12
+ import { applyMergeBorders, collectMergeBorders } from "../utils/merge-borders.js";
12
13
  import { mediaRelTargetFromRels, worksheetPath } from "../utils/ooxml-paths.js";
13
14
  const xmlBuffer = new StringBuf();
14
15
  // ============================================================================================
@@ -384,15 +385,22 @@ class WorksheetWriter {
384
385
  throw new Error("Cannot merge already merged cells");
385
386
  }
386
387
  });
387
- // apply merge
388
- const master = this.getCell(dimensions.top, dimensions.left);
389
- for (let i = dimensions.top; i <= dimensions.bottom; i++) {
390
- for (let j = dimensions.left; j <= dimensions.right; j++) {
391
- if (i > dimensions.top || j > dimensions.left) {
388
+ const { top, left, bottom, right } = dimensions;
389
+ // Collect perimeter borders BEFORE merge overwrites slave styles
390
+ const collected = collectMergeBorders(top, left, bottom, right, (r, c) => this.findCell(r, c));
391
+ // Apply merge
392
+ const master = this.getCell(top, left);
393
+ for (let i = top; i <= bottom; i++) {
394
+ for (let j = left; j <= right; j++) {
395
+ if (i > top || j > left) {
392
396
  this.getCell(i, j).merge(master);
393
397
  }
394
398
  }
395
399
  }
400
+ // Reconstruct position-aware borders (like Excel)
401
+ if (collected) {
402
+ applyMergeBorders(top, left, bottom, right, collected, (r, c) => this.getCell(r, c));
403
+ }
396
404
  // index merge
397
405
  this._merges.push(dimensions);
398
406
  }
@@ -5,7 +5,7 @@
5
5
  * Supports: General, percentages, decimals, thousands separators, dates, currencies,
6
6
  * scientific notation, fractions, elapsed time, and more
7
7
  */
8
- import { excelToDate } from "../../../utils/utils.js";
8
+ import { excelToDate, splitFormatSections } from "../../../utils/utils.js";
9
9
  // =============================================================================
10
10
  // Built-in Format Table (Excel numFmtId to format string mapping)
11
11
  // =============================================================================
@@ -633,7 +633,7 @@ function checkCondition(val, condition) {
633
633
  function chooseFormat(fmt, val) {
634
634
  if (typeof val === "string") {
635
635
  // For text, use the 4th section if available, or just return as-is
636
- const sections = splitFormat(fmt);
636
+ const sections = splitFormatSections(fmt);
637
637
  if (sections.length >= 4 && sections[3]) {
638
638
  // Process quoted text and replace @ with the value
639
639
  const textFmt = processQuotedText(sections[3]);
@@ -644,7 +644,7 @@ function chooseFormat(fmt, val) {
644
644
  if (typeof val === "boolean") {
645
645
  return val ? "TRUE" : "FALSE";
646
646
  }
647
- const sections = splitFormat(fmt);
647
+ const sections = splitFormatSections(fmt);
648
648
  // Check for conditional format in sections
649
649
  const condRegex = /\[(=|>|<|>=|<=|<>)-?\d+(?:\.\d*)?\]/;
650
650
  const hasCondition = (sections[0] && condRegex.test(sections[0])) || (sections[1] && condRegex.test(sections[1]));
@@ -679,42 +679,9 @@ function chooseFormat(fmt, val) {
679
679
  * Check if format section is for negative values (2nd section in multi-section format)
680
680
  */
681
681
  function isNegativeSection(fmt, selectedFmt) {
682
- const sections = splitFormat(fmt);
682
+ const sections = splitFormatSections(fmt);
683
683
  return sections.length >= 2 && sections[1] === selectedFmt;
684
684
  }
685
- /**
686
- * Split format string by semicolons, respecting quoted strings and brackets
687
- */
688
- function splitFormat(fmt) {
689
- const sections = [];
690
- let current = "";
691
- let inQuote = false;
692
- let inBracket = false;
693
- for (let i = 0; i < fmt.length; i++) {
694
- const char = fmt[i];
695
- if (char === '"' && !inBracket) {
696
- inQuote = !inQuote;
697
- current += char;
698
- }
699
- else if (char === "[" && !inQuote) {
700
- inBracket = true;
701
- current += char;
702
- }
703
- else if (char === "]" && !inQuote) {
704
- inBracket = false;
705
- current += char;
706
- }
707
- else if (char === ";" && !inQuote && !inBracket) {
708
- sections.push(current);
709
- current = "";
710
- }
711
- else {
712
- current += char;
713
- }
714
- }
715
- sections.push(current);
716
- return sections;
717
- }
718
685
  /**
719
686
  * Main format function - formats a value according to Excel numFmt
720
687
  * @param fmt The Excel number format string (e.g., "0.00%", "#,##0", "yyyy-mm-dd")
@@ -0,0 +1,105 @@
1
+ import { copyStyle } from "./copy-style.js";
2
+ /**
3
+ * Collect perimeter borders from cells before a merge is applied.
4
+ * Must be called BEFORE cell.merge() overwrites slave styles.
5
+ *
6
+ * Only iterates the four edges of the range, not the full rectangle.
7
+ * For perimeter edges where the cell has no border, falls back to the master's border.
8
+ */
9
+ export function collectMergeBorders(top, left, bottom, right, findCell) {
10
+ const masterBorder = findCell(top, left)?.style?.border;
11
+ const width = right - left + 1;
12
+ const height = bottom - top + 1;
13
+ const topEdges = new Array(width);
14
+ const bottomEdges = new Array(width);
15
+ const leftEdges = new Array(height);
16
+ const rightEdges = new Array(height);
17
+ let hasAny = false;
18
+ // Top & bottom rows
19
+ for (let j = left; j <= right; j++) {
20
+ const idx = j - left;
21
+ const topBorder = findCell(top, j)?.style?.border;
22
+ topEdges[idx] = topBorder?.top || masterBorder?.top;
23
+ if (bottom !== top) {
24
+ const botBorder = findCell(bottom, j)?.style?.border;
25
+ bottomEdges[idx] = botBorder?.bottom || masterBorder?.bottom;
26
+ }
27
+ else {
28
+ bottomEdges[idx] = topBorder?.bottom || masterBorder?.bottom;
29
+ }
30
+ if (topEdges[idx] || bottomEdges[idx]) {
31
+ hasAny = true;
32
+ }
33
+ }
34
+ // Left & right columns
35
+ for (let i = top; i <= bottom; i++) {
36
+ const idx = i - top;
37
+ const leftBorder = findCell(i, left)?.style?.border;
38
+ leftEdges[idx] = leftBorder?.left || masterBorder?.left;
39
+ if (right !== left) {
40
+ const rightBorder = findCell(i, right)?.style?.border;
41
+ rightEdges[idx] = rightBorder?.right || masterBorder?.right;
42
+ }
43
+ else {
44
+ rightEdges[idx] = leftBorder?.right || masterBorder?.right;
45
+ }
46
+ if (leftEdges[idx] || rightEdges[idx]) {
47
+ hasAny = true;
48
+ }
49
+ }
50
+ const diagonal = masterBorder?.diagonal;
51
+ const color = masterBorder?.color;
52
+ if (!hasAny && !diagonal) {
53
+ return undefined;
54
+ }
55
+ return { topEdges, bottomEdges, leftEdges, rightEdges, diagonal, color };
56
+ }
57
+ /**
58
+ * Apply position-aware borders to a merged cell range.
59
+ * Must be called AFTER cell.merge() so that the master style is available.
60
+ *
61
+ * Each cell receives a deep-copied style from the master so that
62
+ * later mutations to one cell do not leak to others.
63
+ */
64
+ export function applyMergeBorders(top, left, bottom, right, collected, getCell) {
65
+ const { topEdges, bottomEdges, leftEdges, rightEdges, diagonal, color } = collected;
66
+ const masterStyle = getCell(top, left).style;
67
+ for (let i = top; i <= bottom; i++) {
68
+ for (let j = left; j <= right; j++) {
69
+ const cell = getCell(i, j);
70
+ const style = copyStyle(masterStyle) || {};
71
+ const newBorder = {};
72
+ let hasBorder = false;
73
+ if (i === top && topEdges[j - left]) {
74
+ newBorder.top = topEdges[j - left];
75
+ hasBorder = true;
76
+ }
77
+ if (i === bottom && bottomEdges[j - left]) {
78
+ newBorder.bottom = bottomEdges[j - left];
79
+ hasBorder = true;
80
+ }
81
+ if (j === left && leftEdges[i - top]) {
82
+ newBorder.left = leftEdges[i - top];
83
+ hasBorder = true;
84
+ }
85
+ if (j === right && rightEdges[i - top]) {
86
+ newBorder.right = rightEdges[i - top];
87
+ hasBorder = true;
88
+ }
89
+ if (diagonal) {
90
+ newBorder.diagonal = diagonal;
91
+ hasBorder = true;
92
+ }
93
+ if (hasBorder) {
94
+ if (color) {
95
+ newBorder.color = color;
96
+ }
97
+ style.border = newBorder;
98
+ }
99
+ else {
100
+ delete style.border;
101
+ }
102
+ cell.style = style;
103
+ }
104
+ }
105
+ }
@@ -11,6 +11,7 @@ import { Encryptor } from "./utils/encryptor.js";
11
11
  import { uint8ArrayToBase64 } from "../../utils/utils.js";
12
12
  import { makePivotTable } from "./pivot-table.js";
13
13
  import { copyStyle } from "./utils/copy-style.js";
14
+ import { applyMergeBorders, collectMergeBorders } from "./utils/merge-borders.js";
14
15
  // Worksheet requirements
15
16
  // Operate as sheet inside workbook or standalone
16
17
  // Load and Save from file and stream
@@ -734,16 +735,25 @@ class Worksheet {
734
735
  throw new Error("Cannot merge already merged cells");
735
736
  }
736
737
  });
737
- // apply merge
738
+ const { top, left, bottom, right } = dimensions;
739
+ // Collect perimeter borders BEFORE merge overwrites slave styles
740
+ const collected = ignoreStyle
741
+ ? undefined
742
+ : collectMergeBorders(top, left, bottom, right, (r, c) => this.findCell(r, c));
743
+ // Apply merge — slave cells inherit the master's full style
738
744
  const master = this.getCell(dimensions.top, dimensions.left);
739
- for (let i = dimensions.top; i <= dimensions.bottom; i++) {
740
- for (let j = dimensions.left; j <= dimensions.right; j++) {
741
- // merge all but the master cell
742
- if (i > dimensions.top || j > dimensions.left) {
745
+ for (let i = top; i <= bottom; i++) {
746
+ for (let j = left; j <= right; j++) {
747
+ if (i > top || j > left) {
743
748
  this.getCell(i, j).merge(master, ignoreStyle);
744
749
  }
745
750
  }
746
751
  }
752
+ // Reconstruct position-aware borders (like Excel):
753
+ // outer borders survive, inner borders are cleared.
754
+ if (collected) {
755
+ applyMergeBorders(top, left, bottom, right, collected, (r, c) => this.getCell(r, c));
756
+ }
747
757
  // index merge
748
758
  this._merges[master.address] = dimensions;
749
759
  }
@@ -138,19 +138,63 @@ export function validInt(value) {
138
138
  const i = typeof value === "number" ? value : parseInt(value, 10);
139
139
  return Number.isNaN(i) ? 0 : i;
140
140
  }
141
+ /**
142
+ * Split an Excel numFmt string by semicolons, respecting quoted strings and brackets.
143
+ *
144
+ * Excel numFmt can have up to 4 sections: `positive ; negative ; zero ; text`.
145
+ * Semicolons inside `"..."` (literal text) or `[...]` (locale/color tags) must NOT
146
+ * be treated as section separators.
147
+ */
148
+ export function splitFormatSections(fmt) {
149
+ const sections = [];
150
+ let current = "";
151
+ let inQuote = false;
152
+ let inBracket = false;
153
+ for (let i = 0; i < fmt.length; i++) {
154
+ const char = fmt[i];
155
+ if (char === '"' && !inBracket) {
156
+ inQuote = !inQuote;
157
+ current += char;
158
+ }
159
+ else if (char === "[" && !inQuote) {
160
+ inBracket = true;
161
+ current += char;
162
+ }
163
+ else if (char === "]" && !inQuote) {
164
+ inBracket = false;
165
+ current += char;
166
+ }
167
+ else if (char === ";" && !inQuote && !inBracket) {
168
+ sections.push(current);
169
+ current = "";
170
+ }
171
+ else {
172
+ current += char;
173
+ }
174
+ }
175
+ sections.push(current);
176
+ return sections;
177
+ }
178
+ /** Reusable regex — no capture groups, so safe for `test()`. */
179
+ const DATE_FMT_RE = /[ymdhMsb]/;
180
+ /** Strips bracket expressions `[...]` and quoted literals `"..."` from a format string. */
181
+ const STRIP_BRACKETS_QUOTES_RE = /\[[^\]]*\]|"[^"]*"/g;
141
182
  export function isDateFmt(fmt) {
142
183
  if (!fmt) {
143
184
  return false;
144
185
  }
145
- // must not be a string fmt
146
- if (fmt.indexOf("@") > -1) {
186
+ // Only the first section (used for positive numbers / dates) determines
187
+ // whether the format represents a date. The "@" text placeholder may
188
+ // legitimately appear in later sections as a text fallback (e.g. "mm/dd/yyyy;@").
189
+ const firstSection = splitFormatSections(fmt)[0];
190
+ // Strip bracket expressions [...] (locale/color tags) and quoted literals "..."
191
+ // before any further checks so that characters inside them are ignored.
192
+ const clean = firstSection.replace(STRIP_BRACKETS_QUOTES_RE, "");
193
+ // "@" in the cleaned section means it's a text format, not a date format.
194
+ if (clean.indexOf("@") > -1) {
147
195
  return false;
148
196
  }
149
- // must remove all chars inside quotes and []
150
- let cleanFmt = fmt.replace(/\[[^\]]*\]/g, "");
151
- cleanFmt = cleanFmt.replace(/"[^"]*"/g, "");
152
- // then check for date formatting chars
153
- return cleanFmt.match(/[ymdhMsb]+/) !== null;
197
+ return DATE_FMT_RE.test(clean);
154
198
  }
155
199
  export function parseBoolean(value) {
156
200
  return value === true || value === "true" || value === 1 || value === "1";
@@ -3,7 +3,7 @@
3
3
  * Re-exports shared utilities and adds browser-specific implementations
4
4
  */
5
5
  // Re-export all shared utilities
6
- export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "./utils.base.js";
6
+ export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, splitFormatSections, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "./utils.base.js";
7
7
  // =============================================================================
8
8
  // File system utilities (Browser stub - always returns false)
9
9
  // =============================================================================
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import fs from "fs";
6
6
  // Re-export all shared utilities
7
- export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "./utils.base.js";
7
+ export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, splitFormatSections, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "./utils.base.js";
8
8
  // =============================================================================
9
9
  // File system utilities (Node.js only)
10
10
  // =============================================================================