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;
 |