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