exceljs-lts 4.5.1 → 4.5.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.
Files changed (36) hide show
  1. package/dist/es5/doc/table.js +89 -2
  2. package/dist/es5/doc/table.js.map +1 -1
  3. package/dist/es5/doc/worksheet.js +12 -6
  4. package/dist/es5/doc/worksheet.js.map +1 -1
  5. package/dist/es5/stream/xlsx/workbook-reader.js +69 -62
  6. package/dist/es5/stream/xlsx/workbook-reader.js.map +1 -1
  7. package/dist/es5/xlsx/xform/book/workbook-xform.js +34 -9
  8. package/dist/es5/xlsx/xform/book/workbook-xform.js.map +1 -1
  9. package/dist/es5/xlsx/xform/sheet/cf/cf-rule-xform.js +4 -0
  10. package/dist/es5/xlsx/xform/sheet/cf/cf-rule-xform.js.map +1 -1
  11. package/dist/es5/xlsx/xform/sheet/cf/conditional-formattings-xform.js +4 -1
  12. package/dist/es5/xlsx/xform/sheet/cf/conditional-formattings-xform.js.map +1 -1
  13. package/dist/es5/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js +40 -2
  14. package/dist/es5/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js.map +1 -1
  15. package/dist/es5/xlsx/xform/style/dxf-ext-xform.js +24 -0
  16. package/dist/es5/xlsx/xform/style/dxf-ext-xform.js.map +1 -0
  17. package/dist/es5/xlsx/xform/table/table-xform.js +2 -1
  18. package/dist/es5/xlsx/xform/table/table-xform.js.map +1 -1
  19. package/dist/exceljs.bare.js +621 -432
  20. package/dist/exceljs.bare.js.map +11 -9
  21. package/dist/exceljs.bare.min.js +8 -8
  22. package/dist/exceljs.bare.min.js.map +1 -1
  23. package/dist/exceljs.js +786 -597
  24. package/dist/exceljs.js.map +11 -9
  25. package/dist/exceljs.min.js +8 -8
  26. package/dist/exceljs.min.js.map +1 -1
  27. package/lib/doc/table.js +89 -2
  28. package/lib/doc/worksheet.js +15 -6
  29. package/lib/stream/xlsx/workbook-reader.js +64 -54
  30. package/lib/xlsx/xform/book/workbook-xform.js +42 -11
  31. package/lib/xlsx/xform/sheet/cf/cf-rule-xform.js +4 -0
  32. package/lib/xlsx/xform/sheet/cf/conditional-formattings-xform.js +3 -1
  33. package/lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js +42 -1
  34. package/lib/xlsx/xform/style/dxf-ext-xform.js +19 -0
  35. package/lib/xlsx/xform/table/table-xform.js +2 -1
  36. package/package.json +1 -1
package/lib/doc/table.js CHANGED
@@ -142,8 +142,9 @@ class Table {
142
142
  }
143
143
 
144
144
  get height() {
145
- // height of the table data
146
- return this.table.rows.length;
145
+ // height of the table data. Excel needs at least one data row, so an empty table
146
+ // keeps one blank row on the sheet (it is not added to table.rows)
147
+ return Math.max(this.table.rows.length, 1);
147
148
  }
148
149
 
149
150
  get filterHeight() {
@@ -246,6 +247,15 @@ class Table {
246
247
  assignStyle(cell, table.columns[j].style);
247
248
  });
248
249
  });
250
+ if (!table.rows.length) {
251
+ // blank placeholder row so the table keeps a valid ref
252
+ const r = worksheet.getRow(row + count++);
253
+ table.columns.forEach((column, j) => {
254
+ const cell = r.getCell(col + j);
255
+ cell.value = null;
256
+ assignStyle(cell, column.style);
257
+ });
258
+ }
249
259
 
250
260
  if (table.totalsRow) {
251
261
  const r = worksheet.getRow(row + count++);
@@ -289,6 +299,10 @@ class Table {
289
299
  cell.value = value;
290
300
  });
291
301
  });
302
+ if (!table.rows.length) {
303
+ // skip the blank placeholder row of an empty table
304
+ count++;
305
+ }
292
306
 
293
307
  if (table.totalsRow) {
294
308
  const r = worksheet.getRow(row + count++);
@@ -315,6 +329,79 @@ class Table {
315
329
 
316
330
  set model(value) {
317
331
  this.table = value;
332
+ if (this.worksheet && !value.rows) {
333
+ // a table parsed from a file has no rows: read them from the sheet cells
334
+ this.loadRows();
335
+ }
336
+ }
337
+
338
+ loadRows() {
339
+ // rebuild ref, column names, rows and totals row formulas of a table parsed from a file
340
+ // from the cells inside its tableRef. This only reads the sheet, it never writes to it.
341
+ const {worksheet, table} = this;
342
+ if (table.headerRow === undefined) {
343
+ table.headerRow = true;
344
+ }
345
+ if (table.totalsRow === undefined) {
346
+ table.totalsRow = false;
347
+ }
348
+ if (!table.style) {
349
+ table.style = {};
350
+ }
351
+ table.columns = table.columns || [];
352
+ const range = colCache.decode(table.tableRef || table.ref);
353
+ const top = range.top || range.row;
354
+ const left = range.left || range.col;
355
+ const bottom = range.bottom || top;
356
+ if (!table.ref) {
357
+ table.ref = colCache.encodeAddress(top, left);
358
+ }
359
+ const cellValue = (r, c) => {
360
+ const cell = worksheet.findCell(r, c);
361
+ return cell ? cell.value : null;
362
+ };
363
+
364
+ let first = top;
365
+ let last = bottom;
366
+ if (table.headerRow) {
367
+ table.columns.forEach((column, j) => {
368
+ if (!column.name) {
369
+ const value = cellValue(top, left + j);
370
+ column.name = value === null || value === undefined ? `Column${j + 1}` : `${value}`;
371
+ }
372
+ });
373
+ first++;
374
+ }
375
+ if (table.totalsRow) {
376
+ table.columns.forEach((column, j) => {
377
+ const cell = worksheet.findCell(bottom, left + j);
378
+ if (!cell) {
379
+ return;
380
+ }
381
+ if (j === 0) {
382
+ if (column.totalsRowLabel === undefined && typeof cell.value === 'string') {
383
+ column.totalsRowLabel = cell.value;
384
+ }
385
+ } else if (cell.formula) {
386
+ if (column.totalsRowFunction === 'custom' && column.totalsRowFormula === undefined) {
387
+ column.totalsRowFormula = cell.formula;
388
+ }
389
+ if (column.totalsRowResult === undefined) {
390
+ column.totalsRowResult = cell.result;
391
+ }
392
+ }
393
+ });
394
+ last--;
395
+ }
396
+
397
+ table.rows = [];
398
+ for (let r = first; r <= last; r++) {
399
+ table.rows.push(table.columns.map((column, j) => cellValue(r, left + j)));
400
+ }
401
+ if (table.rows.length === 1 && table.rows[0].every(v => v === null || v === undefined)) {
402
+ // the blank placeholder row of an empty table
403
+ table.rows = [];
404
+ }
318
405
  }
319
406
 
320
407
  // ================================================================
@@ -660,12 +660,21 @@ class Worksheet {
660
660
  }
661
661
 
662
662
  _mergeCellsInternal(dimensions, ignoreStyle) {
663
- // check cells aren't already merged
664
- _.each(this._merges, merge => {
665
- if (merge.intersects(dimensions)) {
666
- throw new Error('Cannot merge already merged cells');
663
+ // check cells aren't already merged. Any existing merge that intersects the range has a
664
+ // cell inside it that is either a merged (slave) cell or a master indexed in this._merges,
665
+ // so this is O(cells in range) rather than O(existing merges).
666
+ // Check everything before changing anything so a rejected merge leaves the sheet untouched.
667
+ for (let i = dimensions.top; i <= dimensions.bottom; i++) {
668
+ for (let j = dimensions.left; j <= dimensions.right; j++) {
669
+ const cell = this.findCell(i, j);
670
+ if (
671
+ (cell && cell.type === Enums.ValueType.Merge) ||
672
+ this._merges[colCache.encodeAddress(i, j)]
673
+ ) {
674
+ throw new Error('Cannot merge already merged cells');
675
+ }
667
676
  }
668
- });
677
+ }
669
678
 
670
679
  // apply merge
671
680
  const master = this.getCell(dimensions.top, dimensions.left);
@@ -983,7 +992,7 @@ Please leave feedback at https://github.com/exceljs/exceljs/discussions/2575`
983
992
  this._media = value.media.map(medium => new Image(this, medium));
984
993
  this.sheetProtection = value.sheetProtection;
985
994
  this.tables = value.tables.reduce((tables, table) => {
986
- const t = new Table();
995
+ const t = new Table(this);
987
996
  t.model = table;
988
997
  tables[table.name] = t;
989
998
  return tables;
@@ -85,67 +85,77 @@ class WorkbookReader extends EventEmitter {
85
85
  // worksheets, deferred for parsing after shared strings reading
86
86
  const waitingWorkSheets = [];
87
87
 
88
- for await (const entry of iterateStream(zip)) {
89
- let match;
90
- let sheetNo;
91
- switch (entry.path) {
92
- case '_rels/.rels':
93
- break;
94
- case 'xl/_rels/workbook.xml.rels':
95
- await this._parseRels(entry);
96
- break;
97
- case 'xl/workbook.xml':
98
- await this._parseWorkbook(entry);
99
- break;
100
- case 'xl/sharedStrings.xml':
101
- yield* this._parseSharedStrings(entry);
102
- break;
103
- case 'xl/styles.xml':
104
- await this._parseStyles(entry);
105
- break;
106
- default:
107
- if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
108
- match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
109
- sheetNo = match[1];
110
- if (this.sharedStrings && this.workbookRels) {
111
- yield* this._parseWorksheet(iterateStream(entry), sheetNo);
112
- } else {
113
- // create temp file for each worksheet
114
- await new Promise((resolve, reject) => {
115
- tmp.file((err, path, fd, tempFileCleanupCallback) => {
116
- if (err) {
117
- return reject(err);
118
- }
119
- waitingWorkSheets.push({sheetNo, path, tempFileCleanupCallback});
88
+ try {
89
+ for await (const entry of iterateStream(zip)) {
90
+ let match;
91
+ let sheetNo;
92
+ switch (entry.path) {
93
+ case '_rels/.rels':
94
+ break;
95
+ case 'xl/_rels/workbook.xml.rels':
96
+ await this._parseRels(entry);
97
+ break;
98
+ case 'xl/workbook.xml':
99
+ await this._parseWorkbook(entry);
100
+ break;
101
+ case 'xl/sharedStrings.xml':
102
+ yield* this._parseSharedStrings(entry);
103
+ break;
104
+ case 'xl/styles.xml':
105
+ await this._parseStyles(entry);
106
+ break;
107
+ default:
108
+ if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
109
+ match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
110
+ sheetNo = match[1];
111
+ if (this.sharedStrings && this.workbookRels) {
112
+ yield* this._parseWorksheet(iterateStream(entry), sheetNo);
113
+ } else {
114
+ // create temp file for each worksheet
115
+ await new Promise((resolve, reject) => {
116
+ tmp.file((err, path, fd, tempFileCleanupCallback) => {
117
+ if (err) {
118
+ return reject(err);
119
+ }
120
+ waitingWorkSheets.push({sheetNo, path, tempFileCleanupCallback, cleanedUp: false});
120
121
 
121
- const tempStream = fs.createWriteStream(path);
122
- tempStream.on('error', reject);
123
- entry.pipe(tempStream);
124
- return tempStream.on('finish', () => {
125
- return resolve();
122
+ const tempStream = fs.createWriteStream(path);
123
+ tempStream.on('error', reject);
124
+ entry.pipe(tempStream);
125
+ return tempStream.on('finish', () => {
126
+ return resolve();
127
+ });
126
128
  });
127
129
  });
128
- });
130
+ }
131
+ } else if (entry.path.match(/xl\/worksheets\/_rels\/sheet\d+[.]xml.rels/)) {
132
+ match = entry.path.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
133
+ sheetNo = match[1];
134
+ yield* this._parseHyperlinks(iterateStream(entry), sheetNo);
129
135
  }
130
- } else if (entry.path.match(/xl\/worksheets\/_rels\/sheet\d+[.]xml.rels/)) {
131
- match = entry.path.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
132
- sheetNo = match[1];
133
- yield* this._parseHyperlinks(iterateStream(entry), sheetNo);
134
- }
135
- break;
136
+ break;
137
+ }
138
+ entry.autodrain();
136
139
  }
137
- entry.autodrain();
138
- }
139
140
 
140
- for (const {sheetNo, path, tempFileCleanupCallback} of waitingWorkSheets) {
141
- let fileStream = fs.createReadStream(path);
142
- // TODO: Remove once node v8 is deprecated
143
- // Detect and upgrade old fileStreams
144
- if (!fileStream[Symbol.asyncIterator]) {
145
- fileStream = fileStream.pipe(new PassThrough());
141
+ for (const waitingWorkSheet of waitingWorkSheets) {
142
+ let fileStream = fs.createReadStream(waitingWorkSheet.path);
143
+ // TODO: Remove once node v8 is deprecated
144
+ // Detect and upgrade old fileStreams
145
+ if (!fileStream[Symbol.asyncIterator]) {
146
+ fileStream = fileStream.pipe(new PassThrough());
147
+ }
148
+ yield* this._parseWorksheet(fileStream, waitingWorkSheet.sheetNo);
149
+ waitingWorkSheet.tempFileCleanupCallback();
150
+ waitingWorkSheet.cleanedUp = true;
151
+ }
152
+ } finally {
153
+ for (const waitingWorkSheet of waitingWorkSheets) {
154
+ if (!waitingWorkSheet.cleanedUp) {
155
+ waitingWorkSheet.tempFileCleanupCallback();
156
+ waitingWorkSheet.cleanedUp = true;
157
+ }
146
158
  }
147
- yield* this._parseWorksheet(fileStream, sheetNo);
148
- tempFileCleanupCallback();
149
159
  }
150
160
  }
151
161
 
@@ -48,15 +48,18 @@ class WorkbookXform extends BaseXform {
48
48
  let index = 0; // sheets is sparse array - calc index manually
49
49
  model.sheets.forEach(sheet => {
50
50
  if (sheet.pageSetup && sheet.pageSetup.printArea) {
51
- sheet.pageSetup.printArea.split('&&').forEach(printArea => {
52
- const printAreaComponents = printArea.split(':');
53
- const definedName = {
51
+ // Excel allows only one Print_Area per sheet; multiple areas are comma separated ranges
52
+ const ranges = sheet.pageSetup.printArea
53
+ .split('&&')
54
+ .filter(printArea => printArea)
55
+ .map(printArea => `'${sheet.name}'!${toAbsoluteRange(printArea)}`);
56
+ if (ranges.length) {
57
+ printAreas.push({
54
58
  name: '_xlnm.Print_Area',
55
- ranges: [`'${sheet.name}'!$${printAreaComponents[0]}:$${printAreaComponents[1]}`],
59
+ ranges,
56
60
  localSheetId: index,
57
- };
58
- printAreas.push(definedName);
59
- });
61
+ });
62
+ }
60
63
  }
61
64
 
62
65
  if (sheet.pageSetup && (sheet.pageSetup.printTitlesRow || sheet.pageSetup.printTitlesColumn)) {
@@ -197,10 +200,12 @@ class WorkbookXform extends BaseXform {
197
200
  if (!worksheet.pageSetup) {
198
201
  worksheet.pageSetup = {};
199
202
  }
200
- const range = colCache.decodeEx(definedName.ranges[0]);
201
- worksheet.pageSetup.printArea = worksheet.pageSetup.printArea
202
- ? `${worksheet.pageSetup.printArea}&&${range.dimensions}`
203
- : range.dimensions;
203
+ const printArea = definedName.ranges.map(fromDefinedNameRange).join('&&');
204
+ if (printArea) {
205
+ worksheet.pageSetup.printArea = worksheet.pageSetup.printArea
206
+ ? `${worksheet.pageSetup.printArea}&&${printArea}`
207
+ : printArea;
208
+ }
204
209
  }
205
210
  } else if (definedName.name === '_xlnm.Print_Titles') {
206
211
  worksheet = worksheets[definedName.localSheetId];
@@ -242,6 +247,32 @@ class WorkbookXform extends BaseXform {
242
247
  }
243
248
  }
244
249
 
250
+ // convert a print area such as A1:B2, A:B, 1:3 or C3 into an absolute reference
251
+ // ($A$1:$B$2, $A:$B, $1:$3, $C$3)
252
+ function toAbsoluteRange(range) {
253
+ return range
254
+ .split(':')
255
+ .map(part => {
256
+ const match = part.trim().match(/^\$?([A-Za-z]*)\$?(\d*)$/);
257
+ if (!match) {
258
+ return part;
259
+ }
260
+ const [, col, row] = match;
261
+ return `${col ? `$${col.toUpperCase()}` : ''}${row ? `$${row}` : ''}`;
262
+ })
263
+ .join(':');
264
+ }
265
+
266
+ // convert a defined name range such as 'Sheet'!$A$1:$B$2 or Sheet!$A:$B into a print area
267
+ // (A1:B2, A:B)
268
+ function fromDefinedNameRange(range) {
269
+ const reference = range.match(/^(?:(?:'(?:[^']|'')*'|[^'!]*)!)?(.*)$/)[1];
270
+ if (/^\$?[A-Za-z]+\$?\d+:\$?[A-Za-z]+\$?\d+$/.test(reference)) {
271
+ return colCache.decodeEx(reference).dimensions;
272
+ }
273
+ return reference.replace(/\$/g, '');
274
+ }
275
+
245
276
  WorkbookXform.WORKBOOK_ATTRIBUTES = {
246
277
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
247
278
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
@@ -113,6 +113,10 @@ class CfRuleXform extends CompositeXform {
113
113
  return false;
114
114
  }
115
115
  }
116
+ if (rule.type === 'expression' && rule.x14Id) {
117
+ // read from x14:cfRule, rendered by CfRuleExtXform
118
+ return false;
119
+ }
116
120
  return true;
117
121
  }
118
122
 
@@ -1,6 +1,7 @@
1
1
  const BaseXform = require('../../base-xform');
2
2
 
3
3
  const ConditionalFormattingXform = require('./conditional-formatting-xform');
4
+ const CfRuleXform = require('./cf-rule-xform');
4
5
 
5
6
  class ConditionalFormattingsXform extends BaseXform {
6
7
  constructor() {
@@ -29,7 +30,8 @@ class ConditionalFormattingsXform extends BaseXform {
29
30
  rule.priority = nextPriority++;
30
31
  }
31
32
 
32
- if (rule.style) {
33
+ // ext (x14) rules write their style inline as <x14:dxf>, not as a dxfId
34
+ if (rule.style && CfRuleXform.isPrimitive(rule)) {
33
35
  rule.dxfId = options.styles.addDxfStyle(rule.style);
34
36
  }
35
37
  });
@@ -4,6 +4,8 @@ const CompositeXform = require('../../composite-xform');
4
4
 
5
5
  const DatabarExtXform = require('./databar-ext-xform');
6
6
  const IconSetExtXform = require('./icon-set-ext-xform');
7
+ const FExtXform = require('./f-ext-xform');
8
+ const DxfExtXform = require('../../style/dxf-ext-xform');
7
9
 
8
10
  const extIcons = {
9
11
  '3Triangles': true,
@@ -18,6 +20,8 @@ class CfRuleExtXform extends CompositeXform {
18
20
  this.map = {
19
21
  'x14:dataBar': (this.databarXform = new DatabarExtXform()),
20
22
  'x14:iconSet': (this.iconSetXform = new IconSetExtXform()),
23
+ 'xm:f': (this.fXform = new FExtXform()),
24
+ 'x14:dxf': (this.dxfXform = new DxfExtXform()),
21
25
  };
22
26
  }
23
27
 
@@ -35,6 +39,11 @@ class CfRuleExtXform extends CompositeXform {
35
39
  return true;
36
40
  }
37
41
  }
42
+ if (rule.type === 'expression') {
43
+ // expression rules read from x14:cfRule (e.g. formulas that reference other sheets)
44
+ // keep their x14Id and are written back to the extLst
45
+ return !!rule.x14Id;
46
+ }
38
47
  return false;
39
48
  }
40
49
 
@@ -56,6 +65,9 @@ class CfRuleExtXform extends CompositeXform {
56
65
  case 'iconSet':
57
66
  this.renderIconSet(xmlStream, model);
58
67
  break;
68
+ case 'expression':
69
+ this.renderExpression(xmlStream, model);
70
+ break;
59
71
  }
60
72
  }
61
73
 
@@ -82,6 +94,23 @@ class CfRuleExtXform extends CompositeXform {
82
94
  xmlStream.closeNode();
83
95
  }
84
96
 
97
+ renderExpression(xmlStream, model) {
98
+ xmlStream.openNode(this.tag, {
99
+ type: 'expression',
100
+ priority: model.priority,
101
+ id: model.x14Id,
102
+ });
103
+
104
+ if (model.formulae && model.formulae[0]) {
105
+ this.fXform.render(xmlStream, model.formulae[0]);
106
+ }
107
+ if (model.style) {
108
+ this.dxfXform.render(xmlStream, model.style);
109
+ }
110
+
111
+ xmlStream.closeNode();
112
+ }
113
+
85
114
  createNewModel({attributes}) {
86
115
  return {
87
116
  type: attributes.type,
@@ -91,7 +120,19 @@ class CfRuleExtXform extends CompositeXform {
91
120
  }
92
121
 
93
122
  onParserClose(name, parser) {
94
- Object.assign(this.model, parser.model);
123
+ switch (name) {
124
+ case 'xm:f':
125
+ this.model.formulae = this.model.formulae || [];
126
+ this.model.formulae.push(parser.model);
127
+ break;
128
+ case 'x14:dxf':
129
+ // same shape as the style of a regular cfRule (reconciled from its dxfId)
130
+ this.model.style = parser.model;
131
+ break;
132
+ default:
133
+ Object.assign(this.model, parser.model);
134
+ break;
135
+ }
95
136
  }
96
137
  }
97
138
 
@@ -0,0 +1,19 @@
1
+ const DxfXform = require('./dxf-xform');
2
+
3
+ // <x14:dxf> holds a differential format inline in an x14:cfRule (extLst).
4
+ // It has the same children as <dxf> in styles.xml.
5
+ class DxfExtXform extends DxfXform {
6
+ get tag() {
7
+ return 'x14:dxf';
8
+ }
9
+
10
+ render(xmlStream, model) {
11
+ // a dxf read from a file holds numFmt as a NumFmtXform model ({id, formatCode})
12
+ if (model.numFmt && typeof model.numFmt === 'object') {
13
+ model = {...model, numFmtId: model.numFmt.id, numFmt: model.numFmt.formatCode};
14
+ }
15
+ super.render(xmlStream, model);
16
+ }
17
+ }
18
+
19
+ module.exports = DxfExtXform;
@@ -66,7 +66,8 @@ class TableXform extends BaseXform {
66
66
  displayName: attributes.displayName || attributes.name,
67
67
  tableRef: attributes.ref,
68
68
  totalsRow: attributes.totalsRowCount === '1',
69
- headerRow: attributes.headerRowCount === '1',
69
+ // headerRowCount defaults to 1 and Excel usually omits it
70
+ headerRow: attributes.headerRowCount !== '0',
70
71
  };
71
72
  break;
72
73
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceljs-lts",
3
- "version": "4.5.1",
3
+ "version": "4.5.3",
4
4
  "description": "Maintained fork of exceljs — drop-in replacement, fixes Excel \"We found a problem with some content\" repair prompts, the TypeError crash on rich-text shared strings, and lost formula results (0, false, ''). Read and write xlsx and csv files.",
5
5
  "private": false,
6
6
  "license": "MIT",