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.
		
		
		
		
		
			
		
			
				
					
					
						
							348 lines
						
					
					
						
							9.9 KiB
						
					
					
				
			
		
		
	
	
							348 lines
						
					
					
						
							9.9 KiB
						
					
					
				| const fs = require('fs');
 | |
| const Archiver = require('archiver');
 | |
| 
 | |
| const StreamBuf = require('../../utils/stream-buf');
 | |
| 
 | |
| const RelType = require('../../xlsx/rel-type');
 | |
| const StylesXform = require('../../xlsx/xform/style/styles-xform');
 | |
| const SharedStrings = require('../../utils/shared-strings');
 | |
| const DefinedNames = require('../../doc/defined-names');
 | |
| 
 | |
| const CoreXform = require('../../xlsx/xform/core/core-xform');
 | |
| const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
 | |
| const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
 | |
| const AppXform = require('../../xlsx/xform/core/app-xform');
 | |
| const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
 | |
| const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
 | |
| 
 | |
| const WorksheetWriter = require('./worksheet-writer');
 | |
| 
 | |
| const theme1Xml = require('../../xlsx/xml/theme1.js');
 | |
| 
 | |
| class WorkbookWriter {
 | |
|   constructor(options) {
 | |
|     options = options || {};
 | |
| 
 | |
|     this.created = options.created || new Date();
 | |
|     this.modified = options.modified || this.created;
 | |
|     this.creator = options.creator || 'ExcelJS';
 | |
|     this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
 | |
|     this.lastPrinted = options.lastPrinted;
 | |
| 
 | |
|     // using shared strings creates a smaller xlsx file but may use more memory
 | |
|     this.useSharedStrings = options.useSharedStrings || false;
 | |
|     this.sharedStrings = new SharedStrings();
 | |
| 
 | |
|     // style manager
 | |
|     this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
 | |
| 
 | |
|     // defined names
 | |
|     this._definedNames = new DefinedNames();
 | |
| 
 | |
|     this._worksheets = [];
 | |
|     this.views = [];
 | |
| 
 | |
|     this.zipOptions = options.zip;
 | |
| 
 | |
|     this.media = [];
 | |
|     this.commentRefs = [];
 | |
| 
 | |
|     this.zip = Archiver('zip', this.zipOptions);
 | |
|     if (options.stream) {
 | |
|       this.stream = options.stream;
 | |
|     } else if (options.filename) {
 | |
|       this.stream = fs.createWriteStream(options.filename);
 | |
|     } else {
 | |
|       this.stream = new StreamBuf();
 | |
|     }
 | |
|     this.zip.pipe(this.stream);
 | |
| 
 | |
|     // these bits can be added right now
 | |
|     this.promise = Promise.all([this.addThemes(), this.addOfficeRels()]);
 | |
|   }
 | |
| 
 | |
|   get definedNames() {
 | |
|     return this._definedNames;
 | |
|   }
 | |
| 
 | |
|   _openStream(path) {
 | |
|     const stream = new StreamBuf({bufSize: 65536, batch: true});
 | |
|     this.zip.append(stream, {name: path});
 | |
|     stream.on('finish', () => {
 | |
|       stream.emit('zipped');
 | |
|     });
 | |
|     return stream;
 | |
|   }
 | |
| 
 | |
|   _commitWorksheets() {
 | |
|     const commitWorksheet = function(worksheet) {
 | |
|       if (!worksheet.committed) {
 | |
|         return new Promise(resolve => {
 | |
|           worksheet.stream.on('zipped', () => {
 | |
|             resolve();
 | |
|           });
 | |
|           worksheet.commit();
 | |
|         });
 | |
|       }
 | |
|       return Promise.resolve();
 | |
|     };
 | |
|     // if there are any uncommitted worksheets, commit them now and wait
 | |
|     const promises = this._worksheets.map(commitWorksheet);
 | |
|     if (promises.length) {
 | |
|       return Promise.all(promises);
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   async commit() {
 | |
|     // commit all worksheets, then add suplimentary files
 | |
|     await this.promise;
 | |
|     await this.addMedia();
 | |
|     await this._commitWorksheets();
 | |
|     await Promise.all([
 | |
|       this.addContentTypes(),
 | |
|       this.addApp(),
 | |
|       this.addCore(),
 | |
|       this.addSharedStrings(),
 | |
|       this.addStyles(),
 | |
|       this.addWorkbookRels(),
 | |
|     ]);
 | |
|     await this.addWorkbook();
 | |
|     return this._finalize();
 | |
|   }
 | |
| 
 | |
|   get nextId() {
 | |
|     // find the next unique spot to add worksheet
 | |
|     let i;
 | |
|     for (i = 1; i < this._worksheets.length; i++) {
 | |
|       if (!this._worksheets[i]) {
 | |
|         return i;
 | |
|       }
 | |
|     }
 | |
|     return this._worksheets.length || 1;
 | |
|   }
 | |
| 
 | |
|   addImage(image) {
 | |
|     const id = this.media.length;
 | |
|     const medium = Object.assign({}, image, {type: 'image', name: `image${id}.${image.extension}`});
 | |
|     this.media.push(medium);
 | |
|     return id;
 | |
|   }
 | |
| 
 | |
|   getImage(id) {
 | |
|     return this.media[id];
 | |
|   }
 | |
| 
 | |
|   addWorksheet(name, options) {
 | |
|     // it's possible to add a worksheet with different than default
 | |
|     // shared string handling
 | |
|     // in fact, it's even possible to switch it mid-sheet
 | |
|     options = options || {};
 | |
|     const useSharedStrings =
 | |
|       options.useSharedStrings !== undefined ? options.useSharedStrings : this.useSharedStrings;
 | |
| 
 | |
|     if (options.tabColor) {
 | |
|       // eslint-disable-next-line no-console
 | |
|       console.trace('tabColor option has moved to { properties: tabColor: {...} }');
 | |
|       options.properties = Object.assign(
 | |
|         {
 | |
|           tabColor: options.tabColor,
 | |
|         },
 | |
|         options.properties
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     const id = this.nextId;
 | |
|     name = name || `sheet${id}`;
 | |
| 
 | |
|     const worksheet = new WorksheetWriter({
 | |
|       id,
 | |
|       name,
 | |
|       workbook: this,
 | |
|       useSharedStrings,
 | |
|       properties: options.properties,
 | |
|       state: options.state,
 | |
|       pageSetup: options.pageSetup,
 | |
|       views: options.views,
 | |
|       autoFilter: options.autoFilter,
 | |
|       headerFooter: options.headerFooter,
 | |
|     });
 | |
| 
 | |
|     this._worksheets[id] = worksheet;
 | |
|     return worksheet;
 | |
|   }
 | |
| 
 | |
|   getWorksheet(id) {
 | |
|     if (id === undefined) {
 | |
|       return this._worksheets.find(() => true);
 | |
|     }
 | |
|     if (typeof id === 'number') {
 | |
|       return this._worksheets[id];
 | |
|     }
 | |
|     if (typeof id === 'string') {
 | |
|       return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
 | |
|     }
 | |
|     return undefined;
 | |
|   }
 | |
| 
 | |
|   addStyles() {
 | |
|     return new Promise(resolve => {
 | |
|       this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addThemes() {
 | |
|     return new Promise(resolve => {
 | |
|       this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addOfficeRels() {
 | |
|     return new Promise(resolve => {
 | |
|       const xform = new RelationshipsXform();
 | |
|       const xml = xform.toXml([
 | |
|         {Id: 'rId1', Type: RelType.OfficeDocument, Target: 'xl/workbook.xml'},
 | |
|         {Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
 | |
|         {Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
 | |
|       ]);
 | |
|       this.zip.append(xml, {name: '/_rels/.rels'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addContentTypes() {
 | |
|     return new Promise(resolve => {
 | |
|       const model = {
 | |
|         worksheets: this._worksheets.filter(Boolean),
 | |
|         sharedStrings: this.sharedStrings,
 | |
|         commentRefs: this.commentRefs,
 | |
|         media: this.media,
 | |
|       };
 | |
|       const xform = new ContentTypesXform();
 | |
|       const xml = xform.toXml(model);
 | |
|       this.zip.append(xml, {name: '[Content_Types].xml'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addMedia() {
 | |
|     return Promise.all(
 | |
|       this.media.map(medium => {
 | |
|         if (medium.type === 'image') {
 | |
|           const filename = `xl/media/${medium.name}`;
 | |
|           if (medium.filename) {
 | |
|             return this.zip.file(medium.filename, {name: filename});
 | |
|           }
 | |
|           if (medium.buffer) {
 | |
|             return this.zip.append(medium.buffer, {name: filename});
 | |
|           }
 | |
|           if (medium.base64) {
 | |
|             const dataimg64 = medium.base64;
 | |
|             const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
 | |
|             return this.zip.append(content, {name: filename, base64: true});
 | |
|           }
 | |
|         }
 | |
|         throw new Error('Unsupported media');
 | |
|       })
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   addApp() {
 | |
|     return new Promise(resolve => {
 | |
|       const model = {
 | |
|         worksheets: this._worksheets.filter(Boolean),
 | |
|       };
 | |
|       const xform = new AppXform();
 | |
|       const xml = xform.toXml(model);
 | |
|       this.zip.append(xml, {name: 'docProps/app.xml'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addCore() {
 | |
|     return new Promise(resolve => {
 | |
|       const coreXform = new CoreXform();
 | |
|       const xml = coreXform.toXml(this);
 | |
|       this.zip.append(xml, {name: 'docProps/core.xml'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addSharedStrings() {
 | |
|     if (this.sharedStrings.count) {
 | |
|       return new Promise(resolve => {
 | |
|         const sharedStringsXform = new SharedStringsXform();
 | |
|         const xml = sharedStringsXform.toXml(this.sharedStrings);
 | |
|         this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
 | |
|         resolve();
 | |
|       });
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   addWorkbookRels() {
 | |
|     let count = 1;
 | |
|     const relationships = [
 | |
|       {Id: `rId${count++}`, Type: RelType.Styles, Target: 'styles.xml'},
 | |
|       {Id: `rId${count++}`, Type: RelType.Theme, Target: 'theme/theme1.xml'},
 | |
|     ];
 | |
|     if (this.sharedStrings.count) {
 | |
|       relationships.push({
 | |
|         Id: `rId${count++}`,
 | |
|         Type: RelType.SharedStrings,
 | |
|         Target: 'sharedStrings.xml',
 | |
|       });
 | |
|     }
 | |
|     this._worksheets.forEach(worksheet => {
 | |
|       if (worksheet) {
 | |
|         worksheet.rId = `rId${count++}`;
 | |
|         relationships.push({
 | |
|           Id: worksheet.rId,
 | |
|           Type: RelType.Worksheet,
 | |
|           Target: `worksheets/sheet${worksheet.id}.xml`,
 | |
|         });
 | |
|       }
 | |
|     });
 | |
|     return new Promise(resolve => {
 | |
|       const xform = new RelationshipsXform();
 | |
|       const xml = xform.toXml(relationships);
 | |
|       this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   addWorkbook() {
 | |
|     const {zip} = this;
 | |
|     const model = {
 | |
|       worksheets: this._worksheets.filter(Boolean),
 | |
|       definedNames: this._definedNames.model,
 | |
|       views: this.views,
 | |
|       properties: {},
 | |
|       calcProperties: {},
 | |
|     };
 | |
| 
 | |
|     return new Promise(resolve => {
 | |
|       const xform = new WorkbookXform();
 | |
|       xform.prepare(model);
 | |
|       zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _finalize() {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       this.stream.on('error', reject);
 | |
|       this.stream.on('finish', () => {
 | |
|         resolve(this);
 | |
|       });
 | |
|       this.zip.on('error', reject);
 | |
| 
 | |
|       this.zip.finalize();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = WorkbookWriter;
 |