exceljs
Version:
Excel Workbook Manager - Read and Write xlsx and csv Files.
332 lines (289 loc) • 9.98 kB
JavaScript
const fs = require('fs');
const Archiver = require('archiver');
const StreamBuf = require('../../utils/stream-buf');
const RelType = require('../../xlsx/rel-type');
const StylesXform = require('../../xlsx/xform/style/styles-xform');
const SharedStrings = require('../../utils/shared-strings');
const DefinedNames = require('../../doc/defined-names');
const CoreXform = require('../../xlsx/xform/core/core-xform');
const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
const AppXform = require('../../xlsx/xform/core/app-xform');
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
const WorksheetWriter = require('./worksheet-writer');
const theme1Xml = require('../../xlsx/xml/theme1.js');
class WorkbookWriter {
constructor(options) {
options = options || {};
this.created = options.created || new Date();
this.modified = options.modified || this.created;
this.creator = options.creator || 'ExcelJS';
this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
this.lastPrinted = options.lastPrinted;
// 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.zipOptions = options.zip;
this.media = [];
this.commentRefs = [];
this.zip = Archiver('zip', this.zipOptions);
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 = Promise.all([this.addThemes(), this.addOfficeRels()]);
}
get definedNames() {
return this._definedNames;
}
_openStream(path) {
const stream = new StreamBuf({bufSize: 65536, batch: true});
this.zip.append(stream, {name: path});
stream.on('finish', () => {
stream.emit('zipped');
});
return stream;
}
_commitWorksheets() {
const commitWorksheet = function(worksheet) {
if (!worksheet.committed) {
return new Promise(resolve => {
worksheet.stream.on('zipped', () => {
resolve();
});
worksheet.commit();
});
}
return Promise.resolve();
};
// if there are any uncommitted worksheets, commit them now and wait
const promises = this._worksheets.map(commitWorksheet);
if (promises.length) {
return Promise.all(promises);
}
return Promise.resolve();
}
async commit() {
// commit all worksheets, then add suplimentary files
await this.promise;
await this.addMedia();
await this._commitWorksheets();
await Promise.all([this.addContentTypes(), this.addApp(), this.addCore(), this.addSharedStrings(), this.addStyles(), this.addWorkbookRels()]);
await this.addWorkbook();
return this._finalize();
}
get nextId() {
// find the next unique spot to add worksheet
let i;
for (i = 1; i < this._worksheets.length; i++) {
if (!this._worksheets[i]) {
return i;
}
}
return this._worksheets.length || 1;
}
addImage(image) {
const id = this.media.length;
const medium = Object.assign({}, image, {type: 'image', name: `image${id}.${image.extension}`});
this.media.push(medium);
return id;
}
getImage(id) {
return this.media[id];
}
addWorksheet(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 || {};
const useSharedStrings = options.useSharedStrings !== undefined ? options.useSharedStrings : this.useSharedStrings;
if (options.tabColor) {
// eslint-disable-next-line no-console
console.trace('tabColor option has moved to { properties: tabColor: {...} }');
options.properties = Object.assign(
{
tabColor: options.tabColor,
},
options.properties
);
}
const id = this.nextId;
name = name || `sheet${id}`;
const worksheet = new WorksheetWriter({
id,
name,
workbook: this,
useSharedStrings,
properties: options.properties,
state: options.state,
pageSetup: options.pageSetup,
views: options.views,
autoFilter: options.autoFilter,
headerFooter: options.headerFooter,
});
this._worksheets[id] = worksheet;
return worksheet;
}
getWorksheet(id) {
if (id === undefined) {
return this._worksheets.find(() => true);
}
if (typeof id === 'number') {
return this._worksheets[id];
}
if (typeof id === 'string') {
return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
}
return undefined;
}
addStyles() {
return new Promise(resolve => {
this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
resolve();
});
}
addThemes() {
return new Promise(resolve => {
this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
resolve();
});
}
addOfficeRels() {
return new Promise(resolve => {
const xform = new RelationshipsXform();
const xml = xform.toXml([
{Id: 'rId1', Type: RelType.OfficeDocument, Target: 'xl/workbook.xml'},
{Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
{Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
]);
this.zip.append(xml, {name: '/_rels/.rels'});
resolve();
});
}
addContentTypes() {
return new Promise(resolve => {
const model = {
worksheets: this._worksheets.filter(Boolean),
sharedStrings: this.sharedStrings,
commentRefs: this.commentRefs,
media: this.media,
};
const xform = new ContentTypesXform();
const xml = xform.toXml(model);
this.zip.append(xml, {name: '[Content_Types].xml'});
resolve();
});
}
addMedia() {
return Promise.all(
this.media.map(medium => {
if (medium.type === 'image') {
const filename = `xl/media/${medium.name}`;
if (medium.filename) {
return this.zip.file(medium.filename, {name: filename});
}
if (medium.buffer) {
return this.zip.append(medium.buffer, {name: filename});
}
if (medium.base64) {
const dataimg64 = medium.base64;
const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
return this.zip.append(content, {name: filename, base64: true});
}
}
throw new Error('Unsupported media');
})
);
}
addApp() {
return new Promise(resolve => {
const model = {
worksheets: this._worksheets.filter(Boolean),
};
const xform = new AppXform();
const xml = xform.toXml(model);
this.zip.append(xml, {name: 'docProps/app.xml'});
resolve();
});
}
addCore() {
return new Promise(resolve => {
const coreXform = new CoreXform();
const xml = coreXform.toXml(this);
this.zip.append(xml, {name: 'docProps/core.xml'});
resolve();
});
}
addSharedStrings() {
if (this.sharedStrings.count) {
return new Promise(resolve => {
const sharedStringsXform = new SharedStringsXform();
const xml = sharedStringsXform.toXml(this.sharedStrings);
this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
resolve();
});
}
return Promise.resolve();
}
addWorkbookRels() {
let count = 1;
const relationships = [
{Id: `rId${count++}`, Type: RelType.Styles, Target: 'styles.xml'},
{Id: `rId${count++}`, Type: RelType.Theme, Target: 'theme/theme1.xml'},
];
if (this.sharedStrings.count) {
relationships.push({Id: `rId${count++}`, Type: RelType.SharedStrings, Target: 'sharedStrings.xml'});
}
this._worksheets.forEach(worksheet => {
if (worksheet) {
worksheet.rId = `rId${count++}`;
relationships.push({Id: worksheet.rId, Type: RelType.Worksheet, Target: `worksheets/sheet${worksheet.id}.xml`});
}
});
return new Promise(resolve => {
const xform = new RelationshipsXform();
const xml = xform.toXml(relationships);
this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
resolve();
});
}
addWorkbook() {
const {zip} = this;
const model = {
worksheets: this._worksheets.filter(Boolean),
definedNames: this._definedNames.model,
views: this.views,
properties: {},
calcProperties: {},
};
return new Promise(resolve => {
const xform = new WorkbookXform();
xform.prepare(model);
zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
resolve();
});
}
_finalize() {
return new Promise((resolve, reject) => {
this.stream.on('error', reject);
this.stream.on('finish', () => {
resolve(this);
});
this.zip.on('error', reject);
this.zip.finalize();
});
}
}
module.exports = WorkbookWriter;