From f1509526349cb0f014086db44ca6a6e17ee28184 Mon Sep 17 00:00:00 2001 From: Andrii Krupskyi Date: Tue, 7 Sep 2021 11:04:30 +0300 Subject: [PATCH 01/46] fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) (#1815) * fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) * test: add integration test to cover #1814 * fix: lock jasmine version to prevent pipeline got stuck issue Co-authored-by: Andrii Krupskyi Co-authored-by: Siemienik Pawel --- gruntfile.js | 23 +++++++++----- lib/xlsx/xform/style/dxf-xform.js | 5 +-- lib/xlsx/xform/style/styles-xform.js | 16 +++++----- .../workbook-xlsx-writer.spec.js | 25 +++++++++++++++ spec/utils/data/conditional-formatting.json | 31 ++++++++++++++++++- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/gruntfile.js b/gruntfile.js index cc1574170..384b04500 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -27,14 +27,17 @@ module.exports = function(grunt) { browserify: { options: { transform: [ - ['babelify', { - // enable babel transpile for node_modules - global: true, - presets: ['@babel/preset-env'], - // core-js should not be transpiled - // See https://github.com/zloirock/core-js/issues/514 - ignore: [/node_modules[\\/]core-js/], - }], + [ + 'babelify', + { + // enable babel transpile for node_modules + global: true, + presets: ['@babel/preset-env'], + // core-js should not be transpiled + // See https://github.com/zloirock/core-js/issues/514 + ignore: [/node_modules[\\/]core-js/], + }, + ], ], browserifyOptions: { // enable source map for browserify @@ -119,6 +122,10 @@ module.exports = function(grunt) { }, jasmine: { + options: { + version: '3.8.0', + noSandbox: true, + }, dev: { src: ['./dist/exceljs.js'], options: { diff --git a/lib/xlsx/xform/style/dxf-xform.js b/lib/xlsx/xform/style/dxf-xform.js index ea22d365f..5cb2d78b9 100644 --- a/lib/xlsx/xform/style/dxf-xform.js +++ b/lib/xlsx/xform/style/dxf-xform.js @@ -39,8 +39,9 @@ class DxfXform extends BaseXform { if (model.font) { this.map.font.render(xmlStream, model.font); } - if (model.numFmt) { - this.map.numFmt.render(xmlStream, model.numFmt); + if (model.numFmt && model.numFmtId) { + const numFmtModel = {id: model.numFmtId, formatCode: model.numFmt}; + this.map.numFmt.render(xmlStream, numFmtModel); } if (model.fill) { this.map.fill.render(xmlStream, model.fill); diff --git a/lib/xlsx/xform/style/styles-xform.js b/lib/xlsx/xform/style/styles-xform.js index e1071c424..bf749e7bc 100644 --- a/lib/xlsx/xform/style/styles-xform.js +++ b/lib/xlsx/xform/style/styles-xform.js @@ -135,9 +135,7 @@ class StylesXform extends BaseXform { }); xmlStream.closeNode(); - this.map.cellStyleXfs.render(xmlStream, [ - {numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}, - ]); + this.map.cellStyleXfs.render(xmlStream, [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}]); xmlStream.openNode('cellXfs', {count: model.styles.length}); model.styles.forEach(styleXml => { @@ -150,9 +148,7 @@ class StylesXform extends BaseXform { this.map.fonts.render(xmlStream, model.fonts); this.map.fills.render(xmlStream, model.fills); this.map.borders.render(xmlStream, model.borders); - this.map.cellStyleXfs.render(xmlStream, [ - {numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}, - ]); + this.map.cellStyleXfs.render(xmlStream, [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}]); this.map.cellXfs.render(xmlStream, model.styles); } @@ -313,8 +309,7 @@ class StylesXform extends BaseXform { // ------------------------------------------------------- // number format if (style.numFmtId) { - const numFmt = - this.index.numFmt[style.numFmtId] || NumFmtXform.getDefaultFmtCode(style.numFmtId); + const numFmt = this.index.numFmt[style.numFmtId] || NumFmtXform.getDefaultFmtCode(style.numFmtId); if (numFmt) { model.numFmt = numFmt; } @@ -349,6 +344,11 @@ class StylesXform extends BaseXform { } addDxfStyle(style) { + if (style.numFmt) { + // register numFmtId to use it during dxf-xform rendering + style.numFmtId = this._addNumFmtStr(style.numFmt); + } + this.model.dxfs.push(style); return this.model.dxfs.length - 1; } diff --git a/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js b/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js index 6feb93aca..b74d8f496 100644 --- a/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js +++ b/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js @@ -588,5 +588,30 @@ describe('WorkbookWriter', () => { testUtils.checkTestBook(wb2, 'xlsx', ['conditionalFormatting']); }); }); + + it('with conditional formatting that contains numFmt (#1814)', async () => { + const sheet = 'conditionalFormatting'; + const options = {filename: TEST_XLSX_FILE_NAME, useStyles: true}; + + // generate file with conditional formatting that contains styles with numFmt + const wb1 = new ExcelJS.stream.xlsx.WorkbookWriter(options); + const ws1 = wb1.addWorksheet(sheet); + const cf1 = testUtils.conditionalFormatting.abbreviation; + ws1.addConditionalFormatting(cf1); + await wb1.commit(); + + // read generated file and extract saved conditional formatting rule + const wb2 = new ExcelJS.Workbook(); + await wb2.xlsx.readFile(TEST_XLSX_FILE_NAME); + const ws2 = wb2.getWorksheet(sheet); + const [cf2] = ws2.conditionalFormattings; + + // verify that rules from generated file contain styles with valid numFmt + cf2.rules.forEach(rule => { + expect(rule.style.numFmt).to.exist(); + expect(rule.style.numFmt.id).to.be.a('number'); + expect(rule.style.numFmt.formatCode).to.be.a('string'); + }); + }); }); }); diff --git a/spec/utils/data/conditional-formatting.json b/spec/utils/data/conditional-formatting.json index 7a58a3ee0..d80b53998 100644 --- a/spec/utils/data/conditional-formatting.json +++ b/spec/utils/data/conditional-formatting.json @@ -61,6 +61,35 @@ } ] }, + "abbreviation": { + "ref": "A:A", + "rules": [ + { + "type": "cellIs", + "operator": "between", + "formulae": [1000, 1000000], + "style": { "numFmt": "#,##0.000,\\K;-#,##0.000,\\K" } + }, + { + "type": "cellIs", + "operator": "between", + "formulae": [-1000, -1000000], + "style": { "numFmt": "#,##0.000,\\K;-#,##0.000,\\K" } + }, + { + "type": "cellIs", + "operator": "greaterThan", + "formulae": [1000000], + "style": { "numFmt": "#,##0.000,,\\M;-#,##0.000,,\\M" } + }, + { + "type": "cellIs", + "operator": "lessThan", + "formulae": [-1000000], + "style": { "numFmt": "#,##0.000,,\\M;-#,##0.000,,\\M" } + } + ] + }, "types": [ "expression", "cellIs", @@ -71,4 +100,4 @@ "containsText", "timePeriod" ] -} \ No newline at end of file +} From 33d833f76b7a183c73f308697498ea96452a2a4d Mon Sep 17 00:00:00 2001 From: Dmitry <4129726+drdmitry@users.noreply.github.com> Date: Tue, 7 Sep 2021 10:30:49 +0200 Subject: [PATCH 02/46] inlineStr cell type support #1575 (#1576) * inlineStr cell type support #1575 * Added integration test for #1575 Co-authored-by: Siemienik Pawel Co-authored-by: Andreas Lubbe --- lib/stream/xlsx/worksheet-reader.js | 7 +++++++ spec/integration/data/test-issue-1575.xlsx | Bin 0 -> 4776 bytes spec/integration/pr/test-pr-1576.spec.js | 23 +++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 spec/integration/data/test-issue-1575.xlsx create mode 100644 spec/integration/pr/test-pr-1576.spec.js diff --git a/lib/stream/xlsx/worksheet-reader.js b/lib/stream/xlsx/worksheet-reader.js index ade13f426..8cecd5c17 100644 --- a/lib/stream/xlsx/worksheet-reader.js +++ b/lib/stream/xlsx/worksheet-reader.js @@ -214,6 +214,12 @@ class WorksheetReader extends EventEmitter { current = c.v = {text: ''}; } break; + case 'is': + case 't': + if (c) { + current = c.v = {text: ''}; + } + break; case 'mergeCell': break; default: @@ -307,6 +313,7 @@ class WorksheetReader extends EventEmitter { break; } + case 'inlineStr': case 'str': cell.value = utils.xmlDecode(c.v.text); break; diff --git a/spec/integration/data/test-issue-1575.xlsx b/spec/integration/data/test-issue-1575.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8a8caa24aa96be0b811238ba4e3e3417100b0aa0 GIT binary patch literal 4776 zcmai12|Seh_a6J$W-#`$?}U*p*(s4MSxRc`vX3k=mShWKOJl528GE)EWEZZzkQP~D zvS&gF5&F&O-kaOMd;e#acizvu-}Aob`JVHf=NRjgkuw8;Kp>#%lBfybh_MrXBiyAO zylwqm;7&e&{g9DDc(`W|!QkysI;zi*PayM27fW`CHnY``AryoYf*-qgw-1y%5%Ti9 zf=dtyw@1JF&gu)eZ|&DJc939!r79+hK$hw?m;h+i6PqG>DtyEyqFd)$QYZXdHHUoJ zU{fLga*$PF72~b=4$JeGR8Qkh(k;HKrk~|4K%Fk89?b371jFiF)n8p6E(ptCnO(9siZEr_N+tVf{UIr@qd- zuN3c9s1cQLue6#hdyeIQYHh4f1*EcbiJ%|>0I0|S0OLa_0RSK%>XE2PIV;`VN5Vgm z6ac`VQ2Kj%yZJafI{NxZA07^&%t!abbD_*p+X63?c~>LCk__WQT{9nNt~?&_SmuP3 zehqgg>m9$#*M`xK#P~TS4t~!zsy!Qc8~5A|Icye77a0Oj;eT?X=$#{OyQ1)o-+hZT6tA3o70dx9;1aWYSI5?vNG+ zh`)2Wq2z*fXaEfiZn2k>_bRz&M14k4YRVB`-L-Rq|B8_Ramu17>cI0}#S${1NMBy#uDIB1T@VU*X6=+*oMs8wIe*J4>P9 z6-2s!f7-*|f*!h60%~ zzm;dN2Hs<`-hzQVq`pZ-!==8p_p-3j+%)^7k}||kwp0&{7MVYIxTF3fJ$_ed0zWG4 zNP1X%JGviD5-H+&!k|7)JCuPE2SG$bgOcCgiIH2@iG=5K+vpa6-MIzVUXI^Sz~I{F zo(~3hSj&TDvB&P5Qj*A-rGQrTee4A>4ZQbiCS!$ICSb9GwmCq9=EsI|_I-+8S-}n* zl^5I7QB$;xZ*(aI{2#a*2C~r=s6=TZ%-D|Wy2si{v=@?WruAA%@>MsGzm;A*Tpou{@G|U&ACOHqw~aK5HD0W`Z_ZUS}6yp1ZSu1EI9MnYoqQBtUC1_o%BSVD9pgBWgJa6J_@tc{KeUDtk; z0)FzL8Fs*^(lTxW;)cSs^}q9uUBggn%rQJ2Y3003)*iGVq*ge6Y&tNQGc-cutjx7> zAJ&7(l1j=VPEfsPh3d$rG9qG%%eB6K)Djl*v@?jT2@jJFej2fD*`&^dcXt})YMPpM z>OeRtHZGS&j68}g_!JUa>oGM2ck?AfdhgCTBI2~V28#DGvc}GF93*cvlP*p_UhWMI z7T1j=yVE!?Rv@Hm#i#-Y|B=qM8G}VPzY>>}IZ5`$*6DpwD_)>DH__xgcpZtV(9&m8 zGU#YNxrFp{MO_ngwU8cZ>pYH^!PW0&_su5Zv?T5EEOr-7yGfbrjiadzz43X+v7P}4 zas0;#F{%?+8|M`LQnapIaoX5QQuq?cN^2Tgp=T}KZc!@H&c!I>Z!OYZJCJXugt4<# zFC^tKzeq)at8)8ll@E*om@=i_e7tA$Ml~qE5H(6a&Oz<@F}rM-i?PcO9}BIu4c8u4 zd|3ase-2TceUW3MGw3xZ{VHW&gY4&`fh@d4CVLi9;>xR&8i*hQpzn7Rtniy zSJZF2Ghutr47y(&tFABfA+#W441b+N)U9T8U}Cp!I`wUL|M4ZZj5CbXvyu7Pk3Cs> z7e^Gs`}IQ1Fkro}z?H>&-=B$JJeZJzuoq}4jMACH-B%Rin(>d-Vg;mC_Cs^dhT{|M ztmav+-94tHbKKHJU0g1;y%BLcvHV+=>FRKnGw(+4)ld7GO*wsBm*?(I-t51#8Jva) z(uU?qcIDU43>O4;bMTONd1H}HO3816?G+bSEghO9HcPjS#(S%FsECE;m>Xs*O6Wsn zp#Lf~<{yRT<815g=wRyWP3-N&mJUn2NMsiZBOF)z!MGU(qn=4Mj`|BA2R5#OEgmgf zTrP_Nh771sJ}Bn)GWLIdoau@Mi;!rZa81~0+O!Rqda};x;!SP->d$AQ`XuSFMM^@o zLviszW%VCr0;Psy+rEGRR9 zKO*LCbwOh>!lW4Ym?}NCjPvg~?I|Tfc)uBv?(bUJFFtq1C_FY}#eF-y@NPSEIz%qB zNskAO6?&n_Av8=gBHk$daT|7PGX({hsJ*5vp?|tpW3@#3Y^Yv;n$>yU7#nT9pwJJA zmArv2h5T`rw1!T7L}GOpO<yX+fr2tRLe#`=W&2%d3tGFTFELePj4XiGBUxZonlsR zz`m{&5L+mg6f?X`4oj=UI=X-h6iV8nHI2Zcy_^{~s1m>4*rq~X-rL`>S;)x69&?bVc9@9Qnw+-K%Uh3wppQQ_!>Rv&m{f)y`BZi3RT6HftqpJQc8FvgP;yQCE@w zS0aFaBm%Le+If1q9cosTK0&iGL>Ub!pP7uXKO+&N`_3$d@E+rS_6}nA;he*LxrvtK zq%#14X_+#qI##Ei))*`y&2V`qvlTKARt1m@W}RtD1o z!+JO+6VI0*mYanyBqzr`9erkTffHsBwU6yuT=VXKyV1nM?W6zHe@imxfCrh6u2e6Q zza1R6V|nd@pe$H+zlY(TUF>*tVQ2Y5wC$&14w>`>nx&#J!PH~dLj0WQu;U3-$d|&% zL1x1}&2nm!#h81>;i3V|ps@I?oQ+AH92f}}=-rva@&L4a5L(3f9 zi9sKa;x6~pkmla)ez24lNh*$TMR3|eef$nTwd z--n#B`<#JmVPgJm5=F60B?`pk`?dLX0flpG&@rbWRe9goC@v-RTr=Q8FwjIAAFo6VE9YDvA=1r9`% z~qH_7GG4!uYWw#B^Fs(SKcTc zG{R8ksb`zY3F-}wVHxE!yJQ)qJZ5SIojq|pmv561vo@8Z&2))?M?L9(Wi2h?iXU+O zDfl!&;RH$^A_%_gTP8Pb<8z+K2)omBKdRN{( z(i+o#Lqb;Uksk1^v&h4hcq22np3MMt(O`>D2KH~H1Ky8JVf8EpAM=L2wrZ;FR+h!r z6mDc=J1HR{^*cO97X?W|2E?r>ywbG6TJL>pcVYn2`}NOPq;6^%aQrz z=Xgs4AYK6gkPwDoN6JOowYj$>e;xV9W-3<+=fpc5ecq!J193L`X|4pq`RAQ}J1PC@ z@Tf8(4jMlVPJoNx@Yk{9R|iMc2N98}Up*XcTSS%e)2e|#Jp8R$eqDN$ zZHayDr!~|34aZ+y?$^afiHAtYKTVVFaPhy;^RIr6RyC2Pewq&%yan`A_Vv a2s-sQ6kx1RK}qyZLwL0i=<@Yp%>M%wEuv)r literal 0 HcmV?d00001 diff --git a/spec/integration/pr/test-pr-1576.spec.js b/spec/integration/pr/test-pr-1576.spec.js new file mode 100644 index 000000000..bd575be4c --- /dev/null +++ b/spec/integration/pr/test-pr-1576.spec.js @@ -0,0 +1,23 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + describe('pull request 1576 - inlineStr cell type support', () => { + it('Reading test-issue-1575.xlsx', () => { + const wb = new ExcelJS.Workbook(); + return wb.xlsx + .readFile('./spec/integration/data/test-issue-1575.xlsx') + .then(() => { + const ws = wb.getWorksheet('Sheet1'); + expect(ws.getCell('A1').value).to.equal('A'); + expect(ws.getCell('B1').value).to.equal('B'); + expect(ws.getCell('C1').value).to.equal('C'); + expect(ws.getCell('A2').value).to.equal('1.0'); + expect(ws.getCell('B2').value).to.equal('2.0'); + expect(ws.getCell('C2').value).to.equal('3.0'); + expect(ws.getCell('A3').value).to.equal('4.0'); + expect(ws.getCell('B3').value).to.equal('5.0'); + expect(ws.getCell('C3').value).to.equal('6.0'); + }); + }); + }); +}); From fb838ea7a03f5de493691eb0e32836534aee59df Mon Sep 17 00:00:00 2001 From: bno1 Date: Fri, 8 Oct 2021 04:01:42 +0300 Subject: [PATCH 03/46] Fix parsing of boolean attributes (#1849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexandru Șorodoc --- lib/utils/utils.js | 4 ++++ lib/xlsx/xform/sheet/col-xform.js | 12 ++++-------- lib/xlsx/xform/sheet/data-validations-xform.js | 12 ++---------- lib/xlsx/xform/sheet/row-xform.js | 12 ++++-------- lib/xlsx/xform/style/alignment-xform.js | 4 ++-- lib/xlsx/xform/style/border-xform.js | 5 +++-- 6 files changed, 19 insertions(+), 30 deletions(-) diff --git a/lib/utils/utils.js b/lib/utils/utils.js index a85af676d..84cd212c2 100644 --- a/lib/utils/utils.js +++ b/lib/utils/utils.js @@ -163,6 +163,10 @@ const utils = { toIsoDateString(dt) { return dt.toIsoString().subsstr(0, 10); }, + + parseBoolean(value) { + return value === true || value === 'true' || value === 1 || value === '1'; + }, }; module.exports = utils; diff --git a/lib/xlsx/xform/sheet/col-xform.js b/lib/xlsx/xform/sheet/col-xform.js index 93003cbdf..0d47b42c5 100644 --- a/lib/xlsx/xform/sheet/col-xform.js +++ b/lib/xlsx/xform/sheet/col-xform.js @@ -1,3 +1,4 @@ +const utils = require('../../../utils/utils'); const BaseXform = require('../base-xform'); class ColXform extends BaseXform { @@ -51,21 +52,16 @@ class ColXform extends BaseXform { if (node.attributes.style) { model.styleId = parseInt(node.attributes.style, 10); } - if ( - node.attributes.hidden === true || - node.attributes.hidden === 'true' || - node.attributes.hidden === 1 || - node.attributes.hidden === '1' - ) { + if (utils.parseBoolean(node.attributes.hidden)) { model.hidden = true; } - if (node.attributes.bestFit) { + if (utils.parseBoolean(node.attributes.bestFit)) { model.bestFit = true; } if (node.attributes.outlineLevel) { model.outlineLevel = parseInt(node.attributes.outlineLevel, 10); } - if (node.attributes.collapsed) { + if (utils.parseBoolean(node.attributes.collapsed)) { model.collapsed = true; } return true; diff --git a/lib/xlsx/xform/sheet/data-validations-xform.js b/lib/xlsx/xform/sheet/data-validations-xform.js index 5f20f33e3..58d6f098a 100644 --- a/lib/xlsx/xform/sheet/data-validations-xform.js +++ b/lib/xlsx/xform/sheet/data-validations-xform.js @@ -12,19 +12,11 @@ function assign(definedName, attributes, name, defaultValue) { definedName[name] = defaultValue; } } -function parseBool(value) { - switch (value) { - case '1': - case 'true': - return true; - default: - return false; - } -} + function assignBool(definedName, attributes, name, defaultValue) { const value = attributes[name]; if (value !== undefined) { - definedName[name] = parseBool(value); + definedName[name] = utils.parseBoolean(value); } else if (defaultValue !== undefined) { definedName[name] = defaultValue; } diff --git a/lib/xlsx/xform/sheet/row-xform.js b/lib/xlsx/xform/sheet/row-xform.js index 4ccb73fee..890a0fc8a 100644 --- a/lib/xlsx/xform/sheet/row-xform.js +++ b/lib/xlsx/xform/sheet/row-xform.js @@ -1,4 +1,5 @@ const BaseXform = require('../base-xform'); +const utils = require('../../../utils/utils'); const CellXform = require('./cell-xform'); @@ -79,15 +80,10 @@ class RowXform extends BaseXform { if (node.attributes.s) { model.styleId = parseInt(node.attributes.s, 10); } - if ( - node.attributes.hidden === true || - node.attributes.hidden === 'true' || - node.attributes.hidden === 1 || - node.attributes.hidden === '1' - ) { + if (utils.parseBoolean(node.attributes.hidden)) { model.hidden = true; } - if (node.attributes.bestFit) { + if (utils.parseBoolean(node.attributes.bestFit)) { model.bestFit = true; } if (node.attributes.ht) { @@ -96,7 +92,7 @@ class RowXform extends BaseXform { if (node.attributes.outlineLevel) { model.outlineLevel = parseInt(node.attributes.outlineLevel, 10); } - if (node.attributes.collapsed) { + if (utils.parseBoolean(node.attributes.collapsed)) { model.collapsed = true; } return true; diff --git a/lib/xlsx/xform/style/alignment-xform.js b/lib/xlsx/xform/style/alignment-xform.js index 6cd7eb11a..75391be98 100644 --- a/lib/xlsx/xform/style/alignment-xform.js +++ b/lib/xlsx/xform/style/alignment-xform.js @@ -145,8 +145,8 @@ class AlignmentXform extends BaseXform { 'vertical', node.attributes.vertical === 'center' ? 'middle' : node.attributes.vertical ); - add(node.attributes.wrapText, 'wrapText', !!node.attributes.wrapText); - add(node.attributes.shrinkToFit, 'shrinkToFit', !!node.attributes.shrinkToFit); + add(node.attributes.wrapText, 'wrapText', utils.parseBoolean(node.attributes.wrapText)); + add(node.attributes.shrinkToFit, 'shrinkToFit', utils.parseBoolean(node.attributes.shrinkToFit)); add(node.attributes.indent, 'indent', parseInt(node.attributes.indent, 10)); add( node.attributes.textRotation, diff --git a/lib/xlsx/xform/style/border-xform.js b/lib/xlsx/xform/style/border-xform.js index 3066c8188..212484bd6 100644 --- a/lib/xlsx/xform/style/border-xform.js +++ b/lib/xlsx/xform/style/border-xform.js @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ const BaseXform = require('../base-xform'); +const utils = require('../../../utils/utils'); const ColorXform = require('./color-xform'); @@ -156,8 +157,8 @@ class BorderXform extends BaseXform { switch (node.name) { case 'border': this.reset(); - this.diagonalUp = !!node.attributes.diagonalUp; - this.diagonalDown = !!node.attributes.diagonalDown; + this.diagonalUp = utils.parseBoolean(node.attributes.diagonalUp); + this.diagonalDown = utils.parseBoolean(node.attributes.diagonalDown); return true; default: this.parser = this.map[node.name]; From b19b4c0a4201a71acb93648824916ab879cf080b Mon Sep 17 00:00:00 2001 From: Todd Hambley Date: Sat, 9 Oct 2021 05:38:50 -0400 Subject: [PATCH 04/46] add optional custom auto-filter to table (#1670) * add optional custom auto-filter to table * filters may be customFilters or just filters Co-authored-by: Todd Hambley Co-authored-by: Siemienik Pawel --- lib/xlsx/xform/table/custom-filter-xform.js | 33 ++++++++ lib/xlsx/xform/table/filter-column-xform.js | 74 ++++++++++++++++-- lib/xlsx/xform/table/filter-xform.js | 31 ++++++++ spec/integration/data/test-issue-1669.xlsx | Bin 0 -> 11345 bytes ...ptional-custom-autofilter-on-table.spec.js | 8 ++ .../xform/table/custom-filter-xform.spec.js | 30 +++++++ .../xform/table/filter-column-xform.spec.js | 19 +++++ 7 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 lib/xlsx/xform/table/custom-filter-xform.js create mode 100644 lib/xlsx/xform/table/filter-xform.js create mode 100644 spec/integration/data/test-issue-1669.xlsx create mode 100644 spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js create mode 100644 spec/unit/xlsx/xform/table/custom-filter-xform.spec.js diff --git a/lib/xlsx/xform/table/custom-filter-xform.js b/lib/xlsx/xform/table/custom-filter-xform.js new file mode 100644 index 000000000..d0e9d272f --- /dev/null +++ b/lib/xlsx/xform/table/custom-filter-xform.js @@ -0,0 +1,33 @@ +const BaseXform = require('../base-xform'); + +class CustomFilterXform extends BaseXform { + get tag() { + return 'customFilter'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + val: model.val, + operator: model.operator, + }); + } + + parseOpen(node) { + if (node.name === this.tag) { + this.model = { + val: node.attributes.val, + operator: node.attributes.operator, + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = CustomFilterXform; diff --git a/lib/xlsx/xform/table/filter-column-xform.js b/lib/xlsx/xform/table/filter-column-xform.js index 20fd24628..c900996d4 100644 --- a/lib/xlsx/xform/table/filter-column-xform.js +++ b/lib/xlsx/xform/table/filter-column-xform.js @@ -1,6 +1,29 @@ const BaseXform = require('../base-xform'); +const ListXform = require('../list-xform'); + +const CustomFilterXform = require('./custom-filter-xform'); +const FilterXform = require('./filter-xform'); class FilterColumnXform extends BaseXform { + constructor() { + super(); + + this.map = { + customFilters: new ListXform({ + tag: 'customFilters', + count: false, + empty: true, + childXform: new CustomFilterXform(), + }), + filters: new ListXform({ + tag: 'filters', + count: false, + empty: true, + childXform: new FilterXform(), + }), + }; + } + get tag() { return 'filterColumn'; } @@ -10,6 +33,17 @@ class FilterColumnXform extends BaseXform { } render(xmlStream, model) { + if (model.customFilters) { + xmlStream.openNode(this.tag, { + colId: model.colId, + hiddenButton: model.filterButton ? '0' : '1', + }); + + this.map.customFilters.render(xmlStream, model.customFilters); + + xmlStream.closeNode(); + return true; + } xmlStream.leafNode(this.tag, { colId: model.colId, hiddenButton: model.filterButton ? '0' : '1', @@ -18,20 +52,44 @@ class FilterColumnXform extends BaseXform { } parseOpen(node) { - if (node.name === this.tag) { - const {attributes} = node; - this.model = { - filterButton: attributes.hiddenButton === '0', - }; + if (this.parser) { + this.parser.parseOpen(node); return true; } - return false; + const {attributes} = node; + switch (node.name) { + case this.tag: + this.model = { + filterButton: attributes.hiddenButton === '0', + }; + return true; + default: + this.parser = this.map[node.name]; + if (this.parser) { + this.parseOpen(node); + return true; + } + throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`); + } } parseText() {} - parseClose() { - return false; + parseClose(name) { + if (this.parser) { + if (!this.parser.parseClose(name)) { + this.parser = undefined; + } + return true; + } + switch (name) { + case this.tag: + this.model.customFilters = this.map.customFilters.model; + return false; + default: + // could be some unrecognised tags + return true; + } } } diff --git a/lib/xlsx/xform/table/filter-xform.js b/lib/xlsx/xform/table/filter-xform.js new file mode 100644 index 000000000..91a620a02 --- /dev/null +++ b/lib/xlsx/xform/table/filter-xform.js @@ -0,0 +1,31 @@ +const BaseXform = require('../base-xform'); + +class FilterXform extends BaseXform { + get tag() { + return 'filter'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + val: model.val, + }); + } + + parseOpen(node) { + if (node.name === this.tag) { + this.model = { + val: node.attributes.val, + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = FilterXform; diff --git a/spec/integration/data/test-issue-1669.xlsx b/spec/integration/data/test-issue-1669.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..cc9dab8f15c8504047cd7d39916bfe5b9c92c445 GIT binary patch literal 11345 zcmeHtWmuH!+V)6ycej*uhjcd*BGMrU2-4l%9TL)=5=so=(A^=8bR#i<#5=my{#=XY z{*L$Wd+h!Ec|zMu2zOH~0H1_uBKKmY&$lmJ92=>X?P0003T0DuiZfYOz) zedB2M#?e5-&Cbk0kHyu-nj#kliar|v1^NAdum9i^=+qc^4PwJ+!M{eAo-t|AnDBDS-e(MU_;x zcH6o#4pR-xQzDc@HzOqg_V}n_b>AhSe9bha+uZmT5DT41=UbhM^(T9}jEPbVzr=4* zac$4Xp(Uh~*x{Krj-yT4mik9CZj|hbJ&LAXX(`hX$(JORI4-qLS@*P&t+eOdV)2q1 zm^B}bf-}*U|bmpe}ySzV{ruKZS1m*U@S(u!hR;~jd=!j*V6B0ugOMz z2H~J0L>rMI>S|zbX6?Yr^5gzrHT@6P&A)8DJYGo!#D)@lBzqIm3!Ykx#Sm9;5tD7E z)bRF|TR^Xm%A+A(YeCi@iM&6Cy85qM@_Nh2Wp>LU5dg4T~24jRpn+OrRfFu}CIL+@x zx0241@nz+sDe-3q6~WaXd9!yDM$&y|k_&fGMMC)%_s7$4dmW5l%~g8#T2r215otcN z;J2(c&axMvax*ZsZatrC)t&SAkeWo&C}^~0Z)YpB_#vtf9@ueTy|idAhuzshu>P zEY~HKN#(gp6)~l;-lAW$y>+_~9b3Rv;-DV>uE1ghr|F&Nons@meJzf$$s^GUbq$L} zn&>=>4#r#-Nzm)+^1$-Y;DVQerE!t#rG|>y1z#KAY-!jHeYuGurZp@Us?Lt6@27mT-Q!&+^?-KF4ECsxlh+5&KG=tXy{HuINl2(Io41 z%K?RIcfJebb;alnPo^inQ;JLSt+bBlE}v5c(@+Ror^fxkQWvQmg)~;fF-1EgblW9=~sMbwR>|5UVe4GY&rgQO|D5M+0WL$ z`>=aaFB*2d8UJ=8`E}0A%bN3^9i;)Ey65Er{IK&b%>r}9VcbmPte8F&FO=qYBb)DU zearQ1z1AC9+G(A=?D+S+j6C%KW+a{L)>x%i$%_dEa_<|NgDt|BFb2_JwvK1bT_r}Z zqKuZ=eJw?FoqYOT3L^lHZ8KXW%G~Vi>r_W7`-8+R`(wmQF-uu;p`0)|`T45Q=DS7` ztliXKI|Obucf1>3^ogNdEbi41e{T>ZhHW74$|Aj@9%(({w8e{jHD^tf3gW(HzarOz z>c@cZ8+An;@R#*sMO+kZzU$C>mYYrFafNmas{?KI>hkqBO6|17K>vXRrs+%ia6OVfSVbF55n9jL z1V+P+l&!8{VdWW_wNh3wZ5E{};ZrwE0|g{%=#p@mJFyD8ATnVJs6=m)(%HPq zjBMl1erFk;sfOXQN!00$(syNRv&^Lz71QcXJbFTi+!uzNkRAQxgTLjCKAsK~tXo#j z%Y;=>544Wt)3+qrG(`%HFwbr{2aP#CT78b4!W=^G(ReCQz^Thh>rl&x663XdUBkpR zBywS|b-(*t<`jQ-d6NP$h5eB5o&Z9LKTV;7g_)V71MAO){fAA=Na}kt&xR3v1bdAJ zb2MhI@D^r3Y@EUZ4c0$n-F#&bX zmq)xFHYy+#%c7A{s+n;+=IG-qPWebPn?9l`ak2O9qXS)ADyJ3WF@kKYW=5l(xTQ6FnnTXjtjAWXlG-e<&dhbq88^7gJre)b+U45u0WR|hd<&Ax#bir|Z zt}U-_R}ooi?YnN*d*yV6zbe)H=`pbKo<>m!mD@VdY9&vdv_L zEh{y=GnKO&evsEb&)BEAp1bj`7MxilZQLN zS$kTtdiAuP*nG~oaw<7n3d2-j6OLtAFJ@^{#>nLecPWyA@MXjhRb~h=bF4^DCAvvk zOc4Fo`dFVPt?{x>8OG3KLP^`QPu>|X$MUu1qBSs9HM>#tCpo-g`s=jQNJ=O^mqik` zsCMX+O{+(I2+a9@Z>#YISvdwKZWFf0R3E|oh=Ivt0wMI6(F87_pN%-+kzj+rtzwm= zl`OR8(Yb^C85@e4Z;zJK8>ozel>XcT@GI`t%SWVyu(PG|49y}Pay5sj9wT+!fo1jS zIRvL?+F`?s9umt*Aj8OydFi)pqif_4s{Yfjr*rP|4MWH*1pjZz%<)e$H{j{&L&&@a zJCq|jdan44IwF*&lm?iy-Rwd?7k@wyL`;E3KfHaDXi{ha<_*v);7Bek`O& z8ug9AmAn^%O&aZMy9GiHo-?F;{MfJ+xZ+MR!HQTNMl4r!A)hCykT}fkZUt})o|bIf zkTqtM%qY#RaCh(&y?W)1fsBZC!8!e$t0b^rQ`U5BSV_G<5DjeO0;>^LEkT z!rxCY!-?#Bgnkh@3~1Q3=2)P^vw?expYXDp1nj2(;|Z#wbJW6dzVcJn|!nkXbx?Uo2z zU$Jd;2(P8$2(TLZ2Jsue!x(20b;3O_JeMON-^}JG!s*ZxsV;qKPVaXw$6{TS(=pqs zUmB_uPMz_>k4M3%fPR(O#`Nx#pd3%jN@F0M6I6&z>jGmFFxO<-fCwDTnjhk(SlE1O z%mKx9*yAja!Kcd{Pi~^JRilROfH6_6dhd-#$6J1ayC<}HZVMXa-Ky5;WS>k29}%4Z zgZCP>S0W7Uh^>X+f}@S?6;3NH@H;weh^{yF%+@@f%(g)pz|04<@(p&}Cx-Pssc?~M zM$p$SIxPP3!7Lmcib*0!9~V>uwA6E_ebk;oIb*h}tRRllT6(a@5sz|8xXmSm`4)*sMq{7=#Ca}w%zK(9hyfltC z>zN1wP_R|*1F)0WdQSPvJ{{#={`y<%n2Nq=E@{rCC`7+_q4NS&Sp@?muRRF{RIbs{ zl)KcRK%QGnVb1u9v~#}nfxk}ulgMrnbP zN~8%P-VP_91Wj4Y#4_Y*mmen|p%T?ExcT|Ney*LeZ`_YnQ!#Dqm{I@@kfo6Fr-^<( ze1a2|6D}dyM^8IUmL{d@>X68WHk90~Io10k=zsrH}SzvwC8EiXT0$K-xZD;KtJ}*5x+O8>i`W zOQD$)R`2VzF59-Ye&NVZ2wkVPSuL*D>sKEQ?oPg%M1YmUr3E}?jR!NWE@vD2j0v)X zM92zk&DkR{*qK#fTAx&Yr{eIPNe=yDr)M2}?2N={0qJJI{O08+{c=*vyoD{P{o5y;L!3c`;t) z|9N(zRVAe+Oc9p9?rK3Ia;KL}5QQ3Nq#{EOYiCyFk}{Fr{tcT^z!B#uY8*HgKG9kU zY~vs0`2GpZN70u~@=|`ik0U1I2L@8pj=sh#V`?!%f3cNySB&R8zfJt`{XX);JohyT z*W%%&-egMx)}El#Mq;on=oUVyIRvi{6@_(RbMRd3lL*4c?``;pHb>vC2Apz6DVDlk z=v6Fytw07d{E@PKwi;c3tEY;sFmv$K$x=kVO z@O(vkipR0rVTSW?kTArl?X?!Mv1k9sFT%`gUOaslWmBaK>E>%4ar9r9?wUw1h^Qps zqm0oToU5GcIqS9cF=HES#l`mqY1BT(T9gw}B3Du6NISDIzre#&+VM^*)x?|00leNT zZ~xrqx1lwE4%{aJG-!mbg$+J)rm60)R2*(Jch_Yr(SKAO{#;WuZYO!Y$;s8;QKtkb zU5JYHqA`u8jw5muW^1a1JR{>(rLL4bsyhcx*~gshF^krimkOBt1*lwON&a-hZ|6~W zpJqfwjkO#o8!e-!oRGW@8GvQY<>V70j^OfGU6$6_WWX2h%wt}WcjVv>4QnrvkXvxx zIG&jj#V()>*R6;1Kdz#kfzrA97#EPAF4sN5K!z-Q@FF78(HR-(y=BmO35SnIb)6}) zELe<--&xu*Zb?$r*gKCAnvaqs+NV^9>oLqQ=Zv`4!IltzdaI$>Y0)d{ZZRw|JMY`E!k8%yt)O!+GS2H(nWJ%0rYQXD3L-5 zF+zA4`dwCF+u!;y$^3P_YDgSc1o302fA}y5M>lIThabUPzNV7xJR1rSebxuV(Z#I! zZ8A(lL_vuk)FK-NX?2$~0lVgIc$rOb+f%#_6zaX=(&Eu@v&&xQ*1qL7g$hBWh9&k*3{m>@)l7#We+=Ej z)cgy1UPZrxX=!zMnOfQg_DHR1gLwZ=BR<-A`yh|NAm)2`N%hk@TYvR|V1d?E|7Mer zpg3lHux~jDECjwvs9t2$4{1~~ zb=|Tgh^JR z4X}1Bo%B=saYe^;B%GxHCZhEo6KH-0*H&R(w%Z90t8Mo1gfZB0*ujY?9^A-*lHS%V zAfpOw8R@#krSn#sGVgKo*lI%E;QvWjyioU_6STh5);)Sv+!wnlR~HkITuinIk5?J{Gs?$v{;F)cTc@=mg0 z*5^8wa^E47U{m;Sd$lZ}JPDOHk33P2K`$~DP>dW7T$d98Yyv|jZ0(K{7LFWFRbj%0 zo;nTnVIPx=ifnH2vT>H@2fLRX9aAORMQBm=qcnVXw21b*F#PwR^|s%Z9)ZpL2?r2L zi$ds)^Cz_}jO@)!pE=rFzIpYN;g0bg%AFLDT$`>Hdi8MJ*=doepKRKd{iNNzwGsoJk$d+zpbhMmR z1JRr8$|{<;S8wr4R)OHJ0T`1PhTJv_2m}edygCP8`7&Xnu(zY#0k-gJxLwN6ZDMPu zVb8{N9q>k5;Q3fYh?*bx&xd;6#JhUTye%Z zpapU)w2;H2{^NLll(7CRNBmLK`c;(pSe zzSDr(AlEBB*2cRcC3eiUkM`ZX=r!3X%^}X5dr?jzEP{Qv%QPy{a1rP=vOjhc+T$Zx zPy)d__r~IAE~9=Kp)c)J&q%BYo*@Yq%tmriE0Fh^6@lM&C-(3a`)-CqmpG|eYuuQM z-ge>G6j3(lA+DXF@L!3{gMr+2;7y(c1raKZ&jXT#p>MPE#rna)%j*3%%*9RW1{=Hh zkgff5^S<1B&;M7O|Cz-8AKUzgsW>Njr7G87qqe@RAQyH(lb$R1 zwwK>wYuLicWo?ijtDppjUlbXgPqNNlrt2xZ2sv>olngPmbE=$}y&WCVLPeXm4&hB_ zs7Yr>3SNWJh zl~ghUFC3&`@B6}8nad&$%V&~Hl0lbO@U%{;W@aeBd}X81T!g|^x&WPbN%d`VUaY11 zzSL|VhlHbrzwDDv34^x2yNF3Ok>FV9bhopQ8y5QXZiNJRHcXtiy9dEj6ZfO24IfNI z#M6Whunl2E*3$VJWhSvjsX_6oX;HK(65-&mCQm3u%75@fE=sbtnkTqA$c|cffW5uj zo~>7%l@&y#9NGl?652(lZcxA+x^H;PI@v1B?v;HzZ}D_^Yg2~)dA!{v>enL{<<9q$2!7{qgmcrng z_aSWZr&x4SFC}o?9oiJj@=y9*Ks(aX4706U%1pCgW~*;XDp`=XeACqS;f=Ur)Z(GQ zBbk%;R*&mQ!TrKjg(B(QAvsfykBg!?axY%ar;1GVYQuVl+>q+ z8^7%zOZ)K3VlRF2n0_oh-GcqmfjlC{v3a7zE!DgdHA!CO#Xd~;7wT4~Io)hp(onaG z?p9%b(k3k+I%(PRZz+Y27+@?8aZqXyT46%EEvB|6s`j>a4y-1&_GUjqR7j`&zfBg% z(Rs$JDS+5;f|g0qR?rKp*JOr!JWxU8z{LNoC*QNd889XV!h9tp@GNt4ws9=P(V zb}7GnQtU!ek0VsjY?v_pVf@Od*aDB%TXOkkP;dlmD|7}HM-P%v%7VkfBNI-nF+_&0 zP_E^M!$<0d#bSOJXcK{JB31BZ3sdkwj<==#lq7&vUq5z}Wafgib4o9O9G&AQ~`7aS~rdY6(q?i*p*B!`$s*gQbj$!XZ@ zb%ogfds}vA-TyKQLOfc6VsqJqk|6XPJKm|C6b`7-P6z~AiM3)+G!sbfzpCn za2g`MaNwhNgFzU7Qef#HBAx;0O1it2vUwI7+xy6zWVHr^TkWMnSVQpiioe;2Jg&M` z=)7ZWPH9F^y5W1>V;XHYZ-!Ob6$N_MH5~fxPA8f7s80e8u8zHZ!coenoFb!YgD+Xn z$zMHdS5><>wa8JkXJIUy_w!PYRu<5CQLGrpj0Mf+}Slk}f!t{0Acvw2cM9sznzd5z5O9 z;_tqgfk6(Pz&yBP+j{P>r&MGF(o2}HW=@}Md1CKSMDaPC84;tLEWhH-iRym;JhgOr zSY%aiA%s}sxM}A!=gSnGSYJ;X3p?uM6IOn1uiiqQJHSTS7I3sJ;`_^ivVX0=881{-_&dPgN1Xm8_+zbt=*3?Ko*oK5 z9Q*hsIteK>JQxCbDE#*p`!7)dpaAiw@V{xeKg4<1aQ%g}3~A8*yY}ltl!u+JUnu2h zzeV{O=R8Dt*em#jQjPf&;Kuni3jL37 F{{x*wmF55d literal 0 HcmV?d00001 diff --git a/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js b/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js new file mode 100644 index 000000000..3a030c090 --- /dev/null +++ b/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js @@ -0,0 +1,8 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + it('issue 1669 - optional autofilter and custom autofilter on tables', () => { + const wb = new ExcelJS.Workbook(); + return wb.xlsx.readFile('./spec/integration/data/test-issue-1669.xlsx'); + }).timeout(6000); +}); diff --git a/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js b/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js new file mode 100644 index 000000000..971ac53b7 --- /dev/null +++ b/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js @@ -0,0 +1,30 @@ +const testXformHelper = require('../test-xform-helper'); + +const CustomFilterXform = verquire('xlsx/xform/table/custom-filter-xform'); + +const expectations = [ + { + title: 'custom filter', + create() { + return new CustomFilterXform(); + }, + preparedModel: {val: '*brandywine*'}, + xml: '', + parsedModel: {val: '*brandywine*'}, + tests: ['render', 'renderIn', 'parse'], + }, + { + title: 'custom filter with operator', + create() { + return new CustomFilterXform(); + }, + preparedModel: {operator: 'notEqual', val: '4'}, + xml: '', + parsedModel: {operator: 'notEqual', val: '4'}, + tests: ['render', 'renderIn', 'parse'], + }, +]; + +describe('CustomFilterXform', () => { + testXformHelper(expectations); +}); diff --git a/spec/unit/xlsx/xform/table/filter-column-xform.spec.js b/spec/unit/xlsx/xform/table/filter-column-xform.spec.js index 2d9900c17..c15f802fe 100644 --- a/spec/unit/xlsx/xform/table/filter-column-xform.spec.js +++ b/spec/unit/xlsx/xform/table/filter-column-xform.spec.js @@ -31,6 +31,25 @@ const expectations = [ tests: ['prepare', 'render', 'renderIn', 'parse'], options: {index: 1}, }, + { + title: 'with custom filter', + create() { + return new FilterColumnXform(); + }, + initialModel: {filterButton: false, customFilters: [{val: '*brandywine*'}]}, + preparedModel: { + colId: '0', + filterButton: false, + customFilters: [{val: '*brandywine*'}], + }, + xml: + '', + get parsedModel() { + return this.initialModel; + }, + tests: ['prepare', 'render', 'renderIn', 'parse'], + options: {index: 0}, + }, ]; describe('FilterColumnXform', () => { From da5e743126753e5ade98ac33869b9884c546f248 Mon Sep 17 00:00:00 2001 From: ikzhr Date: Sat, 9 Oct 2021 19:01:52 +0900 Subject: [PATCH 05/46] Deep copy inherited style (#1850) * Deep copy style when a row is inserted with inherited style (#1813) * Add tests for row's style * Move setIfExists * Fix equal function --- lib/doc/worksheet.js | 5 ++-- lib/utils/copy-style.js | 43 ++++++++++++++++++++++++++++++ spec/integration/worksheet.spec.js | 32 ++++++++++++++++++++++ spec/unit/utils/copy-style.spec.js | 39 +++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 lib/utils/copy-style.js create mode 100644 spec/unit/utils/copy-style.spec.js diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index ac2f4613b..158391d31 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -9,6 +9,7 @@ const Image = require('./image'); const Table = require('./table'); const DataValidations = require('./data-validations'); const Encryptor = require('../utils/encryptor'); +const {copyStyle} = require('../utils/copy-style'); // Worksheet requirements // Operate as sheet inside workbook or standalone @@ -406,10 +407,10 @@ class Worksheet { _copyStyle(src, dest, styleEmpty = false) { const rSrc = this.getRow(src); const rDst = this.getRow(dest); - rDst.style = Object.freeze({...rSrc.style}); + rDst.style = copyStyle(rSrc.style); // eslint-disable-next-line no-loop-func rSrc.eachCell({includeEmpty: styleEmpty}, (cell, colNumber) => { - rDst.getCell(colNumber).style = Object.freeze({...cell.style}); + rDst.getCell(colNumber).style = copyStyle(cell.style); }); rDst.height = rSrc.height; } diff --git a/lib/utils/copy-style.js b/lib/utils/copy-style.js new file mode 100644 index 000000000..532bf5701 --- /dev/null +++ b/lib/utils/copy-style.js @@ -0,0 +1,43 @@ +const oneDepthCopy = (obj, nestKeys) => ({ + ...obj, + ...nestKeys.reduce((memo, key) => { + if (obj[key]) memo[key] = {...obj[key]}; + return memo; + }, {}), +}); + +const setIfExists = (src, dst, key, nestKeys = []) => { + if (src[key]) dst[key] = oneDepthCopy(src[key], nestKeys); +}; + +const isEmptyObj = obj => Object.keys(obj).length === 0; + +const copyStyle = style => { + if (!style) return style; + if (isEmptyObj(style)) return {}; + + const copied = {...style}; + + setIfExists(style, copied, 'font', ['color']); + setIfExists(style, copied, 'alignment'); + setIfExists(style, copied, 'protection'); + if (style.border) { + setIfExists(style, copied, 'border'); + setIfExists(style.border, copied.border, 'top', ['color']); + setIfExists(style.border, copied.border, 'left', ['color']); + setIfExists(style.border, copied.border, 'bottom', ['color']); + setIfExists(style.border, copied.border, 'right', ['color']); + setIfExists(style.border, copied.border, 'diagonal', ['color']); + } + + if (style.fill) { + setIfExists(style, copied, 'fill', ['fgColor', 'bgColor', 'center']); + if (style.fill.stops) { + copied.fill.stops = style.fill.stops.map(s => oneDepthCopy(s, ['color'])); + } + } + + return copied; +}; + +exports.copyStyle = copyStyle; diff --git a/spec/integration/worksheet.spec.js b/spec/integration/worksheet.spec.js index 705746bed..81868677c 100644 --- a/spec/integration/worksheet.spec.js +++ b/spec/integration/worksheet.spec.js @@ -598,6 +598,38 @@ describe('Worksheet', () => { } }); + it('should style of the inserted row with inherited style be mutable', () => { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet('blort'); + + const dateValue1 = new Date(1970, 1, 1); + const dateValue2 = new Date(1965, 1, 7); + + ws.addRow([1, 'John Doe', dateValue1]); + ws.getRow(1).font = testutils.styles.fonts.comicSansUdB16; + + ws.insertRow(2, [3, 'Jane Doe', dateValue2], 'i'); + ws.insertRow(2, [2, 'Jane Doe', dateValue2], 'o'); + + ws.getRow(2).font = testutils.styles.fonts.broadwayRedOutline20; + ws.getRow(3).font = testutils.styles.fonts.broadwayRedOutline20; + ws.getCell('A2').font = testutils.styles.fonts.arialBlackUI14; + ws.getCell('A3').font = testutils.styles.fonts.arialBlackUI14; + + expect(ws.getRow(2).font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + expect(ws.getRow(3).font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + expect(ws.getCell('A2').font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + expect(ws.getCell('A3').font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + }); + it('iterates over rows', () => { const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet('blort'); diff --git a/spec/unit/utils/copy-style.spec.js b/spec/unit/utils/copy-style.spec.js new file mode 100644 index 000000000..d54d74860 --- /dev/null +++ b/spec/unit/utils/copy-style.spec.js @@ -0,0 +1,39 @@ +const testUtils = require('../../utils/index'); + +const {copyStyle} = verquire('utils/copy-style'); + +const style1 = { + numFmt: testUtils.styles.numFmts.numFmt1, + font: testUtils.styles.fonts.broadwayRedOutline20, + alignment: testUtils.styles.namedAlignments.topLeft, + border: testUtils.styles.borders.thickRainbow, + fill: testUtils.styles.fills.redGreenDarkTrellis, +}; +const style2 = { + fill: testUtils.styles.fills.rgbPathGrad, +}; + +describe('copyStyle', () => { + it('should copy a style deeply', () => { + const copied = copyStyle(style1); + expect(copied).to.deep.equal(style1); + expect(copied.font).to.not.equal(style1.font); + expect(copied.alignment).to.not.equal(style1.alignment); + expect(copied.border).to.not.equal(style1.border); + expect(copied.fill).to.not.equal(style1.fill); + + expect(copyStyle({})).to.deep.equal({}); + }); + + it('should copy fill.stops deeply', () => { + const copied = copyStyle(style2); + expect(copied.fill.stops).to.deep.equal(style2.fill.stops); + expect(copied.fill.stops).to.not.equal(style2.fill.stops); + expect(copied.fill.stops[0]).to.not.equal(style2.fill.stops[0]); + }); + + it('should return the argument if a falsy value passed', () => { + expect(copyStyle(null)).to.equal(null); + expect(copyStyle(undefined)).to.equal(undefined); + }); +}); From 9bf832cb3c777ec26b591d43b113ae208822fb3a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 14 Oct 2021 22:04:12 +0200 Subject: [PATCH 06/46] Upgrade actions/cache and actions/setup-node (#1846) * https://github.com/actions/cache/releases * https://github.com/actions/setup-node/releases --- .github/workflows/exceljs.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/exceljs.yml b/.github/workflows/exceljs.yml index 876b57eae..0f263370b 100644 --- a/.github/workflows/exceljs.yml +++ b/.github/workflows/exceljs.yml @@ -26,13 +26,13 @@ jobs: if: runner.os == 'Windows' - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ./npm-cache key: v1-${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('**/package.json') }} @@ -49,13 +49,13 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v2 with: node-version: 12.x - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} @@ -72,13 +72,13 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v2 with: node-version: 12.x - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} From 695023fe6510e1f17d42ff7460fbe5b705e16560 Mon Sep 17 00:00:00 2001 From: bno1 Date: Sun, 17 Oct 2021 09:44:08 +0300 Subject: [PATCH 07/46] Check object keys in isEqual (#1831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Check object keys in isEqual * Add check for null in isEqual * Add isEqual tests Co-authored-by: Alexandru Șorodoc --- lib/utils/under-dash.js | 19 +++++++ spec/unit/utils/under-dash.spec.js | 79 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 spec/unit/utils/under-dash.spec.js diff --git a/lib/utils/under-dash.js b/lib/utils/under-dash.js index 65c931d22..b6f8a1312 100644 --- a/lib/utils/under-dash.js +++ b/lib/utils/under-dash.js @@ -55,6 +55,7 @@ const _ = { const bType = typeof b; const aArray = Array.isArray(a); const bArray = Array.isArray(b); + let keys; if (aType !== bType) { return false; @@ -73,6 +74,24 @@ const _ = { } return false; } + + if (a === null || b === null) { + return a === b; + } + + // Compare object keys and values + keys = Object.keys(a); + + if (Object.keys(b).length !== keys.length) { + return false; + } + + for (const key of keys) { + if (!b.hasOwnProperty(key)) { + return false; + } + } + return _.every(a, (aValue, key) => { const bValue = b[key]; return _.isEqual(aValue, bValue); diff --git a/spec/unit/utils/under-dash.spec.js b/spec/unit/utils/under-dash.spec.js new file mode 100644 index 000000000..32f47bf12 --- /dev/null +++ b/spec/unit/utils/under-dash.spec.js @@ -0,0 +1,79 @@ +const _ = verquire('utils/under-dash'); +const util = require('util'); + +describe('under-dash', () => { + describe('isEqual', () => { + const values = [ + 0, + 1, + true, + false, + 'string', + 'foobar', + 'other string', + [], + ['array'], + ['array', 'foobar'], + ['array2'], + ['array2', 'foobar'], + {}, + {object: 1}, + {object: 2}, + {object: 1, foobar: 'quux'}, + {object: 2, foobar: 'quux'}, + null, + undefined, + () => {}, + () => {}, + Symbol('foo'), + Symbol('foo'), + Symbol('bar'), + ]; + + function showVal(o) { + return util.inspect(o, {compact: true}); + } + + it('works on simple values', () => { + for (let i = 0; i < values.length; i++) { + for (let j = 0; j < values.length; j++) { + const a = values[i]; + const b = values[j]; + + const assertion = `${showVal(a)} ${i === j ? '==' : '!='} ${showVal( + b + )}`; + expect(_.isEqual(a, b)).to.equal(i === j, `expected ${assertion}`); + } + } + }); + + it('works on complex arrays', () => { + for (let i = 0; i < values.length; i++) { + for (let j = 0; j < values.length; j++) { + const a = [values[i]]; + const b = [values[j]]; + + const assertion = `${showVal(a)} ${i === j ? '==' : '!='} ${showVal( + b + )}`; + expect(_.isEqual(a, b)).to.equal(i === j, `expected ${assertion}`); + } + } + }); + + it('works on complex objects', () => { + for (let i = 0; i < values.length; i++) { + for (let j = 0; j < values.length; j++) { + const a = {key: values[i]}; + const b = {key: values[j]}; + + const assertion = `${showVal(a)} ${i === j ? '==' : '!='} ${showVal( + b + )}`; + expect(_.isEqual(a, b)).to.equal(i === j, `expected ${assertion}`); + } + } + }); + }); +}); From 1d5e5ac02d836f02f2d8b6282b3e1e327f0403b6 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Mon, 1 Nov 2021 12:08:53 +0100 Subject: [PATCH 08/46] Add v17 to testing workflow (#1856) --- .github/workflows/exceljs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/exceljs.yml b/.github/workflows/exceljs.yml index 0f263370b..338ca06a0 100644 --- a/.github/workflows/exceljs.yml +++ b/.github/workflows/exceljs.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: # https://github.com/actions/setup-node/issues/27 - node-version: [8.17.0, 10.x, 12.x, 14.x, 16.x] + node-version: [8.17.0, 10.x, 12.x, 14.x, 16.x, 17.x] os: [ubuntu-latest, macOS-latest, windows-latest] runs-on: ${{ matrix.os }} From 0f6d7657e71f1de3c1fc2a2a75776b7766f0576a Mon Sep 17 00:00:00 2001 From: Valerio Sevilla Date: Mon, 8 Nov 2021 00:22:06 +0100 Subject: [PATCH 09/46] Upgrade jszip to its latest version to date. This version does not have any vulnerability found by Snyk so far (#1895) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 807ad66b8..ce21d2630 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", - "jszip": "^3.5.0", + "jszip": "^3.7.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", From dc45ddfdae944277db19deb92f19b4d540b92b25 Mon Sep 17 00:00:00 2001 From: Xavi <3817677+xjrcode@users.noreply.github.com> Date: Thu, 18 Nov 2021 03:29:32 +0100 Subject: [PATCH 10/46] Update README.md (#1677) * Update README.md Added note about internationalized formula semantic * Update README.md Co-authored-by: Xavi <3817677+xavi-dev@users.noreply.github.com> --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5b8d3a250..d95f99393 100644 --- a/README.md +++ b/README.md @@ -2631,10 +2631,13 @@ An Excel formula for calculating values on the fly. Note that ExcelJS cannot process the formula to generate a result, it must be supplied. +Note that function semantic names must be in English and the separator must be a comma. + E.g. ```javascript worksheet.getCell('A3').value = { formula: 'A1+A2', result: 7 }; +worksheet.getCell('A3').value = { formula: 'SUM(A1,A2)', result: 7 }; ``` Cells also support convenience getters to access the formula and result: From d786c4db66f0e06587a72d1fac0bead394542957 Mon Sep 17 00:00:00 2001 From: Joel Denning Date: Wed, 17 Nov 2021 19:31:33 -0700 Subject: [PATCH 11/46] Set prototype of RegExp correctly. (#1700) Co-authored-by: Siemienik Pawel --- README.md | 2 +- README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d95f99393..59232e2bc 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ try { } return new RegExp(pattern, flags); }; - global.RegExp.prototype = RegExp; + global.RegExp.prototype = RegExp.prototype; } ``` diff --git a/README_zh.md b/README_zh.md index dc05bf49a..491c0c2f5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -196,7 +196,7 @@ try { } return new RegExp(pattern, flags); }; - global.RegExp.prototype = RegExp; + global.RegExp.prototype = RegExp.prototype; } ``` From 4d71b664b7f9faefcf3cc4e1e8a33240f6c712c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Bj=C3=B8rlig?= Date: Sun, 21 Nov 2021 11:24:49 +0100 Subject: [PATCH 12/46] added timeouts (#1733) --- .github/workflows/asset-size.yml | 1 + .github/workflows/exceljs.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/asset-size.yml b/.github/workflows/asset-size.yml index 2ac84661d..f9c813146 100644 --- a/.github/workflows/asset-size.yml +++ b/.github/workflows/asset-size.yml @@ -4,6 +4,7 @@ on: [pull_request] jobs: compare: + timeout-minutes: 15 runs-on: ubuntu-latest steps: diff --git a/.github/workflows/exceljs.yml b/.github/workflows/exceljs.yml index 338ca06a0..2cddfe3a8 100644 --- a/.github/workflows/exceljs.yml +++ b/.github/workflows/exceljs.yml @@ -8,6 +8,7 @@ on: jobs: test: + timeout-minutes: 10 name: Node v${{ matrix.node-version }} on ${{ matrix.os }} strategy: fail-fast: false @@ -44,6 +45,7 @@ jobs: CI: true benchmark: + timeout-minutes: 15 name: Measure performance impact of changes runs-on: ubuntu-latest @@ -67,6 +69,7 @@ jobs: CI: true typescript: + timeout-minutes: 15 name: Ensure typescript compatibility runs-on: ubuntu-latest From 860b862d122c2645f8b34f0f885a64b104f7a538 Mon Sep 17 00:00:00 2001 From: skypesky <2678630761@qq.com> Date: Sun, 21 Nov 2021 18:38:18 +0800 Subject: [PATCH 13/46] fix issue 1676 (#1701) * fix issue 1676 Broken internal link on Google sheets * Update hyperlink-xform.js * fix(issue 1676): add unit test --- lib/xlsx/xform/sheet/hyperlink-xform.js | 29 +++++++++++++++---- .../xlsx/xform/sheet/hyperlink-xform.spec.js | 12 ++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/xlsx/xform/sheet/hyperlink-xform.js b/lib/xlsx/xform/sheet/hyperlink-xform.js index 3fb560a6d..88f4ee2ec 100644 --- a/lib/xlsx/xform/sheet/hyperlink-xform.js +++ b/lib/xlsx/xform/sheet/hyperlink-xform.js @@ -6,11 +6,20 @@ class HyperlinkXform extends BaseXform { } render(xmlStream, model) { - xmlStream.leafNode('hyperlink', { - ref: model.address, - 'r:id': model.rId, - tooltip: model.tooltip, - }); + if (this.isInternalLink(model)) { + xmlStream.leafNode('hyperlink', { + ref: model.address, + 'r:id': model.rId, + tooltip: model.tooltip, + location: model.target, + }); + } else { + xmlStream.leafNode('hyperlink', { + ref: model.address, + 'r:id': model.rId, + tooltip: model.tooltip, + }); + } } parseOpen(node) { @@ -20,6 +29,11 @@ class HyperlinkXform extends BaseXform { rId: node.attributes['r:id'], tooltip: node.attributes.tooltip, }; + + // This is an internal link + if (node.attributes.location) { + this.model.target = node.attributes.location; + } return true; } return false; @@ -30,6 +44,11 @@ class HyperlinkXform extends BaseXform { parseClose() { return false; } + + isInternalLink(model) { + // @example: Sheet2!D3, return true + return model.target && /^[^!]+![a-zA-Z]+[\d]+$/.test(model.target); + } } module.exports = HyperlinkXform; diff --git a/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js b/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js index 4292dfe9d..9e25bbc61 100644 --- a/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js +++ b/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js @@ -15,6 +15,18 @@ const expectations = [ xml: '', tests: ['render', 'renderIn', 'parse'], }, + { + title: 'Internal Link', + create() { + return new HyperlinkXform(); + }, + preparedModel: {address: 'B6', rId: 'rId1', target: 'sheet1!B2'}, + get parsedModel() { + return this.preparedModel; + }, + xml: '', + tests: ['render', 'renderIn', 'parse'], + }, ]; describe('HyperlinkXform', () => { From c228180cbea17fe7b7a6e0cabe58fcc94604f368 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Thu, 13 Apr 2023 15:34:23 +0200 Subject: [PATCH 14/46] ExcelJS/ExcelJS#2237 : Update CI Tests, Drop support for Node v8 (#2242) FEAT https://github.com/exceljs/exceljs/issues/2237 In this PR: * Added node versions 18 and 19 to the tests suite * Drop node v8 tests --- .github/workflows/{exceljs.yml => tests.yml} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename .github/workflows/{exceljs.yml => tests.yml} (95%) diff --git a/.github/workflows/exceljs.yml b/.github/workflows/tests.yml similarity index 95% rename from .github/workflows/exceljs.yml rename to .github/workflows/tests.yml index 2cddfe3a8..29a713612 100644 --- a/.github/workflows/exceljs.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: ExcelJS +name: Tests on: push: @@ -13,8 +13,7 @@ jobs: strategy: fail-fast: false matrix: - # https://github.com/actions/setup-node/issues/27 - node-version: [8.17.0, 10.x, 12.x, 14.x, 16.x, 17.x] + node-version: [10.x, 12.x, 14.x, 16.x, 17.x, 18.x, 19.x] os: [ubuntu-latest, macOS-latest, windows-latest] runs-on: ${{ matrix.os }} From a081694d283a9b78d78b6dba3115a0e86b7e4d51 Mon Sep 17 00:00:00 2001 From: Henry Chan <112690257+hfhchan-plb@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:53:54 +0800 Subject: [PATCH 15/46] Fix types for getWorksheet() (#2223) ## Summary Fix types for getWorksheet(). It can return undefined ## Test plan N/A ## Related to source code (for typings update) [lib/doc/workbook.js#L143](https://github.com/exceljs/exceljs/blob/master/lib/doc/workbook.js#L143) --------- Co-authored-by: Siemienik Pawel --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index c5174c0d2..5b0fb90de 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1743,7 +1743,7 @@ export class Workbook { /** * fetch sheet by name or id */ - getWorksheet(indexOrName: number | string): Worksheet; + getWorksheet(indexOrName?: number | string): Worksheet | undefined; /** * Iterate over all sheets. From 202d2e41b6b4a3ef8a13d9d6cb20fa8e0e973652 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Thu, 13 Apr 2023 20:21:43 +0200 Subject: [PATCH 16/46] Remove legacy badges --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 59232e2bc..9ad3b85d4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # ExcelJS [![Build status](https://github.com/exceljs/exceljs/workflows/ExcelJS/badge.svg)](https://github.com/exceljs/exceljs/actions?query=workflow%3AExcelJS) -[![Code Quality: Javascript](https://img.shields.io/lgtm/grade/javascript/g/exceljs/exceljs.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/exceljs/exceljs/context:javascript) -[![Total Alerts](https://img.shields.io/lgtm/alerts/g/exceljs/exceljs.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/exceljs/exceljs/alerts) Read, manipulate and write spreadsheet data and styles to XLSX and JSON. From 478e2a0e89887e6ffe14b42a6e8e5ecaf9ff5444 Mon Sep 17 00:00:00 2001 From: Takumi Kaji Date: Fri, 14 Apr 2023 22:24:04 +0900 Subject: [PATCH 17/46] Add characters cannot be used for worksheet name (#2126) Co-authored-by: Siemienik Pawel --- lib/doc/workbook.js | 4 ++-- spec/integration/worksheet.spec.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/doc/workbook.js b/lib/doc/workbook.js index a18383317..c05dd7836 100644 --- a/lib/doc/workbook.js +++ b/lib/doc/workbook.js @@ -60,9 +60,9 @@ class Workbook { // Illegal character in worksheet name: asterisk (*), question mark (?), // colon (:), forward slash (/ \), or bracket ([]) - if (/[*?:/\\[\]]/.test(name)) { + if (/[*?:/\\[\]*?:¥/\[]]/.test(name)) { throw new Error( - `Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]` + `Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ] * ? : ¥ / \ [ ]` ); } diff --git a/spec/integration/worksheet.spec.js b/spec/integration/worksheet.spec.js index 81868677c..e0210c8cc 100644 --- a/spec/integration/worksheet.spec.js +++ b/spec/integration/worksheet.spec.js @@ -702,11 +702,11 @@ describe('Worksheet', () => { it('throws an error', () => { const workbook = new ExcelJS.Workbook(); - const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']']; + const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']', '*', '?', ':', '¥', '/', '\', '[', ']']; for (const invalidCharacter of invalidCharacters) { expect(() => workbook.addWorksheet(invalidCharacter)).to.throw( - `Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ]` + `Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ] * ? : ¥ / \ [ ]` ); } }); From 4acab1d40a762dec7da3f198747ee23f0555062e Mon Sep 17 00:00:00 2001 From: Paulius Grabauskas Date: Fri, 14 Apr 2023 16:42:23 +0300 Subject: [PATCH 18/46] Fix issue #1753 Reject promise when workbook reader is writing to temporary file stream and error occurs (#1756) Co-authored-by: Paulius Grabauskas Co-authored-by: Siemienik Pawel --- lib/stream/xlsx/workbook-reader.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/stream/xlsx/workbook-reader.js b/lib/stream/xlsx/workbook-reader.js index 0a2a3c65c..0d3afd62a 100644 --- a/lib/stream/xlsx/workbook-reader.js +++ b/lib/stream/xlsx/workbook-reader.js @@ -119,6 +119,7 @@ class WorkbookReader extends EventEmitter { waitingWorkSheets.push({sheetNo, path, tempFileCleanupCallback}); const tempStream = fs.createWriteStream(path); + tempStream.on('error', reject); entry.pipe(tempStream); return tempStream.on('finish', () => { return resolve(); @@ -298,11 +299,8 @@ class WorkbookReader extends EventEmitter { options: this.options, }); - const matchingRel = (this.workbookRels || []).find( - rel => rel.Target === `worksheets/sheet${sheetNo}.xml` - ); - const matchingSheet = - matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id); + const matchingRel = (this.workbookRels || []).find(rel => rel.Target === `worksheets/sheet${sheetNo}.xml`); + const matchingSheet = matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id); if (matchingSheet) { worksheetReader.id = matchingSheet.id; worksheetReader.name = matchingSheet.name; From fd727053997fb8d5782ee6a5331d933e308d99be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wulf=20S=C3=B6lter?= Date: Sat, 15 Apr 2023 01:49:58 +1200 Subject: [PATCH 19/46] README.md to have correct link for Streaming XLSX (#2186) Co-authored-by: Siemienik Pawel --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ad3b85d4..9886f8bd7 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ To be clear, all contributions added to this library will be included in the lib
  • Streaming I/O
  • From 81c5e09d643105fc847a935f4e4193dfab39454a Mon Sep 17 00:00:00 2001 From: DemoJx <37802689+DemoJj@users.noreply.github.com> Date: Fri, 14 Apr 2023 22:12:58 +0800 Subject: [PATCH 20/46] Added a polyfill of promise.finally to support lower version firefox. (#1982) Co-authored-by: demojx Co-authored-by: Siemienik Pawel --- lib/exceljs.browser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/exceljs.browser.js b/lib/exceljs.browser.js index 80f76c082..d7558203a 100644 --- a/lib/exceljs.browser.js +++ b/lib/exceljs.browser.js @@ -1,5 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies,node/no-unpublished-require */ require('core-js/modules/es.promise'); +require('core-js/modules/es.promise.finally'); require('core-js/modules/es.object.assign'); require('core-js/modules/es.object.keys'); require('core-js/modules/es.object.values'); From 5f306b6ed1ddb363fccbc8d3c398000342074ca5 Mon Sep 17 00:00:00 2001 From: ZyqGitHub1 Date: Fri, 14 Apr 2023 22:21:10 +0800 Subject: [PATCH 21/46] Fix: read this.worksheet before assign it (#1934) Co-authored-by: Siemienik Pawel --- lib/doc/anchor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/doc/anchor.js b/lib/doc/anchor.js index 9aa5f70e2..dd3509a74 100644 --- a/lib/doc/anchor.js +++ b/lib/doc/anchor.js @@ -4,6 +4,8 @@ const colCache = require('../utils/col-cache'); class Anchor { constructor(worksheet, address, offset = 0) { + this.worksheet = worksheet; + if (!address) { this.nativeCol = 0; this.nativeColOff = 0; @@ -29,8 +31,6 @@ class Anchor { this.nativeRow = 0; this.nativeRowOff = 0; } - - this.worksheet = worksheet; } static asInstance(model) { From 5b4fa3b0664ebb267779dbd212ad5f3807bf758d Mon Sep 17 00:00:00 2001 From: jarrod-cocoon <122930640+jarrod-cocoon@users.noreply.github.com> Date: Tue, 18 Apr 2023 05:44:59 -0700 Subject: [PATCH 22/46] chore: upgrade jszip to ^3.10.1 (#2211) No issues found with our usage in https://stuk.github.io/jszip/CHANGES.html, most notable change was 3.9.0 API change, but we are unaffected, due to not using constructor arguments. This patches the zipslip vulnerability in <= 3.7.1 and patched in >= 3.8.0. Co-authored-by: Siemienik Pawel --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce21d2630..d48e94057 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", - "jszip": "^3.7.1", + "jszip": "^3.10.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", From 4460df81307b925d1a3aa32337c06e06cd737604 Mon Sep 17 00:00:00 2001 From: Hugo <51097011+HugoP27@users.noreply.github.com> Date: Tue, 18 Apr 2023 15:24:33 +0200 Subject: [PATCH 23/46] fixed spelling error in README.md file (#2208) becouse changed to because --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9886f8bd7..00c27c491 100644 --- a/README.md +++ b/README.md @@ -371,7 +371,7 @@ workbook.worksheets[0]; //the first one; ``` It's important to know that `workbook.getWorksheet(1) != Workbook.worksheets[0]` and `workbook.getWorksheet(1) != Workbook.worksheets[1]`, -becouse `workbook.worksheets[0].id` may have any value. +because `workbook.worksheets[0].id` may have any value. ## Worksheet State[⬆](#contents) From 77e549a8176d62cd0ada398fc8436ffb5589db5d Mon Sep 17 00:00:00 2001 From: zurmokeeper <3382272560@qq.com> Date: Tue, 25 Apr 2023 02:43:32 +0800 Subject: [PATCH 24/46] fix: Fix xlsx.writeFile() not catching error when error occurs (#2244) * fix: Fix xlsx.writeFile() not catching error when error occurs * temp submit * Additional test cases and formatted writeFile code --------- Co-authored-by: linxl <1658370535@qq.com> Co-authored-by: Siemienik Pawel --- lib/xlsx/xlsx.js | 2 ++ spec/integration/pr/test-pr-2244.spec.js | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 spec/integration/pr/test-pr-2244.spec.js diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js index 4f6bc02c5..268d1ddb3 100644 --- a/lib/xlsx/xlsx.js +++ b/lib/xlsx/xlsx.js @@ -675,6 +675,8 @@ class XLSX { this.write(stream, options).then(() => { stream.end(); + }).catch(err=>{ + reject(err); }); }); } diff --git a/spec/integration/pr/test-pr-2244.spec.js b/spec/integration/pr/test-pr-2244.spec.js new file mode 100644 index 000000000..667446c4c --- /dev/null +++ b/spec/integration/pr/test-pr-2244.spec.js @@ -0,0 +1,23 @@ +const ExcelJS = verquire('exceljs'); + +describe('pull request 2244', () => { + it('pull request 2244- Fix xlsx.writeFile() not catching error when error occurs', async () => { + async function test() { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('sheet'); + const imageId1 = workbook.addImage({ + filename: 'path/to/image.jpg', // Non-existent file + extension: 'jpeg', + }); + worksheet.addImage(imageId1, 'B2:D6'); + await workbook.xlsx.writeFile('test.xlsx'); + } + let error; + try { + await test(); + } catch (err) { + error = err; + } + expect(error).to.be.an('error'); + }); +}); From 887983a92affab5e9967ed68d70e5670c08c147d Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Fri, 5 May 2023 14:05:58 +0200 Subject: [PATCH 25/46] Improve worksheets' naming validation logic. (#2257) ## Summary This PR aims to resolve #2243 issue. Additionally, I transferred the responsibility for name validation from `Workbook` to `Worksheet`, which allowed me to tighten the rules for changing the name using: `ws.name` ## Test plan I completed the missing tests and covers all new assertions. --- lib/doc/workbook.js | 30 +----------- lib/doc/worksheet.js | 51 +++++++++++++++++-- spec/integration/worksheet.spec.js | 78 +++++++++++++++++++++++++----- 3 files changed, 114 insertions(+), 45 deletions(-) diff --git a/lib/doc/workbook.js b/lib/doc/workbook.js index c05dd7836..8e7f46ecd 100644 --- a/lib/doc/workbook.js +++ b/lib/doc/workbook.js @@ -53,30 +53,6 @@ class Workbook { addWorksheet(name, options) { const id = this.nextId; - if (name && name.length > 31) { - // eslint-disable-next-line no-console - console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`); - } - - // Illegal character in worksheet name: asterisk (*), question mark (?), - // colon (:), forward slash (/ \), or bracket ([]) - if (/[*?:/\\[\]*?:¥/\[]]/.test(name)) { - throw new Error( - `Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ] * ? : ¥ / \ [ ]` - ); - } - - if (/(^')|('$)/.test(name)) { - throw new Error( - `The first or last character of worksheet name cannot be a single quotation mark: ${name}` - ); - } - - name = (name || `sheet${id}`).substring(0, 31); - if (this._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) { - throw new Error(`Worksheet name already exists: ${name}`); - } - // if options is a color, call it tabColor (and signal deprecated message) if (options) { if (typeof options === 'string') { @@ -102,10 +78,7 @@ class Workbook { } } - const lastOrderNo = this._worksheets.reduce( - (acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), - 0 - ); + const lastOrderNo = this._worksheets.reduce((acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), 0); const worksheetOptions = Object.assign({}, options, { id, name, @@ -235,7 +208,6 @@ class Workbook { state, workbook: this, })); - worksheet.model = worksheetModel; }); diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 158391d31..80fa123e7 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -20,13 +20,14 @@ const {copyStyle} = require('../utils/copy-style'); class Worksheet { constructor(options) { options = options || {}; + this._workbook = options.workbook; // in a workbook, each sheet will have a number this.id = options.id; this.orderNo = options.orderNo; // and a name - this.name = options.name || `Sheet${this.id}`; + this.name = options.name; // add a state this.state = options.state || 'visible'; @@ -47,8 +48,6 @@ class Worksheet { // record of all row and column pageBreaks this.rowBreaks = []; - this._workbook = options.workbook; - // for tabColor, default row height, outline levels, etc this.properties = Object.assign( {}, @@ -128,6 +127,52 @@ class Worksheet { this.conditionalFormattings = []; } + get name() { + return this._name; + } + + set name(name) { + if (name === undefined) { + name = `sheet${this.id}`; + } + + if (this._name === name) return; + + if (typeof name !== 'string') { + throw new Error('The name has to be a string.'); + } + + if (name === '') { + throw new Error('The name can\'t be empty.'); + } + + if (name === 'History') { + throw new Error('The name "History" is protected. Please use a different name.'); + } + + // Illegal character in worksheet name: asterisk (*), question mark (?), + // colon (:), forward slash (/ \), or bracket ([]) + if (/[*?:/\\[\]]/.test(name)) { + throw new Error(`Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]`); + } + + if (/(^')|('$)/.test(name)) { + throw new Error(`The first or last character of worksheet name cannot be a single quotation mark: ${name}`); + } + + if (name && name.length > 31) { + // eslint-disable-next-line no-console + console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`); + name = name.substring(0, 31); + } + + if (this._workbook._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) { + throw new Error(`Worksheet name already exists: ${name}`); + } + + this._name = name; + } + get workbook() { return this._workbook; } diff --git a/spec/integration/worksheet.spec.js b/spec/integration/worksheet.spec.js index e0210c8cc..a70dfcef8 100644 --- a/spec/integration/worksheet.spec.js +++ b/spec/integration/worksheet.spec.js @@ -677,22 +677,56 @@ describe('Worksheet', () => { expect(ws.getRows(1, 0)).to.equal(undefined); }); - context('when worksheet name is less than or equal 31', () => { it('save the original name', () => { const wb = new ExcelJS.Workbook(); - let ws = wb.addWorksheet('ThisIsAWorksheetName'); + let ws = wb.addWorksheet(); + ws.name = 'ThisIsAWorksheetName'; expect(ws.name).to.equal('ThisIsAWorksheetName'); - ws = wb.addWorksheet('ThisIsAWorksheetNameWith31Chars'); + ws = wb.addWorksheet(); + ws.name = 'ThisIsAWorksheetNameWith31Chars'; expect(ws.name).to.equal('ThisIsAWorksheetNameWith31Chars'); }); }); + context('name is be not empty string', () => { + it('when empty should thrown an error', () => { + const wb = new ExcelJS.Workbook(); + + expect(() => { + const ws = wb.addWorksheet(); + ws.name = ''; + }).to.throw('The name can\'t be empty.'); + }); + it('when isn\'t string should thrown an error', () => { + const wb = new ExcelJS.Workbook(); + + expect(() => { + const ws = wb.addWorksheet(); + ws.name = 0; + }).to.throw('The name has to be a string.'); + }); + }); + + context('when worksheet name is `History`', () => { + it('thrown an error', () => { + const wb = new ExcelJS.Workbook(); + + expect(() => { + const ws = wb.addWorksheet(); + ws.name = 'History'; + }).to.throw( + 'The name "History" is protected. Please use a different name.' + ); + }); + }); + context('when worksheet name is longer than 31', () => { it('keep first 31 characters', () => { const wb = new ExcelJS.Workbook(); - const ws = wb.addWorksheet('ThisIsAWorksheetNameThatIsLongerThan31'); + const ws = wb.addWorksheet(); + ws.name = 'ThisIsAWorksheetNameThatIsLongerThan31'; expect(ws.name).to.equal('ThisIsAWorksheetNameThatIsLonge'); }); @@ -702,11 +736,14 @@ describe('Worksheet', () => { it('throws an error', () => { const workbook = new ExcelJS.Workbook(); - const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']', '*', '?', ':', '¥', '/', '\', '[', ']']; + const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']']; for (const invalidCharacter of invalidCharacters) { - expect(() => workbook.addWorksheet(invalidCharacter)).to.throw( - `Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ] * ? : ¥ / \ [ ]` + expect(() => { + const ws = workbook.addWorksheet(); + ws.name = invalidCharacter; + }).to.throw( + `Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ]` ); } }); @@ -717,7 +754,10 @@ describe('Worksheet', () => { const invalidNames = ['\'sheetName', 'sheetName\'']; for (const invalidName of invalidNames) { - expect(() => workbook.addWorksheet(invalidName)).to.throw( + expect(() => { + const ws = workbook.addWorksheet(); + ws.name = invalidName; + }).to.throw( `The first or last character of worksheet name cannot be a single quotation mark: ${invalidName}` ); } @@ -732,9 +772,13 @@ describe('Worksheet', () => { const invalideName = 'THISISAWORKSHEETNAMEINUPPERCASE'; const expectedError = `Worksheet name already exists: ${invalideName}`; - wb.addWorksheet(validName); + const ws = wb.addWorksheet(); + ws.name = validName; - expect(() => wb.addWorksheet(invalideName)).to.throw(expectedError); + expect(() => { + const newWs = wb.addWorksheet(); + newWs.name = invalideName; + }).to.throw(expectedError); }); it('throws an error', () => { @@ -744,10 +788,18 @@ describe('Worksheet', () => { const invalideName = 'ThisIsAWorksheetNameThatIsLongerThan31'; const expectedError = `Worksheet name already exists: ${validName}`; - wb.addWorksheet(validName); + const ws = wb.addWorksheet(); + ws.name = validName; + + expect(() => { + const newWs = wb.addWorksheet(); + newWs.name = validName; + }).to.throw(expectedError); - expect(() => wb.addWorksheet(validName)).to.throw(expectedError); - expect(() => wb.addWorksheet(invalideName)).to.throw(expectedError); + expect(() => { + const newWs = wb.addWorksheet(); + newWs.name = invalideName; + }).to.throw(expectedError); }); }); }); From 0c4b238c51646c48725e91d73a58a87fe56b7cfc Mon Sep 17 00:00:00 2001 From: babu-ch <42715882+babu-ch@users.noreply.github.com> Date: Fri, 5 May 2023 21:58:33 +0900 Subject: [PATCH 26/46] spliceRows remove last row (#2140) fixes #2125 --- lib/doc/worksheet.js | 3 +++ .../issues/issue-2125-spliceRows-last-row.spec.js | 13 +++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 spec/integration/issues/issue-2125-spliceRows-last-row.spec.js diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 80fa123e7..13e414140 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -490,6 +490,9 @@ class Worksheet { let rSrc; if (nExpand < 0) { // remove rows + if (start === nEnd) { + this._rows[nEnd - 1] = undefined; + } for (i = nKeep; i <= nEnd; i++) { rSrc = this._rows[i - 1]; if (rSrc) { diff --git a/spec/integration/issues/issue-2125-spliceRows-last-row.spec.js b/spec/integration/issues/issue-2125-spliceRows-last-row.spec.js new file mode 100644 index 000000000..6b8737de7 --- /dev/null +++ b/spec/integration/issues/issue-2125-spliceRows-last-row.spec.js @@ -0,0 +1,13 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + it('issue 2125 - spliceRows remove last row', () => { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet(); + ws.addRows([['1st'], ['2nd'], ['3rd']]); + + ws.spliceRows(ws.rowCount, 1); + + expect(ws.getRow(ws.rowCount).getCell(1).value).to.equal('2nd'); + }); +}); From ec92cb3b898bdf7f806ff9d7b8370c955ee8ba20 Mon Sep 17 00:00:00 2001 From: zhengcp <45730337+cpaiyueyue@users.noreply.github.com> Date: Fri, 5 May 2023 21:03:33 +0800 Subject: [PATCH 27/46] fix: fix the loss of column attributes due to incorrect column order (#2222) When I try to read the excel file and display it on the page, I found that the hidden columns of some files were not parsed correctly. ## Summary I found that if the order of the column data parsed here is not positive, the subsequent attributes will be overwritten ![image](https://user-images.githubusercontent.com/45730337/222389920-684a7904-9b9f-483e-9d97-3d1c1aa3cc36.png) ![image](https://user-images.githubusercontent.com/45730337/222389824-6f1c06b6-876d-4178-98e6-b8603bfd3da6.png) ## Test plan Before modification ![image](https://user-images.githubusercontent.com/45730337/222390826-8515397d-a498-4bca-91b2-62cac42cc0ff.png) After modification ![image](https://user-images.githubusercontent.com/45730337/222391194-bb1fe799-0b29-4396-9b10-0fa44767410f.png) Co-authored-by: zhengcp --- .gitignore | 3 ++- lib/doc/column.js | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b2e50bdae..8026a7080 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ _SpecRunner.html .DS_Store # Ignore xlsx files generated during testing -*.xlsx \ No newline at end of file +*.xlsx +enpm-lock.yaml \ No newline at end of file diff --git a/lib/doc/column.js b/lib/doc/column.js index f7636172c..c7fc282fe 100644 --- a/lib/doc/column.js +++ b/lib/doc/column.js @@ -297,6 +297,13 @@ class Column { const columns = []; let count = 1; let index = 0; + /** + * sort cols by min + * If it is not sorted, the subsequent column configuration will be overwritten + * */ + cols = cols.sort(function(pre, next) { + return pre.min - next.min; + }); while (index < cols.length) { const col = cols[index++]; while (count < col.min) { From eec76a1d4e01e634d2fd9b3d2030cd36a0f2add4 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Wed, 23 Aug 2023 18:02:55 +0200 Subject: [PATCH 28/46] Update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 00c27c491..0eac0502f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ +# Siemienik's Announcement + +> Hello to all the contributors and supporters of this project, +> +> I'm writing this with a heavy heart to let you know that the my development of this project has come to a halt. Since 2019, this has been primarily a volunteer effort on my part. While the journey has been incredibly rewarding, the lack of resources and other commitments has affected the pace and progress. +> +> However, I still firmly believe in the potential of this project. The possibilities for further development could include: +> +> 1. **Parallel Development:** Working alongside another project that uses `exceljs` and simultaneously contributing to the evolution of `exceljs`. +> 2. **Community Sponsorship:** Exploring platforms like Open Collective or similar avenues where community members can sponsor and support the ongoing development and maintenance. +> +> Please share your thoughs. I'm open to ideas and feedback from all of you. If you have any suggestions or if you're interested in supporting this project further, please do get in touch. I'd be happy to collaborate and find ways to move forward. +> +> For communication regarding this announcement or potential collaboration, please reach out to me at: [exceljs+announcement@siemienik.com](mailto:exceljs+announcement@siemienik.com) +> +> Thank you for your understanding and continued support. Let's keep the dream alive! +> +> Warm regards, +> +> Paweł Siemienik + + # ExcelJS [![Build status](https://github.com/exceljs/exceljs/workflows/ExcelJS/badge.svg)](https://github.com/exceljs/exceljs/actions?query=workflow%3AExcelJS) From 8a84db249e4c60c6131b04dd4ecc40c7a104df60 Mon Sep 17 00:00:00 2001 From: Raouf Albeni Date: Wed, 23 Aug 2023 19:25:07 +0300 Subject: [PATCH 29/46] Fix: Sheet Properties Types (#2327) --- index.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.d.ts b/index.d.ts index 5b0fb90de..fe3bef75b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1401,6 +1401,13 @@ export interface WorksheetProperties { */ outlineLevelRow: number; + /** + * The outline properties which controls how it will summarize rows and columns + */ + outlineProperties: { + summaryBelow: boolean, + summaryRight: boolean, + }; /** * Default row height (default: 15) */ From 06d014cfe85f0c62233f7d86cf24a50cfecc9c0c Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Thu, 24 Aug 2023 20:33:39 +0200 Subject: [PATCH 30/46] revert --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index 0eac0502f..00c27c491 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,3 @@ -# Siemienik's Announcement - -> Hello to all the contributors and supporters of this project, -> -> I'm writing this with a heavy heart to let you know that the my development of this project has come to a halt. Since 2019, this has been primarily a volunteer effort on my part. While the journey has been incredibly rewarding, the lack of resources and other commitments has affected the pace and progress. -> -> However, I still firmly believe in the potential of this project. The possibilities for further development could include: -> -> 1. **Parallel Development:** Working alongside another project that uses `exceljs` and simultaneously contributing to the evolution of `exceljs`. -> 2. **Community Sponsorship:** Exploring platforms like Open Collective or similar avenues where community members can sponsor and support the ongoing development and maintenance. -> -> Please share your thoughs. I'm open to ideas and feedback from all of you. If you have any suggestions or if you're interested in supporting this project further, please do get in touch. I'd be happy to collaborate and find ways to move forward. -> -> For communication regarding this announcement or potential collaboration, please reach out to me at: [exceljs+announcement@siemienik.com](mailto:exceljs+announcement@siemienik.com) -> -> Thank you for your understanding and continued support. Let's keep the dream alive! -> -> Warm regards, -> -> Paweł Siemienik - - # ExcelJS [![Build status](https://github.com/exceljs/exceljs/workflows/ExcelJS/badge.svg)](https://github.com/exceljs/exceljs/actions?query=workflow%3AExcelJS) From 7c1c3384a5e6b96fa1e7418d0ad10970ce879136 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Tue, 19 Sep 2023 22:23:50 +0200 Subject: [PATCH 31/46] Use node 18 LTS for tsc, and benchmark. Add node 20. to test matrix. Hash action's version (#2354) --- .github/workflows/tests.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29a713612..7ae75016b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [10.x, 12.x, 14.x, 16.x, 17.x, 18.x, 19.x] + node-version: [10.x, 12.x, 14.x, 16.x, 17.x, 18.x, 19.x, 20.x] os: [ubuntu-latest, macOS-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -24,15 +24,15 @@ jobs: git config --global core.autocrlf false git config --global core.symlinks true if: runner.os == 'Windows' - - uses: actions/checkout@v2 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade with: node-version: ${{ matrix.node-version }} - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade with: path: ./npm-cache key: v1-${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('**/package.json') }} @@ -49,14 +49,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade + - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade with: - node-version: 12.x + node-version: 18 - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} @@ -73,14 +73,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade + - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade with: - node-version: 12.x + node-version: 18 - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} From 63795a3ee405cf84424257271219c8a111cf2562 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Tue, 19 Sep 2023 22:37:05 +0200 Subject: [PATCH 32/46] Fix build badge --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 00c27c491..f5bfeaa50 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ExcelJS -[![Build status](https://github.com/exceljs/exceljs/workflows/ExcelJS/badge.svg)](https://github.com/exceljs/exceljs/actions?query=workflow%3AExcelJS) +[![Build Status](https://github.com/exceljs/exceljs/actions/workflows/tests.yml/badge.svg?branch=master&event=push)](https://github.com/exceljs/exceljs/actions/workflows/tests.yml) Read, manipulate and write spreadsheet data and styles to XLSX and JSON. @@ -84,6 +84,10 @@ Versions are updated on release and any change will most likely result in merge To be clear, all contributions added to this library will be included in the library's MIT licence. +### Let's chat together: + +[![SiemaTeam](https://discordapp.com/api/guilds/976854442009825321/widget.png?style=banner2)](https://discord.gg/siema) + # Contents
      From 56a28e988311041981d02c5ed435d0372150fa35 Mon Sep 17 00:00:00 2001 From: Niklas Date: Tue, 19 Sep 2023 23:20:41 +0200 Subject: [PATCH 33/46] Add missing tooltip attribute to CellHyperlinkValue index.d.ts (#2350) --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index fe3bef75b..74f96f92e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -344,6 +344,7 @@ export interface CellRichTextValue { export interface CellHyperlinkValue { text: string; hyperlink: string; + tooltip?: string; } export interface CellFormulaValue { From 804775a33f9d88f24d14153d80df4fc318896fb3 Mon Sep 17 00:00:00 2001 From: Henry Chan <112690257+hfhchan-plb@users.noreply.github.com> Date: Wed, 20 Sep 2023 05:21:37 +0800 Subject: [PATCH 34/46] Increase resilience to generating large workbooks (#2320) * Update xml-stream.js Increase resilience to generating large workbooks --- lib/utils/xml-stream.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/utils/xml-stream.js b/lib/utils/xml-stream.js index 7d606bb83..46a59695e 100644 --- a/lib/utils/xml-stream.js +++ b/lib/utils/xml-stream.js @@ -7,24 +7,19 @@ const OPEN_ANGLE = '<'; const CLOSE_ANGLE = '>'; const OPEN_ANGLE_SLASH = ''; -const EQUALS_QUOTE = '="'; -const QUOTE = '"'; -const SPACE = ' '; function pushAttribute(xml, name, value) { - xml.push(SPACE); - xml.push(name); - xml.push(EQUALS_QUOTE); - xml.push(utils.xmlEncode(value.toString())); - xml.push(QUOTE); + xml.push(` ${name}="${utils.xmlEncode(value.toString())}"`); } function pushAttributes(xml, attributes) { if (attributes) { + const tmp = []; _.each(attributes, (value, name) => { if (value !== undefined) { - pushAttribute(xml, name, value); + pushAttribute(tmp, name, value); } }); + xml.push(tmp.join("")); } } From b392ec5040e513dadeb9ff4b2e321e0f56bfe0eb Mon Sep 17 00:00:00 2001 From: Justin Hendrickson <37857239+justintunev7@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:12:12 -0600 Subject: [PATCH 35/46] repair all 'c2fo.io' links ('c2fo.github.io') (#2324) Co-authored-by: Justin Hendrickson --- README.md | 8 ++++---- README_zh.md | 8 ++++---- index.d.ts | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f5bfeaa50..36f142699 100644 --- a/README.md +++ b/README.md @@ -2203,7 +2203,7 @@ Options supported when reading CSV files. | dateFormats | N | Array | Specify the date encoding format of dayjs. | | map | N | Function | Custom Array.prototype.map() callback function for processing data. | | sheetName | N | String | Specify worksheet name. | -| parserOptions | N | Object | [parseOptions options](https://c2fo.io/fast-csv/docs/parsing/options) @fast-csv/format module to write csv data. | +| parserOptions | N | Object | [parseOptions options](https://c2fo.github.io/fast-csv/docs/parsing/options) @fast-csv/format module to write csv data. | ```javascript // read from a file @@ -2246,7 +2246,7 @@ const options = { return parseFloat(value); } }, - // https://c2fo.io/fast-csv/docs/parsing/options + // https://c2fo.github.io/fast-csv/docs/parsing/options parserOptions: { delimiter: '\t', quote: false, @@ -2282,7 +2282,7 @@ Options supported when writing to a CSV file. | map | N | Function | Custom Array.prototype.map() callback function for processing row values. | | sheetName | N | String | Specify worksheet name. | | sheetId | N | Number | Specify worksheet ID. | -| formatterOptions | N | Object | [formatterOptions options](https://c2fo.io/fast-csv/docs/formatting/options/) @fast-csv/format module to write csv data. | +| formatterOptions | N | Object | [formatterOptions options](https://c2fo.github.io/fast-csv/docs/formatting/options/) @fast-csv/format module to write csv data. | ```javascript @@ -2323,7 +2323,7 @@ const options = { return value; } }, - // https://c2fo.io/fast-csv/docs/formatting/options + // https://c2fo.github.io/fast-csv/docs/formatting/options formatterOptions: { delimiter: '\t', quote: false, diff --git a/README_zh.md b/README_zh.md index 491c0c2f5..4b6c25eeb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2089,7 +2089,7 @@ const buffer = await workbook.xlsx.writeBuffer(); | dateFormats | N | Array | 指定 dayjs 的日期编码格式。 | | map | N | Function | 自定义`Array.prototype.map()` 回调函数,用于处理数据。 | | sheetName | N | String | 指定工作表名称。 | -| parserOptions | N | Object | [parseOptions 选项](https://c2fo.io/fast-csv/docs/parsing/options) @fast-csv/format 模块以写入 csv 数据。 | +| parserOptions | N | Object | [parseOptions 选项](https://c2fo.github.io/fast-csv/docs/parsing/options) @fast-csv/format 模块以写入 csv 数据。 | ```javascript // 从文件读取 @@ -2132,7 +2132,7 @@ const options = { return parseFloat(value); } }, - // https://c2fo.io/fast-csv/docs/parsing/options + // https://c2fo.github.io/fast-csv/docs/parsing/options parserOptions: { delimiter: '\t', quote: false, @@ -2165,7 +2165,7 @@ CSV 解析器使用 [fast-csv](https://www.npmjs.com/package/fast-csv) 读取CSV | map | N | Function | 自定义`Array.prototype.map()` 回调函数,用于处理行值。 | | sheetName | N | String | 指定工作表名称。 | | sheetId | N | Number | 指定工作表 ID。 | -| formatterOptions | N | Object | [formatterOptions 选项](https://c2fo.io/fast-csv/docs/formatting/options/) @fast-csv/format 模块写入csv 数据。 | +| formatterOptions | N | Object | [formatterOptions 选项](https://c2fo.github.io/fast-csv/docs/formatting/options/) @fast-csv/format 模块写入csv 数据。 | ```javascript @@ -2205,7 +2205,7 @@ const options = { return value; } }, - // https://c2fo.io/fast-csv/docs/formatting/options + // https://c2fo.github.io/fast-csv/docs/formatting/options formatterOptions: { delimiter: '\t', quote: false, diff --git a/index.d.ts b/index.d.ts index 74f96f92e..033cef5c0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1495,7 +1495,7 @@ export interface Xlsx { write(stream: import('stream').Stream, options?: Partial): Promise; } -// https://c2fo.io/fast-csv/docs/parsing/options +// https://c2fo.github.io/fast-csv/docs/parsing/options type HeaderArray = (string | undefined | null)[]; type HeaderTransformFunction = (headers: HeaderArray) => HeaderArray; @@ -1536,7 +1536,7 @@ interface RowTransformFunction { (row: Rows): Rows; } -// https://c2fo.io/fast-csv/docs/formatting/options/ +// https://c2fo.github.io/fast-csv/docs/formatting/options/ export interface FastCsvFormatterOptionsArgs { objectMode: boolean; delimiter: string; From bd317dba105aba2f3270bbeff5b8227a39919e81 Mon Sep 17 00:00:00 2001 From: Chlorine Date: Wed, 20 Sep 2023 07:11:31 +0800 Subject: [PATCH 36/46] fix: fix type definitions about last column, formula values and protection (#2309) * fix: fix type definitions about last column, formula values and protection * fix: add boolean type to formula value result * add support for dashed `BorderStyle` --- index.d.ts | 15 ++++++++------- lib/xlsx/xform/style/border-xform.js | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 033cef5c0..843673b31 100644 --- a/index.d.ts +++ b/index.d.ts @@ -230,7 +230,7 @@ export interface Font { } export type BorderStyle = - | 'thin' | 'dotted' | 'hair' | 'medium' | 'double' | 'thick' | 'dashDot' + | 'thin' | 'dotted' | 'hair' | 'medium' | 'double' | 'thick' | 'dashed' | 'dashDot' | 'dashDotDot' | 'slantDashDot' | 'mediumDashed' | 'mediumDashDotDot' | 'mediumDashDot'; export interface Color { @@ -289,6 +289,7 @@ export interface Alignment { export interface Protection { locked: boolean; + hidden: boolean; } export interface Style { @@ -349,15 +350,15 @@ export interface CellHyperlinkValue { export interface CellFormulaValue { formula: string; - result?: number | string | Date | { error: CellErrorValue }; - date1904: boolean; + result?: number | string | boolean | Date | CellErrorValue; + date1904?: boolean; } export interface CellSharedFormulaValue { sharedFormula: string; readonly formula?: string; - result?: number | string | Date | { error: CellErrorValue }; - date1904: boolean; + result?: number | string | boolean | Date | CellErrorValue; + date1904?: boolean; } export declare enum ValueType { @@ -1140,7 +1141,7 @@ export interface Worksheet { /** * Get the last column in a worksheet */ - readonly lastColumn: Column; + readonly lastColumn: Column | undefined; /** * A count of the number of columns that have values. @@ -2022,4 +2023,4 @@ export namespace stream { getColumn(c: number): Column; } } -} +} \ No newline at end of file diff --git a/lib/xlsx/xform/style/border-xform.js b/lib/xlsx/xform/style/border-xform.js index 212484bd6..577058864 100644 --- a/lib/xlsx/xform/style/border-xform.js +++ b/lib/xlsx/xform/style/border-xform.js @@ -89,6 +89,7 @@ class EdgeXform extends BaseXform { EdgeXform.validStyleValues = [ 'thin', + 'dashed', 'dotted', 'dashDot', 'hair', From 243d7d71d246b1c253f7d6da28e44cd185488fe7 Mon Sep 17 00:00:00 2001 From: Arthur Ming Date: Wed, 20 Sep 2023 07:32:06 +0800 Subject: [PATCH 37/46] fix: add spinCount field for WorksheetProtection type (#2284) * fix: add spinCount field for WorksheetProtection type * add spaces --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index 843673b31..08f43df05 100644 --- a/index.d.ts +++ b/index.d.ts @@ -882,6 +882,7 @@ export interface WorksheetProtection { sort: boolean; autoFilter: boolean; pivotTables: boolean; + spinCount: number; } export interface Image { extension: 'jpeg' | 'png' | 'gif'; From 4abccf55dccf7f040bb52a3aad74188b9e68984e Mon Sep 17 00:00:00 2001 From: Yuki Tsujimoto <46666464+ytjmt@users.noreply.github.com> Date: Fri, 22 Sep 2023 04:55:20 +0900 Subject: [PATCH 38/46] Add type definition for WorksheetModel.merges (#2281) --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index 08f43df05..03f78a3c8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -992,6 +992,7 @@ export interface WorksheetModel { views: WorksheetView[]; autoFilter: AutoFilter; media: Media[]; + merges: Range['range'][]; } export type WorksheetState = 'visible' | 'hidden' | 'veryHidden'; From 3178efdd9add0f4f2c0aed4bf3a35dbd1d44f92a Mon Sep 17 00:00:00 2001 From: Willie Date: Thu, 21 Sep 2023 23:18:51 +0100 Subject: [PATCH 39/46] 1842: New xlsx option for ignoring certain nodes for improved performance (#2132) * 1842: New xlsx option for ignoring certain nodes for improved performance * Included test file * Added typescript typings * optimizing ifs * new line EOF * Update README.md - available options --------- Co-authored-by: Willem Le Roux Co-authored-by: Siemienik Pawel --- README.md | 16 +++++++++ index.d.ts | 13 +++++-- lib/xlsx/xform/sheet/worksheet-xform.js | 19 +++++------ lib/xlsx/xlsx.js | 11 +++--- spec/integration/data/.gitignore | 1 + spec/integration/data/test-issue-1842.xlsx | Bin 0 -> 8176 bytes ...42-dataValidations-memory-overload.spec.js | 32 ++++++++++++++++++ 7 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 spec/integration/data/.gitignore create mode 100644 spec/integration/data/test-issue-1842.xlsx create mode 100644 spec/integration/issues/issue-1842-dataValidations-memory-overload.spec.js diff --git a/README.md b/README.md index 36f142699..94525c8bf 100644 --- a/README.md +++ b/README.md @@ -2159,6 +2159,12 @@ faster or more resilient. #### Reading XLSX[⬆](#contents) +Options supported when reading CSV files. + +| Field | Required | Type |Description | +| ---------------- | ----------- | ----------- | ----------- | +| ignoreNodes | N | Array | A list of node names to ignore while loading the XLSX document. Improves performance in some situations.
      Available: `sheetPr`, `dimension`, `sheetViews `, `sheetFormatPr`, `cols `, `sheetData`, `autoFilter `, `mergeCells `, `rowBreaks`, `hyperlinks `, `pageMargins`, `dataValidations`, `pageSetup`, `headerFooter `, `printOptions `, `picture`, `drawing`, `sheetProtection`, `tableParts `, `conditionalFormatting`, `extLst`,| + ```javascript // read from a file const workbook = new Excel.Workbook(); @@ -2176,6 +2182,16 @@ await workbook.xlsx.read(stream); const workbook = new Excel.Workbook(); await workbook.xlsx.load(data); // ... use workbook + + +// using additional options +const workbook = new Excel.Workbook(); +await workbook.xlsx.load(data, { + ignoreNodes: [ + 'dataValidations' // ignores the workbook's Data Validations + ], +}); +// ... use workbook ``` #### Writing XLSX[⬆](#contents) diff --git a/index.d.ts b/index.d.ts index 03f78a3c8..5979806b6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1457,6 +1457,13 @@ export interface JSZipGeneratorOptions { }; } +export interface XlsxReadOptions { + /** + * The list of XML node names to ignore while parsing an XLSX file + */ + ignoreNodes: string[]; +} + export interface XlsxWriteOptions extends stream.xlsx.WorkbookWriterOptions { /** * The option passed to JsZip#generateAsync(options) @@ -1468,19 +1475,19 @@ export interface Xlsx { /** * read from a file */ - readFile(path: string): Promise; + readFile(path: string, options?: Partial): Promise; /** * read from a stream * @param stream */ - read(stream: import('stream').Stream): Promise; + read(stream: import('stream').Stream, options?: Partial): Promise; /** * load from an array buffer * @param buffer */ - load(buffer: Buffer): Promise; + load(buffer: Buffer, options?: Partial): Promise; /** * write to a buffer diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 4b36e37ba..b38930042 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -92,7 +92,10 @@ class WorkSheetXform extends BaseXform { constructor(options) { super(); - const {maxRows, maxCols} = options || {}; + const {maxRows, maxCols, ignoreNodes} = options || {}; + + this.ignoreNodes = ignoreNodes || []; + this.map = { sheetPr: new SheetPropertiesXform(), dimension: new DimensionXform(), @@ -221,9 +224,7 @@ class WorkSheetXform extends BaseXform { }); } let rIdImage = - this.preImageId === medium.imageId - ? drawingRelsHash[medium.imageId] - : drawingRelsHash[drawing.rels.length]; + this.preImageId === medium.imageId ? drawingRelsHash[medium.imageId] : drawingRelsHash[drawing.rels.length]; if (!rIdImage) { rIdImage = nextRid(drawing.rels); drawingRelsHash[drawing.rels.length] = rIdImage; @@ -368,8 +369,8 @@ class WorkSheetXform extends BaseXform { return true; } - this.parser = this.map[node.name]; - if (this.parser) { + if (this.map[node.name] && !this.ignoreNodes.includes(node.name)) { + this.parser = this.map[node.name]; this.parser.parseOpen(node); } return true; @@ -405,11 +406,7 @@ class WorkSheetXform extends BaseXform { false, margins: this.map.pageMargins.model, }; - const pageSetup = Object.assign( - sheetProperties, - this.map.pageSetup.model, - this.map.printOptions.model - ); + const pageSetup = Object.assign(sheetProperties, this.map.pageSetup.model, this.map.printOptions.model); const conditionalFormattings = mergeConditionalFormattings( this.map.conditionalFormatting.model, this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'] diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js index 268d1ddb3..ff2d9a117 100644 --- a/lib/xlsx/xlsx.js +++ b/lib/xlsx/xlsx.js @@ -22,7 +22,7 @@ const TableXform = require('./xform/table/table-xform'); const CommentsXform = require('./xform/comment/comments-xform'); const VmlNotesXform = require('./xform/comment/vml-notes-xform'); -const theme1Xml = require('./xml/theme1.js'); +const theme1Xml = require('./xml/theme1'); function fsReadFileAsync(filename, options) { return new Promise((resolve, reject) => { @@ -285,9 +285,11 @@ class XLSX { entryName = entryName.substr(1); } let stream; - if (entryName.match(/xl\/media\//) || + if ( + entryName.match(/xl\/media\//) || // themes are not parsed as stream - entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/)) { + entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/) + ) { stream = new PassThrough(); stream.write(await entry.async('nodebuffer')); } else { @@ -597,8 +599,7 @@ class XLSX { model.created = model.created || new Date(); model.modified = model.modified || new Date(); - model.useSharedStrings = - options.useSharedStrings !== undefined ? options.useSharedStrings : true; + model.useSharedStrings = options.useSharedStrings !== undefined ? options.useSharedStrings : true; model.useStyles = options.useStyles !== undefined ? options.useStyles : true; // Manage the shared strings diff --git a/spec/integration/data/.gitignore b/spec/integration/data/.gitignore new file mode 100644 index 000000000..9c689962a --- /dev/null +++ b/spec/integration/data/.gitignore @@ -0,0 +1 @@ +!*.xlsx diff --git a/spec/integration/data/test-issue-1842.xlsx b/spec/integration/data/test-issue-1842.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..013639766c377313cae3072515b7ed9d7ab04a4a GIT binary patch literal 8176 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 { + describe('issue 1842 - Memory overload when unnecessary dataValidations apply', () => { + it('when using readFile', async () => { + const wb = new ExcelJS.Workbook(); + await wb.xlsx.readFile(fileName, { + ignoreNodes: ['dataValidations'], + }); + + // arriving here is success + expect(true).to.equal(true); + }); + + it('when loading an in memory buffer', async () => { + const filePath = join(process.cwd(), fileName); + const buffer = readFileSync(filePath); + const wb = new ExcelJS.Workbook(); + await wb.xlsx.load(buffer, { + ignoreNodes: ['dataValidations'], + }); + + // arriving here is success + expect(true).to.equal(true); + }); + }); +}); From 6fb8117b33961e742eee9b0402b7ca99a9e8c419 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Thu, 19 Oct 2023 21:37:42 +0200 Subject: [PATCH 40/46] Update changelog: v4.4.0 preparations (#2555) * Update changelog: v4.4.0 preparations * fix table * remove duplicates * changelog in readme_zh wersion --- README.md | 92 ++++++++++++++++++++-------------------------------- README_zh.md | 82 +++++++++++++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 94525c8bf..e70b1d2be 100644 --- a/README.md +++ b/README.md @@ -18,59 +18,40 @@ npm install exceljs # New Features! - - - +* Merged [fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) #1815](https://github.com/exceljs/exceljs/pull/1815).
      Many thanks to [@andreykrupskii](https://github.com/andreykrupskii) for this contribution! +* Merged [inlineStr cell type support #1575 #1576](https://github.com/exceljs/exceljs/pull/1576).
      Many thanks to [@drdmitry](https://github.com/drdmitry) for this contribution! +* Merged [Fix parsing of boolean attributes #1849](https://github.com/exceljs/exceljs/pull/1849).
      Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [add optional custom auto-filter to table #1670](https://github.com/exceljs/exceljs/pull/1670).
      Many thanks to [@thambley](https://github.com/thambley) for this contribution! +* Merged [Deep copy inherited style #1850](https://github.com/exceljs/exceljs/pull/1850).
      Many thanks to [@ikzhr](https://github.com/ikzhr) for this contribution! +* Merged [Upgrade actions/cache and actions/setup-node #1846](https://github.com/exceljs/exceljs/pull/1846).
      Many thanks to [@cclauss](https://github.com/cclauss) for this contribution! +* Merged [Check object keys in isEqual #1831](https://github.com/exceljs/exceljs/pull/1831).
      Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [Add v17 to testing workflow #1856](https://github.com/exceljs/exceljs/pull/1856).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Upgrade jszip to its latest version to date. This version does not have any vulnerability found by Snyk so far #1895](https://github.com/exceljs/exceljs/pull/1895).
      Many thanks to [@ValerioSevilla](https://github.com/ValerioSevilla) for this contribution! +* Merged [Update README.md #1677](https://github.com/exceljs/exceljs/pull/1677).
      Many thanks to [@xjrcode](https://github.com/xjrcode) for this contribution! +* Merged [(docs): set prototype of RegExp correctly. #1700](https://github.com/exceljs/exceljs/pull/1700).
      Many thanks to [@joeldenning](https://github.com/joeldenning) for this contribution! +* Merged [Added timeouts to github actions #1733](https://github.com/exceljs/exceljs/pull/1733).
      Many thanks to [@alexbjorlig](https://github.com/alexbjorlig) for this contribution! +* Merged [fix issue 1676 #1701](https://github.com/exceljs/exceljs/pull/1701).
      Many thanks to [@skypesky](https://github.com/skypesky) for this contribution! +* Merged [ExcelJS/ExcelJS#2237 : Update CI Tests, Drop support for Node v8 #2242](https://github.com/exceljs/exceljs/pull/2242).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Fix types for getWorksheet() #2223](https://github.com/exceljs/exceljs/pull/2223).
      Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [add characters cannot be used for worksheet name #2126](https://github.com/exceljs/exceljs/pull/2126).
      Many thanks to [@tkm-kj](https://github.com/tkm-kj) for this contribution! +* Merged [Fix issue #1753 Reject promise when workbook reader is writing to temporary file stream and error occurs #1756](https://github.com/exceljs/exceljs/pull/1756).
      Many thanks to [@pauliusg](https://github.com/pauliusg) for this contribution! +* Merged [README.md to have correct link for Streaming XLSX #2186](https://github.com/exceljs/exceljs/pull/2186).
      Many thanks to [@wulfsolter](https://github.com/wulfsolter) for this contribution! +* Merged [Added a polyfill of promise.finally to support lower versions of Firefox. #1982](https://github.com/exceljs/exceljs/pull/1982).
      Many thanks to [@DemoJj](https://github.com/DemoJj) for this contribution! +* Merged [Fix read this.worksheet before assign it #1934](https://github.com/exceljs/exceljs/pull/1934).
      Many thanks to [@ZyqGitHub1](https://github.com/ZyqGitHub1) for this contribution! +* Merged [chore: upgrade jszip to ^3.10.1 #2211](https://github.com/exceljs/exceljs/pull/2211).
      Many thanks to [@jarrod-cocoon](https://github.com/jarrod-cocoon) for this contribution! +* Merged [fixed spelling error in README.md file #2208](https://github.com/exceljs/exceljs/pull/2208).
      Many thanks to [@HugoP27](https://github.com/HugoP27) for this contribution! +* Merged [fix: Fix xlsx.writeFile() not catching error when error occurs #2244](https://github.com/exceljs/exceljs/pull/2244).
      Many thanks to [@zurmokeeper](https://github.com/zurmokeeper) for this contribution! +* Merged [Improve worksheets' naming validation logic. #2257](https://github.com/exceljs/exceljs/pull/2257).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [fix issue 2125 - spliceRows remove last row #2140](https://github.com/exceljs/exceljs/pull/2140).
      Many thanks to [@babu-ch](https://github.com/babu-ch) for this contribution! +* Merged [fix: fix the loss of column attributes due to incorrect column order #2222](https://github.com/exceljs/exceljs/pull/2222).
      Many thanks to [@cpaiyueyue](https://github.com/cpaiyueyue) for this contribution! +* Merged [Fix: Sheet Properties Types #2327](https://github.com/exceljs/exceljs/pull/2327).
      Many thanks to [@albeniraouf](https://github.com/albeniraouf) for this contribution! +* Merged [Use node 18 LTS for tsc, and benchmark. Add node 20. to test matrix. … #2354](https://github.com/exceljs/exceljs/pull/2354).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Add missing tooltip attribute to CellHyperlinkValue index.d.ts #2350](https://github.com/exceljs/exceljs/pull/2350).
      Many thanks to [@NiklasPor](https://github.com/NiklasPor) for this contribution! +* Merged [Increase resilience to generating large workbooks #2320](https://github.com/exceljs/exceljs/pull/2320).
      Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [repair all 'c2fo.io' links ('c2fo.github.io') #2324](https://github.com/exceljs/exceljs/pull/2324).
      Many thanks to [@justintunev7](https://github.com/justintunev7) for this contribution! +* Merged [fix: fix type definitions about last column, formula values and protection #2309](https://github.com/exceljs/exceljs/pull/2309).
      Many thanks to [@gltjk](https://github.com/gltjk) for this contribution! +* Merged [fix: add spinCount field for WorksheetProtection type #2284](https://github.com/exceljs/exceljs/pull/2284).
      Many thanks to [@damingerdai](https://github.com/damingerdai) for this contribution! +* Merged [Add type definition for WorksheetModel.merges #2281](https://github.com/exceljs/exceljs/pull/2281).
      Many thanks to [@ytjmt](https://github.com/ytjmt) for this contribution! # Contributions @@ -2896,7 +2877,7 @@ If any splice operation affects a merged cell, the merge group will not be moved # Release History[⬆](#contents) | Version | Changes | -| ------- | ------- | +|---------| ------- | | 0.0.9 | | | 0.1.0 | | | 0.1.1 |
      • Bug Fixes
        • More textual data written properly to xml (including text, hyperlinks, formula results and format codes)
        • Better date format code recognition
      • Cell Font Style
      | @@ -3039,5 +3020,4 @@ If any splice operation affects a merged cell, the merge group will not be moved | 4.1.1 | | | 4.2.0 | | | 4.2.1 | | - - +| 4.3.0 | | diff --git a/README_zh.md b/README_zh.md index 4b6c25eeb..5f1100363 100644 --- a/README_zh.md +++ b/README_zh.md @@ -16,20 +16,40 @@ npm install exceljs # 新的功能! - +* Merged [fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) #1815](https://github.com/exceljs/exceljs/pull/1815).
      Many thanks to [@andreykrupskii](https://github.com/andreykrupskii) for this contribution! +* Merged [inlineStr cell type support #1575 #1576](https://github.com/exceljs/exceljs/pull/1576).
      Many thanks to [@drdmitry](https://github.com/drdmitry) for this contribution! +* Merged [Fix parsing of boolean attributes #1849](https://github.com/exceljs/exceljs/pull/1849).
      Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [add optional custom auto-filter to table #1670](https://github.com/exceljs/exceljs/pull/1670).
      Many thanks to [@thambley](https://github.com/thambley) for this contribution! +* Merged [Deep copy inherited style #1850](https://github.com/exceljs/exceljs/pull/1850).
      Many thanks to [@ikzhr](https://github.com/ikzhr) for this contribution! +* Merged [Upgrade actions/cache and actions/setup-node #1846](https://github.com/exceljs/exceljs/pull/1846).
      Many thanks to [@cclauss](https://github.com/cclauss) for this contribution! +* Merged [Check object keys in isEqual #1831](https://github.com/exceljs/exceljs/pull/1831).
      Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [Add v17 to testing workflow #1856](https://github.com/exceljs/exceljs/pull/1856).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Upgrade jszip to its latest version to date. This version does not have any vulnerability found by Snyk so far #1895](https://github.com/exceljs/exceljs/pull/1895).
      Many thanks to [@ValerioSevilla](https://github.com/ValerioSevilla) for this contribution! +* Merged [Update README.md #1677](https://github.com/exceljs/exceljs/pull/1677).
      Many thanks to [@xjrcode](https://github.com/xjrcode) for this contribution! +* Merged [(docs): set prototype of RegExp correctly. #1700](https://github.com/exceljs/exceljs/pull/1700).
      Many thanks to [@joeldenning](https://github.com/joeldenning) for this contribution! +* Merged [Added timeouts to github actions #1733](https://github.com/exceljs/exceljs/pull/1733).
      Many thanks to [@alexbjorlig](https://github.com/alexbjorlig) for this contribution! +* Merged [fix issue 1676 #1701](https://github.com/exceljs/exceljs/pull/1701).
      Many thanks to [@skypesky](https://github.com/skypesky) for this contribution! +* Merged [ExcelJS/ExcelJS#2237 : Update CI Tests, Drop support for Node v8 #2242](https://github.com/exceljs/exceljs/pull/2242).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Fix types for getWorksheet() #2223](https://github.com/exceljs/exceljs/pull/2223).
      Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [add characters cannot be used for worksheet name #2126](https://github.com/exceljs/exceljs/pull/2126).
      Many thanks to [@tkm-kj](https://github.com/tkm-kj) for this contribution! +* Merged [Fix issue #1753 Reject promise when workbook reader is writing to temporary file stream and error occurs #1756](https://github.com/exceljs/exceljs/pull/1756).
      Many thanks to [@pauliusg](https://github.com/pauliusg) for this contribution! +* Merged [README.md to have correct link for Streaming XLSX #2186](https://github.com/exceljs/exceljs/pull/2186).
      Many thanks to [@wulfsolter](https://github.com/wulfsolter) for this contribution! +* Merged [Added a polyfill of promise.finally to support lower versions of Firefox. #1982](https://github.com/exceljs/exceljs/pull/1982).
      Many thanks to [@DemoJj](https://github.com/DemoJj) for this contribution! +* Merged [Fix read this.worksheet before assign it #1934](https://github.com/exceljs/exceljs/pull/1934).
      Many thanks to [@ZyqGitHub1](https://github.com/ZyqGitHub1) for this contribution! +* Merged [chore: upgrade jszip to ^3.10.1 #2211](https://github.com/exceljs/exceljs/pull/2211).
      Many thanks to [@jarrod-cocoon](https://github.com/jarrod-cocoon) for this contribution! +* Merged [fixed spelling error in README.md file #2208](https://github.com/exceljs/exceljs/pull/2208).
      Many thanks to [@HugoP27](https://github.com/HugoP27) for this contribution! +* Merged [fix: Fix xlsx.writeFile() not catching error when error occurs #2244](https://github.com/exceljs/exceljs/pull/2244).
      Many thanks to [@zurmokeeper](https://github.com/zurmokeeper) for this contribution! +* Merged [Improve worksheets' naming validation logic. #2257](https://github.com/exceljs/exceljs/pull/2257).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [fix issue 2125 - spliceRows remove last row #2140](https://github.com/exceljs/exceljs/pull/2140).
      Many thanks to [@babu-ch](https://github.com/babu-ch) for this contribution! +* Merged [fix: fix the loss of column attributes due to incorrect column order #2222](https://github.com/exceljs/exceljs/pull/2222).
      Many thanks to [@cpaiyueyue](https://github.com/cpaiyueyue) for this contribution! +* Merged [Fix: Sheet Properties Types #2327](https://github.com/exceljs/exceljs/pull/2327).
      Many thanks to [@albeniraouf](https://github.com/albeniraouf) for this contribution! +* Merged [Use node 18 LTS for tsc, and benchmark. Add node 20. to test matrix. … #2354](https://github.com/exceljs/exceljs/pull/2354).
      Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Add missing tooltip attribute to CellHyperlinkValue index.d.ts #2350](https://github.com/exceljs/exceljs/pull/2350).
      Many thanks to [@NiklasPor](https://github.com/NiklasPor) for this contribution! +* Merged [Increase resilience to generating large workbooks #2320](https://github.com/exceljs/exceljs/pull/2320).
      Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [repair all 'c2fo.io' links ('c2fo.github.io') #2324](https://github.com/exceljs/exceljs/pull/2324).
      Many thanks to [@justintunev7](https://github.com/justintunev7) for this contribution! +* Merged [fix: fix type definitions about last column, formula values and protection #2309](https://github.com/exceljs/exceljs/pull/2309).
      Many thanks to [@gltjk](https://github.com/gltjk) for this contribution! +* Merged [fix: add spinCount field for WorksheetProtection type #2284](https://github.com/exceljs/exceljs/pull/2284).
      Many thanks to [@damingerdai](https://github.com/damingerdai) for this contribution! +* Merged [Add type definition for WorksheetModel.merges #2281](https://github.com/exceljs/exceljs/pull/2281).
      Many thanks to [@ytjmt](https://github.com/ytjmt) for this contribution! # 贡献 @@ -2711,19 +2731,19 @@ sudo apt-get install libfontconfig # 发布历史[⬆](#目录) -| 版本 | 变化 | -| ------- | ------- | -| 0.0.9 | | -| 0.1.0 | | -| 0.1.1 |
      • Bug 修复
        • 可以将更多文本数据正确写入xml(包括文本,超链接,公式结果和格式代码)
        • 更好的日期格式代码识别
      • 单元格字体样式
      | -| 0.1.2 |
      • 修复了 zip 写入时潜在的竞争条件
      | -| 0.1.3 | | -| 0.1.5 |
      • Bug 修复
        • 现在可以在一本工作簿中处理10个或更多工作表
        • 正确添加并引用了 theme1.xml 文件
      • 单元格边框
      | -| 0.1.6 | | -| 0.1.8 |
      • Bug 修复
        • XLSX 文件中包含更兼容的theme1.xml
        • 修复文件名大小写问题
      • 单元格填充
      | -| 0.1.9 |
      • Bug 修复
        • 添加了 docProps 文件以满足 Mac Excel 用户
        • 修复文件名大小写问题
        • 修复工作表 ID 问题
      • 核心工作簿属性
      | -| 0.1.10 | | -| 0.1.11 | | +| Version | Changes | +|---------| ------- | +| 0.0.9 | | +| 0.1.0 | | +| 0.1.1 |
      • Bug Fixes
        • More textual data written properly to xml (including text, hyperlinks, formula results and format codes)
        • Better date format code recognition
      • Cell Font Style
      | +| 0.1.2 |
      • Fixed potential race condition on zip write
      | +| 0.1.3 | | +| 0.1.5 |
      • Bug Fixes
        • Now handles 10 or more worksheets in one workbook
        • theme1.xml file properly added and referenced
      • Cell Borders
      | +| 0.1.6 |
      • Bug Fixes
        • More compatible theme1.xml included in XLSX file
      • Cell Fills
      | +| 0.1.8 |
      • Bug Fixes
        • More compatible theme1.xml included in XLSX file
        • Fixed filename case issue
      • Cell Fills
      | +| 0.1.9 |
      • Bug Fixes
        • Added docProps files to satisfy Mac Excel users
        • Fixed filename case issue
        • Fixed worksheet id issue
      • Core Workbook Properties
      | +| 0.1.10 |
      • Bug Fixes
        • Handles File Not Found error
      • CSV Files
      | +| 0.1.11 | | | 0.2.0 |
      • Streaming XLSX Writer
        • At long last ExcelJS can support writing massive XLSX files in a scalable memory efficient manner. Performance has been optimised and even smaller spreadsheets can be faster to write than the document writer. Options have been added to control the use of shared strings and styles as these can both have a considerable effect on performance
      • Worksheet.lastRow
        • Access the last editable row in a worksheet.
      • Row.commit()
        • For streaming writers, this method commits the row (and any previous rows) to the stream. Committed rows will no longer be editable (and are typically deleted from the worksheet object). For Document type workbooks, this method has no effect.
      | | 0.2.2 |
      • One Billion Cells
        • Achievement Unlocked: A simple test using ExcelJS has created a spreadsheet with 1,000,000,000 cells. Made using random data with 100,000,000 rows of 10 cells per row. I cannot validate the file yet as Excel will not open it and I have yet to implement the streaming reader but I have every confidence that it is good since 1,000,000 rows loads ok.
      | | 0.2.3 |
      • Bug Fixes
      • Streaming XLSX Writer
        • At long last ExcelJS can support writing massive XLSX files in a scalable memory efficient manner. Performance has been optimised and even smaller spreadsheets can be faster to write than the document writer. Options have been added to control the use of shared strings and styles as these can both have a considerable effect on performance
      • Worksheet.lastRow
        • Access the last editable row in a worksheet.
      • Row.commit()
        • For streaming writers, this method commits the row (and any previous rows) to the stream. Committed rows will no longer be editable (and are typically deleted from the worksheet object). For Document type workbooks, this method has no effect.
      | @@ -2848,3 +2868,11 @@ sudo apt-get install libfontconfig | 3.8.0 | | | 3.8.1 | | | 3.8.2 | | +| 3.9.0 | | +| 3.10.0 | | +| 4.0.1 | | +| 4.1.0 | | +| 4.1.1 | | +| 4.2.0 | | +| 4.2.1 | | +| 4.3.0 | | From ac96f9a61e9799c7776bd940f05c4a51d7200209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=40Siemienik=20Pawe=C5=82?= Date: Thu, 19 Oct 2023 21:48:53 +0200 Subject: [PATCH 41/46] 4.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d48e94057..7a70a5e13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "exceljs", - "version": "4.3.0", + "version": "4.4.0", "description": "Excel Workbook Manager - Read and Write xlsx and csv Files.", "private": false, "license": "MIT", From 2abd2d8f17a4b9a731941af3f55dca8c67693b93 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Sat, 28 Oct 2023 16:05:33 +0330 Subject: [PATCH 42/46] Fix typo in docs (#2566) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e70b1d2be..5b0fd8985 100644 --- a/README.md +++ b/README.md @@ -2140,7 +2140,7 @@ faster or more resilient. #### Reading XLSX[⬆](#contents) -Options supported when reading CSV files. +Options supported when reading XLSX files. | Field | Required | Type |Description | | ---------------- | ----------- | ----------- | ----------- | From 2225238343151f0c81035aa4a99895a5425d47a2 Mon Sep 17 00:00:00 2001 From: Siemienik Pawel Date: Sat, 28 Oct 2023 16:20:40 +0200 Subject: [PATCH 43/46] Remove invalid readonly for Column.values (#2571) https://github.com/exceljs/exceljs/issues/514#issuecomment-1782927522 --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5979806b6..81e65fec4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -634,7 +634,7 @@ export interface Column { /** * The cell values in the column */ - values: ReadonlyArray; + values: CellValue; /** * Column letter key @@ -2032,4 +2032,4 @@ export namespace stream { getColumn(c: number): Column; } } -} \ No newline at end of file +} From 6141056205468314ccbd96aca41b295b9f756628 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 31 Oct 2023 22:27:47 +0100 Subject: [PATCH 44/46] Upgrade GitHub Actions cache, checkout, setup-node (#2559) * https://github.com/actions/cache/releases * https://github.com/actions/checkout/releases * https://github.com/actions/setup-node/releases --- .github/workflows/tests.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7ae75016b..7e696d799 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,15 +24,15 @@ jobs: git config --global core.autocrlf false git config --global core.symlinks true if: runner.os == 'Windows' - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #latest v4. TODO upgrade - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d #latest v3. TODO upgrade with: node-version: ${{ matrix.node-version }} - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #latest v3 TODO upgrade with: path: ./npm-cache key: v1-${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('**/package.json') }} @@ -49,14 +49,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade - - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #latest v4. TODO upgrade + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d #latest v3. TODO upgrade with: node-version: 18 - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #latest v3 TODO upgrade with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} @@ -73,14 +73,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade - - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #latest v4. TODO upgrade + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d #latest v3. TODO upgrade with: node-version: 18 - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #latest v3 TODO upgrade with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} From ddab279a882aba8f18fc5127c6d59e3aa9f596c7 Mon Sep 17 00:00:00 2001 From: Michael <216956+mikez@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:58:07 +0100 Subject: [PATCH 45/46] Add pivot table with limitations (#2551) * Add pivot table with limitations ```js worksheet.addPivotTable(configuration); ``` **Note:** Pivot table support is in its early stages with certain limitations, including: - Xlsx files with existing pivot tables can't be read (writing is supported). - Pivot table configurations must have one "value"-item and use the "sum" metric. - Only one pivot table can be added for the entire document. * Update README --- .prettier | 4 +- README.md | 1 + lib/doc/pivot-table.js | 132 ++++++++++++ lib/doc/workbook.js | 3 + lib/doc/worksheet.js | 22 ++ lib/utils/utils.js | 35 +++- lib/xlsx/rel-type.js | 15 +- .../xform/book/workbook-pivot-cache-xform.js | 29 +++ lib/xlsx/xform/book/workbook-xform.js | 12 +- lib/xlsx/xform/core/content-types-xform.js | 19 +- lib/xlsx/xform/pivot-table/cache-field.js | 43 ++++ .../pivot-cache-definition-xform.js | 77 +++++++ .../pivot-table/pivot-cache-records-xform.js | 103 ++++++++++ .../xform/pivot-table/pivot-table-xform.js | 189 ++++++++++++++++++ lib/xlsx/xform/sheet/worksheet-xform.js | 9 + lib/xlsx/xlsx.js | 90 ++++++++- .../integration/workbook/pivot-tables.spec.js | 78 ++++++++ test/test-pivot-table.js | 54 +++++ 18 files changed, 893 insertions(+), 22 deletions(-) create mode 100644 lib/doc/pivot-table.js create mode 100644 lib/xlsx/xform/book/workbook-pivot-cache-xform.js create mode 100644 lib/xlsx/xform/pivot-table/cache-field.js create mode 100644 lib/xlsx/xform/pivot-table/pivot-cache-definition-xform.js create mode 100644 lib/xlsx/xform/pivot-table/pivot-cache-records-xform.js create mode 100644 lib/xlsx/xform/pivot-table/pivot-table-xform.js create mode 100644 spec/integration/workbook/pivot-tables.spec.js create mode 100644 test/test-pivot-table.js diff --git a/.prettier b/.prettier index 470c46807..cbd7f9818 100644 --- a/.prettier +++ b/.prettier @@ -2,6 +2,6 @@ "bracketSpacing": false, "printWidth": 100, "trailingComma": "all", - "bracketSpacing": false, - "arrowParens": "avoid" + "arrowParens": "avoid", + "singleQuote": true, } diff --git a/README.md b/README.md index 5b0fd8985..ccc780828 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ npm install exceljs # New Features! +* Merged [Add pivot table with limitations #2551](https://github.com/exceljs/exceljs/pull/2551).
      Many thanks to Protobi and Michael for this contribution! * Merged [fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) #1815](https://github.com/exceljs/exceljs/pull/1815).
      Many thanks to [@andreykrupskii](https://github.com/andreykrupskii) for this contribution! * Merged [inlineStr cell type support #1575 #1576](https://github.com/exceljs/exceljs/pull/1576).
      Many thanks to [@drdmitry](https://github.com/drdmitry) for this contribution! * Merged [Fix parsing of boolean attributes #1849](https://github.com/exceljs/exceljs/pull/1849).
      Many thanks to [@bno1](https://github.com/bno1) for this contribution! diff --git a/lib/doc/pivot-table.js b/lib/doc/pivot-table.js new file mode 100644 index 000000000..fb3b66405 --- /dev/null +++ b/lib/doc/pivot-table.js @@ -0,0 +1,132 @@ +const {objectFromProps, range, toSortedArray} = require('../utils/utils'); + +// TK(2023-10-10): turn this into a class constructor. + +function makePivotTable(worksheet, model) { + // Example `model`: + // { + // // Source of data: the entire sheet range is taken, + // // akin to `worksheet1.getSheetValues()`. + // sourceSheet: worksheet1, + // + // // Pivot table fields: values indicate field names; + // // they come from the first row in `worksheet1`. + // rows: ['A', 'B'], + // columns: ['C'], + // values: ['E'], // only 1 item possible for now + // metric: 'sum', // only 'sum' possible for now + // } + + validate(worksheet, model); + + const {sourceSheet} = model; + let {rows, columns, values} = model; + + const cacheFields = makeCacheFields(sourceSheet, [...rows, ...columns]); + + // let {rows, columns, values} use indices instead of names; + // names can then be accessed via `pivotTable.cacheFields[index].name`. + // *Note*: Using `reduce` as `Object.fromEntries` requires Node 12+; + // ExcelJS is >=8.3.0 (as of 2023-10-08). + const nameToIndex = cacheFields.reduce((result, cacheField, index) => { + result[cacheField.name] = index; + return result; + }, {}); + rows = rows.map(row => nameToIndex[row]); + columns = columns.map(column => nameToIndex[column]); + values = values.map(value => nameToIndex[value]); + + // form pivot table object + return { + sourceSheet, + rows, + columns, + values, + metric: 'sum', + cacheFields, + // defined in of xl/pivotTables/pivotTable1.xml; + // also used in xl/workbook.xml + cacheId: '10', + }; +} + +function validate(worksheet, model) { + if (worksheet.workbook.pivotTables.length === 1) { + throw new Error( + 'A pivot table was already added. At this time, ExcelJS supports at most one pivot table per file.' + ); + } + + if (model.metric && model.metric !== 'sum') { + throw new Error('Only the "sum" metric is supported at this time.'); + } + + const headerNames = model.sourceSheet.getRow(1).values.slice(1); + const isInHeaderNames = objectFromProps(headerNames, true); + for (const name of [...model.rows, ...model.columns, ...model.values]) { + if (!isInHeaderNames[name]) { + throw new Error(`The header name "${name}" was not found in ${model.sourceSheet.name}.`); + } + } + + if (!model.rows.length) { + throw new Error('No pivot table rows specified.'); + } + + if (!model.columns.length) { + throw new Error('No pivot table columns specified.'); + } + + if (model.values.length !== 1) { + throw new Error('Exactly 1 value needs to be specified at this time.'); + } +} + +function makeCacheFields(worksheet, fieldNamesWithSharedItems) { + // Cache fields are used in pivot tables to reference source data. + // + // Example + // ------- + // Turn + // + // `worksheet` sheet values [ + // ['A', 'B', 'C', 'D', 'E'], + // ['a1', 'b1', 'c1', 4, 5], + // ['a1', 'b2', 'c1', 4, 5], + // ['a2', 'b1', 'c2', 14, 24], + // ['a2', 'b2', 'c2', 24, 35], + // ['a3', 'b1', 'c3', 34, 45], + // ['a3', 'b2', 'c3', 44, 45] + // ]; + // fieldNamesWithSharedItems = ['A', 'B', 'C']; + // + // into + // + // [ + // { name: 'A', sharedItems: ['a1', 'a2', 'a3'] }, + // { name: 'B', sharedItems: ['b1', 'b2'] }, + // { name: 'C', sharedItems: ['c1', 'c2', 'c3'] }, + // { name: 'D', sharedItems: null }, + // { name: 'E', sharedItems: null } + // ] + + const names = worksheet.getRow(1).values; + const nameToHasSharedItems = objectFromProps(fieldNamesWithSharedItems, true); + + const aggregate = columnIndex => { + const columnValues = worksheet.getColumn(columnIndex).values.splice(2); + const columnValuesAsSet = new Set(columnValues); + return toSortedArray(columnValuesAsSet); + }; + + // make result + const result = []; + for (const columnIndex of range(1, names.length)) { + const name = names[columnIndex]; + const sharedItems = nameToHasSharedItems[name] ? aggregate(columnIndex) : null; + result.push({name, sharedItems}); + } + return result; +} + +module.exports = {makePivotTable}; diff --git a/lib/doc/workbook.js b/lib/doc/workbook.js index 8e7f46ecd..dd4893a6e 100644 --- a/lib/doc/workbook.js +++ b/lib/doc/workbook.js @@ -27,6 +27,7 @@ class Workbook { this.title = ''; this.views = []; this.media = []; + this.pivotTables = []; this._definedNames = new DefinedNames(); } @@ -174,6 +175,7 @@ class Workbook { contentStatus: this.contentStatus, themes: this._themes, media: this.media, + pivotTables: this.pivotTables, calcProperties: this.calcProperties, }; } @@ -215,6 +217,7 @@ class Workbook { this.views = value.views; this._themes = value.themes; this.media = value.media || []; + this.pivotTables = value.pivotTables || []; } } diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 13e414140..1855b499e 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -8,6 +8,7 @@ const Enums = require('./enums'); const Image = require('./image'); const Table = require('./table'); const DataValidations = require('./data-validations'); +const {makePivotTable} = require('./pivot-table'); const Encryptor = require('../utils/encryptor'); const {copyStyle} = require('../utils/copy-style'); @@ -124,6 +125,8 @@ class Worksheet { // for tables this.tables = {}; + this.pivotTables = []; + this.conditionalFormattings = []; } @@ -806,6 +809,23 @@ class Worksheet { return Object.values(this.tables); } + // ========================================================================= + // Pivot Tables + addPivotTable(model) { + // eslint-disable-next-line no-console + console.warn( + `Warning: Pivot Table support is experimental. +Please leave feedback at https://github.com/exceljs/exceljs/discussions/2575` + ); + + const pivotTable = makePivotTable(this, model); + + this.pivotTables.push(pivotTable); + this.workbook.pivotTables.push(pivotTable); + + return pivotTable; + } + // =========================================================================== // Conditional Formatting addConditionalFormatting(cf) { @@ -854,6 +874,7 @@ class Worksheet { media: this._media.map(medium => medium.model), sheetProtection: this.sheetProtection, tables: Object.values(this.tables).map(table => table.model), + pivotTables: this.pivotTables, conditionalFormattings: this.conditionalFormattings, }; @@ -920,6 +941,7 @@ class Worksheet { tables[table.name] = t; return tables; }, {}); + this.pivotTables = value.pivotTables; this.conditionalFormattings = value.conditionalFormattings; } } diff --git a/lib/utils/utils.js b/lib/utils/utils.js index 84cd212c2..21dd20ee9 100644 --- a/lib/utils/utils.js +++ b/lib/utils/utils.js @@ -53,9 +53,11 @@ const utils = { }, inherits, dateToExcel(d, date1904) { - return 25569 + ( d.getTime() / (24 * 3600 * 1000) ) - (date1904 ? 1462 : 0); + // eslint-disable-next-line no-mixed-operators + return 25569 + d.getTime() / (24 * 3600 * 1000) - (date1904 ? 1462 : 0); }, excelToDate(v, date1904) { + // eslint-disable-next-line no-mixed-operators const millisecondSinceEpoch = Math.round((v - 25569 + (date1904 ? 1462 : 0)) * 24 * 3600 * 1000); return new Date(millisecondSinceEpoch); }, @@ -167,6 +169,37 @@ const utils = { parseBoolean(value) { return value === true || value === 'true' || value === 1 || value === '1'; }, + + *range(start, stop, step = 1) { + const compareOrder = step > 0 ? (a, b) => a < b : (a, b) => a > b; + for (let value = start; compareOrder(value, stop); value += step) { + yield value; + } + }, + + toSortedArray(values) { + const result = Array.from(values); + + // Note: per default, `Array.prototype.sort()` converts values + // to strings when comparing. Here, if we have numbers, we use + // numeric sort. + if (result.every(item => Number.isFinite(item))) { + const compareNumbers = (a, b) => a - b; + return result.sort(compareNumbers); + } + + return result.sort(); + }, + + objectFromProps(props, value = null) { + // *Note*: Using `reduce` as `Object.fromEntries` requires Node 12+; + // ExcelJs is >=8.3.0 (as of 2023-10-08). + // return Object.fromEntries(props.map(property => [property, value])); + return props.reduce((result, property) => { + result[property] = value; + return result; + }, {}); + }, }; module.exports = utils; diff --git a/lib/xlsx/rel-type.js b/lib/xlsx/rel-type.js index 7cd0a3d05..c9c454b45 100644 --- a/lib/xlsx/rel-type.js +++ b/lib/xlsx/rel-type.js @@ -1,21 +1,20 @@ 'use strict'; module.exports = { - OfficeDocument: - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument', + OfficeDocument: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument', Worksheet: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet', CalcChain: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain', - SharedStrings: - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings', + SharedStrings: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings', Styles: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles', Theme: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme', Hyperlink: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', Image: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', - CoreProperties: - 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', - ExtenderProperties: - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties', + CoreProperties: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', + ExtenderProperties: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties', Comments: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments', VmlDrawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing', Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table', + PivotCacheDefinition: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition', + PivotCacheRecords: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords', + PivotTable: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable', }; diff --git a/lib/xlsx/xform/book/workbook-pivot-cache-xform.js b/lib/xlsx/xform/book/workbook-pivot-cache-xform.js new file mode 100644 index 000000000..894c86bac --- /dev/null +++ b/lib/xlsx/xform/book/workbook-pivot-cache-xform.js @@ -0,0 +1,29 @@ +const BaseXform = require('../base-xform'); + +class WorkbookPivotCacheXform extends BaseXform { + render(xmlStream, model) { + xmlStream.leafNode('pivotCache', { + cacheId: model.cacheId, + 'r:id': model.rId, + }); + } + + parseOpen(node) { + if (node.name === 'pivotCache') { + this.model = { + cacheId: node.attributes.cacheId, + rId: node.attributes['r:id'], + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = WorkbookPivotCacheXform; diff --git a/lib/xlsx/xform/book/workbook-xform.js b/lib/xlsx/xform/book/workbook-xform.js index 5c10a5857..c8f1c9e1b 100644 --- a/lib/xlsx/xform/book/workbook-xform.js +++ b/lib/xlsx/xform/book/workbook-xform.js @@ -11,6 +11,7 @@ const SheetXform = require('./sheet-xform'); const WorkbookViewXform = require('./workbook-view-xform'); const WorkbookPropertiesXform = require('./workbook-properties-xform'); const WorkbookCalcPropertiesXform = require('./workbook-calc-properties-xform'); +const WorkbookPivotCacheXform = require('./workbook-pivot-cache-xform'); class WorkbookXform extends BaseXform { constructor() { @@ -31,6 +32,11 @@ class WorkbookXform extends BaseXform { childXform: new DefinedNameXform(), }), calcPr: new WorkbookCalcPropertiesXform(), + pivotCaches: new ListXform({ + tag: 'pivotCaches', + count: false, + childXform: new WorkbookPivotCacheXform(), + }), }; } @@ -53,10 +59,7 @@ class WorkbookXform extends BaseXform { }); } - if ( - sheet.pageSetup && - (sheet.pageSetup.printTitlesRow || sheet.pageSetup.printTitlesColumn) - ) { + if (sheet.pageSetup && (sheet.pageSetup.printTitlesRow || sheet.pageSetup.printTitlesColumn)) { const ranges = []; if (sheet.pageSetup.printTitlesColumn) { @@ -99,6 +102,7 @@ class WorkbookXform extends BaseXform { this.map.sheets.render(xmlStream, model.sheets); this.map.definedNames.render(xmlStream, model.definedNames); this.map.calcPr.render(xmlStream, model.calcProperties); + this.map.pivotCaches.render(xmlStream, model.pivotTables); xmlStream.closeNode(); } diff --git a/lib/xlsx/xform/core/content-types-xform.js b/lib/xlsx/xform/core/content-types-xform.js index 2999c62aa..5e8ff5564 100644 --- a/lib/xlsx/xform/core/content-types-xform.js +++ b/lib/xlsx/xform/core/content-types-xform.js @@ -40,6 +40,22 @@ class ContentTypesXform extends BaseXform { }); }); + if ((model.pivotTables || []).length) { + // Note(2023-10-06): assuming at most one pivot table for now. + xmlStream.leafNode('Override', { + PartName: '/xl/pivotCache/pivotCacheDefinition1.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml', + }); + xmlStream.leafNode('Override', { + PartName: '/xl/pivotCache/pivotCacheRecords1.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml', + }); + xmlStream.leafNode('Override', { + PartName: '/xl/pivotTables/pivotTable1.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml', + }); + } + xmlStream.leafNode('Override', { PartName: '/xl/theme/theme1.xml', ContentType: 'application/vnd.openxmlformats-officedocument.theme+xml', @@ -53,8 +69,7 @@ class ContentTypesXform extends BaseXform { if (hasSharedStrings) { xmlStream.leafNode('Override', { PartName: '/xl/sharedStrings.xml', - ContentType: - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml', + ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml', }); } diff --git a/lib/xlsx/xform/pivot-table/cache-field.js b/lib/xlsx/xform/pivot-table/cache-field.js new file mode 100644 index 000000000..50f790a94 --- /dev/null +++ b/lib/xlsx/xform/pivot-table/cache-field.js @@ -0,0 +1,43 @@ +class CacheField { + constructor({name, sharedItems}) { + // string type + // + // { + // 'name': 'A', + // 'sharedItems': ['a1', 'a2', 'a3'] + // } + // + // or + // + // integer type + // + // { + // 'name': 'D', + // 'sharedItems': null + // } + this.name = name; + this.sharedItems = sharedItems; + } + + render() { + // PivotCache Field: http://www.datypic.com/sc/ooxml/e-ssml_cacheField-1.html + // Shared Items: http://www.datypic.com/sc/ooxml/e-ssml_sharedItems-1.html + + // integer types + if (this.sharedItems === null) { + // TK(2023-07-18): left out attributes... minValue="5" maxValue="45" + return ` + + `; + } + + // string types + return ` + + ${this.sharedItems.map(item => ``).join('')} + + `; + } +} + +module.exports = CacheField; diff --git a/lib/xlsx/xform/pivot-table/pivot-cache-definition-xform.js b/lib/xlsx/xform/pivot-table/pivot-cache-definition-xform.js new file mode 100644 index 000000000..18f4ef379 --- /dev/null +++ b/lib/xlsx/xform/pivot-table/pivot-cache-definition-xform.js @@ -0,0 +1,77 @@ +const BaseXform = require('../base-xform'); +const CacheField = require('./cache-field'); +const XmlStream = require('../../../utils/xml-stream'); + +class PivotCacheDefinitionXform extends BaseXform { + constructor() { + super(); + + this.map = {}; + } + + prepare(model) { + // TK + } + + get tag() { + // http://www.datypic.com/sc/ooxml/e-ssml_pivotCacheDefinition.html + return 'pivotCacheDefinition'; + } + + render(xmlStream, model) { + const {sourceSheet, cacheFields} = model; + + xmlStream.openXml(XmlStream.StdDocAttributes); + xmlStream.openNode(this.tag, { + ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES, + 'r:id': 'rId1', + refreshOnLoad: '1', // important for our implementation to work + refreshedBy: 'Author', + refreshedDate: '45125.026046874998', + createdVersion: '8', + refreshedVersion: '8', + minRefreshableVersion: '3', + recordCount: cacheFields.length + 1, + }); + + xmlStream.openNode('cacheSource', {type: 'worksheet'}); + xmlStream.leafNode('worksheetSource', { + ref: sourceSheet.dimensions.shortRange, + sheet: sourceSheet.name, + }); + xmlStream.closeNode(); + + xmlStream.openNode('cacheFields', {count: cacheFields.length}); + // Note: keeping this pretty-printed for now to ease debugging. + xmlStream.writeXml(cacheFields.map(cacheField => new CacheField(cacheField).render()).join('\n ')); + xmlStream.closeNode(); + + xmlStream.closeNode(); + } + + parseOpen(node) { + // TK + } + + parseText(text) { + // TK + } + + parseClose(name) { + // TK + } + + reconcile(model, options) { + // TK + } +} + +PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES = { + xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', + 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', + 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006', + 'mc:Ignorable': 'xr', + 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision', +}; + +module.exports = PivotCacheDefinitionXform; diff --git a/lib/xlsx/xform/pivot-table/pivot-cache-records-xform.js b/lib/xlsx/xform/pivot-table/pivot-cache-records-xform.js new file mode 100644 index 000000000..220ec04a5 --- /dev/null +++ b/lib/xlsx/xform/pivot-table/pivot-cache-records-xform.js @@ -0,0 +1,103 @@ +const XmlStream = require('../../../utils/xml-stream'); + +const BaseXform = require('../base-xform'); + +class PivotCacheRecordsXform extends BaseXform { + constructor() { + super(); + + this.map = {}; + } + + prepare(model) { + // TK + } + + get tag() { + // http://www.datypic.com/sc/ooxml/e-ssml_pivotCacheRecords.html + return 'pivotCacheRecords'; + } + + render(xmlStream, model) { + const {sourceSheet, cacheFields} = model; + const sourceBodyRows = sourceSheet.getSheetValues().slice(2); + + xmlStream.openXml(XmlStream.StdDocAttributes); + xmlStream.openNode(this.tag, { + ...PivotCacheRecordsXform.PIVOT_CACHE_RECORDS_ATTRIBUTES, + count: sourceBodyRows.length, + }); + xmlStream.writeXml(renderTable()); + xmlStream.closeNode(); + + // Helpers + + function renderTable() { + const rowsInXML = sourceBodyRows.map(row => { + const realRow = row.slice(1); + return [...renderRowLines(realRow)].join(''); + }); + return rowsInXML.join(''); + } + + function* renderRowLines(row) { + // PivotCache Record: http://www.datypic.com/sc/ooxml/e-ssml_r-1.html + // Note: pretty-printing this for now to ease debugging. + yield '\n '; + for (const [index, cellValue] of row.entries()) { + yield '\n '; + yield renderCell(cellValue, cacheFields[index].sharedItems); + } + yield '\n '; + } + + function renderCell(value, sharedItems) { + // no shared items + // -------------------------------------------------- + if (sharedItems === null) { + if (Number.isFinite(value)) { + // Numeric value: http://www.datypic.com/sc/ooxml/e-ssml_n-2.html + return ``; + } + // Character Value: http://www.datypic.com/sc/ooxml/e-ssml_s-2.html + return ``; + + } + + // shared items + // -------------------------------------------------- + const sharedItemsIndex = sharedItems.indexOf(value); + if (sharedItemsIndex < 0) { + throw new Error(`${JSON.stringify(value)} not in sharedItems ${JSON.stringify(sharedItems)}`); + } + // Shared Items Index: http://www.datypic.com/sc/ooxml/e-ssml_x-9.html + return ``; + } + } + + parseOpen(node) { + // TK + } + + parseText(text) { + // TK + } + + parseClose(name) { + // TK + } + + reconcile(model, options) { + // TK + } +} + +PivotCacheRecordsXform.PIVOT_CACHE_RECORDS_ATTRIBUTES = { + xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', + 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', + 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006', + 'mc:Ignorable': 'xr', + 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision', +}; + +module.exports = PivotCacheRecordsXform; diff --git a/lib/xlsx/xform/pivot-table/pivot-table-xform.js b/lib/xlsx/xform/pivot-table/pivot-table-xform.js new file mode 100644 index 000000000..56db810cd --- /dev/null +++ b/lib/xlsx/xform/pivot-table/pivot-table-xform.js @@ -0,0 +1,189 @@ +const XmlStream = require('../../../utils/xml-stream'); +const BaseXform = require('../base-xform'); + +class PivotTableXform extends BaseXform { + constructor() { + super(); + + this.map = {}; + } + + prepare(model) { + // TK + } + + get tag() { + // http://www.datypic.com/sc/ooxml/e-ssml_pivotTableDefinition.html + return 'pivotTableDefinition'; + } + + render(xmlStream, model) { + // eslint-disable-next-line no-unused-vars + const {rows, columns, values, metric, cacheFields, cacheId} = model; + + // Examples + // -------- + // rows: [0, 1], // only 2 items possible for now + // columns: [2], // only 1 item possible for now + // values: [4], // only 1 item possible for now + // metric: 'sum', // only 'sum' possible for now + // + // the numbers are indices into `cacheFields`. + + xmlStream.openXml(XmlStream.StdDocAttributes); + xmlStream.openNode(this.tag, { + ...PivotTableXform.PIVOT_TABLE_ATTRIBUTES, + 'xr:uid': '{267EE50F-B116-784D-8DC2-BA77DE3F4F4A}', + name: 'PivotTable2', + cacheId, + applyNumberFormats: '0', + applyBorderFormats: '0', + applyFontFormats: '0', + applyPatternFormats: '0', + applyAlignmentFormats: '0', + applyWidthHeightFormats: '1', + dataCaption: 'Values', + updatedVersion: '8', + minRefreshableVersion: '3', + useAutoFormatting: '1', + itemPrintTitles: '1', + createdVersion: '8', + indent: '0', + compact: '0', + compactData: '0', + multipleFieldFilters: '0', + }); + + // Note: keeping this pretty-printed and verbose for now to ease debugging. + // + // location: ref="A3:E15" + // pivotFields + // rowFields and rowItems + // colFields and colItems + // dataFields + // pivotTableStyleInfo + xmlStream.writeXml(` + + + ${renderPivotFields(model)} + + + ${rows.map(rowIndex => ``).join('\n ')} + + + + + + ${columns.map(columnIndex => ``).join('\n ')} + + + + + + + + + + + + + + + + + `); + + xmlStream.closeNode(); + } + + parseOpen(node) { + // TK + } + + parseText(text) { + // TK + } + + parseClose(name) { + // TK + } + + reconcile(model, options) { + // TK + } +} + +// Helpers + +function renderPivotFields(pivotTable) { + /* eslint-disable no-nested-ternary */ + return pivotTable.cacheFields + .map((cacheField, fieldIndex) => { + const fieldType = + pivotTable.rows.indexOf(fieldIndex) >= 0 + ? 'row' + : pivotTable.columns.indexOf(fieldIndex) >= 0 + ? 'column' + : pivotTable.values.indexOf(fieldIndex) >= 0 + ? 'value' + : null; + return renderPivotField(fieldType, cacheField.sharedItems); + }) + .join(''); +} + +function renderPivotField(fieldType, sharedItems) { + // fieldType: 'row', 'column', 'value', null + + const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"'; + + if (fieldType === 'row' || fieldType === 'column') { + const axis = fieldType === 'row' ? 'axisRow' : 'axisCol'; + return ` + + + ${sharedItems.map((item, index) => ``).join('\n ')} + + + `; + } + return ` + + `; +} + +PivotTableXform.PIVOT_TABLE_ATTRIBUTES = { + xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', + 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006', + 'mc:Ignorable': 'xr', + 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision', +}; + +module.exports = PivotTableXform; diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index b38930042..f1fd59580 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -280,6 +280,15 @@ class WorkSheetXform extends BaseXform { }); }); + // prepare pivot tables + if ((model.pivotTables || []).length) { + rels.push({ + Id: nextRid(rels), + Type: RelType.PivotTable, + Target: '../pivotTables/pivotTable1.xml', + }); + } + // prepare ext items this.map.extLst.prepare(model, options); } diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js index ff2d9a117..ab62797ec 100644 --- a/lib/xlsx/xlsx.js +++ b/lib/xlsx/xlsx.js @@ -19,6 +19,9 @@ const WorkbookXform = require('./xform/book/workbook-xform'); const WorksheetXform = require('./xform/sheet/worksheet-xform'); const DrawingXform = require('./xform/drawing/drawing-xform'); const TableXform = require('./xform/table/table-xform'); +const PivotCacheRecordsXform = require('./xform/pivot-table/pivot-cache-records-xform'); +const PivotCacheDefinitionXform = require('./xform/pivot-table/pivot-cache-definition-xform'); +const PivotTableXform = require('./xform/pivot-table/pivot-table-xform'); const CommentsXform = require('./xform/comment/comments-xform'); const VmlNotesXform = require('./xform/comment/vml-notes-xform'); @@ -471,6 +474,71 @@ class XLSX { }); } + addPivotTables(zip, model) { + if (!model.pivotTables.length) return; + + const pivotTable = model.pivotTables[0]; + + const pivotCacheRecordsXform = new PivotCacheRecordsXform(); + const pivotCacheDefinitionXform = new PivotCacheDefinitionXform(); + const pivotTableXform = new PivotTableXform(); + const relsXform = new RelationshipsXform(); + + // pivot cache records + // -------------------------------------------------- + // copy of the source data. + // + // Note: cells in the columns of the source data which are part + // of the "rows" or "columns" of the pivot table configuration are + // replaced by references to their __cache field__ identifiers. + // See "pivot cache definition" below. + + let xml = pivotCacheRecordsXform.toXml(pivotTable); + zip.append(xml, {name: 'xl/pivotCache/pivotCacheRecords1.xml'}); + + // pivot cache definition + // -------------------------------------------------- + // cache source (source data): + // ref="A1:E7" on sheet="Sheet1" + // cache fields: + // - 0: "A" (a1, a2, a3) + // - 1: "B" (b1, b2) + // - ... + + xml = pivotCacheDefinitionXform.toXml(pivotTable); + zip.append(xml, {name: 'xl/pivotCache/pivotCacheDefinition1.xml'}); + + xml = relsXform.toXml([ + { + Id: 'rId1', + Type: XLSX.RelType.PivotCacheRecords, + Target: 'pivotCacheRecords1.xml', + }, + ]); + zip.append(xml, {name: 'xl/pivotCache/_rels/pivotCacheDefinition1.xml.rels'}); + + // pivot tables (on destination worksheet) + // -------------------------------------------------- + // location: ref="A3:E15" + // pivotFields + // rowFields and rowItems + // colFields and colItems + // dataFields + // pivotTableStyleInfo + + xml = pivotTableXform.toXml(pivotTable); + zip.append(xml, {name: 'xl/pivotTables/pivotTable1.xml'}); + + xml = relsXform.toXml([ + { + Id: 'rId1', + Type: XLSX.RelType.PivotCacheDefinition, + Target: '../pivotCache/pivotCacheDefinition1.xml', + }, + ]); + zip.append(xml, {name: 'xl/pivotTables/_rels/pivotTable1.xml.rels'}); + } + async addContentTypes(zip, model) { const xform = new ContentTypesXform(); const xml = xform.toXml(model); @@ -520,6 +588,15 @@ class XLSX { Target: 'sharedStrings.xml', }); } + if ((model.pivotTables || []).length) { + const pivotTable = model.pivotTables[0]; + pivotTable.rId = `rId${count++}`; + relationships.push({ + Id: pivotTable.rId, + Type: XLSX.RelType.PivotCacheDefinition, + Target: 'pivotCache/pivotCacheDefinition1.xml', + }); + } model.worksheets.forEach(worksheet => { worksheet.rId = `rId${count++}`; relationships.push({ @@ -656,6 +733,7 @@ class XLSX { await this.addSharedStrings(zip, model); // always after worksheets await this.addDrawings(zip, model); await this.addTables(zip, model); + await this.addPivotTables(zip, model); await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]); await this.addMedia(zip, model); await Promise.all([this.addApp(zip, model), this.addCore(zip, model)]); @@ -674,11 +752,13 @@ class XLSX { reject(error); }); - this.write(stream, options).then(() => { - stream.end(); - }).catch(err=>{ - reject(err); - }); + this.write(stream, options) + .then(() => { + stream.end(); + }) + .catch(err => { + reject(err); + }); }); } diff --git a/spec/integration/workbook/pivot-tables.spec.js b/spec/integration/workbook/pivot-tables.spec.js new file mode 100644 index 000000000..30758f8f2 --- /dev/null +++ b/spec/integration/workbook/pivot-tables.spec.js @@ -0,0 +1,78 @@ +// *Note*: `fs.promises` not supported before Node.js 11.14.0; +// ExcelJS version range '>=8.3.0' (as of 2023-10-08). +const fs = require('fs'); +const {promisify} = require('util'); + +const fsReadFileAsync = promisify(fs.readFile); + +const JSZip = require('jszip'); + +const ExcelJS = verquire('exceljs'); + +const PIVOT_TABLE_FILEPATHS = [ + 'xl/pivotCache/pivotCacheRecords1.xml', + 'xl/pivotCache/pivotCacheDefinition1.xml', + 'xl/pivotCache/_rels/pivotCacheDefinition1.xml.rels', + 'xl/pivotTables/pivotTable1.xml', + 'xl/pivotTables/_rels/pivotTable1.xml.rels', +]; + +const TEST_XLSX_FILEPATH = './spec/out/wb.test.xlsx'; + +const TEST_DATA = [ + ['A', 'B', 'C', 'D', 'E'], + ['a1', 'b1', 'c1', 4, 5], + ['a1', 'b2', 'c1', 4, 5], + ['a2', 'b1', 'c2', 14, 24], + ['a2', 'b2', 'c2', 24, 35], + ['a3', 'b1', 'c3', 34, 45], + ['a3', 'b2', 'c3', 44, 45], +]; + +// ============================================================================= +// Tests + +describe('Workbook', () => { + describe('Pivot Tables', () => { + it('if pivot table added, then certain xml and rels files are added', async () => { + const workbook = new ExcelJS.Workbook(); + + const worksheet1 = workbook.addWorksheet('Sheet1'); + worksheet1.addRows(TEST_DATA); + + const worksheet2 = workbook.addWorksheet('Sheet2'); + worksheet2.addPivotTable({ + sourceSheet: worksheet1, + rows: ['A', 'B'], + columns: ['C'], + values: ['E'], + metric: 'sum', + }); + + return workbook.xlsx.writeFile(TEST_XLSX_FILEPATH).then(async () => { + const buffer = await fsReadFileAsync(TEST_XLSX_FILEPATH); + const zip = await JSZip.loadAsync(buffer); + for (const filepath of PIVOT_TABLE_FILEPATHS) { + expect(zip.files[filepath]).to.not.be.undefined(); + } + }); + }); + + it('if pivot table NOT added, then certain xml and rels files are not added', () => { + const workbook = new ExcelJS.Workbook(); + + const worksheet1 = workbook.addWorksheet('Sheet1'); + worksheet1.addRows(TEST_DATA); + + workbook.addWorksheet('Sheet2'); + + return workbook.xlsx.writeFile(TEST_XLSX_FILEPATH).then(async () => { + const buffer = await fsReadFileAsync(TEST_XLSX_FILEPATH); + const zip = await JSZip.loadAsync(buffer); + for (const filepath of PIVOT_TABLE_FILEPATHS) { + expect(zip.files[filepath]).to.be.undefined(); + } + }); + }); + }); +}); diff --git a/test/test-pivot-table.js b/test/test-pivot-table.js new file mode 100644 index 000000000..fa48f14ec --- /dev/null +++ b/test/test-pivot-table.js @@ -0,0 +1,54 @@ +// -------------------------------------------------- +// This enables the generation of a XLSX pivot table +// with several restrictions +// +// Last updated: 2023-10-19 +// -------------------------------------------------- +/* eslint-disable */ + +function main(filepath) { + const Excel = require('../lib/exceljs.nodejs.js'); + + const workbook = new Excel.Workbook(); + + const worksheet1 = workbook.addWorksheet('Sheet1'); + worksheet1.addRows([ + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'], + ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 4, 5], + ['a1', 'b2', 'c1', 'd2', 'e1', 'f1', 4, 5], + ['a2', 'b1', 'c2', 'd1', 'e2', 'f1', 14, 24], + ['a2', 'b2', 'c2', 'd2', 'e2', 'f2', 24, 35], + ['a3', 'b1', 'c3', 'd1', 'e3', 'f2', 34, 45], + ['a3', 'b2', 'c3', 'd2', 'e3', 'f2', 44, 45], + ]); + + const worksheet2 = workbook.addWorksheet('Sheet2'); + worksheet2.addPivotTable({ + // Source of data: the entire sheet range is taken; + // akin to `worksheet1.getSheetValues()`. + sourceSheet: worksheet1, + // Pivot table fields: values indicate field names; + // they come from the first row in `worksheet1`. + rows: ['A', 'B', 'E'], + columns: ['C', 'D'], + values: ['H'], // only 1 item possible for now + metric: 'sum', // only 'sum' possible for now + }); + + save(workbook, filepath); +} + +function save(workbook, filepath) { + const HrStopwatch = require('./utils/hr-stopwatch'); + const stopwatch = new HrStopwatch(); + stopwatch.start(); + + workbook.xlsx.writeFile(filepath).then(() => { + const microseconds = stopwatch.microseconds; + console.log('Done.'); + console.log('Time taken:', microseconds); + }); +} + +const [, , filepath] = process.argv; +main(filepath); From 5bed18b45e824f409b08456b59b87430ded023ab Mon Sep 17 00:00:00 2001 From: Jamie Buck <131281239+jbuck-lineleap@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:48:37 -0500 Subject: [PATCH 46/46] fix: table column style bug and addTable colum type (#2649) * fix table column style bug and addTable colum type * make TableColumnProperties.style optional --- index.d.ts | 5 +++++ lib/doc/table.js | 2 +- test/test-table.js | 8 ++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 81e65fec4..af7524ca7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1832,6 +1832,11 @@ export interface TableColumnProperties { * Optional formula for custom functions */ totalsRowFormula?: string; + + /** + * Styles applied to the column + */ + style?: Partial