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
			| 
											2 years ago
										 | 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; |