| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717 |
- const _ = require('../../utils/under-dash');
- const RelType = require('../../xlsx/rel-type');
- const colCache = require('../../utils/col-cache');
- const Encryptor = require('../../utils/encryptor');
- const Dimensions = require('../../doc/range');
- const StringBuf = require('../../utils/string-buf');
- const Row = require('../../doc/row');
- const Column = require('../../doc/column');
- const SheetRelsWriter = require('./sheet-rels-writer');
- const SheetCommentsWriter = require('./sheet-comments-writer');
- const DataValidations = require('../../doc/data-validations');
- const xmlBuffer = new StringBuf();
- // ============================================================================================
- // Xforms
- const ListXform = require('../../xlsx/xform/list-xform');
- const DataValidationsXform = require('../../xlsx/xform/sheet/data-validations-xform');
- const SheetPropertiesXform = require('../../xlsx/xform/sheet/sheet-properties-xform');
- const SheetFormatPropertiesXform = require('../../xlsx/xform/sheet/sheet-format-properties-xform');
- const ColXform = require('../../xlsx/xform/sheet/col-xform');
- const RowXform = require('../../xlsx/xform/sheet/row-xform');
- const HyperlinkXform = require('../../xlsx/xform/sheet/hyperlink-xform');
- const SheetViewXform = require('../../xlsx/xform/sheet/sheet-view-xform');
- const SheetProtectionXform = require('../../xlsx/xform/sheet/sheet-protection-xform');
- const PageMarginsXform = require('../../xlsx/xform/sheet/page-margins-xform');
- const PageSetupXform = require('../../xlsx/xform/sheet/page-setup-xform');
- const AutoFilterXform = require('../../xlsx/xform/sheet/auto-filter-xform');
- const PictureXform = require('../../xlsx/xform/sheet/picture-xform');
- const ConditionalFormattingsXform = require('../../xlsx/xform/sheet/cf/conditional-formattings-xform');
- const HeaderFooterXform = require('../../xlsx/xform/sheet/header-footer-xform');
- const RowBreaksXform = require('../../xlsx/xform/sheet/row-breaks-xform');
- // since prepare and render are functional, we can use singletons
- const xform = {
- dataValidations: new DataValidationsXform(),
- sheetProperties: new SheetPropertiesXform(),
- sheetFormatProperties: new SheetFormatPropertiesXform(),
- columns: new ListXform({tag: 'cols', length: false, childXform: new ColXform()}),
- row: new RowXform(),
- hyperlinks: new ListXform({tag: 'hyperlinks', length: false, childXform: new HyperlinkXform()}),
- sheetViews: new ListXform({tag: 'sheetViews', length: false, childXform: new SheetViewXform()}),
- sheetProtection: new SheetProtectionXform(),
- pageMargins: new PageMarginsXform(),
- pageSeteup: new PageSetupXform(),
- autoFilter: new AutoFilterXform(),
- picture: new PictureXform(),
- conditionalFormattings: new ConditionalFormattingsXform(),
- headerFooter: new HeaderFooterXform(),
- rowBreaks: new RowBreaksXform(),
- };
- // ============================================================================================
- class WorksheetWriter {
- constructor(options) {
- // in a workbook, each sheet will have a number
- this.id = options.id;
- // and a name
- this.name = options.name || `Sheet${this.id}`;
- // add a state
- this.state = options.state || 'visible';
- // rows are stored here while they need to be worked on.
- // when they are committed, they will be deleted.
- this._rows = [];
- // column definitions
- this._columns = null;
- // column keys (addRow convenience): key ==> this._columns index
- this._keys = {};
- // keep a record of all row and column pageBreaks
- this._merges = [];
- this._merges.add = function() {}; // ignore cell instruction
- // keep record of all hyperlinks
- this._sheetRelsWriter = new SheetRelsWriter(options);
- this._sheetCommentsWriter = new SheetCommentsWriter(this, this._sheetRelsWriter, options);
- // keep a record of dimensions
- this._dimensions = new Dimensions();
- // first uncommitted row
- this._rowZero = 1;
- // committed flag
- this.committed = false;
- // for data validations
- this.dataValidations = new DataValidations();
- // for sharing formulae
- this._formulae = {};
- this._siFormulae = 0;
- // keep a record of conditionalFormattings
- this.conditionalFormatting = [];
- // keep a record of all row and column pageBreaks
- this.rowBreaks = [];
- // for default row height, outline levels, etc
- this.properties = Object.assign(
- {},
- {
- defaultRowHeight: 15,
- dyDescent: 55,
- outlineLevelCol: 0,
- outlineLevelRow: 0,
- },
- options.properties
- );
- this.headerFooter = Object.assign(
- {},
- {
- differentFirst: false,
- differentOddEven: false,
- oddHeader: null,
- oddFooter: null,
- evenHeader: null,
- evenFooter: null,
- firstHeader: null,
- firstFooter: null,
- },
- options.headerFooter
- );
- // for all things printing
- this.pageSetup = Object.assign(
- {},
- {
- margins: {left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3},
- orientation: 'portrait',
- horizontalDpi: 4294967295,
- verticalDpi: 4294967295,
- fitToPage: !!(
- options.pageSetup &&
- (options.pageSetup.fitToWidth || options.pageSetup.fitToHeight) &&
- !options.pageSetup.scale
- ),
- pageOrder: 'downThenOver',
- blackAndWhite: false,
- draft: false,
- cellComments: 'None',
- errors: 'displayed',
- scale: 100,
- fitToWidth: 1,
- fitToHeight: 1,
- paperSize: undefined,
- showRowColHeaders: false,
- showGridLines: false,
- horizontalCentered: false,
- verticalCentered: false,
- rowBreaks: null,
- colBreaks: null,
- },
- options.pageSetup
- );
- // using shared strings creates a smaller xlsx file but may use more memory
- this.useSharedStrings = options.useSharedStrings || false;
- this._workbook = options.workbook;
- this.hasComments = false;
- // views
- this._views = options.views || [];
- // auto filter
- this.autoFilter = options.autoFilter || null;
- this._media = [];
- // worksheet protection
- this.sheetProtection = null;
- // start writing to stream now
- this._writeOpenWorksheet();
- this.startedData = false;
- }
- get workbook() {
- return this._workbook;
- }
- get stream() {
- if (!this._stream) {
- // eslint-disable-next-line no-underscore-dangle
- this._stream = this._workbook._openStream(`/xl/worksheets/sheet${this.id}.xml`);
- // pause stream to prevent 'data' events
- this._stream.pause();
- }
- return this._stream;
- }
- // 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');
- }
- commit() {
- if (this.committed) {
- return;
- }
- // commit all rows
- this._rows.forEach(cRow => {
- if (cRow) {
- // write the row to the stream
- this._writeRow(cRow);
- }
- });
- // we _cannot_ accept new rows from now on
- this._rows = null;
- if (!this.startedData) {
- this._writeOpenSheetData();
- }
- this._writeCloseSheetData();
- this._writeAutoFilter();
- this._writeMergeCells();
- // for some reason, Excel can't handle dimensions at the bottom of the file
- // this._writeDimensions();
- this._writeHyperlinks();
- this._writeConditionalFormatting();
- this._writeDataValidations();
- this._writeSheetProtection();
- this._writePageMargins();
- this._writePageSetup();
- this._writeBackground();
- this._writeHeaderFooter();
- this._writeRowBreaks();
- // Legacy Data tag for comments
- this._writeLegacyData();
- this._writeCloseWorksheet();
- // signal end of stream to workbook
- this.stream.end();
- this._sheetCommentsWriter.commit();
- // also commit the hyperlinks if any
- this._sheetRelsWriter.commit();
- this.committed = true;
- }
- // return the current dimensions of the writer
- get dimensions() {
- return this._dimensions;
- }
- get views() {
- return this._views;
- }
- // =========================================================================
- // Columns
- // get the current columns array.
- get columns() {
- return this._columns;
- }
- // set the columns from an array of column definitions.
- // Note: any headers defined will overwrite existing values.
- set columns(value) {
- // calculate max header row count
- this._headerRowCount = value.reduce((pv, cv) => {
- const headerCount = (cv.header && 1) || (cv.headers && cv.headers.length) || 0;
- return Math.max(pv, headerCount);
- }, 0);
- // construct Column objects
- let count = 1;
- const columns = (this._columns = []);
- value.forEach(defn => {
- const column = new Column(this, count++, false);
- columns.push(column);
- column.defn = defn;
- });
- }
- 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);
- }
- // 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;
- // otherwise, 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];
- }
- // =========================================================================
- // Rows
- get _nextRow() {
- return this._rowZero + this._rows.length;
- }
- // iterate over every uncommitted row in the worksheet, including maybe empty rows
- eachRow(options, iteratee) {
- if (!iteratee) {
- iteratee = options;
- options = undefined;
- }
- if (options && options.includeEmpty) {
- const n = this._nextRow;
- for (let i = this._rowZero; i < n; i++) {
- iteratee(this.getRow(i), i);
- }
- } else {
- this._rows.forEach(row => {
- if (row.hasValues) {
- iteratee(row, row.number);
- }
- });
- }
- }
- _commitRow(cRow) {
- // since rows must be written in order, we commit all rows up till and including cRow
- let found = false;
- while (this._rows.length && !found) {
- const row = this._rows.shift();
- this._rowZero++;
- if (row) {
- this._writeRow(row);
- found = row.number === cRow.number;
- this._rowZero = row.number + 1;
- }
- }
- }
- get lastRow() {
- // returns last uncommitted row
- if (this._rows.length) {
- return this._rows[this._rows.length - 1];
- }
- return undefined;
- }
- // find a row (if exists) by row number
- findRow(rowNumber) {
- const index = rowNumber - this._rowZero;
- return this._rows[index];
- }
- getRow(rowNumber) {
- const index = rowNumber - this._rowZero;
- // may fail if rows have been comitted
- if (index < 0) {
- throw new Error('Out of bounds: this row has been committed');
- }
- let row = this._rows[index];
- if (!row) {
- this._rows[index] = row = new Row(this, rowNumber);
- }
- return row;
- }
- addRow(value) {
- const row = new Row(this, this._nextRow);
- this._rows[row.number - this._rowZero] = row;
- row.values = value;
- return row;
- }
- // ================================================================================
- // Cells
- // returns the cell at [r,c] or address given by r. If not found, return undefined
- findCell(r, c) {
- const address = colCache.getAddress(r, c);
- const row = this.findRow(address.row);
- return row ? row.findCell(address.column) : undefined;
- }
- // return the cell at [r,c] or address given by r. If not found, create a new one.
- getCell(r, c) {
- const address = colCache.getAddress(r, c);
- const row = this.getRow(address.row);
- return row.getCellEx(address);
- }
- mergeCells(...cells) {
- // may fail if rows have been comitted
- const dimensions = new Dimensions(cells);
- // check cells aren't already merged
- this._merges.forEach(merge => {
- if (merge.intersects(dimensions)) {
- throw new Error('Cannot merge already merged cells');
- }
- });
- // apply merge
- const master = this.getCell(dimensions.top, dimensions.left);
- for (let i = dimensions.top; i <= dimensions.bottom; i++) {
- for (let j = dimensions.left; j <= dimensions.right; j++) {
- if (i > dimensions.top || j > dimensions.left) {
- this.getCell(i, j).merge(master);
- }
- }
- }
- // index merge
- this._merges.push(dimensions);
- }
- // ===========================================================================
- // Conditional Formatting
- addConditionalFormatting(cf) {
- this.conditionalFormatting.push(cf);
- }
- removeConditionalFormatting(filter) {
- if (typeof filter === 'number') {
- this.conditionalFormatting.splice(filter, 1);
- } else if (filter instanceof Function) {
- this.conditionalFormatting = this.conditionalFormatting.filter(filter);
- } else {
- this.conditionalFormatting = [];
- }
- }
- // =========================================================================
- addBackgroundImage(imageId) {
- this._background = {
- imageId,
- };
- }
- getBackgroundImageId() {
- return this._background && this._background.imageId;
- }
- // =========================================================================
- // Worksheet Protection
- protect(password, options) {
- // TODO: make this function truly async
- // perhaps marshal to worker thread or something
- return new Promise(resolve => {
- this.sheetProtection = {
- sheet: true,
- };
- if (options && 'spinCount' in options) {
- // force spinCount to be integer >= 0
- options.spinCount = Number.isFinite(options.spinCount) ? Math.round(Math.max(0, options.spinCount)) : 100000;
- }
- if (password) {
- this.sheetProtection.algorithmName = 'SHA-512';
- this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64');
- this.sheetProtection.spinCount = options && 'spinCount' in options ? options.spinCount : 100000; // allow user specified spinCount
- this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(
- password,
- 'SHA512',
- this.sheetProtection.saltValue,
- this.sheetProtection.spinCount
- );
- }
- if (options) {
- this.sheetProtection = Object.assign(this.sheetProtection, options);
- if (!password && 'spinCount' in options) {
- delete this.sheetProtection.spinCount;
- }
- }
- resolve();
- });
- }
- unprotect() {
- this.sheetProtection = null;
- }
- // ================================================================================
- _write(text) {
- xmlBuffer.reset();
- xmlBuffer.addText(text);
- this.stream.write(xmlBuffer);
- }
- _writeSheetProperties(xmlBuf, properties, pageSetup) {
- const sheetPropertiesModel = {
- outlineProperties: properties && properties.outlineProperties,
- tabColor: properties && properties.tabColor,
- pageSetup:
- pageSetup && pageSetup.fitToPage
- ? {
- fitToPage: pageSetup.fitToPage,
- }
- : undefined,
- };
- xmlBuf.addText(xform.sheetProperties.toXml(sheetPropertiesModel));
- }
- _writeSheetFormatProperties(xmlBuf, properties) {
- const sheetFormatPropertiesModel = properties
- ? {
- defaultRowHeight: properties.defaultRowHeight,
- dyDescent: properties.dyDescent,
- outlineLevelCol: properties.outlineLevelCol,
- outlineLevelRow: properties.outlineLevelRow,
- }
- : undefined;
- if (properties.defaultColWidth) {
- sheetFormatPropertiesModel.defaultColWidth = properties.defaultColWidth;
- }
- xmlBuf.addText(xform.sheetFormatProperties.toXml(sheetFormatPropertiesModel));
- }
- _writeOpenWorksheet() {
- xmlBuffer.reset();
- xmlBuffer.addText('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
- xmlBuffer.addText(
- '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"' +
- ' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"' +
- ' xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"' +
- ' mc:Ignorable="x14ac"' +
- ' xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">'
- );
- this._writeSheetProperties(xmlBuffer, this.properties, this.pageSetup);
- xmlBuffer.addText(xform.sheetViews.toXml(this.views));
- this._writeSheetFormatProperties(xmlBuffer, this.properties);
- this.stream.write(xmlBuffer);
- }
- _writeColumns() {
- const cols = Column.toModel(this.columns);
- if (cols) {
- xform.columns.prepare(cols, {styles: this._workbook.styles});
- this.stream.write(xform.columns.toXml(cols));
- }
- }
- _writeOpenSheetData() {
- this._write('<sheetData>');
- }
- _writeRow(row) {
- if (!this.startedData) {
- this._writeColumns();
- this._writeOpenSheetData();
- this.startedData = true;
- }
- if (row.hasValues || row.height) {
- const {model} = row;
- const options = {
- styles: this._workbook.styles,
- sharedStrings: this.useSharedStrings ? this._workbook.sharedStrings : undefined,
- hyperlinks: this._sheetRelsWriter.hyperlinksProxy,
- merges: this._merges,
- formulae: this._formulae,
- siFormulae: this._siFormulae,
- comments: [],
- };
- xform.row.prepare(model, options);
- this.stream.write(xform.row.toXml(model));
- if (options.comments.length) {
- this.hasComments = true;
- this._sheetCommentsWriter.addComments(options.comments);
- }
- }
- }
- _writeCloseSheetData() {
- this._write('</sheetData>');
- }
- _writeMergeCells() {
- if (this._merges.length) {
- xmlBuffer.reset();
- xmlBuffer.addText(`<mergeCells count="${this._merges.length}">`);
- this._merges.forEach(merge => {
- xmlBuffer.addText(`<mergeCell ref="${merge}"/>`);
- });
- xmlBuffer.addText('</mergeCells>');
- this.stream.write(xmlBuffer);
- }
- }
- _writeHyperlinks() {
- // eslint-disable-next-line no-underscore-dangle
- this.stream.write(xform.hyperlinks.toXml(this._sheetRelsWriter._hyperlinks));
- }
- _writeConditionalFormatting() {
- const options = {
- styles: this._workbook.styles,
- };
- xform.conditionalFormattings.prepare(this.conditionalFormatting, options);
- this.stream.write(xform.conditionalFormattings.toXml(this.conditionalFormatting));
- }
- _writeRowBreaks() {
- this.stream.write(xform.rowBreaks.toXml(this.rowBreaks));
- }
- _writeDataValidations() {
- this.stream.write(xform.dataValidations.toXml(this.dataValidations.model));
- }
- _writeSheetProtection() {
- this.stream.write(xform.sheetProtection.toXml(this.sheetProtection));
- }
- _writePageMargins() {
- this.stream.write(xform.pageMargins.toXml(this.pageSetup.margins));
- }
- _writePageSetup() {
- this.stream.write(xform.pageSeteup.toXml(this.pageSetup));
- }
- _writeHeaderFooter() {
- this.stream.write(xform.headerFooter.toXml(this.headerFooter));
- }
- _writeAutoFilter() {
- this.stream.write(xform.autoFilter.toXml(this.autoFilter));
- }
- _writeBackground() {
- if (this._background) {
- if (this._background.imageId !== undefined) {
- const image = this._workbook.getImage(this._background.imageId);
- const pictureId = this._sheetRelsWriter.addMedia({
- Target: `../media/${image.name}`,
- Type: RelType.Image,
- });
- this._background = {
- ...this._background,
- rId: pictureId,
- };
- }
- this.stream.write(xform.picture.toXml({rId: this._background.rId}));
- }
- }
- _writeLegacyData() {
- if (this.hasComments) {
- xmlBuffer.reset();
- xmlBuffer.addText(`<legacyDrawing r:id="${this._sheetCommentsWriter.vmlRelId}"/>`);
- this.stream.write(xmlBuffer);
- }
- }
- _writeDimensions() {
- // for some reason, Excel can't handle dimensions at the bottom of the file
- // and we don't know the dimensions until the commit, so don't write them.
- // this._write('<dimension ref="' + this._dimensions + '"/>');
- }
- _writeCloseWorksheet() {
- this._write('</worksheet>');
- }
- }
- module.exports = WorksheetWriter;
|