exceljs-lts 4.5.3 → 4.5.5

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 (45) hide show
  1. package/README.md +43 -11
  2. package/README_zh.md +29 -10
  3. package/dist/es5/doc/cell.js +37 -12
  4. package/dist/es5/doc/cell.js.map +1 -1
  5. package/dist/es5/doc/data-validations.js +94 -1
  6. package/dist/es5/doc/data-validations.js.map +1 -1
  7. package/dist/es5/doc/image.js +28 -0
  8. package/dist/es5/doc/image.js.map +1 -1
  9. package/dist/es5/doc/worksheet.js +8 -0
  10. package/dist/es5/doc/worksheet.js.map +1 -1
  11. package/dist/es5/stream/xlsx/workbook-reader.js +2 -1
  12. package/dist/es5/stream/xlsx/workbook-reader.js.map +1 -1
  13. package/dist/es5/stream/xlsx/workbook-writer.js +21 -1
  14. package/dist/es5/stream/xlsx/workbook-writer.js.map +1 -1
  15. package/dist/es5/stream/xlsx/worksheet-reader.js +1 -1
  16. package/dist/es5/stream/xlsx/worksheet-reader.js.map +1 -1
  17. package/dist/es5/stream/xlsx/worksheet-writer.js +89 -0
  18. package/dist/es5/stream/xlsx/worksheet-writer.js.map +1 -1
  19. package/dist/es5/xlsx/rel-type.js +1 -0
  20. package/dist/es5/xlsx/rel-type.js.map +1 -1
  21. package/dist/es5/xlsx/xform/sheet/auto-filter-xform.js +53 -5
  22. package/dist/es5/xlsx/xform/sheet/auto-filter-xform.js.map +1 -1
  23. package/dist/es5/xlsx/xform/sheet/data-validations-xform.js +14 -3
  24. package/dist/es5/xlsx/xform/sheet/data-validations-xform.js.map +1 -1
  25. package/dist/exceljs.bare.js +237 -23
  26. package/dist/exceljs.bare.js.map +8 -8
  27. package/dist/exceljs.bare.min.js +6 -6
  28. package/dist/exceljs.bare.min.js.map +1 -1
  29. package/dist/exceljs.js +237 -23
  30. package/dist/exceljs.js.map +8 -8
  31. package/dist/exceljs.min.js +5 -5
  32. package/dist/exceljs.min.js.map +1 -1
  33. package/index.d.ts +40 -3
  34. package/lib/doc/cell.js +37 -12
  35. package/lib/doc/data-validations.js +83 -1
  36. package/lib/doc/image.js +32 -0
  37. package/lib/doc/worksheet.js +9 -0
  38. package/lib/stream/xlsx/workbook-reader.js +6 -1
  39. package/lib/stream/xlsx/workbook-writer.js +19 -0
  40. package/lib/stream/xlsx/worksheet-reader.js +1 -1
  41. package/lib/stream/xlsx/worksheet-writer.js +73 -0
  42. package/lib/xlsx/rel-type.js +1 -0
  43. package/lib/xlsx/xform/sheet/auto-filter-xform.js +42 -3
  44. package/lib/xlsx/xform/sheet/data-validations-xform.js +14 -3
  45. package/package.json +1 -1
package/dist/exceljs.js CHANGED
@@ -286,6 +286,9 @@ const {
286
286
  slideFormula
287
287
  } = require('../utils/shared-formula');
288
288
  const Note = require('./note');
289
+ const {
290
+ copyStyle
291
+ } = require('../utils/copy-style');
289
292
  // Cell requirements
290
293
  // Operate inside a worksheet
291
294
  // Store and retrieve a value with a range of types: text, number, date, hyperlink, reference, formula, etc.
@@ -315,7 +318,8 @@ class Cell {
315
318
 
316
319
  // help GC by removing cyclic (and other) references
317
320
  destroy() {
318
- delete this.style;
321
+ delete this._style;
322
+ delete this._styleShared;
319
323
  delete this._value;
320
324
  delete this._row;
321
325
  delete this._column;
@@ -324,38 +328,59 @@ class Cell {
324
328
 
325
329
  // =========================================================================
326
330
  // Styles stuff
331
+
332
+ // Cells loaded from a file share one style object per xf. The object is only
333
+ // copied when the style is handed out (cell.style), so code that mutates it
334
+ // in place (cell.style.fill = ...) changes this cell alone.
335
+ get style() {
336
+ if (this._styleShared) {
337
+ this._style = copyStyle(this._style);
338
+ this._styleShared = false;
339
+ }
340
+ return this._style;
341
+ }
342
+ set style(value) {
343
+ this._style = value;
344
+ this._styleShared = false;
345
+ }
346
+
347
+ // use a style object that other cells may share (e.g. a cached xf style)
348
+ _setSharedStyle(style) {
349
+ this._style = style;
350
+ this._styleShared = true;
351
+ }
327
352
  get numFmt() {
328
- return this.style.numFmt;
353
+ return this._style.numFmt;
329
354
  }
330
355
  set numFmt(value) {
331
356
  this._setStyle('numFmt', value);
332
357
  }
333
358
  get font() {
334
- return this.style.font;
359
+ return this._style.font;
335
360
  }
336
361
  set font(value) {
337
362
  this._setStyle('font', value);
338
363
  }
339
364
  get alignment() {
340
- return this.style.alignment;
365
+ return this._style.alignment;
341
366
  }
342
367
  set alignment(value) {
343
368
  this._setStyle('alignment', value);
344
369
  }
345
370
  get border() {
346
- return this.style.border;
371
+ return this._style.border;
347
372
  }
348
373
  set border(value) {
349
374
  this._setStyle('border', value);
350
375
  }
351
376
  get fill() {
352
- return this.style.fill;
377
+ return this._style.fill;
353
378
  }
354
379
  set fill(value) {
355
380
  this._setStyle('fill', value);
356
381
  }
357
382
  get protection() {
358
- return this.style.protection;
383
+ return this._style.protection;
359
384
  }
360
385
  set protection(value) {
361
386
  this._setStyle('protection', value);
@@ -363,7 +388,7 @@ class Cell {
363
388
  _setStyle(name, value) {
364
389
  // style objects can be shared (e.g. by every cell loaded with the same xf),
365
390
  // so never mutate one in place; give this cell a new object instead
366
- const oldStyle = this.style;
391
+ const oldStyle = this._style;
367
392
  const style = {
368
393
  ...oldStyle,
369
394
  [name]: value
@@ -375,12 +400,12 @@ class Cell {
375
400
  const merges = master._mergeCount > 0 && this.worksheet._merges;
376
401
  // the streaming WorksheetWriter keeps its merges in an array
377
402
  const range = merges && (Array.isArray(merges) ? merges.find(merge => merge.top === master.row && merge.left === master.col) : merges[master.address]);
378
- if (range && (master === this || master.style === oldStyle)) {
403
+ if (range && (master === this || master._style === oldStyle)) {
379
404
  for (let r = range.top; r <= range.bottom; r++) {
380
405
  const row = this.worksheet.findRow(r);
381
406
  for (let c = range.left; row && c <= range.right; c++) {
382
407
  const cell = row.findCell(c);
383
- if (cell && cell.style === oldStyle) {
408
+ if (cell && cell._style === oldStyle) {
384
409
  cell.style = style;
385
410
  }
386
411
  }
@@ -588,7 +613,7 @@ class Cell {
588
613
  const {
589
614
  model
590
615
  } = this._value;
591
- model.style = this.style;
616
+ model.style = this._style;
592
617
  if (this._comment) {
593
618
  model.comment = this._comment.model;
594
619
  }
@@ -606,7 +631,7 @@ class Cell {
606
631
  }
607
632
  }
608
633
  if (value.style) {
609
- this.style = value.style;
634
+ this._setSharedStyle(value.style);
610
635
  } else {
611
636
  this.style = {};
612
637
  }
@@ -1263,7 +1288,7 @@ const Value = {
1263
1288
  };
1264
1289
  module.exports = Cell;
1265
1290
 
1266
- },{"../utils/col-cache":20,"../utils/shared-formula":24,"../utils/under-dash":27,"./enums":7,"./note":9}],4:[function(require,module,exports){
1291
+ },{"../utils/col-cache":20,"../utils/copy-style":21,"../utils/shared-formula":24,"../utils/under-dash":27,"./enums":7,"./note":9}],4:[function(require,module,exports){
1267
1292
  'use strict';
1268
1293
 
1269
1294
  const _ = require('../utils/under-dash');
@@ -1544,23 +1569,116 @@ module.exports = Column;
1544
1569
  },{"../utils/col-cache":20,"../utils/under-dash":27,"./enums":7}],5:[function(require,module,exports){
1545
1570
  "use strict";
1546
1571
 
1572
+ const colCache = require('../utils/col-cache');
1573
+ function isRange(address) {
1574
+ return typeof address === 'string' && address.includes(':');
1575
+ }
1547
1576
  class DataValidations {
1548
1577
  constructor(model) {
1549
1578
  this.model = model || {};
1579
+ // Range-keyed entries (e.g. 'A2:A1048576', kept intact when a file is loaded) are tracked
1580
+ // separately so per-cell add/find stay O(1) when there are none (exceljs#1842).
1581
+ this._ranges = new Map();
1582
+ Object.keys(this.model).forEach(key => {
1583
+ if (isRange(key)) {
1584
+ this._addRange(key);
1585
+ }
1586
+ });
1550
1587
  }
1551
1588
  add(address, validation) {
1589
+ if (isRange(address)) {
1590
+ this._addRange(address);
1591
+ } else {
1592
+ this._splitRangeAt(address);
1593
+ }
1552
1594
  return this.model[address] = validation;
1553
1595
  }
1554
1596
  find(address) {
1555
- return this.model[address];
1597
+ const validation = this.model[address];
1598
+ if (validation !== undefined || isRange(address) || !this._ranges.size) {
1599
+ return validation;
1600
+ }
1601
+ // fall back to range-keyed entries that contain the address
1602
+ const key = this._findRangeKey(address);
1603
+ return key ? this.model[key] : undefined;
1556
1604
  }
1557
1605
  remove(address) {
1606
+ if (isRange(address)) {
1607
+ this._ranges.delete(address);
1608
+ } else {
1609
+ this._splitRangeAt(address);
1610
+ }
1558
1611
  this.model[address] = undefined;
1559
1612
  }
1613
+ _addRange(key) {
1614
+ if (!this._ranges.has(key)) {
1615
+ this._ranges.set(key, colCache.decodeEx(key));
1616
+ }
1617
+ }
1618
+ _findRangeKey(address) {
1619
+ if (!this._ranges.size) {
1620
+ return undefined;
1621
+ }
1622
+ const {
1623
+ row,
1624
+ col
1625
+ } = colCache.decodeAddress(address);
1626
+ for (const [key, {
1627
+ top,
1628
+ left,
1629
+ bottom,
1630
+ right
1631
+ }] of this._ranges) {
1632
+ if (this.model[key] && row >= top && row <= bottom && col >= left && col <= right) {
1633
+ return key;
1634
+ }
1635
+ }
1636
+ return undefined;
1637
+ }
1638
+
1639
+ // If a single cell lies inside a range-keyed entry, replace that entry with up to four
1640
+ // rectangles covering the rest of the range, so the cell can get its own validation
1641
+ // (or none) without overlapping sqrefs.
1642
+ _splitRangeAt(address) {
1643
+ const key = this._findRangeKey(address);
1644
+ if (!key) {
1645
+ return;
1646
+ }
1647
+ const validation = this.model[key];
1648
+ const {
1649
+ row,
1650
+ col
1651
+ } = colCache.decodeAddress(address);
1652
+ const {
1653
+ top,
1654
+ left,
1655
+ bottom,
1656
+ right
1657
+ } = this._ranges.get(key);
1658
+ delete this.model[key];
1659
+ this._ranges.delete(key);
1660
+ const addRect = (t, l, b, r) => {
1661
+ if (t > b || l > r) {
1662
+ return;
1663
+ }
1664
+ const tl = colCache.encodeAddress(t, l);
1665
+ if (t === b && l === r) {
1666
+ this.model[tl] = validation;
1667
+ } else {
1668
+ const rangeKey = `${tl}:${colCache.encodeAddress(b, r)}`;
1669
+ this.model[rangeKey] = validation;
1670
+ this._addRange(rangeKey);
1671
+ }
1672
+ };
1673
+ addRect(top, left, row - 1, right); // rows above
1674
+ addRect(row + 1, left, bottom, right); // rows below
1675
+ addRect(row, left, row, col - 1); // left of the cell
1676
+ addRect(row, col + 1, row, right); // right of the cell
1677
+ }
1560
1678
  }
1561
1679
  module.exports = DataValidations;
1562
1680
 
1563
- },{}],6:[function(require,module,exports){
1681
+ },{"../utils/col-cache":20}],6:[function(require,module,exports){
1564
1682
  'use strict';
1565
1683
 
1566
1684
  const _ = require('../utils/under-dash');
@@ -1851,6 +1969,34 @@ class Image {
1851
1969
  }
1852
1970
  }
1853
1971
  }
1972
+ function isFiniteNumber(value) {
1973
+ return typeof value === 'number' && Number.isFinite(value);
1974
+ }
1975
+ Image.validateRange = function validateRange(range) {
1976
+ if (typeof range === 'string') {
1977
+ let decoded;
1978
+ try {
1979
+ decoded = colCache.decode(range);
1980
+ } catch (e) {
1981
+ decoded = {};
1982
+ }
1983
+ const isRange = ['top', 'left', 'bottom', 'right'].every(key => isFiniteNumber(decoded[key]));
1984
+ const isAddress = isFiniteNumber(decoded.row) && isFiniteNumber(decoded.col);
1985
+ if (!isRange && !isAddress) {
1986
+ throw new Error(`Invalid image range "${range}": expected a cell range such as "B2:D6"`);
1987
+ }
1988
+ return;
1989
+ }
1990
+ if (!range || typeof range !== 'object') {
1991
+ throw new Error('Invalid image range: expected a cell range string or an object with tl and br or ext');
1992
+ }
1993
+ if (!range.br && !range.ext) {
1994
+ throw new Error('Invalid image range: an object range needs either br or ext');
1995
+ }
1996
+ if (range.ext && typeof range.ext !== 'object') {
1997
+ throw new Error('Invalid image range: ext must be an object with width and height');
1998
+ }
1999
+ };
1854
2000
  module.exports = Image;
1855
2001
 
1856
2002
  },{"../utils/col-cache":20,"./anchor":2}],9:[function(require,module,exports){
@@ -4177,7 +4323,14 @@ class Worksheet {
4177
4323
 
4178
4324
  // =========================================================================
4179
4325
  // Images
4326
+ _validateImageId(imageId) {
4327
+ if (imageId === undefined || imageId === null || !this._workbook.getImage(imageId)) {
4328
+ throw new Error(`Image with id ${imageId} not found. Add it with workbook.addImage() first`);
4329
+ }
4330
+ }
4180
4331
  addImage(imageId, range) {
4332
+ this._validateImageId(imageId);
4333
+ Image.validateRange(range);
4181
4334
  const model = {
4182
4335
  type: 'image',
4183
4336
  imageId,
@@ -4189,6 +4342,7 @@ class Worksheet {
4189
4342
  return this._media.filter(m => m.type === 'image');
4190
4343
  }
4191
4344
  addBackgroundImage(imageId) {
4345
+ this._validateImageId(imageId);
4192
4346
  const model = {
4193
4347
  type: 'background',
4194
4348
  imageId
@@ -6387,6 +6541,7 @@ module.exports = {
6387
6541
  CoreProperties: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
6388
6542
  ExtenderProperties: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
6389
6543
  Comments: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
6544
+ Drawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
6390
6545
  VmlDrawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
6391
6546
  Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
6392
6547
  PivotCacheDefinition: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
@@ -9652,18 +9807,66 @@ class AutoFilterXform extends BaseXform {
9652
9807
  const firstAddress = getAddress(model.from);
9653
9808
  const secondAddress = getAddress(model.to);
9654
9809
  if (firstAddress && secondAddress) {
9655
- xmlStream.leafNode('autoFilter', {
9656
- ref: `${firstAddress}:${secondAddress}`
9657
- });
9810
+ const ref = `${firstAddress}:${secondAddress}`;
9811
+ if (model.exclude && model.exclude.length) {
9812
+ // hide the filter button on the excluded columns (0 = leftmost column of the range)
9813
+ xmlStream.openNode('autoFilter', {
9814
+ ref
9815
+ });
9816
+ model.exclude.forEach(colId => {
9817
+ xmlStream.leafNode('filterColumn', {
9818
+ colId,
9819
+ hiddenButton: '1'
9820
+ });
9821
+ });
9822
+ xmlStream.closeNode();
9823
+ } else {
9824
+ xmlStream.leafNode('autoFilter', {
9825
+ ref
9826
+ });
9827
+ }
9658
9828
  }
9659
9829
  }
9660
9830
  }
9661
9831
  }
9662
9832
  parseOpen(node) {
9663
- if (node.name === 'autoFilter') {
9664
- this.model = node.attributes.ref;
9833
+ switch (node.name) {
9834
+ case 'autoFilter':
9835
+ this.ref = node.attributes.ref;
9836
+ this.exclude = [];
9837
+ this.model = this.ref;
9838
+ return true;
9839
+ case 'filterColumn':
9840
+ {
9841
+ const {
9842
+ colId,
9843
+ hiddenButton,
9844
+ showButton
9845
+ } = node.attributes;
9846
+ const hidden = hiddenButton === '1' || hiddenButton === 'true' || showButton === '0' || showButton === 'false';
9847
+ if (hidden && colId !== undefined) {
9848
+ this.exclude.push(parseInt(colId, 10));
9849
+ }
9850
+ return true;
9851
+ }
9852
+ default:
9853
+ return true;
9665
9854
  }
9666
9855
  }
9856
+ parseClose(name) {
9857
+ if (name === 'autoFilter') {
9858
+ if (this.exclude.length && this.ref) {
9859
+ const [from, to] = this.ref.split(':');
9860
+ this.model = {
9861
+ from,
9862
+ to: to || from,
9863
+ exclude: this.exclude
9864
+ };
9865
+ }
9866
+ return false;
9867
+ }
9868
+ return true;
9869
+ }
9667
9870
  }
9668
9871
  module.exports = AutoFilterXform;
9669
9872
 
@@ -11345,6 +11548,9 @@ const utils = require('../../../utils/utils');
11345
11548
  const colCache = require('../../../utils/col-cache');
11346
11549
  const BaseXform = require('../base-xform');
11347
11550
  const Range = require('../../../doc/range');
11551
+
11552
+ // sqref ranges with more cells than this are kept as a single range-keyed model entry
11553
+ const MAX_EXPANDED_RANGE_CELLS = 1000;
11348
11554
  function assign(definedName, attributes, name, defaultValue) {
11349
11555
  const value = attributes[name];
11350
11556
  if (value !== undefined) {
@@ -11544,12 +11750,20 @@ class DataValidationsXform extends BaseXform {
11544
11750
  }
11545
11751
  // The four known cases: 1. E4:L9 N4:U9 2.E4 L9 3. N4:U9 4. E4
11546
11752
  const list = this._address.split(/\s+/g) || [];
11753
+ // Large ranges (e.g. a whole column 'A2:A1048576') are kept as one range-keyed entry
11754
+ // instead of being expanded into one entry per cell, which could take minutes and
11755
+ // gigabytes (exceljs#1842). DataValidations.find() resolves them. Small ranges are
11756
+ // still expanded so the model keeps its per-cell shape and lookups stay O(1).
11547
11757
  list.forEach(addr => {
11548
11758
  if (addr.includes(':')) {
11549
11759
  const range = new Range(addr);
11550
- range.forEachAddress(address => {
11551
- this.model[address] = this._dataValidation;
11552
- });
11760
+ if (range.count > MAX_EXPANDED_RANGE_CELLS) {
11761
+ this.model[range.range] = this._dataValidation;
11762
+ } else {
11763
+ range.forEachAddress(address => {
11764
+ this.model[address] = this._dataValidation;
11765
+ });
11766
+ }
11553
11767
  } else {
11554
11768
  this.model[addr] = this._dataValidation;
11555
11769
  }