read-excel-file
Version:
Read `.xlsx` files in a web browser or in Node.js
246 lines (235 loc) • 13.1 kB
JavaScript
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