amendsheet 0.1.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/COMPARISON.md +47 -0
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/dist/index.cjs +3475 -0
- package/dist/index.d.cts +325 -0
- package/dist/index.d.ts +325 -0
- package/dist/index.js +3468 -0
- package/package.json +86 -0
package/COMPARISON.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Round-trip fidelity
|
|
2
|
+
|
|
3
|
+
Read a real `.xlsx`, write it back, and check that nothing was lost. Measured over 73 files — ordinary workbooks plus ones carrying charts, pivot tables, drawings, macros and defined names — against a pinned version of the incumbent. Generated by `npm run harness:doc`; do not edit by hand.
|
|
4
|
+
|
|
5
|
+
## Results
|
|
6
|
+
|
|
7
|
+
| Library | Clean | Lossy | Failed | Rewrote |
|
|
8
|
+
| --- | --: | --: | --: | --: |
|
|
9
|
+
| amendsheet (this library) | 73 | 0 | 0 | 0 |
|
|
10
|
+
| amendsheet (this library), after editing one cell | 73 | 0 | 0 | 0 |
|
|
11
|
+
| exceljs@4.4.0 | 25 | 46 | 2 | 71 |
|
|
12
|
+
|
|
13
|
+
- **Clean** — every part and every cell value survived. **Lossy** — at least one part or value was dropped or altered. **Failed** — the library threw instead of producing a file.
|
|
14
|
+
- **Rewrote** — files where a part that was never touched came back with different bytes. A clean file can still be rewritten: it means the library reserialised parts it did not need to, which is where an unmodelled detail quietly goes missing.
|
|
15
|
+
|
|
16
|
+
## Where the loss falls
|
|
17
|
+
|
|
18
|
+
**exceljs@4.4.0**
|
|
19
|
+
|
|
20
|
+
| Feature | Files damaged |
|
|
21
|
+
| --- | --: |
|
|
22
|
+
| charts | 12 |
|
|
23
|
+
| drawings | 12 |
|
|
24
|
+
| definedNames | 8 |
|
|
25
|
+
| colWidths | 4 |
|
|
26
|
+
| pivotTables | 3 |
|
|
27
|
+
| pivotCaches | 3 |
|
|
28
|
+
| customXml | 2 |
|
|
29
|
+
| vbaMacros | 1 |
|
|
30
|
+
| mergedCells | 1 |
|
|
31
|
+
| hyperlinks | 1 |
|
|
32
|
+
|
|
33
|
+
## Method
|
|
34
|
+
|
|
35
|
+
- Each library reads the file and writes it back through its own API. The output is unzipped and compared to the input part by part, and every cell value is compared after a second read, so a value that survives the file but decodes differently still counts as a loss.
|
|
36
|
+
- Parts that legitimately differ on every write are excluded: document timestamps (`docProps/core.xml`, `docProps/app.xml`) and the calculation chain, which a spreadsheet rebuilds anyway.
|
|
37
|
+
- Formatting is compared per cell, by the style each cell resolves to, not by counting entries in the style registry — deduping or pruning an unused format is not a loss.
|
|
38
|
+
- The "after editing one cell" pass changes a single cell and asks the same question, so a writer that spills changes across the document beyond the one edit shows up.
|
|
39
|
+
|
|
40
|
+
## Reproduce
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npm run fixtures:real # fetch the corpus
|
|
44
|
+
npm run harness # the full per-file report
|
|
45
|
+
npm run harness:doc # regenerate this file
|
|
46
|
+
```
|
|
47
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jacob Roberson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# AmendSheet
|
|
2
|
+
|
|
3
|
+
Amend `.xlsx` files. Everything you didn't touch stays exactly as it was.
|
|
4
|
+
|
|
5
|
+
Reads and edits spreadsheets in Node and the browser, checked in both. The
|
|
6
|
+
contents of any part the library doesn't interpret get written back byte for
|
|
7
|
+
byte, so charts, pivot tables, drawings and macros all survive a read and a
|
|
8
|
+
save. Changing one cell won't quietly throw away the rest of the workbook.
|
|
9
|
+
|
|
10
|
+
The ZIP container itself is rebuilt, so an untouched file comes back the same
|
|
11
|
+
document but not the same bytes: compression, timestamps and entry order are
|
|
12
|
+
the writer's own.
|
|
13
|
+
|
|
14
|
+
One dependency, `fflate`, for the ZIP container. There is no transitive tree to
|
|
15
|
+
audit.
|
|
16
|
+
|
|
17
|
+
## Fidelity
|
|
18
|
+
|
|
19
|
+
The claim is measured, not asserted. `npm run harness` reads 73 real files —
|
|
20
|
+
Apache POI's test data plus hand-built edge cases — writes each back, and
|
|
21
|
+
compares the result part by part and value by value. Nothing is lost and no
|
|
22
|
+
untouched part is rewritten, before an edit or after one.
|
|
23
|
+
|
|
24
|
+
For a reference point the same files go through `exceljs@4.4.0`, a mature and
|
|
25
|
+
much broader library: it keeps 25 of them intact and drops a part or a value in
|
|
26
|
+
46, most often a chart, a drawing or a defined name. That is the cost of
|
|
27
|
+
rebuilding the whole document on write, and it is the one thing this library sets
|
|
28
|
+
out not to do. The full table and the method are in
|
|
29
|
+
[`COMPARISON.md`](COMPARISON.md), regenerated with `npm run harness:doc`.
|
|
30
|
+
|
|
31
|
+
## On AI
|
|
32
|
+
|
|
33
|
+
Heads up: this was built with heavy use of AI assistance, partly as a test of
|
|
34
|
+
what that produces when held to normal standards. Tests are written first,
|
|
35
|
+
`./verify.sh` gates every commit, and the test files are real spreadsheets this
|
|
36
|
+
library had no hand in creating.
|
|
37
|
+
|
|
38
|
+
## Use
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { readWorkbook } from 'amendsheet'
|
|
42
|
+
|
|
43
|
+
const workbook = readWorkbook(bytes)
|
|
44
|
+
|
|
45
|
+
const sheet = workbook.sheet('Summary') ?? workbook.sheets[0]
|
|
46
|
+
|
|
47
|
+
for (const cell of sheet.cells()) {
|
|
48
|
+
console.log(cell.reference, cell.value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sheet.cell('B7')?.value
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`cells()` yields every cell the sheet stores, which includes cells holding only
|
|
55
|
+
formatting and cells that were cleared. Those arrive as `kind: 'empty'`, so
|
|
56
|
+
filter on `kind` if you only want content.
|
|
57
|
+
|
|
58
|
+
Editing:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const workbook = readWorkbook(bytes)
|
|
62
|
+
const sheet = workbook.sheet('Summary') ?? workbook.sheets[0]
|
|
63
|
+
|
|
64
|
+
sheet.set('B7', 42)
|
|
65
|
+
sheet.set('C1', 'a new cell')
|
|
66
|
+
sheet.set('D2', new Date('2024-01-01'))
|
|
67
|
+
sheet.set('E1', null) // clears the value, keeps the formatting
|
|
68
|
+
sheet.set('F9', { formula: 'SUM(F1:F8)' })
|
|
69
|
+
sheet.set('G1', 1234.5, { numberFormat: '"$"#,##0.00' })
|
|
70
|
+
|
|
71
|
+
const out = workbook.toBytes() // synchronous
|
|
72
|
+
await writeFile('out.xlsx', out)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`set` takes a number, string, boolean, `Date`, `{ formula }`, or `null` to clear
|
|
76
|
+
a cell. Pass `{ numberFormat }` to choose a format code; without one the cell
|
|
77
|
+
keeps whatever formatting it had. If the
|
|
78
|
+
cell or its row isn't there yet, both get created, and the declared dimension
|
|
79
|
+
grows to cover them. The style of a replaced cell is kept, so its formatting
|
|
80
|
+
survives the edit. Writing a `Date` into a cell with no date format applies one,
|
|
81
|
+
reusing a format the file already has where possible.
|
|
82
|
+
|
|
83
|
+
Formatting goes through the same options, and composes into one cell format:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const workbook = readWorkbook(bytes)
|
|
87
|
+
const sheet = workbook.sheet('Summary') ?? workbook.sheets[0]
|
|
88
|
+
|
|
89
|
+
sheet.set('A1', 1234.5, {
|
|
90
|
+
numberFormat: '"$"#,##0.00',
|
|
91
|
+
font: { bold: true, color: 'FF0000' },
|
|
92
|
+
fill: { type: 'solid', color: 'FFFF00' },
|
|
93
|
+
border: { all: { style: 'thin' } },
|
|
94
|
+
alignment: { horizontal: 'center', wrapText: true },
|
|
95
|
+
protection: { locked: false },
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// format() changes only the formatting, so a formula cell keeps its expression.
|
|
99
|
+
sheet.format('B2', { font: { italic: true }, border: { bottom: { style: 'medium' } } })
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A `font` merges onto the cell's current one, so `{ font: { bold: true } }` adds
|
|
103
|
+
bold without disturbing its size or colour. Besides `bold`, `italic`, `size`,
|
|
104
|
+
`color` and `name`, a font carries `strike`, `verticalAlign` (`superscript` or
|
|
105
|
+
`subscript`) and `underline`, which is `true` for a single underline or one of
|
|
106
|
+
`double`, `singleAccounting`, `doubleAccounting`. A `fill` is a discriminated
|
|
107
|
+
union: `{ type: 'solid', color }` for a plain background, or `{ type: 'pattern',
|
|
108
|
+
pattern, color, background }` for one of the ECMA-376 pattern types such as
|
|
109
|
+
`lightGrid`, where `color` is the pattern's foreground. A
|
|
110
|
+
`border` sets sides by name, or `all` at once, merging onto the sides the cell
|
|
111
|
+
already has. An `alignment` places the text — `horizontal`, `vertical`,
|
|
112
|
+
`wrapText`, `textRotation` (0–180, or 255 to stack top to bottom) and `indent` —
|
|
113
|
+
and merges the same way, so setting `wrapText` leaves a horizontal choice alone.
|
|
114
|
+
A `protection` sets `locked` and `hidden`, which take effect once the worksheet
|
|
115
|
+
itself is protected. A colour is an `RRGGBB` or `AARRGGBB` hex string, a theme
|
|
116
|
+
reference `{ theme, tint }` (an index into the workbook's colour scheme; `tint`
|
|
117
|
+
lightens or darkens it, -1 to 1), or an indexed palette entry `{ indexed }`. A
|
|
118
|
+
theme or indexed colour is read back and written as the reference it is, not
|
|
119
|
+
resolved to the hex it displays as, so editing a cell keeps the theme colour its
|
|
120
|
+
font already carried rather than dropping it.
|
|
121
|
+
|
|
122
|
+
`worksheet.protect()` turns that worksheet protection on — it is what makes a
|
|
123
|
+
cell's `locked` and `hidden` bite. With no argument it matches Excel's Protect
|
|
124
|
+
Sheet default: every cell locked, formatting, inserting, deleting, sorting and
|
|
125
|
+
filtering barred, and selecting cells still allowed. Pass options to name the
|
|
126
|
+
actions that stay permitted, each `true` to allow it:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const sheet = readWorkbook(bytes).sheets[0]
|
|
130
|
+
sheet?.protect({ formatCells: true, sort: true, selectLockedCells: false })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
It replaces any protection the sheet already had. Passwords are not written, so
|
|
134
|
+
protection guards against accidental edits, not a determined one.
|
|
135
|
+
`worksheet.protection` reads the protection back in the same shape, or is
|
|
136
|
+
undefined when the sheet is not protected.
|
|
137
|
+
|
|
138
|
+
`worksheet.merge('A1:B2')` merges a range, joining any merges the sheet already
|
|
139
|
+
has. Excel shows only the top-left cell; the others keep their values, since a
|
|
140
|
+
merge does not clear them, and a write to one is refused the way it already was
|
|
141
|
+
for a merge the file came with.
|
|
142
|
+
|
|
143
|
+
`worksheet.setRowHeight(1, 30)` sizes a row in points, and
|
|
144
|
+
`worksheet.setColumnWidth('A', 24)` sizes a column in Excel's width units,
|
|
145
|
+
splitting a `cols` range that spans more than the one column so the rest keeps
|
|
146
|
+
its own width.
|
|
147
|
+
|
|
148
|
+
An edit the format cannot hold is refused by `set` itself, with an `XlsxError`
|
|
149
|
+
naming the cell. `NaN`, an infinity, a character XML has no way to encode, a
|
|
150
|
+
date outside the workbook's epoch, and overwriting the cell that defines a
|
|
151
|
+
shared formula are all refused this way. A refused edit is not recorded, so the
|
|
152
|
+
rest of the batch still writes and `cell()` never reports a value the file is
|
|
153
|
+
not going to receive.
|
|
154
|
+
|
|
155
|
+
`cell.value` is a discriminated union:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
type CellValue =
|
|
159
|
+
| { kind: 'number'; value: number }
|
|
160
|
+
| { kind: 'text'; value: string }
|
|
161
|
+
| { kind: 'boolean'; value: boolean }
|
|
162
|
+
| { kind: 'error'; value: string }
|
|
163
|
+
| { kind: 'empty' }
|
|
164
|
+
| { kind: 'date'; value: Date; serial: number }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
There is no date type in the file format. A date is a number with a date number
|
|
168
|
+
format applied, so `kind: 'date'` comes from resolving the cell's style. The
|
|
169
|
+
`serial` is kept so the stored value can go back unchanged.
|
|
170
|
+
|
|
171
|
+
Which format codes count as dates is a heuristic over the code, and it is not
|
|
172
|
+
frozen: a release may start or stop calling a given format a date, and a cell
|
|
173
|
+
can move between `number` and `date` without the type changing to warn you. Both
|
|
174
|
+
carry the same stored double, in `serial` or `value`, so read it by kind:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const stored =
|
|
178
|
+
value.kind === 'date' ? value.serial : value.kind === 'number' ? value.value : undefined
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
If you need a stable classification, read `cell.numberFormat` and decide for
|
|
182
|
+
yourself.
|
|
183
|
+
|
|
184
|
+
Dates are calendar dates, not instants. `new Date(2024, 0, 1)` writes the serial
|
|
185
|
+
for that day, and reading it back gives a `Date` whose `getDate()` is 1, in
|
|
186
|
+
whatever timezone the code runs in. Build dates the ordinary way rather than
|
|
187
|
+
with `Date.UTC`.
|
|
188
|
+
|
|
189
|
+
Reading a formula gives you `cell.formula`, which is either
|
|
190
|
+
`{ kind: 'expression', expression }` — the source without the leading `=` — or
|
|
191
|
+
`{ kind: 'shared', master }` for a cell that follows a shared formula, naming
|
|
192
|
+
the cell the expression is stored on. A shared formula is written once and
|
|
193
|
+
reused down a column, so the followers hold a cached value and nothing else.
|
|
194
|
+
|
|
195
|
+
Write one with `{ formula }` rather than a string beginning with `=`, so text
|
|
196
|
+
that happens to start with `=` stays text. Nothing here computes a result, so
|
|
197
|
+
the cell is written without one and the workbook is marked for recalculation.
|
|
198
|
+
Until something opens it, that cell reads back with a value of `kind: 'empty'`
|
|
199
|
+
and its expression in `cell.formula`.
|
|
200
|
+
|
|
201
|
+
## Compatibility
|
|
202
|
+
|
|
203
|
+
What a minor release is allowed to do, so you know which branches are safe:
|
|
204
|
+
|
|
205
|
+
- **`XlsxErrorCode` is open.** New codes arrive in minor releases, because there
|
|
206
|
+
is no knowing today every way an xlsx can be malformed. Switch with a default.
|
|
207
|
+
An existing code will not change meaning without a major version.
|
|
208
|
+
`bad-reference` and `unwritable-value` mean the caller passed something the
|
|
209
|
+
library cannot use; every other code is about the file, or the runtime reading
|
|
210
|
+
it, not a value you passed.
|
|
211
|
+
- **`kind: 'date'` is not a frozen classification.** See above.
|
|
212
|
+
- **A `Date` written and read back is the same date.** A serial another
|
|
213
|
+
application wrote can carry finer resolution than a millisecond, which is all
|
|
214
|
+
a `Date` holds.
|
|
215
|
+
|
|
216
|
+
## Not done yet
|
|
217
|
+
|
|
218
|
+
- Overwriting the anchor of a shared formula, an array formula or a data table
|
|
219
|
+
is refused rather than breaking the cells that spill from it. Writing into a
|
|
220
|
+
merged cell that is not the anchor is refused for the same reason: a value
|
|
221
|
+
there would never show.
|
|
222
|
+
- A digital signature survives as a part but any edit invalidates it, since the
|
|
223
|
+
package is rebuilt and the bytes it signed change.
|
|
224
|
+
- Nothing evaluates formulas, so a written one has no value until a spreadsheet
|
|
225
|
+
application opens the file.
|
|
226
|
+
- A part the edit does not touch is copied through still compressed, so a large
|
|
227
|
+
workbook is not fully decompressed to change one cell. A sheet is read and
|
|
228
|
+
patched in its bytes rather than decoded to one string, so the ceiling on a
|
|
229
|
+
single sheet is the largest buffer this runtime allocates rather than V8's
|
|
230
|
+
~512MB string limit; past that a part is refused with `part-too-large`.
|
|
231
|
+
Nothing streams a sheet through in chunks, though, so it is still held whole
|
|
232
|
+
in memory.
|
|
233
|
+
- Charts, pivot tables and drawings are preserved but never created.
|
|
234
|
+
- `cell` exposes a cell's `font`, `fill`, `border` and `alignment`, but only the
|
|
235
|
+
parts this library models. A gradient fill is preserved in the file but not
|
|
236
|
+
reported. A theme colour is reported as its `{ theme, tint }` reference rather
|
|
237
|
+
than resolved to the hex it displays as, so reading the colour a theme names
|
|
238
|
+
would need the theme part this library does not yet interpret.
|
|
239
|
+
- A table grows to include a cell written directly below or to the right of it,
|
|
240
|
+
the way Excel would, adding a column when it grows sideways. Other ranges that
|
|
241
|
+
name cells are still copied, not adjusted: chart ranges, defined names and
|
|
242
|
+
conditional formatting keep the extent they had.
|
|
243
|
+
- Chartsheets and dialogsheets aren't listed in `sheets`, since they hold no
|
|
244
|
+
cells. They're still written back untouched.
|
|
245
|
+
|
|
246
|
+
## Speed
|
|
247
|
+
|
|
248
|
+
`node scripts/bench.mjs` writes into a sheet of the given size, in the four
|
|
249
|
+
shapes that have caused trouble: replacing cells, appending rows, writing dates,
|
|
250
|
+
and reading between every write.
|
|
251
|
+
|
|
252
|
+
Each is linear in the number of edits, and tens of milliseconds against 10,000
|
|
253
|
+
rows on a laptop. Reading between writes used to be quadratic; the same run took
|
|
254
|
+
7 minutes 46 seconds.
|
|
255
|
+
|
|
256
|
+
## Layout
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
src/lib/ the library
|
|
260
|
+
src/harness/ round-trip measurement
|
|
261
|
+
src/adapters/ libraries the harness measures
|
|
262
|
+
src/fixtures/ builds and fetches test files
|
|
263
|
+
fixtures/ the test files themselves
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Verify
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
./verify.sh
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Formats, lints, typechecks, greps for banned constructs, runs the tests with
|
|
273
|
+
coverage thresholds, and checks the built package. Run it before every commit.
|
|
274
|
+
|
|
275
|
+
## How the tests are built
|
|
276
|
+
|
|
277
|
+
Coverage says a line ran, not that its output was right, so three things sit on
|
|
278
|
+
top of the ordinary tests.
|
|
279
|
+
|
|
280
|
+
**Invariants over fragments.** `src/testing/invariants.ts` holds the assertions
|
|
281
|
+
every write must satisfy: the sheet is well formed, cells sit inside rows, no
|
|
282
|
+
reference appears twice, and no part outside the edited sheet changed. Checking
|
|
283
|
+
for a substring passes on output that contains the right fragment inside a
|
|
284
|
+
broken document.
|
|
285
|
+
|
|
286
|
+
**Properties over examples.** `properties.test.ts` generates edits against all
|
|
287
|
+
60 real files and asserts what must hold for any of them: the sheet still
|
|
288
|
+
parses, edited cells read back as written, untouched cells are untouched, and
|
|
289
|
+
the order edits were made in does not matter. The seed is fixed so a failure
|
|
290
|
+
reproduces, and a failing case is shrunk to the fewest edits that still fail.
|
|
291
|
+
Hand-written tests only cover failure modes somebody already imagined.
|
|
292
|
+
|
|
293
|
+
**Mutation testing.** `npm run mutate` breaks the library one edit at a time and
|
|
294
|
+
checks that some test notices. It is slow, so it is not part of `verify.sh`.
|
|
295
|
+
A survivor is either a real gap or a mutation that changes nothing; both need
|
|
296
|
+
reading.
|
|
297
|
+
|
|
298
|
+
## Checking against something that isn't us
|
|
299
|
+
|
|
300
|
+
Every check above validates against this library's own reader, so a file we
|
|
301
|
+
write wrongly and read back wrongly looks right everywhere. `npm run external`
|
|
302
|
+
writes a value into each of the 60 real files and converts the result with
|
|
303
|
+
LibreOffice, which is a different OOXML implementation with its own opinions
|
|
304
|
+
about what is valid.
|
|
305
|
+
|
|
306
|
+
Each file is converted before and after the edit. A fixture LibreOffice can't
|
|
307
|
+
open to begin with proves nothing, so only one that opened before and fails
|
|
308
|
+
after counts. Currently 60 of 60 open after editing and LibreOffice finds the
|
|
309
|
+
written value in all of them.
|
|
310
|
+
|
|
311
|
+
It needs LibreOffice installed and takes a few minutes, so it isn't part of
|
|
312
|
+
`./verify.sh`.
|
|
313
|
+
|
|
314
|
+
`npm run browser` is the other outside check. It bundles the built library with
|
|
315
|
+
fflate, then reads a fixture, edits a cell and writes it back inside a headless
|
|
316
|
+
Chrome, confirming the code runs where no Node API exists. It drives Chrome over
|
|
317
|
+
the DevTools protocol directly, so it adds no dependency, and `./verify.sh` runs
|
|
318
|
+
it, skipping cleanly on a machine with no Chrome.
|
|
319
|
+
|
|
320
|
+
## Round-trip harness
|
|
321
|
+
|
|
322
|
+
`npm run harness` reads every test file and reports what changed: missing ZIP
|
|
323
|
+
parts, markup features whose count dropped, and cell values that differ. It runs
|
|
324
|
+
twice. Once writing the file straight back out, which measures the container and
|
|
325
|
+
nothing else. Then again after writing a cell past the last row in use, which is
|
|
326
|
+
the measurement that matches the claim — every existing cell has to come back
|
|
327
|
+
unchanged, and the only parts allowed to differ are the sheet the edit landed
|
|
328
|
+
in, the four parts an edit legitimately rewrites (`styles.xml`,
|
|
329
|
+
`sharedStrings.xml`, `workbook.xml`, `[Content_Types].xml`), and
|
|
330
|
+
`calcChain.xml`, which is deleted so the reader recomputes the values a formula
|
|
331
|
+
edit invalidates.
|
|
332
|
+
|
|
333
|
+
Nothing is lost or rewritten across the 73 committed files in either pass, and
|
|
334
|
+
both gate `./verify.sh`.
|
|
335
|
+
|
|
336
|
+
## Fixtures
|
|
337
|
+
|
|
338
|
+
- `fixtures/real/` has 60 files from Apache POI's test data, pinned to a commit.
|
|
339
|
+
Real spreadsheet applications wrote these. See `PROVENANCE.md`.
|
|
340
|
+
- `fixtures/quirks/` holds hand-built files for legal but unusual constructs:
|
|
341
|
+
inline strings, missing cell references, rows out of order, a `dimension` that
|
|
342
|
+
lies, the 1904 epoch, the 1900 leap-year bug, shared formulas, columns past Z,
|
|
343
|
+
XML entities, boolean and error cells.
|
|
344
|
+
- `fixtures/generated/` has feature-rich workbooks from another library, used as
|
|
345
|
+
a smoke test. Weaker than the real files, since no JavaScript library wrote
|
|
346
|
+
those.
|
|
347
|
+
- `fixtures/manual/` is for your own files. Gitignored.
|
|
348
|
+
|
|
349
|
+
`npm run fixtures` builds the generated and quirk files. `npm run fixtures:real`
|
|
350
|
+
pulls more from the pinned POI commit.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT, see `LICENSE`.
|
|
355
|
+
|
|
356
|
+
The files in `fixtures/real/` come from Apache POI and stay under Apache-2.0,
|
|
357
|
+
with that project's `LICENSE` and `NOTICE` kept alongside them. They are test
|
|
358
|
+
data only. The published package contains just `dist/`, so none of it ships.
|