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.
		
		
		
		
		
			
		
			
				
					340 lines
				
				10 KiB
			
		
		
			
		
	
	
					340 lines
				
				10 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								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, options = {}) {
							 | 
						||
| 
								 | 
							
								    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);
							 | 
						||
| 
								 | 
							
								                  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;
							 |