@node-projects/excelforge 2.0.0
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/README.md +566 -0
- package/dist/core/SharedStrings.d.ts +11 -0
- package/dist/core/SharedStrings.js +67 -0
- package/dist/core/SharedStrings.js.map +1 -0
- package/dist/core/Workbook.d.ts +42 -0
- package/dist/core/Workbook.js +459 -0
- package/dist/core/Workbook.js.map +1 -0
- package/dist/core/WorkbookReader.d.ts +43 -0
- package/dist/core/WorkbookReader.js +563 -0
- package/dist/core/WorkbookReader.js.map +1 -0
- package/dist/core/Worksheet.d.ts +78 -0
- package/dist/core/Worksheet.js +568 -0
- package/dist/core/Worksheet.js.map +1 -0
- package/dist/core/properties.d.ts +91 -0
- package/dist/core/properties.js +265 -0
- package/dist/core/properties.js.map +1 -0
- package/dist/core/types.d.ts +388 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/features/ChartBuilder.d.ts +2 -0
- package/dist/features/ChartBuilder.js +165 -0
- package/dist/features/ChartBuilder.js.map +1 -0
- package/dist/features/TableBuilder.d.ts +2 -0
- package/dist/features/TableBuilder.js +36 -0
- package/dist/features/TableBuilder.js.map +1 -0
- package/dist/index-min.js +259 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/styles/StyleRegistry.d.ts +19 -0
- package/dist/styles/StyleRegistry.js +215 -0
- package/dist/styles/StyleRegistry.js.map +1 -0
- package/dist/styles/builders.d.ts +91 -0
- package/dist/styles/builders.js +136 -0
- package/dist/styles/builders.js.map +1 -0
- package/dist/utils/helpers.d.ts +26 -0
- package/dist/utils/helpers.js +85 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/xmlParser.d.ts +17 -0
- package/dist/utils/xmlParser.js +179 -0
- package/dist/utils/xmlParser.js.map +1 -0
- package/dist/utils/zip.d.ts +11 -0
- package/dist/utils/zip.js +571 -0
- package/dist/utils/zip.js.map +1 -0
- package/dist/utils/zipReader.d.ts +6 -0
- package/dist/utils/zipReader.js +84 -0
- package/dist/utils/zipReader.js.map +1 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
# ExcelForge π
|
|
2
|
+
|
|
3
|
+
A **complete TypeScript library** for reading and writing Excel `.xlsx` files with **zero external dependencies**. Works in browsers, Node.js, Deno, Bun, and edge runtimes.
|
|
4
|
+
|
|
5
|
+
Inspired by EPPlus (C#), ExcelForge gives you the full power of the OOXML spec β including real DEFLATE compression, round-trip editing of existing files, and rich property support.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
| Category | Features |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **Read existing files** | Load `.xlsx` from file, `Uint8Array`, `base64`, or `Blob` |
|
|
14
|
+
| **Patch-only writes** | Re-serialise only changed sheets; preserve pivot tables, VBA, charts, unknown parts verbatim |
|
|
15
|
+
| **Compression** | Full LZ77 + Huffman DEFLATE (levels 0β9). Typical XML compresses 80β85% |
|
|
16
|
+
| **Cell Values** | Strings, numbers, booleans, dates, formulas, array formulas, rich text |
|
|
17
|
+
| **Styles** | Fonts, solid/pattern/gradient fills, all border styles, alignment, 30+ number format presets |
|
|
18
|
+
| **Layout** | Merge cells, freeze/split panes, column widths, row heights, hide rows/cols, outline grouping |
|
|
19
|
+
| **Charts** | Bar, column (stacked/100%), line, area, pie, doughnut, scatter, radar, bubble |
|
|
20
|
+
| **Images** | PNG, JPEG, GIF β two-cell or one-cell anchors |
|
|
21
|
+
| **Tables** | Styled Excel tables with totals row, filter buttons, column definitions |
|
|
22
|
+
| **Conditional Formatting** | Cell rules, color scales, data bars, icon sets, top/bottom N, above/below average |
|
|
23
|
+
| **Data Validation** | Dropdowns, whole number, decimal, date, time, text length, custom formula |
|
|
24
|
+
| **Sparklines** | Line, bar, stacked β with high/low/first/last/negative colors |
|
|
25
|
+
| **Page Setup** | Paper size, orientation, margins, headers/footers (odd/even/first), print options |
|
|
26
|
+
| **Protection** | Sheet protection with password, cell locking/hiding |
|
|
27
|
+
| **Named Ranges** | Workbook and sheet-scoped |
|
|
28
|
+
| **Auto Filter** | Dropdown filters on column headers |
|
|
29
|
+
| **Hyperlinks** | External URLs, mailto, internal navigation |
|
|
30
|
+
| **Comments** | Cell comments with author |
|
|
31
|
+
| **Multiple Sheets** | Any number, hidden/veryHidden, tab colors |
|
|
32
|
+
| **Core Properties** | Title, author, subject, keywords, description, language, revision, category⦠|
|
|
33
|
+
| **Extended Properties** | Company, manager, application, appVersion, hyperlinkBase, word/line/page counts⦠|
|
|
34
|
+
| **Custom Properties** | Typed key-value store: string, int, decimal, bool, date, r8, i8 |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Copy the src/ directory into your project, or compile to dist/ first:
|
|
42
|
+
tsc --outDir dist --target ES2020 --module NodeNext --moduleResolution NodeNext \
|
|
43
|
+
--declaration --strict --skipLibCheck src/index.ts [all src files]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No `npm install` required β zero runtime dependencies.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick Start β Create a workbook
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { Workbook, style, Colors, NumFmt } from './src/index.js';
|
|
54
|
+
|
|
55
|
+
const wb = new Workbook();
|
|
56
|
+
wb.coreProperties = { title: 'Q4 Report', creator: 'Alice', language: 'en-US' };
|
|
57
|
+
wb.extendedProperties = { company: 'Acme Corp', appVersion: '1.0' };
|
|
58
|
+
|
|
59
|
+
const ws = wb.addSheet('Sales Data');
|
|
60
|
+
|
|
61
|
+
// Header row
|
|
62
|
+
ws.writeRow(1, 1, ['Product', 'Q1', 'Q2', 'Q3', 'Q4', 'Total']);
|
|
63
|
+
for (let c = 1; c <= 6; c++) {
|
|
64
|
+
ws.setStyle(1, c, style().bold().bg(Colors.ExcelBlue).fontColor(Colors.White).center().build());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Data rows
|
|
68
|
+
ws.writeArray(2, 1, [
|
|
69
|
+
['Widget A', 1200, 1350, 1100, 1500],
|
|
70
|
+
['Widget B', 800, 950, 870, 1020],
|
|
71
|
+
['Gadget X', 2100, 1980, 2250, 2400],
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// SUM formulas
|
|
75
|
+
for (let r = 2; r <= 4; r++) {
|
|
76
|
+
ws.setFormula(r, 6, `SUM(B${r}:E${r})`);
|
|
77
|
+
ws.setStyle(r, 6, style().bold().build());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ws.freeze(1, 0); // freeze first row
|
|
81
|
+
|
|
82
|
+
// Output β compression level 6 by default (80β85% smaller than STORE)
|
|
83
|
+
await wb.writeFile('./report.xlsx'); // Node.js
|
|
84
|
+
await wb.download('report.xlsx'); // Browser
|
|
85
|
+
const bytes = await wb.build(); // Uint8Array (any runtime)
|
|
86
|
+
const b64 = await wb.buildBase64(); // base64 string
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Reading & modifying existing files
|
|
92
|
+
|
|
93
|
+
ExcelForge can load existing `.xlsx` files and either read their contents or patch them. Only the sheets you mark as dirty are re-serialised on write; everything else β pivot tables, VBA, drawings, slicers, macros β is preserved verbatim from the original ZIP.
|
|
94
|
+
|
|
95
|
+
### Loading
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Node.js / Deno / Bun
|
|
99
|
+
const wb = await Workbook.fromFile('./existing.xlsx');
|
|
100
|
+
|
|
101
|
+
// Universal (Uint8Array)
|
|
102
|
+
const wb = await Workbook.fromBytes(uint8Array);
|
|
103
|
+
|
|
104
|
+
// Browser (File / Blob input element)
|
|
105
|
+
const wb = await Workbook.fromBlob(fileInputElement.files[0]);
|
|
106
|
+
|
|
107
|
+
// base64 string (e.g. from an API or email attachment)
|
|
108
|
+
const wb = await Workbook.fromBase64(base64String);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Reading data
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
console.log(wb.getSheetNames()); // ['Sheet1', 'Summary', 'Config']
|
|
115
|
+
|
|
116
|
+
const ws = wb.getSheet('Summary');
|
|
117
|
+
const cell = ws.getCell(3, 2); // row 3, col 2
|
|
118
|
+
console.log(cell.value); // 'Q4 Revenue'
|
|
119
|
+
console.log(cell.formula); // 'SUM(B10:B20)'
|
|
120
|
+
console.log(cell.style?.font?.bold); // true
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Modifying and saving
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const wb = await Workbook.fromFile('./report.xlsx');
|
|
127
|
+
const ws = wb.getSheet('Sales');
|
|
128
|
+
|
|
129
|
+
// Make changes
|
|
130
|
+
ws.setValue(5, 3, 99000);
|
|
131
|
+
ws.setStyle(5, 3, style().bg(Colors.LightGreen).build());
|
|
132
|
+
ws.writeRow(20, 1, ['TOTAL', '', '=SUM(C2:C19)']);
|
|
133
|
+
|
|
134
|
+
// Mark the sheet dirty β it will be re-serialised on write.
|
|
135
|
+
// Sheets NOT marked dirty are written back byte-for-byte from the original.
|
|
136
|
+
wb.markDirty('Sales');
|
|
137
|
+
|
|
138
|
+
// Patch properties without re-serialising any sheets
|
|
139
|
+
wb.coreProperties.title = 'Updated Report';
|
|
140
|
+
wb.setCustomProperty('Status', { type: 'string', value: 'Approved' });
|
|
141
|
+
|
|
142
|
+
await wb.writeFile('./report_updated.xlsx');
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Tip:** If you forget to call `markDirty()`, your cell changes won't appear in the output because the original sheet XML will be used. Always call it after modifying a loaded sheet.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Compression
|
|
150
|
+
|
|
151
|
+
ExcelForge includes a full pure-TypeScript DEFLATE implementation (LZ77 lazy matching + dynamic/fixed Huffman coding) with no external dependencies. XML content β the bulk of any `.xlsx` β typically compresses to 80β85% of its original size.
|
|
152
|
+
|
|
153
|
+
### Setting the compression level
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const wb = new Workbook();
|
|
157
|
+
wb.compressionLevel = 6; // 0β9, default 6
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Level | Description | Typical size vs STORE |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `0` | STORE β no compression, fastest | baseline |
|
|
163
|
+
| `1` | FAST β fixed Huffman, minimal LZ77 | ~75% smaller |
|
|
164
|
+
| `6` | **DEFAULT** β dynamic Huffman + lazy LZ77 | ~82% smaller |
|
|
165
|
+
| `9` | BEST β maximum LZ77 effort | ~83% smaller (marginal gain over 6) |
|
|
166
|
+
|
|
167
|
+
Level 6 is the default and the recommended choice β it achieves most of the compression benefit of level 9 at a fraction of the CPU cost.
|
|
168
|
+
|
|
169
|
+
### Per-entry level override
|
|
170
|
+
|
|
171
|
+
The `buildZip` function used internally also supports per-entry overrides, useful if you want images (already compressed) stored uncompressed while XML entries are compressed:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { buildZip } from './src/utils/zip.js';
|
|
175
|
+
|
|
176
|
+
const zip = buildZip([
|
|
177
|
+
{ name: 'xl/worksheets/sheet1.xml', data: xmlBytes }, // uses global level
|
|
178
|
+
{ name: 'xl/media/image1.png', data: pngBytes, level: 0 }, // forced STORE
|
|
179
|
+
{ name: 'xl/styles.xml', data: stylesBytes, level: 9 }, // max compression
|
|
180
|
+
], { level: 6 });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
By default, `buildZip` automatically stores image file types (`png`, `jpg`, `gif`, `tiff`, `emf`, `wmf`) uncompressed since they're already compressed formats.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Document Properties
|
|
188
|
+
|
|
189
|
+
ExcelForge reads and writes all three OOXML property namespaces.
|
|
190
|
+
|
|
191
|
+
### Core properties (`docProps/core.xml`)
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
wb.coreProperties = {
|
|
195
|
+
title: 'Annual Report 2024',
|
|
196
|
+
subject: 'Financial Summary',
|
|
197
|
+
creator: 'Finance Team',
|
|
198
|
+
keywords: 'excel quarterly finance',
|
|
199
|
+
description: 'Auto-generated from ERP export',
|
|
200
|
+
lastModifiedBy: 'Alice',
|
|
201
|
+
revision: '3',
|
|
202
|
+
language: 'en-US',
|
|
203
|
+
category: 'Finance',
|
|
204
|
+
contentStatus: 'Final',
|
|
205
|
+
created: new Date('2024-01-01'),
|
|
206
|
+
// modified is always set to current time on write
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Extended properties (`docProps/app.xml`)
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
wb.extendedProperties = {
|
|
214
|
+
application: 'ExcelForge',
|
|
215
|
+
appVersion: '1.0.0',
|
|
216
|
+
company: 'Acme Corp',
|
|
217
|
+
manager: 'Bob Smith',
|
|
218
|
+
hyperlinkBase: 'https://intranet.acme.com/',
|
|
219
|
+
docSecurity: 0,
|
|
220
|
+
linksUpToDate: true,
|
|
221
|
+
// These are computed automatically on write:
|
|
222
|
+
// titlesOfParts, headingPairs
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Custom properties (`docProps/custom.xml`)
|
|
227
|
+
|
|
228
|
+
Custom properties support typed values β they appear in Excel under **File β Properties β Custom**.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// Set custom properties at workbook level
|
|
232
|
+
wb.customProperties = [
|
|
233
|
+
{ name: 'ProjectCode', value: { type: 'string', value: 'PRJ-2024-007' } },
|
|
234
|
+
{ name: 'Revision', value: { type: 'int', value: 5 } },
|
|
235
|
+
{ name: 'Budget', value: { type: 'decimal', value: 125000.00 } },
|
|
236
|
+
{ name: 'IsApproved', value: { type: 'bool', value: true } },
|
|
237
|
+
{ name: 'ReviewDate', value: { type: 'date', value: new Date() } },
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
// Or use the helper methods
|
|
241
|
+
wb.setCustomProperty('Status', { type: 'string', value: 'In Review' });
|
|
242
|
+
wb.setCustomProperty('Score', { type: 'decimal', value: 9.7 });
|
|
243
|
+
wb.removeCustomProperty('OldField');
|
|
244
|
+
|
|
245
|
+
// Read back
|
|
246
|
+
const proj = wb.getCustomProperty('ProjectCode');
|
|
247
|
+
console.log(proj?.value.value); // 'PRJ-2024-007'
|
|
248
|
+
|
|
249
|
+
// Full list
|
|
250
|
+
for (const p of wb.customProperties) {
|
|
251
|
+
console.log(p.name, p.value.type, p.value.value);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Available value types: `string`, `int`, `decimal`, `bool`, `date`, `r8` (8-byte float), `i8` (BigInt).
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Cell API reference
|
|
260
|
+
|
|
261
|
+
### Writing values
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
ws.setValue(row, col, value); // string | number | boolean | Date
|
|
265
|
+
ws.setFormula(row, col, 'SUM(A1:A5)');
|
|
266
|
+
ws.setArrayFormula(row, col, 'row*col formula', 'A1:C3');
|
|
267
|
+
ws.setStyle(row, col, cellStyle);
|
|
268
|
+
ws.setCell(row, col, { value, formula, style, comment, hyperlink });
|
|
269
|
+
|
|
270
|
+
// Bulk writes
|
|
271
|
+
ws.writeRow(row, startCol, [v1, v2, v3]);
|
|
272
|
+
ws.writeArray(startRow, startCol, [[...], [...], ...]);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Reading values
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const cell = ws.getCell(row, col);
|
|
279
|
+
cell.value // the stored value (string | number | boolean | undefined)
|
|
280
|
+
cell.formula // formula string if present
|
|
281
|
+
cell.style // CellStyle object
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Styles
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { style, Colors, NumFmt, Styles } from './src/index.js';
|
|
288
|
+
|
|
289
|
+
// Fluent builder
|
|
290
|
+
const headerStyle = style()
|
|
291
|
+
.bold()
|
|
292
|
+
.italic()
|
|
293
|
+
.fontSize(13)
|
|
294
|
+
.fontColor(Colors.White)
|
|
295
|
+
.bg(Colors.ExcelBlue)
|
|
296
|
+
.border('thin')
|
|
297
|
+
.center()
|
|
298
|
+
.wrapText()
|
|
299
|
+
.numFmt(NumFmt.Currency)
|
|
300
|
+
.build();
|
|
301
|
+
|
|
302
|
+
// Built-in presets
|
|
303
|
+
ws.setStyle(1, 1, Styles.bold);
|
|
304
|
+
ws.setStyle(1, 2, Styles.headerBlue);
|
|
305
|
+
ws.setStyle(2, 3, Styles.currency);
|
|
306
|
+
ws.setStyle(3, 4, Styles.percent);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Number formats
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
NumFmt.General // General
|
|
313
|
+
NumFmt.Integer // 0
|
|
314
|
+
NumFmt.Decimal2 // #,##0.00
|
|
315
|
+
NumFmt.Currency // $#,##0.00
|
|
316
|
+
NumFmt.Percent // 0%
|
|
317
|
+
NumFmt.Percent2 // 0.00%
|
|
318
|
+
NumFmt.Scientific // 0.00E+00
|
|
319
|
+
NumFmt.ShortDate // mm-dd-yy
|
|
320
|
+
NumFmt.LongDate // d-mmm-yy
|
|
321
|
+
NumFmt.Time // h:mm:ss AM/PM
|
|
322
|
+
NumFmt.DateTime // m/d/yy h:mm
|
|
323
|
+
NumFmt.Accounting // _($* #,##0.00_)
|
|
324
|
+
NumFmt.Text // @
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Layout
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
ws.merge(r1, c1, r2, c2); // merge a range
|
|
331
|
+
ws.mergeByRef('A1:D1');
|
|
332
|
+
ws.freeze(rows, cols); // freeze panes
|
|
333
|
+
ws.setColumn(colIndex, { width: 20, hidden: false, style });
|
|
334
|
+
ws.setRow(rowIndex, { height: 30, hidden: false });
|
|
335
|
+
ws.autoFilter = { ref: 'A1:E1' };
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Conditional formatting
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
ws.addConditionalFormat({
|
|
342
|
+
sqref: 'C2:C100',
|
|
343
|
+
type: 'colorScale',
|
|
344
|
+
colorScale: {
|
|
345
|
+
min: { type: 'min', color: 'FFF8696B' },
|
|
346
|
+
max: { type: 'max', color: 'FF63BE7B' },
|
|
347
|
+
},
|
|
348
|
+
priority: 1,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
ws.addConditionalFormat({
|
|
352
|
+
sqref: 'D2:D100',
|
|
353
|
+
type: 'dataBar',
|
|
354
|
+
dataBar: { color: 'FF638EC6' },
|
|
355
|
+
priority: 2,
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Data validation
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
ws.addDataValidation({
|
|
363
|
+
sqref: 'B2:B100',
|
|
364
|
+
type: 'list',
|
|
365
|
+
formula1: '"North,South,East,West"',
|
|
366
|
+
showDropDown: false,
|
|
367
|
+
errorTitle: 'Invalid Region',
|
|
368
|
+
error: 'Please select a valid region.',
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Charts
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
ws.addChart({
|
|
376
|
+
type: 'bar',
|
|
377
|
+
title: 'Sales by Region',
|
|
378
|
+
series: [{ name: 'Q1 Sales', dataRange: 'Sheet1!B2:B6', catRange: 'Sheet1!A2:A6' }],
|
|
379
|
+
position: { from: { row: 1, col: 8 }, to: { row: 20, col: 16 } },
|
|
380
|
+
legend: { position: 'bottom' },
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Supported chart types: `bar`, `col`, `colStacked`, `col100`, `barStacked`, `bar100`, `line`, `lineStacked`, `area`, `pie`, `doughnut`, `scatter`, `radar`, `bubble`.
|
|
385
|
+
|
|
386
|
+
### Images
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { readFileSync } from 'fs';
|
|
390
|
+
const imgData = readFileSync('./logo.png');
|
|
391
|
+
|
|
392
|
+
ws.addImage({
|
|
393
|
+
data: imgData, // Buffer, Uint8Array, or base64 string
|
|
394
|
+
format: 'png',
|
|
395
|
+
from: { row: 1, col: 1 },
|
|
396
|
+
to: { row: 8, col: 4 },
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Page setup
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
ws.pageSetup = {
|
|
404
|
+
paperSize: 9, // A4
|
|
405
|
+
orientation: 'landscape',
|
|
406
|
+
scale: 90,
|
|
407
|
+
fitToPage: true,
|
|
408
|
+
fitToWidth: 1,
|
|
409
|
+
fitToHeight: 0,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
ws.pageMargins = {
|
|
413
|
+
left: 0.5, right: 0.5, top: 0.75, bottom: 0.75,
|
|
414
|
+
header: 0.3, footer: 0.3,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
ws.headerFooter = {
|
|
418
|
+
oddHeader: '&C&BQ4 Report&B',
|
|
419
|
+
oddFooter: '&LExcelForge&RPage &P of &N',
|
|
420
|
+
};
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Sheet protection
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
ws.protect('mypassword', {
|
|
427
|
+
formatCells: false, // allow formatting
|
|
428
|
+
insertRows: false, // allow inserting rows
|
|
429
|
+
deleteRows: false,
|
|
430
|
+
sort: false,
|
|
431
|
+
autoFilter: false,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Lock individual cells (requires sheet protection to take effect)
|
|
435
|
+
ws.setCell(1, 1, { value: 'Locked', style: { locked: true } });
|
|
436
|
+
ws.setCell(2, 1, { value: 'Editable', style: { locked: false } });
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Output methods
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// Node.js: write to file
|
|
445
|
+
await wb.writeFile('./output.xlsx');
|
|
446
|
+
|
|
447
|
+
// Browser: trigger download
|
|
448
|
+
await wb.download('report.xlsx');
|
|
449
|
+
|
|
450
|
+
// Any runtime: get bytes
|
|
451
|
+
const bytes: Uint8Array = await wb.build();
|
|
452
|
+
const b64: string = await wb.buildBase64();
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## ZIP / Compression API
|
|
458
|
+
|
|
459
|
+
The `buildZip` and `deflateRaw` utilities are exported for direct use:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { buildZip, deflateRaw, type ZipEntry, type ZipOptions } from './src/utils/zip.js';
|
|
463
|
+
|
|
464
|
+
// deflateRaw: compress bytes with raw DEFLATE (no zlib header)
|
|
465
|
+
const compressed = deflateRaw(data, 6); // level 0β9
|
|
466
|
+
|
|
467
|
+
// buildZip: assemble a ZIP archive
|
|
468
|
+
const zip = buildZip(entries, { level: 6 });
|
|
469
|
+
|
|
470
|
+
// ZipEntry shape
|
|
471
|
+
interface ZipEntry {
|
|
472
|
+
name: string;
|
|
473
|
+
data: Uint8Array;
|
|
474
|
+
level?: number; // per-entry override
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ZipOptions shape
|
|
478
|
+
interface ZipOptions {
|
|
479
|
+
level?: number; // global default (0β9)
|
|
480
|
+
noCompress?: string[]; // extensions to always STORE
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Architecture overview
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
ExcelForge
|
|
490
|
+
βββ core/
|
|
491
|
+
β βββ Workbook.ts β orchestrates build/read/patch, holds properties
|
|
492
|
+
β βββ Worksheet.ts β cells, formulas, styles, drawings, page setup
|
|
493
|
+
β βββ WorkbookReader.ts β parse existing XLSX (ZIP β XML β object model)
|
|
494
|
+
β βββ SharedStrings.ts β string deduplication table
|
|
495
|
+
β βββ properties.ts β core / extended / custom property read+write
|
|
496
|
+
β βββ types.ts β all 80+ TypeScript interfaces
|
|
497
|
+
βββ styles/
|
|
498
|
+
β βββ StyleRegistry.ts β interns fonts/fills/borders/xfs, emits styles.xml
|
|
499
|
+
β βββ builders.ts β fluent style() builder, Colors/NumFmt/Styles presets
|
|
500
|
+
βββ features/
|
|
501
|
+
β βββ ChartBuilder.ts β DrawingML chart XML for 15+ chart types
|
|
502
|
+
β βββ TableBuilder.ts β Excel table XML
|
|
503
|
+
βββ utils/
|
|
504
|
+
βββ zip.ts β ZIP writer with full LZ77+Huffman DEFLATE
|
|
505
|
+
βββ zipReader.ts β ZIP reader (STORE + DEFLATE via DecompressionStream)
|
|
506
|
+
βββ xmlParser.ts β roundtrip-safe XML parser (preserves unknown nodes)
|
|
507
|
+
βββ helpers.ts β cell ref math, XML escaping, date serials, EMU conversion
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Round-trip / patch strategy
|
|
511
|
+
|
|
512
|
+
When you load an existing `.xlsx` and call `wb.build()`:
|
|
513
|
+
|
|
514
|
+
1. The original ZIP is read and every entry is retained as raw bytes.
|
|
515
|
+
2. Sheets **not** marked dirty via `wb.markDirty(name)` are written back verbatim β their original bytes are preserved unchanged.
|
|
516
|
+
3. Sheets that **are** marked dirty are re-serialised with any changes applied.
|
|
517
|
+
4. Core/extended/custom properties are always rewritten (they're cheap and typically user-modified).
|
|
518
|
+
5. Styles and shared strings are always rewritten (dirty sheets need fresh indices).
|
|
519
|
+
6. All other parts β drawings, charts, images, pivot tables, VBA modules, custom XML, connections, theme β are preserved verbatim.
|
|
520
|
+
|
|
521
|
+
This means you can safely open a complex Excel file produced by another tool, change a few cells, and save without losing any features ExcelForge doesn't understand.
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Browser usage
|
|
526
|
+
|
|
527
|
+
ExcelForge is fully tree-shakeable and has zero runtime dependencies. In the browser, use `CompressionStream` / `DecompressionStream` (available in all modern browsers since 2022) for decompression when reading files.
|
|
528
|
+
|
|
529
|
+
```html
|
|
530
|
+
<input type="file" id="file" accept=".xlsx">
|
|
531
|
+
<script type="module">
|
|
532
|
+
import { Workbook } from './dist/index.js';
|
|
533
|
+
|
|
534
|
+
document.getElementById('file').addEventListener('change', async (e) => {
|
|
535
|
+
const file = e.target.files[0];
|
|
536
|
+
const wb = await Workbook.fromBlob(file);
|
|
537
|
+
|
|
538
|
+
console.log('Sheets:', wb.getSheetNames());
|
|
539
|
+
console.log('Title:', wb.coreProperties.title);
|
|
540
|
+
|
|
541
|
+
const ws = wb.getSheet(wb.getSheetNames()[0]);
|
|
542
|
+
console.log('A1:', ws.getCell(1, 1).value);
|
|
543
|
+
|
|
544
|
+
// Modify and re-download
|
|
545
|
+
ws.setValue(1, 1, 'Modified!');
|
|
546
|
+
wb.markDirty(wb.getSheetNames()[0]);
|
|
547
|
+
await wb.download('modified.xlsx');
|
|
548
|
+
});
|
|
549
|
+
</script>
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Changelog
|
|
555
|
+
|
|
556
|
+
### v2.0 β Read, Modify, Compress
|
|
557
|
+
|
|
558
|
+
- **Read existing XLSX files** β `Workbook.fromFile()`, `fromBytes()`, `fromBase64()`, `fromBlob()`
|
|
559
|
+
- **Patch-only writes** β preserve unknown parts verbatim, only re-serialise dirty sheets
|
|
560
|
+
- **Full DEFLATE compression** β pure-TypeScript LZ77 + dynamic Huffman (levels 0β9), 80β85% smaller output
|
|
561
|
+
- **Extended & custom properties** β full read/write of `core.xml`, `app.xml`, `custom.xml`
|
|
562
|
+
- **New utilities** β `zipReader.ts`, `xmlParser.ts`, `properties.ts`
|
|
563
|
+
|
|
564
|
+
### v1.0 β Initial release
|
|
565
|
+
|
|
566
|
+
- Full XLSX write support: cells, formulas, styles, charts, images, tables, conditional formatting, data validation, sparklines, page setup, protection, named ranges, auto filter, hyperlinks, comments
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RichTextRun } from '../core/types.js';
|
|
2
|
+
export declare class SharedStrings {
|
|
3
|
+
private table;
|
|
4
|
+
private strings;
|
|
5
|
+
private _count;
|
|
6
|
+
get count(): number;
|
|
7
|
+
get uniqueCount(): number;
|
|
8
|
+
intern(s: string): number;
|
|
9
|
+
internRichText(runs: RichTextRun[]): number;
|
|
10
|
+
toXml(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { escapeXml } from '../utils/helpers.js';
|
|
2
|
+
export class SharedStrings {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.table = new Map();
|
|
5
|
+
this.strings = [];
|
|
6
|
+
this._count = 0;
|
|
7
|
+
}
|
|
8
|
+
get count() { return this._count; }
|
|
9
|
+
get uniqueCount() { return this.strings.length; }
|
|
10
|
+
intern(s) {
|
|
11
|
+
this._count++;
|
|
12
|
+
const existing = this.table.get(s);
|
|
13
|
+
if (existing !== undefined)
|
|
14
|
+
return existing;
|
|
15
|
+
const idx = this.strings.length;
|
|
16
|
+
this.strings.push(s);
|
|
17
|
+
this.table.set(s, idx);
|
|
18
|
+
return idx;
|
|
19
|
+
}
|
|
20
|
+
internRichText(runs) {
|
|
21
|
+
const key = JSON.stringify(runs);
|
|
22
|
+
this._count++;
|
|
23
|
+
const existing = this.table.get(key);
|
|
24
|
+
if (existing !== undefined)
|
|
25
|
+
return existing;
|
|
26
|
+
const xml = runs.map(r => {
|
|
27
|
+
const rPr = r.font ? richFontXml(r.font) : '';
|
|
28
|
+
return `<r>${rPr}<t xml:space="preserve">${escapeXml(r.text)}</t></r>`;
|
|
29
|
+
}).join('');
|
|
30
|
+
const idx = this.strings.length;
|
|
31
|
+
this.strings.push('\x00RICH\x00' + xml);
|
|
32
|
+
this.table.set(key, idx);
|
|
33
|
+
return idx;
|
|
34
|
+
}
|
|
35
|
+
toXml() {
|
|
36
|
+
const items = this.strings.map(s => {
|
|
37
|
+
if (s.startsWith('\x00RICH\x00')) {
|
|
38
|
+
return `<si>${s.slice(6)}</si>`;
|
|
39
|
+
}
|
|
40
|
+
const needsSpace = s !== s.trim() || s.includes('\n');
|
|
41
|
+
return `<si><t${needsSpace ? ' xml:space="preserve"' : ''}>${escapeXml(s)}</t></si>`;
|
|
42
|
+
}).join('');
|
|
43
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
44
|
+
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${this._count}" uniqueCount="${this.strings.length}">
|
|
45
|
+
${items}
|
|
46
|
+
</sst>`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function richFontXml(f) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (f.bold)
|
|
52
|
+
parts.push('<b/>');
|
|
53
|
+
if (f.italic)
|
|
54
|
+
parts.push('<i/>');
|
|
55
|
+
if (f.strike)
|
|
56
|
+
parts.push('<strike/>');
|
|
57
|
+
if (f.underline && f.underline !== 'none')
|
|
58
|
+
parts.push(`<u val="${f.underline}"/>`);
|
|
59
|
+
if (f.size)
|
|
60
|
+
parts.push(`<sz val="${f.size}"/>`);
|
|
61
|
+
if (f.color)
|
|
62
|
+
parts.push(`<color rgb="${f.color.startsWith('#') ? 'FF' + f.color.slice(1) : f.color}"/>`);
|
|
63
|
+
if (f.name)
|
|
64
|
+
parts.push(`<name val="${escapeXml(f.name)}"/>`);
|
|
65
|
+
return parts.length ? `<rPr>${parts.join('')}</rPr>` : '';
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=SharedStrings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SharedStrings.js","sourceRoot":"","sources":["../../src/core/SharedStrings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { WorkbookProperties, NamedRange, WorksheetOptions } from '../core/types.js';
|
|
2
|
+
import { Worksheet } from './Worksheet.js';
|
|
3
|
+
import { type CoreProperties, type ExtendedProperties, type CustomProperty } from './properties.js';
|
|
4
|
+
export declare class Workbook {
|
|
5
|
+
private sheets;
|
|
6
|
+
private namedRanges;
|
|
7
|
+
properties: WorkbookProperties;
|
|
8
|
+
compressionLevel: number;
|
|
9
|
+
coreProperties: CoreProperties;
|
|
10
|
+
extendedProperties: ExtendedProperties;
|
|
11
|
+
customProperties: CustomProperty[];
|
|
12
|
+
private _readResult?;
|
|
13
|
+
private _dirtySheets;
|
|
14
|
+
markDirty(sheetIndexOrName: number | string): void;
|
|
15
|
+
static fromBytes(data: Uint8Array): Promise<Workbook>;
|
|
16
|
+
static fromBase64(b64: string): Promise<Workbook>;
|
|
17
|
+
static fromFile(path: string): Promise<Workbook>;
|
|
18
|
+
static fromBlob(blob: Blob): Promise<Workbook>;
|
|
19
|
+
addSheet(name: string, options?: WorksheetOptions): Worksheet;
|
|
20
|
+
getSheet(name: string): Worksheet | undefined;
|
|
21
|
+
getSheetByIndex(idx: number): Worksheet | undefined;
|
|
22
|
+
getSheetNames(): string[];
|
|
23
|
+
removeSheet(name: string): this;
|
|
24
|
+
addNamedRange(nr: NamedRange): this;
|
|
25
|
+
getCustomProperty(name: string): CustomProperty | undefined;
|
|
26
|
+
setCustomProperty(name: string, value: CustomProperty['value']): this;
|
|
27
|
+
removeCustomProperty(name: string): this;
|
|
28
|
+
build(): Promise<Uint8Array>;
|
|
29
|
+
private _buildPatched;
|
|
30
|
+
private _buildFresh;
|
|
31
|
+
private _syncLegacyProperties;
|
|
32
|
+
private _patchWorkbookXml;
|
|
33
|
+
private _buildWorkbookRels;
|
|
34
|
+
private _relsToXml;
|
|
35
|
+
private _buildRootRels;
|
|
36
|
+
private _patchContentTypes;
|
|
37
|
+
buildBase64(): Promise<string>;
|
|
38
|
+
writeFile(path: string): Promise<void>;
|
|
39
|
+
private _buildCommentsXml;
|
|
40
|
+
private _buildVmlXml;
|
|
41
|
+
download(filename?: string): Promise<void>;
|
|
42
|
+
}
|