@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
@@ -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.browser.js";
8
+ import { excelToDate, splitFormatSections } from "../../../utils/utils.browser.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,44 @@
1
+ interface StyleObject {
2
+ [key: string]: any;
3
+ }
4
+ type BorderEdge = {
5
+ style?: string;
6
+ color?: Record<string, any>;
7
+ } | undefined;
8
+ /**
9
+ * Borders collected from the perimeter of a merge range.
10
+ * Stored as flat arrays indexed by row/column offset for O(1) lookup.
11
+ */
12
+ export interface CollectedBorders {
13
+ /** top edge border for each column (index = col - left) */
14
+ topEdges: BorderEdge[];
15
+ /** bottom edge border for each column (index = col - left) */
16
+ bottomEdges: BorderEdge[];
17
+ /** left edge border for each row (index = row - top) */
18
+ leftEdges: BorderEdge[];
19
+ /** right edge border for each row (index = row - top) */
20
+ rightEdges: BorderEdge[];
21
+ diagonal?: any;
22
+ color?: any;
23
+ }
24
+ /**
25
+ * Collect perimeter borders from cells before a merge is applied.
26
+ * Must be called BEFORE cell.merge() overwrites slave styles.
27
+ *
28
+ * Only iterates the four edges of the range, not the full rectangle.
29
+ * For perimeter edges where the cell has no border, falls back to the master's border.
30
+ */
31
+ export declare function collectMergeBorders(top: number, left: number, bottom: number, right: number, findCell: (r: number, c: number) => {
32
+ style: StyleObject;
33
+ } | undefined): CollectedBorders | undefined;
34
+ /**
35
+ * Apply position-aware borders to a merged cell range.
36
+ * Must be called AFTER cell.merge() so that the master style is available.
37
+ *
38
+ * Each cell receives a deep-copied style from the master so that
39
+ * later mutations to one cell do not leak to others.
40
+ */
41
+ export declare function applyMergeBorders(top: number, left: number, bottom: number, right: number, collected: CollectedBorders, getCell: (r: number, c: number) => {
42
+ style: StyleObject;
43
+ }): void;
44
+ export {};
@@ -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.browser.js";
11
11
  import { uint8ArrayToBase64 } from "../../utils/utils.browser.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
  }
@@ -24,6 +24,14 @@ export declare function xmlDecode(text: string): string;
24
24
  */
25
25
  export declare function xmlEncode(text: string): string;
26
26
  export declare function validInt(value: string | number): number;
27
+ /**
28
+ * Split an Excel numFmt string by semicolons, respecting quoted strings and brackets.
29
+ *
30
+ * Excel numFmt can have up to 4 sections: `positive ; negative ; zero ; text`.
31
+ * Semicolons inside `"..."` (literal text) or `[...]` (locale/color tags) must NOT
32
+ * be treated as section separators.
33
+ */
34
+ export declare function splitFormatSections(fmt: string): string[];
27
35
  export declare function isDateFmt(fmt: string | null | undefined): boolean;
28
36
  export declare function parseBoolean(value: unknown): boolean;
29
37
  export declare function range(start: number, stop: number, step?: number): Generator<number>;
@@ -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";
@@ -2,5 +2,5 @@
2
2
  * Browser utility functions
3
3
  * Re-exports shared utilities and adds browser-specific implementations
4
4
  */
5
- export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "@utils/utils.base";
5
+ export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, splitFormatSections, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "@utils/utils.base";
6
6
  export declare function fileExists(_path: string): Promise<boolean>;
@@ -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
  // =============================================================================
@@ -2,5 +2,5 @@
2
2
  * Node.js utility functions
3
3
  * Re-exports shared utilities and adds Node.js-specific implementations
4
4
  */
5
- export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "@utils/utils.base";
5
+ export { delay, dateToExcel, excelToDate, parseOoxmlDate, xmlDecode, xmlEncode, validInt, isDateFmt, splitFormatSections, parseBoolean, range, toSortedArray, bufferToString, base64ToUint8Array, uint8ArrayToBase64, stringToUtf16Le } from "@utils/utils.base";
6
6
  export declare function fileExists(path: string): Promise<boolean>;
@@ -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
  // =============================================================================
@@ -12,6 +12,7 @@ const column_1 = require("../column.js");
12
12
  const sheet_rels_writer_1 = require("./sheet-rels-writer.js");
13
13
  const sheet_comments_writer_1 = require("./sheet-comments-writer.js");
14
14
  const data_validations_1 = require("../data-validations.js");
15
+ const merge_borders_1 = require("../utils/merge-borders.js");
15
16
  const ooxml_paths_1 = require("../utils/ooxml-paths.js");
16
17
  const xmlBuffer = new string_buf_1.StringBuf();
17
18
  // ============================================================================================
@@ -387,15 +388,22 @@ class WorksheetWriter {
387
388
  throw new Error("Cannot merge already merged cells");
388
389
  }
389
390
  });
390
- // apply merge
391
- const master = this.getCell(dimensions.top, dimensions.left);
392
- for (let i = dimensions.top; i <= dimensions.bottom; i++) {
393
- for (let j = dimensions.left; j <= dimensions.right; j++) {
394
- if (i > dimensions.top || j > dimensions.left) {
391
+ const { top, left, bottom, right } = dimensions;
392
+ // Collect perimeter borders BEFORE merge overwrites slave styles
393
+ const collected = (0, merge_borders_1.collectMergeBorders)(top, left, bottom, right, (r, c) => this.findCell(r, c));
394
+ // Apply merge
395
+ const master = this.getCell(top, left);
396
+ for (let i = top; i <= bottom; i++) {
397
+ for (let j = left; j <= right; j++) {
398
+ if (i > top || j > left) {
395
399
  this.getCell(i, j).merge(master);
396
400
  }
397
401
  }
398
402
  }
403
+ // Reconstruct position-aware borders (like Excel)
404
+ if (collected) {
405
+ (0, merge_borders_1.applyMergeBorders)(top, left, bottom, right, collected, (r, c) => this.getCell(r, c));
406
+ }
399
407
  // index merge
400
408
  this._merges.push(dimensions);
401
409
  }
@@ -638,7 +638,7 @@ function checkCondition(val, condition) {
638
638
  function chooseFormat(fmt, val) {
639
639
  if (typeof val === "string") {
640
640
  // For text, use the 4th section if available, or just return as-is
641
- const sections = splitFormat(fmt);
641
+ const sections = (0, utils_1.splitFormatSections)(fmt);
642
642
  if (sections.length >= 4 && sections[3]) {
643
643
  // Process quoted text and replace @ with the value
644
644
  const textFmt = processQuotedText(sections[3]);
@@ -649,7 +649,7 @@ function chooseFormat(fmt, val) {
649
649
  if (typeof val === "boolean") {
650
650
  return val ? "TRUE" : "FALSE";
651
651
  }
652
- const sections = splitFormat(fmt);
652
+ const sections = (0, utils_1.splitFormatSections)(fmt);
653
653
  // Check for conditional format in sections
654
654
  const condRegex = /\[(=|>|<|>=|<=|<>)-?\d+(?:\.\d*)?\]/;
655
655
  const hasCondition = (sections[0] && condRegex.test(sections[0])) || (sections[1] && condRegex.test(sections[1]));
@@ -684,42 +684,9 @@ function chooseFormat(fmt, val) {
684
684
  * Check if format section is for negative values (2nd section in multi-section format)
685
685
  */
686
686
  function isNegativeSection(fmt, selectedFmt) {
687
- const sections = splitFormat(fmt);
687
+ const sections = (0, utils_1.splitFormatSections)(fmt);
688
688
  return sections.length >= 2 && sections[1] === selectedFmt;
689
689
  }
690
- /**
691
- * Split format string by semicolons, respecting quoted strings and brackets
692
- */
693
- function splitFormat(fmt) {
694
- const sections = [];
695
- let current = "";
696
- let inQuote = false;
697
- let inBracket = false;
698
- for (let i = 0; i < fmt.length; i++) {
699
- const char = fmt[i];
700
- if (char === '"' && !inBracket) {
701
- inQuote = !inQuote;
702
- current += char;
703
- }
704
- else if (char === "[" && !inQuote) {
705
- inBracket = true;
706
- current += char;
707
- }
708
- else if (char === "]" && !inQuote) {
709
- inBracket = false;
710
- current += char;
711
- }
712
- else if (char === ";" && !inQuote && !inBracket) {
713
- sections.push(current);
714
- current = "";
715
- }
716
- else {
717
- current += char;
718
- }
719
- }
720
- sections.push(current);
721
- return sections;
722
- }
723
690
  /**
724
691
  * Main format function - formats a value according to Excel numFmt
725
692
  * @param fmt The Excel number format string (e.g., "0.00%", "#,##0", "yyyy-mm-dd")
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectMergeBorders = collectMergeBorders;
4
+ exports.applyMergeBorders = applyMergeBorders;
5
+ const copy_style_1 = require("./copy-style.js");
6
+ /**
7
+ * Collect perimeter borders from cells before a merge is applied.
8
+ * Must be called BEFORE cell.merge() overwrites slave styles.
9
+ *
10
+ * Only iterates the four edges of the range, not the full rectangle.
11
+ * For perimeter edges where the cell has no border, falls back to the master's border.
12
+ */
13
+ function collectMergeBorders(top, left, bottom, right, findCell) {
14
+ const masterBorder = findCell(top, left)?.style?.border;
15
+ const width = right - left + 1;
16
+ const height = bottom - top + 1;
17
+ const topEdges = new Array(width);
18
+ const bottomEdges = new Array(width);
19
+ const leftEdges = new Array(height);
20
+ const rightEdges = new Array(height);
21
+ let hasAny = false;
22
+ // Top & bottom rows
23
+ for (let j = left; j <= right; j++) {
24
+ const idx = j - left;
25
+ const topBorder = findCell(top, j)?.style?.border;
26
+ topEdges[idx] = topBorder?.top || masterBorder?.top;
27
+ if (bottom !== top) {
28
+ const botBorder = findCell(bottom, j)?.style?.border;
29
+ bottomEdges[idx] = botBorder?.bottom || masterBorder?.bottom;
30
+ }
31
+ else {
32
+ bottomEdges[idx] = topBorder?.bottom || masterBorder?.bottom;
33
+ }
34
+ if (topEdges[idx] || bottomEdges[idx]) {
35
+ hasAny = true;
36
+ }
37
+ }
38
+ // Left & right columns
39
+ for (let i = top; i <= bottom; i++) {
40
+ const idx = i - top;
41
+ const leftBorder = findCell(i, left)?.style?.border;
42
+ leftEdges[idx] = leftBorder?.left || masterBorder?.left;
43
+ if (right !== left) {
44
+ const rightBorder = findCell(i, right)?.style?.border;
45
+ rightEdges[idx] = rightBorder?.right || masterBorder?.right;
46
+ }
47
+ else {
48
+ rightEdges[idx] = leftBorder?.right || masterBorder?.right;
49
+ }
50
+ if (leftEdges[idx] || rightEdges[idx]) {
51
+ hasAny = true;
52
+ }
53
+ }
54
+ const diagonal = masterBorder?.diagonal;
55
+ const color = masterBorder?.color;
56
+ if (!hasAny && !diagonal) {
57
+ return undefined;
58
+ }
59
+ return { topEdges, bottomEdges, leftEdges, rightEdges, diagonal, color };
60
+ }
61
+ /**
62
+ * Apply position-aware borders to a merged cell range.
63
+ * Must be called AFTER cell.merge() so that the master style is available.
64
+ *
65
+ * Each cell receives a deep-copied style from the master so that
66
+ * later mutations to one cell do not leak to others.
67
+ */
68
+ function applyMergeBorders(top, left, bottom, right, collected, getCell) {
69
+ const { topEdges, bottomEdges, leftEdges, rightEdges, diagonal, color } = collected;
70
+ const masterStyle = getCell(top, left).style;
71
+ for (let i = top; i <= bottom; i++) {
72
+ for (let j = left; j <= right; j++) {
73
+ const cell = getCell(i, j);
74
+ const style = (0, copy_style_1.copyStyle)(masterStyle) || {};
75
+ const newBorder = {};
76
+ let hasBorder = false;
77
+ if (i === top && topEdges[j - left]) {
78
+ newBorder.top = topEdges[j - left];
79
+ hasBorder = true;
80
+ }
81
+ if (i === bottom && bottomEdges[j - left]) {
82
+ newBorder.bottom = bottomEdges[j - left];
83
+ hasBorder = true;
84
+ }
85
+ if (j === left && leftEdges[i - top]) {
86
+ newBorder.left = leftEdges[i - top];
87
+ hasBorder = true;
88
+ }
89
+ if (j === right && rightEdges[i - top]) {
90
+ newBorder.right = rightEdges[i - top];
91
+ hasBorder = true;
92
+ }
93
+ if (diagonal) {
94
+ newBorder.diagonal = diagonal;
95
+ hasBorder = true;
96
+ }
97
+ if (hasBorder) {
98
+ if (color) {
99
+ newBorder.color = color;
100
+ }
101
+ style.border = newBorder;
102
+ }
103
+ else {
104
+ delete style.border;
105
+ }
106
+ cell.style = style;
107
+ }
108
+ }
109
+ }