@xlsxflow/core 1.1.1 → 1.1.3
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/CHANGELOG.md +122 -94
- package/README.md +5 -3
- package/dist/index.cjs +92 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.mjs +91 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,94 +1,122 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- `
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- `
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- `
|
|
41
|
-
-
|
|
42
|
-
- `
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- `
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.3
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `SheetEditor.setCells` refuses addresses past column XFD or row 1,048,576, instead of writing a file Excel reports as damaged.
|
|
8
|
+
- `SheetWriter` and `SheetEditor` refuse text longer than Excel's 32,767 characters per cell.
|
|
9
|
+
- Colours are checked: ARGB (`FFFF0000`) and RGB (`FF0000`, `#FF0000`) hex are accepted, anything else (`'red'`) throws instead of making Excel repair the file.
|
|
10
|
+
- Picture anchors past the last column or row throw; a range given the wrong way round (`F20:A2`) is turned round.
|
|
11
|
+
- `SheetReader`: a shared string index past the end of the table reads as an empty cell, not as the index.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `checkCellText` is exported, for `@xlsxflow/pro`.
|
|
16
|
+
|
|
17
|
+
## 1.1.2
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `SheetWriter` throws an error for a row longer than 16,384 cells or past row 1,048,576, instead of writing a file Excel reports as damaged.
|
|
22
|
+
- `SheetEditor.setCells`: a date in a cell without a date format gets `yyyy-mm-dd` (or `yyyy-mm-dd hh:mm:ss` with a time), as in `SheetWriter`, instead of showing as a serial number.
|
|
23
|
+
- `OdsWriter` stores formula results, so readers that don't recalculate show them.
|
|
24
|
+
- `sheetToJson` keeps every column when headers repeat: the second `Name` becomes `Name_2`.
|
|
25
|
+
- `parseCsv` returns `null` for empty unquoted fields with `{ convert: false }` too.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `StylePatcher` is exported, for `@xlsxflow/pro`.
|
|
30
|
+
|
|
31
|
+
## 1.1.1
|
|
32
|
+
|
|
33
|
+
- README: the XlsxFlow website address, and Pro at a flat $5.
|
|
34
|
+
- More npm keywords: `xls` and `ods`.
|
|
35
|
+
|
|
36
|
+
## 1.1.0
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `SheetWriter`: workbook properties (title, author, company and so on), defined names, hidden and very hidden sheets, and sheet views (zoom, gridlines, headings, right-to-left). `SheetReader.readWorkbook` reads them back.
|
|
41
|
+
- `{ formatted: true }` reports each cell's text as Excel shows it, from its number format.
|
|
42
|
+
- `SheetEditor.insertRows`, `deleteRows`, `insertColumns` and `deleteColumns`. Everything that points at the moved cells moves with them: formulas on all sheets, defined names, merges, conditional formats, validations, hyperlinks, filters, page breaks, column widths, tables, pictures, notes, sparklines, data tables, chart series and pivot sources.
|
|
43
|
+
- `SheetEditor.setCells` edits and restyles cells of existing files; `addSheet` and `deleteSheet` add and remove sheets.
|
|
44
|
+
- `parseCsv` streams CSV rows, which `SheetWriter.addSheet` turns into a sheet.
|
|
45
|
+
- `SheetReader` reads Excel 97-2003 `.xls` files and OpenDocument `.ods` files: values, dates, formula results, merges, hidden rows, columns and sheets, frozen panes, defined names and document properties. `.ods` also gives formulas, hyperlinks and notes.
|
|
46
|
+
- `OdsWriter` writes `.ods` files: values, dates, formulas, merges, column widths, frozen panes and hidden sheets.
|
|
47
|
+
- Password-protected `.xlsx` files are rejected with an error that points to `decryptWorkbook` in `@xlsxflow/pro`, instead of a ZIP error.
|
|
48
|
+
- Cell notes on write (plain or formatted text) and `getComments()` on read.
|
|
49
|
+
- Conditional formats `cellIs`, `expression`, `top10`, `aboveAverage`, text rules, `duplicateValues`/`uniqueValues` and `iconSet`.
|
|
50
|
+
- Excel tables, sheet protection, page setup (margins, header and footer, print area and titles), row heights, hidden rows and columns, outline grouping, tab colour, and validation operators and messages.
|
|
51
|
+
- `Date` cell values, hyperlinks (URLs and locations in the workbook), `autoFilter`, and an optional shared string table on write.
|
|
52
|
+
- Reading returns formulas (`formulas: true`, shared formulas expanded), styles (`styles: true`) and rich text (`richText: true`). Theme and indexed colours are resolved to ARGB.
|
|
53
|
+
- PNG, JPEG and GIF images (`images` sheet option), anchored to a cell or stretched over a range. `getImages()` reads them back.
|
|
54
|
+
- Low-level parts are exported for add-ons: the ZIP reader and writer, `resolveWorkbookParts`, `readSharedStrings`, and `mapFormulaRefs`/`shiftFormula`.
|
|
55
|
+
- Tested: `.xlsm` files keep their macros through `SheetEditor`; the library runs in Cloudflare Workers without `nodejs_compat` (`scripts/workers` checks it).
|
|
56
|
+
|
|
57
|
+
### Security
|
|
58
|
+
|
|
59
|
+
Fixed crashes, hangs and memory blowups caused by crafted files:
|
|
60
|
+
|
|
61
|
+
- A ZIP directory pointing outside the file crashed Node.
|
|
62
|
+
- Unclosed tags and elements made the parser quadratic. The XML tokenizer is now linear on giant tags, text nodes and CDATA.
|
|
63
|
+
- Repeated hidden columns, far-right cells, fraction formats with long denominators and large style tables were slow or used unbounded memory.
|
|
64
|
+
- One deflated entry could be read many times under different names.
|
|
65
|
+
|
|
66
|
+
Also:
|
|
67
|
+
|
|
68
|
+
- `SheetEditor.edit` takes `maxUncompressedBytes`.
|
|
69
|
+
- Option values typed as enums are escaped, and appended rows encode control characters.
|
|
70
|
+
- `sheetToJson` keeps a `__proto__` header as an ordinary key.
|
|
71
|
+
- In-memory parts are capped at 1 GiB uncompressed by default.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- Sheets with frozen panes were all marked as selected, so Excel opened them grouped.
|
|
76
|
+
- The reader dropped or corrupted cells at stream chunk boundaries.
|
|
77
|
+
- Styles pointed at the wrong font, fill or border, and styles used only by `AsyncIterable` rows were missing from `styles.xml`.
|
|
78
|
+
- Number formats with quoted text, escapes or colours (`#,##0.00 "USD"`, `[Red]0.0`) were taken for date formats.
|
|
79
|
+
- Reading a corrupt file without calling `getMetadata()` caused an unhandled promise rejection, which is fatal in Node.
|
|
80
|
+
- `addSheet` accepted sheet names Excel refuses to open. It now throws.
|
|
81
|
+
- Files Excel would repair:
|
|
82
|
+
- gradient stops were written as `<gradientStop>`;
|
|
83
|
+
- `vertical: 'middle'` was written as is instead of `center`;
|
|
84
|
+
- conditional formats came after validations.
|
|
85
|
+
- Data bar `minValue`/`maxValue` were ignored, every conditional format had priority 1, and colours and ranges were not XML-escaped.
|
|
86
|
+
- Strings with `_xHHHH_`, control characters, CR, or leading and trailing spaces now survive a round trip. `NaN` and `Infinity` are written as `#NUM!`.
|
|
87
|
+
- Reader:
|
|
88
|
+
- Strict OOXML and non-standard part names work;
|
|
89
|
+
- empty `<v/>` reads as empty and `-0` as `0`;
|
|
90
|
+
- out-of-order cells land in the right column;
|
|
91
|
+
- time-only values are no longer a day off, and datetimes keep milliseconds.
|
|
92
|
+
- frozen panes saved by Excel as `frozenSplit` (frozen after a split) are reported.
|
|
93
|
+
- `sheetToJson` ignored its `headerRowIndex` argument.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- The package ships compiled ESM and CommonJS builds with type declarations.
|
|
98
|
+
- Browser bundles no longer try to resolve Node's `fs`.
|
|
99
|
+
- `[Content_Types].xml` is written last in the ZIP.
|
|
100
|
+
- Real backpressure in the ZIP writer and worksheet stream. Producer errors now error the output stream instead of hanging it.
|
|
101
|
+
- About 2.5× faster reads, and a table-driven CRC-32 for faster writes.
|
|
102
|
+
- Styles, formulas and conditional formats are part of the MIT core. Paid add-ons are in the separate `@xlsxflow/pro` package.
|
|
103
|
+
- Benchmarks corrected: the earlier ExcelJS write time came from a cold first run, and write-excel-file was run with an old API.
|
|
104
|
+
|
|
105
|
+
## 1.0.0
|
|
106
|
+
|
|
107
|
+
- `SheetWriter` and `SheetEditor` stream the ZIP with data descriptors instead of buffering the file in memory.
|
|
108
|
+
- Dates are returned as ISO-8601 strings, detected from each cell's number format.
|
|
109
|
+
|
|
110
|
+
## 0.3.0-beta
|
|
111
|
+
|
|
112
|
+
- Workbooks with several sheets.
|
|
113
|
+
- Fixed a buffer bug that made parsing quadratic.
|
|
114
|
+
|
|
115
|
+
## 0.2.0-beta
|
|
116
|
+
|
|
117
|
+
- Deflate compression with `CompressionStream`: a 1M-cell file went from 30 MB to 2.9 MB.
|
|
118
|
+
- Inline and rich text strings are read.
|
|
119
|
+
|
|
120
|
+
## 0.1.0-beta
|
|
121
|
+
|
|
122
|
+
- First release: streaming reader, writer, and shared strings.
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Parts held in memory (workbook, shared strings, styles) are capped at 1 GiB unco
|
|
|
83
83
|
|
|
84
84
|
**Reading files from untrusted users** (uploads on a server): set `maxUncompressedBytes` to what you expect, such as `50_000_000`. Corrupt or crafted ZIPs (overlapping entries, entries that inflate past their stated size, directories pointing outside the file) are rejected either way. The limit also bounds the empty cells added before far-right cells, since one tiny cell in column XFD pads its row to 16,384 values. Values are returned as written: hyperlinks may be `javascript:` URLs and text may start with `=`, so check them before putting them in a web page or a CSV that a spreadsheet will open.
|
|
85
85
|
|
|
86
|
-
`sheetToJson(rows, headerRowIndex = 0)` turns the rows into objects keyed by the header row, and `streamToCsv(rows)` returns the sheet as CSV text. Both hold the whole result in memory.
|
|
86
|
+
`sheetToJson(rows, headerRowIndex = 0)` turns the rows into objects keyed by the header row (a repeated header gets a suffix: `Name`, `Name_2`), and `streamToCsv(rows)` returns the sheet as CSV text. Both hold the whole result in memory.
|
|
87
87
|
|
|
88
88
|
In Node.js, read straight from disk with `XlsxFlow.readFile`, or pass `await createFileReader(path)` to any function that takes a reader:
|
|
89
89
|
|
|
@@ -140,7 +140,7 @@ import { SheetWriter, parseCsv } from '@xlsxflow/core';
|
|
|
140
140
|
const xlsx = new SheetWriter().addSheet('Data', parseCsv(csvStream)).write();
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
Quoted fields can hold delimiters, line breaks and `""`. Unquoted numbers and `TRUE`/`FALSE` become numbers and booleans, and empty fields become empty cells; `{ convert: false }` keeps everything as text, and `{ delimiter: ';' }` sets the delimiter. Dates stay text, since CSV files do not say which date order they use.
|
|
143
|
+
Quoted fields can hold delimiters, line breaks and `""`. Unquoted numbers and `TRUE`/`FALSE` become numbers and booleans, and empty fields become empty cells (`null`; a quoted `""` stays an empty string); `{ convert: false }` keeps everything else as text, and `{ delimiter: ';' }` sets the delimiter. Dates stay text, since CSV files do not say which date order they use.
|
|
144
144
|
|
|
145
145
|
### .xls and .ods Files
|
|
146
146
|
|
|
@@ -158,7 +158,7 @@ and document properties. `formatted: true` works on both; `formulas: true` works
|
|
|
158
158
|
[`@xlsxflow/pro`](#free-and-pro).
|
|
159
159
|
|
|
160
160
|
`OdsWriter` writes `.ods` with the same rows as `SheetWriter`: values, dates, formulas (converted to
|
|
161
|
-
OpenFormula), merged cells, column widths, frozen panes, hidden sheets and document properties. Cell styles,
|
|
161
|
+
OpenFormula, with their results), merged cells, column widths, frozen panes, hidden sheets and document properties. Cell styles,
|
|
162
162
|
hyperlinks, notes and images are not written to `.ods`.
|
|
163
163
|
|
|
164
164
|
```typescript
|
|
@@ -224,6 +224,8 @@ writer.addSheet('Sales', [
|
|
|
224
224
|
});
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
Colours are ARGB hex (`FFC00000`); RGB hex (`C00000` or `#C00000`) is opaque. A cell holds at most 32,767 characters, as in Excel.
|
|
228
|
+
|
|
227
229
|
Other sheet options:
|
|
228
230
|
|
|
229
231
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -350,7 +350,16 @@ async function sheetToJson(parseResult, headerRowIndex = 0) {
|
|
|
350
350
|
if (skip-- > 0)
|
|
351
351
|
continue;
|
|
352
352
|
if (headers.length === 0) {
|
|
353
|
-
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
headers = row.cells.map((c, i) => {
|
|
355
|
+
const base = c !== null && c !== undefined ? String(c) : `Column${i + 1}`;
|
|
356
|
+
// A repeated header gets a suffix ("Name_2") so its column isn't lost
|
|
357
|
+
let name = base;
|
|
358
|
+
for (let n = 2; seen.has(name); n++)
|
|
359
|
+
name = `${base}_${n}`;
|
|
360
|
+
seen.add(name);
|
|
361
|
+
return name;
|
|
362
|
+
});
|
|
354
363
|
continue;
|
|
355
364
|
}
|
|
356
365
|
const obj = {};
|
|
@@ -455,6 +464,21 @@ function decodeXString(s) {
|
|
|
455
464
|
// Excel's sheet size (A..XFD, 1..1048576)
|
|
456
465
|
const MAX_COLUMNS$1 = 16384;
|
|
457
466
|
const MAX_ROWS$1 = 1048576;
|
|
467
|
+
// Excel's limit on the characters in one cell
|
|
468
|
+
const MAX_CELL_TEXT = 32767;
|
|
469
|
+
function checkCellText(s, ref) {
|
|
470
|
+
if (s.length > MAX_CELL_TEXT)
|
|
471
|
+
throw new Error(`Cell ${ref} has ${s.length} characters, more than Excel's ${MAX_CELL_TEXT}.`);
|
|
472
|
+
return s;
|
|
473
|
+
}
|
|
474
|
+
// Colours are ARGB hex ("FFFF0000"); "FF0000" and "#FF0000" get an opaque alpha. Anything else
|
|
475
|
+
// makes Excel repair the file.
|
|
476
|
+
function argb(c) {
|
|
477
|
+
const m = /^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.exec(String(c));
|
|
478
|
+
if (!m)
|
|
479
|
+
throw new Error(`Invalid colour "${c}": use ARGB hex such as "FFFF0000".`);
|
|
480
|
+
return (m[1].length === 6 ? 'FF' + m[1] : m[1]).toUpperCase();
|
|
481
|
+
}
|
|
458
482
|
// 0-based column of letters like "BC" (or a reference like "BC12"); -1 when there are none
|
|
459
483
|
function colIndex(ref) {
|
|
460
484
|
const match = ref.match(/[A-Za-z]+/);
|
|
@@ -554,6 +578,8 @@ function encodeXString(s) {
|
|
|
554
578
|
.replace(/_(?=x[0-9A-Fa-f]{4}_)/g, '_x005F_')
|
|
555
579
|
.replace(/[\x00-\x08\x0B\x0C\r\x0E-\x1F]/g, c => `_x${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}_`);
|
|
556
580
|
}
|
|
581
|
+
// The format SheetWriter gives a date without one: the time only when there is one
|
|
582
|
+
const defaultDateFormat = (d) => d.getTime() % 86400000 === 0 ? 'yyyy-mm-dd' : 'yyyy-mm-dd hh:mm:ss';
|
|
557
583
|
// True when a number format renders dates/times. Ignores quoted text, escapes, colours and
|
|
558
584
|
// locale tags, so "0 \"days\"", "[Red]0.0" and "#,##0 \"USD\"" are not mistaken for dates.
|
|
559
585
|
function isDateFormatCode(code) {
|
|
@@ -1450,7 +1476,8 @@ function parseWorksheet(xmlTokenStream, sharedStrings, styles, is1904 = false, o
|
|
|
1450
1476
|
}
|
|
1451
1477
|
else if (currentCellType === 's') {
|
|
1452
1478
|
const index = parseInt(currentCellValue, 10);
|
|
1453
|
-
|
|
1479
|
+
// An index past the table points nowhere: the cell is empty
|
|
1480
|
+
resolvedValue = sharedStrings.get(index) ?? null;
|
|
1454
1481
|
}
|
|
1455
1482
|
else if (currentCellType === 'b') {
|
|
1456
1483
|
resolvedValue = currentCellValue === '1' || currentCellValue === 'true';
|
|
@@ -1624,9 +1651,10 @@ async function readSheetImages(readText, readBytes, sheetPath) {
|
|
|
1624
1651
|
const escapeXml$4 = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
1625
1652
|
const cellPos = (ref) => {
|
|
1626
1653
|
const m = /^\$?([A-Za-z]{1,3})\$?(\d+)$/.exec(ref.trim());
|
|
1627
|
-
|
|
1654
|
+
const col = m ? colIndex(m[1]) : -1, row = m ? parseInt(m[2], 10) - 1 : -1;
|
|
1655
|
+
if (col < 0 || col >= MAX_COLUMNS$1 || row < 0 || row >= MAX_ROWS$1)
|
|
1628
1656
|
throw new Error(`Invalid anchor cell "${ref}".`);
|
|
1629
|
-
return { col
|
|
1657
|
+
return { col, row };
|
|
1630
1658
|
};
|
|
1631
1659
|
const marker = (tag, { col, row }) => `<xdr:${tag}><xdr:col>${col}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>${row}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:${tag}>`;
|
|
1632
1660
|
const DRAWING_NS = 'xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" ' +
|
|
@@ -1638,9 +1666,11 @@ const DRAWING_NS = 'xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/
|
|
|
1638
1666
|
function anchorXml(place, natural, body, attrs = '') {
|
|
1639
1667
|
if ('range' in place) {
|
|
1640
1668
|
const [from, to = from] = place.range.split(':');
|
|
1641
|
-
const
|
|
1642
|
-
// The object fills the range: it ends at the far edge of the last cell
|
|
1643
|
-
|
|
1669
|
+
const a = cellPos(from), b = cellPos(to);
|
|
1670
|
+
// The object fills the range, given either way round: it ends at the far edge of the last cell
|
|
1671
|
+
const start = { col: Math.min(a.col, b.col), row: Math.min(a.row, b.row) };
|
|
1672
|
+
const end = { col: Math.max(a.col, b.col) + 1, row: Math.max(a.row, b.row) + 1 };
|
|
1673
|
+
return `<xdr:twoCellAnchor editAs="oneCell"${attrs}>${marker('from', start)}${marker('to', end)}${body}<xdr:clientData/></xdr:twoCellAnchor>`;
|
|
1644
1674
|
}
|
|
1645
1675
|
const { width: w, height: h } = natural;
|
|
1646
1676
|
const width = place.width ?? (place.height ? w * place.height / h : w);
|
|
@@ -2625,22 +2655,22 @@ function fontXml(font, nameTag = 'name') {
|
|
|
2625
2655
|
if (font.size)
|
|
2626
2656
|
xml += `<sz val="${escapeXml$3(font.size)}"/>`;
|
|
2627
2657
|
if (font.color)
|
|
2628
|
-
xml += `<color rgb="${
|
|
2658
|
+
xml += `<color rgb="${argb(font.color)}"/>`;
|
|
2629
2659
|
if (font.name)
|
|
2630
2660
|
xml += `<${nameTag} val="${escapeXml$3(font.name)}"/>`;
|
|
2631
2661
|
return xml;
|
|
2632
2662
|
}
|
|
2633
|
-
const borderSideXml = (tag, s) => s ? `<${tag} style="${escapeXml$3(s.style)}">${s.color ? `<color rgb="${
|
|
2663
|
+
const borderSideXml = (tag, s) => s ? `<${tag} style="${escapeXml$3(s.style)}">${s.color ? `<color rgb="${argb(s.color)}"/>` : ''}</${tag}>` : `<${tag}/>`;
|
|
2634
2664
|
function borderXml(b) {
|
|
2635
2665
|
const side = borderSideXml;
|
|
2636
2666
|
return `<border>${side('left', b.left)}${side('right', b.right)}${side('top', b.top)}${side('bottom', b.bottom)}<diagonal/></border>`;
|
|
2637
2667
|
}
|
|
2638
2668
|
function fillXml(fill) {
|
|
2639
2669
|
if (fill.type === 'gradient') {
|
|
2640
|
-
const stops = fill.stops.map(s => `<stop position="${s.position}"><color rgb="${
|
|
2670
|
+
const stops = fill.stops.map(s => `<stop position="${s.position}"><color rgb="${argb(s.color)}"/></stop>`).join('');
|
|
2641
2671
|
return `<fill><gradientFill degree="${escapeXml$3(fill.degree ?? 0)}">${stops}</gradientFill></fill>`;
|
|
2642
2672
|
}
|
|
2643
|
-
return `<fill><patternFill patternType="solid"><fgColor rgb="${
|
|
2673
|
+
return `<fill><patternFill patternType="solid"><fgColor rgb="${argb(fill.fgColor)}"/></patternFill></fill>`;
|
|
2644
2674
|
}
|
|
2645
2675
|
// Attributes of an <alignment> element
|
|
2646
2676
|
function alignmentAttrs(alignment) {
|
|
@@ -2682,7 +2712,7 @@ class StyleEngine {
|
|
|
2682
2712
|
xml += `<numFmt numFmtId="${this.registerNumFmt(style.numFmt)}" formatCode="${escapeXml$3(style.numFmt)}"/>`;
|
|
2683
2713
|
if (style.fill?.type === 'solid') {
|
|
2684
2714
|
// A differential solid fill takes its colour from bgColor
|
|
2685
|
-
const c =
|
|
2715
|
+
const c = argb(style.fill.fgColor);
|
|
2686
2716
|
xml += `<fill><patternFill patternType="solid"><fgColor rgb="${c}"/><bgColor rgb="${c}"/></patternFill></fill>`;
|
|
2687
2717
|
}
|
|
2688
2718
|
if (style.border)
|
|
@@ -2810,7 +2840,7 @@ ${this.dxfs.size ? `<dxfs count="${this.dxfs.size}">${[...this.dxfs.keys()].map(
|
|
|
2810
2840
|
function escapeXml$2(val) {
|
|
2811
2841
|
return String(val).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2812
2842
|
}
|
|
2813
|
-
const color = (rgb) => `<color rgb="${
|
|
2843
|
+
const color = (rgb) => `<color rgb="${argb(rgb)}"/>`;
|
|
2814
2844
|
const bound = (type, value) => value === undefined ? `<cfvo type="${type}"/>` : `<cfvo type="num" val="${escapeXml$2(value)}"/>`;
|
|
2815
2845
|
const formula = (f) => `<formula>${escapeXml$2(String(f).replace(/^=/, ''))}</formula>`;
|
|
2816
2846
|
// Excel string literal inside a formula
|
|
@@ -3362,7 +3392,6 @@ function escapeXml$1(val) {
|
|
|
3362
3392
|
function isStyledCell$1(v) {
|
|
3363
3393
|
return typeof v === 'object' && v !== null && 'value' in v;
|
|
3364
3394
|
}
|
|
3365
|
-
const DAY_MS = 86400000;
|
|
3366
3395
|
// "A1:C10" -> "$A$1:$C$10"
|
|
3367
3396
|
const absoluteRef = (ref) => ref.replace(/([A-Za-z]+)(\d+)/g, '$$$1$$$2');
|
|
3368
3397
|
function stringXml(s) {
|
|
@@ -3640,7 +3669,7 @@ class SheetWriter {
|
|
|
3640
3669
|
const page = options.pageSetup;
|
|
3641
3670
|
const fitToPage = page && (page.fitToWidth !== undefined || page.fitToHeight !== undefined);
|
|
3642
3671
|
const sheetPr = options.tabColor || fitToPage
|
|
3643
|
-
? `<sheetPr>${options.tabColor ? `<tabColor rgb="${
|
|
3672
|
+
? `<sheetPr>${options.tabColor ? `<tabColor rgb="${argb(options.tabColor)}"/>` : ''}${fitToPage ? '<pageSetUpPr fitToPage="1"/>' : ''}</sheetPr>`
|
|
3644
3673
|
: '';
|
|
3645
3674
|
const sheetFormatPr = outlineRow || outlineCol
|
|
3646
3675
|
? `<sheetFormatPr defaultRowHeight="15"${outlineRow ? ` outlineLevelRow="${outlineRow}"` : ''}${outlineCol ? ` outlineLevelCol="${outlineCol}"` : ''}/>`
|
|
@@ -3678,6 +3707,11 @@ class SheetWriter {
|
|
|
3678
3707
|
if (done)
|
|
3679
3708
|
break;
|
|
3680
3709
|
const rowNum = ri + 1;
|
|
3710
|
+
// Past Excel's sheet size the file is damaged, so refuse it instead
|
|
3711
|
+
if (rowNum > MAX_ROWS$1)
|
|
3712
|
+
throw new Error(`Row ${rowNum} is past Excel's last row (${MAX_ROWS$1}).`);
|
|
3713
|
+
if (row.length > MAX_COLUMNS$1)
|
|
3714
|
+
throw new Error(`Row ${rowNum} has ${row.length} cells, more than Excel's ${MAX_COLUMNS$1} columns.`);
|
|
3681
3715
|
chunkStr += optionRowsBefore(rowNum);
|
|
3682
3716
|
if (rowOptions[nextRowOption]?.[0] === rowNum)
|
|
3683
3717
|
nextRowOption++;
|
|
@@ -3688,7 +3722,7 @@ class SheetWriter {
|
|
|
3688
3722
|
let cellStyle = styledCell.style;
|
|
3689
3723
|
if (val instanceof Date && !cellStyle?.numFmt) {
|
|
3690
3724
|
// A date needs a date format, or Excel shows the bare serial number
|
|
3691
|
-
cellStyle = { ...cellStyle, numFmt: val
|
|
3725
|
+
cellStyle = { ...cellStyle, numFmt: defaultDateFormat(val) };
|
|
3692
3726
|
}
|
|
3693
3727
|
const style = cellStyle ? this.styleEngine.registerStyle(cellStyle) : 0;
|
|
3694
3728
|
const sAttr = style > 0 ? ` s="${style}"` : '';
|
|
@@ -3739,6 +3773,7 @@ class SheetWriter {
|
|
|
3739
3773
|
return `<c r="${colRef}"${sAttr}><v>${dateToSerial(val)}</v></c>`;
|
|
3740
3774
|
}
|
|
3741
3775
|
if (typeof val === 'string') {
|
|
3776
|
+
checkCellText(val, colRef);
|
|
3742
3777
|
if (this.writerOptions.sharedStrings) {
|
|
3743
3778
|
let index = this.sharedStrings.get(val);
|
|
3744
3779
|
if (index === undefined)
|
|
@@ -4144,6 +4179,16 @@ class StylePatcher {
|
|
|
4144
4179
|
this.memo.set(key, id);
|
|
4145
4180
|
return id;
|
|
4146
4181
|
}
|
|
4182
|
+
// The format for date `d` in a cell of format `base`: `base` when it already shows dates, otherwise
|
|
4183
|
+
// `base` with a date format, since a date in a General cell shows as its serial number
|
|
4184
|
+
dateFormat(base, d) {
|
|
4185
|
+
const xf = this.xfs[base] ?? this.added.cellXfs[base - this.xfs.length] ?? '';
|
|
4186
|
+
const id = parseInt(attr(/^<[^>]*>/.exec(xf)?.[0] ?? '', 'numFmtId') ?? '0', 10);
|
|
4187
|
+
const code = [...this.numFmts].find(([, v]) => v === id)?.[0];
|
|
4188
|
+
if (isBuiltinDateFormat(id) || (code !== undefined && isDateFormatCode(code)))
|
|
4189
|
+
return base;
|
|
4190
|
+
return this.patch(base, { numFmt: defaultDateFormat(d) });
|
|
4191
|
+
}
|
|
4147
4192
|
toXml() {
|
|
4148
4193
|
let xml = this.xml;
|
|
4149
4194
|
for (const section of ['numFmts', 'fonts', 'fills', 'borders', 'cellXfs']) {
|
|
@@ -4699,7 +4744,7 @@ function editedCell(p, ref, attrs, edit) {
|
|
|
4699
4744
|
}
|
|
4700
4745
|
if (typeof edit === 'object')
|
|
4701
4746
|
return `<${p}c r="${ref}"${attrs}><${p}f>${escapeXml$5(edit.formula.replace(/^=/, ''))}</${p}f></${p}c>`;
|
|
4702
|
-
const text = escapeXml$5(encodeXString(edit));
|
|
4747
|
+
const text = escapeXml$5(encodeXString(checkCellText(edit, ref)));
|
|
4703
4748
|
const t = /^\s|\s$/.test(edit) ? `<${p}t xml:space="preserve">${text}</${p}t>` : `<${p}t>${text}</${p}t>`;
|
|
4704
4749
|
return `<${p}c r="${ref}"${attrs} t="inlineStr"><${p}is>${t}</${p}is></${p}c>`;
|
|
4705
4750
|
}
|
|
@@ -4750,9 +4795,9 @@ class SheetEditor {
|
|
|
4750
4795
|
this.cellEdits.set(sheetName, rows = new Map());
|
|
4751
4796
|
for (const [ref, edit] of Object.entries(cells)) {
|
|
4752
4797
|
const m = /^\$?([A-Za-z]{1,3})\$?(\d+)$/.exec(ref);
|
|
4753
|
-
|
|
4798
|
+
const r = m ? parseInt(m[2], 10) : 0;
|
|
4799
|
+
if (!m || r < 1 || r > MAX_ROWS$1 || colIndex(m[1]) >= MAX_COLUMNS$1)
|
|
4754
4800
|
throw new Error(`Invalid cell reference "${ref}".`);
|
|
4755
|
-
const r = parseInt(m[2], 10);
|
|
4756
4801
|
if (!rows.has(r))
|
|
4757
4802
|
rows.set(r, new Map());
|
|
4758
4803
|
rows.get(r).set(colIndex(m[1]), edit);
|
|
@@ -4938,7 +4983,10 @@ class SheetEditor {
|
|
|
4938
4983
|
let patcher;
|
|
4939
4984
|
const styled = [...editsByPath.values(), ...added.values()]
|
|
4940
4985
|
.some(rows => [...rows.values()].some(cells => [...cells.values()].some(e => styleOf(e))));
|
|
4941
|
-
|
|
4986
|
+
// Dates may need a date format added to their cells
|
|
4987
|
+
const dated = [...editsByPath.values(), ...added.values()]
|
|
4988
|
+
.some(rows => [...rows.values()].some(cells => [...cells.values()].some(e => contentOf(e) instanceof Date)));
|
|
4989
|
+
if (styled || (dated && parts.styles)) {
|
|
4942
4990
|
if (!parts.styles)
|
|
4943
4991
|
throw new Error('Workbook has no styles part.');
|
|
4944
4992
|
patcher = new StylePatcher(await read(parts.styles));
|
|
@@ -5015,6 +5063,10 @@ class SheetEditor {
|
|
|
5015
5063
|
attrs = `${attrs.replace(/\ss="[^"]*"/, '')} s="${patcher.patch(base, style)}"`;
|
|
5016
5064
|
}
|
|
5017
5065
|
const content = contentOf(edit);
|
|
5066
|
+
if (content instanceof Date && patcher) {
|
|
5067
|
+
const base = parseInt(/\ss="(\d+)"/.exec(attrs)?.[1] ?? '0', 10);
|
|
5068
|
+
attrs = `${attrs.replace(/\ss="[^"]*"/, '')} s="${patcher.dateFormat(base, content)}"`;
|
|
5069
|
+
}
|
|
5018
5070
|
if (content === KEEP) {
|
|
5019
5071
|
return existing?.[2] !== undefined ? `<${p}c r="${ref}"${attrs}>${existing[2]}</${p}c>` : `<${p}c r="${ref}"${attrs}/>`;
|
|
5020
5072
|
}
|
|
@@ -5164,7 +5216,7 @@ class SheetEditor {
|
|
|
5164
5216
|
if (typeof val === 'number')
|
|
5165
5217
|
return `<c r="${colRef}"><v>${val}</v></c>`;
|
|
5166
5218
|
if (typeof val === 'string') {
|
|
5167
|
-
return `<c r="${colRef}" t="inlineStr"><is><t xml:space="preserve">${escapeXml$5(encodeXString(val))}</t></is></c>`;
|
|
5219
|
+
return `<c r="${colRef}" t="inlineStr"><is><t xml:space="preserve">${escapeXml$5(encodeXString(checkCellText(val, colRef)))}</t></is></c>`;
|
|
5168
5220
|
}
|
|
5169
5221
|
return `<c r="${colRef}"/>`;
|
|
5170
5222
|
}).join('');
|
|
@@ -5776,7 +5828,8 @@ async function* parseCsv(input, options = {}) {
|
|
|
5776
5828
|
let first = true;
|
|
5777
5829
|
const rows = [];
|
|
5778
5830
|
const endField = () => {
|
|
5779
|
-
|
|
5831
|
+
// An empty unquoted field is null in both modes; a quoted "" stays an empty string
|
|
5832
|
+
row.push(quoted ? field : !convert ? (field === '' ? null : field) : convertField(field));
|
|
5780
5833
|
field = '';
|
|
5781
5834
|
quoted = false;
|
|
5782
5835
|
};
|
|
@@ -5908,11 +5961,14 @@ function textXml(s) {
|
|
|
5908
5961
|
}
|
|
5909
5962
|
const dateValue = (d) => d.toISOString().slice(0, 19); // UTC, like SheetWriter
|
|
5910
5963
|
const hasTime = (d) => d.getTime() % 86400000 !== 0;
|
|
5911
|
-
function cellXml(cell, span, covered) {
|
|
5964
|
+
function cellXml(cell, span, covered, evaluate) {
|
|
5912
5965
|
if (covered)
|
|
5913
5966
|
return '<table:covered-table-cell/>';
|
|
5914
|
-
|
|
5967
|
+
let value = isStyledCell(cell) ? cell.value : cell;
|
|
5915
5968
|
const formula = isStyledCell(cell) && cell.formula ? ` table:formula="${escapeXml(toOpenFormula(cell.formula))}"` : '';
|
|
5969
|
+
// A formula without a value gets its result stored, as SheetWriter does, for readers that don't recalculate
|
|
5970
|
+
if (formula && value == null)
|
|
5971
|
+
value = evaluate(cell.formula);
|
|
5916
5972
|
if (value === null || value === undefined || (typeof value === 'number' && !isFinite(value))) {
|
|
5917
5973
|
return formula || span ? `<table:table-cell${formula}${span}/>` : '<table:table-cell/>';
|
|
5918
5974
|
}
|
|
@@ -6005,6 +6061,15 @@ class OdsWriter {
|
|
|
6005
6061
|
const merges = (sheet.options.mergeCells ?? []).map(parseRange);
|
|
6006
6062
|
const lastMergeRow = Math.max(-1, ...merges.map(m => m.r2));
|
|
6007
6063
|
const mergeAt = (r, c) => merges.find(m => m.r1 === r && m.c1 === c);
|
|
6064
|
+
// Formulas resolve against the sheet they live in; streamed rows can't be looked back at
|
|
6065
|
+
const engine = new FormulaEngine();
|
|
6066
|
+
if (Array.isArray(sheet.rows)) {
|
|
6067
|
+
engine.loadData(sheet.rows.map(row => row.map(cell => {
|
|
6068
|
+
const v = isStyledCell(cell) ? cell.value : cell;
|
|
6069
|
+
return v instanceof Date ? dateToSerial(v) : v ?? null;
|
|
6070
|
+
})));
|
|
6071
|
+
}
|
|
6072
|
+
const evaluate = (f) => engine.evaluate(f);
|
|
6008
6073
|
const covered = (r, c) => merges.some(m => r >= m.r1 && r <= m.r2 && c >= m.c1 && c <= m.c2 && (r !== m.r1 || c !== m.c1));
|
|
6009
6074
|
let r = 0, batch = '';
|
|
6010
6075
|
const writeRow = (row) => {
|
|
@@ -6013,7 +6078,7 @@ class OdsWriter {
|
|
|
6013
6078
|
for (let c = 0; c < width; c++) {
|
|
6014
6079
|
const m = mergeAt(r, c);
|
|
6015
6080
|
const span = m ? ` table:number-columns-spanned="${m.c2 - m.c1 + 1}" table:number-rows-spanned="${m.r2 - m.r1 + 1}"` : '';
|
|
6016
|
-
xml += cellXml(row[c], span, covered(r, c));
|
|
6081
|
+
xml += cellXml(row[c], span, covered(r, c), evaluate);
|
|
6017
6082
|
}
|
|
6018
6083
|
r++;
|
|
6019
6084
|
return xml + (width ? '' : '<table:table-cell/>') + '</table:table-row>';
|
|
@@ -6106,10 +6171,12 @@ exports.SheetEditor = SheetEditor;
|
|
|
6106
6171
|
exports.SheetReader = SheetReader;
|
|
6107
6172
|
exports.SheetWriter = SheetWriter;
|
|
6108
6173
|
exports.StyleEngine = StyleEngine;
|
|
6174
|
+
exports.StylePatcher = StylePatcher;
|
|
6109
6175
|
exports.XlsxFlow = XlsxFlow;
|
|
6110
6176
|
exports.ZipRandomAccessParser = ZipRandomAccessParser;
|
|
6111
6177
|
exports.ZipStreamWriter = ZipStreamWriter;
|
|
6112
6178
|
exports.anchorXml = anchorXml;
|
|
6179
|
+
exports.checkCellText = checkCellText;
|
|
6113
6180
|
exports.colIndex = colIndex;
|
|
6114
6181
|
exports.colLetter = colLetter;
|
|
6115
6182
|
exports.crc32 = crc32;
|