You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
368 lines
10 KiB
368 lines
10 KiB
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;
|