From 8eb2a0d73fa486f7f6bff0c646ff4340829674c1 Mon Sep 17 00:00:00 2001 From: bruno Date: Sat, 3 Aug 2019 21:16:24 -0400 Subject: [PATCH] Introducing comments for the Stream Writer --- lib/stream/xlsx/sheet-comments-writer.js | 121 +++++++++++++++++++++++ lib/stream/xlsx/sheet-rels-writer.js | 4 + lib/stream/xlsx/workbook-writer.js | 2 + lib/stream/xlsx/worksheet-writer.js | 39 ++++++-- test/test-comment-stream-writer.js | 35 +++++++ 5 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 lib/stream/xlsx/sheet-comments-writer.js create mode 100644 test/test-comment-stream-writer.js diff --git a/lib/stream/xlsx/sheet-comments-writer.js b/lib/stream/xlsx/sheet-comments-writer.js new file mode 100644 index 000000000..316673a02 --- /dev/null +++ b/lib/stream/xlsx/sheet-comments-writer.js @@ -0,0 +1,121 @@ +'use strict'; + +const XmlStream = require('../../utils/xml-stream'); +const RelType = require('../../xlsx/rel-type'); +const colCache = require('../../utils/col-cache'); +const CommentXform = require('../../xlsx/xform/comment/comment-xform'); +const VmlNoteXform = require('../../xlsx/xform/comment/vml-note-xform'); + +const SheetCommentsWriter = (module.exports = function(worksheet, sheetRelsWriter, options) { + // in a workbook, each sheet will have a number + this.id = options.id; + this.count = 0; + this._worksheet = worksheet; + this._workbook = options.workbook; + this._sheetRelsWriter = sheetRelsWriter; +}); + +SheetCommentsWriter.prototype = { + get commentsStream() { + if (!this._commentsStream) { + // eslint-disable-next-line no-underscore-dangle + this._commentsStream = this._workbook._openStream(`/xl/comments${this.id}.xml`); + } + return this._commentsStream; + }, + get vmlStream() { + if (!this._vmlStream) { + // eslint-disable-next-line no-underscore-dangle + this._vmlStream = this._workbook._openStream(`xl/drawings/vmlDrawing${this.id}.vml`); + } + return this._vmlStream; + }, + + _addRelationships(){ + const commentRel = { + Type: RelType.Comments, + Target: `../comments${this.id}.xml`, + }; + this._sheetRelsWriter.addRelationship(commentRel); + + const vmlDrawingRel = { + Type: RelType.VmlDrawing, + Target: `../drawings/vmlDrawing${this.id}.vml`, + }; + this.vmlRelId = this._sheetRelsWriter.addRelationship(vmlDrawingRel); + }, + + _addCommentRefs(){ + this._workbook.commentRefs.push({ + commentName: `comments${this.id}`, + vmlDrawing: `vmlDrawing${this.id}`, + }); + }, + + _writeOpen() { + this.commentsStream.write( + '' + + '' + + 'Author' + + '' + ); + this.vmlStream.write( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); + }, + + _writeComment(comment, index) { + const commentXform = new CommentXform(); + const commentsXmlStream = new XmlStream(); + commentXform.render(commentsXmlStream, comment); + this.commentsStream.write(commentsXmlStream.xml); + + const vmlNoteXform = new VmlNoteXform(); + const vmlXmlStream = new XmlStream(); + vmlNoteXform.render(vmlXmlStream, comment, index); + this.vmlStream.write(vmlXmlStream.xml); + }, + + _writeClose() { + this.commentsStream.write(''); + this.vmlStream.write(''); + }, + + addComments(comments){ + if(comments && comments.length){ + if(!this.startedData){ + this._worksheet.comments = []; + this._writeOpen(); + this._addRelationships(); + this._addCommentRefs(); + this.startedData = true; + } + + comments.forEach(item => { + item.refAddress = colCache.decodeAddress(item.ref); + }); + + comments.forEach((comment)=>{ + this._writeComment(comment, this.count); + this.count += 1; + }); + } + }, + + commit() { + if (this.count) { + this._writeClose(); + this.commentsStream.end(); + this.vmlStream.end(); + } + }, + +}; diff --git a/lib/stream/xlsx/sheet-rels-writer.js b/lib/stream/xlsx/sheet-rels-writer.js index fbfcc365d..29446003e 100644 --- a/lib/stream/xlsx/sheet-rels-writer.js +++ b/lib/stream/xlsx/sheet-rels-writer.js @@ -64,6 +64,10 @@ SheetRelsWriter.prototype = { return this._writeRelationship(media); }, + addRelationship(rel) { + return this._writeRelationship(rel); + }, + commit() { if (this.count) { // write xml utro diff --git a/lib/stream/xlsx/workbook-writer.js b/lib/stream/xlsx/workbook-writer.js index c881fab4a..e081423ce 100644 --- a/lib/stream/xlsx/workbook-writer.js +++ b/lib/stream/xlsx/workbook-writer.js @@ -47,6 +47,7 @@ const WorkbookWriter = (module.exports = function(options) { this.zipOptions = options.zip; this.media = []; + this.commentRefs = []; this.zip = Archiver('zip', this.zipOptions); if (options.stream) { @@ -197,6 +198,7 @@ WorkbookWriter.prototype = { const model = { worksheets: this._worksheets.filter(Boolean), sharedStrings: this.sharedStrings, + commentRefs: this.commentRefs, }; const xform = new ContentTypesXform(); const xml = xform.toXml(model); diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index 62087738f..6e29db7e2 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -13,6 +13,7 @@ const Row = require('../../doc/row'); const Column = require('../../doc/column'); const SheetRelsWriter = require('./sheet-rels-writer'); +const SheetCommentsWriter = require('./sheet-comments-writer'); const DataValidations = require('../../doc/data-validations'); const xmlBuffer = new StringBuf(); @@ -37,10 +38,10 @@ const xform = { dataValidations: new DataValidationsXform(), sheetProperties: new SheetPropertiesXform(), sheetFormatProperties: new SheetFormatPropertiesXform(), - columns: new ListXform({ tag: 'cols', length: false, childXform: new ColXform() }), + columns: new ListXform({tag: 'cols', length: false, childXform: new ColXform()}), row: new RowXform(), - hyperlinks: new ListXform({ tag: 'hyperlinks', length: false, childXform: new HyperlinkXform() }), - sheetViews: new ListXform({ tag: 'sheetViews', length: false, childXform: new SheetViewXform() }), + hyperlinks: new ListXform({tag: 'hyperlinks', length: false, childXform: new HyperlinkXform()}), + sheetViews: new ListXform({tag: 'sheetViews', length: false, childXform: new SheetViewXform()}), pageMargins: new PageMarginsXform(), pageSeteup: new PageSetupXform(), autoFilter: new AutoFilterXform(), @@ -76,6 +77,9 @@ const WorksheetWriter = (module.exports = function(options) { // keep record of all hyperlinks this._sheetRelsWriter = new SheetRelsWriter(options); + this._sheetCommentsWriter = new SheetCommentsWriter(this, this._sheetRelsWriter, options); + + // keep a record of dimensions this._dimensions = new Dimensions(); @@ -108,7 +112,7 @@ const WorksheetWriter = (module.exports = function(options) { this.pageSetup = Object.assign( {}, { - margins: { left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3 }, + margins: {left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3}, orientation: 'portrait', horizontalDpi: 4294967295, verticalDpi: 4294967295, @@ -137,6 +141,8 @@ const WorksheetWriter = (module.exports = function(options) { this._workbook = options.workbook; + this.hasComments = false; + // views this._views = options.views || []; @@ -198,6 +204,7 @@ WorksheetWriter.prototype = { // we _cannot_ accept new rows from now on this._rows = null; + if (!this.startedData) { this._writeOpenSheetData(); } @@ -213,11 +220,15 @@ WorksheetWriter.prototype = { this._writePageMargins(); this._writePageSetup(); this._writeBackground(); + + // Legacy Data tag for comments + this._writeLegacyData(); + this._writeCloseWorksheet(); - // signal end of stream to workbook this.stream.end(); + this._sheetCommentsWriter.commit(); // also commit the hyperlinks if any this._sheetRelsWriter.commit(); @@ -468,7 +479,7 @@ WorksheetWriter.prototype = { _writeColumns() { const cols = Column.toModel(this.columns); if (cols) { - xform.columns.prepare(cols, { styles: this._workbook.styles }); + xform.columns.prepare(cols, {styles: this._workbook.styles}); this.stream.write(xform.columns.toXml(cols)); } }, @@ -483,7 +494,7 @@ WorksheetWriter.prototype = { } if (row.hasValues || row.height) { - const { model } = row; + const {model} = row; const options = { styles: this._workbook.styles, sharedStrings: this.useSharedStrings ? this._workbook.sharedStrings : undefined, @@ -491,9 +502,16 @@ WorksheetWriter.prototype = { merges: this._merges, formulae: this._formulae, siFormulae: this._siFormulae, + comments: [], }; xform.row.prepare(model, options); this.stream.write(xform.row.toXml(model)); + + if(options.comments.length){ + this.hasComments = true; + this._sheetCommentsWriter.addComments(options.comments); + } + } }, _writeCloseSheetData() { @@ -532,6 +550,13 @@ WorksheetWriter.prototype = { this.stream.write(xform.picture.toXml(this._background)); } }, + _writeLegacyData() { + if(this.hasComments){ + xmlBuffer.reset(); + xmlBuffer.addText(``); + this.stream.write(xmlBuffer); + } + }, _writeDimensions() { // for some reason, Excel can't handle dimensions at the bottom of the file // and we don't know the dimensions until the commit, so don't write them. diff --git a/test/test-comment-stream-writer.js b/test/test-comment-stream-writer.js new file mode 100644 index 000000000..61a150961 --- /dev/null +++ b/test/test-comment-stream-writer.js @@ -0,0 +1,35 @@ +const Excel = require('../lib/exceljs.nodejs.js'); +const HrStopwatch = require('./utils/hr-stopwatch'); + +const [, , filename] = process.argv; + +const wb = new Excel.stream.xlsx.WorkbookWriter({filename}); +const ws = wb.addWorksheet('Foo'); +ws.getCell('B2').value = 5; +ws.getCell('B2').note = { + texts: [ + {'font': {'size': 12, 'color': {'theme': 0}, 'name': 'Calibri', 'family': 2, 'scheme': 'minor'}, 'text': 'This is '}, + {'font': {'italic': true, 'size': 12, 'color': {'theme': 0}, 'name': 'Calibri', 'scheme': 'minor'}, 'text': 'a'}, + {'font': {'size': 12, 'color': {'theme': 1}, 'name': 'Calibri', 'family': 2, 'scheme': 'minor'}, 'text': ' '}, + {'font': {'size': 12, 'color': {'argb': 'FFFF6600'}, 'name': 'Calibri', 'scheme': 'minor'}, 'text': 'colorful'}, + {'font': {'size': 12, 'color': {'theme': 1}, 'name': 'Calibri', 'family': 2, 'scheme': 'minor'}, 'text': ' text '}, + {'font': {'size': 12, 'color': {'argb': 'FFCCFFCC'}, 'name': 'Calibri', 'scheme': 'minor'}, 'text': 'with'}, + {'font': {'size': 12, 'color': {'theme': 1}, 'name': 'Calibri', 'family': 2, 'scheme': 'minor'}, 'text': ' in-cell '}, + {'font': {'bold': true, 'size': 12, 'color': {'theme': 1}, 'name': 'Calibri', 'family': 2, 'scheme': 'minor'}, 'text': 'format'}, + ], +}; + +ws.getCell('D2').value = 'Zoo'; +ws.getCell('D2').note = 'Plain Text Comment'; + +const stopwatch = new HrStopwatch(); +stopwatch.start(); +wb.commit() + .then(() => { + const micros = stopwatch.microseconds; + console.log('Done.'); + console.log('Time taken:', micros); + }) + .catch(error => { + console.log(error.message); + });