@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
|
@@ -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.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 =
|
|
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,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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
//
|
|
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";
|
|
@@ -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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|