excel4node
Version:
Library to create Formatted Excel Files.
658 lines (580 loc) • 22.1 kB
JavaScript
const xmlbuilder = require('xmlbuilder');
const JSZip = require('jszip');
const fs = require('fs');
const CTColor = require('../style/classes/ctColor.js');
const utils = require('../utils');
let addRootContentTypesXML = (promiseObj) => {
// Required as stated in §12.2
return new Promise((resolve, reject) => {
let xml = xmlbuilder.create(
'Types', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
)
.att('xmlns', 'http://schemas.openxmlformats.org/package/2006/content-types');
let contentTypesAdded = [];
let extensionsAdded = [];
promiseObj.wb.sheets.forEach((s, i) => {
if (s.drawingCollection.length > 0) {
s.drawingCollection.drawings.forEach((d) => {
if (extensionsAdded.indexOf(d.extension) < 0) {
let typeRef = d.contentType + '.' + d.extension;
if (contentTypesAdded.indexOf(typeRef) < 0) {
xml.ele('Default').att('ContentType', d.contentType).att('Extension', d.extension);
contentTypesAdded.push(typeRef);
}
extensionsAdded.push(d.extension);
}
});
}
if (Object.keys(s.comments).length > 0) {
if (extensionsAdded.indexOf('vml') < 0) {
xml.ele('Default').att('Extension', 'vml').att('ContentType', 'application/vnd.openxmlformats-officedocument.vmlDrawing');
extensionsAdded.push('vml');
}
}
});
xml.ele('Default').att('ContentType', 'application/xml').att('Extension', 'xml');
xml.ele('Default').att('ContentType', 'application/vnd.openxmlformats-package.relationships+xml').att('Extension', 'rels');
xml.ele('Override').att('ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml').att('PartName', '/xl/workbook.xml');
promiseObj.wb.sheets.forEach((s, i) => {
xml.ele('Override')
.att('ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml')
.att('PartName', `/xl/worksheets/sheet${i + 1}.xml`);
if (s.drawingCollection.length > 0) {
xml.ele('Override')
.att('ContentType', 'application/vnd.openxmlformats-officedocument.drawing+xml')
.att('PartName', '/xl/drawings/drawing' + s.sheetId + '.xml');
}
if (Object.keys(s.comments).length > 0) {
xml.ele('Override')
.att('ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml')
.att('PartName', '/xl/comments' + s.sheetId + '.xml');
}
});
xml.ele('Override').att('ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml').att('PartName', '/xl/styles.xml');
xml.ele('Override').att('ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml').att('PartName', '/xl/sharedStrings.xml');
xml.ele('Override').att('ContentType', 'application/vnd.openxmlformats-package.core-properties+xml').att('PartName', '/docProps/core.xml');
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.file('[Content_Types].xml', xmlString);
resolve(promiseObj);
});
};
let addRootRelsXML = (promiseObj) => {
// Required as stated in §12.2
return new Promise((resolve, reject) => {
let xml = xmlbuilder.create(
'Relationships', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
)
.att('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
xml
.ele('Relationship')
.att('Id', 'rId1')
.att('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument')
.att('Target', 'xl/workbook.xml');
xml
.ele('Relationship')
.att('Id', 'rId2')
.att('Type', 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties')
.att('Target', 'docProps/core.xml');
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('_rels').file('.rels', xmlString);
resolve(promiseObj);
});
};
let addWorkbookXML = (promiseObj) => {
// Required as stated in §12.2
return new Promise((resolve, reject) => {
let xml = xmlbuilder.create(
'workbook', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
);
xml.att('mc:Ignorable', 'x15');
xml.att('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
xml.att('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006');
xml.att('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
xml.att('xmlns:x15', 'http://schemas.microsoft.com/office/spreadsheetml/2010/11/main');
let booksViewEle = xml.ele('bookViews');
let workbookViewEle = booksViewEle.ele('workbookView');
// bookViews (§18.2.1)
if (promiseObj.wb.opts.workbookView) {
const viewOpts = promiseObj.wb.opts.workbookView;
if (viewOpts.activeTab !== null && viewOpts.activeTab !== undefined) {
workbookViewEle.att('activeTab', viewOpts.activeTab);
} else {
let firstVisibleTab = 0;
for (let i = 0; i < promiseObj.wb.sheets.length; i++) {
const sheet = promiseObj.wb.sheets[i];
if (!sheet.opts.hidden) {
firstVisibleTab = i;
break;
}
}
workbookViewEle.att('activeTab', firstVisibleTab);
}
if (viewOpts.autoFilterDateGrouping) {
workbookViewEle.att('autoFilterDateGrouping', utils.boolToInt(viewOpts.autoFilterDateGrouping));
}
if (viewOpts.firstSheet) {
workbookViewEle.att('firstSheet', viewOpts.firstSheet);
}
if (viewOpts.minimized) {
workbookViewEle.att('minimized', utils.boolToInt(viewOpts.minimized));
}
if (viewOpts.showHorizontalScroll) {
workbookViewEle.att('showHorizontalScroll', utils.boolToInt(viewOpts.showHorizontalScroll));
}
if (viewOpts.showSheetTabs) {
workbookViewEle.att('showSheetTabs', utils.boolToInt(viewOpts.showSheetTabs));
}
if (viewOpts.showVerticalScroll) {
workbookViewEle.att('showVerticalScroll', utils.boolToInt(viewOpts.showVerticalScroll));
}
if (viewOpts.tabRatio) {
workbookViewEle.att('tabRatio', viewOpts.tabRatio);
}
if (viewOpts.visibility) {
workbookViewEle.att('visibility', viewOpts.visibility);
}
if (viewOpts.windowWidth) {
workbookViewEle.att('windowWidth', viewOpts.windowWidth);
}
if (viewOpts.windowHeight) {
workbookViewEle.att('windowHeight', viewOpts.windowHeight);
}
if (viewOpts.xWindow) {
workbookViewEle.att('xWindow', viewOpts.xWindow);
}
if (viewOpts.yWindow) {
workbookViewEle.att('yWindow', viewOpts.yWindow);
}
if (viewOpts.showComments) {
workbookViewEle.att('showComments', viewOpts.showComments);
}
}
let sheetsEle = xml.ele('sheets');
promiseObj.wb.sheets.forEach((s, i) => {
const sheet = sheetsEle.ele('sheet')
.att('name', s.name)
.att('sheetId', i + 1)
.att('r:id', `rId${i + 1}`);
if (s.opts.hidden) {
sheet.att('state', 'hidden');
}
if (s.printArea) {
const name = s.name;
const startCellRef = `$${utils.getExcelAlpha(s.printArea.startCol)}$${s.printArea.startRow}`;
const endCellRef = `$${utils.getExcelAlpha(s.printArea.endCol)}$${s.printArea.endRow}`;
s.wb.definedNameCollection.addDefinedName({
name: '_xlnm.Print_Area',
localSheetId: s.localSheetId,
refFormula: `'${name}'!${startCellRef}:${endCellRef}`,
});
}
});
if (!promiseObj.wb.definedNameCollection.isEmpty) {
promiseObj.wb.definedNameCollection.addToXMLele(xml);
}
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('xl').file('workbook.xml', xmlString);
resolve(promiseObj);
});
};
let addWorkbookRelsXML = (promiseObj) => {
// Required as stated in §12.2
return new Promise((resolve, reject) => {
let xml = xmlbuilder.create(
'Relationships', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
)
.att('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
xml
.ele('Relationship')
.att('Id', `rId${promiseObj.wb.sheets.length + 1}`)
.att('Target', 'sharedStrings.xml')
.att('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
xml
.ele('Relationship')
.att('Id', `rId${promiseObj.wb.sheets.length + 2}`)
.att('Target', 'styles.xml')
.att('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles');
promiseObj.wb.sheets.forEach((s, i) => {
xml
.ele('Relationship')
.att('Id', `rId${i + 1}`)
.att('Target', `worksheets/sheet${i + 1}.xml`)
.att('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet');
});
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('xl').folder('_rels').file('workbook.xml.rels', xmlString);
resolve(promiseObj);
});
};
let addCorePropertiesXML = (promiseObj) => {
let xml = xmlbuilder.create(
'cp:coreProperties', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true
}
)
.att('xmlns:cp', 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties')
.att('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
.att('xmlns:dcterms', 'http://purl.org/dc/terms/')
.att('xmlns:dcmitype', 'http://purl.org/dc/dcmitype/')
.att('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
xml.ele('dc:creator').text(promiseObj.wb.author);
xml.ele('cp:lastModifiedBy').text(promiseObj.wb.author);
let dtStr = new Date().toISOString();
xml.ele('dcterms:created').att('xsi:type', 'dcterms:W3CDTF').text(dtStr);
xml.ele('dcterms:modified').att('xsi:type', 'dcterms:W3CDTF').text(dtStr);
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('docProps').file('core.xml', xmlString);
return promiseObj;
};
let addWorksheetsXML = (promiseObj) => {
// Required as stated in §12.2
return new Promise((resolve, reject) => {
let curSheet = 0;
let processNextSheet = () => {
let thisSheet = promiseObj.wb.sheets[curSheet];
if (thisSheet) {
curSheet++;
thisSheet.generateXML()
.then((xml) => {
return new Promise((resolve) => {
// Add worksheet to zip
promiseObj.xlsx.folder('xl').folder('worksheets').file(`sheet${curSheet}.xml`, xml);
resolve();
});
})
.then(() => {
return thisSheet.generateRelsXML();
})
.then((xml) => {
return new Promise((resolve) => {
if (xml) {
promiseObj.xlsx.folder('xl').folder('worksheets').folder('_rels').file(`sheet${curSheet}.xml.rels`, xml);
}
resolve();
});
})
.then(() => {
return thisSheet.generateCommentsXML();
})
.then((xml) => {
return new Promise((resolve) => {
if (xml) {
promiseObj.xlsx.folder('xl').file(`comments${curSheet}.xml`, xml);
}
resolve();
});
})
.then(() => {
return thisSheet.generateCommentsVmlXML();
})
.then((xml) => {
return new Promise((resolve) => {
if (xml) {
promiseObj.xlsx.folder('xl').folder('drawings').file(`commentsVml${curSheet}.vml`, xml);
}
resolve();
});
})
.then(processNextSheet)
.catch((e) => {
promiseObj.wb.logger.error(e.stack);
});
} else {
resolve(promiseObj);
}
};
processNextSheet();
});
};
/**
* Generate XML for SharedStrings.xml file and add it to zip file. Called from _writeToBuffer()
* @private
* @memberof Workbook
* @param {Object} promiseObj object containing jszip instance, workbook intance and xmlvars
* @return {Promise} Resolves with promiseObj
*/
let addSharedStringsXML = (promiseObj) => {
// §12.3.15 Shared String Table Part
return new Promise((resolve, reject) => {
let xml = xmlbuilder.create(
'sst', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
)
.att('count', promiseObj.wb.sharedStrings.length)
.att('uniqueCount', promiseObj.wb.sharedStrings.length)
.att('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
promiseObj.wb.sharedStrings.forEach((s) => {
if (typeof s === 'string') {
xml.ele('si').ele('t').txt(s);
} else if (s instanceof Array) {
let thisSI = xml.ele('si');
let theseRuns = []; // §18.4.4 r (Rich Text Run)
let currProps = {};
let curRun;
let i = 0;
while (i < s.length) {
if (typeof s[i] === 'string') {
if (curRun === undefined) {
theseRuns.push({
props: {},
text: ''
});
curRun = theseRuns[theseRuns.length - 1];
}
curRun.text = curRun.text + s[i];
} else if (typeof s[i] === 'object') {
theseRuns.push({
props: {},
text: ''
});
curRun = theseRuns[theseRuns.length - 1];
Object.keys(s[i]).forEach((k) => {
currProps[k] = s[i][k];
});
Object.keys(currProps).forEach((k) => {
curRun.props[k] = currProps[k];
});
if (s[i].value !== undefined) {
curRun.text = s[i].value;
}
}
i++;
}
theseRuns.forEach((run) => {
if (Object.keys(run).length < 1) {
thisSI.ele('t', run.text).att('xml:space', 'preserve');
} else {
let thisRun = thisSI.ele('r');
let thisRunProps = thisRun.ele('rPr');
typeof run.props.name === 'string' ? thisRunProps.ele('rFont').att('val', run.props.name) : null;
run.props.bold === true ? thisRunProps.ele('b') : null;
run.props.italics === true ? thisRunProps.ele('i') : null;
run.props.strike === true ? thisRunProps.ele('strike') : null;
run.props.outline === true ? thisRunProps.ele('outline') : null;
run.props.shadow === true ? thisRunProps.ele('shadow') : null;
run.props.condense === true ? thisRunProps.ele('condense') : null;
run.props.extend === true ? thisRunProps.ele('extend') : null;
if (typeof run.props.color === 'string') {
let thisColor = new CTColor(run.props.color);
thisColor.addToXMLele(thisRunProps);
}
typeof run.props.size === 'number' ? thisRunProps.ele('sz').att('val', run.props.size) : null;
run.props.underline === true ? thisRunProps.ele('u') : null;
typeof run.props.vertAlign === 'string' ? thisRunProps.ele('vertAlign').att('val', run.props.vertAlign) : null;
thisRun.ele('t', run.text).att('xml:space', 'preserve');
}
});
}
});
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('xl').file('sharedStrings.xml', xmlString);
resolve(promiseObj);
});
};
let addStylesXML = (promiseObj) => {
// §12.3.20 Styles Part
return new Promise((resolve, reject) => {
let xml = xmlbuilder.create(
'styleSheet', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
)
.att('mc:Ignorable', 'x14ac')
.att('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main')
.att('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006')
.att('xmlns:x14ac', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac');
if (promiseObj.wb.styleData.numFmts.length > 0) {
let nfXML = xml
.ele('numFmts')
.att('count', promiseObj.wb.styleData.numFmts.length);
promiseObj.wb.styleData.numFmts.forEach((nf) => {
nf.addToXMLele(nfXML);
});
}
let fontXML = xml
.ele('fonts')
.att('count', promiseObj.wb.styleData.fonts.length);
promiseObj.wb.styleData.fonts.forEach((f) => {
f.addToXMLele(fontXML);
});
let fillXML = xml
.ele('fills')
.att('count', promiseObj.wb.styleData.fills.length);
promiseObj.wb.styleData.fills.forEach((f) => {
let fXML = fillXML.ele('fill');
f.addToXMLele(fXML);
});
let borderXML = xml
.ele('borders')
.att('count', promiseObj.wb.styleData.borders.length);
promiseObj.wb.styleData.borders.forEach((b) => {
b.addToXMLele(borderXML);
});
let cellStyleXfsXML = xml
.ele('cellStyleXfs')
.att('count', 1);
cellStyleXfsXML.ele('xf')
.att('numFmtId', '0')
.att('fontId', '0')
.att('fillId','0')
.att('borderId', '0')
.att('applyFont', 'true')
.att('applyBorder', 'false')
.att('applyAlignment', 'false')
.att('applyProtection', 'false');
let cellXfsXML = xml
.ele('cellXfs')
.att('count', promiseObj.wb.styles.length);
promiseObj.wb.styles.forEach((s) => {
s.addXFtoXMLele(cellXfsXML);
});
// insert default cell style
let cellStylesXML = xml
.ele('cellStyles')
.att('count', 1);
cellStylesXML.ele('cellStyle')
.att('name', 'Normal')
.att('xfId', '0')
.att('builtinId', '0');
if (promiseObj.wb.dxfCollection.length > 0) {
promiseObj.wb.dxfCollection.addToXMLele(xml);
}
let xmlString = xml.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('xl').file('styles.xml', xmlString);
resolve(promiseObj);
});
};
let addDrawingsXML = (promiseObj) => {
return new Promise((resolve) => {
if (!promiseObj.wb.mediaCollection.isEmpty) {
promiseObj.wb.sheets.forEach((ws) => {
if (!ws.drawingCollection.isEmpty) {
let drawingRelXML = xmlbuilder.create('Relationships', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
})
.att('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
let drawingsXML = xmlbuilder.create(
'xdr:wsDr', {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true,
'allowSurrogateChars': true
}
);
drawingsXML
.att('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main')
.att('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
ws.drawingCollection.drawings.forEach((d) => {
if (d.kind === 'image') {
let target = 'image' + d.id + '.' + d.extension;
let image = d.imagePath ? fs.readFileSync(d.imagePath) : d.image;
promiseObj.xlsx.folder('xl').folder('media').file(target, image);
drawingRelXML.ele('Relationship')
.att('Id', d.rId)
.att('Target', '../media/' + target)
.att('Type', d.type);
}
d.addToXMLele(drawingsXML);
});
let drawingsXMLStr = drawingsXML.doc().end(promiseObj.xmlOutVars);
let drawingRelXMLStr = drawingRelXML.doc().end(promiseObj.xmlOutVars);
promiseObj.xlsx.folder('xl').folder('drawings').file('drawing' + ws.sheetId + '.xml', drawingsXMLStr);
promiseObj.xlsx.folder('xl').folder('drawings').folder('_rels').file('drawing' + ws.sheetId + '.xml.rels', drawingRelXMLStr);
}
});
}
resolve(promiseObj);
});
};
/**
* Use JSZip to generate file to a node buffer
* @private
* @memberof Workbook
* @param {Workbook} wb Workbook instance
* @return {Promise} resolves with Buffer
*/
let writeToBuffer = (wb) => {
return new Promise((resolve, reject) => {
let promiseObj = {
wb: wb,
xlsx: new JSZip(),
xmlOutVars: {}
};
if (promiseObj.wb.sheets.length === 0) {
promiseObj.wb.addWorksheet();
}
addRootContentTypesXML(promiseObj)
.then(addWorksheetsXML)
.then(addRootRelsXML)
.then(addWorkbookXML)
.then(addWorkbookRelsXML)
.then(addCorePropertiesXML)
.then(addSharedStringsXML)
.then(addStylesXML)
.then(addDrawingsXML)
.then(() => {
wb.opts.jszip.type = 'nodebuffer';
promiseObj.xlsx.generateAsync(wb.opts.jszip)
.then((buf) => {
resolve(buf);
})
.catch((e) => {
reject(e);
});
})
.catch((e) => {
reject(e);
});
});
};
/**
* @desc Currently only used for testing the XML generated for a Workbook.
* @param {*} wb Workbook instance
* @return {Promise} resolves with Workbook XML
*/
let workbookXML = (wb) => {
let promiseObj = {
wb: wb,
xlsx: new JSZip(),
xmlOutVars: {}
};
return addWorkbookXML(promiseObj).then((result) => {
return result.xlsx.files['xl/workbook.xml']._data;
});
}
module.exports = {
writeToBuffer,
workbookXML
};