From f75811ccf4fbdf2e24d8503a151e8891e9c252c9 Mon Sep 17 00:00:00 2001 From: Carlos Beltran Date: Wed, 15 Jan 2020 15:11:39 -0500 Subject: [PATCH 01/12] function duplicateRows added --- lib/doc/worksheet.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index db600d7a6..f06032cec 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -344,6 +344,49 @@ class Worksheet { }); } + duplicateRow(start, count) { + //I want to add after the start row + start++; + + const nKeep = start; + const nEnd = this._rows.length; + let i; + let rSrc; + + // insert new cells + for (i = nEnd; i >= nKeep; i--) { + rSrc = this._rows[i - 1]; + if (rSrc) { + const rDst = this.getRow(i + count); + rDst.values = rSrc.values; + rDst.style = rSrc.style; + // eslint-disable-next-line no-loop-func + rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => { + rDst.getCell(colNumber).style = cell.style; + }); + } + else { + this._rows[i + count - 1] = undefined; + } + } + + //Reference to the original row + rSrc = this._rows[start-2]; + + // now copy over the new values and styles + for (i = 0; i < count; i++) { + const rDst = this.getRow(start + i); + rDst.values = rSrc.values + rDst.style = rSrc.style; + rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => { + rDst.getCell(colNumber).style = cell.style; + }); + } + + // account for defined names + this.workbook.definedNames.spliceRows(this.name, start, 0, count); + } + spliceRows(start, count) { // same problem as row.splice, except worse. const inserts = Array.prototype.slice.call(arguments, 2); From 0ad34778b2bb29bdfd6fc632e8249163518c8c21 Mon Sep 17 00:00:00 2001 From: Carlos Beltran Date: Sat, 18 Jan 2020 13:43:45 -0500 Subject: [PATCH 02/12] Eslint changes --- lib/doc/worksheet.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index f06032cec..c74c9222c 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -345,7 +345,7 @@ class Worksheet { } duplicateRow(start, count) { - //I want to add after the start row + // I want to add after the start row start++; const nKeep = start; @@ -370,13 +370,13 @@ class Worksheet { } } - //Reference to the original row + // Reference to the original row rSrc = this._rows[start-2]; // now copy over the new values and styles for (i = 0; i < count; i++) { const rDst = this.getRow(start + i); - rDst.values = rSrc.values + rDst.values = rSrc.values; rDst.style = rSrc.style; rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => { rDst.getCell(colNumber).style = cell.style; From ecc0012f7b9a72c15f14397a20dc51290be3ca80 Mon Sep 17 00:00:00 2001 From: Alexander Pruss Date: Mon, 2 Dec 2019 15:38:13 +0100 Subject: [PATCH 03/12] 1041-Allowing multiple print areas to be parsed --- lib/xlsx/xform/book/workbook-xform.js | 19 +++++++------ test/testMultiplePrintAreaOut.js | 40 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 test/testMultiplePrintAreaOut.js diff --git a/lib/xlsx/xform/book/workbook-xform.js b/lib/xlsx/xform/book/workbook-xform.js index 9db20c626..a21fa9ebf 100644 --- a/lib/xlsx/xform/book/workbook-xform.js +++ b/lib/xlsx/xform/book/workbook-xform.js @@ -34,14 +34,17 @@ class WorkbookXform extends BaseXform { let index = 0; // sheets is sparse array - calc index manually model.sheets.forEach(sheet => { if (sheet.pageSetup && sheet.pageSetup.printArea) { - const printArea = sheet.pageSetup.printArea.split(':'); - const definedName = { - name: '_xlnm.Print_Area', - ranges: [`'${sheet.name}'!$${printArea[0]}:$${printArea[1]}`], - localSheetId: index, - }; - printAreas.push(definedName); - } + sheet.pageSetup.printArea.split('&&').forEach(printArea => { + const printAreaComponents = printArea.split(':'); + const definedName = { + name: '_xlnm.Print_Area', + ranges: [`'${sheet.name}'!$${printAreaComponents[0]}:$${printAreaComponents[1]}`], + localSheetId: index, + }; + printAreas.push(definedName); + }); + } + if (sheet.pageSetup && (sheet.pageSetup.printTitlesRow || sheet.pageSetup.printTitlesColumn)) { const ranges = []; diff --git a/test/testMultiplePrintAreaOut.js b/test/testMultiplePrintAreaOut.js new file mode 100644 index 000000000..b6728f051 --- /dev/null +++ b/test/testMultiplePrintAreaOut.js @@ -0,0 +1,40 @@ +const Excel = require('../excel'); + +const {Workbook} = Excel; + +const [, , filename] = process.argv; + +const wb = new Workbook(); +const ws = wb.addWorksheet('test sheet'); + +for (let row = 1; row <= 10; row++) { + const values = []; + if (row === 1) { + values.push(''); + for (let col = 2; col <= 10; col++) { + values.push(`Col ${col}`); + } + } else { + for (let col = 1; col <= 10; col++) { + if (col === 1) { + values.push(`Row ${row}`); + } else { + values.push(`${row}-${col}`); + } + } + } + ws.addRow(values); +} + +ws.pageSetup.printTitlesColumn = 'A:A'; +ws.pageSetup.printTitlesRow = '1:1'; +ws.pageSetup.printArea = 'A1:B5&&A6:B10'; + +wb.xlsx + .writeFile(filename) + .then(() => { + console.log('Done.'); + }) + .catch(error => { + console.log(error.message); + }); From 86b8ef15ea8cb5e63ee9f9f54379c3d668f507e4 Mon Sep 17 00:00:00 2001 From: Alexander Pruss Date: Mon, 2 Dec 2019 15:53:05 +0100 Subject: [PATCH 04/12] 1041: Updating Readme with documentation for multiple print areas. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7391ee416..90cd7a840 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,9 @@ worksheet.pageSetup.margins = { // Set Print Area for a sheet worksheet.pageSetup.printArea = 'A1:G20'; +// Set multiple Print Areas by separating print areas with '&&' +worksheet.pageSetup.printArea = 'A1:G10&&A11:G20'; + // Repeat specific rows on every printed page worksheet.pageSetup.printTitlesRow = '1:3'; From 8bf10116318da1549a37668286765631f5241177 Mon Sep 17 00:00:00 2001 From: Guyon Date: Wed, 15 Jan 2020 08:00:35 +0000 Subject: [PATCH 05/12] adding Ext conditional formattings --- .eslintrc | 3 +- lib/doc/table.js | 1 - lib/xlsx/xform/base-xform.js | 48 ++- lib/xlsx/xform/book/workbook-xform.js | 4 +- lib/xlsx/xform/composite-xform.js | 75 ++-- .../xform/sheet/cf-ext/cf-icon-ext-xform.js | 27 ++ .../xform/sheet/cf-ext/cf-rule-ext-xform.js | 98 +++++ lib/xlsx/xform/sheet/cf-ext/cfvo-ext-xform.js | 41 ++ .../conditional-formatting-ext-xform.js | 64 +++ .../conditional-formattings-ext-xform.js | 52 +++ .../xform/sheet/cf-ext/databar-ext-xform.js | 82 ++++ lib/xlsx/xform/sheet/cf-ext/f-ext-xform.js | 25 ++ .../xform/sheet/cf-ext/icon-set-ext-xform.js | 73 ++++ .../xform/sheet/cf-ext/sqref-ext-xform.js | 25 ++ lib/xlsx/xform/sheet/cf/cf-rule-xform.js | 106 ++--- lib/xlsx/xform/sheet/cf/color-scale-xform.js | 48 +-- .../cf/conditional-formatting-ext-xform.js | 383 ------------------ .../sheet/cf/conditional-formatting-xform.js | 42 +- .../cf/conditional-formattings-ext-xform.js | 34 -- .../sheet/cf/conditional-formattings-xform.js | 5 - lib/xlsx/xform/sheet/cf/databar-xform.js | 56 +-- lib/xlsx/xform/sheet/cf/ext-lst-ref-xform.js | 57 +-- lib/xlsx/xform/sheet/cf/icon-set-xform.js | 51 +-- lib/xlsx/xform/sheet/ext-lst-xform.js | 103 ++--- lib/xlsx/xform/sheet/worksheet-xform.js | 70 +++- spec/unit/utils/encryptor.spec.js | 11 - .../xform/book/defined-name-xform.spec.js | 3 +- test/test-cf.js | 97 ++++- 28 files changed, 860 insertions(+), 824 deletions(-) create mode 100644 lib/xlsx/xform/sheet/cf-ext/cf-icon-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/cfvo-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/conditional-formatting-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/conditional-formattings-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/databar-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/f-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/icon-set-ext-xform.js create mode 100644 lib/xlsx/xform/sheet/cf-ext/sqref-ext-xform.js delete mode 100644 lib/xlsx/xform/sheet/cf/conditional-formatting-ext-xform.js delete mode 100644 lib/xlsx/xform/sheet/cf/conditional-formattings-ext-xform.js diff --git a/.eslintrc b/.eslintrc index 5d8eef9e0..6c319e8da 100644 --- a/.eslintrc +++ b/.eslintrc @@ -39,6 +39,7 @@ "default-case": ["off"], "object-curly-spacing": ["error", "never"], "node/process-exit-as-throw": ["off"], - "prefer-object-spread": ["off"] + "prefer-object-spread": ["off"], + "no-unused-vars": ["error", {"vars": "all", "args": "none", "ignoreRestSiblings": true}] } } diff --git a/lib/doc/table.js b/lib/doc/table.js index 04d18c312..d5710e244 100644 --- a/lib/doc/table.js +++ b/lib/doc/table.js @@ -306,7 +306,6 @@ class Table { this.store(); } - /* eslint-disable no-unused-vars */ addRow(values, rowNumber) { // Add a row of data, either insert at rowNumber or append this.cacheState(); diff --git a/lib/xlsx/xform/base-xform.js b/lib/xlsx/xform/base-xform.js index 4e75dcc08..22c79ee31 100644 --- a/lib/xlsx/xform/base-xform.js +++ b/lib/xlsx/xform/base-xform.js @@ -19,19 +19,19 @@ class BaseXform { // convert model to xml } - parseOpen(/* node */) { - // Sax Open Node event + parseOpen(node) { + // XML node opened } - parseText(/* node */) { - // Sax Text event + parseText(text) { + // chunk of text encountered for current node } - parseClose(/* name */) { - // Sax Close Node event + parseClose(name) { + // XML node closed } - reconcile(/* model, options */) { + reconcile(model, options) { // optional post-parse step (opposite to prepare) } @@ -69,6 +69,7 @@ class BaseXform { parser.on('opentag', node => { try { + // console.log('opentag', node.name); this.parseOpen(node); } catch (error) { abort(error); @@ -83,6 +84,7 @@ class BaseXform { }); parser.on('closetag', name => { try { + // console.log('closetag', name); if (!this.parseClose(name)) { resolve(this.model); } @@ -91,6 +93,7 @@ class BaseXform { } }); parser.on('end', () => { + // console.log('end'); resolve(this.model); }); parser.on('error', error => { @@ -121,27 +124,32 @@ class BaseXform { // ============================================================ // Useful Utilities - static toAttribute(value, dflt) { - if ((value !== undefined) && (value !== dflt)) { + static toAttribute(value, dflt, allways = false) { + if (value === undefined) { + if (allways) { + return dflt; + } + } else if (allways || (value !== dflt)) { return value.toString(); } return undefined; } - static toStringAttribute(value, dflt) { - if (value !== dflt) { - return value; - } - return undefined; + static toStringAttribute(value, dflt, allways = false) { + return BaseXform.toAttribute(value, dflt, allways); } static toStringValue(attr, dflt) { return (attr === undefined) ? dflt : attr; } - static toBoolAttribute(value, dflt) { - if ((value !== undefined) && (value !== dflt)) { + static toBoolAttribute(value, dflt, allways = false) { + if (value === undefined) { + if (allways) { + return dflt; + } + } else if (allways || (value !== dflt)) { return value ? '1' : '0'; } return undefined; @@ -151,16 +159,16 @@ class BaseXform { return (attr === undefined) ? dflt : (attr === '1'); } - static toIntAttribute(value, dflt) { - return BaseXform.toAttribute(value, dflt); + static toIntAttribute(value, dflt, allways = false) { + return BaseXform.toAttribute(value, dflt, allways); } static toIntValue(attr, dflt) { return (attr === undefined) ? dflt : parseInt(attr, 10); } - static toFloatAttribute(value, dflt) { - return BaseXform.toAttribute(value, dflt); + static toFloatAttribute(value, dflt, allways = false) { + return BaseXform.toAttribute(value, dflt, allways); } static toFloatValue(attr, dflt) { diff --git a/lib/xlsx/xform/book/workbook-xform.js b/lib/xlsx/xform/book/workbook-xform.js index a21fa9ebf..ae5a73e53 100644 --- a/lib/xlsx/xform/book/workbook-xform.js +++ b/lib/xlsx/xform/book/workbook-xform.js @@ -182,7 +182,9 @@ class WorkbookXform extends BaseXform { worksheet.pageSetup = {}; } const range = colCache.decodeEx(definedName.ranges[0]); - worksheet.pageSetup.printArea = range.dimensions; + worksheet.pageSetup.printArea = worksheet.pageSetup.printArea ? + `${worksheet.pageSetup.printArea}&&${range.dimensions}` : + range.dimensions; } } else if (definedName.name === '_xlnm.Print_Titles') { worksheet = worksheets[definedName.localSheetId]; diff --git a/lib/xlsx/xform/composite-xform.js b/lib/xlsx/xform/composite-xform.js index 2d5697141..6058dd310 100644 --- a/lib/xlsx/xform/composite-xform.js +++ b/lib/xlsx/xform/composite-xform.js @@ -1,76 +1,55 @@ const BaseXform = require('./base-xform'); -class CompositeXform extends BaseXform { - constructor(options) { - super(); - - this.tag = options.tag; - this.attrs = options.attrs; - this.children = options.children; - this.map = this.children.reduce((map, child) => { - const name = child.name || child.tag; - const tag = child.tag || child.name; - map[tag] = child; - child.name = name; - child.tag = tag; - return map; - }, {}); - } +/* 'virtual' methods used as a form of documentation */ +/* eslint-disable class-methods-use-this */ - prepare(model, options) { - this.children.forEach(child => { - child.xform.prepare(model[child.tag], options); - }); - } - - render(xmlStream, model) { - xmlStream.openNode(this.tag, this.attrs); - this.children.forEach(child => { - child.xform.render(xmlStream, model[child.name]); - }); - xmlStream.closeNode(); +// base class for xforms that are composed of other xforms +// offers some default implementations +class CompositeXform extends BaseXform { + createNewModel(node) { + return {}; } parseOpen(node) { + // Typical pattern for composite xform + this.parser = this.parser || this.map[node.name]; if (this.parser) { - this.parser.xform.parseOpen(node); + this.parser.parseOpen(node); return true; } - switch (node.name) { - case this.tag: - this.model = {}; - return true; - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.xform.parseOpen(node); - return true; - } + + if (node.name === this.tag) { + this.model = this.createNewModel(node); + return true; } + return false; } parseText(text) { + // Default implementation. Send text to child parser if (this.parser) { - this.parser.xform.parseText(text); + this.parser.parseText(text); } } + onParserClose(name, parser) { + // parseClose has seen a child parser close + // now need to incorporate into this.model somehow + this.model[name] = parser.model; + } + parseClose(name) { + // Default implementation if (this.parser) { - if (!this.parser.xform.parseClose(name)) { - this.model[this.parser.name] = this.parser.xform.model; + if (!this.parser.parseClose(name)) { + this.onParserClose(name, this.parser); this.parser = undefined; } return true; } - return false; - } - reconcile(model, options) { - this.children.forEach(child => { - child.xform.prepare(model[child.tag], options); - }); + return (name !== this.tag); } } diff --git a/lib/xlsx/xform/sheet/cf-ext/cf-icon-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/cf-icon-ext-xform.js new file mode 100644 index 000000000..f6f223337 --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/cf-icon-ext-xform.js @@ -0,0 +1,27 @@ +const BaseXform = require('../../base-xform'); + +class CfIconExtXform extends BaseXform { + get tag() { + return 'x14:cfIcon'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + iconSet: model.iconSet, + iconId: model.iconId, + }); + } + + parseOpen({attributes}) { + this.model = { + iconSet: attributes.iconSet, + iconId: BaseXform.toIntValue(attributes.iconId), + }; + } + + parseClose(name) { + return name !== this.tag; + } +} + +module.exports = CfIconExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js new file mode 100644 index 000000000..d6919f8e3 --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js @@ -0,0 +1,98 @@ +const uuid = require('uuid'); +const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); + +const DatabarExtXform = require('./databar-ext-xform'); +const IconSetExtXform = require('./icon-set-ext-xform'); + +const extIcons = { + '3Triangles': true, + '3Stars': true, + '5Boxes': true, +}; + +class CfRuleExtXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'x14:dataBar': this.databarXform = new DatabarExtXform(), + 'x14:iconSet': this.iconSetXform = new IconSetExtXform(), + }; + } + + get tag() { + return 'x14:cfRule'; + } + + static isExt(rule) { + // is this rule primitive? + if (rule.type === 'dataBar') { + return DatabarExtXform.isExt(rule); + } + if (rule.type === 'iconSet') { + if (rule.custom || extIcons[rule.iconSet]) { + return true; + } + } + return false; + } + + prepare(model) { + if (CfRuleExtXform.isExt(model)) { + model.x14Id = `{${uuid.v4()}}`.toUpperCase(); + } + } + + render(xmlStream, model) { + if (!CfRuleExtXform.isExt(model)) { + return; + } + + switch (model.type) { + case 'dataBar': + this.renderDataBar(xmlStream, model); + break; + case 'iconSet': + this.renderIconSet(xmlStream, model); + break; + } + } + + renderDataBar(xmlStream, model) { + xmlStream.openNode(this.tag, { + type: 'dataBar', + id: model.x14Id, + }); + + this.databarXform.render(xmlStream, model); + + xmlStream.closeNode(); + } + + renderIconSet(xmlStream, model) { + xmlStream.openNode(this.tag, { + type: 'iconSet', + priority: model.priority, + id: `{${uuid.v4()}}`, + }); + + this.iconSetXform.render(xmlStream, model); + + xmlStream.closeNode(); + } + + createNewModel({attributes}) { + return { + type: attributes.type, + x14Id: attributes.id, + priority: BaseXform.toIntValue(attributes.priority), + }; + } + + onParserClose(name, parser) { + Object.assign(this.model, parser.model); + } +} + +module.exports = CfRuleExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/cfvo-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/cfvo-ext-xform.js new file mode 100644 index 000000000..47bef0519 --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/cfvo-ext-xform.js @@ -0,0 +1,41 @@ +const CompositeXform = require('../../composite-xform'); + +const FExtXform = require('./f-ext-xform'); + +class CfvoExtXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'xm:f': this.fExtXform = new FExtXform(), + }; + } + + get tag() { + return 'x14:cfvo'; + } + + render(xmlStream, model) { + xmlStream.openNode(this.tag, { + type: model.type, + }); + this.fExtXform.render(xmlStream, model.value); + xmlStream.closeNode(); + } + + createNewModel(node) { + return { + type: node.attributes.type, + }; + } + + onParserClose(name, parser) { + switch (name) { + case 'xm:f': + this.model.value = parser.model ? parseFloat(parser.model) : 0; + break; + } + } +} + +module.exports = CfvoExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/conditional-formatting-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/conditional-formatting-ext-xform.js new file mode 100644 index 000000000..d37a683ae --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/conditional-formatting-ext-xform.js @@ -0,0 +1,64 @@ +const CompositeXform = require('../../composite-xform'); + +const SqRefExtXform = require('./sqref-ext-xform'); +const CfRuleExtXform = require('./cf-rule-ext-xform'); + +class ConditionalFormattingExtXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'xm:sqref': this.sqRef = new SqRefExtXform(), + 'x14:cfRule': this.cfRule = new CfRuleExtXform(), + }; + } + + get tag() { + return 'x14:conditionalFormatting'; + } + + prepare(model, options) { + model.rules.forEach(rule =>{ + this.cfRule.prepare(rule, options); + }); + } + + render(xmlStream, model) { + if (!model.rules.some(CfRuleExtXform.isExt)) { + return; + } + + xmlStream.openNode(this.tag, { + 'xmlns:xm': 'http://schemas.microsoft.com/office/excel/2006/main', + }); + + model.rules + .filter(CfRuleExtXform.isExt) + .forEach(rule => this.cfRule.render(xmlStream, rule)); + + // for some odd reason, Excel needs the node to be after the rules + this.sqRef.render(xmlStream, model.ref); + + xmlStream.closeNode(); + } + + createNewModel() { + return { + rules: [], + }; + } + + onParserClose(name, parser) { + switch (name) { + case 'xm:sqref': + this.model.ref = parser.model; + break; + + case 'x14:cfRule': + this.model.rules.push(parser.model); + break; + } + } +} + +module.exports = ConditionalFormattingExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/conditional-formattings-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/conditional-formattings-ext-xform.js new file mode 100644 index 000000000..2d0a940e4 --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/conditional-formattings-ext-xform.js @@ -0,0 +1,52 @@ +const CompositeXform = require('../../composite-xform'); + +const CfRuleExtXform = require('./cf-rule-ext-xform'); +const ConditionalFormattingExtXform = require('./conditional-formatting-ext-xform'); + +class ConditionalFormattingsExtXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'x14:conditionalFormatting': this.cfXform = new ConditionalFormattingExtXform(), + }; + } + + get tag() { + return 'x14:conditionalFormattings'; + } + + hasContent(model) { + if (model.hasExtContent === undefined) { + model.hasExtContent = model.some( + cf => cf.rules.some(CfRuleExtXform.isExt) + ); + } + return model.hasExtContent; + } + + prepare(model, options) { + model.forEach(cf =>{ + this.cfXform.prepare(cf, options); + }); + } + + render(xmlStream, model) { + if (this.hasContent(model)) { + xmlStream.openNode(this.tag); + model.forEach(cf => this.cfXform.render(xmlStream, cf)); + xmlStream.closeNode(); + } + } + + createNewModel() { + return []; + } + + onParserClose(name, parser) { + // model is array of conditional formatting objects + this.model.push(parser.model); + } +} + +module.exports = ConditionalFormattingsExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/databar-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/databar-ext-xform.js new file mode 100644 index 000000000..d01ee926d --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/databar-ext-xform.js @@ -0,0 +1,82 @@ +const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); + +const ColorXform = require('../../style/color-xform'); +const CfvoExtXform = require('./cfvo-ext-xform'); + +class DatabarExtXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'x14:cfvo': this.cfvoXform = new CfvoExtXform(), + 'x14:borderColor': this.borderColorXform = new ColorXform('x14:borderColor'), + 'x14:negativeBorderColor': this.negativeBorderColorXform = new ColorXform('x14:negativeBorderColor'), + 'x14:negativeFillColor': this.negativeFillColorXform = new ColorXform('x14:negativeFillColor'), + 'x14:axisColor': this.axisColorXform = new ColorXform('x14:axisColor'), + }; + } + + static isExt(rule) { + // not all databars need ext + // TODO: refine this + return !rule.gradient; + } + + get tag() { + return 'x14:dataBar'; + } + + render(xmlStream, model) { + xmlStream.openNode(this.tag, { + minLength: BaseXform.toIntAttribute(model.minLength, 0, true), + maxLength: BaseXform.toIntAttribute(model.maxLength, 100, true), + border: BaseXform.toBoolAttribute(model.border, false), + gradient: BaseXform.toBoolAttribute(model.gradient, true), + negativeBarColorSameAsPositive: BaseXform.toBoolAttribute(model.negativeBarColorSameAsPositive, true), + negativeBarBorderColorSameAsPositive: BaseXform.toBoolAttribute(model.negativeBarBorderColorSameAsPositive, true), + axisPosition: BaseXform.toAttribute(model.axisPosition, 'auto'), + direction: BaseXform.toAttribute(model.direction, 'leftToRight'), + }); + + model.cfvo.forEach(cfvo => { + this.cfvoXform.render(xmlStream, cfvo); + }); + + this.borderColorXform.render(xmlStream, model.borderColor); + this.negativeBorderColorXform.render(xmlStream, model.negativeBorderColor); + this.negativeFillColorXform.render(xmlStream, model.negativeFillColor); + this.axisColorXform.render(xmlStream, model.axisColor); + + xmlStream.closeNode(); + } + + createNewModel({attributes}) { + return { + cfvo: [], + minLength: BaseXform.toIntValue(attributes.minLength, 0), + maxLength: BaseXform.toIntValue(attributes.maxLength, 100), + border: BaseXform.toBoolValue(attributes.border, false), + gradient: BaseXform.toBoolValue(attributes.gradient, true), + negativeBarColorSameAsPositive: BaseXform.toBoolValue(attributes.negativeBarColorSameAsPositive, true), + negativeBarBorderColorSameAsPositive: BaseXform.toBoolValue(attributes.negativeBarBorderColorSameAsPositive, true), + axisPosition: BaseXform.toStringValue(attributes.axisPosition, 'auto'), + direction: BaseXform.toStringValue(attributes.direction, 'leftToRight'), + }; + } + + onParserClose(name, parser) { + const [,prop] = name.split(':'); + switch (prop) { + case 'cfvo': + this.model.cfvo.push(parser.model); + break; + + default: + this.model[prop] = parser.model; + break; + } + } +} + +module.exports = DatabarExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/f-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/f-ext-xform.js new file mode 100644 index 000000000..90509c734 --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/f-ext-xform.js @@ -0,0 +1,25 @@ +const BaseXform = require('../../base-xform'); + +class FExtXform extends BaseXform { + get tag() { + return 'xm:f'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, null, model); + } + + parseOpen() { + this.model = ''; + } + + parseText(text) { + this.model += text; + } + + parseClose(name) { + return name !== this.tag; + } +} + +module.exports = FExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/icon-set-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/icon-set-ext-xform.js new file mode 100644 index 000000000..71c73f79a --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/icon-set-ext-xform.js @@ -0,0 +1,73 @@ +const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); + +const CfvoExtXform = require('./cfvo-ext-xform'); +const CfIconExtXform = require('./cf-icon-ext-xform'); + +class IconSetExtXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'x14:cfvo': this.cfvoXform = new CfvoExtXform(), + 'x14:cfIcon': this.cfIconXform = new CfIconExtXform(), + }; + } + + get tag() { + return 'x14:iconSet'; + } + + render(xmlStream, model) { + xmlStream.openNode(this.tag, { + iconSet: BaseXform.toStringAttribute(model.iconSet, '3TrafficLights'), + reverse: BaseXform.toBoolAttribute(model.reverse, false), + showValue: BaseXform.toBoolAttribute(model.showValue, true), + custom: BaseXform.toBoolAttribute(model.icons, false), + }); + + model.cfvo.forEach(cfvo => { + this.cfvoXform.render(xmlStream, cfvo); + }); + + if (model.icons) { + model.icons.forEach((icon, i) => { + icon.iconId = i; + this.cfIconXform.render(xmlStream, icon); + }); + } + + xmlStream.closeNode(); + } + + createNewModel({attributes}) { + return { + cfvo: [], + iconSet: BaseXform.toStringValue(attributes.iconSet, '3TrafficLights'), + reverse: BaseXform.toBoolValue(attributes.reverse, false), + showValue: BaseXform.toBoolValue(attributes.showValue, true), + }; + } + + onParserClose(name, parser) { + const [,prop] = name.split(':'); + switch (prop) { + case 'cfvo': + this.model.cfvo.push(parser.model); + break; + + case 'cfIcon': + if (!this.model.icons) { + this.model.icons = []; + } + this.model.icons.push(parser.model); + break; + + default: + this.model[prop] = parser.model; + break; + } + } +} + +module.exports = IconSetExtXform; diff --git a/lib/xlsx/xform/sheet/cf-ext/sqref-ext-xform.js b/lib/xlsx/xform/sheet/cf-ext/sqref-ext-xform.js new file mode 100644 index 000000000..fce94073b --- /dev/null +++ b/lib/xlsx/xform/sheet/cf-ext/sqref-ext-xform.js @@ -0,0 +1,25 @@ +const BaseXform = require('../../base-xform'); + +class SqrefExtXform extends BaseXform { + get tag() { + return 'xm:sqref'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, null, model); + } + + parseOpen() { + this.model = ''; + } + + parseText(text) { + this.model += text; + } + + parseClose(name) { + return name !== this.tag; + } +} + +module.exports = SqrefExtXform; diff --git a/lib/xlsx/xform/sheet/cf/cf-rule-xform.js b/lib/xlsx/xform/sheet/cf/cf-rule-xform.js index d4d85ac4b..0667a6d31 100644 --- a/lib/xlsx/xform/sheet/cf/cf-rule-xform.js +++ b/lib/xlsx/xform/sheet/cf/cf-rule-xform.js @@ -1,5 +1,6 @@ -const uuid = require('uuid'); const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); + const Range = require('../../../../doc/range'); const DatabarXform = require('./databar-xform'); @@ -70,8 +71,8 @@ const getTimePeriodFormula = model => { } }; -const opType = attr => { - const {type, operator} = attr; +const opType = attributes => { + const {type, operator} = attributes; switch (type) { case 'containsText': case 'containsBlanks': @@ -88,13 +89,13 @@ const opType = attr => { } }; -class CfRuleXform extends BaseXform { +class CfRuleXform extends CompositeXform { constructor() { super(); this.map = { dataBar: this.databarXform = new DatabarXform(), - extLst: this.extLstXform = new ExtLstRefXform(), + extLst: this.extLstRefXform = new ExtLstRefXform(), formula: this.formulaXform = new FormulaXform(), colorScale: this.colorScaleXform = new ColorScaleXform(), iconSet: this.iconSetXform = new IconSetXform(), @@ -180,9 +181,9 @@ class CfRuleXform extends BaseXform { type: 'top10', dxfId: model.dxfId, priority: model.priority, - percent: model.percent ? '1' : undefined, - bottom: model.bottom ? '1' : undefined, - rank: model.rank || 10, + percent: BaseXform.toBoolAttribute(model.percent, false), + bottom: BaseXform.toBoolAttribute(model.bottom, false), + rank: BaseXform.toIntValue(model.rank, 10, true), }); } @@ -191,7 +192,7 @@ class CfRuleXform extends BaseXform { type: 'aboveAverage', dxfId: model.dxfId, priority: model.priority, - aboveAverage: (model.aboveAverage === false) ? '0' : undefined, + aboveAverage: BaseXform.toBoolAttribute(model.aboveAverage, true), }); } @@ -202,9 +203,7 @@ class CfRuleXform extends BaseXform { }); this.databarXform.render(xmlStream, model); - - model.x14Id = `{${uuid.v4()}}`; - this.extLstXform.render(xmlStream, model); + this.extLstRefXform.render(xmlStream, model); xmlStream.closeNode(); } @@ -241,7 +240,7 @@ class CfRuleXform extends BaseXform { type: model.operator, dxfId: model.dxfId, priority: model.priority, - operator: model.operator === 'containsText' ? model.operator : undefined, + operator: BaseXform.toStringAttribute(model.operator, 'containsText'), }); const formula = getTextFormula(model); @@ -268,66 +267,35 @@ class CfRuleXform extends BaseXform { xmlStream.closeNode(); } - parseOpen(node) { - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - - switch (node.name) { - case this.tag: - this.model = { - ...opType(node.attributes), - dxfId: BaseXform.toIntValue(node.attributes.dxfId), - priority: BaseXform.toIntValue(node.attributes.priority), - timePeriod: node.attributes.timePeriod, - percent: BaseXform.toBoolValue(node.attributes.percent), - bottom: BaseXform.toBoolValue(node.attributes.bottom), - rank: BaseXform.toIntValue(node.attributes.rank), - aboveAverage: BaseXform.toBoolValue(node.attributes.aboveAverage), - }; - return true; - - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - } - return false; + createNewModel({attributes}) { + return { + ...opType(attributes), + dxfId: BaseXform.toIntValue(attributes.dxfId), + priority: BaseXform.toIntValue(attributes.priority), + timePeriod: attributes.timePeriod, + percent: BaseXform.toBoolValue(attributes.percent), + bottom: BaseXform.toBoolValue(attributes.bottom), + rank: BaseXform.toIntValue(attributes.rank), + aboveAverage: BaseXform.toBoolValue(attributes.aboveAverage), + }; } - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } - } + onParserClose(name, parser) { + switch(name) { + case 'dataBar': + case 'extLst': + case 'colorScale': + case 'iconSet': + // merge parser model with ours + Object.assign(this.model, parser.model); + break; - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - switch(name) { - case 'dataBar': - case 'extLst': - case 'colorScale': - case 'iconSet': - // merge parser model with ours - Object.assign(this.model, this.parser.model); - break; - - case 'formula': - // except - formula is a string and appends to formulae - this.model.formulae = this.model.formulae || []; - this.model.formulae.push(this.parser.model); - break; - } - - this.parser = null; - } - return true; + case 'formula': + // except - formula is a string and appends to formulae + this.model.formulae = this.model.formulae || []; + this.model.formulae.push(parser.model); + break; } - return name !== this.tag; } } diff --git a/lib/xlsx/xform/sheet/cf/color-scale-xform.js b/lib/xlsx/xform/sheet/cf/color-scale-xform.js index 8b93968ad..a4ea0ec31 100644 --- a/lib/xlsx/xform/sheet/cf/color-scale-xform.js +++ b/lib/xlsx/xform/sheet/cf/color-scale-xform.js @@ -1,9 +1,9 @@ -const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); const ColorXform = require('../../style/color-xform'); const CfvoXform = require('./cfvo-xform'); -class ColorScaleXform extends BaseXform { +class ColorScaleXform extends CompositeXform { constructor() { super(); @@ -30,45 +30,15 @@ class ColorScaleXform extends BaseXform { xmlStream.closeNode(); } - parseOpen(node) { - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - - switch (node.name) { - case this.tag: - this.model = { - cfvo: [], - color: [], - }; - break; - - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - } - return false; - } - - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } + createNewModel(node) { + return { + cfvo: [], + color: [], + }; } - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - this.model[name].push(this.parser.model); - this.parser = null; - } - return true; - } - return name !== this.tag; + onParserClose(name, parser) { + this.model[name].push(parser.model); } } diff --git a/lib/xlsx/xform/sheet/cf/conditional-formatting-ext-xform.js b/lib/xlsx/xform/sheet/cf/conditional-formatting-ext-xform.js deleted file mode 100644 index 1d8813e98..000000000 --- a/lib/xlsx/xform/sheet/cf/conditional-formatting-ext-xform.js +++ /dev/null @@ -1,383 +0,0 @@ -const _ = require('../../../../utils/under-dash'); -const utils = require('../../../../utils/utils'); -const colCache = require('../../../../utils/col-cache'); -const BaseXform = require('../../base-xform'); -const Range = require('../../../../doc/range'); - -// formatting rules appear to be directly in - -// custom - by formula -// -// -// IF(MOD(ROW()+COLUMN(),2)=0,TRUE,FALSE) -// -// -// -// greaterThan -// -// -// 13 -// -// -// top 10% -// -// -// -// bottom 10 -// -// -// -// gradient fill -// Needs some study -// solid fill -// like gradient fill -// Green Yellow Red (looks a bit like gradient path) -// -// -// -// -// -// -// -// -// -// -// -// -// Arrow Icons -// -// -// -// -// -// -// -// -// -// Star Icons - needs more work -// Equal to 13 -// -// -// 13 -// -// -// Text Contains -// -// -// NOT(ISERROR(SEARCH("sday",A1))) -// -// -// Multiple (fully overlapping) -// -// -// -// -// Intersecting -// -// -// -// -// -// -// Infinite range -// -// -// 11 -// 15 -// -// -// -// -// IF(MOD(B1,5)=3,TRUE,FALSE) -// -// -// Special Dates -// -// -// AND(TODAY()-ROUNDDOWN(A1,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(A1,0)-TODAY()<=7-WEEKDAY(TODAY())) -// -// -// -// AND(TODAY()-ROUNDDOWN(A1,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(A1,0)<(WEEKDAY(TODAY())+7)) -// -// -// -// AND(ROUNDDOWN(A1,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(A1,0)-TODAY()<(15-WEEKDAY(TODAY()))) -// -// -// -// FLOOR(A1,1)=TODAY()-1 -// -// -// FLOOR(A1,1)=TODAY() -// -// -// FLOOR(A1,1)=TODAY()+1 -// -// -// AND(TODAY()-FLOOR(A1,1)<=6,FLOOR(A1,1)<=TODAY()) -// -// -// AND(MONTH(A1)=MONTH(EDATE(TODAY(),0-1)),YEAR(A1)=YEAR(EDATE(TODAY(),0-1))) -// -// -// AND(MONTH(A1)=MONTH(TODAY()),YEAR(A1)=YEAR(TODAY())) -// -// -// AND(MONTH(A1)=MONTH(EDATE(TODAY(),0+1)),YEAR(A1)=YEAR(EDATE(TODAY(),0+1))) -// -// - - -function assign(definedName, attributes, name, defaultValue) { - const value = attributes[name]; - if (value !== undefined) { - definedName[name] = value; - } else if (defaultValue !== undefined) { - 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); - } else if (defaultValue !== undefined) { - definedName[name] = defaultValue; - } -} - -function optimiseDataValidations(model) { - // Squeeze alike data validations together into rectangular ranges - // to reduce file size and speed up Excel load time - const dvList = _ - .map(model, (dataValidation, address) => ({ - address, - dataValidation, - marked: false, - })) - .sort((a, b) => _.strcmp(a.address, b.address)); - const dvMap = _.keyBy(dvList, 'address'); - const matchCol = (addr, height, col) => { - for (let i = 0; i < height; i++) { - const otherAddress = colCache.encodeAddress(addr.row + i, col); - if (!model[otherAddress] || !_.isEqual(model[addr.address], model[otherAddress])) { - return false; - } - } - return true; - }; - return dvList - .map(dv => { - if (!dv.marked) { - const addr = colCache.decodeAddress(dv.address); - - // iterate downwards - finding matching cells - let height = 1; - let otherAddress = colCache.encodeAddress(addr.row + height, addr.col); - while (model[otherAddress] && _.isEqual(dv.dataValidation, model[otherAddress])) { - height++; - otherAddress = colCache.encodeAddress(addr.row + height, addr.col); - } - - // iterate rightwards... - - let width = 1; - while (matchCol(addr, height, addr.col + width)) { - width++; - } - - // mark all included addresses - for (let i = 0; i < height; i++) { - for (let j = 0; j < width; j++) { - otherAddress = colCache.encodeAddress(addr.row + i, addr.col + j); - dvMap[otherAddress].marked = true; - } - } - - if ((height > 1) || (width > 1)) { - const bottom = addr.row + (height - 1); - const right = addr.col + (width - 1); - return { - ...dv.dataValidation, - sqref: `${dv.address}:${colCache.encodeAddress(bottom, right)}`, - }; - } - return { - ...dv.dataValidation, - sqref: dv.address, - }; - } - return null; - }) - .filter(Boolean); -} - -class DataValidationsXform extends BaseXform { - get tag() { - return 'dataValidations'; - } - - render(xmlStream, model) { - const optimizedModel = optimiseDataValidations(model); - if (optimizedModel.length) { - xmlStream.openNode('dataValidations', {count: optimizedModel.length}); - - optimizedModel.forEach(value => { - xmlStream.openNode('dataValidation'); - - if (value.type !== 'any') { - xmlStream.addAttribute('type', value.type); - - if (value.operator && value.type !== 'list' && value.operator !== 'between') { - xmlStream.addAttribute('operator', value.operator); - } - if (value.allowBlank) { - xmlStream.addAttribute('allowBlank', '1'); - } - } - if (value.showInputMessage) { - xmlStream.addAttribute('showInputMessage', '1'); - } - if (value.promptTitle) { - xmlStream.addAttribute('promptTitle', value.promptTitle); - } - if (value.prompt) { - xmlStream.addAttribute('prompt', value.prompt); - } - if (value.showErrorMessage) { - xmlStream.addAttribute('showErrorMessage', '1'); - } - if (value.errorStyle) { - xmlStream.addAttribute('errorStyle', value.errorStyle); - } - if (value.errorTitle) { - xmlStream.addAttribute('errorTitle', value.errorTitle); - } - if (value.error) { - xmlStream.addAttribute('error', value.error); - } - xmlStream.addAttribute('sqref', value.sqref); - (value.formulae || []).forEach((formula, index) => { - xmlStream.openNode(`formula${index + 1}`); - if (value.type === 'date') { - xmlStream.writeText(utils.dateToExcel(new Date(formula))); - } else { - xmlStream.writeText(formula); - } - xmlStream.closeNode(); - }); - xmlStream.closeNode(); - }); - xmlStream.closeNode(); - } - } - - parseOpen(node) { - switch (node.name) { - case 'dataValidations': - this.model = {}; - return true; - - case 'dataValidation': { - this._address = node.attributes.sqref; - const dataValidation = node.attributes.type ? - {type: node.attributes.type, formulae: []} : - {type: 'any'}; - - if (node.attributes.type) { - assignBool(dataValidation, node.attributes, 'allowBlank'); - } - assignBool(dataValidation, node.attributes, 'showInputMessage'); - assignBool(dataValidation, node.attributes, 'showErrorMessage'); - - switch (dataValidation.type) { - case 'any': - case 'list': - case 'custom': - break; - default: - assign(dataValidation, node.attributes, 'operator', 'between'); - break; - } - assign(dataValidation, node.attributes, 'promptTitle'); - assign(dataValidation, node.attributes, 'prompt'); - assign(dataValidation, node.attributes, 'errorStyle'); - assign(dataValidation, node.attributes, 'errorTitle'); - assign(dataValidation, node.attributes, 'error'); - - this._dataValidation = dataValidation; - return true; - } - - case 'formula1': - case 'formula2': - this._formula = []; - return true; - - default: - return false; - } - } - - parseText(text) { - if (this._formula) { - this._formula.push(text); - } - } - - parseClose(name) { - switch (name) { - case 'dataValidations': - - return false; - case 'dataValidation': - if (!this._dataValidation.formulae || !this._dataValidation.formulae.length) { - delete this._dataValidation.formulae; - delete this._dataValidation.operator; - } - if (this._address.includes(':')) { - const range = new Range(this._address); - range.forEachAddress(address => { - this.model[address] = this._dataValidation; - }); - } else { - this.model[this._address] = this._dataValidation; - } - return true; - case 'formula1': - case 'formula2': { - let formula = this._formula.join(''); - switch (this._dataValidation.type) { - case 'whole': - case 'textLength': - formula = parseInt(formula, 10); - break; - case 'decimal': - formula = parseFloat(formula); - break; - case 'date': - formula = utils.excelToDate(parseFloat(formula)); - break; - default: - break; - } - this._dataValidation.formulae.push(formula); - this._formula = undefined; - return true; - } - default: - return true; - } - } -} - -module.exports = DataValidationsXform; diff --git a/lib/xlsx/xform/sheet/cf/conditional-formatting-xform.js b/lib/xlsx/xform/sheet/cf/conditional-formatting-xform.js index 7134b1345..ae41ec86d 100644 --- a/lib/xlsx/xform/sheet/cf/conditional-formatting-xform.js +++ b/lib/xlsx/xform/sheet/cf/conditional-formatting-xform.js @@ -1,8 +1,8 @@ -const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); const CfRuleXform = require('./cf-rule-xform'); -class ConditionalFormattingXform extends BaseXform { +class ConditionalFormattingXform extends CompositeXform { constructor() { super(); @@ -33,39 +33,15 @@ class ConditionalFormattingXform extends BaseXform { xmlStream.closeNode(); } - parseOpen(node) { - this.parser = this.parser || this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - - if (node.name === this.tag) { - this.model = { - ref: node.attributes.sqref, - rules: [], - }; - return true; - } - - return false; - } - - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } + createNewModel({attributes}) { + return { + ref: attributes.sqref, + rules: [], + }; } - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - this.model.rules.push(this.parser.model); - this.parser = null; - } - return true; - } - return name !== this.tag; + onParserClose(name, parser) { + this.model.rules.push(parser.model); } } diff --git a/lib/xlsx/xform/sheet/cf/conditional-formattings-ext-xform.js b/lib/xlsx/xform/sheet/cf/conditional-formattings-ext-xform.js deleted file mode 100644 index 79ca3afab..000000000 --- a/lib/xlsx/xform/sheet/cf/conditional-formattings-ext-xform.js +++ /dev/null @@ -1,34 +0,0 @@ -const BaseXform = require('../../base-xform'); - -const ConditionalFormattingExtXform = require('./conditional-formatting-ext-xform'); - -/* eslint-disable */ - -class ConditionalFormattingsExtXform extends BaseXform { - constructor() { - super(); - - this.cfXform = new ConditionalFormattingExtXform(); - } - - get tag() { - return 'x14:conditionalFormattings'; - } - - render(xmlStream, model) { - // TBD - - } - - parseOpen(node) { - // TBD - } - - parseText(text) { - } - - parseClose(name) { - } -} - -module.exports = ConditionalFormattingsExtXform; diff --git a/lib/xlsx/xform/sheet/cf/conditional-formattings-xform.js b/lib/xlsx/xform/sheet/cf/conditional-formattings-xform.js index 508f2ddee..370d231ae 100644 --- a/lib/xlsx/xform/sheet/cf/conditional-formattings-xform.js +++ b/lib/xlsx/xform/sheet/cf/conditional-formattings-xform.js @@ -7,11 +7,6 @@ class ConditionalFormattingsXform extends BaseXform { super(); this.cfXform = new ConditionalFormattingXform(); - - // thanks to Excel, conditionalFormatting nodes are directly inside - // , not inside some node so - // we create initial model here - this.reset(); } get tag() { diff --git a/lib/xlsx/xform/sheet/cf/databar-xform.js b/lib/xlsx/xform/sheet/cf/databar-xform.js index 091135bc6..a789e1d0f 100644 --- a/lib/xlsx/xform/sheet/cf/databar-xform.js +++ b/lib/xlsx/xform/sheet/cf/databar-xform.js @@ -1,9 +1,9 @@ -const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); const ColorXform = require('../../style/color-xform'); const CfvoXform = require('./cfvo-xform'); -class DatabarXform extends BaseXform { +class DatabarXform extends CompositeXform { constructor() { super(); @@ -23,52 +23,26 @@ class DatabarXform extends BaseXform { model.cfvo.forEach(cfvo => { this.cfvoXform.render(xmlStream, cfvo); }); - model.color.forEach(color => { - this.colorXform.render(xmlStream, color); - }); + this.colorXform.render(xmlStream, model.color); xmlStream.closeNode(); } - parseOpen(node) { - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - - switch (node.name) { - case this.tag: - this.model = { - cfvo: [], - color: [], - }; - break; - - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - } - return false; - } - - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } + createNewModel() { + return { + cfvo: [], + }; } - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - this.model[name].push(this.parser.model); - this.parser = null; - } - return true; + onParserClose(name, parser) { + switch (name) { + case 'cfvo': + this.model.cfvo.push(parser.model); + break; + case 'color': + this.model.color = parser.model; + break; } - return name !== this.tag; } } diff --git a/lib/xlsx/xform/sheet/cf/ext-lst-ref-xform.js b/lib/xlsx/xform/sheet/cf/ext-lst-ref-xform.js index f98823d65..9100dfcbd 100644 --- a/lib/xlsx/xform/sheet/cf/ext-lst-ref-xform.js +++ b/lib/xlsx/xform/sheet/cf/ext-lst-ref-xform.js @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); class X14IdXform extends BaseXform { get tag() { @@ -23,12 +24,12 @@ class X14IdXform extends BaseXform { } } -class ExtXform extends BaseXform { +class ExtXform extends CompositeXform { constructor() { super(); this.map = { - 'x14:id': new X14IdXform(), + 'x14:id': this.idXform = new X14IdXform(), }; } @@ -42,50 +43,21 @@ class ExtXform extends BaseXform { 'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main', }); - this.map['x14:id'].render(model.x14Id); + this.idXform.render(xmlStream, model.x14Id); xmlStream.closeNode(); } - parseOpen(node) { - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - - switch (node.name) { - case this.tag: - this.model = {}; - return true; - - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - } - return true; - } + createNewModel() { + return {}; } - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } - } - - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - this.parser = undefined; - } - return true; - } - - return name !== this.tag; + onParserClose(name, parser) { + this.model.x14Id = parser.model; } } -class ExtLstRefXform extends BaseXform { +class ExtLstRefXform extends CompositeXform { constructor() { super(); this.map = { @@ -103,15 +75,12 @@ class ExtLstRefXform extends BaseXform { xmlStream.closeNode(); } - parseOpen(node) { - this.model = { - type: node.attributes.type, - value: node.attributes.val, - }; + createNewModel() { + return {}; } - parseClose(name) { - return name !== this.tag; + onParserClose(name, parser) { + Object.assign(this.model, parser.model); } } diff --git a/lib/xlsx/xform/sheet/cf/icon-set-xform.js b/lib/xlsx/xform/sheet/cf/icon-set-xform.js index 65ee78d94..610db9046 100644 --- a/lib/xlsx/xform/sheet/cf/icon-set-xform.js +++ b/lib/xlsx/xform/sheet/cf/icon-set-xform.js @@ -1,8 +1,9 @@ const BaseXform = require('../../base-xform'); +const CompositeXform = require('../../composite-xform'); const CfvoXform = require('./cfvo-xform'); -class IconSetXform extends BaseXform { +class IconSetXform extends CompositeXform { constructor() { super(); @@ -29,47 +30,17 @@ class IconSetXform extends BaseXform { xmlStream.closeNode(); } - parseOpen(node) { - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - - switch (node.name) { - case this.tag: - this.model = { - iconSet: BaseXform.toStringValue(node.attributes.iconSet, '3TrafficLights'), - reverse: BaseXform.toBoolValue(node.attributes.reverse), - showValue: BaseXform.toBoolValue(node.attributes.showValue), - cfvo: [], - }; - break; - - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - return true; - } - } - return false; - } - - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } + createNewModel({attributes}) { + return { + iconSet: BaseXform.toStringValue(attributes.iconSet, '3TrafficLights'), + reverse: BaseXform.toBoolValue(attributes.reverse), + showValue: BaseXform.toBoolValue(attributes.showValue), + cfvo: [], + }; } - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - this.model[name].push(this.parser.model); - this.parser = null; - } - return true; - } - return name !== this.tag; + onParserClose(name, parser) { + this.model[name].push(parser.model); } } diff --git a/lib/xlsx/xform/sheet/ext-lst-xform.js b/lib/xlsx/xform/sheet/ext-lst-xform.js index a0f93602b..98d9740cb 100644 --- a/lib/xlsx/xform/sheet/ext-lst-xform.js +++ b/lib/xlsx/xform/sheet/ext-lst-xform.js @@ -1,78 +1,85 @@ -const BaseXform = require('../base-xform'); +/* eslint-disable max-classes-per-file */ +const CompositeXform = require('../composite-xform'); -const ConditionalFormattingsExt = require('./cf/conditional-formattings-ext-xform'); +const ConditionalFormattingsExt = require('./cf-ext/conditional-formattings-ext-xform'); -class ExtLstXform extends BaseXform { +class ExtXform extends CompositeXform { constructor() { super(); this.map = { - 'x14:conditionalFormattings': new ConditionalFormattingsExt(), + 'x14:conditionalFormattings': this.conditionalFormattings = new ConditionalFormattingsExt(), }; } get tag() { - return 'extLst'; + return 'ext'; } - render(xmlStream, model) { - let hasContent = false; - xmlStream.addRollback(); - xmlStream.openNode('extLst'); + hasContent(model) { + return this.conditionalFormattings.hasContent(model.conditionalFormattings); + } - // conditional formatting + prepare(model, options) { + this.conditionalFormattings.prepare(model.conditionalFormattings, options); + } + + render(xmlStream, model) { xmlStream.openNode('ext', { uri: '{78C0D931-6437-407d-A8EE-F0AAD7539E65}', 'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main', }); - const cfCursor = xmlStream.cursor; - this.map['x14:conditionalFormattings'].render(model.conditionalFormattings); - hasContent = hasContent || (cfCursor !== xmlStream.cursor); - xmlStream.closeNode(); + + this.conditionalFormattings.render(xmlStream, model.conditionalFormattings); xmlStream.closeNode(); - if (hasContent) { - xmlStream.commit(); - } else { - xmlStream.rollback(); - } } - parseOpen(node) { - if (this.parser) { - this.parser.parseOpen(node); - return true; - } + createNewModel() { + return {}; + } - switch (node.name) { - case 'extLst': - this.model = {}; - return true; - case 'ext': - return true; - default: - this.parser = this.map[node.name]; - if (this.parser) { - this.parser.parseOpen(node); - } - return true; - } + onParserClose(name, parser) { + this.model[name] = parser.model; } +} - parseText(text) { - if (this.parser) { - this.parser.parseText(text); - } +class ExtLstXform extends CompositeXform { + constructor() { + super(); + + this.map = { + 'ext': this.ext = new ExtXform(), + }; } - parseClose(name) { - if (this.parser) { - if (!this.parser.parseClose(name)) { - this.parser = undefined; - } - return true; + get tag() { + return 'extLst'; + } + + prepare(model, options) { + this.ext.prepare(model, options); + } + + hasContent(model) { + return this.ext.hasContent(model); + } + + render(xmlStream, model) { + if (!this.hasContent(model)) { + return; } - return (name !== 'extList'); + xmlStream.openNode('extLst'); + this.ext.render(xmlStream, model); + xmlStream.closeNode(); + } + + createNewModel() { + return {}; + } + + onParserClose(name, parser) { + Object.assign(this.model, parser.model); } } diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 88fc5204c..767dfc84a 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -29,6 +29,64 @@ const TablePartXform = require('./table-part-xform'); const RowBreaksXform = require('./row-breaks-xform'); const HeaderFooterXform = require('./header-footer-xform'); const ConditionalFormattingsXform = require('./cf/conditional-formattings-xform'); +const ExtListXform = require('./ext-lst-xform'); + +const mergeRule = (rule, extRule) => { + Object.keys(extRule).forEach(key => { + const value = rule[key]; + const extValue = extRule[key]; + if ((value === undefined) && (extValue !== undefined)) { + rule[key] = extValue; + } + }); +}; + +const mergeConditionalFormattings = (model, extModel) => { + // conditional formattings are rendered in worksheet.conditionalFormatting and also in + // worksheet.extLst.ext.x14:conditionalFormattings + // some (e.g. dataBar) are even spread across both! + if (!extModel || !extModel.length) { + return model; + } + if (!model || !model.length) { + return extModel; + } + + // index model rules by x14Id + const cfMap = {}; + const ruleMap = {}; + model.forEach(cf => { + cfMap[cf.ref] = cf; + cf.rules.forEach(rule => { + const {x14Id} = rule; + if (x14Id) { + ruleMap[x14Id] = rule; + } + }); + }); + + extModel.forEach(extCf => { + extCf.rules.forEach(extRule => { + const rule = ruleMap[extRule.x14Id]; + if (rule) { + // merge with matching rule + mergeRule(rule, extRule); + } else if (cfMap[extCf.ref]) { + // reuse existing cf ref + cfMap[extCf.ref].rules.push(extRule); + } else { + // create new cf + model.push({ + ref: extCf.ref, + rules: [extRule], + }); + } + }); + }); + + // need to cope with rules in extModel that don't exist in model + return model; +}; class WorkSheetXform extends BaseXform { constructor(options) { @@ -62,6 +120,7 @@ class WorkSheetXform extends BaseXform { sheetProtection: new SheetProtectionXform(), tableParts: new ListXform({tag: 'tableParts', count: true, childXform: new TablePartXform()}), conditionalFormatting: new ConditionalFormattingsXform(), + extLst: new ExtListXform(), }; } @@ -191,6 +250,9 @@ class WorkSheetXform extends BaseXform { } }); }); + + // prepare ext items + this.map.extLst.prepare(model, options); } render(xmlStream, model) { @@ -248,6 +310,8 @@ class WorkSheetXform extends BaseXform { this.map.picture.render(xmlStream, model.background); // Note: must be after drawing this.map.tableParts.render(xmlStream, model.tables); + this.map.extLst.render(xmlStream, model); + if (model.rels) { // add a node for each comment model.rels.forEach(rel => { @@ -307,6 +371,10 @@ class WorkSheetXform extends BaseXform { margins: this.map.pageMargins.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'] + ); this.model = { dimensions: this.map.dimension.model, cols: this.map.cols.model, @@ -321,7 +389,7 @@ class WorkSheetXform extends BaseXform { background: this.map.picture.model, drawing: this.map.drawing.model, tables: this.map.tableParts.model, - conditionalFormattings: this.map.conditionalFormatting.model, + conditionalFormattings, }; if (this.map.autoFilter.model) { diff --git a/spec/unit/utils/encryptor.spec.js b/spec/unit/utils/encryptor.spec.js index 7dcdeb5ab..e69de29bb 100644 --- a/spec/unit/utils/encryptor.spec.js +++ b/spec/unit/utils/encryptor.spec.js @@ -1,11 +0,0 @@ -const Encryptor = verquire('utils/encryptor'); - -describe('Encryptor', () => { - it('Generates SHA-512 hash for given password, salt value and spin count', () => { - const password = '123'; - const saltValue = '6tC6yotbNa8JaMaDvbUgxw=='; - const spinCount = 100000; - const hash = Encryptor.convertPasswordToHash(password, 'SHA512', saltValue, spinCount); - expect(hash).to.equal('RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw=='); - }); -}); diff --git a/spec/unit/xlsx/xform/book/defined-name-xform.spec.js b/spec/unit/xlsx/xform/book/defined-name-xform.spec.js index 3a2311542..9dd2e1499 100644 --- a/spec/unit/xlsx/xform/book/defined-name-xform.spec.js +++ b/spec/unit/xlsx/xform/book/defined-name-xform.spec.js @@ -24,8 +24,7 @@ const expectations = [ localSheetId: 0, ranges: ['bar!$A$1:$C$10'], }, - xml: - 'bar!$A$1:$C$10', + xml: 'bar!$A$1:$C$10', parsedModel: { name: '_xlnm.Print_Area', localSheetId: 0, diff --git a/test/test-cf.js b/test/test-cf.js index 0f687d48f..6b60dcb99 100644 --- a/test/test-cf.js +++ b/test/test-cf.js @@ -347,9 +347,9 @@ shapesWS.addConditionalFormatting({ ], }); -addTable(shapesWS, 'A9:e15'); +addTable(shapesWS, 'A9:E15'); shapesWS.addConditionalFormatting({ - ref: 'A9:e15', + ref: 'A9:E15', rules: [ { type: 'iconSet', @@ -364,6 +364,98 @@ shapesWS.addConditionalFormatting({ ], }); +// ============================================================================ +// Shapes +const extSshapesWS = wb.addWorksheet('Ext Shapes'); + +addTable(extSshapesWS, 'A1:E7'); +extSshapesWS.addConditionalFormatting({ + ref: 'A1:E7', + rules: [ + { + type: 'iconSet', + iconSet: '3Stars', + cfvo: [ + {type: 'percent', value: 0}, + {type: 'percent', value: 33}, + {type: 'percent', value: 67}, + ], + }, + ], +}); + +addTable(extSshapesWS, 'G1:K7'); +extSshapesWS.addConditionalFormatting({ + ref: 'G1:K7', + rules: [ + { + type: 'iconSet', + iconSet: '3Triangles', + cfvo: [ + {type: 'percent', value: 0}, + {type: 'percent', value: 33}, + {type: 'percent', value: 67}, + ], + }, + ], +}); + +addTable(extSshapesWS, 'M1:Q7'); +extSshapesWS.addConditionalFormatting({ + ref: 'M1:Q7', + rules: [ + { + type: 'iconSet', + iconSet: '5Boxes', + cfvo: [ + {type: 'percent', value: 0}, + {type: 'percent', value: 20}, + {type: 'percent', value: 40}, + {type: 'percent', value: 60}, + {type: 'percent', value: 80}, + ], + }, + ], +}); + + +// ============================================================================ +// Databar +const databarWS = wb.addWorksheet('Databar'); + +addTable(databarWS, 'A1:E7'); +databarWS.addConditionalFormatting({ + ref: 'A1:E7', + rules: [ + { + type: 'dataBar', + color: {argb: 'FFFF0000'}, + gradient: true, + cfvo: [ + {type: 'num', value: 5}, + {type: 'num', value: 20}, + ], + }, + ], +}); + +addTable(databarWS, 'G1:K7'); +databarWS.addConditionalFormatting({ + ref: 'G1:K7', + rules: [ + { + type: 'dataBar', + color: {argb: 'FF00FF00'}, + gradient: false, + cfvo: [ + {type: 'num', value: 5}, + {type: 'num', value: 20}, + ], + }, + ], +}); + + // ============================================================================ // Cell Is const cellIsWS = wb.addWorksheet('Cell Is'); @@ -535,7 +627,6 @@ dateWS.addConditionalFormatting({ }); - // ============================================================================ // Save From 2a4f343c0c8cf634146f744fd03abb6eea602b49 Mon Sep 17 00:00:00 2001 From: Maxim Kutepov Date: Fri, 20 Dec 2019 12:36:53 +0500 Subject: [PATCH 06/12] fix typings for cell.note --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 2fcc92863..cbbb36b2c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -436,7 +436,7 @@ export interface Cell extends Style, Address { /** * comment of the cell */ - comment: Comment; + note: Comment; /** * convenience getter to access the formula From 4cd7a3a8e082612e7b0f033d56600e613016bd0d Mon Sep 17 00:00:00 2001 From: Maxim Kutepov Date: Mon, 23 Dec 2019 09:40:31 +0500 Subject: [PATCH 07/12] increase version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9bf12b5b1..4734f7b3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "exceljs", - "version": "3.5.0", + "version": "3.5.1", "description": "Excel Workbook Manager - Read and Write xlsx and csv Files.", "private": false, "license": "MIT", From 38d43404701135700482285f4b09dd6437cbbba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Siemienik=20Pawe=C5=82?= Date: Sat, 11 Jan 2020 23:56:56 +0100 Subject: [PATCH 08/12] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4734f7b3f..9bf12b5b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "exceljs", - "version": "3.5.1", + "version": "3.5.0", "description": "Excel Workbook Manager - Read and Write xlsx and csv Files.", "private": false, "license": "MIT", From 87268943c3458de46c1d9dd9c629764fb4d210eb Mon Sep 17 00:00:00 2001 From: Guyon Date: Wed, 15 Jan 2020 08:17:16 +0000 Subject: [PATCH 09/12] doc --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 90cd7a840..bcb4678b3 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,17 @@ npm install exceljs
  • - Conditional Formatting - A subset of Excel Conditional formatting has been implemented! - Specifically the formatting rules that do not require XML to be rendered - inside an <extLst> node, or in other words everything except - databar and three icon sets (3Triangles, 3Stars, 5Boxes). - These will be implemented in due course + Merged 1041 multiple print areas #1042. + Many thanks to Alexander Pruss for this contribution.
  • - Merged remove core-js/ import #1030. - Many thanks to jeffrey n. carre for this contribution. - This change is used to create a new browserified bundle artefact that does not include - any polyfills. - See Browserify for details. + Merged fix typings for cell.note #1058. + Many thanks to xydens for this contribution. +
  • +
  • + Conditional Formatting has been completed. + The <extLst> conditional formattings including dataBar and the + three iconSet types (3Triangles, 3Stars, 5Boxes) are now available.
@@ -2589,4 +2587,5 @@ If any splice operation affects a merged cell, the merge group will not be moved | 3.3.0 | | | 3.3.1 | | | 3.4.0 | | +| 3.5.0 |
  • Conditional Formatting A subset of Excel Conditional formatting has been implemented! Specifically the formatting rules that do not require XML to be rendered inside an <extLst> node, or in other words everything except databar and three icon sets (3Triangles, 3Stars, 5Boxes). These will be implemented in due course
  • Merged remove core-js/ import #1030. Many thanks to jeffrey n. carre for this contribution. This change is used to create a new browserified bundle artefact that does not include any polyfills. See Browserify for details.
| From 8b0ef456efdc636a031b3d39a2c58d295b1b6248 Mon Sep 17 00:00:00 2001 From: Carlos Beltran Date: Wed, 15 Jan 2020 15:11:39 -0500 Subject: [PATCH 10/12] function duplicateRows added --- lib/doc/worksheet.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index db600d7a6..f06032cec 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -344,6 +344,49 @@ class Worksheet { }); } + duplicateRow(start, count) { + //I want to add after the start row + start++; + + const nKeep = start; + const nEnd = this._rows.length; + let i; + let rSrc; + + // insert new cells + for (i = nEnd; i >= nKeep; i--) { + rSrc = this._rows[i - 1]; + if (rSrc) { + const rDst = this.getRow(i + count); + rDst.values = rSrc.values; + rDst.style = rSrc.style; + // eslint-disable-next-line no-loop-func + rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => { + rDst.getCell(colNumber).style = cell.style; + }); + } + else { + this._rows[i + count - 1] = undefined; + } + } + + //Reference to the original row + rSrc = this._rows[start-2]; + + // now copy over the new values and styles + for (i = 0; i < count; i++) { + const rDst = this.getRow(start + i); + rDst.values = rSrc.values + rDst.style = rSrc.style; + rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => { + rDst.getCell(colNumber).style = cell.style; + }); + } + + // account for defined names + this.workbook.definedNames.spliceRows(this.name, start, 0, count); + } + spliceRows(start, count) { // same problem as row.splice, except worse. const inserts = Array.prototype.slice.call(arguments, 2); From 11a7c646715e532d1ee47a54e46906dcb47c0134 Mon Sep 17 00:00:00 2001 From: Carlos Beltran Date: Sat, 18 Jan 2020 13:43:45 -0500 Subject: [PATCH 11/12] Eslint changes --- lib/doc/worksheet.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index f06032cec..c74c9222c 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -345,7 +345,7 @@ class Worksheet { } duplicateRow(start, count) { - //I want to add after the start row + // I want to add after the start row start++; const nKeep = start; @@ -370,13 +370,13 @@ class Worksheet { } } - //Reference to the original row + // Reference to the original row rSrc = this._rows[start-2]; // now copy over the new values and styles for (i = 0; i < count; i++) { const rDst = this.getRow(start + i); - rDst.values = rSrc.values + rDst.values = rSrc.values; rDst.style = rSrc.style; rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => { rDst.getCell(colNumber).style = cell.style; From e6b817f7b88b7808a300e8bc9f13a1e018b49159 Mon Sep 17 00:00:00 2001 From: Carlos Beltran Date: Mon, 20 Jan 2020 01:13:06 -0500 Subject: [PATCH 12/12] Duplicate Rows test added --- spec/integration/workbook/workbook.spec.js | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/integration/workbook/workbook.spec.js b/spec/integration/workbook/workbook.spec.js index 1dfd90792..a0136874a 100644 --- a/spec/integration/workbook/workbook.spec.js +++ b/spec/integration/workbook/workbook.spec.js @@ -576,6 +576,29 @@ describe('Workbook', () => { }); }); + describe('Duplicate Rows', () => { + it('Duplicate rows properly', () => { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet('duplicateTest'); + ws.getCell('A1').value = 'OneInfo'; + ws.duplicateRow(1,2); + + return wb.xlsx + .writeFile(TEST_XLSX_FILE_NAME) + .then(() => { + const wb2 = new ExcelJS.Workbook(); + return wb2.xlsx.readFile(TEST_XLSX_FILE_NAME); + }) + .then(wb2 => { + const ws2 = wb2.getWorksheet('duplicateTest'); + + expect(ws2.getCell('A2').value).to.equal('OneInfo'); + expect(ws2.getCell('A3').value).to.equal('OneInfo'); + }); + }); + }); + + describe('Merge Cells', () => { it('serialises and deserialises properly', () => { const wb = new ExcelJS.Workbook();