@niicojs/excel 0.3.0 → 0.3.1
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/LICENSE +20 -20
- package/README.md +585 -585
- package/dist/index.cjs +498 -489
- package/dist/index.d.cts +5 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +498 -489
- package/package.json +1 -1
- package/src/index.ts +45 -45
- package/src/pivot-cache.ts +300 -300
- package/src/pivot-table.ts +684 -684
- package/src/range.ts +154 -154
- package/src/shared-strings.ts +178 -178
- package/src/styles.ts +819 -819
- package/src/table.ts +386 -386
- package/src/types.ts +313 -307
- package/src/utils/address.ts +121 -121
- package/src/utils/xml.ts +140 -140
- package/src/workbook.ts +1390 -1390
- package/src/worksheet.ts +879 -869
package/src/range.ts
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
import type { CellValue, CellStyle, RangeAddress } from './types';
|
|
2
|
-
import type { Worksheet } from './worksheet';
|
|
3
|
-
import { toAddress, normalizeRange } from './utils/address';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Represents a range of cells in a worksheet
|
|
7
|
-
*/
|
|
8
|
-
export class Range {
|
|
9
|
-
private _worksheet: Worksheet;
|
|
10
|
-
private _range: RangeAddress;
|
|
11
|
-
|
|
12
|
-
constructor(worksheet: Worksheet, range: RangeAddress) {
|
|
13
|
-
this._worksheet = worksheet;
|
|
14
|
-
this._range = normalizeRange(range);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get the range address as a string
|
|
19
|
-
*/
|
|
20
|
-
get address(): string {
|
|
21
|
-
const start = toAddress(this._range.start.row, this._range.start.col);
|
|
22
|
-
const end = toAddress(this._range.end.row, this._range.end.col);
|
|
23
|
-
if (start === end) return start;
|
|
24
|
-
return `${start}:${end}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get the number of rows in the range
|
|
29
|
-
*/
|
|
30
|
-
get rowCount(): number {
|
|
31
|
-
return this._range.end.row - this._range.start.row + 1;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Get the number of columns in the range
|
|
36
|
-
*/
|
|
37
|
-
get colCount(): number {
|
|
38
|
-
return this._range.end.col - this._range.start.col + 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get all values in the range as a 2D array
|
|
43
|
-
*/
|
|
44
|
-
get values(): CellValue[][] {
|
|
45
|
-
return this.getValues();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get all values in the range as a 2D array with options
|
|
50
|
-
*/
|
|
51
|
-
getValues(options: { createMissing?: boolean } = {}): CellValue[][] {
|
|
52
|
-
const { createMissing = true } = options;
|
|
53
|
-
const result: CellValue[][] = [];
|
|
54
|
-
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
55
|
-
const row: CellValue[] = [];
|
|
56
|
-
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
57
|
-
if (createMissing) {
|
|
58
|
-
const cell = this._worksheet.cell(r, c);
|
|
59
|
-
row.push(cell.value);
|
|
60
|
-
} else {
|
|
61
|
-
const cell = this._worksheet.getCellIfExists(r, c);
|
|
62
|
-
row.push(cell?.value ?? null);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
result.push(row);
|
|
66
|
-
}
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Set values in the range from a 2D array
|
|
72
|
-
*/
|
|
73
|
-
set values(data: CellValue[][]) {
|
|
74
|
-
for (let r = 0; r < data.length && r < this.rowCount; r++) {
|
|
75
|
-
const row = data[r];
|
|
76
|
-
for (let c = 0; c < row.length && c < this.colCount; c++) {
|
|
77
|
-
const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
|
|
78
|
-
cell.value = row[c];
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get all formulas in the range as a 2D array
|
|
85
|
-
*/
|
|
86
|
-
get formulas(): (string | undefined)[][] {
|
|
87
|
-
const result: (string | undefined)[][] = [];
|
|
88
|
-
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
89
|
-
const row: (string | undefined)[] = [];
|
|
90
|
-
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
91
|
-
const cell = this._worksheet.cell(r, c);
|
|
92
|
-
row.push(cell.formula);
|
|
93
|
-
}
|
|
94
|
-
result.push(row);
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Set formulas in the range from a 2D array
|
|
101
|
-
*/
|
|
102
|
-
set formulas(data: (string | undefined)[][]) {
|
|
103
|
-
for (let r = 0; r < data.length && r < this.rowCount; r++) {
|
|
104
|
-
const row = data[r];
|
|
105
|
-
for (let c = 0; c < row.length && c < this.colCount; c++) {
|
|
106
|
-
const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
|
|
107
|
-
cell.formula = row[c];
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get the style of the top-left cell
|
|
114
|
-
*/
|
|
115
|
-
get style(): CellStyle {
|
|
116
|
-
return this._worksheet.cell(this._range.start.row, this._range.start.col).style;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Set style for all cells in the range
|
|
121
|
-
*/
|
|
122
|
-
set style(style: CellStyle) {
|
|
123
|
-
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
124
|
-
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
125
|
-
const cell = this._worksheet.cell(r, c);
|
|
126
|
-
cell.style = style;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Iterate over all cells in the range
|
|
133
|
-
*/
|
|
134
|
-
*[Symbol.iterator]() {
|
|
135
|
-
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
136
|
-
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
137
|
-
yield this._worksheet.cell(r, c);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Iterate over cells row by row
|
|
144
|
-
*/
|
|
145
|
-
*rows() {
|
|
146
|
-
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
147
|
-
const row = [];
|
|
148
|
-
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
149
|
-
row.push(this._worksheet.cell(r, c));
|
|
150
|
-
}
|
|
151
|
-
yield row;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
1
|
+
import type { CellValue, CellStyle, RangeAddress } from './types';
|
|
2
|
+
import type { Worksheet } from './worksheet';
|
|
3
|
+
import { toAddress, normalizeRange } from './utils/address';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a range of cells in a worksheet
|
|
7
|
+
*/
|
|
8
|
+
export class Range {
|
|
9
|
+
private _worksheet: Worksheet;
|
|
10
|
+
private _range: RangeAddress;
|
|
11
|
+
|
|
12
|
+
constructor(worksheet: Worksheet, range: RangeAddress) {
|
|
13
|
+
this._worksheet = worksheet;
|
|
14
|
+
this._range = normalizeRange(range);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the range address as a string
|
|
19
|
+
*/
|
|
20
|
+
get address(): string {
|
|
21
|
+
const start = toAddress(this._range.start.row, this._range.start.col);
|
|
22
|
+
const end = toAddress(this._range.end.row, this._range.end.col);
|
|
23
|
+
if (start === end) return start;
|
|
24
|
+
return `${start}:${end}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of rows in the range
|
|
29
|
+
*/
|
|
30
|
+
get rowCount(): number {
|
|
31
|
+
return this._range.end.row - this._range.start.row + 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the number of columns in the range
|
|
36
|
+
*/
|
|
37
|
+
get colCount(): number {
|
|
38
|
+
return this._range.end.col - this._range.start.col + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all values in the range as a 2D array
|
|
43
|
+
*/
|
|
44
|
+
get values(): CellValue[][] {
|
|
45
|
+
return this.getValues();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get all values in the range as a 2D array with options
|
|
50
|
+
*/
|
|
51
|
+
getValues(options: { createMissing?: boolean } = {}): CellValue[][] {
|
|
52
|
+
const { createMissing = true } = options;
|
|
53
|
+
const result: CellValue[][] = [];
|
|
54
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
55
|
+
const row: CellValue[] = [];
|
|
56
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
57
|
+
if (createMissing) {
|
|
58
|
+
const cell = this._worksheet.cell(r, c);
|
|
59
|
+
row.push(cell.value);
|
|
60
|
+
} else {
|
|
61
|
+
const cell = this._worksheet.getCellIfExists(r, c);
|
|
62
|
+
row.push(cell?.value ?? null);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
result.push(row);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Set values in the range from a 2D array
|
|
72
|
+
*/
|
|
73
|
+
set values(data: CellValue[][]) {
|
|
74
|
+
for (let r = 0; r < data.length && r < this.rowCount; r++) {
|
|
75
|
+
const row = data[r];
|
|
76
|
+
for (let c = 0; c < row.length && c < this.colCount; c++) {
|
|
77
|
+
const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
|
|
78
|
+
cell.value = row[c];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all formulas in the range as a 2D array
|
|
85
|
+
*/
|
|
86
|
+
get formulas(): (string | undefined)[][] {
|
|
87
|
+
const result: (string | undefined)[][] = [];
|
|
88
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
89
|
+
const row: (string | undefined)[] = [];
|
|
90
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
91
|
+
const cell = this._worksheet.cell(r, c);
|
|
92
|
+
row.push(cell.formula);
|
|
93
|
+
}
|
|
94
|
+
result.push(row);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set formulas in the range from a 2D array
|
|
101
|
+
*/
|
|
102
|
+
set formulas(data: (string | undefined)[][]) {
|
|
103
|
+
for (let r = 0; r < data.length && r < this.rowCount; r++) {
|
|
104
|
+
const row = data[r];
|
|
105
|
+
for (let c = 0; c < row.length && c < this.colCount; c++) {
|
|
106
|
+
const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
|
|
107
|
+
cell.formula = row[c];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the style of the top-left cell
|
|
114
|
+
*/
|
|
115
|
+
get style(): CellStyle {
|
|
116
|
+
return this._worksheet.cell(this._range.start.row, this._range.start.col).style;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set style for all cells in the range
|
|
121
|
+
*/
|
|
122
|
+
set style(style: CellStyle) {
|
|
123
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
124
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
125
|
+
const cell = this._worksheet.cell(r, c);
|
|
126
|
+
cell.style = style;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Iterate over all cells in the range
|
|
133
|
+
*/
|
|
134
|
+
*[Symbol.iterator]() {
|
|
135
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
136
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
137
|
+
yield this._worksheet.cell(r, c);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Iterate over cells row by row
|
|
144
|
+
*/
|
|
145
|
+
*rows() {
|
|
146
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
147
|
+
const row = [];
|
|
148
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
149
|
+
row.push(this._worksheet.cell(r, c));
|
|
150
|
+
}
|
|
151
|
+
yield row;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/shared-strings.ts
CHANGED
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseXml,
|
|
3
|
-
findElement,
|
|
4
|
-
getChildren,
|
|
5
|
-
getAttr,
|
|
6
|
-
XmlNode,
|
|
7
|
-
stringifyXml,
|
|
8
|
-
createElement,
|
|
9
|
-
createText,
|
|
10
|
-
} from './utils/xml';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Manages the shared strings table from xl/sharedStrings.xml
|
|
14
|
-
* Excel stores strings in a shared table to reduce file size
|
|
15
|
-
*/
|
|
16
|
-
export class SharedStrings {
|
|
17
|
-
private entries: SharedStringEntry[] = [];
|
|
18
|
-
private stringToIndex: Map<string, number> = new Map();
|
|
19
|
-
private _dirty = false;
|
|
20
|
-
private _totalCount = 0;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Parse shared strings from XML content
|
|
24
|
-
*/
|
|
25
|
-
static parse(xml: string): SharedStrings {
|
|
26
|
-
const ss = new SharedStrings();
|
|
27
|
-
const parsed = parseXml(xml);
|
|
28
|
-
const sst = findElement(parsed, 'sst');
|
|
29
|
-
if (!sst) return ss;
|
|
30
|
-
|
|
31
|
-
const countAttr = getAttr(sst, 'count');
|
|
32
|
-
if (countAttr) {
|
|
33
|
-
const total = parseInt(countAttr, 10);
|
|
34
|
-
if (Number.isFinite(total) && total >= 0) {
|
|
35
|
-
ss._totalCount = total;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const children = getChildren(sst, 'sst');
|
|
40
|
-
for (const child of children) {
|
|
41
|
-
if ('si' in child) {
|
|
42
|
-
const siChildren = getChildren(child, 'si');
|
|
43
|
-
const text = ss.extractText(siChildren);
|
|
44
|
-
ss.entries.push({ text, node: child });
|
|
45
|
-
ss.stringToIndex.set(text, ss.entries.length - 1);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (ss._totalCount === 0 && ss.entries.length > 0) {
|
|
50
|
-
ss._totalCount = ss.entries.length;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return ss;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract text from a string item (si element)
|
|
58
|
-
* Handles both simple <t> elements and rich text <r> elements
|
|
59
|
-
*/
|
|
60
|
-
private extractText(nodes: XmlNode[]): string {
|
|
61
|
-
let text = '';
|
|
62
|
-
for (const node of nodes) {
|
|
63
|
-
if ('t' in node) {
|
|
64
|
-
// Simple text: <t>value</t>
|
|
65
|
-
const tChildren = getChildren(node, 't');
|
|
66
|
-
for (const child of tChildren) {
|
|
67
|
-
if ('#text' in child) {
|
|
68
|
-
text += child['#text'] as string;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
} else if ('r' in node) {
|
|
72
|
-
// Rich text: <r><t>value</t></r>
|
|
73
|
-
const rChildren = getChildren(node, 'r');
|
|
74
|
-
for (const rChild of rChildren) {
|
|
75
|
-
if ('t' in rChild) {
|
|
76
|
-
const tChildren = getChildren(rChild, 't');
|
|
77
|
-
for (const child of tChildren) {
|
|
78
|
-
if ('#text' in child) {
|
|
79
|
-
text += child['#text'] as string;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return text;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get a string by index
|
|
91
|
-
*/
|
|
92
|
-
getString(index: number): string | undefined {
|
|
93
|
-
return this.entries[index]?.text;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Add a string and return its index
|
|
98
|
-
* If the string already exists, returns the existing index
|
|
99
|
-
*/
|
|
100
|
-
addString(str: string): number {
|
|
101
|
-
const existing = this.stringToIndex.get(str);
|
|
102
|
-
if (existing !== undefined) {
|
|
103
|
-
this._totalCount++;
|
|
104
|
-
this._dirty = true;
|
|
105
|
-
return existing;
|
|
106
|
-
}
|
|
107
|
-
const index = this.entries.length;
|
|
108
|
-
const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [
|
|
109
|
-
createText(str),
|
|
110
|
-
]);
|
|
111
|
-
const siElement = createElement('si', {}, [tElement]);
|
|
112
|
-
this.entries.push({ text: str, node: siElement });
|
|
113
|
-
this.stringToIndex.set(str, index);
|
|
114
|
-
this._totalCount++;
|
|
115
|
-
this._dirty = true;
|
|
116
|
-
return index;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Check if the shared strings table has been modified
|
|
121
|
-
*/
|
|
122
|
-
get dirty(): boolean {
|
|
123
|
-
return this._dirty;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get the count of strings
|
|
128
|
-
*/
|
|
129
|
-
get count(): number {
|
|
130
|
-
return this.entries.length;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get total usage count of shared strings
|
|
135
|
-
*/
|
|
136
|
-
get totalCount(): number {
|
|
137
|
-
return Math.max(this._totalCount, this.entries.length);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Generate XML for the shared strings table
|
|
142
|
-
*/
|
|
143
|
-
toXml(): string {
|
|
144
|
-
const siElements: XmlNode[] = [];
|
|
145
|
-
for (const entry of this.entries) {
|
|
146
|
-
if (entry.node) {
|
|
147
|
-
siElements.push(entry.node);
|
|
148
|
-
} else {
|
|
149
|
-
const str = entry.text;
|
|
150
|
-
const tElement = createElement(
|
|
151
|
-
't',
|
|
152
|
-
str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {},
|
|
153
|
-
[createText(str)],
|
|
154
|
-
);
|
|
155
|
-
const siElement = createElement('si', {}, [tElement]);
|
|
156
|
-
siElements.push(siElement);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const totalCount = Math.max(this._totalCount, this.entries.length);
|
|
161
|
-
const sst = createElement(
|
|
162
|
-
'sst',
|
|
163
|
-
{
|
|
164
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
165
|
-
count: String(totalCount),
|
|
166
|
-
uniqueCount: String(this.entries.length),
|
|
167
|
-
},
|
|
168
|
-
siElements,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sst])}`;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
interface SharedStringEntry {
|
|
176
|
-
text: string;
|
|
177
|
-
node?: XmlNode;
|
|
178
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
parseXml,
|
|
3
|
+
findElement,
|
|
4
|
+
getChildren,
|
|
5
|
+
getAttr,
|
|
6
|
+
XmlNode,
|
|
7
|
+
stringifyXml,
|
|
8
|
+
createElement,
|
|
9
|
+
createText,
|
|
10
|
+
} from './utils/xml';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages the shared strings table from xl/sharedStrings.xml
|
|
14
|
+
* Excel stores strings in a shared table to reduce file size
|
|
15
|
+
*/
|
|
16
|
+
export class SharedStrings {
|
|
17
|
+
private entries: SharedStringEntry[] = [];
|
|
18
|
+
private stringToIndex: Map<string, number> = new Map();
|
|
19
|
+
private _dirty = false;
|
|
20
|
+
private _totalCount = 0;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse shared strings from XML content
|
|
24
|
+
*/
|
|
25
|
+
static parse(xml: string): SharedStrings {
|
|
26
|
+
const ss = new SharedStrings();
|
|
27
|
+
const parsed = parseXml(xml);
|
|
28
|
+
const sst = findElement(parsed, 'sst');
|
|
29
|
+
if (!sst) return ss;
|
|
30
|
+
|
|
31
|
+
const countAttr = getAttr(sst, 'count');
|
|
32
|
+
if (countAttr) {
|
|
33
|
+
const total = parseInt(countAttr, 10);
|
|
34
|
+
if (Number.isFinite(total) && total >= 0) {
|
|
35
|
+
ss._totalCount = total;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const children = getChildren(sst, 'sst');
|
|
40
|
+
for (const child of children) {
|
|
41
|
+
if ('si' in child) {
|
|
42
|
+
const siChildren = getChildren(child, 'si');
|
|
43
|
+
const text = ss.extractText(siChildren);
|
|
44
|
+
ss.entries.push({ text, node: child });
|
|
45
|
+
ss.stringToIndex.set(text, ss.entries.length - 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (ss._totalCount === 0 && ss.entries.length > 0) {
|
|
50
|
+
ss._totalCount = ss.entries.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return ss;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract text from a string item (si element)
|
|
58
|
+
* Handles both simple <t> elements and rich text <r> elements
|
|
59
|
+
*/
|
|
60
|
+
private extractText(nodes: XmlNode[]): string {
|
|
61
|
+
let text = '';
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
if ('t' in node) {
|
|
64
|
+
// Simple text: <t>value</t>
|
|
65
|
+
const tChildren = getChildren(node, 't');
|
|
66
|
+
for (const child of tChildren) {
|
|
67
|
+
if ('#text' in child) {
|
|
68
|
+
text += child['#text'] as string;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else if ('r' in node) {
|
|
72
|
+
// Rich text: <r><t>value</t></r>
|
|
73
|
+
const rChildren = getChildren(node, 'r');
|
|
74
|
+
for (const rChild of rChildren) {
|
|
75
|
+
if ('t' in rChild) {
|
|
76
|
+
const tChildren = getChildren(rChild, 't');
|
|
77
|
+
for (const child of tChildren) {
|
|
78
|
+
if ('#text' in child) {
|
|
79
|
+
text += child['#text'] as string;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a string by index
|
|
91
|
+
*/
|
|
92
|
+
getString(index: number): string | undefined {
|
|
93
|
+
return this.entries[index]?.text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Add a string and return its index
|
|
98
|
+
* If the string already exists, returns the existing index
|
|
99
|
+
*/
|
|
100
|
+
addString(str: string): number {
|
|
101
|
+
const existing = this.stringToIndex.get(str);
|
|
102
|
+
if (existing !== undefined) {
|
|
103
|
+
this._totalCount++;
|
|
104
|
+
this._dirty = true;
|
|
105
|
+
return existing;
|
|
106
|
+
}
|
|
107
|
+
const index = this.entries.length;
|
|
108
|
+
const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [
|
|
109
|
+
createText(str),
|
|
110
|
+
]);
|
|
111
|
+
const siElement = createElement('si', {}, [tElement]);
|
|
112
|
+
this.entries.push({ text: str, node: siElement });
|
|
113
|
+
this.stringToIndex.set(str, index);
|
|
114
|
+
this._totalCount++;
|
|
115
|
+
this._dirty = true;
|
|
116
|
+
return index;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if the shared strings table has been modified
|
|
121
|
+
*/
|
|
122
|
+
get dirty(): boolean {
|
|
123
|
+
return this._dirty;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the count of strings
|
|
128
|
+
*/
|
|
129
|
+
get count(): number {
|
|
130
|
+
return this.entries.length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get total usage count of shared strings
|
|
135
|
+
*/
|
|
136
|
+
get totalCount(): number {
|
|
137
|
+
return Math.max(this._totalCount, this.entries.length);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate XML for the shared strings table
|
|
142
|
+
*/
|
|
143
|
+
toXml(): string {
|
|
144
|
+
const siElements: XmlNode[] = [];
|
|
145
|
+
for (const entry of this.entries) {
|
|
146
|
+
if (entry.node) {
|
|
147
|
+
siElements.push(entry.node);
|
|
148
|
+
} else {
|
|
149
|
+
const str = entry.text;
|
|
150
|
+
const tElement = createElement(
|
|
151
|
+
't',
|
|
152
|
+
str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {},
|
|
153
|
+
[createText(str)],
|
|
154
|
+
);
|
|
155
|
+
const siElement = createElement('si', {}, [tElement]);
|
|
156
|
+
siElements.push(siElement);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const totalCount = Math.max(this._totalCount, this.entries.length);
|
|
161
|
+
const sst = createElement(
|
|
162
|
+
'sst',
|
|
163
|
+
{
|
|
164
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
165
|
+
count: String(totalCount),
|
|
166
|
+
uniqueCount: String(this.entries.length),
|
|
167
|
+
},
|
|
168
|
+
siElements,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sst])}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface SharedStringEntry {
|
|
176
|
+
text: string;
|
|
177
|
+
node?: XmlNode;
|
|
178
|
+
}
|