UNPKG

@elbstack/xlsx-populate

Version:

Excel XLSX parser/generator written in JavaScript with Node.js and browser support, jQuery/d3-style method chaining, and a focus on keeping existing workbook features and styles in tact.

801 lines (702 loc) 32.9 kB
"use strict"; const _ = require("lodash"); const fs = require("fs"); const JSZip = require('jszip'); const externals = require("./externals"); const regexify = require("./regexify"); const blank = require("./blank")(); const xmlq = require("./xmlq"); const Sheet = require("./Sheet"); const ContentTypes = require("./ContentTypes"); const AppProperties = require("./AppProperties"); const CoreProperties = require("./CoreProperties"); const Relationships = require("./Relationships"); const SharedStrings = require("./SharedStrings"); const StyleSheet = require("./StyleSheet"); const Encryptor = require("./Encryptor"); const XmlParser = require("./XmlParser"); const XmlBuilder = require("./XmlBuilder"); const ArgHandler = require("./ArgHandler"); const addressConverter = require("./addressConverter"); // Options for adding files to zip. Do not create folders and use a fixed time at epoch. // The default JSZip behavior uses current time, which causes idential workbooks to be different each time. const zipFileOpts = { date: new Date(0), createFolders: false }; // Initialize the parser and builder. const xmlParser = new XmlParser(); const xmlBuilder = new XmlBuilder(); // Initialize the encryptor if present (can be excluded in browser build). const encryptor = typeof Encryptor === "function" && new Encryptor(); // Characters not allowed in sheet names. const badSheetNameChars = ['\\', '/', '*', '[', ']', ':', '?']; // Excel limits sheet names to 31 chars. const maxSheetNameLength = 31; // Order of the nodes as defined by the spec. const nodeOrder = [ "fileVersion", "fileSharing", "workbookPr", "workbookProtection", "bookViews", "sheets", "functionGroups", "externalReferences", "definedNames", "calcPr", "oleSize", "customWorkbookViews", "pivotCaches", "smartTagPr", "smartTagTypes", "webPublishing", "fileRecoveryPr", "webPublishObjects", "extLst" ]; /** * A workbook. */ class Workbook { /** * Create a new blank workbook. * @returns {Promise.<Workbook>} The workbook. * @ignore */ static fromBlankAsync() { return Workbook.fromDataAsync(blank); } /** * Loads a workbook from a data object. (Supports any supported [JSZip data types]{@link https://stuk.github.io/jszip/documentation/api_jszip/load_async.html}.) * @param {string|Array.<number>|ArrayBuffer|Uint8Array|Buffer|Blob|Promise.<*>} data - The data to load. * @param {{}} [opts] - Options * @returns {Promise.<Workbook>} The workbook. * @ignore */ static fromDataAsync(data, opts) { return new Workbook()._initAsync(data, opts); } /** * Loads a workbook from file. * @param {string} path - The path to the workbook. * @param {{}} [opts] - Options * @returns {Promise.<Workbook>} The workbook. * @ignore */ static fromFileAsync(path, opts) { if (process.browser) throw new Error("Workbook.fromFileAsync is not supported in the browser"); return new externals.Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) return reject(err); resolve(data); }); }).then(data => Workbook.fromDataAsync(data, opts)); } /** * Get the active sheet in the workbook. * @returns {Sheet} The active sheet. *//** * Set the active sheet in the workbook. * @param {Sheet|string|number} sheet - The sheet or name of sheet or index of sheet to activate. The sheet must not be hidden. * @returns {Workbook} The workbook. */ activeSheet() { return new ArgHandler('Workbook.activeSheet') .case(() => { return this._activeSheet; }) .case('*', sheet => { // Get the sheet from name/index if needed. if (!(sheet instanceof Sheet)) sheet = this.sheet(sheet); // Check if the sheet is hidden. if (sheet.hidden()) throw new Error("You may not activate a hidden sheet."); // Deselect all sheets except the active one (mirroring ying Excel behavior). _.forEach(this._sheets, current => { current.tabSelected(current === sheet); }); this._activeSheet = sheet; return this; }) .handle(arguments); } /** * Add a new sheet to the workbook. * @param {string} name - The name of the sheet. Must be unique, less than 31 characters, and may not contain the following characters: \ / * [ ] : ? * @param {number|string|Sheet} [indexOrBeforeSheet] The index to move the sheet to or the sheet (or name of sheet) to move this sheet before. Omit this argument to move to the end of the workbook. * @returns {Sheet} The new sheet. */ addSheet(name, indexOrBeforeSheet) { // Validate the sheet name. if (!name || typeof name !== "string") throw new Error("Invalid sheet name."); if (_.some(badSheetNameChars, char => name.indexOf(char) >= 0)) throw new Error(`Sheet name may not contain any of the following characters: ${badSheetNameChars.join(" ")}`); if (name.length > maxSheetNameLength) throw new Error(`Sheet name may not be greater than ${maxSheetNameLength} characters.`); if (this.sheet(name)) throw new Error(`Sheet with name "${name}" already exists.`); // Get the destination index of new sheet. let index; if (_.isNil(indexOrBeforeSheet)) { index = this._sheets.length; } else if (_.isInteger(indexOrBeforeSheet)) { index = indexOrBeforeSheet; } else { if (!(indexOrBeforeSheet instanceof Sheet)) { indexOrBeforeSheet = this.sheet(indexOrBeforeSheet); if (!indexOrBeforeSheet) throw new Error("Invalid before sheet reference."); } index = this._sheets.indexOf(indexOrBeforeSheet); } // Add a new relationship for the new sheet and create the new sheet ID node. const relationship = this._relationships.add("worksheet"); // Leave target blank as it will be filled later. const sheetIdNode = { name: "sheet", attributes: { name, sheetId: ++this._maxSheetId, 'r:id': relationship.attributes.Id }, children: [] }; // Create the new sheet. const sheet = new Sheet(this, sheetIdNode); // Insert the sheet at the appropriate index. this._sheets.splice(index, 0, sheet); return sheet; } /** * Gets a defined name scoped to the workbook. * @param {string} name - The defined name. * @returns {undefined|string|Cell|Range|Row|Column} What the defined name refers to or undefined if not found. Will return the string formula if not a Row, Column, Cell, or Range. *//** * Set a defined name scoped to the workbook. * @param {string} name - The defined name. * @param {string|Cell|Range|Row|Column} refersTo - What the name refers to. * @returns {Workbook} The workbook. */ definedName() { return new ArgHandler("Workbook.definedName") .case('string', name => { return this.scopedDefinedName(undefined, name); }) .case(['string', '*'], (name, refersTo) => { this.scopedDefinedName(undefined, name, refersTo); return this; }) .handle(arguments); } /** * Delete a sheet from the workbook. * @param {Sheet|string|number} sheet - The sheet or name of sheet or index of sheet to move. * @returns {Workbook} The workbook. */ deleteSheet(sheet) { // Get the sheet to move. if (!(sheet instanceof Sheet)) { sheet = this.sheet(sheet); if (!sheet) throw new Error("Invalid move sheet reference."); } // Make sure we are not deleting the only visible sheet. const visibleSheets = _.filter(this._sheets, sheet => !sheet.hidden()); if (visibleSheets.length === 1 && visibleSheets[0] === sheet) { throw new Error("This sheet may not be deleted as a workbook must contain at least one visible sheet."); } // Remove the sheet. let index = this._sheets.indexOf(sheet); this._sheets.splice(index, 1); // Set the new active sheet. if (sheet === this.activeSheet()) { if (index >= this._sheets.length) index--; this.activeSheet(index); } return this; } /** * Find the given pattern in the workbook and optionally replace it. * @param {string|RegExp} pattern - The pattern to look for. Providing a string will result in a case-insensitive substring search. Use a RegExp for more sophisticated searches. * @param {string|function} [replacement] - The text to replace or a String.replace callback function. If pattern is a string, all occurrences of the pattern in each cell will be replaced. * @returns {boolean} A flag indicating if the pattern was found. */ find(pattern, replacement) { pattern = regexify(pattern); let matches = []; this._sheets.forEach(sheet => { matches = matches.concat(sheet.find(pattern, replacement)); }); return matches; } /** * Move a sheet to a new position. * @param {Sheet|string|number} sheet - The sheet or name of sheet or index of sheet to move. * @param {number|string|Sheet} [indexOrBeforeSheet] The index to move the sheet to or the sheet (or name of sheet) to move this sheet before. Omit this argument to move to the end of the workbook. * @returns {Workbook} The workbook. */ moveSheet(sheet, indexOrBeforeSheet) { // Get the sheet to move. if (!(sheet instanceof Sheet)) { sheet = this.sheet(sheet); if (!sheet) throw new Error("Invalid move sheet reference."); } // Get the to/from indexes. const from = this._sheets.indexOf(sheet); let to; if (_.isNil(indexOrBeforeSheet)) { to = this._sheets.length - 1; } else if (_.isInteger(indexOrBeforeSheet)) { to = indexOrBeforeSheet; } else { if (!(indexOrBeforeSheet instanceof Sheet)) { indexOrBeforeSheet = this.sheet(indexOrBeforeSheet); if (!indexOrBeforeSheet) throw new Error("Invalid before sheet reference."); } to = this._sheets.indexOf(indexOrBeforeSheet); } // Insert the sheet at the appropriate place. this._sheets.splice(to, 0, this._sheets.splice(from, 1)[0]); return this; } /** * Generates the workbook output. * @param {string} [type] - The type of the data to return: base64, binarystring, uint8array, arraybuffer, blob, nodebuffer. Defaults to 'nodebuffer' in Node.js and 'blob' in browsers. * @returns {string|Uint8Array|ArrayBuffer|Blob|Buffer} The data. *//** * Generates the workbook output. * @param {{}} [opts] Options * @param {string} [opts.type] - The type of the data to return: base64, binarystring, uint8array, arraybuffer, blob, nodebuffer. Defaults to 'nodebuffer' in Node.js and 'blob' in browsers. * @param {string} [opts.password] - The password to use to encrypt the workbook. * @returns {string|Uint8Array|ArrayBuffer|Blob|Buffer} The data. */ outputAsync(opts) { opts = opts || {}; if (typeof opts === 'string') opts = { type: opts }; this._setSheetRefs(); let definedNamesNode = xmlq.findChild(this._node, "definedNames"); this._sheets.forEach((sheet, i) => { if (!sheet._autoFilter) return; if (!definedNamesNode) { definedNamesNode = { name: "definedNames", attributes: {}, children: [] }; xmlq.insertInOrder(this._node, definedNamesNode, nodeOrder); } xmlq.appendChild(definedNamesNode, { name: "definedName", attributes: { name: "_xlnm._FilterDatabase", localSheetId: i, hidden: "1" }, children: [sheet._autoFilter.address({ includeSheetName: true, anchored: true })] }); }); this._sheetsNode.children = []; this._sheets.forEach((sheet, i) => { const sheetPath = `xl/worksheets/sheet${i + 1}.xml`; const sheetRelsPath = `xl/worksheets/_rels/sheet${i + 1}.xml.rels`; const sheetXmls = sheet.toXmls(); const relationship = this._relationships.findById(sheetXmls.id.attributes['r:id']); relationship.attributes.Target = `worksheets/sheet${i + 1}.xml`; this._sheetsNode.children.push(sheetXmls.id); this._zip.file(sheetPath, xmlBuilder.build(sheetXmls.sheet), zipFileOpts); const relationshipsXml = xmlBuilder.build(sheetXmls.relationships); if (relationshipsXml) { this._zip.file(sheetRelsPath, relationshipsXml, zipFileOpts); } else { this._zip.remove(sheetRelsPath); } }); // Set the app security to true if a password is set, false if not. // this._appProperties.isSecure(!!opts.password); // Convert the various components to XML strings and add them to the zip. this._zip.file("[Content_Types].xml", xmlBuilder.build(this._contentTypes), zipFileOpts); this._zip.file("docProps/app.xml", xmlBuilder.build(this._appProperties), zipFileOpts); this._zip.file("docProps/core.xml", xmlBuilder.build(this._coreProperties), zipFileOpts); this._zip.file("xl/_rels/workbook.xml.rels", xmlBuilder.build(this._relationships), zipFileOpts); this._zip.file("xl/sharedStrings.xml", xmlBuilder.build(this._sharedStrings), zipFileOpts); this._zip.file("xl/styles.xml", xmlBuilder.build(this._styleSheet), zipFileOpts); this._zip.file("xl/workbook.xml", xmlBuilder.build(this._node), zipFileOpts); // Generate the zip. return this._zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }).then(output => { // If a password is set, encrypt the workbook. if (opts.password) output = encryptor.encrypt(output, opts.password); // Convert and return return this._convertBufferToOutput(output, opts.type); }); } /** * Gets the sheet with the provided name or index (0-based). * @param {string|number} sheetNameOrIndex - The sheet name or index. * @returns {Sheet|undefined} The sheet or undefined if not found. */ sheet(sheetNameOrIndex) { if (_.isInteger(sheetNameOrIndex)) return this._sheets[sheetNameOrIndex]; return _.find(this._sheets, sheet => sheet.name() === sheetNameOrIndex); } /** * Get an array of all the sheets in the workbook. * @returns {Array.<Sheet>} The sheets. */ sheets() { return this._sheets.slice(); } /** * Gets an individual property. * @param {string} name - The name of the property. * @returns {*} The property. *//** * Gets multiple properties. * @param {Array.<string>} names - The names of the properties. * @returns {object.<string, *>} Object whose keys are the property names and values are the properties. *//** * Sets an individual property. * @param {string} name - The name of the property. * @param {*} value - The value to set. * @returns {Workbook} The workbook. *//** * Sets multiple properties. * @param {object.<string, *>} properties - Object whose keys are the property names and values are the values to set. * @returns {Workbook} The workbook. */ property() { return new ArgHandler("Workbook.property") .case('string', name => { // Get single value return this._coreProperties.get(name); }) .case('array', names => { // Get list of values const values = {}; names.forEach(name => { values[name] = this._coreProperties.get(name); }); return values; }) .case(['string', '*'], (name, value) => { // Set a single value for all cells to a single value this._coreProperties.set(name, value); return this; }) .case('object', nameValues => { // Object of key value pairs to set for (const name in nameValues) { if (!nameValues.hasOwnProperty(name)) continue; const value = nameValues[name]; this._coreProperties.set(name, value); } return this; }) .handle(arguments); } /** * Get access to core properties object * @returns {CoreProperties} The core properties. */ properties() { return this._coreProperties; } /** * Write the workbook to file. (Not supported in browsers.) * @param {string} path - The path of the file to write. * @param {{}} [opts] - Options * @param {string} [opts.password] - The password to encrypt the workbook. * @returns {Promise.<undefined>} A promise. */ toFileAsync(path, opts) { if (process.browser) throw new Error("Workbook.toFileAsync is not supported in the browser."); return this.outputAsync(opts) .then(data => new externals.Promise((resolve, reject) => { fs.writeFile(path, data, err => { if (err) return reject(err); resolve(); }); })); } /** * Gets a scoped defined name. * @param {Sheet} sheetScope - The sheet the name is scoped to. Use undefined for workbook scope. * @param {string} name - The defined name. * @returns {undefined|Cell|Range|Row|Column} What the defined name refers to. * @ignore *//** * Sets a scoped defined name. * @param {Sheet} sheetScope - The sheet the name is scoped to. Use undefined for workbook scope. * @param {string} name - The defined name. * @param {undefined|Cell|Range|Row|Column} refersTo - What the defined name refers to. * @returns {Workbook} The workbook. * @ignore */ scopedDefinedName(sheetScope, name, refersTo) { let definedNamesNode = xmlq.findChild(this._node, "definedNames"); let definedNameNode = definedNamesNode && _.find(definedNamesNode.children, node => node.attributes.name === name && node.localSheet === sheetScope); return new ArgHandler('Workbook.scopedDefinedName') .case(['*', 'string'], () => { // Get the address from the definedNames node. const refersTo = definedNameNode && definedNameNode.children[0]; if (!refersTo) return undefined; // Try to parse the address. const ref = addressConverter.fromAddress(refersTo); if (!ref) return refersTo; // Load the appropriate selection type. const sheet = this.sheet(ref.sheetName); if (ref.type === 'cell') return sheet.cell(ref.rowNumber, ref.columnNumber); if (ref.type === 'range') return sheet.range(ref.startRowNumber, ref.startColumnNumber, ref.endRowNumber, ref.endColumnNumber); if (ref.type === 'row') return sheet.row(ref.rowNumber); if (ref.type === 'column') return sheet.column(ref.columnNumber); return refersTo; }) .case(['*', 'string', 'nil'], () => { if (definedNameNode) xmlq.removeChild(definedNamesNode, definedNameNode); if (definedNamesNode && !definedNamesNode.children.length) xmlq.removeChild(this._node, definedNamesNode); return this; }) .case(['*', 'string', '*'], () => { if (typeof refersTo !== 'string') { refersTo = refersTo.address({ includeSheetName: true, anchored: true }); } if (!definedNamesNode) { definedNamesNode = { name: "definedNames", attributes: {}, children: [] }; xmlq.insertInOrder(this._node, definedNamesNode, nodeOrder); } if (!definedNameNode) { definedNameNode = { name: "definedName", attributes: { name }, children: [refersTo] }; if (sheetScope) definedNameNode.localSheet = sheetScope; xmlq.appendChild(definedNamesNode, definedNameNode); } definedNameNode.children = [refersTo]; return this; }) .handle(arguments); } /** * Get the shared strings table. * @returns {SharedStrings} The shared strings table. * @ignore */ sharedStrings() { return this._sharedStrings; } /** * Get the style sheet. * @returns {StyleSheet} The style sheet. * @ignore */ styleSheet() { return this._styleSheet; } /** * Initialize the workbook. (This is separated from the constructor to ease testing.) * @param {string|ArrayBuffer|Uint8Array|Buffer|Blob} data - The data to load. * @param {{}} [opts] - Options * @param {boolean} [opts.base64=false] - No used unless input is a string. True if the input string is base64 encoded, false for binary. * @returns {Promise.<Workbook>} The workbook. * @private */ _initAsync(data, opts) { opts = opts || {}; this._maxSheetId = 0; this._sheets = []; return externals.Promise.resolve() .then(() => { // Make sure the input is a Buffer return this._convertInputToBufferAsync(data, opts.base64) .then(buffer => { data = buffer; }); }) .then(() => { if (!opts.password) return; return encryptor.decryptAsync(data, opts.password) .then(decrypted => { data = decrypted; }); }) .then(() => JSZip.loadAsync(data)) .then(zip => { this._zip = zip; return this._parseNodesAsync([ "[Content_Types].xml", "docProps/app.xml", "docProps/core.xml", "xl/_rels/workbook.xml.rels", "xl/sharedStrings.xml", "xl/styles.xml", "xl/workbook.xml" ]); }) .then(nodes => { const contentTypesNode = nodes[0]; const appPropertiesNode = nodes[1]; const corePropertiesNode = nodes[2]; const relationshipsNode = nodes[3]; const sharedStringsNode = nodes[4]; const styleSheetNode = nodes[5]; const workbookNode = nodes[6]; // Load the various components. this._contentTypes = new ContentTypes(contentTypesNode); this._appProperties = new AppProperties(appPropertiesNode); this._coreProperties = new CoreProperties(corePropertiesNode); this._relationships = new Relationships(relationshipsNode); this._sharedStrings = new SharedStrings(sharedStringsNode); this._styleSheet = new StyleSheet(styleSheetNode); this._node = workbookNode; // Add the shared strings relationship if it doesn't exist. if (!this._relationships.findByType("sharedStrings")) { this._relationships.add("sharedStrings", "sharedStrings.xml"); } // Add the shared string content type if it doesn't exist. if (!this._contentTypes.findByPartName("/xl/sharedStrings.xml")) { this._contentTypes.add("/xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"); } // Kill the calc chain. It's not required and the workbook will corrupt unless we keep it up to date. this._zip.remove("xl/calcChain.xml"); // Load each sheet. this._sheetsNode = xmlq.findChild(this._node, "sheets"); return externals.Promise.all(_.map(this._sheetsNode.children, (sheetIdNode, i) => { if (sheetIdNode.attributes.sheetId > this._maxSheetId) this._maxSheetId = sheetIdNode.attributes.sheetId; return this._parseNodesAsync([`xl/worksheets/sheet${i + 1}.xml`, `xl/worksheets/_rels/sheet${i + 1}.xml.rels`]) .then(nodes => { const sheetNode = nodes[0]; const sheetRelationshipsNode = nodes[1]; // Insert at position i as the promises will resolve at different times. this._sheets[i] = new Sheet(this, sheetIdNode, sheetNode, sheetRelationshipsNode); }); })); }) .then(() => this._parseSheetRefs()) .then(() => this); } /** * Parse files out of zip into XML node objects. * @param {Array.<string>} names - The file names to parse. * @returns {Promise.<Array.<{}>>} An array of the parsed objects. * @private */ _parseNodesAsync(names) { return externals.Promise.all(_.map(names, name => this._zip.file(name))) .then(files => externals.Promise.all(_.map(files, file => file && file.async("string")))) .then(texts => externals.Promise.all(_.map(texts, text => text && xmlParser.parseAsync(text)))); } /** * Parse the sheet references out so we can reorder freely. * @returns {undefined} * @private */ _parseSheetRefs() { // Parse the active sheet. const bookViewsNode = xmlq.findChild(this._node, "bookViews"); const workbookViewNode = bookViewsNode && xmlq.findChild(bookViewsNode, "workbookView"); const activeTabId = workbookViewNode && workbookViewNode.attributes.activeTab || 0; this._activeSheet = this._sheets[activeTabId]; // Set the location sheet on the defined name nodes. The defined name should point to the index of the sheet // but reordering sheets messes this up. So store it on the node and we'll update the index on XML build. const definedNamesNode = xmlq.findChild(this._node, "definedNames"); if (definedNamesNode) { _.forEach(definedNamesNode.children, definedNameNode => { if (definedNameNode.attributes.hasOwnProperty("localSheetId")) { definedNameNode.localSheet = this._sheets[definedNameNode.attributes.localSheetId]; } }); } } /** * Set the proper sheet references in the XML. * @returns {undefined} * @private */ _setSheetRefs() { // Set the active sheet. let bookViewsNode = xmlq.findChild(this._node, "bookViews"); if (!bookViewsNode) { bookViewsNode = { name: 'bookViews', attributes: {}, children: [] }; xmlq.insertInOrder(this._node, bookViewsNode, nodeOrder); } let workbookViewNode = xmlq.findChild(bookViewsNode, "workbookView"); if (!workbookViewNode) { workbookViewNode = { name: 'workbookView', attributes: {}, children: [] }; xmlq.appendChild(bookViewsNode, workbookViewNode); } workbookViewNode.attributes.activeTab = this._sheets.indexOf(this._activeSheet); // Set the defined names local sheet indexes. const definedNamesNode = xmlq.findChild(this._node, "definedNames"); if (definedNamesNode) { _.forEach(definedNamesNode.children, definedNameNode => { if (definedNameNode.localSheet) { definedNameNode.attributes.localSheetId = this._sheets.indexOf(definedNameNode.localSheet); } }); } } /** * Convert buffer to desired output format * @param {Buffer} buffer - The buffer * @param {string} type - The type to convert to: buffer/nodebuffer, blob, base64, binarystring, uint8array, arraybuffer * @returns {Buffer|Blob|string|Uint8Array|ArrayBuffer} The output * @private */ _convertBufferToOutput(buffer, type) { if (!type) type = process.browser ? "blob" : "nodebuffer"; if (type === "buffer" || type === "nodebuffer") return buffer; if (process.browser && type === "blob") return new Blob([buffer], { type: Workbook.MIME_TYPE }); if (type === "base64") return buffer.toString("base64"); if (type === "binarystring") return buffer.toString("utf8"); if (type === "uint8array") return new Uint8Array(buffer); if (type === "arraybuffer") return new Uint8Array(buffer).buffer; throw new Error(`Output type '${type}' not supported.`); } /** * Convert input to buffer * @param {Buffer|Blob|string|Uint8Array|ArrayBuffer} input - The input * @param {boolean} [base64=false] - Only applies if input is a string. If true, the string is base64 encoded, false for binary * @returns {Promise.<Buffer>} The buffer. * @private */ _convertInputToBufferAsync(input, base64) { return externals.Promise.resolve() .then(() => { if (Buffer.isBuffer(input)) return input; if (process.browser && input instanceof Blob) { return new externals.Promise(resolve => { const fileReader = new FileReader(); fileReader.onload = event => { resolve(Buffer.from(event.target.result)); }; fileReader.readAsArrayBuffer(input); }); } if (typeof input === "string" && base64) return Buffer.from(input, "base64"); if (typeof input === "string" && !base64) return Buffer.from(input, "utf8"); if (input instanceof Uint8Array || input instanceof ArrayBuffer) return Buffer.from(input); throw new Error(`Input type unknown.`); }); } } /** * The XLSX mime type. * @type {string} * @ignore */ Workbook.MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; module.exports = Workbook; /* xl/workbook.xml <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <workbook 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="x15" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"> <fileVersion appName="xl" lastEdited="7" lowestEdited="7" rupBuild="16925"/> <workbookPr defaultThemeVersion="164011"/> <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"> <mc:Choice Requires="x15"> <x15ac:absPath url="\path\to\file" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac"/> </mc:Choice> </mc:AlternateContent> <bookViews> <workbookView xWindow="3720" yWindow="0" windowWidth="27870" windowHeight="12795"/> </bookViews> <sheets> <sheet name="Sheet1" sheetId="1" r:id="rId1"/> </sheets> <calcPr calcId="171027"/> <extLst> <ext uri="{140A7094-0E35-4892-8432-C4D2E57EDEB5}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"> <x15:workbookPr chartTrackingRefBase="1"/> </ext> </extLst> </workbook> // */