@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.
Files changed (48) hide show
  1. package/README.md +566 -0
  2. package/dist/core/SharedStrings.d.ts +11 -0
  3. package/dist/core/SharedStrings.js +67 -0
  4. package/dist/core/SharedStrings.js.map +1 -0
  5. package/dist/core/Workbook.d.ts +42 -0
  6. package/dist/core/Workbook.js +459 -0
  7. package/dist/core/Workbook.js.map +1 -0
  8. package/dist/core/WorkbookReader.d.ts +43 -0
  9. package/dist/core/WorkbookReader.js +563 -0
  10. package/dist/core/WorkbookReader.js.map +1 -0
  11. package/dist/core/Worksheet.d.ts +78 -0
  12. package/dist/core/Worksheet.js +568 -0
  13. package/dist/core/Worksheet.js.map +1 -0
  14. package/dist/core/properties.d.ts +91 -0
  15. package/dist/core/properties.js +265 -0
  16. package/dist/core/properties.js.map +1 -0
  17. package/dist/core/types.d.ts +388 -0
  18. package/dist/core/types.js +2 -0
  19. package/dist/core/types.js.map +1 -0
  20. package/dist/features/ChartBuilder.d.ts +2 -0
  21. package/dist/features/ChartBuilder.js +165 -0
  22. package/dist/features/ChartBuilder.js.map +1 -0
  23. package/dist/features/TableBuilder.d.ts +2 -0
  24. package/dist/features/TableBuilder.js +36 -0
  25. package/dist/features/TableBuilder.js.map +1 -0
  26. package/dist/index-min.js +259 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.js +7 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/styles/StyleRegistry.d.ts +19 -0
  31. package/dist/styles/StyleRegistry.js +215 -0
  32. package/dist/styles/StyleRegistry.js.map +1 -0
  33. package/dist/styles/builders.d.ts +91 -0
  34. package/dist/styles/builders.js +136 -0
  35. package/dist/styles/builders.js.map +1 -0
  36. package/dist/utils/helpers.d.ts +26 -0
  37. package/dist/utils/helpers.js +85 -0
  38. package/dist/utils/helpers.js.map +1 -0
  39. package/dist/utils/xmlParser.d.ts +17 -0
  40. package/dist/utils/xmlParser.js +179 -0
  41. package/dist/utils/xmlParser.js.map +1 -0
  42. package/dist/utils/zip.d.ts +11 -0
  43. package/dist/utils/zip.js +571 -0
  44. package/dist/utils/zip.js.map +1 -0
  45. package/dist/utils/zipReader.d.ts +6 -0
  46. package/dist/utils/zipReader.js +84 -0
  47. package/dist/utils/zipReader.js.map +1 -0
  48. 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
+ }