UNPKG

read-excel-file

Version:

Read `.xlsx` files in a web browser or in Node.js

246 lines (235 loc) 13.1 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _createForOfIteratorHelperLoose(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (it) return (it = it.call(o)).next.bind(it); if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; return function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } import parseSpreadsheetInfo from './parseSpreadsheetInfo.js'; import parseFilePaths from './parseFilePaths.js'; import parseStyles from './parseStyles.js'; import parseSharedStrings from './parseSharedStrings.js'; import parseSheet from './parseSheet.js'; import convertValuesFromUint8ArraysToStrings from '../utility/convertValuesFromUint8ArraysToStrings.js'; import checkpoint from '../utility/checkpoint.js'; import isPromise from '../utility/isPromise.js'; import InvalidSpreadsheetError from './InvalidSpreadsheetError.js'; import SheetNotFoundError from './SheetNotFoundError.js'; /** * Reads data from an `.xlsx` file. * @param {function} parseXml — SAX XML parser. * @param {Record<string,Uint8Array>} contents - A map of `.xml` files inside the `.xlsx` file (which itself is just a zipped directory). * @param {object} [options] * @return {Promise<Sheet[]>} */ function parseSpreadsheetContents(parseXml, contents_) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; // For an introduction in reading `.xlsx` files see "The minimum viable XLSX reader": // https://www.brendanlong.com/the-minimum-viable-xlsx-reader.html // Convert the values in `contents_` from `Uint8Array`s to `string`s. // // This function is a bit of a bottleneck on large `.xlsx` files. // For example, when running the benchmark, the time of calling this function is: // // * "1mb.xlsx" — 2 // * "10mb.xlsx" — 7 // * "50mb.xlsx" — 35 // // When running this code in a worker, it's no longer a "bottleneck" // because in that case it doesn't block the main thread. // var contents = convertValuesFromUint8ArraysToStrings(contents_); // Because of how `.xlsx` file contents are defined in the specification, // it will have to be read in 3 passes: // * First pass — read the actual file paths // * Second pass — read "shared strings" and "styles" // * Thirs pass — read the sheets data checkpoint('parse spreadsheet info and file paths'); // Get spreadsheet info and the paths to files. return readFiles(getXmlFilesAtFixedPaths(), contents, parseXml).then(function (_ref) { var spreadsheetInfo = _ref.spreadsheetInfo, filePaths = _ref.filePaths; checkpoint('parse "shared strings" and "styles"'); // Parse "shared strings" and "styles". return readFiles(getXmlFilesAtNonFixedPaths(filePaths), contents, parseXml).then(function (_ref2) { var sharedStrings = _ref2.sharedStrings, styles = _ref2.styles; var sheetRelationIdsToRead = options.sheets ? options.sheets.map(function (sheet) { return getSheetRelationId(sheet, spreadsheetInfo.sheets); }) : spreadsheetInfo.sheets.map(function (_) { return _.relationId; }); checkpoint("parse sheet".concat(sheetRelationIdsToRead.length === 1 ? '' : 's', " data")); var dateFormatDetectionCache = []; // Parse sheets data. return readFiles(getSheetDataXmlFiles(filePaths, sheetRelationIdsToRead, { sharedStrings: sharedStrings, styles: styles, epoch1904: spreadsheetInfo.epoch1904, dateFormatDetectionCache: dateFormatDetectionCache, options: options }), contents, parseXml).then(function (sheetsData) { checkpoint('end'); // Return sheets data. return sheetRelationIdsToRead.map(function (sheetRelationId) { return { sheet: getSheetNameByRelationId(sheetRelationId, spreadsheetInfo.sheets), data: sheetsData[sheetRelationId] }; }); }); }); }); } /** * Reads data from an `.xlsx` file in a worker. * @param {function} [createWorkerFunction] — Creates a worker function. Not used. * @param {function} parseXml — SAX XML parser. * @param {Record<string,Uint8Array>} contents - A map of `.xml` files inside the `.xlsx` file (which itself is just a zipped directory). * @param {object} [options] * @return {Promise<Sheet[]>} */ export default function parseSpreadsheetContentsInWorker(createWorkerFunction, parseXml, contents, options) { // Assign a default value of `null` to `parseNumber()` function in the `options`. // The reason is that the worker code requires it to be non-`undefined`. // Otherwise, it would throw "parseNumber is not defined". if (!(options && options.parseNumber)) { options = _objectSpread(_objectSpread({}, options), {}, { parseNumber: null }); } return parseSpreadsheetContents(parseXml, contents, options); } function getSheetRelationId(sheet, sheets) { if (typeof sheet === 'string') { for (var _iterator = _createForOfIteratorHelperLoose(sheets), _step; !(_step = _iterator()).done;) { var _sheet = _step.value; if (_sheet.name === sheet) { return _sheet.relationId; } } } else { if (sheet <= sheets.length) { return sheets[sheet - 1].relationId; } } throw new SheetNotFoundError(sheet, sheets.map(function (_) { return _.name; })); } function getSheetNameByRelationId(sheetRelationId, sheets) { for (var _iterator2 = _createForOfIteratorHelperLoose(sheets), _step2; !(_step2 = _iterator2()).done;) { var sheet = _step2.value; if (sheet.relationId === sheetRelationId) { return sheet.name; } } // The only way of getting `sheetRelationId` here is from the `sheets`, // so this error is not technically possible. And if it is thrown // then it means that there's a bug in the code because it's not // supposed to get `sheetRelationId` from anywhere other than the `sheets`. throw new Error("Sheet relation ID not found: ".concat(sheetRelationId)); } function getXmlFilesAtFixedPaths() { return { // Read the paths to certain files inside the `.xlsx` file, which is itself just a `.zip` archive. // These paths aren't standardized between different spreadsheet editors. // https://github.com/tidyverse/readxl/issues/104 'xl/_rels/workbook.xml.rels': { name: 'filePaths', parse: parseFilePaths }, // General info on the spreadsheet. 'xl/workbook.xml': { name: 'spreadsheetInfo', parse: parseSpreadsheetInfo } }; } function getXmlFilesAtNonFixedPaths(filePaths) { var _ref3; return _ref3 = {}, _defineProperty(_ref3, filePaths.sharedStrings || 'xl/sharedStrings.xml', { name: 'sharedStrings', // `parseSharedStrings()` returns a `Promise`. parse: parseSharedStrings, // It seems that "sharedStrings.xml" is not required to exist. // For example, that could be the case when a spreadsheet doesn't contain any strings. // https://github.com/catamphetamine/read-excel-file/issues/85 fallback: Promise.resolve([]) }), _defineProperty(_ref3, filePaths.styles || 'xl/styles.xml', { name: 'styles', parse: parseStyles, fallback: {} }), _ref3; } // Returns the list of sheet data `.xml` files. function getSheetDataXmlFiles(filePaths, sheetRelationIdsToRead, sheetDataParserParameters) { return Object.keys(filePaths.sheets).filter(function (sheetRelationId) { return sheetRelationIdsToRead.includes(sheetRelationId); }).reduce(function (filesInfo, sheetRelationId) { return _objectSpread(_objectSpread({}, filesInfo), {}, _defineProperty({}, filePaths.sheets[sheetRelationId], { name: sheetRelationId, // `parseSheet()` returns a `Promise`. parse: function parse(content, parseXml) { return parseSheet(content, parseXml, sheetDataParserParameters); } })); }, {}); } // In case of converting `.zip` file reader from a "read-and-return" one to a "streaming" one, // this function could be modified to process the files as they come rather than all-at-once. // Reads files from inside an `.xlsx` archive by file paths. // // In case of converting `.zip` file reader from a "read-and-return" one to a "streaming" one, // this function could be modified to process the files as they come rather than all-at-once. // // But there's a catch: inside an `.xlsx` file, some file paths are not fixed // and are instead defined in "xl/_rels/workbook.xml.rels" file, // which presents a "chicken and an egg" dilemma: how could one possibly // read an `.xlsx` file in one go when the order of the files inside it isn't fixed // and could be random. Most likely, in the majority of cases, "xl/_rels/workbook.xml.rels" // file is gonna be one of the first in a given `.xlsx` archive, but still it's not guaranteed. // A solution would be reading an `.xlsx` file in two passes: one pass would be just to read // the "xl/_rels/workbook.xml.rels" and ignore decompressing anything else, // and then the second pass would be to read all other files whose paths are now known. // // Returns: // * If none of the `parse()` functions returned a `Promise`, it returns a map of files' contents. // * If any of the `parse()` functions returned a `Promise`, it returns a `Promise` that resolves to a map of files' contents. // function readFiles(filesInfo, contents, parseXml) { // Get files' contents. var results = {}; var _loop = function _loop() { var filePath = _Object$keys[_i]; var fileInfo = filesInfo[filePath]; results[fileInfo.name] = contents[filePath] === undefined ? fileInfo.fallback === undefined ? function () { throw new InvalidSpreadsheetError("\"".concat(filePath, "\" file not found inside the `.xlsx` file")); }() : fileInfo.fallback : fileInfo.parse(contents[filePath], parseXml); }; for (var _i = 0, _Object$keys = Object.keys(filesInfo); _i < _Object$keys.length; _i++) { _loop(); } // Resolve any `Promise`s. var promises = []; var _loop2 = function _loop2() { var name = _Object$keys2[_i2]; if (isPromise(results[name])) { promises.push(results[name].then(function (result) { results[name] = result; })); } }; for (var _i2 = 0, _Object$keys2 = Object.keys(results); _i2 < _Object$keys2.length; _i2++) { _loop2(); } if (promises.length > 0) { return Promise.all(promises).then(function () { return results; }); } return results; } //# sourceMappingURL=parseSpreadsheetContents.js.map