exceljs
Version:
Excel Workbook Manager - Read and Write xlsx and csv Files.
363 lines (359 loc) • 10.8 kB
JavaScript
"use strict";
const fs = require('fs');
const {
EventEmitter
} = require('events');
const {
PassThrough,
Readable
} = require('readable-stream');
const nodeStream = require('stream');
const unzip = require('unzipper');
const tmp = require('tmp');
const iterateStream = require('../../utils/iterate-stream');
const parseSax = require('../../utils/parse-sax');
const StyleManager = require('../../xlsx/xform/style/styles-xform');
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
const WorksheetReader = require('./worksheet-reader');
const HyperlinkReader = require('./hyperlink-reader');
tmp.setGracefulCleanup();
class WorkbookReader extends EventEmitter {
constructor(input) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
super();
this.input = input;
this.options = {
worksheets: 'emit',
sharedStrings: 'cache',
hyperlinks: 'ignore',
styles: 'ignore',
entries: 'ignore',
...options
};
this.styles = new StyleManager();
this.styles.init();
}
_getStream(input) {
if (input instanceof nodeStream.Readable || input instanceof Readable) {
return input;
}
if (typeof input === 'string') {
return fs.createReadStream(input);
}
throw new Error(`Could not recognise input: ${input}`);
}
async read(input, options) {
try {
for await (const {
eventType,
value
} of this.parse(input, options)) {
switch (eventType) {
case 'shared-strings':
this.emit(eventType, value);
break;
case 'worksheet':
this.emit(eventType, value);
await value.read();
break;
case 'hyperlinks':
this.emit(eventType, value);
break;
}
}
this.emit('end');
this.emit('finished');
} catch (error) {
this.emit('error', error);
}
}
async *[Symbol.asyncIterator]() {
for await (const {
eventType,
value
} of this.parse()) {
if (eventType === 'worksheet') {
yield value;
}
}
}
async *parse(input, options) {
if (options) this.options = options;
const stream = this.stream = this._getStream(input || this.input);
const zip = unzip.Parse({
forceStream: true
});
stream.pipe(zip);
// worksheets, deferred for parsing after shared strings reading
const waitingWorkSheets = [];
for await (const entry of iterateStream(zip)) {
let match;
let sheetNo;
switch (entry.path) {
case '_rels/.rels':
break;
case 'xl/_rels/workbook.xml.rels':
await this._parseRels(entry);
break;
case 'xl/workbook.xml':
await this._parseWorkbook(entry);
break;
case 'xl/sharedStrings.xml':
yield* this._parseSharedStrings(entry);
break;
case 'xl/styles.xml':
await this._parseStyles(entry);
break;
default:
if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
sheetNo = match[1];
if (this.sharedStrings && this.workbookRels) {
yield* this._parseWorksheet(iterateStream(entry), sheetNo);
} else {
// create temp file for each worksheet
await new Promise((resolve, reject) => {
tmp.file((err, path, fd, tempFileCleanupCallback) => {
if (err) {
return reject(err);
}
waitingWorkSheets.push({
sheetNo,
path,
tempFileCleanupCallback
});
const tempStream = fs.createWriteStream(path);
tempStream.on('error', reject);
entry.pipe(tempStream);
return tempStream.on('finish', () => {
return resolve();
});
});
});
}
} else if (entry.path.match(/xl\/worksheets\/_rels\/sheet\d+[.]xml.rels/)) {
match = entry.path.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
sheetNo = match[1];
yield* this._parseHyperlinks(iterateStream(entry), sheetNo);
}
break;
}
entry.autodrain();
}
for (const {
sheetNo,
path,
tempFileCleanupCallback
} of waitingWorkSheets) {
let fileStream = fs.createReadStream(path);
// TODO: Remove once node v8 is deprecated
// Detect and upgrade old fileStreams
if (!fileStream[Symbol.asyncIterator]) {
fileStream = fileStream.pipe(new PassThrough());
}
yield* this._parseWorksheet(fileStream, sheetNo);
tempFileCleanupCallback();
}
}
_emitEntry(payload) {
if (this.options.entries === 'emit') {
this.emit('entry', payload);
}
}
async _parseRels(entry) {
const xform = new RelationshipsXform();
this.workbookRels = await xform.parseStream(iterateStream(entry));
}
async _parseWorkbook(entry) {
this._emitEntry({
type: 'workbook'
});
const workbook = new WorkbookXform();
await workbook.parseStream(iterateStream(entry));
this.properties = workbook.map.workbookPr;
this.model = workbook.model;
}
async *_parseSharedStrings(entry) {
this._emitEntry({
type: 'shared-strings'
});
switch (this.options.sharedStrings) {
case 'cache':
this.sharedStrings = [];
break;
case 'emit':
break;
default:
return;
}
let text = null;
let richText = [];
let index = 0;
let font = null;
for await (const events of parseSax(iterateStream(entry))) {
for (const {
eventType,
value
} of events) {
if (eventType === 'opentag') {
const node = value;
switch (node.name) {
case 'b':
font = font || {};
font.bold = true;
break;
case 'charset':
font = font || {};
font.charset = parseInt(node.attributes.charset, 10);
break;
case 'color':
font = font || {};
font.color = {};
if (node.attributes.rgb) {
font.color.argb = node.attributes.argb;
}
if (node.attributes.val) {
font.color.argb = node.attributes.val;
}
if (node.attributes.theme) {
font.color.theme = node.attributes.theme;
}
break;
case 'family':
font = font || {};
font.family = parseInt(node.attributes.val, 10);
break;
case 'i':
font = font || {};
font.italic = true;
break;
case 'outline':
font = font || {};
font.outline = true;
break;
case 'rFont':
font = font || {};
font.name = node.value;
break;
case 'si':
font = null;
richText = [];
text = null;
break;
case 'sz':
font = font || {};
font.size = parseInt(node.attributes.val, 10);
break;
case 'strike':
break;
case 't':
text = null;
break;
case 'u':
font = font || {};
font.underline = true;
break;
case 'vertAlign':
font = font || {};
font.vertAlign = node.attributes.val;
break;
}
} else if (eventType === 'text') {
text = text ? text + value : value;
} else if (eventType === 'closetag') {
const node = value;
switch (node.name) {
case 'r':
richText.push({
font,
text
});
font = null;
text = null;
break;
case 'si':
if (this.options.sharedStrings === 'cache') {
this.sharedStrings.push(richText.length ? {
richText
} : text);
} else if (this.options.sharedStrings === 'emit') {
yield {
index: index++,
text: richText.length ? {
richText
} : text
};
}
richText = [];
font = null;
text = null;
break;
}
}
}
}
}
async _parseStyles(entry) {
this._emitEntry({
type: 'styles'
});
if (this.options.styles === 'cache') {
this.styles = new StyleManager();
await this.styles.parseStream(iterateStream(entry));
}
}
*_parseWorksheet(iterator, sheetNo) {
this._emitEntry({
type: 'worksheet',
id: sheetNo
});
const worksheetReader = new WorksheetReader({
workbook: this,
id: sheetNo,
iterator,
options: this.options
});
const matchingRel = (this.workbookRels || []).find(rel => rel.Target === `worksheets/sheet${sheetNo}.xml`);
const matchingSheet = matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id);
if (matchingSheet) {
worksheetReader.id = matchingSheet.id;
worksheetReader.name = matchingSheet.name;
worksheetReader.state = matchingSheet.state;
}
if (this.options.worksheets === 'emit') {
yield {
eventType: 'worksheet',
value: worksheetReader
};
}
}
*_parseHyperlinks(iterator, sheetNo) {
this._emitEntry({
type: 'hyperlinks',
id: sheetNo
});
const hyperlinksReader = new HyperlinkReader({
workbook: this,
id: sheetNo,
iterator,
options: this.options
});
if (this.options.hyperlinks === 'emit') {
yield {
eventType: 'hyperlinks',
value: hyperlinksReader
};
}
}
}
// for reference - these are the valid values for options
WorkbookReader.Options = {
worksheets: ['emit', 'ignore'],
sharedStrings: ['cache', 'emit', 'ignore'],
hyperlinks: ['cache', 'emit', 'ignore'],
styles: ['cache', 'ignore'],
entries: ['emit', 'ignore']
};
module.exports = WorkbookReader;
//# sourceMappingURL=workbook-reader.js.map