exceljs
Version:
Excel Workbook Manager - Read and Write xlsx and csv Files.
365 lines (330 loc) • 10.6 kB
JavaScript
const {EventEmitter} = require('events');
const parseSax = require('../../utils/parse-sax');
const _ = require('../../utils/under-dash');
const utils = require('../../utils/utils');
const colCache = require('../../utils/col-cache');
const Dimensions = require('../../doc/range');
const Row = require('../../doc/row');
const Column = require('../../doc/column');
class WorksheetReader extends EventEmitter {
constructor({workbook, id, iterator, options}) {
super();
this.workbook = workbook;
this.id = id;
this.iterator = iterator;
this.options = options || {};
// and a name
this.name = `Sheet${this.id}`;
// column definitions
this._columns = null;
this._keys = {};
// keep a record of dimensions
this._dimensions = new Dimensions();
}
// destroy - not a valid operation for a streaming writer
// even though some streamers might be able to, it's a bad idea.
destroy() {
throw new Error('Invalid Operation: destroy');
}
// return the current dimensions of the writer
get dimensions() {
return this._dimensions;
}
// =========================================================================
// Columns
// get the current columns array.
get columns() {
return this._columns;
}
// get a single column by col number. If it doesn't exist, it and any gaps before it
// are created.
getColumn(c) {
if (typeof c === 'string') {
// if it matches a key'd column, return that
const col = this._keys[c];
if (col) {
return col;
}
// otherise, assume letter
c = colCache.l2n(c);
}
if (!this._columns) {
this._columns = [];
}
if (c > this._columns.length) {
let n = this._columns.length + 1;
while (n <= c) {
this._columns.push(new Column(this, n++));
}
}
return this._columns[c - 1];
}
getColumnKey(key) {
return this._keys[key];
}
setColumnKey(key, value) {
this._keys[key] = value;
}
deleteColumnKey(key) {
delete this._keys[key];
}
eachColumnKey(f) {
_.each(this._keys, f);
}
async read() {
try {
for await (const events of this.parse()) {
for (const {eventType, value} of events) {
this.emit(eventType, value);
}
}
this.emit('finished');
} catch (error) {
this.emit('error', error);
}
}
async *[Symbol.asyncIterator]() {
for await (const events of this.parse()) {
for (const {eventType, value} of events) {
if (eventType === 'row') {
yield value;
}
}
}
}
async *parse() {
const {iterator, options} = this;
let emitSheet = false;
let emitHyperlinks = false;
let hyperlinks = null;
switch (options.worksheets) {
case 'emit':
emitSheet = true;
break;
case 'prep':
break;
default:
break;
}
switch (options.hyperlinks) {
case 'emit':
emitHyperlinks = true;
break;
case 'cache':
this.hyperlinks = hyperlinks = {};
break;
default:
break;
}
if (!emitSheet && !emitHyperlinks && !hyperlinks) {
return;
}
// references
const {sharedStrings, styles, properties} = this.workbook;
// xml position
let inCols = false;
let inRows = false;
let inHyperlinks = false;
// parse state
let cols = null;
let row = null;
let c = null;
let current = null;
for await (const events of parseSax(iterator)) {
const worksheetEvents = [];
for (const {eventType, value} of events) {
if (eventType === 'opentag') {
const node = value;
if (emitSheet) {
switch (node.name) {
case 'cols':
inCols = true;
cols = [];
break;
case 'sheetData':
inRows = true;
break;
case 'col':
if (inCols) {
cols.push({
min: parseInt(node.attributes.min, 10),
max: parseInt(node.attributes.max, 10),
width: parseFloat(node.attributes.width),
styleId: parseInt(node.attributes.style || '0', 10),
});
}
break;
case 'row':
if (inRows) {
const r = parseInt(node.attributes.r, 10);
row = new Row(this, r);
if (node.attributes.ht) {
row.height = parseFloat(node.attributes.ht);
}
if (node.attributes.s) {
const styleId = parseInt(node.attributes.s, 10);
const style = styles.getStyleModel(styleId);
if (style) {
row.style = style;
}
}
}
break;
case 'c':
if (row) {
c = {
ref: node.attributes.r,
s: parseInt(node.attributes.s, 10),
t: node.attributes.t,
};
}
break;
case 'f':
if (c) {
current = c.f = {text: ''};
}
break;
case 'v':
if (c) {
current = c.v = {text: ''};
}
break;
case 'mergeCell':
break;
default:
break;
}
}
// =================================================================
//
if (emitHyperlinks || hyperlinks) {
switch (node.name) {
case 'hyperlinks':
inHyperlinks = true;
break;
case 'hyperlink':
if (inHyperlinks) {
const hyperlink = {
ref: node.attributes.ref,
rId: node.attributes['r:id'],
};
if (emitHyperlinks) {
worksheetEvents.push({eventType: 'hyperlink', value: hyperlink});
} else {
hyperlinks[hyperlink.ref] = hyperlink;
}
}
break;
default:
break;
}
}
} else if (eventType === 'text') {
// only text data is for sheet values
if (emitSheet) {
if (current) {
current.text += value;
}
}
} else if (eventType === 'closetag') {
const node = value;
if (emitSheet) {
switch (node.name) {
case 'cols':
inCols = false;
this._columns = Column.fromModel(cols);
break;
case 'sheetData':
inRows = false;
break;
case 'row':
this._dimensions.expandRow(row);
worksheetEvents.push({eventType: 'row', value: row});
row = null;
break;
case 'c':
if (row && c) {
const address = colCache.decodeAddress(c.ref);
const cell = row.getCell(address.col);
if (c.s) {
const style = styles.getStyleModel(c.s);
if (style) {
cell.style = style;
}
}
if (c.f) {
const cellValue = {
formula: c.f.text,
};
if (c.v) {
if (c.t === 'str') {
cellValue.result = utils.xmlDecode(c.v.text);
} else {
cellValue.result = parseFloat(c.v.text);
}
}
cell.value = cellValue;
} else if (c.v) {
switch (c.t) {
case 's': {
const index = parseInt(c.v.text, 10);
if (sharedStrings) {
cell.value = sharedStrings[index];
} else {
cell.value = {
sharedString: index,
};
}
break;
}
case 'str':
cell.value = utils.xmlDecode(c.v.text);
break;
case 'e':
cell.value = {error: c.v.text};
break;
case 'b':
cell.value = parseInt(c.v.text, 10) !== 0;
break;
default:
if (utils.isDateFmt(cell.numFmt)) {
cell.value = utils.excelToDate(parseFloat(c.v.text), properties.model && properties.model.date1904);
} else {
cell.value = parseFloat(c.v.text);
}
break;
}
}
if (hyperlinks) {
const hyperlink = hyperlinks[c.ref];
if (hyperlink) {
cell.text = cell.value;
cell.value = undefined;
cell.hyperlink = hyperlink;
}
}
c = null;
}
break;
default:
break;
}
}
if (emitHyperlinks || hyperlinks) {
switch (node.name) {
case 'hyperlinks':
inHyperlinks = false;
break;
default:
break;
}
}
}
}
if (worksheetEvents.length > 0) {
yield worksheetEvents;
}
}
}
}
module.exports = WorksheetReader;