@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.
- package/dist/browser/modules/excel/stream/worksheet-writer.js +13 -5
- package/dist/browser/modules/excel/utils/cell-format.js +4 -37
- package/dist/browser/modules/excel/utils/merge-borders.d.ts +44 -0
- package/dist/browser/modules/excel/utils/merge-borders.js +105 -0
- package/dist/browser/modules/excel/worksheet.js +15 -5
- package/dist/browser/utils/utils.base.d.ts +8 -0
- package/dist/browser/utils/utils.base.js +51 -7
- package/dist/browser/utils/utils.browser.d.ts +1 -1
- package/dist/browser/utils/utils.browser.js +1 -1
- package/dist/browser/utils/utils.d.ts +1 -1
- package/dist/browser/utils/utils.js +1 -1
- package/dist/cjs/modules/excel/stream/worksheet-writer.js +13 -5
- package/dist/cjs/modules/excel/utils/cell-format.js +3 -36
- package/dist/cjs/modules/excel/utils/merge-borders.js +109 -0
- package/dist/cjs/modules/excel/worksheet.js +15 -5
- package/dist/cjs/utils/utils.base.js +52 -7
- package/dist/cjs/utils/utils.browser.js +2 -1
- package/dist/cjs/utils/utils.js +2 -1
- package/dist/esm/modules/excel/stream/worksheet-writer.js +13 -5
- package/dist/esm/modules/excel/utils/cell-format.js +4 -37
- package/dist/esm/modules/excel/utils/merge-borders.js +105 -0
- package/dist/esm/modules/excel/worksheet.js +15 -5
- package/dist/esm/utils/utils.base.js +51 -7
- package/dist/esm/utils/utils.browser.js +1 -1
- package/dist/esm/utils/utils.js +1 -1
- package/dist/iife/excelts.iife.js +140 -38
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +23 -23
- package/dist/types/modules/excel/utils/merge-borders.d.ts +44 -0
- package/dist/types/utils/utils.base.d.ts +8 -0
- package/dist/types/utils/utils.browser.d.ts +1 -1
- package/dist/types/utils/utils.d.ts +1 -1
- 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
|
-
|
|
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 =
|
|
743
|
-
for (let j =
|
|
744
|
-
|
|
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
|
-
//
|
|
163
|
-
|
|
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
|
-
|
|
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; } });
|
package/dist/cjs/utils/utils.js
CHANGED
|
@@ -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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
740
|
-
for (let j =
|
|
741
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
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
|
-
|
|
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
|
// =============================================================================
|
package/dist/esm/utils/utils.js
CHANGED
|
@@ -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
|
// =============================================================================
|