xlvane 1.0.0 → 1.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/README.md CHANGED
@@ -6,7 +6,7 @@ Read, inspect and write XLSX files; input files are treated as untrusted. ESM on
6
6
  npm install xlvane
7
7
  ```
8
8
 
9
- The package also contains `docs/` (error codes, JSON schemas, exceljs compatibility), the Agent Skill `skills/xlvane/SKILL.md` and `llms-full.txt` (all of it in one file), for reading without network access: after installing, they are in `node_modules/xlvane/`. Links below point to the same files on GitHub.
9
+ The package also contains `docs/` (error codes, JSON schemas, exceljs compatibility), the Agent Skill `skills/xlvane/SKILL.md` and `llms-full.txt` (all of it in one file), for reading without network access: after installing, they are in `node_modules/xlvane/`. Links below point to the same files on GitHub. The skill can be installed into a project with the [`skills`](https://github.com/vercel-labs/skills) CLI, offline: `npx skills add ./node_modules/xlvane/skills --skill xlvane`.
10
10
 
11
11
  ## Quick start
12
12
 
@@ -30,12 +30,12 @@ From a shell: `npx xlvane inspect orders.xlsx`, then `npx xlvane read orders.xls
30
30
  - [Tasks in one call](#tasks-in-one-call): tables of rows, CSV, JSON, editing files, diff · [inserting and deleting rows](#inserting-and-deleting-rows)
31
31
  - [Large files: streaming](#large-files-streaming)
32
32
  - [Inspecting a workbook](#inspecting-a-workbook) · [Errors and warnings](#errors-and-warnings) · [Untrusted input and limits](#untrusted-input-and-limits)
33
- - [Limitations](#limitations) · [Package size](#package-size)
33
+ - [Limitations](#limitations) · [Package size](#package-size) · [Links](#links)
34
34
  - exceljs users: [`@xlvane/compat`](https://www.npmjs.com/package/@xlvane/compat) and `xlvane migrate`
35
35
 
36
36
  ## Tested environments
37
37
 
38
- The core API (reading, writing, inspect, tables, CSV, editing, diff, streaming with Web Streams) and `@xlvane/compat` are tested in Node.js 22, 24 and 26, Bun, Deno, Chromium (Playwright) and the Cloudflare Workers runtime (workerd), with the same test bundle (`npm run test:runtimes`). Reading is tested on 340 files of the Apache POI test corpus (written by Excel for Windows, Mac and the web, LibreOffice, ONLYOFFICE and others, and damaged files) and on files saved by LibreOffice; see `tests/corpus/`.
38
+ The core API (reading, writing, inspect, tables, CSV, editing, diff, streaming with Web Streams) and `@xlvane/compat` are tested in Node.js 22, 24 and 26, Bun, Deno, Chromium (Playwright) and the Cloudflare Workers runtime (workerd), with the same test bundle (`npm run test:runtimes`). In Deno, import from `npm:xlvane`; in browsers and Workers, through a bundler such as Vite, esbuild or webpack. `xlvane/node` and the command line need Node.js. Reading is tested on 340 files of the Apache POI test corpus (written by Excel for Windows, Mac and the web, LibreOffice, ONLYOFFICE and others, and damaged files) and on files saved by LibreOffice; see `tests/corpus/`.
39
39
 
40
40
  ## Command line in 5 minutes
41
41
 
@@ -105,7 +105,7 @@ In Node.js, `writeWorkbookFile(workbook, 'sales.xlsx')` from `xlvane/node` write
105
105
  - Values: numbers, strings, booleans, `Date`s (stored as serial numbers with a `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss` format unless the style sets one), `{ error: '#N/A' }`, and `{ formula, result }`, also as an array formula (`{ formula, array: 'A1:A3' }`) or a shared formula (`{ formula, shared: 'B1:B9' }` on the first cell, `{ sharedFormula: 'B1' }` on the others). xlvane writes the formula text and the cached result you give; it does not calculate formulas.
106
106
  - Styles: font (name, size, bold, italic, underline, strike, colour, superscript and subscript, family, scheme, charset, outline, shadow), fill (solid colour, pattern, or linear and path gradient), borders (top, right, bottom, left, diagonal), alignment (horizontal, vertical, wrap, shrink to fit, indent, rotation, reading order), cell protection (`locked`, `hidden`) and number format. Identical styles are stored once. `sheet.style(range, style)` merges a style into every cell of a range.
107
107
  - Layout: column widths and hidden columns, row heights and hidden rows, outline levels of rows and columns (`row(n, { outlineLevel: 1, collapsed })`, `column('B', { outlineLevel: 1 })`), merged ranges, frozen rows and columns, autofilter, hidden and very hidden sheets. `sheet.properties({ defaultRowHeight, outlineLevelRow, outlineLevelCol, summaryBelow, summaryRight })` sets the sheet's format properties.
108
- - Workbook: `workbook.views([{ x, y, width, height, firstSheet, activeTab, visibility }])` sets the workbook windows and `workbook.calculation({ fullCalcOnLoad: true })` asks applications to recalculate when the file is opened.
108
+ - Workbook: `workbook.views([{ x, y, width, height, firstSheet, activeTab, visibility }])` sets the workbook windows (`activeTab` and `firstSheet` are checked against the sheets when the workbook is written) and `workbook.calculation({ fullCalcOnLoad: true })` asks applications to recalculate when the file is opened. `addSheet(name, { sheetId })` sets the sheet's id in the file (1 to 65534, unique; other sheets get the lowest unused ids), and reading gives `sheet.sheetId`.
109
109
  - Writer options: `writeWorkbook(workbook, { sharedStrings: false })` writes text inline instead of in a shared string table; `{ compressionLevel: 0 }` (0–9, default 6) stores or compresses the parts. The same options work for `writeWorkbookFile` and, with `compressionLevel`, `writeWorkbookStream`.
110
110
  - Invalid input is rejected when it is set, with an `XlvError` saying what and where, for example `XLV_E_INVALID_SHEET_NAME` or `XLV_E_INVALID_STYLE` with the allowed values.
111
111
  - The output is deterministic: the same workbook always gives the same bytes. The exception is a protection password (`protect({ password })`), whose verifier gets a random salt; pass a fixed salt through `hashPassword` for identical bytes. The test suite opens written files that use all of these features in LibreOffice and checks the values, styles and layout it sees.
@@ -151,7 +151,7 @@ console.log(read.hyperlinks, read.notes, read.cell('B2')?.note?.text, read.tabCo
151
151
 
152
152
  - **Hyperlinks:** `link(range, { url | location, tooltip?, display? })`. A `url` (web page, `mailto:`, file) is stored as an external relationship of the sheet; a `location` (a place in the workbook) is stored without one. The cell text is set with `set`. Reading gives `sheet.hyperlinks` and `cell.hyperlink`; xlvane never opens a target, and `XLV_W_UNUSUAL_HYPERLINK` reports targets other than http(s) or mailto.
153
153
  - **Notes (comments):** `note(cell, text | { text | runs, author?, box? })`; `box` sets the note box: `margins` in millimetres, `autoMargins`, `locked`, `lockText`, `anchor` (`'absolute'`, `'oneCell'`, `'twoCell'`); reading gives the same as `note.box`. The cell needs no value. Notes are written hidden, as spreadsheet applications show them when the pointer rests on the cell. Reading gives `sheet.notes` and `cell.note` (`text`, `author`, and `runs` for rich text). Threaded comments of current Excel versions are not read; their placeholder notes are, with `XLV_W_THREADED_COMMENTS`.
154
- - **Tab colour and view:** `tabColor(color)`, and `view({ zoom, zoomScaleNormal, showGridLines, showRowColHeaders, showRuler, rightToLeft, layout, split, topLeftCell, activeCell })`. Reading gives `sheet.tabColor`, `sheet.view`, `sheet.properties`, `workbook.views` and `workbook.calculation`.
154
+ - **Tab colour and view:** `tabColor(color)`, and `view({ zoom, zoomScaleNormal, showGridLines, showRowColHeaders, showRuler, rightToLeft, layout, split, topLeftCell, activeCell })`; a split window takes `split: { x, y, topLeftCell }` in points, where `topLeftCell` is the first cell shown in the bottom-right pane. Reading gives `sheet.tabColor`, `sheet.view`, `sheet.properties`, `workbook.views` and `workbook.calculation`.
155
155
  - **Streaming:** `writeWorkbookStream` takes `hyperlinks` and `notes` per row (`{ values, hyperlinks: [...], notes: [...] }`, by column position) and `tabColor`/`view` per sheet. `openWorkbookStream` sets `cell.note` on the rows it returns and lists a sheet's notes with `notes(sheet)`; it does not set `cell.hyperlink`.
156
156
  - `editCells` and `appendRows` keep existing links and notes but cannot add new ones yet.
157
157
 
@@ -293,10 +293,10 @@ const { bytes: shorter } = deleteRows(wider, { sheet: 'Orders', at: 10, count: 3
293
293
  ```
294
294
 
295
295
  - Like `editCells`, they change an existing file in place and keep every other part as it was.
296
- - References follow the rows as in a spreadsheet application: formulas on every sheet, merged cells, hyperlinks, notes, the autofilter, conditional formats, data validation, page breaks and defined names. A range grows when rows are inserted inside it and shrinks when some of its rows are deleted; a reference whose cells are all deleted becomes `#REF!`. The rules are in docs/facts/ref-adjust.md (observed in LibreOffice; not yet checked against Excel).
296
+ - References follow the rows as in a spreadsheet application: formulas on every sheet, merged cells, hyperlinks, notes, the autofilter, conditional formats (with their thresholds), data validation, page breaks and defined names. A range grows when rows are inserted inside it and shrinks when some of its rows are deleted; a reference whose cells are all deleted becomes `#REF!`. The rules are in docs/facts/ref-adjust.md (observed in LibreOffice; not yet checked against Excel).
297
297
  - Tables move, grow and shrink with their rows; deleting a table's header row, totals row or all of its data rows is refused.
298
298
  - Pictures move with their rows. Drawings with charts, shapes or form controls, a deletion that would cut a picture, parts xlvane cannot rewrite (charts or pivot tables that may refer to the sheet), and formulas xlvane cannot rewrite safely (for example 3-D references such as `Jan:Mar!A1` that include the sheet) are refused with `XLV_E_NOT_SUPPORTED`. Data that would be pushed past row 1048576 gives `XLV_E_LIMIT_EXCEEDED`.
299
- - They also refuse sheets whose conditional formats use the [MS-XLSX] extension, which xlvane writes for data bars with an extension setting (`gradient`, `borderColor`, `negativeFillColor`, `negativeBorderColor`, `axisPosition`, `axisColor`, `direction`, or an `autoMin`/`autoMax` threshold) and for the icon sets `3Stars`, `3Triangles`, `5Boxes` and `NoIcons`; insert the rows before adding such rules, or add the rules again after the change.
299
+ - Conditional formats and data validations of the [MS-XLSX] extension (extended data bars, the icon sets `3Stars`, `3Triangles`, `5Boxes` and `NoIcons`, and rules written by Excel) move with the rows too, and so do thresholds that refer to cells (`{ type: 'formula', value: '$C$12' }`). Other extended content that refers to cells, such as sparklines or slicers, is refused.
300
300
  - Sheet views (selection, frozen panes) are not changed.
301
301
  - `adjustFormula` and `adjustRange` apply the same rules to formula text you hold yourself.
302
302
 
@@ -335,7 +335,7 @@ const stream = writeWorkbookStream({ sheets: [{ name: 'Data', rows: rows() }] })
335
335
 
336
336
  ## Benchmark
337
337
 
338
- 100 000 rows × 20 columns (numbers, texts, dates, booleans). Median of 5 runs, each in a fresh Node.js 22.22.0 process. Machine: Intel Xeon @ 2.10 GHz, 4 logical cores, 15.7 GiB RAM, Linux (a cloud VM). Measured on 2026-10-30. Peak RSS includes Node.js and the imported libraries; the baseline with all three libraries imported is 135 MiB. Method, raw data and caveats: [bench/perf/RESULTS.md](../../bench/perf/RESULTS.md). Reproduce with `npm run bench`.
338
+ 100 000 rows × 20 columns (numbers, texts, dates, booleans). Median of 5 runs, each in a fresh Node.js 22.22.0 process. Machine: Intel Xeon @ 2.10 GHz, 4 logical cores, 15.7 GiB RAM, Linux (a cloud VM). Measured on 2026-10-30. Peak RSS includes Node.js and the imported libraries; the baseline with all three libraries imported is 135 MiB. Method, raw data and caveats: [bench/perf/RESULTS.md](https://github.com/DevXord/xlvane/blob/main/bench/perf/RESULTS.md). Reproduce with `npm run bench`.
339
339
 
340
340
  | Write | Time, s | Peak RSS, MiB | | Read | Time, s | Peak RSS, MiB |
341
341
  |---|---|---|---|---|---|---|
@@ -392,7 +392,7 @@ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { max
392
392
 
393
393
  ## Package size
394
394
 
395
- **Size budget:** 300 kB of gzip-compressed JavaScript for the library files in `dist/` (level 9, unminified as published; measured 208.6 kB on 2026-10-31). **CLI size budget:** 40 kB for `dist/cli.js`, which is not loaded by `import 'xlvane'` (measured 25.7 kB on 2026-10-31). `npm run check:pkg` fails when the budget is exceeded. The only dependency, fflate, adds about 22 kB gzip (its `esm/browser.js`, measured the same way).
395
+ **Size budget:** 300 kB of gzip-compressed JavaScript for the library files in `dist/` (level 9, unminified as published; measured 209.1 kB for 1.0.0). **CLI size budget:** 40 kB for `dist/cli.js`, which is not loaded by `import 'xlvane'` (measured 27.3 kB for 1.0.0). `npm run check:pkg` fails when the budget is exceeded. The only dependency, fflate, adds about 22 kB gzip (its `esm/browser.js`, measured the same way).
396
396
 
397
397
  ## Limitations
398
398
 
@@ -401,4 +401,10 @@ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { max
401
401
  - Charts, shapes, pivot tables, slicers and form controls are not modelled: reading skips them (with a warning for drawings), `editCells` keeps them unchanged, and row operations refuse sheets where they would have to move.
402
402
  - The command line reads files, not standard input.
403
403
 
404
+ ## Links
405
+
406
+ - Changes in each version: [CHANGELOG.md](https://github.com/DevXord/xlvane/blob/main/CHANGELOG.md)
407
+ - Bugs and questions: [GitHub issues](https://github.com/DevXord/xlvane/issues)
408
+ - Reporting a vulnerability: [SECURITY.md](https://github.com/DevXord/xlvane/blob/main/SECURITY.md)
409
+
404
410
  License: MIT
package/dist/cli.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { It as suggest, Sn as XlvError, _ as isoDate, an as parseRange, b as readWorkbook, g as inspect } from "./options-B7UhudO_.js";
3
- import { a as appendRows, c as fromCSV, d as fromJSON, f as pickSheet, h as toJSON, l as inferValue, n as insertRows, o as editCells, p as readTable, s as diff, t as deleteRows, u as toCSV } from "./rows-Cwte1t8m.js";
4
- import { i as readInput } from "./node-files-uazELnqk.js";
2
+ import { Bt as suggest, En as XlvError, _ as isoDate, b as readWorkbook, g as inspect, ln as parseRange } from "./options-CwSp2pA4.js";
3
+ import { a as appendRows, c as fromCSV, d as fromJSON, f as pickSheet, h as toJSON, l as inferValue, n as insertRows, o as editCells, p as readTable, s as diff, t as deleteRows, u as toCSV } from "./rows-DKwlr0rg.js";
4
+ import { i as readInput } from "./node-files-B0iqSJIJ.js";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { chmod, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
7
  import process from "node:process";
8
- import { parseArgs, styleText } from "node:util";
8
+ import { createRequire } from "node:module";
9
9
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
10
+ // src/cli/node-util.ts
11
+ /** The `node:util` module. */
12
+ const nodeUtil = createRequire(import.meta.url)("node:util");
10
13
  // src/cli/args.ts
11
14
  /** Options every command accepts. */
12
15
  const GLOBAL_OPTIONS = {
@@ -64,7 +67,7 @@ function parseCommandArgs(spec, args) {
64
67
  ...o.short ? { short: o.short } : {},
65
68
  ...o.multiple ? { multiple: true } : {}
66
69
  };
67
- const { tokens } = parseArgs({
70
+ const { tokens } = nodeUtil.parseArgs({
68
71
  args: [...args],
69
72
  options: config,
70
73
  strict: false,
@@ -166,6 +169,259 @@ function headerOption(args, command) {
166
169
  option: "--header"
167
170
  });
168
171
  }
172
+ // src/cli/east-asian-width.ts
173
+ /**
174
+ * Code points whose East_Asian_Width is Wide (W) or Fullwidth (F), including the unassigned code
175
+ * points that default to W, as sorted pairs of first and last code point. Unicode 17.0.0.
176
+ */
177
+ const EAST_ASIAN_WIDE = [
178
+ 4352,
179
+ 4447,
180
+ 8986,
181
+ 8987,
182
+ 9001,
183
+ 9002,
184
+ 9193,
185
+ 9196,
186
+ 9200,
187
+ 9200,
188
+ 9203,
189
+ 9203,
190
+ 9725,
191
+ 9726,
192
+ 9748,
193
+ 9749,
194
+ 9776,
195
+ 9783,
196
+ 9800,
197
+ 9811,
198
+ 9855,
199
+ 9855,
200
+ 9866,
201
+ 9871,
202
+ 9875,
203
+ 9875,
204
+ 9889,
205
+ 9889,
206
+ 9898,
207
+ 9899,
208
+ 9917,
209
+ 9918,
210
+ 9924,
211
+ 9925,
212
+ 9934,
213
+ 9934,
214
+ 9940,
215
+ 9940,
216
+ 9962,
217
+ 9962,
218
+ 9970,
219
+ 9971,
220
+ 9973,
221
+ 9973,
222
+ 9978,
223
+ 9978,
224
+ 9981,
225
+ 9981,
226
+ 9989,
227
+ 9989,
228
+ 9994,
229
+ 9995,
230
+ 10024,
231
+ 10024,
232
+ 10060,
233
+ 10060,
234
+ 10062,
235
+ 10062,
236
+ 10067,
237
+ 10069,
238
+ 10071,
239
+ 10071,
240
+ 10133,
241
+ 10135,
242
+ 10160,
243
+ 10160,
244
+ 10175,
245
+ 10175,
246
+ 11035,
247
+ 11036,
248
+ 11088,
249
+ 11088,
250
+ 11093,
251
+ 11093,
252
+ 11904,
253
+ 11929,
254
+ 11931,
255
+ 12019,
256
+ 12032,
257
+ 12245,
258
+ 12272,
259
+ 12350,
260
+ 12353,
261
+ 12438,
262
+ 12441,
263
+ 12543,
264
+ 12549,
265
+ 12591,
266
+ 12593,
267
+ 12686,
268
+ 12688,
269
+ 12773,
270
+ 12783,
271
+ 12830,
272
+ 12832,
273
+ 12871,
274
+ 12880,
275
+ 42124,
276
+ 42128,
277
+ 42182,
278
+ 43360,
279
+ 43388,
280
+ 44032,
281
+ 55203,
282
+ 63744,
283
+ 64255,
284
+ 65040,
285
+ 65049,
286
+ 65072,
287
+ 65106,
288
+ 65108,
289
+ 65126,
290
+ 65128,
291
+ 65131,
292
+ 65281,
293
+ 65376,
294
+ 65504,
295
+ 65510,
296
+ 94176,
297
+ 94180,
298
+ 94192,
299
+ 94198,
300
+ 94208,
301
+ 101589,
302
+ 101631,
303
+ 101662,
304
+ 101760,
305
+ 101874,
306
+ 110576,
307
+ 110579,
308
+ 110581,
309
+ 110587,
310
+ 110589,
311
+ 110590,
312
+ 110592,
313
+ 110882,
314
+ 110898,
315
+ 110898,
316
+ 110928,
317
+ 110930,
318
+ 110933,
319
+ 110933,
320
+ 110948,
321
+ 110951,
322
+ 110960,
323
+ 111355,
324
+ 119552,
325
+ 119638,
326
+ 119648,
327
+ 119670,
328
+ 126980,
329
+ 126980,
330
+ 127183,
331
+ 127183,
332
+ 127374,
333
+ 127374,
334
+ 127377,
335
+ 127386,
336
+ 127488,
337
+ 127490,
338
+ 127504,
339
+ 127547,
340
+ 127552,
341
+ 127560,
342
+ 127568,
343
+ 127569,
344
+ 127584,
345
+ 127589,
346
+ 127744,
347
+ 127776,
348
+ 127789,
349
+ 127797,
350
+ 127799,
351
+ 127868,
352
+ 127870,
353
+ 127891,
354
+ 127904,
355
+ 127946,
356
+ 127951,
357
+ 127955,
358
+ 127968,
359
+ 127984,
360
+ 127988,
361
+ 127988,
362
+ 127992,
363
+ 128062,
364
+ 128064,
365
+ 128064,
366
+ 128066,
367
+ 128252,
368
+ 128255,
369
+ 128317,
370
+ 128331,
371
+ 128334,
372
+ 128336,
373
+ 128359,
374
+ 128378,
375
+ 128378,
376
+ 128405,
377
+ 128406,
378
+ 128420,
379
+ 128420,
380
+ 128507,
381
+ 128591,
382
+ 128640,
383
+ 128709,
384
+ 128716,
385
+ 128716,
386
+ 128720,
387
+ 128722,
388
+ 128725,
389
+ 128728,
390
+ 128732,
391
+ 128735,
392
+ 128747,
393
+ 128748,
394
+ 128756,
395
+ 128764,
396
+ 128992,
397
+ 129003,
398
+ 129008,
399
+ 129008,
400
+ 129292,
401
+ 129338,
402
+ 129340,
403
+ 129349,
404
+ 129351,
405
+ 129535,
406
+ 129648,
407
+ 129660,
408
+ 129664,
409
+ 129674,
410
+ 129678,
411
+ 129734,
412
+ 129736,
413
+ 129736,
414
+ 129741,
415
+ 129756,
416
+ 129759,
417
+ 129770,
418
+ 129775,
419
+ 129784,
420
+ 131072,
421
+ 196605,
422
+ 196608,
423
+ 262141
424
+ ];
169
425
  // src/cli/output.ts
170
426
  /**
171
427
  * Whether to colour a stream. `util.styleText` respects NO_COLOR, NODE_DISABLE_COLORS,
@@ -208,7 +464,7 @@ var Output = class {
208
464
  this.width = width;
209
465
  }
210
466
  style(format, text) {
211
- return this.color && text !== "" ? styleText(format, text, { validateStream: false }) : text;
467
+ return this.color && text !== "" ? nodeUtil.styleText(format, text, { validateStream: false }) : text;
212
468
  }
213
469
  bold(text) {
214
470
  return this.style("bold", text);
@@ -230,11 +486,11 @@ var Output = class {
230
486
  }
231
487
  /**
232
488
  * An aligned table. Columns are cut with "…" to fit `width`; the widest columns shrink first.
233
- * Width is counted in UTF-16 code units, so wide characters may misalign (documented).
489
+ * Widths are display columns ({@link displayWidth}), so CJK text and emoji stay aligned.
234
490
  */
235
491
  table(headers, rows, indent = "", align = []) {
236
492
  const count = headers.length;
237
- const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => oneLine(r[i] ?? "").length)));
493
+ const widths = headers.map((h, i) => Math.max(displayWidth(oneLine(h)), ...rows.map((r) => displayWidth(oneLine(r[i] ?? "")))));
238
494
  const gap = 2;
239
495
  const available = Math.max(count * 4, this.width - indent.length - gap * (count - 1));
240
496
  let total = widths.reduce((a, b) => a + b, 0);
@@ -248,7 +504,8 @@ var Output = class {
248
504
  const line = (cells, head) => indent + cells.map((c, i) => {
249
505
  const w = widths[i] ?? 0;
250
506
  const text = fit(oneLine(c), w);
251
- const padded = align[i] === "right" && !head ? text.padStart(w) : i === count - 1 ? text : text.padEnd(w);
507
+ const pad = " ".repeat(Math.max(0, w - displayWidth(text)));
508
+ const padded = align[i] === "right" && !head ? pad + text : i === count - 1 ? text : text + pad;
252
509
  return head ? this.bold(padded) : padded;
253
510
  }).join(" ".repeat(gap)).trimEnd();
254
511
  return [line(headers, true), ...rows.map((r) => line(r, false))].join("\n");
@@ -300,8 +557,70 @@ function terminalSafe(text, keepStyles) {
300
557
  }
301
558
  return start === 0 ? text : result + text.slice(start);
302
559
  }
560
+ /** Cuts text to `width` display columns with "…", between grapheme clusters. */
303
561
  function fit(text, width) {
304
- return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}…`;
562
+ if (displayWidth(text) <= width) return text;
563
+ let result = "";
564
+ let used = 0;
565
+ for (const part of graphemes(text)) {
566
+ const w = displayWidth(part);
567
+ if (used + w > width - 1) break;
568
+ result += part;
569
+ used += w;
570
+ }
571
+ return `${result}…`;
572
+ }
573
+ /**
574
+ * Grapheme clusters of a text (Intl.Segmenter), or its code points when the Node.js build has no
575
+ * Intl support, so that cutting never splits a surrogate pair or a letter from its accent.
576
+ */
577
+ function graphemes(text) {
578
+ if (segmenter === void 0) return Array.from(text);
579
+ return Array.from(segmenter.segment(text), (s) => s.segment);
580
+ }
581
+ const segmenter = typeof Intl === "object" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : void 0;
582
+ /**
583
+ * The number of terminal columns a text takes, counted as Node.js counts them for its own output
584
+ * (src/node_i18n.cc GetColumnWidth, v22.23.4; F-UNICODE-01): East Asian Wide and Fullwidth
585
+ * characters (Unicode East_Asian_Width W and F) and emoji with Emoji_Presentation take two
586
+ * columns; control and format characters (except the soft hyphen), nonspacing and enclosing marks
587
+ * and emoji modifiers that are not Wide take none; every other character, including East Asian
588
+ * Ambiguous ones, takes one. Each code point of an emoji sequence is counted, as Node.js does by
589
+ * default, so a terminal that draws the sequence as one picture shows it narrower. Colour codes
590
+ * (`ESC [ … m`, as `Output` writes them) take no columns, as Node.js strips them first.
591
+ */
592
+ function displayWidth(text) {
593
+ if (text.includes("\x1B")) text = text.replace(/\u001b\[[0-9;]*m/g, "");
594
+ let width = 0;
595
+ for (let i = 0; i < text.length; i++) {
596
+ const code = text.charCodeAt(i);
597
+ if (code >= 32 && code < 127) width++;
598
+ else {
599
+ for (const ch of text.slice(i)) width += codePointWidth(ch.codePointAt(0) ?? 0, ch);
600
+ break;
601
+ }
602
+ }
603
+ return width;
604
+ }
605
+ function codePointWidth(code, ch) {
606
+ if (isEastAsianWide(code) || EMOJI_PRESENTATION.test(ch)) return 2;
607
+ if (code !== 173 && ZERO_WIDTH.test(ch)) return 0;
608
+ return 1;
609
+ }
610
+ const EMOJI_PRESENTATION = /^\p{Emoji_Presentation}$/u;
611
+ const ZERO_WIDTH = /^[\p{Cc}\p{Cf}\p{Me}\p{Mn}\p{Emoji_Modifier}]$/u;
612
+ /** Binary search in the sorted [first, last, first, last, …] ranges of EAST_ASIAN_WIDE. */
613
+ function isEastAsianWide(code) {
614
+ if (code < 4352) return false;
615
+ let lo = 0;
616
+ let hi = EAST_ASIAN_WIDE.length / 2 - 1;
617
+ while (lo <= hi) {
618
+ const mid = lo + hi >> 1;
619
+ if (code < EAST_ASIAN_WIDE[mid * 2]) hi = mid - 1;
620
+ else if (code > EAST_ASIAN_WIDE[mid * 2 + 1]) lo = mid + 1;
621
+ else return true;
622
+ }
623
+ return false;
305
624
  }
306
625
  /** Quotes an argument for a POSIX shell when needed, so "Try:" commands can be copied. */
307
626
  function quoteArg(arg) {
@@ -333,12 +652,12 @@ function humanSize(bytes) {
333
652
  if (bytes < 1e6) return `${(bytes / 1e3).toFixed(1)} kB`;
334
653
  return `${(bytes / 1e6).toFixed(1)} MB`;
335
654
  }
336
- /** Wraps a paragraph at `width` columns; lines after the first start with `indent`. */
655
+ /** Wraps a paragraph at `width` display columns; lines after the first start with `indent`. */
337
656
  function wrap(text, width, indent = "", firstIndent = indent) {
338
657
  const lines = [];
339
658
  let line = "";
340
659
  let prefix = firstIndent;
341
- for (const word of text.split(/\s+/).filter((w) => w !== "")) if (line !== "" && prefix.length + line.length + 1 + word.length > width) {
660
+ for (const word of text.split(/\s+/).filter((w) => w !== "")) if (line !== "" && displayWidth(prefix) + displayWidth(line) + 1 + displayWidth(word) > width) {
342
661
  lines.push(prefix + line);
343
662
  prefix = indent;
344
663
  line = word;
@@ -1519,23 +1838,15 @@ const MAX_FILE_BYTES = 2097152;
1519
1838
  * exceljs API that @xlvane/compat does not support or supports only partly
1520
1839
  * (docs/exceljs-compat.md). Matching is textual, so a hit is a place to look at, not proof.
1521
1840
  */
1522
- const PATTERNS = [
1523
- {
1524
- api: "addImage({ filename })",
1525
- status: "not supported",
1526
- re: /\.addImage\s*\(\s*\{\s*filename\b/
1527
- },
1528
- {
1529
- api: "ModelContainer",
1530
- status: "not supported",
1531
- re: /\bModelContainer\b/
1532
- },
1533
- {
1534
- api: "deep import (exceljs/lib/...)",
1535
- status: "not supported",
1536
- re: /['"]exceljs\/lib\/[^'"]*['"]/
1537
- }
1538
- ];
1841
+ const PATTERNS = [{
1842
+ api: "ModelContainer",
1843
+ status: "not supported",
1844
+ re: /\bModelContainer\b/
1845
+ }, {
1846
+ api: "deep import (exceljs/lib/...)",
1847
+ status: "not supported",
1848
+ re: /['"]exceljs\/lib\/[^'"]*['"]/
1849
+ }];
1539
1850
  async function sourceFiles(root) {
1540
1851
  const files = [];
1541
1852
  let limited = false;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as InspectOptions, $n as verifyPassword, $t as DataValidationOperator, A as toJSON, An as Hyperlink, At as Workbook, B as FormulaResult, Bn as PrintErrors, Bt as CfValue, C as TableRow, Cn as ImageAnchorInput, Ct as Sheet, D as fromJSON, Dn as SheetImage, Dt as SheetState, E as WriteTableOptions, En as ImagePositionInput, Et as SheetProperties, F as CellInput, Fn as PageOrder, Ft as TableDefinitionColumn, G as SheetPropertiesOptions, Gn as SheetProtectionOptions, Gt as ConditionalFormatRule, H as NewSheet, Hn as PrintTitles, Ht as CfValueType, I as ColumnOptions, In as PageOrientation, It as TableInput, J as WriteWorkbookOptions, Jn as HashPasswordOptions, Jt as DataBarAxisPosition, K as SheetViewOptions, Kn as WorkbookProtection, Kt as ConditionalFormatRuleInput, L as CreateWorkbookOptions, Ln as PageSetup, Lt as TableStyleOptions, M as StandardSchemaV1, Mn as HeaderFooter, Mt as WorkbookView, N as AddSheetOptions, Nn as PageBreaks, Nt as TableColumnInput, O as readTable, On as WorkbookImage, Ot as SheetView, P as CalculationOptions, Pn as PageMargins, Pt as TableDefinition, Q as InspectDefinedName, Qn as legacyPasswordHash, Qt as DataValidationImeMode, R as ERROR_VALUES, Rn as PageSetupOptions, Rt as TotalsRowFunction, S as TableResult, Sn as ImageAnchor, St as RowInfo, T as ToJSONOptions, Tn as ImageOptions, Tt as SheetLayout, U as NewWorkbook, Un as SheetProtection, Ut as ConditionalFormat, V as FreezeOptions, Vn as PrintOptions, Vt as CfValueInput, W as RowOptions, Wn as SheetProtectionFlags, Wt as ConditionalFormatOperator, X as writeWorkbook, Xn as PasswordVerifier, Xt as DataValidation, Y as createWorkbook, Yn as PasswordHash, Yt as DataBarDirection, Z as InspectColumn, Zn as hashPassword, Zt as DataValidationErrorStyle, _ as JsonTable, _n as ReadingOrder, _r as DefinedNameOptions, _t as DateSystem, a as WriteWorkbookStreamOptions, an as BorderEdge, ar as XlvDetails, at as ReadOptions, b as TableColumnSpec, bn as VerticalAlignment, bt as FrozenPanes, c as OpenWorkbookStreamOptions, cn as CellStyle, cr as XlvErrorInit, ct as NoteBox, d as StreamRow, dn as FontScheme, dr as XlvWarning, dt as NoteMargins, en as DataValidationRule, er as ShaAlgorithm, et as InspectReport, f as StreamSheetInfo, fn as FontStyle, fr as XlvWarningCode, ft as Cell, g as readTableStream, gn as ProtectionStyle, gr as DefinedName, gt as Conformance, h as openWorkbookStream, hn as PatternType, hr as ReadLimits, ht as ColumnInfo, i as WriteTableStreamOptions, in as AlignmentStyle, ir as XlvDetailValue, it as inspect, j as writeTable, jn as HyperlinkTarget, jt as WorkbookFormat, k as readTableAsync, kn as imageFormat, kt as SplitPanes, l as ReadTableStreamOptions, ln as Color, lr as XlvErrorJSON, lt as NoteBoxInput, m as WorkbookStream, mn as HorizontalAlignment, mr as DEFAULT_READ_LIMITS, mt as CellValue, n as StreamRowInput, nn as IconSetType, nr as ERROR_CODES, nt as InspectTypeCounts, o as writeTableStream, on as BorderLineStyle, or as XlvError, ot as readWorkbook, p as TableStreamItem, pn as GradientFill, pr as isXlvError, pt as CellType, q as WorkbookViewOptions, qn as WorkbookProtectionOptions, qt as ConditionalFormatType, r as StreamSheetSpec, rn as TimePeriod, rr as WARNING_CODES, rt as InspectValue, s as writeWorkbookStream, sn as BorderStyle, sr as XlvErrorCode, st as Note, t as StreamDefinedName, tn as DataValidationType, tr as WorkbookProperties, tt as InspectSheet, u as STREAM_LIMIT_DEFAULTS, un as FillStyle, ur as XlvLocation, ut as NoteInput, v as ReadTableOptions, vn as UnderlineStyle, vt as Formula, w as TableValue, wn as ImageFormat, wt as SheetKind, x as TableIssue, xn as AnchorMarker, xt as RichTextRun, y as TableColumn, yn as VerticalAlignRun, yt as FormulaKind, z as ErrorValue, zn as PrintCellComments, zt as CfIcon } from "./stream-write-Q-sef804.js";
1
+ import { $ as InspectOptions, $n as verifyPassword, $t as DataValidationOperator, A as toJSON, An as Hyperlink, At as Workbook, B as FormulaResult, Bn as PrintErrors, Bt as CfValue, C as TableRow, Cn as ImageAnchorInput, Ct as Sheet, D as fromJSON, Dn as SheetImage, Dt as SheetState, E as WriteTableOptions, En as ImagePositionInput, Et as SheetProperties, F as CellInput, Fn as PageOrder, Ft as TableDefinitionColumn, G as SheetPropertiesOptions, Gn as SheetProtectionOptions, Gt as ConditionalFormatRule, H as NewSheet, Hn as PrintTitles, Ht as CfValueType, I as ColumnOptions, In as PageOrientation, It as TableInput, J as WriteWorkbookOptions, Jn as HashPasswordOptions, Jt as DataBarAxisPosition, K as SheetViewOptions, Kn as WorkbookProtection, Kt as ConditionalFormatRuleInput, L as CreateWorkbookOptions, Ln as PageSetup, Lt as TableStyleOptions, M as StandardSchemaV1, Mn as HeaderFooter, Mt as WorkbookView, N as AddSheetOptions, Nn as PageBreaks, Nt as TableColumnInput, O as readTable, On as WorkbookImage, Ot as SheetView, P as CalculationOptions, Pn as PageMargins, Pt as TableDefinition, Q as InspectDefinedName, Qn as legacyPasswordHash, Qt as DataValidationImeMode, R as ERROR_VALUES, Rn as PageSetupOptions, Rt as TotalsRowFunction, S as TableResult, Sn as ImageAnchor, St as RowInfo, T as ToJSONOptions, Tn as ImageOptions, Tt as SheetLayout, U as NewWorkbook, Un as SheetProtection, Ut as ConditionalFormat, V as FreezeOptions, Vn as PrintOptions, Vt as CfValueInput, W as RowOptions, Wn as SheetProtectionFlags, Wt as ConditionalFormatOperator, X as writeWorkbook, Xn as PasswordVerifier, Xt as DataValidation, Y as createWorkbook, Yn as PasswordHash, Yt as DataBarDirection, Z as InspectColumn, Zn as hashPassword, Zt as DataValidationErrorStyle, _ as JsonTable, _n as ReadingOrder, _r as DefinedNameOptions, _t as DateSystem, a as WriteWorkbookStreamOptions, an as BorderEdge, ar as XlvDetails, at as ReadOptions, b as TableColumnSpec, bn as VerticalAlignment, bt as FrozenPanes, c as OpenWorkbookStreamOptions, cn as CellStyle, cr as XlvErrorInit, ct as NoteBox, d as StreamRow, dn as FontScheme, dr as XlvWarning, dt as NoteMargins, en as DataValidationRule, er as ShaAlgorithm, et as InspectReport, f as StreamSheetInfo, fn as FontStyle, fr as XlvWarningCode, ft as Cell, g as readTableStream, gn as ProtectionStyle, gr as DefinedName, gt as Conformance, h as openWorkbookStream, hn as PatternType, hr as ReadLimits, ht as ColumnInfo, i as WriteTableStreamOptions, in as AlignmentStyle, ir as XlvDetailValue, it as inspect, j as writeTable, jn as HyperlinkTarget, jt as WorkbookFormat, k as readTableAsync, kn as imageFormat, kt as SplitPanes, l as ReadTableStreamOptions, ln as Color, lr as XlvErrorJSON, lt as NoteBoxInput, m as WorkbookStream, mn as HorizontalAlignment, mr as DEFAULT_READ_LIMITS, mt as CellValue, n as StreamRowInput, nn as IconSetType, nr as ERROR_CODES, nt as InspectTypeCounts, o as writeTableStream, on as BorderLineStyle, or as XlvError, ot as readWorkbook, p as TableStreamItem, pn as GradientFill, pr as isXlvError, pt as CellType, q as WorkbookViewOptions, qn as WorkbookProtectionOptions, qt as ConditionalFormatType, r as StreamSheetSpec, rn as TimePeriod, rr as WARNING_CODES, rt as InspectValue, s as writeWorkbookStream, sn as BorderStyle, sr as XlvErrorCode, st as Note, t as StreamDefinedName, tn as DataValidationType, tr as WorkbookProperties, tt as InspectSheet, u as STREAM_LIMIT_DEFAULTS, un as FillStyle, ur as XlvLocation, ut as NoteInput, v as ReadTableOptions, vn as UnderlineStyle, vt as Formula, w as TableValue, wn as ImageFormat, wt as SheetKind, x as TableIssue, xn as AnchorMarker, xt as RichTextRun, y as TableColumn, yn as VerticalAlignRun, yt as FormulaKind, z as ErrorValue, zn as PrintCellComments, zt as CfIcon } from "./stream-write-GJXORYQ2.js";
2
2
  /**
3
3
  * Options for {@link toCSV}.
4
4
  *
@@ -392,15 +392,14 @@ interface DeleteRowsOptions extends EditOptions {
392
392
  * Inserts empty rows into a sheet of an existing XLSX file and returns the new file. Rows below
393
393
  * move down, and every reference follows them as in a spreadsheet application: formulas on all
394
394
  * sheets (ranges that the new rows fall into grow), merged cells, hyperlinks, notes, the
395
- * autofilter, conditional formats, data validation, page breaks, defined names, tables (which
396
- * grow when rows are inserted inside them) and pictures placed on the sheet. Every other part of
397
- * the file stays exactly as it was.
395
+ * autofilter, conditional formats (also those of the [MS-XLSX] extension, and thresholds that
396
+ * refer to cells), data validation (also of the extension), page breaks, defined names, tables
397
+ * (which grow when rows are inserted inside them) and pictures placed on the sheet. Every other
398
+ * part of the file stays exactly as it was.
398
399
  *
399
400
  * Refused with `XLV_E_NOT_SUPPORTED`, leaving the file unchanged: a drawing that holds charts,
400
401
  * shapes or anything besides pictures; embedded objects, form controls, scenarios, cell watches,
401
- * custom sheet views, extended content that refers to cells (data bars, sparklines, slicers -
402
- * including the extended data bar settings and the icon sets `3Stars`, `3Triangles`, `5Boxes`, `NoIcons`
403
- * that xlvane itself writes) and
402
+ * custom sheet views, other extended content that refers to cells (sparklines, slicers) and
404
403
  * other related parts xlvane does not know; charts or pivot tables anywhere in the workbook that
405
404
  * may refer to the sheet; data tables; an array formula the change would split; and formulas it
406
405
  * cannot rewrite safely (docs/facts/ref-adjust.md).
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Bt as hashPassword, Cn as isXlvError, Ht as verifyPassword, Pt as DEFAULT_READ_LIMITS, Sn as XlvError, Vt as legacyPasswordHash, b as readWorkbook, bn as ERROR_CODES, c as NewSheet, f as createWorkbook, g as inspect, l as NewWorkbook, m as writeWorkbook, qt as imageFormat, s as ERROR_VALUES, xn as WARNING_CODES, z as shiftFormula } from "./options-B7UhudO_.js";
2
- import { a as appendRows, c as fromCSV, d as fromJSON, g as writeTable, h as toJSON, i as adjustRange, m as readTableAsync, n as insertRows, o as editCells, p as readTable, r as adjustFormula, s as diff, t as deleteRows, u as toCSV } from "./rows-Cwte1t8m.js";
3
- import { a as readTableStream, i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS, t as writeTableStream } from "./stream-write-CTgWff2P.js";
1
+ import { Dn as isXlvError, En as XlvError, Gt as legacyPasswordHash, Kt as verifyPassword, Rt as DEFAULT_READ_LIMITS, Tn as WARNING_CODES, Wt as hashPassword, Zt as imageFormat, b as readWorkbook, c as NewSheet, f as createWorkbook, g as inspect, l as NewWorkbook, m as writeWorkbook, s as ERROR_VALUES, wn as ERROR_CODES, z as shiftFormula } from "./options-CwSp2pA4.js";
2
+ import { a as appendRows, c as fromCSV, d as fromJSON, g as writeTable, h as toJSON, i as adjustRange, m as readTableAsync, n as insertRows, o as editCells, p as readTable, r as adjustFormula, s as diff, t as deleteRows, u as toCSV } from "./rows-DKwlr0rg.js";
3
+ import { a as readTableStream, i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS, t as writeTableStream } from "./stream-write-BKppyMz0.js";
4
4
  export { DEFAULT_READ_LIMITS, ERROR_CODES, ERROR_VALUES, NewSheet, NewWorkbook, STREAM_LIMIT_DEFAULTS, WARNING_CODES, XlvError, adjustFormula, adjustRange, appendRows, createWorkbook, deleteRows, diff, editCells, fromCSV, fromJSON, hashPassword, imageFormat, insertRows, inspect, isXlvError, legacyPasswordHash, openWorkbookStream, readTable, readTableAsync, readTableStream, readWorkbook, shiftFormula, toCSV, toJSON, verifyPassword, writeTable, writeTableStream, writeWorkbook, writeWorkbookStream };
@@ -1,4 +1,4 @@
1
- import { Ft as resolveLimits, Sn as XlvError } from "./options-B7UhudO_.js";
1
+ import { En as XlvError, zt as resolveLimits } from "./options-CwSp2pA4.js";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  // src/util/node-files.ts
4
4
  /** The path as shown in messages. */
package/dist/node.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as InspectOptions, At as Workbook, J as WriteWorkbookOptions, U as NewWorkbook, a as WriteWorkbookStreamOptions, at as ReadOptions, c as OpenWorkbookStreamOptions, et as InspectReport, m as WorkbookStream } from "./stream-write-Q-sef804.js";
1
+ import { $ as InspectOptions, At as Workbook, J as WriteWorkbookOptions, U as NewWorkbook, a as WriteWorkbookStreamOptions, at as ReadOptions, c as OpenWorkbookStreamOptions, et as InspectReport, m as WorkbookStream } from "./stream-write-GJXORYQ2.js";
2
2
  /**
3
3
  * Reads an XLSX file from disk. Node.js only; in other runtimes pass the bytes to
4
4
  * `readWorkbook` from `xlvane`. The file size is checked against `limits.maxInputBytes`
package/dist/node.js CHANGED
@@ -1,6 +1,6 @@
1
- import { Ft as resolveLimits, Sn as XlvError, b as readWorkbook, g as inspect, m as writeWorkbook } from "./options-B7UhudO_.js";
2
- import { i as readInput, n as describePath, r as fileError, t as checkFile } from "./node-files-uazELnqk.js";
3
- import { i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS } from "./stream-write-CTgWff2P.js";
1
+ import { En as XlvError, b as readWorkbook, g as inspect, m as writeWorkbook, zt as resolveLimits } from "./options-CwSp2pA4.js";
2
+ import { i as readInput, n as describePath, r as fileError, t as checkFile } from "./node-files-B0iqSJIJ.js";
3
+ import { i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS } from "./stream-write-BKppyMz0.js";
4
4
  import { openAsBlob } from "node:fs";
5
5
  import { open, unlink, writeFile } from "node:fs/promises";
6
6
  // src/node.ts