exceljs
Version:
Excel Workbook Manager - Read and Write xlsx and csv Files.
321 lines (298 loc) • 10.2 kB
JavaScript
/**
* Copyright (c) 2015 Guyon Roche
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
'use strict';
var fs = require('fs');
var Bluebird = require('bluebird');
var _ = require('underscore');
var Archiver = require('archiver');
var utils = require('../../utils/utils');
var StreamBuf = require('../../utils/stream-buf');
var RelType = require('../../xlsx/rel-type');
var StylesXform = require('../../xlsx/xform/style/styles-xform');
var SharedStrings = require('../../utils/shared-strings');
var DefinedNames = require('../../doc/defined-names');
var CoreXform = require('../../xlsx/xform/core/core-xform');
var RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
var ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
var AppXform = require('../../xlsx/xform/core/app-xform');
var WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
var SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
var WorksheetWriter = require('./worksheet-writer');
var WorkbookWriter = module.exports = function(options) {
options = options || {};
//console.log(JSON.stringify(options, null, ' '))
this.created = options.created || new Date();
this.modified = options.modified || this.created;
this.creator = options.creator || 'ExcelJS';
this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
// using shared strings creates a smaller xlsx file but may use more memory
this.useSharedStrings = options.useSharedStrings || false;
this.sharedStrings = new SharedStrings();
// style manager
this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
// defined names
this._definedNames = new DefinedNames();
this._worksheets = [];
this.views = [];
this.zip = Archiver('zip');
if (options.stream) {
this.stream = options.stream;
} else if (options.filename) {
this.stream = fs.createWriteStream(options.filename);
} else {
this.stream = new StreamBuf();
}
this.zip.pipe(this.stream);
// these bits can be added right now
this.promise = Bluebird.all([
this.addThemes(),
this.addOfficeRels()
]);
};
WorkbookWriter.prototype = {
get definedNames() {
return this._definedNames;
},
_openStream: function(path) {
var self = this;
var stream = new StreamBuf({bufSize: 65536, batch: true});
self.zip.append(stream, { name: path });
stream.on('end', function() {
stream.emit('zipped');
});
return stream;
},
_commitWorksheets: function() {
var commitWorksheet = function(worksheet) {
if (!worksheet.committed) {
return new Bluebird(function(resolve) {
worksheet.stream.on('zipped', function() {
resolve();
});
worksheet.commit();
});
} else {
return Bluebird.resolve();
}
};
// if there are any uncommitted worksheets, commit them now and wait
var promises = this._worksheets.map(commitWorksheet);
if (promises.length) {
return Bluebird.all(promises);
} else {
return Bluebird.resolve();
}
},
commit: function() {
var self = this;
// commit all worksheets, then add suplimentary files
return this.promise.then(function() {
return self._commitWorksheets();
})
.then(function() {
return Bluebird.all([
self.addContentTypes(),
self.addApp(),
self.addCore(),
self.addSharedStrings(),
self.addStyles(),
self.addWorkbookRels()
]);
})
.then(function() {
return self.addWorkbook();
})
.then(function(){
return self._finalize();
});
},
get nextId() {
// find the next unique spot to add worksheet
var i;
for (i = 1; i < this._worksheets.length; i++) {
if (!this._worksheets[i]) {
return i;
}
}
return this._worksheets.length || 1;
},
addWorksheet: function(name, options) {
// it's possible to add a worksheet with different than default
// shared string handling
// in fact, it's even possible to switch it mid-sheet
options = options || {};
var useSharedStrings = options.useSharedStrings !== undefined ?
options.useSharedStrings :
this.useSharedStrings;
var id = this.nextId;
name = name || 'sheet' + id;
var worksheet = new WorksheetWriter({
id: id,
name: name,
workbook: this,
useSharedStrings: useSharedStrings,
properties: options.properties,
pageSetup: options.pageSetup
});
this._worksheets[id] = worksheet;
return worksheet;
},
getWorksheet: function(id) {
if (id === undefined) {
return _.find(this._worksheets, function() { return true; });
} else if (typeof(id) === 'number') {
return this._worksheets[id];
} else if (typeof id === 'string') {
return _.find(this._worksheets, function(worksheet) {
return worksheet.name == id;
});
} else {
return undefined;
}
},
addStyles: function() {
var self = this;
return new Bluebird(function(resolve) {
self.zip.append(self.styles.xml, {name: 'xl/styles.xml'});
resolve();
});
},
addThemes: function() {
var self = this;
return utils.readModuleFile(require.resolve('../../xlsx/xml/theme1.xml'))
.then(function(data){
self.zip.append(data, { name: 'xl/theme/theme1.xml' });
});
},
addOfficeRels: function() {
var self = this;
return new Bluebird(function(resolve) {
var xform = new RelationshipsXform();
var xml = xform.toXml([
{rId: 'rId1', type: RelType.OfficeDocument, target: 'xl/workbook.xml'}
]);
self.zip.append(xml, {name: '/_rels/.rels'});
resolve();
});
},
addContentTypes: function() {
var self = this;
return new Bluebird(function(resolve) {
var model = {
worksheets: self._worksheets
};
var xform = new ContentTypesXform();
var xml = xform.toXml(model);
self.zip.append(xml, {name: '[Content_Types].xml'});
resolve();
});
},
addApp: function() {
var self = this;
return new Bluebird(function(resolve) {
var model = {
worksheets: self._worksheets
};
var xform = new AppXform();
var xml = xform.toXml(model);
self.zip.append(xml, {name: 'docProps/app.xml'});
resolve();
});
},
addCore: function() {
var self = this;
return new Bluebird(function(resolve) {
var coreXform = new CoreXform();
var xml = coreXform.toXml(self);
self.zip.append(xml, {name: 'docProps/core.xml'});
resolve();
});
},
addSharedStrings: function() {
var self = this;
if (this.sharedStrings.count) {
return new Bluebird(function(resolve) {
var sharedStringsXform = new SharedStringsXform();
var xml = sharedStringsXform.toXml(self.sharedStrings);
self.zip.append(xml, {name: '/xl/sharedStrings.xml'});
resolve();
});
} else {
return Bluebird.resolve();
}
},
addWorkbookRels: function() {
var self = this;
var count = 1;
var relationships = [
{rId: 'rId' + (count++), type: RelType.Styles, target: 'styles.xml'},
{rId: 'rId' + (count++), type: RelType.Theme, target: 'theme/theme1.xml'}
];
if (this.sharedStrings.count) {
relationships.push(
{rId: 'rId' + (count++), type: RelType.SharedStrings, target: 'sharedStrings.xml'}
);
}
_.each(this._worksheets, function (worksheet) {
worksheet.rId = 'rId' + (count++);
relationships.push(
{rId: worksheet.rId, type: RelType.Worksheet, target: 'worksheets/sheet' + worksheet.id + '.xml'}
);
});
return new Bluebird(function(resolve) {
var xform = new RelationshipsXform();
var xml = xform.toXml(relationships);
self.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
resolve();
});
},
addWorkbook: function() {
var zip = this.zip;
var model = {
worksheets: this._worksheets,
definedNames: this._definedNames.model,
views: this.views
};
return new Bluebird(function(resolve) {
var xform = new WorkbookXform();
xform.prepare(model);
zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
resolve();
});
},
_finalize: function() {
var self = this;
return new Bluebird(function(resolve, reject) {
self.stream.on('error', function(error){
reject(error);
});
self.stream.on('finish', function(){
resolve(self);
});
self.zip.on('error', function(error){
reject(error);
});
self.zip.finalize();
});
}
};