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.
		
		
		
		
		
			
		
			
				
					
					
						
							692 lines
						
					
					
						
							22 KiB
						
					
					
				
			
		
		
	
	
							692 lines
						
					
					
						
							22 KiB
						
					
					
				| const fs = require('fs');
 | |
| const JSZip = require('jszip');
 | |
| const {PassThrough} = require('readable-stream');
 | |
| const ZipStream = require('../utils/zip-stream');
 | |
| const StreamBuf = require('../utils/stream-buf');
 | |
| 
 | |
| const utils = require('../utils/utils');
 | |
| const XmlStream = require('../utils/xml-stream');
 | |
| const {bufferToString} = require('../utils/browser-buffer-decode');
 | |
| 
 | |
| const StylesXform = require('./xform/style/styles-xform');
 | |
| 
 | |
| const CoreXform = require('./xform/core/core-xform');
 | |
| const SharedStringsXform = require('./xform/strings/shared-strings-xform');
 | |
| const RelationshipsXform = require('./xform/core/relationships-xform');
 | |
| const ContentTypesXform = require('./xform/core/content-types-xform');
 | |
| const AppXform = require('./xform/core/app-xform');
 | |
| const WorkbookXform = require('./xform/book/workbook-xform');
 | |
| const WorksheetXform = require('./xform/sheet/worksheet-xform');
 | |
| const DrawingXform = require('./xform/drawing/drawing-xform');
 | |
| const TableXform = require('./xform/table/table-xform');
 | |
| const CommentsXform = require('./xform/comment/comments-xform');
 | |
| const VmlNotesXform = require('./xform/comment/vml-notes-xform');
 | |
| 
 | |
| const theme1Xml = require('./xml/theme1.js');
 | |
| 
 | |
| function fsReadFileAsync(filename, options) {
 | |
|   return new Promise((resolve, reject) => {
 | |
|     fs.readFile(filename, options, (error, data) => {
 | |
|       if (error) {
 | |
|         reject(error);
 | |
|       } else {
 | |
|         resolve(data);
 | |
|       }
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| class XLSX {
 | |
|   constructor(workbook) {
 | |
|     this.workbook = workbook;
 | |
|   }
 | |
| 
 | |
|   // ===============================================================================
 | |
|   // Workbook
 | |
|   // =========================================================================
 | |
|   // Read
 | |
| 
 | |
|   async readFile(filename, options) {
 | |
|     if (!(await utils.fs.exists(filename))) {
 | |
|       throw new Error(`File not found: ${filename}`);
 | |
|     }
 | |
|     const stream = fs.createReadStream(filename);
 | |
|     try {
 | |
|       const workbook = await this.read(stream, options);
 | |
|       stream.close();
 | |
|       return workbook;
 | |
|     } catch (error) {
 | |
|       stream.close();
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   parseRels(stream) {
 | |
|     const xform = new RelationshipsXform();
 | |
|     return xform.parseStream(stream);
 | |
|   }
 | |
| 
 | |
|   parseWorkbook(stream) {
 | |
|     const xform = new WorkbookXform();
 | |
|     return xform.parseStream(stream);
 | |
|   }
 | |
| 
 | |
|   parseSharedStrings(stream) {
 | |
|     const xform = new SharedStringsXform();
 | |
|     return xform.parseStream(stream);
 | |
|   }
 | |
| 
 | |
|   reconcile(model, options) {
 | |
|     const workbookXform = new WorkbookXform();
 | |
|     const worksheetXform = new WorksheetXform(options);
 | |
|     const drawingXform = new DrawingXform();
 | |
|     const tableXform = new TableXform();
 | |
| 
 | |
|     workbookXform.reconcile(model);
 | |
| 
 | |
|     // reconcile drawings with their rels
 | |
|     const drawingOptions = {
 | |
|       media: model.media,
 | |
|       mediaIndex: model.mediaIndex,
 | |
|     };
 | |
|     Object.keys(model.drawings).forEach(name => {
 | |
|       const drawing = model.drawings[name];
 | |
|       const drawingRel = model.drawingRels[name];
 | |
|       if (drawingRel) {
 | |
|         drawingOptions.rels = drawingRel.reduce((o, rel) => {
 | |
|           o[rel.Id] = rel;
 | |
|           return o;
 | |
|         }, {});
 | |
|         (drawing.anchors || []).forEach(anchor => {
 | |
|           const hyperlinks = anchor.picture && anchor.picture.hyperlinks;
 | |
|           if (hyperlinks && drawingOptions.rels[hyperlinks.rId]) {
 | |
|             hyperlinks.hyperlink = drawingOptions.rels[hyperlinks.rId].Target;
 | |
|             delete hyperlinks.rId;
 | |
|           }
 | |
|         });
 | |
|         drawingXform.reconcile(drawing, drawingOptions);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // reconcile tables with the default styles
 | |
|     const tableOptions = {
 | |
|       styles: model.styles,
 | |
|     };
 | |
|     Object.values(model.tables).forEach(table => {
 | |
|       tableXform.reconcile(table, tableOptions);
 | |
|     });
 | |
| 
 | |
|     const sheetOptions = {
 | |
|       styles: model.styles,
 | |
|       sharedStrings: model.sharedStrings,
 | |
|       media: model.media,
 | |
|       mediaIndex: model.mediaIndex,
 | |
|       date1904: model.properties && model.properties.date1904,
 | |
|       drawings: model.drawings,
 | |
|       comments: model.comments,
 | |
|       tables: model.tables,
 | |
|       vmlDrawings: model.vmlDrawings,
 | |
|     };
 | |
|     model.worksheets.forEach(worksheet => {
 | |
|       worksheet.relationships = model.worksheetRels[worksheet.sheetNo];
 | |
|       worksheetXform.reconcile(worksheet, sheetOptions);
 | |
|     });
 | |
| 
 | |
|     // delete unnecessary parts
 | |
|     delete model.worksheetHash;
 | |
|     delete model.worksheetRels;
 | |
|     delete model.globalRels;
 | |
|     delete model.sharedStrings;
 | |
|     delete model.workbookRels;
 | |
|     delete model.sheetDefs;
 | |
|     delete model.styles;
 | |
|     delete model.mediaIndex;
 | |
|     delete model.drawings;
 | |
|     delete model.drawingRels;
 | |
|     delete model.vmlDrawings;
 | |
|   }
 | |
| 
 | |
|   async _processWorksheetEntry(stream, model, sheetNo, options, path) {
 | |
|     const xform = new WorksheetXform(options);
 | |
|     const worksheet = await xform.parseStream(stream);
 | |
|     worksheet.sheetNo = sheetNo;
 | |
|     model.worksheetHash[path] = worksheet;
 | |
|     model.worksheets.push(worksheet);
 | |
|   }
 | |
| 
 | |
|   async _processCommentEntry(stream, model, name) {
 | |
|     const xform = new CommentsXform();
 | |
|     const comments = await xform.parseStream(stream);
 | |
|     model.comments[`../${name}.xml`] = comments;
 | |
|   }
 | |
| 
 | |
|   async _processTableEntry(stream, model, name) {
 | |
|     const xform = new TableXform();
 | |
|     const table = await xform.parseStream(stream);
 | |
|     model.tables[`../tables/${name}.xml`] = table;
 | |
|   }
 | |
| 
 | |
|   async _processWorksheetRelsEntry(stream, model, sheetNo) {
 | |
|     const xform = new RelationshipsXform();
 | |
|     const relationships = await xform.parseStream(stream);
 | |
|     model.worksheetRels[sheetNo] = relationships;
 | |
|   }
 | |
| 
 | |
|   async _processMediaEntry(entry, model, filename) {
 | |
|     const lastDot = filename.lastIndexOf('.');
 | |
|     // if we can't determine extension, ignore it
 | |
|     if (lastDot >= 1) {
 | |
|       const extension = filename.substr(lastDot + 1);
 | |
|       const name = filename.substr(0, lastDot);
 | |
|       await new Promise((resolve, reject) => {
 | |
|         const streamBuf = new StreamBuf();
 | |
|         streamBuf.on('finish', () => {
 | |
|           model.mediaIndex[filename] = model.media.length;
 | |
|           model.mediaIndex[name] = model.media.length;
 | |
|           const medium = {
 | |
|             type: 'image',
 | |
|             name,
 | |
|             extension,
 | |
|             buffer: streamBuf.toBuffer(),
 | |
|           };
 | |
|           model.media.push(medium);
 | |
|           resolve();
 | |
|         });
 | |
|         entry.on('error', error => {
 | |
|           reject(error);
 | |
|         });
 | |
|         entry.pipe(streamBuf);
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async _processDrawingEntry(entry, model, name) {
 | |
|     const xform = new DrawingXform();
 | |
|     const drawing = await xform.parseStream(entry);
 | |
|     model.drawings[name] = drawing;
 | |
|   }
 | |
| 
 | |
|   async _processDrawingRelsEntry(entry, model, name) {
 | |
|     const xform = new RelationshipsXform();
 | |
|     const relationships = await xform.parseStream(entry);
 | |
|     model.drawingRels[name] = relationships;
 | |
|   }
 | |
| 
 | |
|   async _processVmlDrawingEntry(entry, model, name) {
 | |
|     const xform = new VmlNotesXform();
 | |
|     const vmlDrawing = await xform.parseStream(entry);
 | |
|     model.vmlDrawings[`../drawings/${name}.vml`] = vmlDrawing;
 | |
|   }
 | |
| 
 | |
|   async _processThemeEntry(entry, model, name) {
 | |
|     await new Promise((resolve, reject) => {
 | |
|       // TODO: stream entry into buffer and store the xml in the model.themes[]
 | |
|       const stream = new StreamBuf();
 | |
|       entry.on('error', reject);
 | |
|       stream.on('error', reject);
 | |
|       stream.on('finish', () => {
 | |
|         model.themes[name] = stream.read().toString();
 | |
|         resolve();
 | |
|       });
 | |
|       entry.pipe(stream);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @deprecated since version 4.0. You should use `#read` instead. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md
 | |
|    */
 | |
|   createInputStream() {
 | |
|     throw new Error(
 | |
|       '`XLSX#createInputStream` is deprecated. You should use `XLSX#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md'
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   async read(stream, options) {
 | |
|     // TODO: Remove once node v8 is deprecated
 | |
|     // Detect and upgrade old streams
 | |
|     if (!stream[Symbol.asyncIterator] && stream.pipe) {
 | |
|       stream = stream.pipe(new PassThrough());
 | |
|     }
 | |
|     const chunks = [];
 | |
|     for await (const chunk of stream) {
 | |
|       chunks.push(chunk);
 | |
|     }
 | |
|     return this.load(Buffer.concat(chunks), options);
 | |
|   }
 | |
| 
 | |
|   async load(data, options) {
 | |
|     let buffer;
 | |
|     if (options && options.base64) {
 | |
|       buffer = Buffer.from(data.toString(), 'base64');
 | |
|     } else {
 | |
|       buffer = data;
 | |
|     }
 | |
| 
 | |
|     const model = {
 | |
|       worksheets: [],
 | |
|       worksheetHash: {},
 | |
|       worksheetRels: [],
 | |
|       themes: {},
 | |
|       media: [],
 | |
|       mediaIndex: {},
 | |
|       drawings: {},
 | |
|       drawingRels: {},
 | |
|       comments: {},
 | |
|       tables: {},
 | |
|       vmlDrawings: {},
 | |
|     };
 | |
| 
 | |
|     const zip = await JSZip.loadAsync(buffer);
 | |
|     for (const entry of Object.values(zip.files)) {
 | |
|       /* eslint-disable no-await-in-loop */
 | |
|       if (!entry.dir) {
 | |
|         let entryName = entry.name;
 | |
|         if (entryName[0] === '/') {
 | |
|           entryName = entryName.substr(1);
 | |
|         }
 | |
|         let stream;
 | |
|         if (entryName.match(/xl\/media\//) ||
 | |
|           // themes are not parsed as stream
 | |
|           entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/)) {
 | |
|           stream = new PassThrough();
 | |
|           stream.write(await entry.async('nodebuffer'));
 | |
|         } else {
 | |
|           // use object mode to avoid buffer-string convention
 | |
|           stream = new PassThrough({
 | |
|             writableObjectMode: true,
 | |
|             readableObjectMode: true,
 | |
|           });
 | |
|           let content;
 | |
|           // https://www.npmjs.com/package/process
 | |
|           if (process.browser) {
 | |
|             // running in browser, use TextDecoder if possible
 | |
|             content = bufferToString(await entry.async('nodebuffer'));
 | |
|           } else {
 | |
|             // running in node.js
 | |
|             content = await entry.async('string');
 | |
|           }
 | |
|           const chunkSize = 16 * 1024;
 | |
|           for (let i = 0; i < content.length; i += chunkSize) {
 | |
|             stream.write(content.substring(i, i + chunkSize));
 | |
|           }
 | |
|         }
 | |
|         stream.end();
 | |
|         switch (entryName) {
 | |
|           case '_rels/.rels':
 | |
|             model.globalRels = await this.parseRels(stream);
 | |
|             break;
 | |
| 
 | |
|           case 'xl/workbook.xml': {
 | |
|             const workbook = await this.parseWorkbook(stream);
 | |
|             model.sheets = workbook.sheets;
 | |
|             model.definedNames = workbook.definedNames;
 | |
|             model.views = workbook.views;
 | |
|             model.properties = workbook.properties;
 | |
|             model.calcProperties = workbook.calcProperties;
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           case 'xl/_rels/workbook.xml.rels':
 | |
|             model.workbookRels = await this.parseRels(stream);
 | |
|             break;
 | |
| 
 | |
|           case 'xl/sharedStrings.xml':
 | |
|             model.sharedStrings = new SharedStringsXform();
 | |
|             await model.sharedStrings.parseStream(stream);
 | |
|             break;
 | |
| 
 | |
|           case 'xl/styles.xml':
 | |
|             model.styles = new StylesXform();
 | |
|             await model.styles.parseStream(stream);
 | |
|             break;
 | |
| 
 | |
|           case 'docProps/app.xml': {
 | |
|             const appXform = new AppXform();
 | |
|             const appProperties = await appXform.parseStream(stream);
 | |
|             model.company = appProperties.company;
 | |
|             model.manager = appProperties.manager;
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           case 'docProps/core.xml': {
 | |
|             const coreXform = new CoreXform();
 | |
|             const coreProperties = await coreXform.parseStream(stream);
 | |
|             Object.assign(model, coreProperties);
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           default: {
 | |
|             let match = entryName.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
 | |
|             if (match) {
 | |
|               await this._processWorksheetEntry(stream, model, match[1], options, entryName);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
 | |
|             if (match) {
 | |
|               await this._processWorksheetRelsEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/);
 | |
|             if (match) {
 | |
|               await this._processThemeEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/media\/([a-zA-Z0-9]+[.][a-zA-Z0-9]{3,4})$/);
 | |
|             if (match) {
 | |
|               await this._processMediaEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/drawings\/([a-zA-Z0-9]+)[.]xml/);
 | |
|             if (match) {
 | |
|               await this._processDrawingEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/(comments\d+)[.]xml/);
 | |
|             if (match) {
 | |
|               await this._processCommentEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/tables\/(table\d+)[.]xml/);
 | |
|             if (match) {
 | |
|               await this._processTableEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/drawings\/_rels\/([a-zA-Z0-9]+)[.]xml[.]rels/);
 | |
|             if (match) {
 | |
|               await this._processDrawingRelsEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|             match = entryName.match(/xl\/drawings\/(vmlDrawing\d+)[.]vml/);
 | |
|             if (match) {
 | |
|               await this._processVmlDrawingEntry(stream, model, match[1]);
 | |
|               break;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.reconcile(model, options);
 | |
| 
 | |
|     // apply model
 | |
|     this.workbook.model = model;
 | |
|     return this.workbook;
 | |
|   }
 | |
| 
 | |
|   // =========================================================================
 | |
|   // Write
 | |
| 
 | |
|   async addMedia(zip, model) {
 | |
|     await Promise.all(
 | |
|       model.media.map(async medium => {
 | |
|         if (medium.type === 'image') {
 | |
|           const filename = `xl/media/${medium.name}.${medium.extension}`;
 | |
|           if (medium.filename) {
 | |
|             const data = await fsReadFileAsync(medium.filename);
 | |
|             return zip.append(data, {name: filename});
 | |
|           }
 | |
|           if (medium.buffer) {
 | |
|             return zip.append(medium.buffer, {name: filename});
 | |
|           }
 | |
|           if (medium.base64) {
 | |
|             const dataimg64 = medium.base64;
 | |
|             const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
 | |
|             return zip.append(content, {name: filename, base64: true});
 | |
|           }
 | |
|         }
 | |
|         throw new Error('Unsupported media');
 | |
|       })
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   addDrawings(zip, model) {
 | |
|     const drawingXform = new DrawingXform();
 | |
|     const relsXform = new RelationshipsXform();
 | |
| 
 | |
|     model.worksheets.forEach(worksheet => {
 | |
|       const {drawing} = worksheet;
 | |
|       if (drawing) {
 | |
|         drawingXform.prepare(drawing, {});
 | |
|         let xml = drawingXform.toXml(drawing);
 | |
|         zip.append(xml, {name: `xl/drawings/${drawing.name}.xml`});
 | |
| 
 | |
|         xml = relsXform.toXml(drawing.rels);
 | |
|         zip.append(xml, {name: `xl/drawings/_rels/${drawing.name}.xml.rels`});
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addTables(zip, model) {
 | |
|     const tableXform = new TableXform();
 | |
| 
 | |
|     model.worksheets.forEach(worksheet => {
 | |
|       const {tables} = worksheet;
 | |
|       tables.forEach(table => {
 | |
|         tableXform.prepare(table, {});
 | |
|         const tableXml = tableXform.toXml(table);
 | |
|         zip.append(tableXml, {name: `xl/tables/${table.target}`});
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async addContentTypes(zip, model) {
 | |
|     const xform = new ContentTypesXform();
 | |
|     const xml = xform.toXml(model);
 | |
|     zip.append(xml, {name: '[Content_Types].xml'});
 | |
|   }
 | |
| 
 | |
|   async addApp(zip, model) {
 | |
|     const xform = new AppXform();
 | |
|     const xml = xform.toXml(model);
 | |
|     zip.append(xml, {name: 'docProps/app.xml'});
 | |
|   }
 | |
| 
 | |
|   async addCore(zip, model) {
 | |
|     const coreXform = new CoreXform();
 | |
|     zip.append(coreXform.toXml(model), {name: 'docProps/core.xml'});
 | |
|   }
 | |
| 
 | |
|   async addThemes(zip, model) {
 | |
|     const themes = model.themes || {theme1: theme1Xml};
 | |
|     Object.keys(themes).forEach(name => {
 | |
|       const xml = themes[name];
 | |
|       const path = `xl/theme/${name}.xml`;
 | |
|       zip.append(xml, {name: path});
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async addOfficeRels(zip) {
 | |
|     const xform = new RelationshipsXform();
 | |
|     const xml = xform.toXml([
 | |
|       {Id: 'rId1', Type: XLSX.RelType.OfficeDocument, Target: 'xl/workbook.xml'},
 | |
|       {Id: 'rId2', Type: XLSX.RelType.CoreProperties, Target: 'docProps/core.xml'},
 | |
|       {Id: 'rId3', Type: XLSX.RelType.ExtenderProperties, Target: 'docProps/app.xml'},
 | |
|     ]);
 | |
|     zip.append(xml, {name: '_rels/.rels'});
 | |
|   }
 | |
| 
 | |
|   async addWorkbookRels(zip, model) {
 | |
|     let count = 1;
 | |
|     const relationships = [
 | |
|       {Id: `rId${count++}`, Type: XLSX.RelType.Styles, Target: 'styles.xml'},
 | |
|       {Id: `rId${count++}`, Type: XLSX.RelType.Theme, Target: 'theme/theme1.xml'},
 | |
|     ];
 | |
|     if (model.sharedStrings.count) {
 | |
|       relationships.push({
 | |
|         Id: `rId${count++}`,
 | |
|         Type: XLSX.RelType.SharedStrings,
 | |
|         Target: 'sharedStrings.xml',
 | |
|       });
 | |
|     }
 | |
|     model.worksheets.forEach(worksheet => {
 | |
|       worksheet.rId = `rId${count++}`;
 | |
|       relationships.push({
 | |
|         Id: worksheet.rId,
 | |
|         Type: XLSX.RelType.Worksheet,
 | |
|         Target: `worksheets/sheet${worksheet.id}.xml`,
 | |
|       });
 | |
|     });
 | |
|     const xform = new RelationshipsXform();
 | |
|     const xml = xform.toXml(relationships);
 | |
|     zip.append(xml, {name: 'xl/_rels/workbook.xml.rels'});
 | |
|   }
 | |
| 
 | |
|   async addSharedStrings(zip, model) {
 | |
|     if (model.sharedStrings && model.sharedStrings.count) {
 | |
|       zip.append(model.sharedStrings.xml, {name: 'xl/sharedStrings.xml'});
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async addStyles(zip, model) {
 | |
|     const {xml} = model.styles;
 | |
|     if (xml) {
 | |
|       zip.append(xml, {name: 'xl/styles.xml'});
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async addWorkbook(zip, model) {
 | |
|     const xform = new WorkbookXform();
 | |
|     zip.append(xform.toXml(model), {name: 'xl/workbook.xml'});
 | |
|   }
 | |
| 
 | |
|   async addWorksheets(zip, model) {
 | |
|     // preparation phase
 | |
|     const worksheetXform = new WorksheetXform();
 | |
|     const relationshipsXform = new RelationshipsXform();
 | |
|     const commentsXform = new CommentsXform();
 | |
|     const vmlNotesXform = new VmlNotesXform();
 | |
| 
 | |
|     // write sheets
 | |
|     model.worksheets.forEach(worksheet => {
 | |
|       let xmlStream = new XmlStream();
 | |
|       worksheetXform.render(xmlStream, worksheet);
 | |
|       zip.append(xmlStream.xml, {name: `xl/worksheets/sheet${worksheet.id}.xml`});
 | |
| 
 | |
|       if (worksheet.rels && worksheet.rels.length) {
 | |
|         xmlStream = new XmlStream();
 | |
|         relationshipsXform.render(xmlStream, worksheet.rels);
 | |
|         zip.append(xmlStream.xml, {name: `xl/worksheets/_rels/sheet${worksheet.id}.xml.rels`});
 | |
|       }
 | |
| 
 | |
|       if (worksheet.comments.length > 0) {
 | |
|         xmlStream = new XmlStream();
 | |
|         commentsXform.render(xmlStream, worksheet);
 | |
|         zip.append(xmlStream.xml, {name: `xl/comments${worksheet.id}.xml`});
 | |
| 
 | |
|         xmlStream = new XmlStream();
 | |
|         vmlNotesXform.render(xmlStream, worksheet);
 | |
|         zip.append(xmlStream.xml, {name: `xl/drawings/vmlDrawing${worksheet.id}.vml`});
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _finalize(zip) {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       zip.on('finish', () => {
 | |
|         resolve(this);
 | |
|       });
 | |
|       zip.on('error', reject);
 | |
|       zip.finalize();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   prepareModel(model, options) {
 | |
|     // ensure following properties have sane values
 | |
|     model.creator = model.creator || 'ExcelJS';
 | |
|     model.lastModifiedBy = model.lastModifiedBy || 'ExcelJS';
 | |
|     model.created = model.created || new Date();
 | |
|     model.modified = model.modified || new Date();
 | |
| 
 | |
|     model.useSharedStrings =
 | |
|       options.useSharedStrings !== undefined ? options.useSharedStrings : true;
 | |
|     model.useStyles = options.useStyles !== undefined ? options.useStyles : true;
 | |
| 
 | |
|     // Manage the shared strings
 | |
|     model.sharedStrings = new SharedStringsXform();
 | |
| 
 | |
|     // add a style manager to handle cell formats, fonts, etc.
 | |
|     model.styles = model.useStyles ? new StylesXform(true) : new StylesXform.Mock();
 | |
| 
 | |
|     // prepare all of the things before the render
 | |
|     const workbookXform = new WorkbookXform();
 | |
|     const worksheetXform = new WorksheetXform();
 | |
| 
 | |
|     workbookXform.prepare(model);
 | |
| 
 | |
|     const worksheetOptions = {
 | |
|       sharedStrings: model.sharedStrings,
 | |
|       styles: model.styles,
 | |
|       date1904: model.properties.date1904,
 | |
|       drawingsCount: 0,
 | |
|       media: model.media,
 | |
|     };
 | |
|     worksheetOptions.drawings = model.drawings = [];
 | |
|     worksheetOptions.commentRefs = model.commentRefs = [];
 | |
|     let tableCount = 0;
 | |
|     model.tables = [];
 | |
|     model.worksheets.forEach(worksheet => {
 | |
|       // assign unique filenames to tables
 | |
|       worksheet.tables.forEach(table => {
 | |
|         tableCount++;
 | |
|         table.target = `table${tableCount}.xml`;
 | |
|         table.id = tableCount;
 | |
|         model.tables.push(table);
 | |
|       });
 | |
| 
 | |
|       worksheetXform.prepare(worksheet, worksheetOptions);
 | |
|     });
 | |
| 
 | |
|     // TODO: workbook drawing list
 | |
|   }
 | |
| 
 | |
|   async write(stream, options) {
 | |
|     options = options || {};
 | |
|     const {model} = this.workbook;
 | |
|     const zip = new ZipStream.ZipWriter(options.zip);
 | |
|     zip.pipe(stream);
 | |
| 
 | |
|     this.prepareModel(model, options);
 | |
| 
 | |
|     // render
 | |
|     await this.addContentTypes(zip, model);
 | |
|     await this.addOfficeRels(zip, model);
 | |
|     await this.addWorkbookRels(zip, model);
 | |
|     await this.addWorksheets(zip, model);
 | |
|     await this.addSharedStrings(zip, model); // always after worksheets
 | |
|     await this.addDrawings(zip, model);
 | |
|     await this.addTables(zip, model);
 | |
|     await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]);
 | |
|     await this.addMedia(zip, model);
 | |
|     await Promise.all([this.addApp(zip, model), this.addCore(zip, model)]);
 | |
|     await this.addWorkbook(zip, model);
 | |
|     return this._finalize(zip);
 | |
|   }
 | |
| 
 | |
|   writeFile(filename, options) {
 | |
|     const stream = fs.createWriteStream(filename);
 | |
| 
 | |
|     return new Promise((resolve, reject) => {
 | |
|       stream.on('finish', () => {
 | |
|         resolve();
 | |
|       });
 | |
|       stream.on('error', error => {
 | |
|         reject(error);
 | |
|       });
 | |
| 
 | |
|       this.write(stream, options).then(() => {
 | |
|         stream.end();
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async writeBuffer(options) {
 | |
|     const stream = new StreamBuf();
 | |
|     await this.write(stream, options);
 | |
|     return stream.read();
 | |
|   }
 | |
| }
 | |
| 
 | |
| XLSX.RelType = require('./rel-type');
 | |
| 
 | |
| module.exports = XLSX;
 |