@kobold/excel
Version:
Kobold excel data handler
174 lines (127 loc) • 7.33 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _Object$defineProperty = require("@babel/runtime-corejs3/core-js/object/define-property");
_Object$defineProperty(exports, "__esModule", {
value: true
});
exports.Sheet = void 0;
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes"));
var _findIndex = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/find-index"));
var _keys = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/keys"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/map"));
var _slice = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/slice"));
var _map2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/map"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
var _binaryParser = require("binary-parser");
var _files = require("./files");
var _utilities = require("./utilities");
// TODO: where should this live? is it composed by excel, or is it handled as part of the page file reader itself?
// Should page pre-parse rows?
const rowHeaderParser = new _binaryParser.Parser().endianess('big').uint32('dataSize').uint16('rowCount');
const rowHeaderSize = rowHeaderParser.sizeOf();
const languageStringMap = {
[_files.Language.NONE]: 'NONE',
[_files.Language.JAPANESE]: 'ja',
[_files.Language.ENGLISH]: 'en',
[_files.Language.GERMAN]: 'de',
[_files.Language.FRENCH]: 'fr',
[_files.Language.CHINESE_SIMPLIFIED]: 'chs',
[_files.Language.CHINESE_TRADITIONAL]: 'cht',
[_files.Language.KOREAN]: 'ko',
[_files.Language.TRADITIONAL_CHINESE]: 'tc'
};
class Sheet {
constructor(opts) {
(0, _defineProperty2.default)(this, "kobold", void 0);
(0, _defineProperty2.default)(this, "RowClass", void 0);
(0, _defineProperty2.default)(this, "language", void 0);
(0, _defineProperty2.default)(this, "headerCache", void 0);
(0, _defineProperty2.default)(this, "pageCache", new _map2.default());
this.kobold = opts.kobold;
this.RowClass = opts.RowClass;
this.language = opts.language;
}
async *getRows(opts) {
var _context;
const header = await this.getHeader(); // Make sure they pass the lower of the two as froms
if ((opts === null || opts === void 0 ? void 0 : opts.from) != null && opts.to != null) {
(0, _utilities.assert)(opts.from <= opts.to, `Specified "from" row index must precede "to" index.`);
} // Work out what pages the requested from/to rows reside in, falling back to first/last. Prettier _really_ fucks these up
// prettier-ignore
const from = (opts === null || opts === void 0 ? void 0 : opts.from) != null ? this.getPageForRow(header.pages, opts.from) : 0; // prettier-ignore
const to = (opts === null || opts === void 0 ? void 0 : opts.to) != null ? this.getPageForRow(header.pages, opts.to) : header.pages.length - 1;
const pageDefinitions = (0, _slice.default)(_context = header.pages).call(_context, from, to + 1); // Preload the pages we'll be looping
const pagePreLoad = (0, _map.default)(pageDefinitions).call(pageDefinitions, ({
startId
}) => this.getPage(startId)); // holy nested loops batman
for await (const page of pagePreLoad) {
for (const index of (0, _keys.default)(_context2 = page.rowOffsets).call(_context2)) {
var _context2, _opts$from, _opts$to;
// Skip rows not contained within the requested range, if any
if (index < ((_opts$from = opts === null || opts === void 0 ? void 0 : opts.from) !== null && _opts$from !== void 0 ? _opts$from : 0) || index > ((_opts$to = opts === null || opts === void 0 ? void 0 : opts.to) !== null && _opts$to !== void 0 ? _opts$to : Infinity)) {
continue;
}
const rowHeader = this.parseRowHeader(page, index); // TODO: DEFAULT Variants seem to be stable on rowCount:1 here, but keep an eye on it
for (let subIndex = 0; subIndex < rowHeader.rowCount; subIndex++) {
yield this.buildRow(header, page, index, subIndex);
}
}
}
}
async getRow(index, subIndex = 0) {
const header = await this.getHeader(); // Work out what page the requested row is on
const pageDefinition = header.pages[this.getPageForRow(header.pages, index)];
const page = await this.getPage(pageDefinition.startId);
return this.buildRow(header, page, index, subIndex);
}
getPageForRow(pages, index) {
const pageIndex = (0, _findIndex.default)(pages).call(pages, page => page.startId <= index && page.startId + page.rowCount > index);
(0, _utilities.assert)(pageIndex >= 0, `Requested index ${index} is not defined by any sheet pages`);
return pageIndex;
} // TODO: Probably should rename this given how much logic i'm throwing in
async getHeader() {
if (this.headerCache == null) {
const path = `exd/${this.RowClass.sheet}.exh`;
this.headerCache = await this.kobold.getFile(path, _files.ExcelHeader);
} // If the sheet _only_ supports NONE, silently fall back
const {
languages
} = this.headerCache;
if (languages.length === 1 && languages[0] === _files.Language.NONE) {
this.language = _files.Language.NONE;
} // If the requested language isn't supported by the sheet, panic
(0, _utilities.assert)((0, _includes.default)(languages).call(languages, this.language), `Requested language ${_files.Language[this.language]} is not provided by sheet ${this.RowClass.sheet} (provided: ${(0, _map.default)(languages).call(languages, lang => _files.Language[lang]).join(', ')})`);
return this.headerCache;
}
async getPage(startId) {
const language = this.language; // exd/{sheetName}_{page}[_{languageString}].exd
const path = language === _files.Language.NONE ? `exd/${this.RowClass.sheet}_${startId}.exd` : `exd/${this.RowClass.sheet}_${startId}_${languageStringMap[language]}.exd`.toLowerCase();
let page = this.pageCache.get(path);
if (page == null) {
page = await this.kobold.getFile(path, _files.ExcelPage);
this.pageCache.set(path, page);
}
return page;
}
parseRowHeader(page, index) {
const rowOffset = page.rowOffsets.get(index);
(0, _utilities.assert)(rowOffset != null); // TODO: This subarray call is unfortunate - look into getting rid of it
return rowHeaderParser.parse(page.data.subarray(rowOffset, rowOffset + rowHeaderSize));
}
buildRow(header, page, index, subIndex) {
const rowOffset = page.rowOffsets.get(index);
(0, _utilities.assert)(rowOffset != null); // TODO: This is duplicated work when using getRows() - look into consolidating
const rowHeader = this.parseRowHeader(page, index);
(0, _utilities.assert)(subIndex < 0 || subIndex < rowHeader.rowCount, `Requested subrow ${subIndex} of row ${index} outside valid range 0 - ${rowHeader.rowCount - 1}`);
const rowStart = rowOffset + rowHeaderSize;
const rowLength = header.rowSize + rowHeader.dataSize;
const rowData = page.data.subarray(rowStart, rowStart + rowLength);
return new this.RowClass({
index,
subIndex,
sheetHeader: header,
data: rowData
});
}
}
exports.Sheet = Sheet;