├── .babelrc ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── index.cjs ├── index.cjs.js ├── index.d.ts ├── index.js ├── map ├── index.cjs ├── index.cjs.js ├── index.d.ts ├── index.js └── package.json ├── node ├── index.cjs ├── index.cjs.js ├── index.d.ts ├── index.js └── package.json ├── package-lock.json ├── package.json ├── project.sublime-project ├── rollup.config.mjs ├── runnable └── create-commonjs-package-json.js ├── schema ├── index.cjs ├── index.cjs.js ├── index.d.ts ├── index.js └── package.json ├── source ├── read │ ├── coordinates.js │ ├── dropEmptyColumns.js │ ├── dropEmptyColumns.test.js │ ├── dropEmptyRows.js │ ├── dropEmptyRows.test.js │ ├── getData.js │ ├── isDateTimestamp.js │ ├── parseCell.js │ ├── parseCellValue.js │ ├── parseCells.js │ ├── parseDate.js │ ├── parseDate.test.js │ ├── parseDimensions.js │ ├── parseFilePaths.js │ ├── parseProperties.js │ ├── parseSharedStrings.js │ ├── parseSheet.js │ ├── parseStyles.js │ ├── readSheetNamesBrowser.js │ ├── readSheetNamesNode.js │ ├── readSheetNamesNode.test.js │ ├── readSheetNamesWebWorker.js │ ├── readXlsx.js │ ├── readXlsxFileBrowser.js │ ├── readXlsxFileContents.js │ ├── readXlsxFileNode.js │ ├── readXlsxFileNode.test.js │ ├── readXlsxFileWebWorker.js │ ├── schema │ │ ├── convertMapToSchema.js │ │ ├── convertMapToSchema.test.js │ │ ├── mapToObjects.js │ │ ├── mapToObjects.legacy.js │ │ ├── mapToObjects.legacy.test.js │ │ ├── mapToObjects.spreadsheet.js │ │ ├── mapToObjects.spreadsheet.test.js │ │ └── mapToObjects.test.js │ ├── unpackXlsxFileBrowser.js │ └── unpackXlsxFileNode.js ├── types │ ├── Boolean.js │ ├── Date.js │ ├── Email.js │ ├── Email.test.js │ ├── Integer.js │ ├── Integer.test.js │ ├── InvalidError.js │ ├── Number.js │ ├── String.js │ ├── URL.js │ └── URL.test.js └── xml │ ├── dom.js │ ├── xlsx.js │ ├── xml.js │ ├── xmlBrowser.js │ └── xpath │ ├── README.md │ ├── xlsx-xpath.js │ ├── xpathBrowser.js │ └── xpathNode.js ├── test ├── 1904.test.js ├── boolean.test.js ├── buffer.test.js ├── date.test.js ├── exports.test.js ├── inline-string.test.js ├── merged-cells.test.js ├── nonAsciiCharacterEncoding.test.js ├── parseNumber.test.js ├── requiredFunction.test.js ├── schemaEmptyRows.test.js ├── setup.js ├── sharedStrings.test.js ├── sheet.test.js ├── spreadsheets │ ├── 1904.xlsx │ ├── boolean.xlsx │ ├── course.xlsx │ ├── date.xlsx │ ├── excel_mac_2011-basic.xlsx │ ├── excel_mac_2011-formatting.xlsx │ ├── excel_multiple_text_nodes.xlsx │ ├── inline-string.xlsx │ ├── merged-cells.xlsx │ ├── multiple-sheets.xlsx │ ├── nonAsciiCharacterEncoding.xlsx │ ├── schemaEmptyRows.xlsx │ ├── sharedStrings.r.t.xlsx │ ├── string-formula.xlsx │ ├── trim.xlsx │ └── workbook-xml-namespace.xlsx ├── string-formula.test.js ├── test.test.js ├── trim.test.js └── workbook-xml-namespace.test.js ├── types.d.ts ├── web-worker ├── index.cjs ├── index.cjs.js ├── index.d.ts ├── index.js └── package.json └── website ├── index.html └── lib ├── prism.css ├── prism.js └── promise-polyfill.min.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": 3 | { 4 | "development": 5 | { 6 | "presets": 7 | [ 8 | "@babel/env" 9 | ], 10 | 11 | "plugins": 12 | [ 13 | "@babel/transform-runtime", 14 | ["@babel/transform-for-of", { "loose": true }], 15 | "@babel/proposal-class-properties" 16 | ] 17 | }, 18 | "commonjs": 19 | { 20 | "presets": 21 | [ 22 | "@babel/env" 23 | ], 24 | 25 | "plugins": 26 | [ 27 | ["@babel/transform-for-of", { "loose": true }], 28 | "@babel/proposal-class-properties" 29 | ] 30 | }, 31 | "es6": 32 | { 33 | "presets": 34 | [ 35 | ["@babel/env", { modules: false }] 36 | ], 37 | 38 | "plugins": 39 | [ 40 | ["@babel/transform-for-of", { "loose": true }], 41 | "@babel/proposal-class-properties" 42 | ] 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | # npm errors 4 | npm-debug.log 5 | 6 | # for OS X users 7 | .DS_Store 8 | 9 | # test coverage folder 10 | /coverage/ 11 | 12 | # browser builds 13 | /bundle/ 14 | 15 | # builds 16 | /commonjs/ 17 | /modules/ 18 | 19 | # Sublime 20 | *.sublime-workspace -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:12 2 | 3 | pages: 4 | script: 5 | - npm install 6 | - npm run build 7 | - mv ./bundle ./public 8 | - cp --recursive ./website/* ./public/ 9 | 10 | artifacts: 11 | paths: 12 | - public 13 | 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # git 2 | .gitignore 3 | .gitattributes 4 | 5 | # Babel 6 | .babelrc 7 | 8 | # Travis CI 9 | .travis.yml 10 | 11 | # test coverage folder 12 | /coverage/ 13 | 14 | # npm errors 15 | npm-debug.log 16 | 17 | # for OS X users 18 | .DS_Store 19 | 20 | # webpack config 21 | /webpack.config.babel.js 22 | 23 | /test/ 24 | /source/ 25 | *.test.js 26 | 27 | # Sublime 28 | *.sublime-workspace 29 | *.sublime-project 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | script: 6 | - "npm run test-travis" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 5.8.0 / 01.05.2024 2 | ================== 3 | 4 | * Added new parameters to replace the old `includeNullValues: true` parameter: `schemaPropertyValueEmptyCellValue`, `schemaPropertyValueForMissingColumn`, `getEmptyObjectValue`. Now `includeNullValues: true` could be replaced with the following combination of parameters: 5 | * `schemaPropertyValueForMissingColumn: null` 6 | * `schemaPropertyValueForEmptyCell: null` 7 | * `getEmptyObjectValue = (object, { path? }) => null` 8 | * Added `schemaPropertyShouldSkipRequiredValidationForMissingColumn: () => boolean` parameter. It is `() => false` by default. 9 | * Added `getEmptyArrayValue: (array, { path }) => any` parameter. It is `() => null` by default. 10 | 11 | 5.5.0 / 14.09.2022 12 | ================== 13 | 14 | * [Moved](https://gitlab.com/catamphetamine/read-excel-file/-/issues/62) from `jszip` to `fflate`. Most likely not a "breaking change". See [browser support](https://github.com/101arrowz/fflate/#browser-support). 15 | 16 | 5.4.3 / 20.07.2022 17 | ================== 18 | 19 | * [Added](https://gitlab.com/catamphetamine/read-excel-file/-/issues/7) `ignoreEmptyRows: false` option when parsing using a `schema`. 20 | 21 | * Changed `errors.row` property when parsing using a `schema`: from "object number" to "spreadsheet row number". Example: was `1` for the first row of data, now is `2` for the first row of data (because `1` now is the header row). 22 | 23 | 5.4.0 / 04.07.2022 24 | ================== 25 | 26 | * [Fixed](https://gitlab.com/catamphetamine/read-excel-file/-/issues/54) non-ASCII character encoding by forcing Node.js version of the library to read zipped contents of an XLSX file in UTF-8 character encoding. I suppose it won't break the existing code. 27 | 28 | 5.3.5 / 26.06.2022 29 | ================== 30 | 31 | * Added `includeNullValues: true` option when parsing spreadsheet data using a `schema`. By default, it ignores all `null` values (ignores all empty cells). 32 | 33 | 5.3.4 / 11.06.2022 34 | ================== 35 | 36 | * Added an optional `reason?: string` property of a with-schema parsing error. 37 | 38 | 5.3.3 / 24.05.2022 39 | ================== 40 | 41 | * Added `trim: false` option. 42 | 43 | 5.3.0 / 18.05.2022 44 | ================== 45 | 46 | * Migrated to [ES Modules](https://gitlab.com/catamphetamine/read-excel-file/-/issues/44) exports. 47 | 48 | 5.2.27 / 11.02.2022 49 | ================== 50 | 51 | * Added `readSheetNames()` function. 52 | 53 | 5.2.25 / 19.11.2021 54 | ================== 55 | 56 | * [Fixed](https://github.com/catamphetamine/read-excel-file/issues/102) skipping empty rows and columns at the start. 57 | 58 | 5.2.22 / 11.11.2021 59 | ================== 60 | 61 | * [Added](https://github.com/catamphetamine/read-excel-file/issues/100) `/web-worker` export. 62 | 63 | 5.2.11 / 08.10.2021 64 | ================== 65 | 66 | * Added TypeScript "typings". 67 | 68 | 5.2.0 / 17.06.2021 69 | ================== 70 | 71 | * (internal) Removed `xpath` dependency to reduce bundle size. 72 | 73 | * (internal) Removed `xmldom` dependency in the browser to reduce bundle size. 74 | 75 | * (internal) Fixed date parser: in previous versions it was setting time to `12:00` instead of `00:00`. 76 | 77 | * (internal) `readXlsxFile()`: Added support for `e`, `d`, `z` and `inlineStr` cell types. 78 | 79 | 5.1.0 / 06.04.2021 80 | ================== 81 | 82 | * Simply updated all dependencies to their latest version. 83 | 84 | 5.0.0 / 27.12.2020 85 | ================== 86 | 87 | * `readXlsxFile()` now [doesn't skip](https://gitlab.com/catamphetamine/read-excel-file/-/issues/10) empty rows or columns: it only skips empty rows or columns at the end, but not in the beginning and not in the middle as it used to. 88 | 89 | * Removed `"URL"`, `"Email"`, `"Integer"` types. Use non-string exported ones instead: `URL`, `Email`, `Integer`. 90 | 91 | * Removed undocumented `convertToJson()` export. 92 | 93 | * Removed undocumented `read-excel-file/json` export. 94 | 95 | 4.1.0 / 09.11.2020 96 | ================== 97 | 98 | * Renamed schema entry `parse()` function: now it's called `type`. This way, `type` could be both a built-in type and a custom type. 99 | 100 | * Changed the built-in `"Integer"`, `"URL"` and `"Email"` types: now they're exported functions again instead of strings. Strings still work. 101 | 102 | * Added `map` parameter: similar to `schema` but doesn't perform any parsing or validation. Can be used to map an Excel file to an array of objects that could be parsed/validated using [`yup`](https://github.com/jquense/yup). 103 | 104 | * `type` of a schema entry is no longer required: if no `type` is specified, then the cell value is returned "as is" (string, or number, or boolean, or `Date`). 105 | 106 | 4.0.8 / 08.11.2020 107 | ================== 108 | 109 | * Updated `JSZip` to the latest version. The [issue](https://gitlab.com/catamphetamine/read-excel-file/-/issues/8). The [original issue](https://github.com/catamphetamine/read-excel-file/issues/54). 110 | 111 | 4.0.0 / 25.05.2019 112 | ================== 113 | 114 | * (breaking change) Turned out that `sheetId` is [not the file name](https://github.com/tidyverse/readxl/issues/104) of the sheet. Instead, the filename of the sheet is looked up by `r:id` (or `ns:id`) in the `xl/_rels/workbook.xml.rels` file. That means that reading Excel file sheets by their numeric `sheet` ID is no longer supported in `readXlsxFile()` and if `sheet` option is specified then it means either "sheet index" (starting from `1`) or "sheet name". Also, removed the old deprecated way of passing `sheet` option directly as `readXlsxFile(file, sheet)` instead of `readXlsxFile(file, { sheet })`. 115 | 116 | 3.0.1 / 13.05.2019 117 | ================== 118 | 119 | * Fixed [IE 11 error](https://github.com/catamphetamine/read-excel-file/issues/26) `"XPathResult is undefined"` by including a polyfill for XPath. This resulted in the browser bundle becoming larger in size by 100 kilobytes. 120 | 121 | 3.0.0 / 30.06.2018 122 | ================== 123 | 124 | * (breaking change) Calling this library with `getSheets: true` option now returns an array of objects of shape `{ name }` rather than an object of shape `{ [id]: 'name' }`. Same's for calling this library with `properties: true` option. 125 | 126 | * (breaking change) Previous versions returned empty data in case of an error. Now if there're any errors they're thrown as-is and not suppressed. 127 | 128 | * (unlikely breaking change) Previous versions read the `sheet` having ID `1` by default. It was [discovered](https://github.com/catamphetamine/read-excel-file/issues/24) that this could lead to unintuitive behavior in some Excel editors when sheets order is changed by a user: in some editors a sheet with ID `1` could be moved to, for example, the second position, and would still have the ID `1` so for such Excel files by default the library would read the second sheet instead of the first one which would result in confusing behavior. In any case, numerical sheet IDs are inherently internal to the Excel file structure and shouldn't be externalized in any way (in this case, in the code reading such files) so the library now still accepts the numerical `sheet` parameter but rather than being interpreted as a numerical sheet ID it's now interpreted as a numerical sheet index (starting from `1`). If your code passes a numerical `sheet` ID parameter to the library then it will most likely behave the same way with the new version because in most cases a numerical sheet ID is the same as a numerical sheet index. This change is very unlikely to break anyone's code, but just to conform with the SEMVER specification this change is released as a "breaking change" because theoretically there could exist some very rare users affected by the change. 129 | 130 | * (very unlikely breaking change) Removed legacy support for numerical `sheet` IDs passed not as numbers but as strings. For example, `sheet: "2"` instead of `sheet: 2`. A string `sheet` parameter is now always treated as a sheet name. 131 | 132 | 2.0.1 / 26.06.2018 133 | ================== 134 | 135 | * Fixed `NaN`s appearing in the input instead of `null`s (and empty columns not being trimmed). 136 | 137 | * Added "smart date parser" which autodetects and parses most date formats. 138 | 139 | 2.0.0 / 09.06.2018 140 | ================== 141 | 142 | * (breaking change) If using `readXlsx()` without `schema` parameter it now parses boolean cell values as `true`/`false` and numerical cell values are now parsed as numbers, and also date cell values are parsed as dates in some cases (numbers otherwise). If using `readXlsx()` with `schema` parameter then there are no breaking changes. 143 | 144 | * Added `dateFormat` parameter (e.g. `mm/dd/yyyy`) for parsing dates automatically when using `readXlsx()` without `schema` parameter. 145 | 146 | * Added `read-excel-file/json` export for `convertToJson()`. 147 | 148 | 1.3.0 / 11.05.2018 149 | ================== 150 | 151 | * Refactored exports. 152 | * Fixed some empty columns returning an empty string instead of `null`. 153 | * Added `Integer` type for integer `Number`s. Also `URL` and `Email`. 154 | * Export `parseExcelDate()` function. 155 | * If both `parse()` and `type` are defined in a schema then `parse()` takes priority over `type`. 156 | 157 | 1.2.0 / 25.03.2018 158 | ================== 159 | 160 | * Rewrote `schema` JSON parsing. 161 | 162 | 1.1.0 / 24.03.2018 163 | ================== 164 | 165 | * Added `schema` option for JSON parsing. 166 | 167 | 1.0.0 / 21.03.2018 168 | ================== 169 | 170 | * Initial release. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and free environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a censorship-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating an open and free environment 15 | include: 16 | 17 | * Not constraining the language to be "welcoming" or "inclusive" 18 | * Not demanding show of empathy towards other community members 19 | * Not dictating anyone to be respectful of differing viewpoints and experiences 20 | * Not forcing anyone to change their views or opinions regardless of those 21 | * Not intimidating other people into accepting your own views or opinions 22 | * Not blackmailing other people to disclose their personal views or opinions 23 | * Not constraining other people from publishing their personal views or opinions in an unintrusive way 24 | * Focusing on what is best for the ecosystem 25 | 26 | Examples of acceptable behavior by participants include: 27 | 28 | * The use of sexualized language 29 | * Occasional trolling or insulting comments that are not completely off-topic 30 | 31 | Examples of unacceptable behavior by participants include: 32 | 33 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 34 | * Unwelcome sexual attention or advances 35 | * Public harassment or personal attacks when carried out in an bold or intrusive way 36 | * Private harassment 37 | * Any actions that are in violation of the local laws or otherwise considered illegal 38 | * Other conduct which could reasonably be considered inappropriate in an open and free setting 39 | 40 | ## Our Responsibilities 41 | 42 | Project maintainers are responsible for clarifying the standards of acceptable 43 | behavior and are free to take appropriate and fair corrective action in 44 | response to any instances of unacceptable behavior. 45 | 46 | Project maintainers have the right and authority to remove, edit, or 47 | reject comments, commits, code, wiki edits, issues, and other contributions 48 | that are not aligned to this Code of Conduct, or to ban temporarily or 49 | permanently any contributor for other behaviors that they deem inappropriate, 50 | threatening, offensive, or harmful. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies both within project spaces and in public spaces 55 | when an individual is representing the project or its community. Examples of 56 | representing a project or community include using an official project e-mail 57 | address, posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. Representation of a project may be 59 | further defined and clarified by project maintainers. 60 | 61 | ## Enforcement 62 | 63 | Instances of unacceptable behavior may be reported by contacting the project team. 64 | The complaints will likely be reviewed and investigated and may result in a response that 65 | is deemed necessary and appropriate to the circumstances. The project team should maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 gitlab.com/catamphetamine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.cjs: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('./commonjs/read/readXlsxFileBrowser.js').default 2 | exports['default'] = require('./commonjs/read/readXlsxFileBrowser.js').default 3 | exports.readSheetNames = require('./commonjs/read/readSheetNamesBrowser.js').default 4 | exports.parseExcelDate = require('./commonjs/read/parseDate.js').default 5 | exports.Integer = require('./commonjs/types/Integer.js').default 6 | exports.Email = require('./commonjs/types/Email.js').default 7 | exports.URL = require('./commonjs/types/URL.js').default 8 | -------------------------------------------------------------------------------- /index.cjs.js: -------------------------------------------------------------------------------- 1 | // This file is deprecated. 2 | // It's the same as `index.cjs`, just with an added `*.js` extension. 3 | // It fixes the issue when some software doesn't see files with `*.cjs` file extensions 4 | // when used as the `main` property value in `package.json`. 5 | 6 | exports = module.exports = require('./commonjs/read/readXlsxFileBrowser.js').default 7 | exports['default'] = require('./commonjs/read/readXlsxFileBrowser.js').default 8 | exports.readSheetNames = require('./commonjs/read/readSheetNamesBrowser.js').default 9 | exports.parseExcelDate = require('./commonjs/read/parseDate.js').default 10 | exports.Integer = require('./commonjs/types/Integer.js').default 11 | exports.Email = require('./commonjs/types/Email.js').default 12 | exports.URL = require('./commonjs/types/URL.js').default 13 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ParseWithSchemaOptions, 3 | ParseWithMapOptions, 4 | ParseWithoutSchemaOptions, 5 | ParsedObjectsResult, 6 | Row 7 | } from './types.d.js'; 8 | 9 | export { 10 | Schema, 11 | ParsedObjectsResult, 12 | Error, 13 | CellValue, 14 | Row, 15 | Integer, 16 | Email, 17 | URL 18 | } from './types.d.js'; 19 | 20 | export function parseExcelDate(excelSerialDate: number) : typeof Date; 21 | 22 | type Input = File | Blob | ArrayBuffer; 23 | 24 | export function readXlsxFile(input: Input, options: ParseWithSchemaOptions) : Promise>; 25 | export function readXlsxFile(input: Input, options: ParseWithMapOptions) : Promise>; 26 | export function readXlsxFile(input: Input, options?: ParseWithoutSchemaOptions) : Promise; 27 | 28 | export function readSheetNames(input: Input) : Promise; 29 | 30 | export default readXlsxFile; 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from './modules/read/readXlsxFileBrowser.js' 2 | export { default as readSheetNames } from './modules/read/readSheetNamesBrowser.js' 3 | export { default as parseExcelDate } from './modules/read/parseDate.js' 4 | export { default as Integer } from './modules/types/Integer.js' 5 | export { default as Email } from './modules/types/Email.js' 6 | export { default as URL } from './modules/types/URL.js' 7 | -------------------------------------------------------------------------------- /map/index.cjs: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('../commonjs/read/schema/mapToObjects.js').default 2 | exports['default'] = require('../commonjs/read/schema/mapToObjects.js').default -------------------------------------------------------------------------------- /map/index.cjs.js: -------------------------------------------------------------------------------- 1 | // This file is deprecated. 2 | // It's the same as `index.cjs`, just with an added `*.js` extension. 3 | // It fixes the issue when some software doesn't see files with `*.cjs` file extensions 4 | // when used as the `main` property value in `package.json`. 5 | 6 | exports = module.exports = require('../commonjs/read/schema/mapToObjects.js').default 7 | exports['default'] = require('../commonjs/read/schema/mapToObjects.js').default -------------------------------------------------------------------------------- /map/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Row, 3 | Schema, 4 | Error, 5 | MappingParameters 6 | } from '../types.d.js'; 7 | 8 | export { 9 | MappingParameters 10 | } from '../types.d.js' 11 | 12 | export default function map(data: Row[], schema: Schema, options?: MappingParameters): { 13 | rows: T[]; 14 | errors: Error[]; 15 | }; -------------------------------------------------------------------------------- /map/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from '../modules/read/schema/mapToObjects.js' -------------------------------------------------------------------------------- /map/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "read-excel-file/schema", 4 | "version": "1.0.0", 5 | "main": "index.cjs", 6 | "module": "index.js", 7 | "types": "./index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./index.d.ts", 12 | "import": "./index.js", 13 | "require": "./index.cjs" 14 | } 15 | }, 16 | "sideEffects": false 17 | } 18 | -------------------------------------------------------------------------------- /node/index.cjs: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('../commonjs/read/readXlsxFileNode.js').default 2 | exports['default'] = require('../commonjs/read/readXlsxFileNode.js').default 3 | exports.readSheetNames = require('../commonjs/read/readSheetNamesNode.js').default 4 | exports.parseExcelDate = require('../commonjs/read/parseDate.js').default 5 | exports.Integer = require('../commonjs/types/Integer.js').default 6 | exports.Email = require('../commonjs/types/Email.js').default 7 | exports.URL = require('../commonjs/types/URL.js').default -------------------------------------------------------------------------------- /node/index.cjs.js: -------------------------------------------------------------------------------- 1 | // This file is deprecated. 2 | // It's the same as `index.cjs`, just with an added `*.js` extension. 3 | // It fixes the issue when some software doesn't see files with `*.cjs` file extensions 4 | // when used as the `main` property value in `package.json`. 5 | 6 | exports = module.exports = require('../commonjs/read/readXlsxFileNode.js').default 7 | exports['default'] = require('../commonjs/read/readXlsxFileNode.js').default 8 | exports.readSheetNames = require('../commonjs/read/readSheetNamesNode.js').default 9 | exports.parseExcelDate = require('../commonjs/read/parseDate.js').default 10 | exports.Integer = require('../commonjs/types/Integer.js').default 11 | exports.Email = require('../commonjs/types/Email.js').default 12 | exports.URL = require('../commonjs/types/URL.js').default -------------------------------------------------------------------------------- /node/index.d.ts: -------------------------------------------------------------------------------- 1 | // See the discussion: 2 | // https://github.com/catamphetamine/read-excel-file/issues/71 3 | 4 | import { PathLike } from 'fs'; 5 | import { Stream } from 'stream'; 6 | 7 | import { 8 | ParseWithSchemaOptions, 9 | ParseWithMapOptions, 10 | ParseWithoutSchemaOptions, 11 | ParsedObjectsResult, 12 | Row 13 | } from '../types.d.js'; 14 | 15 | export { 16 | Schema, 17 | ParsedObjectsResult, 18 | Error, 19 | CellValue, 20 | Row, 21 | Integer, 22 | Email, 23 | URL 24 | } from '../types.d.js'; 25 | 26 | export function parseExcelDate(excelSerialDate: number) : typeof Date; 27 | 28 | type Input = Stream | Buffer | PathLike; 29 | 30 | export function readXlsxFile(input: Input, options: ParseWithSchemaOptions) : Promise>; 31 | export function readXlsxFile(input: Input, options: ParseWithMapOptions) : Promise>; 32 | export function readXlsxFile(input: Input, options?: ParseWithoutSchemaOptions) : Promise; 33 | 34 | export function readSheetNames(input: Input) : Promise; 35 | 36 | export default readXlsxFile; -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from '../modules/read/readXlsxFileNode.js' 2 | export { default as readSheetNames } from '../modules/read/readSheetNamesNode.js' 3 | export { default as parseExcelDate } from '../modules/read/parseDate.js' 4 | export { default as Integer } from '../modules/types/Integer.js' 5 | export { default as Email } from '../modules/types/Email.js' 6 | export { default as URL } from '../modules/types/URL.js' 7 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "read-excel-file/node", 4 | "version": "1.0.0", 5 | "main": "index.cjs", 6 | "module": "index.js", 7 | "types": "./index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./index.d.ts", 12 | "import": "./index.js", 13 | "require": "./index.cjs" 14 | } 15 | }, 16 | "sideEffects": false 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "read-excel-file", 3 | "version": "5.8.8", 4 | "description": "Read small to medium `*.xlsx` files in a browser or Node.js. Parse to JSON with a strict schema.", 5 | "module": "index.js", 6 | "main": "index.cjs", 7 | "type": "module", 8 | "exports": { 9 | ".": { 10 | "types": "./index.d.ts", 11 | "import": "./index.js", 12 | "require": "./index.cjs" 13 | }, 14 | "./node": { 15 | "types": "./node/index.d.ts", 16 | "import": "./node/index.js", 17 | "require": "./node/index.cjs" 18 | }, 19 | "./web-worker": { 20 | "types": "./web-worker/index.d.ts", 21 | "import": "./web-worker/index.js", 22 | "require": "./web-worker/index.cjs" 23 | }, 24 | "./schema": { 25 | "types": "./schema/index.d.ts", 26 | "import": "./schema/index.js", 27 | "require": "./schema/index.cjs" 28 | }, 29 | "./map": { 30 | "types": "./map/index.d.ts", 31 | "import": "./map/index.js", 32 | "require": "./map/index.cjs" 33 | }, 34 | "./package.json": "./package.json" 35 | }, 36 | "sideEffects": false, 37 | "types": "./index.d.ts", 38 | "scripts": { 39 | "test": "mocha --colors --bail --reporter spec --require ./test/setup.js \"./{,!(node_modules|commonjs|modules)/**/}*.test.js\" --recursive", 40 | "clean-for-build": "rimraf ./commonjs/**/* ./modules/**/*", 41 | "build-commonjs-modules": "better-npm-run build-commonjs-modules", 42 | "build-commonjs-package.json": "node runnable/create-commonjs-package-json.js", 43 | "build-commonjs": "npm-run-all build-commonjs-modules build-commonjs-package.json", 44 | "build-es6-modules": "better-npm-run build-es6-modules", 45 | "browser-build": "rollup --config rollup.config.mjs", 46 | "build": "npm-run-all clean-for-build build-commonjs build-es6-modules browser-build", 47 | "prepublishOnly": "npm-run-all build test browser-build" 48 | }, 49 | "dependencies": { 50 | "@xmldom/xmldom": "^0.8.2", 51 | "fflate": "^0.7.3", 52 | "unzipper": "^0.12.2" 53 | }, 54 | "devDependencies": { 55 | "@babel/cli": "^7.17.10", 56 | "@babel/core": "^7.17.12", 57 | "@babel/plugin-proposal-class-properties": "^7.17.12", 58 | "@babel/plugin-transform-for-of": "^7.17.12", 59 | "@babel/plugin-transform-runtime": "^7.17.12", 60 | "@babel/preset-env": "^7.17.12", 61 | "@babel/register": "^7.17.7", 62 | "better-npm-run": "^0.1.1", 63 | "chai": "^4.3.6", 64 | "core-js": "^3.22.5", 65 | "mocha": "^10.0.0", 66 | "npm-run-all": "^4.1.5", 67 | "regenerator-runtime": "^0.13.9", 68 | "rimraf": "^3.0.2", 69 | "rollup": "^2.73.0", 70 | "rollup-plugin-commonjs": "^10.1.0", 71 | "rollup-plugin-json": "^4.0.0", 72 | "rollup-plugin-node-resolve": "^5.2.0", 73 | "rollup-plugin-terser": "^7.0.2", 74 | "xpath": "0.0.32" 75 | }, 76 | "betterScripts": { 77 | "browser-build": { 78 | "command": "webpack --mode production --progress --colors", 79 | "env": { 80 | "WEBPACK_ENV": "build" 81 | } 82 | }, 83 | "build-commonjs-modules": { 84 | "command": "babel ./source --out-dir ./commonjs --source-maps", 85 | "env": { 86 | "BABEL_ENV": "commonjs" 87 | } 88 | }, 89 | "build-es6-modules": { 90 | "command": "babel ./source --out-dir ./modules --source-maps", 91 | "env": { 92 | "BABEL_ENV": "es6" 93 | } 94 | } 95 | }, 96 | "repository": { 97 | "type": "git", 98 | "url": "https://gitlab.com/catamphetamine/read-excel-file" 99 | }, 100 | "keywords": [ 101 | "excel", 102 | "xlsx", 103 | "browser", 104 | "json" 105 | ], 106 | "author": "catamphetamine ", 107 | "license": "MIT", 108 | "bugs": { 109 | "url": "https://gitlab.com/catamphetamine/read-excel-file/issues" 110 | }, 111 | "homepage": "https://gitlab.com/catamphetamine/read-excel-file#readme" 112 | } 113 | -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": ".", 7 | "folder_exclude_patterns": ["read-excel-file/node_modules", "read-excel-file/coverage", "read-excel-file/modules", "read-excel-file/lib", "read-excel-file/commonjs", "project.sublime-workspace"], 8 | "file_exclude_patterns": ["read-excel-file/bundle/read-excel-file.min.js", "read-excel-file/bundle/read-excel-file.min.js.map"] 9 | } 10 | ], 11 | "settings": 12 | { 13 | "tab_size": 2, 14 | "translate_tabs_to_spaces": false, 15 | "trim_trailing_white_space_on_save": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import json from 'rollup-plugin-json' 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | import commonjs from 'rollup-plugin-commonjs' 4 | import { terser } from 'rollup-plugin-terser' 5 | 6 | export default [ 7 | { 8 | // `index.js` didn't work because it "mixes named and default exports". 9 | // input: './index', 10 | input: './modules/read/readXlsxFileBrowser', 11 | plugins: [ 12 | json(), 13 | terser(), 14 | nodeResolve({ 15 | browser: true 16 | }), 17 | commonjs() 18 | ], 19 | external: [ 20 | // 'react', 21 | // 'prop-types' 22 | ], 23 | output: { 24 | format: 'umd', 25 | name: 'readXlsxFile', 26 | file: 'bundle/read-excel-file.min.js', 27 | sourcemap: true, 28 | globals: { 29 | // 'react': 'React', 30 | // 'prop-types': 'PropTypes' 31 | } 32 | } 33 | } 34 | ] -------------------------------------------------------------------------------- /runnable/create-commonjs-package-json.js: -------------------------------------------------------------------------------- 1 | // Creates a `package.json` file in the CommonJS `build` folder. 2 | // That marks that whole folder as CommonJS so that Node.js doesn't complain 3 | // about `require()`-ing those files. 4 | 5 | import fs from 'fs' 6 | 7 | fs.writeFileSync('./commonjs/package.json', JSON.stringify({ 8 | name: 'read-excel-file/commonjs', 9 | type: 'commonjs', 10 | private: true 11 | }, null, 2), 'utf8') -------------------------------------------------------------------------------- /schema/index.cjs: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('../commonjs/read/schema/mapToObjects.legacy.js').default 2 | exports['default'] = require('../commonjs/read/schema/mapToObjects.legacy.js').default -------------------------------------------------------------------------------- /schema/index.cjs.js: -------------------------------------------------------------------------------- 1 | // This file is deprecated. 2 | // It's the same as `index.cjs`, just with an added `*.js` extension. 3 | // It fixes the issue when some software doesn't see files with `*.cjs` file extensions 4 | // when used as the `main` property value in `package.json`. 5 | 6 | exports = module.exports = require('../commonjs/read/schema/mapToObjects.legacy.js').default 7 | exports['default'] = require('../commonjs/read/schema/mapToObjects.legacy.js').default -------------------------------------------------------------------------------- /schema/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Row, 3 | Schema 4 | } from '../types.d.js'; 5 | 6 | export default function mapWithLegacyBehavior(data: Row[], schema: Schema, options?: { 7 | ignoreEmptyRows?: boolean, 8 | includeNullValues?: boolean, 9 | isColumnOriented?: boolean, 10 | rowMap?: Record 11 | }): T[]; -------------------------------------------------------------------------------- /schema/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from '../modules/read/schema/mapToObjects.legacy.js' -------------------------------------------------------------------------------- /schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "read-excel-file/schema", 4 | "version": "1.0.0", 5 | "main": "index.cjs", 6 | "module": "index.js", 7 | "types": "./index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./index.d.ts", 12 | "import": "./index.js", 13 | "require": "./index.cjs" 14 | } 15 | }, 16 | "sideEffects": false 17 | } 18 | -------------------------------------------------------------------------------- /source/read/coordinates.js: -------------------------------------------------------------------------------- 1 | // Maps "A1"-like coordinates to `{ row, column }` numeric coordinates. 2 | const LETTERS = ["", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] 3 | 4 | export function calculateDimensions (cells) { 5 | const comparator = (a, b) => a - b 6 | const allRows = cells.map(cell => cell.row).sort(comparator) 7 | const allCols = cells.map(cell => cell.column).sort(comparator) 8 | const minRow = allRows[0] 9 | const maxRow = allRows[allRows.length - 1] 10 | const minCol = allCols[0] 11 | const maxCol = allCols[allCols.length - 1] 12 | 13 | return [ 14 | { row: minRow, column: minCol }, 15 | { row: maxRow, column: maxCol } 16 | ] 17 | } 18 | 19 | // Converts a letter coordinate to a digit coordinate. 20 | // Examples: "A" -> 1, "B" -> 2, "Z" -> 26, "AA" -> 27, etc. 21 | function columnLettersToNumber(columnLetters) { 22 | // `for ... of ...` would require Babel polyfill for iterating a string. 23 | let n = 0 24 | let i = 0 25 | while (i < columnLetters.length) { 26 | n *= 26 27 | n += LETTERS.indexOf(columnLetters[i]) 28 | i++ 29 | } 30 | return n 31 | } 32 | 33 | export function parseCellCoordinates(coords) { 34 | // Coordinate examples: "AA2091", "R988", "B1". 35 | coords = coords.split(/(\d+)/) 36 | return [ 37 | // Row. 38 | parseInt(coords[1]), 39 | // Column. 40 | columnLettersToNumber(coords[0].trim()) 41 | ] 42 | } -------------------------------------------------------------------------------- /source/read/dropEmptyColumns.js: -------------------------------------------------------------------------------- 1 | export default function dropEmptyColumns(data, { 2 | accessor = _ => _, 3 | onlyTrimAtTheEnd 4 | } = {}) { 5 | let i = data[0].length - 1 6 | while (i >= 0) { 7 | let empty = true 8 | for (const row of data) { 9 | if (accessor(row[i]) !== null) { 10 | empty = false 11 | break 12 | } 13 | } 14 | if (empty) { 15 | let j = 0; 16 | while (j < data.length) { 17 | data[j].splice(i, 1) 18 | j++ 19 | } 20 | } else if (onlyTrimAtTheEnd) { 21 | break 22 | } 23 | i-- 24 | } 25 | return data 26 | } -------------------------------------------------------------------------------- /source/read/dropEmptyColumns.test.js: -------------------------------------------------------------------------------- 1 | import dropEmptyColumns from './dropEmptyColumns.js' 2 | 3 | describe('dropEmptyColumns', () => { 4 | it('should drop empty columns (only at the end)', () => { 5 | dropEmptyColumns([ 6 | [null, 'A', 'B', 'C', null, null], 7 | [null, 'D', null, null, null, null], 8 | [null, null, null, null, null, null], 9 | [null, null, 'E', 'F', 'G', null] 10 | ], { 11 | onlyTrimAtTheEnd: true 12 | }) 13 | .should.deep.equal([ 14 | [null, 'A', 'B', 'C', null], 15 | [null, 'D', null, null, null], 16 | [null, null, null, null, null], 17 | [null, null, 'E', 'F', 'G'] 18 | ]) 19 | }) 20 | 21 | it('should drop empty columns', () => { 22 | dropEmptyColumns([ 23 | [null, 'A', 'B', 'C', null, null], 24 | [null, 'D', null, null, null, null], 25 | [null, null, null, null, null, null], 26 | [null, null, 'E', 'F', 'G', null] 27 | ]) 28 | .should.deep.equal([ 29 | ['A', 'B', 'C', null], 30 | ['D', null, null, null], 31 | [null, null, null, null], 32 | [null, 'E', 'F', 'G'] 33 | ]) 34 | }) 35 | }) -------------------------------------------------------------------------------- /source/read/dropEmptyRows.js: -------------------------------------------------------------------------------- 1 | export default function dropEmptyRows(data, { 2 | rowIndexMap, 3 | accessor = _ => _, 4 | onlyTrimAtTheEnd 5 | } = {}) { 6 | // Drop empty rows. 7 | let i = data.length - 1 8 | while (i >= 0) { 9 | // Check if the row is empty. 10 | let empty = true 11 | for (const cell of data[i]) { 12 | if (accessor(cell) !== null) { 13 | empty = false 14 | break 15 | } 16 | } 17 | // Remove the empty row. 18 | if (empty) { 19 | data.splice(i, 1) 20 | if (rowIndexMap) { 21 | rowIndexMap.splice(i, 1) 22 | } 23 | } else if (onlyTrimAtTheEnd) { 24 | break 25 | } 26 | i-- 27 | } 28 | return data 29 | } -------------------------------------------------------------------------------- /source/read/dropEmptyRows.test.js: -------------------------------------------------------------------------------- 1 | import dropEmptyRows from './dropEmptyRows.js' 2 | 3 | describe('dropEmptyRows', () => { 4 | it('should drop empty rows (only at the end)', () => { 5 | dropEmptyRows([ 6 | [null, null, null], 7 | ['A', 'B', 'C'], 8 | [null, 'D', null], 9 | [null, null, null], 10 | ['E', 'F', 'G'], 11 | [null, null, null] 12 | ], { 13 | onlyTrimAtTheEnd: true 14 | }) 15 | .should.deep.equal([ 16 | [null, null, null], 17 | ['A', 'B', 'C'], 18 | [null, 'D', null], 19 | [null, null, null], 20 | ['E', 'F', 'G'] 21 | ]) 22 | }) 23 | 24 | it('should drop empty rows', () => { 25 | dropEmptyRows([ 26 | [null, null, null], 27 | ['A', 'B', 'C'], 28 | [null, 'D', null], 29 | [null, null, null], 30 | ['E', 'F', 'G'], 31 | [null, null, null] 32 | ]) 33 | .should.deep.equal([ 34 | ['A', 'B', 'C'], 35 | [null, 'D', null], 36 | ['E', 'F', 'G'] 37 | ]) 38 | }) 39 | 40 | it('should generate row map when dropping empty rows', () => { 41 | const rowIndexMap = [0, 1, 2, 3, 4] 42 | 43 | dropEmptyRows([ 44 | [null, null, null], 45 | ['A', 'B', 'C'], 46 | [null, 'D', null], 47 | [null, null, null], 48 | ['E', 'F', 'G'] 49 | ], 50 | { rowIndexMap }) 51 | .should.deep.equal([ 52 | ['A', 'B', 'C'], 53 | [null, 'D', null], 54 | ['E', 'F', 'G'] 55 | ]) 56 | 57 | rowIndexMap.should.deep.equal([1, 2, 4]) 58 | }) 59 | }) -------------------------------------------------------------------------------- /source/read/getData.js: -------------------------------------------------------------------------------- 1 | import dropEmptyRows from './dropEmptyRows.js' 2 | import dropEmptyColumns from './dropEmptyColumns.js' 3 | 4 | export default function getData(sheet, options) { 5 | const { dimensions, cells } = sheet 6 | 7 | // If the sheet is empty. 8 | if (cells.length === 0) { 9 | return [] 10 | } 11 | 12 | const [leftTop, rightBottom] = dimensions 13 | 14 | // Don't discard empty rows or columns at the start. 15 | // https://github.com/catamphetamine/read-excel-file/issues/102 16 | // const colsCount = (rightBottom.column - leftTop.column) + 1 17 | // const rowsCount = (rightBottom.row - leftTop.row) + 1 18 | 19 | const colsCount = rightBottom.column 20 | const rowsCount = rightBottom.row 21 | 22 | // Initialize spreadsheet data structure. 23 | let data = new Array(rowsCount) 24 | let i = 0 25 | while (i < rowsCount) { 26 | data[i] = new Array(colsCount) 27 | let j = 0 28 | while (j < colsCount) { 29 | data[i][j] = null 30 | j++ 31 | } 32 | i++ 33 | } 34 | 35 | // Fill in spreadsheet `data`. 36 | // (this code implies that `cells` aren't necessarily sorted by row and column: 37 | // maybe that's not correct, this piece code was initially copy-pasted 38 | // from some other library that used `XPath`) 39 | for (const cell of cells) { 40 | // Don't discard empty rows or columns at the start. 41 | // https://github.com/catamphetamine/read-excel-file/issues/102 42 | // const rowIndex = cell.row - leftTop.row 43 | // const columnIndex = cell.column - leftTop.column 44 | const rowIndex = cell.row - 1 45 | const columnIndex = cell.column - 1 46 | // Ignore the data in the cell if it's outside of the spreadsheet's "dimensions". 47 | if (columnIndex < colsCount && rowIndex < rowsCount) { 48 | data[rowIndex][columnIndex] = cell.value 49 | } 50 | } 51 | 52 | // Fill in the row map. 53 | const { rowMap: rowIndexMap } = options 54 | if (rowIndexMap) { 55 | let i = 0 56 | while (i < data.length) { 57 | rowIndexMap[i] = i 58 | i++ 59 | } 60 | } 61 | 62 | // Drop empty columns or rows. 63 | data = dropEmptyRows( 64 | dropEmptyColumns(data, { onlyTrimAtTheEnd: true }), 65 | { onlyTrimAtTheEnd: true, rowIndexMap } 66 | ) 67 | 68 | // Optionally transform data before applying `schema`. 69 | if (options.transformData) { 70 | data = options.transformData(data) 71 | // data = options.transformData(data, { 72 | // dropEmptyRowsAndColumns(data) { 73 | // return dropEmptyRows(dropEmptyColumns(data), { rowIndexMap }) 74 | // } 75 | // }) 76 | } 77 | 78 | return data 79 | } -------------------------------------------------------------------------------- /source/read/isDateTimestamp.js: -------------------------------------------------------------------------------- 1 | // XLSX does have "d" type for dates, but it's not commonly used. 2 | // Instead, it prefers using "n" type for storing dates as timestamps. 3 | // 4 | // Whether a numeric value is a number or a date timestamp, it sometimes could be 5 | // detected by looking at the value "format" and seeing if it's a date-specific one. 6 | // https://github.com/catamphetamine/read-excel-file/issues/3#issuecomment-395770777 7 | // 8 | // The list of generic numeric value "formats": 9 | // https://xlsxwriter.readthedocs.io/format.html#format-set-num-format 10 | // 11 | export default function isDateTimestamp(styleId, styles, options) { 12 | if (styleId) { 13 | const style = styles[styleId] 14 | if (!style) { 15 | throw new Error(`Cell style not found: ${styleId}`) 16 | } 17 | if (!style.numberFormat) { 18 | return false 19 | } 20 | if ( 21 | // Whether it's a "number format" that's conventionally used for storing date timestamps. 22 | BUILT_IN_DATE_NUMBER_FORMAT_IDS.indexOf(Number(style.numberFormat.id)) >= 0 || 23 | // Whether it's a "number format" that uses a "formatting template" 24 | // that the developer is certain is a date formatting template. 25 | (options.dateFormat && style.numberFormat.template === options.dateFormat) || 26 | // Whether the "smart formatting template" feature is not disabled 27 | // and it has detected that it's a date formatting template by looking at it. 28 | (options.smartDateParser !== false && style.numberFormat.template && isDateTemplate(style.numberFormat.template)) 29 | ) { 30 | return true 31 | } 32 | } 33 | } 34 | 35 | // https://hexdocs.pm/xlsxir/number_styles.html 36 | const BUILT_IN_DATE_NUMBER_FORMAT_IDS = [14,15,16,17,18,19,20,21,22,27,30,36,45,46,47,50,57] 37 | 38 | // On some date formats, there's an "[$-414]" prefix. 39 | // I don't have any idea what that is. 40 | // 41 | // https://stackoverflow.com/questions/4730152/what-indicates-an-office-open-xml-cell-contains-a-date-time-value 42 | // 43 | // Examples: 44 | // 45 | // * 27 (built-in format) "[$-404]e/m/d" 46 | // * 164 (custom format) "[$-414]mmmm\ yyyy;@" 47 | // 48 | const DATE_FORMAT_WEIRD_PREFIX = /^\[\$-414\]/ 49 | 50 | // On some date formats, there's an ";@" postfix. 51 | // I don't have any idea what that is. 52 | // Examples: 53 | // 54 | // * 164 (custom format) "m/d/yyyy;@" 55 | // * 164 (custom format) "[$-414]mmmm\ yyyy;@" 56 | // 57 | const DATE_FORMAT_WEIRD_POSTFIX = /;@$/ 58 | 59 | function isDateTemplate(template) { 60 | // Date format tokens could be in upper case or in lower case. 61 | // There seems to be no single standard. 62 | // So lowercase the template first. 63 | template = template.toLowerCase() 64 | 65 | // On some date formats, there's an "[$-414]" prefix. 66 | // I don't have any idea what that is. Trim it. 67 | template = template.replace(DATE_FORMAT_WEIRD_PREFIX, '') 68 | 69 | // On some date formats, there's an ";@" postfix. 70 | // I don't have any idea what that is. Trim it. 71 | template = template.replace(DATE_FORMAT_WEIRD_POSTFIX, '') 72 | 73 | const tokens = template.split(/\W+/) 74 | for (const token of tokens) { 75 | if (DATE_TEMPLATE_TOKENS.indexOf(token) < 0) { 76 | return false 77 | } 78 | } 79 | return true 80 | } 81 | 82 | // These tokens could be in upper case or in lower case. 83 | // There seems to be no single standard, so using lower case. 84 | const DATE_TEMPLATE_TOKENS = [ 85 | // Seconds (min two digits). Example: "05". 86 | 'ss', 87 | // Minutes (min two digits). Example: "05". Could also be "Months". Weird. 88 | 'mm', 89 | // Hours. Example: "1". 90 | 'h', 91 | // Hours (min two digits). Example: "01". 92 | 'hh', 93 | // "AM" part of "AM/PM". Lowercased just in case. 94 | 'am', 95 | // "PM" part of "AM/PM". Lowercased just in case. 96 | 'pm', 97 | // Day. Example: "1" 98 | 'd', 99 | // Day (min two digits). Example: "01" 100 | 'dd', 101 | // Month (numeric). Example: "1". 102 | 'm', 103 | // Month (numeric, min two digits). Example: "01". Could also be "Minutes". Weird. 104 | 'mm', 105 | // Month (shortened month name). Example: "Jan". 106 | 'mmm', 107 | // Month (full month name). Example: "January". 108 | 'mmmm', 109 | // Two-digit year. Example: "20". 110 | 'yy', 111 | // Full year. Example: "2020". 112 | 'yyyy', 113 | 114 | // I don't have any idea what "e" means. 115 | // It's used in "built-in" XLSX formats: 116 | // * 27 '[$-404]e/m/d'; 117 | // * 36 '[$-404]e/m/d'; 118 | // * 50 '[$-404]e/m/d'; 119 | // * 57 '[$-404]e/m/d'; 120 | 'e' 121 | ]; -------------------------------------------------------------------------------- /source/read/parseCell.js: -------------------------------------------------------------------------------- 1 | import parseCellValue from './parseCellValue.js' 2 | 3 | import { 4 | parseCellCoordinates 5 | } from './coordinates.js' 6 | 7 | import { 8 | getCellValue, 9 | getCellInlineStringValue 10 | } from '../xml/xlsx.js' 11 | 12 | import { 13 | getOuterXml 14 | } from '../xml/dom.js' 15 | 16 | // Example of a ``ell element: 17 | // 18 | // 19 | // string — formula. 20 | // string — formula pre-computed value. 21 | // 22 | // string — an `inlineStr` string (rather than a "common string" from a dictionary). 23 | // 24 | // 25 | // ... 26 | // 27 | // string 28 | // 29 | // 30 | // string 31 | // 32 | // 33 | // 34 | // 35 | // 36 | // 37 | // 38 | // 39 | // 40 | // 41 | export default function parseCell(node, sheet, xml, values, styles, properties, options) { 42 | const coords = parseCellCoordinates(node.getAttribute('r')) 43 | 44 | const valueElement = getCellValue(sheet, node) 45 | 46 | // For `xpath`, `value` can be `undefined` while for native `DOMParser` it's `null`. 47 | // So using `value && ...` instead of `if (value !== undefined) { ... }` here 48 | // for uniform compatibility with both `xpath` and native `DOMParser`. 49 | let value = valueElement && valueElement.textContent 50 | 51 | let type 52 | if (node.hasAttribute('t')) { 53 | type = node.getAttribute('t') 54 | } 55 | 56 | return { 57 | row: coords[0], 58 | column: coords[1], 59 | value: parseCellValue(value, type, { 60 | getInlineStringValue: () => getCellInlineStringValue(sheet, node), 61 | getInlineStringXml: () => getOuterXml(node), 62 | getStyleId: () => node.getAttribute('s'), 63 | styles, 64 | values, 65 | properties, 66 | options 67 | }) 68 | } 69 | } -------------------------------------------------------------------------------- /source/read/parseCellValue.js: -------------------------------------------------------------------------------- 1 | import parseDate from './parseDate.js' 2 | import isDateTimestamp from './isDateTimestamp.js' 3 | 4 | // Parses a string `value` of a cell. 5 | export default function parseCellValue(value, type, { 6 | getInlineStringValue, 7 | getInlineStringXml, 8 | getStyleId, 9 | styles, 10 | values, 11 | properties, 12 | options 13 | }) { 14 | if (!type) { 15 | // Default cell type is "n" (numeric). 16 | // http://www.datypic.com/sc/ooxml/t-ssml_CT_Cell.html 17 | type = 'n' 18 | } 19 | 20 | // Available Excel cell types: 21 | // https://github.com/SheetJS/sheetjs/blob/19620da30be2a7d7b9801938a0b9b1fd3c4c4b00/docbits/52_datatype.md 22 | // 23 | // Some other document (seems to be old): 24 | // http://webapp.docx4java.org/OnlineDemo/ecma376/SpreadsheetML/ST_CellType.html 25 | // 26 | switch (type) { 27 | // XLSX tends to store all strings as "shared" (indexed) ones 28 | // using "s" cell type (for saving on strage space). 29 | // "str" cell type is then generally only used for storing 30 | // formula-pre-calculated cell values. 31 | case 'str': 32 | value = parseString(value, options) 33 | break 34 | 35 | // Sometimes, XLSX stores strings as "inline" strings rather than "shared" (indexed) ones. 36 | // Perhaps the specification doesn't force it to use one or another. 37 | // Example: `Test 123`. 38 | case 'inlineStr': 39 | value = getInlineStringValue() 40 | if (value === undefined) { 41 | throw new Error(`Unsupported "inline string" cell value structure: ${getInlineStringXml()}`) 42 | } 43 | value = parseString(value, options) 44 | break 45 | 46 | // XLSX tends to store string values as "shared" (indexed) ones. 47 | // "Shared" strings is a way for an Excel editor to reduce 48 | // the file size by storing "commonly used" strings in a dictionary 49 | // and then referring to such strings by their index in that dictionary. 50 | // Example: `0`. 51 | case 's': 52 | // If a cell has no value then there's no `` element for it. 53 | // If a `` element exists then it's not empty. 54 | // The ``alue is a key in the "shared strings" dictionary of the 55 | // XLSX file, so look it up in the `values` dictionary by the numeric key. 56 | const sharedStringIndex = Number(value) 57 | if (isNaN(sharedStringIndex)) { 58 | throw new Error(`Invalid "shared" string index: ${value}`) 59 | } 60 | if (sharedStringIndex >= values.length) { 61 | throw new Error(`An out-of-bounds "shared" string index: ${value}`) 62 | } 63 | value = values[sharedStringIndex] 64 | value = parseString(value, options) 65 | break 66 | 67 | // Boolean (TRUE/FALSE) values are stored as either "1" or "0" 68 | // in cells of type "b". 69 | case 'b': 70 | if (value === '1') { 71 | value = true 72 | } else if (value === '0') { 73 | value = false 74 | } else { 75 | throw new Error(`Unsupported "boolean" cell value: ${value}`) 76 | } 77 | break 78 | 79 | // XLSX specification seems to support cells of type "z": 80 | // blank "stub" cells that should be ignored by data processing utilities. 81 | case 'z': 82 | value = undefined 83 | break 84 | 85 | // XLSX specification also defines cells of type "e" containing a numeric "error" code. 86 | // It's not clear what that means though. 87 | // They also wrote: "and `w` property stores its common name". 88 | // It's unclear what they meant by that. 89 | case 'e': 90 | value = decodeError(value) 91 | break 92 | 93 | // XLSX supports date cells of type "d", though seems like it (almost?) never 94 | // uses it for storing dates, preferring "n" numeric timestamp cells instead. 95 | // The value of a "d" cell is supposedly a string in "ISO 8601" format. 96 | // I haven't seen an XLSX file having such cells. 97 | // Example: `2021-06-10T00:47:45.700Z`. 98 | case 'd': 99 | if (value === undefined) { 100 | break 101 | } 102 | const parsedDate = new Date(value) 103 | if (isNaN(parsedDate.valueOf())) { 104 | throw new Error(`Unsupported "date" cell value: ${value}`) 105 | } 106 | value = parsedDate 107 | break 108 | 109 | // Numeric cells have type "n". 110 | case 'n': 111 | if (value === undefined) { 112 | break 113 | } 114 | const isDateTimestampNumber = isDateTimestamp(getStyleId(), styles, options) 115 | // XLSX does have "d" type for dates, but it's not commonly used. 116 | // Instead, it prefers using "n" type for storing dates as timestamps. 117 | if (isDateTimestampNumber) { 118 | // Parse the number from string. 119 | value = parseNumberDefault(value) 120 | // Parse the number as a date timestamp. 121 | value = parseDate(value, properties) 122 | } else { 123 | // Parse the number from string. 124 | // Supports custom parsing function to work around javascript number encoding precision issues. 125 | // https://gitlab.com/catamphetamine/read-excel-file/-/issues/85 126 | value = (options.parseNumber || parseNumberDefault)(value) 127 | } 128 | break 129 | 130 | default: 131 | throw new TypeError(`Cell type not supported: ${type}`) 132 | } 133 | 134 | // Convert empty values to `null`. 135 | if (value === undefined) { 136 | value = null 137 | } 138 | 139 | return value 140 | } 141 | 142 | // Decodes numeric error code to a string code. 143 | // https://github.com/SheetJS/sheetjs/blob/19620da30be2a7d7b9801938a0b9b1fd3c4c4b00/docbits/52_datatype.md 144 | function decodeError(errorCode) { 145 | // While the error values are determined by the application, 146 | // the following are some example error values that could be used: 147 | switch (errorCode) { 148 | case 0x00: 149 | return '#NULL!' 150 | case 0x07: 151 | return '#DIV/0!' 152 | case 0x0F: 153 | return '#VALUE!' 154 | case 0x17: 155 | return '#REF!' 156 | case 0x1D: 157 | return '#NAME?' 158 | case 0x24: 159 | return '#NUM!' 160 | case 0x2A: 161 | return '#N/A' 162 | case 0x2B: 163 | return '#GETTING_DATA' 164 | default: 165 | // Such error code doesn't exist. I made it up. 166 | return `#ERROR_${errorCode}` 167 | } 168 | } 169 | 170 | function parseString(value, options) { 171 | // In some weird cases, a developer might want to disable 172 | // the automatic trimming of all strings. 173 | // For example, leading spaces might express a tree-like hierarchy. 174 | // https://github.com/catamphetamine/read-excel-file/pull/106#issuecomment-1136062917 175 | if (options.trim !== false) { 176 | value = value.trim() 177 | } 178 | if (value === '') { 179 | value = undefined 180 | } 181 | return value 182 | } 183 | 184 | // Parses a number from string. 185 | // Throws an error if the number couldn't be parsed. 186 | // When parsing floating-point number, is affected by 187 | // the javascript number encoding precision issues: 188 | // https://www.youtube.com/watch?v=2gIxbTn7GSc 189 | // https://www.avioconsulting.com/blog/overcoming-javascript-numeric-precision-issues 190 | function parseNumberDefault(stringifiedNumber) { 191 | const parsedNumber = Number(stringifiedNumber) 192 | if (isNaN(parsedNumber)) { 193 | throw new Error(`Invalid "numeric" cell value: ${stringifiedNumber}`) 194 | } 195 | return parsedNumber 196 | } -------------------------------------------------------------------------------- /source/read/parseCells.js: -------------------------------------------------------------------------------- 1 | import parseCell from './parseCell.js' 2 | 3 | import { 4 | getCells, 5 | getMergedCells 6 | } from '../xml/xlsx.js' 7 | 8 | export default function parseCells(sheet, xml, values, styles, properties, options) { 9 | const cells = getCells(sheet) 10 | 11 | if (cells.length === 0) { 12 | return [] 13 | } 14 | 15 | // const mergedCells = getMergedCells(sheet) 16 | // for (const mergedCell of mergedCells) { 17 | // const [from, to] = mergedCell.split(':').map(parseCellCoordinates) 18 | // console.log('Merged Cell.', 'From:', from, 'To:', to) 19 | // } 20 | 21 | return cells.map((node) => { 22 | return parseCell(node, sheet, xml, values, styles, properties, options) 23 | }) 24 | } -------------------------------------------------------------------------------- /source/read/parseDate.js: -------------------------------------------------------------------------------- 1 | // Parses an Excel Date ("serial") into a corresponding javascript Date in UTC+0 timezone. 2 | // (with time equal to 00:00) 3 | // 4 | // https://www.pcworld.com/article/3063622/software/mastering-excel-date-time-serial-numbers-networkdays-datevalue-and-more.html 5 | // "If you need to calculate dates in your spreadsheets, 6 | // Excel uses its own unique system, which it calls Serial Numbers". 7 | // 8 | export default function parseExcelDate(excelSerialDate, options) { 9 | // https://support.microsoft.com/en-gb/help/214330/differences-between-the-1900-and-the-1904-date-system-in-excel 10 | if (options && options.epoch1904) { 11 | excelSerialDate += 1462 12 | } 13 | 14 | // "Excel serial date" is just 15 | // the count of days since `01/01/1900` 16 | // (seems that it may be even fractional). 17 | // 18 | // The count of days elapsed 19 | // since `01/01/1900` (Excel epoch) 20 | // till `01/01/1970` (Unix epoch). 21 | // Accounts for leap years 22 | // (19 of them, yielding 19 extra days). 23 | const daysBeforeUnixEpoch = 70 * 365 + 19 24 | 25 | // An hour, approximately, because a minute 26 | // may be longer than 60 seconds, due to "leap seconds". 27 | // 28 | // Still, Javascript `Date` (and UNIX time in general) intentionally 29 | // drops the concept of "leap seconds" in order to make things simpler. 30 | // So it's fine. 31 | // https://stackoverflow.com/questions/53019726/where-are-the-leap-seconds-in-javascript 32 | // 33 | // "The JavaScript Date object specifically adheres to the concept of Unix Time 34 | // (albeit with higher precision). This is part of the POSIX specification, 35 | // and thus is sometimes called "POSIX Time". It does not count leap seconds, 36 | // but rather assumes every day had exactly 86,400 seconds. You can read about 37 | // this in section 20.3.1.1 of the current ECMAScript specification, which states: 38 | // 39 | // "Time is measured in ECMAScript in milliseconds since 01 January, 1970 UTC. 40 | // In time values leap seconds are ignored. It is assumed that there are exactly 41 | // 86,400,000 milliseconds per day." 42 | // 43 | // The fact is, that the unpredictable nature of leap seconds makes them very 44 | // difficult to work with in APIs. One can't generally pass timestamps around 45 | // that need leap seconds tables to be interpreted correctly, and expect that 46 | // one system will interpret them the same as another. For example, while your 47 | // example timestamp 1483228826 is 2017-01-01T00:00:00Z on your system, 48 | // it would be interpreted as 2017-01-01T00:00:26Z on POSIX based systems, 49 | // or systems without leap second tables. So they aren't portable. 50 | // Even on systems that have full updated tables, there's no telling what those 51 | // tables will contain in the future (beyond the 6-month IERS announcement period), 52 | // so I can't produce a future timestamp without risk that it may eventually change. 53 | // 54 | // To be clear - to support leap seconds in a programming language, the implementation 55 | // must go out of its way to do so, and must make tradeoffs that are not always acceptable. 56 | // Though there are exceptions, the general position is to not support them - not because 57 | // of any subversion or active countermeasures, but because supporting them properly is much, 58 | // much harder." 59 | // 60 | // https://en.wikipedia.org/wiki/Unix_time#Leap_seconds 61 | // https://en.wikipedia.org/wiki/Leap_year 62 | // https://en.wikipedia.org/wiki/Leap_second 63 | // 64 | const hour = 60 * 60 * 1000 65 | 66 | return new Date(Math.round((excelSerialDate - daysBeforeUnixEpoch) * 24 * hour)) 67 | } -------------------------------------------------------------------------------- /source/read/parseDate.test.js: -------------------------------------------------------------------------------- 1 | import parseDate from './parseDate.js' 2 | 3 | describe('parseDate', () => { 4 | it('should parse Excel "serial" dates', () => { 5 | const date = convertToUTCTimezone(new Date(2018, 3 - 1, 24)) 6 | // Excel stores dates as integers. 7 | // E.g. '24/03/2018' === 43183 8 | parseDate(43183).getTime().should.equal(date.getTime()) 9 | }) 10 | }) 11 | 12 | // Converts timezone to UTC while preserving the same time 13 | function convertToUTCTimezone(date) { 14 | // Doesn't account for leap seconds but I guess that's ok 15 | // given that javascript's own `Date()` does not either. 16 | // https://www.timeanddate.com/time/leap-seconds-background.html 17 | // 18 | // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset 19 | // 20 | return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000) 21 | } 22 | -------------------------------------------------------------------------------- /source/read/parseDimensions.js: -------------------------------------------------------------------------------- 1 | import { 2 | parseCellCoordinates 3 | } from './coordinates.js' 4 | 5 | import { 6 | getDimensions 7 | } from '../xml/xlsx.js' 8 | 9 | // `dimensions` defines the spreadsheet area containing all non-empty cells. 10 | // https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetdimension?view=openxml-2.8.1 11 | export default function parseDimensions(sheet) { 12 | let dimensions = getDimensions(sheet) 13 | if (dimensions) { 14 | dimensions = dimensions.split(':').map(parseCellCoordinates).map(([row, column]) => ({ 15 | row, 16 | column 17 | })) 18 | // Sometimes there can be just a single cell as a spreadsheet's "dimensions". 19 | // For example, the default "dimensions" in Apache POI library is "A1", 20 | // meaning that only the first cell in the spreadsheet is used. 21 | // 22 | // A quote from Apache POI library: 23 | // "Single cell ranges are formatted like single cell references (e.g. 'A1' instead of 'A1:A1')." 24 | // 25 | if (dimensions.length === 1) { 26 | dimensions = [dimensions[0], dimensions[0]] 27 | } 28 | return dimensions 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /source/read/parseFilePaths.js: -------------------------------------------------------------------------------- 1 | import { 2 | getRelationships 3 | } from '../xml/xlsx.js' 4 | 5 | /** 6 | * Returns sheet file paths. 7 | * Seems that the correct place to look for the `sheetId` -> `filename` mapping 8 | * is `xl/_rels/workbook.xml.rels` file. 9 | * https://github.com/tidyverse/readxl/issues/104 10 | * @param {string} content — `xl/_rels/workbook.xml.rels` file contents. 11 | * @param {object} xml 12 | * @return {object} 13 | */ 14 | export default function parseFilePaths(content, xml) { 15 | // Example: 16 | // 17 | // ... 18 | // 22 | // 23 | const document = xml.createDocument(content) 24 | 25 | const filePaths = { 26 | sheets: {}, 27 | sharedStrings: undefined, 28 | styles: undefined 29 | } 30 | 31 | const addFilePathInfo = (relationship) => { 32 | const filePath = relationship.getAttribute('Target') 33 | const fileType = relationship.getAttribute('Type') 34 | switch (fileType) { 35 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles': 36 | filePaths.styles = getFilePath(filePath) 37 | break 38 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings': 39 | filePaths.sharedStrings = getFilePath(filePath) 40 | break 41 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet': 42 | filePaths.sheets[relationship.getAttribute('Id')] = getFilePath(filePath) 43 | break 44 | } 45 | } 46 | 47 | getRelationships(document).forEach(addFilePathInfo) 48 | 49 | // Seems like "sharedStrings.xml" is not required to exist. 50 | // For example, when the spreadsheet doesn't contain any strings. 51 | // https://github.com/catamphetamine/read-excel-file/issues/85 52 | // if (!filePaths.sharedStrings) { 53 | // throw new Error('"sharedStrings.xml" file not found in the *.xlsx file') 54 | // } 55 | 56 | return filePaths 57 | } 58 | 59 | function getFilePath(path) { 60 | // Normally, `path` is a relative path inside the ZIP archive, 61 | // like "worksheets/sheet1.xml", or "sharedStrings.xml", or "styles.xml". 62 | // There has been one weird case when file path was an absolute path, 63 | // like "/xl/worksheets/sheet1.xml" (specifically for sheets): 64 | // https://github.com/catamphetamine/read-excel-file/pull/95 65 | // Other libraries (like `xlsx`) and software (like Google Docs) 66 | // seem to support such absolute file paths, so this library does too. 67 | if (path[0] === '/') { 68 | return path.slice('/'.length) 69 | } 70 | // // Seems like a path could also be a URL. 71 | // // http://officeopenxml.com/anatomyofOOXML-xlsx.php 72 | // if (/^[a-z]+\:\/\//.test(path)) { 73 | // return path 74 | // } 75 | return 'xl/' + path 76 | } -------------------------------------------------------------------------------- /source/read/parseProperties.js: -------------------------------------------------------------------------------- 1 | import { 2 | getWorkbookProperties, 3 | getSheets 4 | } from '../xml/xlsx.js' 5 | 6 | // I guess `xl/workbook.xml` file should always be present inside the *.xlsx archive. 7 | export default function parseProperties(content, xml) { 8 | const book = xml.createDocument(content) 9 | 10 | const properties = {}; 11 | 12 | // Read `` element to detect whether dates are 1900-based or 1904-based. 13 | // https://support.microsoft.com/en-gb/help/214330/differences-between-the-1900-and-the-1904-date-system-in-excel 14 | // http://webapp.docx4java.org/OnlineDemo/ecma376/SpreadsheetML/workbookPr.html 15 | 16 | const workbookProperties = getWorkbookProperties(book) 17 | 18 | if (workbookProperties && workbookProperties.getAttribute('date1904') === '1') { 19 | properties.epoch1904 = true 20 | } 21 | 22 | // Get sheets info (indexes, names, if they're available). 23 | // Example: 24 | // 25 | // 30 | // 31 | // http://www.datypic.com/sc/ooxml/e-ssml_sheet-1.html 32 | 33 | properties.sheets = [] 34 | 35 | const addSheetInfo = (sheet) => { 36 | if (sheet.getAttribute('name')) { 37 | properties.sheets.push({ 38 | id: sheet.getAttribute('sheetId'), 39 | name: sheet.getAttribute('name'), 40 | relationId: sheet.getAttribute('r:id') 41 | }) 42 | } 43 | } 44 | 45 | getSheets(book).forEach(addSheetInfo) 46 | 47 | return properties; 48 | } -------------------------------------------------------------------------------- /source/read/parseSharedStrings.js: -------------------------------------------------------------------------------- 1 | import { 2 | getSharedStrings 3 | } from '../xml/xlsx.js' 4 | 5 | export default function parseSharedStrings(content, xml) { 6 | if (!content) { 7 | return [] 8 | } 9 | return getSharedStrings(xml.createDocument(content)) 10 | } -------------------------------------------------------------------------------- /source/read/parseSheet.js: -------------------------------------------------------------------------------- 1 | import parseCells from './parseCells.js' 2 | import parseDimensions from './parseDimensions.js' 3 | 4 | import { calculateDimensions } from './coordinates.js' 5 | 6 | export default function parseSheet(content, xml, values, styles, properties, options) { 7 | const sheet = xml.createDocument(content) 8 | 9 | const cells = parseCells(sheet, xml, values, styles, properties, options) 10 | 11 | // `dimensions` defines the spreadsheet area containing all non-empty cells. 12 | // https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetdimension?view=openxml-2.8.1 13 | const dimensions = parseDimensions(sheet) || calculateDimensions(cells) 14 | 15 | return { cells, dimensions } 16 | } -------------------------------------------------------------------------------- /source/read/parseStyles.js: -------------------------------------------------------------------------------- 1 | import { 2 | getBaseStyles, 3 | getCellStyles, 4 | getNumberFormats 5 | } from '../xml/xlsx.js' 6 | 7 | // http://officeopenxml.com/SSstyles.php 8 | // Returns an array of cell styles. 9 | // A cell style index is its ID. 10 | export default function parseStyles(content, xml) { 11 | if (!content) { 12 | return {} 13 | } 14 | 15 | // https://social.msdn.microsoft.com/Forums/sqlserver/en-US/708978af-b598-45c4-a598-d3518a5a09f0/howwhen-is-cellstylexfs-vs-cellxfs-applied-to-a-cell?forum=os_binaryfile 16 | // https://www.office-forums.com/threads/cellxfs-cellstylexfs.2163519/ 17 | const doc = xml.createDocument(content) 18 | 19 | const baseStyles = getBaseStyles(doc) 20 | .map(parseCellStyle) 21 | 22 | const numberFormats = getNumberFormats(doc) 23 | .map(parseNumberFormatStyle) 24 | .reduce((formats, format) => { 25 | // Format ID is a numeric index. 26 | // There're some standard "built-in" formats (in Excel) up to about `100`. 27 | formats[format.id] = format 28 | return formats 29 | }, []) 30 | 31 | const getCellStyle = (xf) => { 32 | if (xf.hasAttribute('xfId')) { 33 | return { 34 | ...baseStyles[xf.xfId], 35 | ...parseCellStyle(xf, numberFormats) 36 | } 37 | } 38 | return parseCellStyle(xf, numberFormats) 39 | } 40 | 41 | return getCellStyles(doc).map(getCellStyle) 42 | } 43 | 44 | function parseNumberFormatStyle(numFmt) { 45 | return { 46 | id: numFmt.getAttribute('numFmtId'), 47 | template: numFmt.getAttribute('formatCode') 48 | } 49 | } 50 | 51 | // http://www.datypic.com/sc/ooxml/e-ssml_xf-2.html 52 | function parseCellStyle(xf, numFmts) { 53 | const style = {} 54 | if (xf.hasAttribute('numFmtId')) { 55 | const numberFormatId = xf.getAttribute('numFmtId') 56 | // Built-in number formats don't have a `` element in `styles.xml`. 57 | // https://hexdocs.pm/xlsxir/number_styles.html 58 | if (numFmts[numberFormatId]) { 59 | style.numberFormat = numFmts[numberFormatId] 60 | } else { 61 | style.numberFormat = { id: numberFormatId } 62 | } 63 | } 64 | return style 65 | } -------------------------------------------------------------------------------- /source/read/readSheetNamesBrowser.js: -------------------------------------------------------------------------------- 1 | import readXlsxFile from './readXlsxFileBrowser.js' 2 | 3 | /** 4 | * Reads the list of sheet names in an XLSX file in a web browser. 5 | * @param {file} file - A file being uploaded in the browser. 6 | * @return {Promise} Resolves to an array of objects of shape `{ name: string }`. 7 | */ 8 | export default function readSheetNames(file) { 9 | return readXlsxFile(file, { getSheets: true }) 10 | .then(sheets => sheets.map(sheet => sheet.name)) 11 | } -------------------------------------------------------------------------------- /source/read/readSheetNamesNode.js: -------------------------------------------------------------------------------- 1 | import readXlsxFile from './readXlsxFileNode.js' 2 | 3 | /** 4 | * Reads the list of sheet names in an XLSX file in Node.js. 5 | * @param {(string|Stream|Buffer)} input - A Node.js readable stream or a `Buffer` or a path to a file. 6 | * @return {Promise} Resolves to an array of objects of shape `{ name: string }`. 7 | */ 8 | export default function readSheetNames(input) { 9 | return readXlsxFile(input, { getSheets: true }) 10 | .then(sheets => sheets.map(sheet => sheet.name)) 11 | } -------------------------------------------------------------------------------- /source/read/readSheetNamesNode.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import readSheetNamesNode from './readSheetNamesNode.js' 4 | 5 | describe('readSheetNamesNode', () => { 6 | it('should read the list of sheet names in an *.xlsx file in Node.js', () => { 7 | return readSheetNamesNode(path.resolve('./test/spreadsheets/multiple-sheets.xlsx')).then((sheetNames) => { 8 | sheetNames.should.deep.equal(['sheet 1', 'sheet 2']) 9 | }) 10 | }) 11 | }) -------------------------------------------------------------------------------- /source/read/readSheetNamesWebWorker.js: -------------------------------------------------------------------------------- 1 | import readXlsxFile from './readXlsxFileWebWorker.js' 2 | 3 | /** 4 | * Reads the list of sheet names in an XLSX file in a Web Worker. 5 | * @param {file} file - The file. 6 | * @return {Promise} Resolves to an array of objects of shape `{ name: string }`. 7 | */ 8 | export default function readSheetNames(file) { 9 | return readXlsxFile(file, { getSheets: true }) 10 | .then(sheets => sheets.map(sheet => sheet.name)) 11 | } -------------------------------------------------------------------------------- /source/read/readXlsx.js: -------------------------------------------------------------------------------- 1 | import parseProperties from './parseProperties.js' 2 | import parseFilePaths from './parseFilePaths.js' 3 | import parseStyles from './parseStyles.js' 4 | import parseSharedStrings from './parseSharedStrings.js' 5 | import parseSheet from './parseSheet.js' 6 | import getData from './getData.js' 7 | 8 | // For an introduction in reading `*.xlsx` files see "The minimum viable XLSX reader": 9 | // https://www.brendanlong.com/the-minimum-viable-xlsx-reader.html 10 | 11 | /** 12 | * Reads an (unzipped) XLSX file structure into a 2D array of cells. 13 | * @param {object} contents - A list of XML files inside XLSX file (which is a zipped directory). 14 | * @param {number?} options.sheet - Workbook sheet id (`1` by default). 15 | * @param {string?} options.dateFormat - Date format, e.g. "mm/dd/yyyy". Values having this format template set will be parsed as dates. 16 | * @param {object} contents - A list of XML files inside XLSX file (which is a zipped directory). 17 | * @return {object} An object of shape `{ data, cells, properties }`. `data: string[][]` is an array of rows, each row being an array of cell values. `cells: string[][]` is an array of rows, each row being an array of cells. `properties: object` is the spreadsheet properties (e.g. whether date epoch is 1904 instead of 1900). 18 | */ 19 | export default function readXlsx(contents, xml, options = {}) { 20 | if (!options.sheet) { 21 | options = { 22 | sheet: 1, 23 | ...options 24 | } 25 | } 26 | 27 | const getXmlFileContent = (filePath) => { 28 | if (!contents[filePath]) { 29 | throw new Error(`"${filePath}" file not found inside the *.xlsx file zip archive`) 30 | } 31 | return contents[filePath] 32 | } 33 | 34 | // Some Excel editors don't want to use standard naming scheme for sheet files. 35 | // https://github.com/tidyverse/readxl/issues/104 36 | const filePaths = parseFilePaths(getXmlFileContent('xl/_rels/workbook.xml.rels'), xml) 37 | 38 | // Default file path for "shared strings": "xl/sharedStrings.xml". 39 | const values = filePaths.sharedStrings 40 | ? parseSharedStrings(getXmlFileContent(filePaths.sharedStrings), xml) 41 | : [] 42 | 43 | // Default file path for "styles": "xl/styles.xml". 44 | const styles = filePaths.styles 45 | ? parseStyles(getXmlFileContent(filePaths.styles), xml) 46 | : {} 47 | 48 | const properties = parseProperties(getXmlFileContent('xl/workbook.xml'), xml) 49 | 50 | // A feature for getting the list of sheets in an Excel file. 51 | // https://github.com/catamphetamine/read-excel-file/issues/14 52 | if (options.getSheets) { 53 | return properties.sheets.map(({ name }) => ({ 54 | name 55 | })) 56 | } 57 | 58 | // Find the sheet by name, or take the first one. 59 | const sheetId = getSheetId(options.sheet, properties.sheets) 60 | 61 | // If the sheet wasn't found then throw an error. 62 | // Example: "xl/worksheets/sheet1.xml". 63 | if (!sheetId || !filePaths.sheets[sheetId]) { 64 | throw createSheetNotFoundError(options.sheet, properties.sheets) 65 | } 66 | 67 | // Parse sheet data. 68 | const sheet = parseSheet( 69 | getXmlFileContent(filePaths.sheets[sheetId]), 70 | xml, 71 | values, 72 | styles, 73 | properties, 74 | options 75 | ) 76 | 77 | options = { 78 | // Create a `rowIndexMap` for the original dataset, if not passed, 79 | // because "empty" rows will be dropped from the input data. 80 | rowMap: [], 81 | ...options 82 | } 83 | 84 | // Get spreadsheet data. 85 | const data = getData(sheet, options) 86 | 87 | // Can return properties, if required. 88 | if (options.properties) { 89 | return { 90 | data, 91 | properties 92 | } 93 | } 94 | 95 | // Return spreadsheet data. 96 | return data 97 | } 98 | 99 | function getSheetId(sheet, sheets) { 100 | if (typeof sheet === 'number') { 101 | const _sheet = sheets[sheet - 1] 102 | return _sheet && _sheet.relationId 103 | } 104 | for (const _sheet of sheets) { 105 | if (_sheet.name === sheet) { 106 | return _sheet.relationId 107 | } 108 | } 109 | } 110 | 111 | function createSheetNotFoundError(sheet, sheets) { 112 | const sheetsList = sheets && sheets.map((sheet, i) => `"${sheet.name}" (#${i + 1})`).join(', ') 113 | return new Error(`Sheet ${typeof sheet === 'number' ? '#' + sheet : '"' + sheet + '"'} not found in the *.xlsx file.${sheets ? ' Available sheets: ' + sheetsList + '.' : ''}`) 114 | } -------------------------------------------------------------------------------- /source/read/readXlsxFileBrowser.js: -------------------------------------------------------------------------------- 1 | import xml from '../xml/xmlBrowser.js' 2 | 3 | import unpackXlsxFile from './unpackXlsxFileBrowser.js' 4 | import readXlsxFileContents from './readXlsxFileContents.js' 5 | 6 | /** 7 | * Reads XLSX file into a 2D array of cells in a browser. 8 | * @param {file} file - A file being uploaded in the browser. 9 | * @param {object?} options 10 | * @param {(number|string)?} options.sheet - Excel document sheet to read. Defaults to `1`. Will only read this sheet and skip others. 11 | * @return {Promise} Resolves to a 2D array of cells: an array of rows, each row being an array of cells. 12 | */ 13 | export default function readXlsxFile(file, options = {}) { 14 | return unpackXlsxFile(file) 15 | .then((entries) => readXlsxFileContents(entries, xml, options)) 16 | } -------------------------------------------------------------------------------- /source/read/readXlsxFileContents.js: -------------------------------------------------------------------------------- 1 | import readXlsx from './readXlsx.js' 2 | 3 | import mapToObjectsLegacyBehavior from './schema/mapToObjects.legacy.js' 4 | import mapToObjectsSpreadsheetBehavior from './schema/mapToObjects.spreadsheet.js' 5 | 6 | import convertMapToSchema from './schema/convertMapToSchema.js' 7 | 8 | export default function readXlsxFileContents(entries, xml, { schema, map, ...options}) { 9 | if (!schema && map) { 10 | schema = convertMapToSchema(map) 11 | } 12 | // `readXlsx()` adds `options.rowMap`, if not passed. 13 | const result = readXlsx(entries, xml, { ...options, properties: schema || options.properties }) 14 | if (schema) { 15 | return mapToObjectsSpreadsheetBehavior(mapToObjectsLegacyBehavior, result.data, schema, { 16 | ...options, 17 | properties: result.properties 18 | }) 19 | } 20 | return result 21 | } -------------------------------------------------------------------------------- /source/read/readXlsxFileNode.js: -------------------------------------------------------------------------------- 1 | import xml from '../xml/xml.js' 2 | 3 | import unpackXlsxFile from './unpackXlsxFileNode.js' 4 | import readXlsxFileContents from './readXlsxFileContents.js' 5 | 6 | /** 7 | * Reads XLSX file into a 2D array of cells in a browser. 8 | * @param {(string|Stream|Buffer)} input - A Node.js readable stream or a `Buffer` or a path to a file. 9 | * @param {object?} options 10 | * @param {(number|string)?} options.sheet - Excel document sheet to read. Defaults to `1`. Will only read this sheet and skip others. 11 | * @return {Promise} Resolves to a 2D array of cells: an array of rows, each row being an array of cells. 12 | */ 13 | export default function readXlsxFile(input, options = {}) { 14 | return unpackXlsxFile(input) 15 | .then((entries) => readXlsxFileContents(entries, xml, options)) 16 | } -------------------------------------------------------------------------------- /source/read/readXlsxFileNode.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import readXlsxFileNode from './readXlsxFileNode.js' 4 | 5 | describe('readXlsxFileNode', () => { 6 | it('should read *.xlsx file on Node.js and parse it to JSON', () => { 7 | const schema = { 8 | 'START DATE': { 9 | prop: 'date', 10 | type: Date 11 | }, 12 | 'NUMBER OF STUDENTS': { 13 | prop: 'numberOfStudents', 14 | type: Number, 15 | required: true 16 | }, 17 | 'COURSE': { 18 | prop: 'course', 19 | type: { 20 | 'IS FREE': { 21 | prop: 'isFree', 22 | type: Boolean 23 | // Excel stores booleans as numbers: 24 | // `1` is `true` and `0` is `false`. 25 | // Such numbers are parsed into booleans. 26 | }, 27 | 'COST': { 28 | prop: 'cost', 29 | type: Number 30 | }, 31 | 'COURSE TITLE': { 32 | prop: 'title', 33 | type: String 34 | } 35 | } 36 | }, 37 | 'CONTACT': { 38 | prop: 'contact', 39 | required: true, 40 | parse(value) { 41 | return '+11234567890' 42 | } 43 | } 44 | } 45 | 46 | const rowMap = [] 47 | 48 | return readXlsxFileNode(path.resolve('./test/spreadsheets/course.xlsx'), { schema, rowMap }).then(({ rows }) => { 49 | rows[0].date = rows[0].date.getTime() 50 | rows.should.deep.equal([{ 51 | date: convertToUTCTimezone(new Date(2018, 2, 24)).getTime(), 52 | numberOfStudents: 123, 53 | course: { 54 | isFree: false, 55 | cost: 210.45, 56 | title: 'Chemistry' 57 | }, 58 | contact: '+11234567890' 59 | }]) 60 | rowMap.should.deep.equal([0, 1]) 61 | }) 62 | }) 63 | 64 | it('should read *.xlsx file on Node.js and map it to JSON', () => { 65 | const map = { 66 | 'START DATE': 'date', 67 | 'NUMBER OF STUDENTS': 'numberOfStudents', 68 | 'COURSE': { 69 | 'course': { 70 | 'IS FREE': 'isFree', 71 | 'COST': 'cost', 72 | 'COURSE TITLE': 'title' 73 | } 74 | }, 75 | 'CONTACT': 'contact' 76 | } 77 | 78 | const rowMap = [] 79 | 80 | return readXlsxFileNode(path.resolve('./test/spreadsheets/course.xlsx'), { map, rowMap }).then(({ rows, errors }) => { 81 | errors.should.deep.equal([]) 82 | rows[0].date = rows[0].date.getTime() 83 | rows.should.deep.equal([{ 84 | date: convertToUTCTimezone(new Date(2018, 2, 24)).getTime(), 85 | numberOfStudents: 123, 86 | course: { 87 | isFree: false, 88 | cost: 210.45, 89 | title: 'Chemistry' 90 | }, 91 | contact: '(123) 456-7890' 92 | }]) 93 | rowMap.should.deep.equal([0, 1]) 94 | }) 95 | }) 96 | }) 97 | 98 | // Converts timezone to UTC while preserving the same time 99 | function convertToUTCTimezone(date) { 100 | // Doesn't account for leap seconds but I guess that's ok 101 | // given that javascript's own `Date()` does not either. 102 | // https://www.timeanddate.com/time/leap-seconds-background.html 103 | // 104 | // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset 105 | // 106 | return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000) 107 | } 108 | -------------------------------------------------------------------------------- /source/read/readXlsxFileWebWorker.js: -------------------------------------------------------------------------------- 1 | import xml from '../xml/xml.js' 2 | 3 | import unpackXlsxFile from './unpackXlsxFileBrowser.js' 4 | import readXlsxFileContents from './readXlsxFileContents.js' 5 | 6 | /** 7 | * Reads XLSX file into a 2D array of cells in a web worker. 8 | * @param {file} file - The file. 9 | * @param {object?} options 10 | * @param {(number|string)?} options.sheet - Excel document sheet to read. Defaults to `1`. Will only read this sheet and skip others. 11 | * @return {Promise} Resolves to a 2D array of cells: an array of rows, each row being an array of cells. 12 | */ 13 | export default function readXlsxFile(file, options = {}) { 14 | return unpackXlsxFile(file) 15 | .then((entries) => readXlsxFileContents(entries, xml, options)) 16 | } -------------------------------------------------------------------------------- /source/read/schema/convertMapToSchema.js: -------------------------------------------------------------------------------- 1 | export default function convertMapToSchema(map) { 2 | const schema = {} 3 | for (const key of Object.keys(map)) { 4 | let prop = map[key] 5 | let type 6 | if (typeof prop === 'object') { 7 | prop = Object.keys(map[key])[0] 8 | type = convertMapToSchema(map[key][prop]) 9 | } 10 | schema[key] = { 11 | prop 12 | } 13 | if (type) { 14 | schema[key].type = type 15 | } 16 | } 17 | return schema 18 | } -------------------------------------------------------------------------------- /source/read/schema/convertMapToSchema.test.js: -------------------------------------------------------------------------------- 1 | import convertMapToSchema from './convertMapToSchema.js' 2 | 3 | describe('convertMapToSchema', () => { 4 | it('should convert map to schema', () => { 5 | const map = { 6 | 'START DATE': 'date', 7 | 'NUMBER OF STUDENTS': 'numberOfStudents', 8 | 'COURSE': { 9 | 'course': { 10 | 'IS FREE': 'isFree', 11 | 'COURSE TITLE': 'title' 12 | } 13 | }, 14 | 'CONTACT': 'contact', 15 | 'STATUS': 'status' 16 | } 17 | convertMapToSchema(map).should.deep.equal({ 18 | 'START DATE': { 19 | prop: 'date' 20 | }, 21 | 'NUMBER OF STUDENTS': { 22 | prop: 'numberOfStudents' 23 | }, 24 | 'COURSE': { 25 | prop: 'course', 26 | type: { 27 | 'IS FREE': { 28 | prop: 'isFree' 29 | }, 30 | 'COURSE TITLE': { 31 | prop: 'title' 32 | } 33 | } 34 | }, 35 | 'CONTACT': { 36 | prop: 'contact' 37 | }, 38 | 'STATUS': { 39 | prop: 'status' 40 | } 41 | }) 42 | }) 43 | }) -------------------------------------------------------------------------------- /source/read/schema/mapToObjects.js: -------------------------------------------------------------------------------- 1 | import NumberType from '../../types/Number.js' 2 | import StringType from '../../types/String.js' 3 | import BooleanType from '../../types/Boolean.js' 4 | import DateType from '../../types/Date.js' 5 | 6 | const DEFAULT_OPTIONS = { 7 | schemaPropertyValueForMissingColumn: undefined, 8 | schemaPropertyValueForUndefinedCellValue: undefined, 9 | schemaPropertyValueForNullCellValue: null, 10 | schemaPropertyShouldSkipRequiredValidationForMissingColumn: () => false, 11 | // `getEmptyObjectValue(object, { path })` applies to both the top-level object 12 | // and any of its sub-objects. 13 | getEmptyObjectValue: () => null, 14 | getEmptyArrayValue: () => null, 15 | isColumnOriented: false, 16 | arrayValueSeparator: ',' 17 | } 18 | 19 | /** 20 | * (this function is exported from `read-excel-file/map`) 21 | * Converts spreadsheet-alike data structure into an array of objects. 22 | * The first row should be the list of column headers. 23 | * @param {any[][]} data - An array of rows, each row being an array of cells. 24 | * @param {object} schema 25 | * @param {object} [options] 26 | * @param {null} [options.schemaPropertyValueForMissingColumn] — By default, when some of the `schema` columns are missing in the input `data`, those properties are set to `undefined` in the output objects. Pass `schemaPropertyValueForMissingColumn: null` to set such "missing column" properties to `null` in the output objects. 27 | * @param {null} [options.schemaPropertyValueForNullCellValue] — By default, when it encounters a `null` value in a cell in input `data`, it sets it to `undefined` in the output object. Pass `schemaPropertyValueForNullCellValue: null` to make it set such values as `null`s in output objects. 28 | * @param {null} [options.schemaPropertyValueForUndefinedCellValue] — By default, when it encounters an `undefined` value in a cell in input `data`, it it sets it to `undefined` in the output object. Pass `schemaPropertyValueForUndefinedCellValue: null` to make it set such values as `null`s in output objects. 29 | * @param {boolean} [options.schemaPropertyShouldSkipRequiredValidationForMissingColumn(column: string, { object })] — By default, it does apply `required` validation to `schema` properties for which columns are missing in the input `data`. One could pass a custom `schemaPropertyShouldSkipRequiredValidationForMissingColumn(column, { object })` to disable `required` validation for missing columns in some or all cases. 30 | * @param {function} [options.getEmptyObjectValue(object, { path })] — By default, it returns `null` for an "empty" resulting object. One could override that value using `getEmptyObjectValue(object, { path })` parameter. The value applies to both top-level object and any nested sub-objects in case of a nested schema, hence the additional `path?: string` parameter. 31 | * @param {function} [getEmptyArrayValue(array, { path })] — By default, it returns `null` for an "empty" array value. One could override that value using `getEmptyArrayValue(array, { path })` parameter. 32 | * @param {boolean} [options.isColumnOriented] — By default, the headers are assumed to be the first row in the `data`. Pass `isColumnOriented: true` if the headers are the first column in the `data`. i.e. if `data` is "transposed". 33 | * @param {object} [options.rowIndexMap] — Custom row index mapping `data` rows. If present, will overwrite the indexes of `data` rows with the indexes from this `rowIndexMap`. 34 | * @return {object[]} 35 | */ 36 | export default function mapToObjects(data, schema, options) { 37 | if (options) { 38 | options = { 39 | ...DEFAULT_OPTIONS, 40 | ...options 41 | } 42 | } else { 43 | options = DEFAULT_OPTIONS 44 | } 45 | 46 | const { 47 | isColumnOriented, 48 | rowIndexMap 49 | } = options 50 | 51 | validateSchema(schema) 52 | 53 | if (isColumnOriented) { 54 | data = transpose(data) 55 | } 56 | 57 | const columns = data[0] 58 | 59 | const results = [] 60 | const errors = [] 61 | 62 | for (let i = 1; i < data.length; i++) { 63 | const result = read(schema, data[i], i, undefined, columns, errors, options) 64 | results.push(result) 65 | } 66 | 67 | // Set the correct `row` number in `errors` if a custom `rowIndexMap` is supplied. 68 | if (rowIndexMap) { 69 | for (const error of errors) { 70 | // Convert the `row` index in `data` to the 71 | // actual `row` index in the spreadsheet. 72 | // `- 1` converts row number to row index. 73 | // `+ 1` converts row index to row number. 74 | error.row = rowIndexMap[error.row - 1] + 1 75 | } 76 | } 77 | 78 | return { 79 | rows: results, 80 | errors 81 | } 82 | } 83 | 84 | function read(schema, row, rowIndex, path, columns, errors, options) { 85 | const object = {} 86 | let isEmptyObject = true 87 | 88 | const createError = ({ 89 | column, 90 | value, 91 | error: errorMessage, 92 | reason 93 | }) => { 94 | const error = { 95 | error: errorMessage, 96 | row: rowIndex + 1, 97 | column, 98 | value 99 | } 100 | if (reason) { 101 | error.reason = reason 102 | } 103 | if (schema[column].type) { 104 | error.type = schema[column].type 105 | } 106 | return error 107 | } 108 | 109 | const pendingRequiredChecks = [] 110 | 111 | // For each schema entry. 112 | for (const key of Object.keys(schema)) { 113 | const schemaEntry = schema[key] 114 | const isNestedSchema = typeof schemaEntry.type === 'object' && !Array.isArray(schemaEntry.type) 115 | 116 | // The path of this property inside the resulting object. 117 | const propertyPath = `${path || ''}.${schemaEntry.prop}` 118 | 119 | // Read the cell value for the schema entry. 120 | let cellValue 121 | const columnIndex = columns.indexOf(key) 122 | const isMissingColumn = columnIndex < 0 123 | if (!isMissingColumn) { 124 | cellValue = row[columnIndex] 125 | } 126 | 127 | let value 128 | let error 129 | let reason 130 | 131 | // Get property `value` from cell value. 132 | if (isNestedSchema) { 133 | value = read(schemaEntry.type, row, rowIndex, propertyPath, columns, errors, options) 134 | } else { 135 | if (isMissingColumn) { 136 | value = options.schemaPropertyValueForMissingColumn 137 | } 138 | else if (cellValue === undefined) { 139 | value = options.schemaPropertyValueForUndefinedCellValue 140 | } 141 | else if (cellValue === null) { 142 | value = options.schemaPropertyValueForNullCellValue 143 | } 144 | else if (Array.isArray(schemaEntry.type)) { 145 | const array = parseArray(cellValue, options.arrayValueSeparator).map((_value) => { 146 | if (error) { 147 | return 148 | } 149 | const result = parseValue(_value, schemaEntry, options) 150 | if (result.error) { 151 | // In case of an error, `value` won't be returned and will just be reported 152 | // as part of an `error` object, so it's fine assigning just an element of the array. 153 | value = _value 154 | error = result.error 155 | reason = result.reason 156 | } 157 | return result.value 158 | }) 159 | if (!error) { 160 | const isEmpty = array.every(isEmptyValue) 161 | value = isEmpty ? options.getEmptyArrayValue(array, { path: propertyPath }) : array 162 | } 163 | } else { 164 | const result = parseValue(cellValue, schemaEntry, options) 165 | error = result.error 166 | reason = result.reason 167 | value = error ? cellValue : result.value 168 | } 169 | } 170 | 171 | // Apply `required` validation if the value is "empty". 172 | if (!error && isEmptyValue(value)) { 173 | if (schemaEntry.required) { 174 | // Will perform this `required()` validation in the end, 175 | // when all properties of the mapped object have been mapped. 176 | pendingRequiredChecks.push({ column: key, value, isMissingColumn }) 177 | } 178 | } 179 | 180 | if (error) { 181 | // If there was an error then the property value in the `object` will be `undefined`, 182 | // i.e it won't add the property value to the mapped object. 183 | errors.push(createError({ 184 | column: key, 185 | value, 186 | error, 187 | reason 188 | })) 189 | } else { 190 | // Possibly unmark the mapped object as "empty". 191 | if (isEmptyObject && !isEmptyValue(value)) { 192 | isEmptyObject = false 193 | } 194 | // Set the value in the mapped object. 195 | // Skip setting `undefined` values because they're already `undefined`. 196 | if (value !== undefined) { 197 | object[schemaEntry.prop] = value 198 | } 199 | } 200 | } 201 | 202 | // Return `null` for an "empty" mapped object. 203 | if (isEmptyObject) { 204 | return options.getEmptyObjectValue(object, { path }) 205 | } 206 | 207 | // Perform any `required` validations. 208 | for (const { column, value, isMissingColumn } of pendingRequiredChecks) { 209 | // Can optionally skip `required` validation for missing columns. 210 | const skipRequiredValidation = isMissingColumn && options.schemaPropertyShouldSkipRequiredValidationForMissingColumn(column, { object }) 211 | if (!skipRequiredValidation) { 212 | const { required } = schema[column] 213 | const isRequired = typeof required === 'boolean' ? required : required(object) 214 | if (isRequired) { 215 | errors.push(createError({ 216 | column, 217 | value, 218 | error: 'required' 219 | })) 220 | } 221 | } 222 | } 223 | 224 | // Return the mapped object. 225 | return object 226 | } 227 | 228 | /** 229 | * Converts textual value to a javascript typed value. 230 | * @param {any} value 231 | * @param {object} schemaEntry 232 | * @return {{ value: any, error: string }} 233 | */ 234 | export function parseValue(value, schemaEntry, options) { 235 | if (value === null) { 236 | return { value: null } 237 | } 238 | let result 239 | if (schemaEntry.parse) { 240 | result = parseCustomValue(value, schemaEntry.parse) 241 | } else if (schemaEntry.type) { 242 | result = parseValueOfType( 243 | value, 244 | // Supports parsing array types. 245 | // See `parseArray()` function for more details. 246 | // Example `type`: String[] 247 | // Input: 'Barack Obama, "String, with, colons", Donald Trump' 248 | // Output: ['Barack Obama', 'String, with, colons', 'Donald Trump'] 249 | Array.isArray(schemaEntry.type) ? schemaEntry.type[0] : schemaEntry.type, 250 | options 251 | ) 252 | } else { 253 | result = { value: value } 254 | // throw new Error('Invalid schema entry: no .type and no .parse():\n\n' + JSON.stringify(schemaEntry, null, 2)) 255 | } 256 | // If errored then return the error. 257 | if (result.error) { 258 | return result 259 | } 260 | if (result.value !== null) { 261 | if (schemaEntry.oneOf && schemaEntry.oneOf.indexOf(result.value) < 0) { 262 | return { error: 'invalid', reason: 'unknown' } 263 | } 264 | if (schemaEntry.validate) { 265 | try { 266 | schemaEntry.validate(result.value) 267 | } catch (error) { 268 | return { error: error.message } 269 | } 270 | } 271 | } 272 | return result 273 | } 274 | 275 | /** 276 | * Converts textual value to a custom value using supplied `.parse()`. 277 | * @param {any} value 278 | * @param {function} parse 279 | * @return {{ value: any, error: string }} 280 | */ 281 | function parseCustomValue(value, parse) { 282 | try { 283 | const parsedValue = parse(value) 284 | if (parsedValue === undefined) { 285 | return { value: null } 286 | } 287 | return { value: parsedValue } 288 | } catch (error) { 289 | const result = { error: error.message } 290 | if (error.reason) { 291 | result.reason = error.reason; 292 | } 293 | return result 294 | } 295 | } 296 | 297 | /** 298 | * Converts textual value to a javascript typed value. 299 | * @param {any} value 300 | * @param {} type 301 | * @return {{ value: (string|number|Date|boolean), error: string, reason?: string }} 302 | */ 303 | function parseValueOfType(value, type, options) { 304 | switch (type) { 305 | case String: 306 | return parseCustomValue(value, StringType) 307 | 308 | case Number: 309 | return parseCustomValue(value, NumberType) 310 | 311 | case Date: 312 | return parseCustomValue(value, (value) => DateType(value, { properties: options.properties })) 313 | 314 | case Boolean: 315 | return parseCustomValue(value, BooleanType) 316 | 317 | default: 318 | if (typeof type === 'function') { 319 | return parseCustomValue(value, type) 320 | } 321 | throw new Error(`Unsupported schema type: ${type && type.name || type}`) 322 | } 323 | } 324 | 325 | export function getBlock(string, endCharacter, startIndex) { 326 | let i = 0 327 | let substring = '' 328 | let character 329 | while (startIndex + i < string.length) { 330 | const character = string[startIndex + i] 331 | if (character === endCharacter) { 332 | return [substring, i] 333 | } 334 | else if (character === '"') { 335 | const block = getBlock(string, '"', startIndex + i + 1) 336 | substring += block[0] 337 | i += '"'.length + block[1] + '"'.length 338 | } 339 | else { 340 | substring += character 341 | i++ 342 | } 343 | } 344 | return [substring, i] 345 | } 346 | 347 | /** 348 | * Parses a string of comma-separated substrings into an array of substrings. 349 | * (the `export` is just for tests) 350 | * @param {string} string — A string of comma-separated substrings. 351 | * @return {string[]} An array of substrings. 352 | */ 353 | export function parseArray(string, arrayValueSeparator) { 354 | const blocks = [] 355 | let index = 0 356 | while (index < string.length) { 357 | const [substring, length] = getBlock(string, arrayValueSeparator, index) 358 | index += length + arrayValueSeparator.length 359 | blocks.push(substring.trim()) 360 | } 361 | return blocks 362 | } 363 | 364 | // Transpose a 2D array. 365 | // https://stackoverflow.com/questions/17428587/transposing-a-2d-array-in-javascript 366 | const transpose = array => array[0].map((_, i) => array.map(row => row[i])) 367 | 368 | function validateSchema(schema) { 369 | for (const key of Object.keys(schema)) { 370 | const entry = schema[key] 371 | if (!entry.prop) { 372 | throw new Error(`"prop" not defined for schema entry "${key}".`) 373 | } 374 | } 375 | } 376 | 377 | function isEmptyValue(value) { 378 | return value === undefined || value === null 379 | } -------------------------------------------------------------------------------- /source/read/schema/mapToObjects.legacy.js: -------------------------------------------------------------------------------- 1 | import mapToObjects from './mapToObjects.js' 2 | 3 | export default function mapToObjectsLegacyBehavior(data, schema, options = {}) { 4 | const { 5 | includeNullValues, 6 | ignoreEmptyRows, 7 | isColumnOriented, 8 | rowMap 9 | } = options 10 | const defaultConversionOptions = { 11 | schemaPropertyValueForMissingColumn: undefined, 12 | schemaPropertyValueForUndefinedCellValue: undefined, 13 | schemaPropertyValueForNullCellValue: undefined, 14 | schemaPropertyShouldSkipRequiredValidationForMissingColumn: (column, { path }) => false, 15 | getEmptyObjectValue: (object, { path }) => path ? undefined : null, 16 | getEmptyArrayValue: () => null, 17 | arrayValueSeparator: ',' 18 | } 19 | if (includeNullValues) { 20 | defaultConversionOptions.schemaPropertyValueForMissingColumn = null 21 | defaultConversionOptions.schemaPropertyValueForUndefinedCellValue = null 22 | defaultConversionOptions.schemaPropertyValueForNullCellValue = null 23 | defaultConversionOptions.getEmptyObjectValue = (object, { path }) => null 24 | } 25 | const result = mapToObjects(data, schema, { 26 | ...defaultConversionOptions, 27 | rowIndexMap: rowMap, 28 | isColumnOriented 29 | }) 30 | if (ignoreEmptyRows !== false) { 31 | result.rows = result.rows.filter(_ => _ !== defaultConversionOptions.getEmptyObjectValue(_, { path: undefined })) 32 | } 33 | return result 34 | } -------------------------------------------------------------------------------- /source/read/schema/mapToObjects.legacy.test.js: -------------------------------------------------------------------------------- 1 | import mapToObjects from './mapToObjects.legacy.js' 2 | 3 | import Integer from '../../types/Integer.js' 4 | 5 | describe('mapToObjects (legacy behavior)', () => { 6 | it('should include `null` values when `includeNullValues: true` option is passed', function() { 7 | const { rows } = mapToObjects( 8 | [ 9 | ['A', 'B', 'CA', 'CB'], 10 | ['a', 'b', 'ca', null], 11 | ['a', null] 12 | ], 13 | { 14 | A: { 15 | prop: 'a', 16 | type: String 17 | }, 18 | B: { 19 | prop: 'b', 20 | type: String 21 | }, 22 | C: { 23 | prop: 'c', 24 | type: { 25 | CA: { 26 | prop: 'a', 27 | type: String 28 | }, 29 | CB: { 30 | prop: 'b', 31 | type: String 32 | } 33 | } 34 | } 35 | }, 36 | { 37 | includeNullValues: true 38 | } 39 | ) 40 | 41 | rows.should.deep.equal([ 42 | { a: 'a', b: 'b', c: { a: 'ca', b: null } }, 43 | { a: 'a', b: null, c: null }, 44 | ]) 45 | }) 46 | 47 | it('should handle missing columns / empty cells (default) (`required: false`)', () => { 48 | const { rows, errors } = mapToObjects([ 49 | [ 50 | 'COLUMN_2', 51 | 'COLUMN_3', 52 | 'COLUMN_4' 53 | ], [ 54 | '12', 55 | '13', 56 | '14' 57 | ], [ 58 | '22', 59 | '23', 60 | null 61 | ] 62 | ], { 63 | COLUMN_1: { 64 | prop: 'column1', 65 | type: String, 66 | required: false 67 | }, 68 | COLUMN_2: { 69 | prop: 'column2', 70 | type: String, 71 | required: false 72 | }, 73 | COLUMN_4: { 74 | prop: 'column4', 75 | type: String, 76 | required: false 77 | }, 78 | COLUMN_5: { 79 | prop: 'column5', 80 | type: String, 81 | required: false 82 | } 83 | }) 84 | 85 | errors.should.deep.equal([]) 86 | 87 | // Legacy behavior. 88 | rows.should.deep.equal([{ 89 | column2: '12', 90 | column4: '14' 91 | }, { 92 | column2: '22' 93 | }]) 94 | }) 95 | 96 | it('should handle missing columns / empty cells (`includeNullValues: true`) (`required: false`)', () => { 97 | const { rows, errors } = mapToObjects([ 98 | [ 99 | 'COLUMN_2', 100 | 'COLUMN_3', 101 | 'COLUMN_4' 102 | ], [ 103 | '12', 104 | '13', 105 | '14' 106 | ], [ 107 | '22', 108 | '23', 109 | null 110 | ] 111 | ], { 112 | COLUMN_1: { 113 | prop: 'column1', 114 | type: String, 115 | required: false 116 | }, 117 | COLUMN_2: { 118 | prop: 'column2', 119 | type: String, 120 | required: false 121 | }, 122 | COLUMN_4: { 123 | prop: 'column4', 124 | type: String, 125 | required: false 126 | }, 127 | COLUMN_5: { 128 | prop: 'column5', 129 | type: String, 130 | required: false 131 | } 132 | }, { 133 | includeNullValues: true 134 | }) 135 | 136 | errors.should.deep.equal([]) 137 | 138 | rows.should.deep.equal([{ 139 | column1: null, 140 | column2: '12', 141 | column4: '14', 142 | column5: null 143 | }, { 144 | column1: null, 145 | column2: '22', 146 | column4: null, 147 | column5: null 148 | }]) 149 | }) 150 | 151 | it('should require fields when cell value is empty', () => { 152 | const { rows, errors } = mapToObjects([ 153 | [ 154 | 'NUMBER', 155 | 'STRING' 156 | ], 157 | [ 158 | null, 159 | 'abc' 160 | ] 161 | ], { 162 | NUMBER: { 163 | prop: 'number', 164 | type: Number, 165 | required: true 166 | }, 167 | STRING: { 168 | prop: 'string', 169 | type: String, 170 | required: true 171 | } 172 | }) 173 | 174 | errors.should.deep.equal([{ 175 | error: 'required', 176 | row: 2, 177 | column: 'NUMBER', 178 | type: Number, 179 | // value: null, 180 | value: undefined 181 | }]) 182 | 183 | rows.should.deep.equal([{ 184 | string: 'abc' 185 | }]) 186 | }) 187 | 188 | it('shouldn\'t require fields when cell value is empty and object is empty too', () => { 189 | const { rows, errors } = mapToObjects([ 190 | [ 191 | 'NUMBER' 192 | ], 193 | [ 194 | null 195 | ] 196 | ], { 197 | NUMBER: { 198 | prop: 'number', 199 | type: Number, 200 | required: true 201 | } 202 | }) 203 | 204 | rows.should.deep.equal([]) 205 | }) 206 | 207 | it('should parse arrays (and remove `null` empty objects from result)', () => { 208 | const { rows, errors } = mapToObjects([ 209 | [ 210 | 'NAMES' 211 | ], [ 212 | 'Barack Obama, "String, with, colons", Donald Trump' 213 | ], [ 214 | null 215 | ] 216 | ], { 217 | NAMES: { 218 | prop: 'names', 219 | type: [String] 220 | } 221 | }) 222 | 223 | errors.should.deep.equal([]) 224 | 225 | rows.should.deep.equal([{ 226 | names: ['Barack Obama', 'String, with, colons', 'Donald Trump'] 227 | }]) 228 | }) 229 | 230 | it('should parse integers (and drop `null` errored objects from result)', () => 231 | { 232 | const { rows, errors } = mapToObjects([ 233 | [ 234 | 'INTEGER' 235 | ], [ 236 | '1' 237 | ], [ 238 | '1.2' 239 | ] 240 | ], { 241 | INTEGER: { 242 | prop: 'value', 243 | type: Integer 244 | } 245 | }) 246 | 247 | errors.length.should.equal(1) 248 | errors[0].should.deep.equal({ 249 | error: 'invalid', 250 | reason: 'not_an_integer', 251 | row: 3, 252 | column: 'INTEGER', 253 | type: Integer, 254 | value: '1.2' 255 | }) 256 | 257 | rows.should.deep.equal([{ 258 | value: 1 259 | }]) 260 | }) 261 | 262 | it('should not include `null` values by default (and set `null` for an "empty" object)', function() { 263 | const { rows } = mapToObjects( 264 | [ 265 | ['A', 'B', 'CA', 'CB'], 266 | ['a', 'b', 'ca', null], 267 | ['a', null] 268 | ], 269 | { 270 | A: { 271 | prop: 'a', 272 | type: String 273 | }, 274 | B: { 275 | prop: 'b', 276 | type: String 277 | }, 278 | C: { 279 | prop: 'c', 280 | type: { 281 | CA: { 282 | prop: 'a', 283 | type: String 284 | }, 285 | CB: { 286 | prop: 'b', 287 | type: String 288 | } 289 | } 290 | } 291 | } 292 | ) 293 | 294 | rows.should.deep.equal([ 295 | { a: 'a', b: 'b', c: { a: 'ca' } }, 296 | { a: 'a' }, 297 | ]) 298 | }) 299 | }) -------------------------------------------------------------------------------- /source/read/schema/mapToObjects.spreadsheet.js: -------------------------------------------------------------------------------- 1 | // Renames some of the `react-excel-file` options to `mapToObjects()` options. 2 | export default function mapToObjectsSpreadsheetBehavior(mapToObjects, data, schema, options = {}) { 3 | const { 4 | schemaPropertyValueForEmptyCell, 5 | ...restOptions 6 | } = options 7 | return mapToObjects(data, schema, { 8 | ...restOptions, 9 | schemaPropertyValueForNullCellValue: schemaPropertyValueForEmptyCell 10 | }) 11 | } -------------------------------------------------------------------------------- /source/read/schema/mapToObjects.spreadsheet.test.js: -------------------------------------------------------------------------------- 1 | import mapToObjects_ from './mapToObjects.js' 2 | import mapToObjectsSpreadsheetBehavior from './mapToObjects.spreadsheet.js' 3 | 4 | function mapToObjects(data, schema, options) { 5 | return mapToObjectsSpreadsheetBehavior(mapToObjects_, data, schema, options) 6 | } 7 | 8 | describe('mapToObjects (spreadsheet behavior)', () => { 9 | it('should handle missing columns / empty cells (`schemaPropertyValueForMissingColumn: null`) (`required: false`)', () => { 10 | const { rows, errors } = mapToObjects([ 11 | [ 12 | 'COLUMN_2', 13 | 'COLUMN_3', 14 | 'COLUMN_4' 15 | ], [ 16 | '12', 17 | '13', 18 | '14' 19 | ], [ 20 | '22', 21 | '23', 22 | null 23 | ] 24 | ], { 25 | COLUMN_1: { 26 | prop: 'column1', 27 | type: String, 28 | required: false 29 | }, 30 | COLUMN_2: { 31 | prop: 'column2', 32 | type: String, 33 | required: false 34 | }, 35 | COLUMN_4: { 36 | prop: 'column4', 37 | type: String, 38 | required: false 39 | }, 40 | COLUMN_5: { 41 | prop: 'column5', 42 | type: String, 43 | required: false 44 | } 45 | }, { 46 | schemaPropertyValueForMissingColumn: null 47 | }) 48 | 49 | errors.should.deep.equal([]) 50 | 51 | rows.should.deep.equal([{ 52 | column1: null, 53 | column2: '12', 54 | column4: '14', 55 | column5: null 56 | }, { 57 | column1: null, 58 | column2: '22', 59 | // column4: undefined, 60 | column5: null 61 | }]) 62 | }) 63 | 64 | it('should handle missing columns / empty cells (`schemaPropertyValueForEmptyCell: null`) (`required: false`)', () => { 65 | const { rows, errors } = mapToObjects([ 66 | [ 67 | 'COLUMN_2', 68 | 'COLUMN_3', 69 | 'COLUMN_4' 70 | ], [ 71 | '12', 72 | '13', 73 | '14' 74 | ], [ 75 | '22', 76 | '23', 77 | null 78 | ] 79 | ], { 80 | COLUMN_1: { 81 | prop: 'column1', 82 | type: String, 83 | required: false 84 | }, 85 | COLUMN_2: { 86 | prop: 'column2', 87 | type: String, 88 | required: false 89 | }, 90 | COLUMN_4: { 91 | prop: 'column4', 92 | type: String, 93 | required: false 94 | }, 95 | COLUMN_5: { 96 | prop: 'column5', 97 | type: String, 98 | required: false 99 | } 100 | }, { 101 | schemaPropertyValueForEmptyCell: null 102 | }) 103 | 104 | errors.should.deep.equal([]) 105 | 106 | rows.should.deep.equal([{ 107 | // column1: undefined, 108 | column2: '12', 109 | column4: '14', 110 | // column5: undefined 111 | }, { 112 | // column1: undefined, 113 | column2: '22', 114 | column4: null, 115 | // column5: undefined 116 | }]) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /source/read/schema/mapToObjects.test.js: -------------------------------------------------------------------------------- 1 | import mapToObjects, { parseArray, getBlock } from './mapToObjects.js' 2 | 3 | import Integer from '../../types/Integer.js' 4 | import URL from '../../types/URL.js' 5 | import Email from '../../types/Email.js' 6 | 7 | const date = convertToUTCTimezone(new Date(2018, 3 - 1, 24)) 8 | 9 | describe('mapToObjects', () => { 10 | it('should parse arrays', () => { 11 | getBlock('abc"de,f"g,h', ',', 0).should.deep.equal(['abcde,fg', 10]) 12 | parseArray(' abc"de,f"g , h ', ',').should.deep.equal(['abcde,fg', 'h']) 13 | }) 14 | 15 | it('should convert to json', () => { 16 | const { rows, errors } = mapToObjects([ 17 | [ 18 | 'DATE', 19 | 'NUMBER', 20 | 'BOOLEAN', 21 | 'STRING', 22 | 'PHONE', 23 | 'PHONE_TYPE' 24 | ], [ 25 | new Date(Date.parse('03/24/2018') - new Date().getTimezoneOffset() * 60 * 1000), // '43183', // '03/24/2018', 26 | '123', 27 | true, 28 | 'abc', 29 | '(123) 456-7890', 30 | '(123) 456-7890' 31 | ] 32 | ], { 33 | DATE: { 34 | prop: 'date', 35 | type: Date 36 | }, 37 | NUMBER: { 38 | prop: 'number', 39 | type: Number 40 | }, 41 | BOOLEAN: { 42 | prop: 'boolean', 43 | type: Boolean 44 | }, 45 | STRING: { 46 | prop: 'string', 47 | type: String 48 | }, 49 | PHONE: { 50 | prop: 'phone', 51 | parse(value) { 52 | return '+11234567890' 53 | } 54 | }, 55 | PHONE_TYPE: { 56 | prop: 'phoneType', 57 | type(value) { 58 | return '+11234567890' 59 | } 60 | } 61 | }) 62 | 63 | errors.should.deep.equal([]) 64 | 65 | // Convert `Date` to `String` for equality check. 66 | rows[0].date = rows[0].date.toISOString() 67 | 68 | rows.should.deep.equal([{ 69 | date: date.toISOString(), 70 | number: 123, 71 | phone: '+11234567890', 72 | phoneType: '+11234567890', 73 | boolean: true, 74 | string: 'abc' 75 | }]) 76 | }) 77 | 78 | it('should support schema entries with no `type`s', () => { 79 | const { rows, errors } = mapToObjects([ 80 | [ 81 | 'DATE', 82 | 'NUMBER', 83 | 'BOOLEAN', 84 | 'STRING' 85 | ], [ 86 | new Date(Date.parse('03/24/2018') - new Date().getTimezoneOffset() * 60 * 1000), // '43183', // '03/24/2018', 87 | 123, 88 | true, 89 | 'abc' 90 | ] 91 | ], { 92 | DATE: { 93 | prop: 'date' 94 | }, 95 | NUMBER: { 96 | prop: 'number' 97 | }, 98 | BOOLEAN: { 99 | prop: 'boolean' 100 | }, 101 | STRING: { 102 | prop: 'string' 103 | } 104 | }) 105 | 106 | errors.should.deep.equal([]) 107 | 108 | // Convert `Date` to `String` for equality check. 109 | rows[0].date = rows[0].date.toISOString() 110 | 111 | rows.should.deep.equal([{ 112 | date: date.toISOString(), 113 | number: 123, 114 | boolean: true, 115 | string: 'abc' 116 | }]) 117 | }) 118 | 119 | it('should require fields when cell value is empty', () => { 120 | const { rows, errors } = mapToObjects([ 121 | [ 122 | 'NUMBER', 123 | 'STRING' 124 | ], 125 | [ 126 | null, 127 | 'abc' 128 | ] 129 | ], { 130 | NUMBER: { 131 | prop: 'number', 132 | type: Number, 133 | required: true 134 | }, 135 | STRING: { 136 | prop: 'string', 137 | type: String, 138 | required: true 139 | } 140 | }) 141 | 142 | errors.should.deep.equal([{ 143 | error: 'required', 144 | row: 2, 145 | column: 'NUMBER', 146 | type: Number, 147 | value: null 148 | }]) 149 | 150 | rows.should.deep.equal([{ 151 | number: null, 152 | string: 'abc' 153 | }]) 154 | }) 155 | 156 | it('shouldn\'t require fields when cell value is empty and object is empty too', () => { 157 | const { rows, errors } = mapToObjects([ 158 | [ 159 | 'NUMBER' 160 | ], 161 | [ 162 | null 163 | ] 164 | ], { 165 | NUMBER: { 166 | prop: 'number', 167 | type: Number, 168 | required: true 169 | } 170 | }) 171 | 172 | rows.should.deep.equal([null]) 173 | }) 174 | 175 | it('should parse arrays', () => { 176 | const { rows, errors } = mapToObjects([ 177 | [ 178 | 'NAMES' 179 | ], [ 180 | 'Barack Obama, "String, with, colons", Donald Trump' 181 | ], [ 182 | null 183 | ] 184 | ], { 185 | NAMES: { 186 | prop: 'names', 187 | type: [String] 188 | } 189 | }) 190 | 191 | errors.should.deep.equal([]) 192 | 193 | rows.should.deep.equal([{ 194 | names: ['Barack Obama', 'String, with, colons', 'Donald Trump'] 195 | }, null]) 196 | }) 197 | 198 | it('should parse integers', () => 199 | { 200 | const { rows, errors } = mapToObjects([ 201 | [ 202 | 'INTEGER' 203 | ], [ 204 | '1' 205 | ], [ 206 | '1.2' 207 | ] 208 | ], { 209 | INTEGER: { 210 | prop: 'value', 211 | type: Integer 212 | } 213 | }) 214 | 215 | errors.length.should.equal(1) 216 | errors[0].should.deep.equal({ 217 | error: 'invalid', 218 | reason: 'not_an_integer', 219 | row: 3, 220 | column: 'INTEGER', 221 | type: Integer, 222 | value: '1.2' 223 | }) 224 | 225 | rows.should.deep.equal([{ 226 | value: 1 227 | }, null]) 228 | }) 229 | 230 | it('should parse URLs', () => 231 | { 232 | const { rows, errors } = mapToObjects([ 233 | [ 234 | 'URL' 235 | ], [ 236 | 'https://kremlin.ru' 237 | ], [ 238 | 'kremlin.ru' 239 | ] 240 | ], { 241 | URL: { 242 | prop: 'value', 243 | type: URL 244 | } 245 | }) 246 | 247 | errors.length.should.equal(1) 248 | errors[0].row.should.equal(3) 249 | errors[0].column.should.equal('URL') 250 | errors[0].error.should.equal('invalid') 251 | 252 | rows.should.deep.equal([{ 253 | value: 'https://kremlin.ru' 254 | }, null]) 255 | }) 256 | 257 | it('should parse Emails', () => 258 | { 259 | const { rows, errors } = mapToObjects([ 260 | [ 261 | 'EMAIL' 262 | ], [ 263 | 'vladimir.putin@kremlin.ru' 264 | ], [ 265 | '123' 266 | ] 267 | ], { 268 | EMAIL: { 269 | prop: 'value', 270 | type: Email 271 | } 272 | }) 273 | 274 | errors.length.should.equal(1) 275 | errors[0].row.should.equal(3) 276 | errors[0].column.should.equal('EMAIL') 277 | errors[0].error.should.equal('invalid') 278 | 279 | rows.should.deep.equal([{ 280 | value: 'vladimir.putin@kremlin.ru' 281 | }, null]) 282 | }) 283 | 284 | it('should call .validate()', () => { 285 | const { rows, errors } = mapToObjects([ 286 | [ 287 | 'NAME' 288 | ], [ 289 | 'George Bush' 290 | ] 291 | ], { 292 | NAME: { 293 | prop: 'name', 294 | type: String, 295 | required: true, 296 | validate: (value) => { 297 | if (value === 'George Bush') { 298 | throw new Error('custom-error') 299 | } 300 | } 301 | } 302 | }) 303 | 304 | errors.should.deep.equal([{ 305 | error: 'custom-error', 306 | row: 2, 307 | column: 'NAME', 308 | type: String, 309 | value: 'George Bush' 310 | }]) 311 | 312 | rows.should.deep.equal([null]) 313 | }) 314 | 315 | it('should validate numbers', () => { 316 | const { rows, errors } = mapToObjects([ 317 | [ 318 | 'NUMBER' 319 | ], [ 320 | '123abc' 321 | ] 322 | ], { 323 | NUMBER: { 324 | prop: 'number', 325 | type: Number, 326 | required: true 327 | } 328 | }) 329 | 330 | errors.should.deep.equal([{ 331 | error: 'invalid', 332 | reason: 'not_a_number', 333 | row: 2, 334 | column: 'NUMBER', 335 | type: Number, 336 | value: '123abc' 337 | }]) 338 | 339 | rows.should.deep.equal([null]) 340 | }) 341 | 342 | it('should validate booleans', () => { 343 | const { rows, errors } = mapToObjects([ 344 | [ 345 | 'TRUE', 346 | 'FALSE', 347 | 'INVALID' 348 | ], [ 349 | true, 350 | false, 351 | 'TRUE' 352 | ] 353 | ], { 354 | TRUE: { 355 | prop: 'true', 356 | type: Boolean, 357 | required: true 358 | }, 359 | FALSE: { 360 | prop: 'false', 361 | type: Boolean, 362 | required: true 363 | }, 364 | INVALID: { 365 | prop: 'invalid', 366 | type: Boolean, 367 | required: true 368 | } 369 | }) 370 | 371 | errors.should.deep.equal([{ 372 | error: 'invalid', 373 | reason: 'not_a_boolean', 374 | row: 2, 375 | column: 'INVALID', 376 | type: Boolean, 377 | value: 'TRUE' 378 | }]) 379 | 380 | rows.should.deep.equal([{ 381 | true: true, 382 | false: false 383 | }]) 384 | }) 385 | 386 | it('should validate dates', () => { 387 | const { rows, errors } = mapToObjects([ 388 | [ 389 | 'DATE', 390 | 'INVALID' 391 | ], [ 392 | 43183, // 03/24/2018', 393 | '-' 394 | ], [ 395 | date, // 03/24/2018',, 396 | '-' 397 | ] 398 | ], { 399 | DATE: { 400 | prop: 'date', 401 | type: Date, 402 | required: true 403 | }, 404 | INVALID: { 405 | prop: 'invalid', 406 | type: Date, 407 | required: true 408 | } 409 | }) 410 | 411 | errors.should.deep.equal([{ 412 | error: 'invalid', 413 | reason: 'not_a_date', 414 | row: 2, 415 | column: 'INVALID', 416 | type: Date, 417 | value: '-' 418 | }, { 419 | error: 'invalid', 420 | reason: 'not_a_date', 421 | row: 3, 422 | column: 'INVALID', 423 | type: Date, 424 | value: '-' 425 | }]) 426 | 427 | rows.should.deep.equal([{ 428 | date 429 | }, { 430 | date 431 | }]) 432 | }) 433 | 434 | it('should throw parse() errors', () => { 435 | const { rows, errors } = mapToObjects([ 436 | [ 437 | 'PHONE', 438 | 'PHONE_TYPE' 439 | ], [ 440 | '123', 441 | '123' 442 | ] 443 | ], { 444 | PHONE: { 445 | prop: 'phone', 446 | parse: () => { 447 | throw new Error('invalid') 448 | } 449 | }, 450 | PHONE_TYPE: { 451 | prop: 'phoneType', 452 | parse: () => { 453 | throw new Error('invalid') 454 | } 455 | } 456 | }) 457 | 458 | errors.should.deep.equal([{ 459 | error: 'invalid', 460 | row: 2, 461 | column: 'PHONE', 462 | value: '123' 463 | }, { 464 | error: 'invalid', 465 | row: 2, 466 | column: 'PHONE_TYPE', 467 | value: '123' 468 | }]) 469 | 470 | rows.should.deep.equal([null]) 471 | }) 472 | 473 | it('should map row numbers', () => { 474 | const { rows, errors } = mapToObjects([ 475 | [ 476 | 'NUMBER' 477 | ], [ 478 | '123abc' 479 | ] 480 | ], { 481 | NUMBER: { 482 | prop: 'number', 483 | type: Number 484 | } 485 | }, { 486 | rowIndexMap: [2, 5] 487 | }) 488 | 489 | errors.should.deep.equal([{ 490 | error: 'invalid', 491 | reason: 'not_a_number', 492 | row: 6, 493 | column: 'NUMBER', 494 | type: Number, 495 | value: '123abc' 496 | }]) 497 | }) 498 | 499 | it('should validate "oneOf" (valid)', () => { 500 | const { rows, errors } = mapToObjects([ 501 | [ 502 | 'STATUS' 503 | ], 504 | [ 505 | 'STARTED' 506 | ] 507 | ], { 508 | STATUS: { 509 | prop: 'status', 510 | type: String, 511 | oneOf: [ 512 | 'STARTED', 513 | 'FINISHED' 514 | ] 515 | } 516 | }) 517 | 518 | errors.length.should.equal(0) 519 | }) 520 | 521 | it('should validate "oneOf" (not valid)', () => { 522 | const { rows, errors } = mapToObjects([ 523 | [ 524 | 'STATUS' 525 | ], 526 | [ 527 | 'SCHEDULED' 528 | ] 529 | ], { 530 | STATUS: { 531 | prop: 'status', 532 | type: String, 533 | oneOf: [ 534 | 'STARTED', 535 | 'FINISHED' 536 | ] 537 | } 538 | }) 539 | 540 | errors.should.deep.equal([{ 541 | error: 'invalid', 542 | reason: 'unknown', 543 | row: 2, 544 | column: 'STATUS', 545 | type: String, 546 | value: 'SCHEDULED' 547 | }]) 548 | }) 549 | 550 | it('should not include `null` values by default', function() { 551 | const { rows } = mapToObjects( 552 | [ 553 | ['A', 'B', 'CA', 'CB'], 554 | ['a', 'b', 'ca', null], 555 | ['a', null] 556 | ], 557 | { 558 | A: { 559 | prop: 'a', 560 | type: String 561 | }, 562 | B: { 563 | prop: 'b', 564 | type: String 565 | }, 566 | C: { 567 | prop: 'c', 568 | type: { 569 | CA: { 570 | prop: 'a', 571 | type: String 572 | }, 573 | CB: { 574 | prop: 'b', 575 | type: String 576 | } 577 | } 578 | } 579 | } 580 | ) 581 | 582 | rows.should.deep.equal([ 583 | { a: 'a', b: 'b', c: { a: 'ca', b: null } }, 584 | { a: 'a', b: null, c: null }, 585 | ]) 586 | }) 587 | 588 | it('should handle missing columns / empty cells (default) (`required: false`)', () => { 589 | const { rows, errors } = mapToObjects([ 590 | [ 591 | 'COLUMN_2', 592 | 'COLUMN_3', 593 | 'COLUMN_4' 594 | ], [ 595 | '12', 596 | '13', 597 | '14' 598 | ], [ 599 | '22', 600 | '23', 601 | null 602 | ] 603 | ], { 604 | COLUMN_1: { 605 | prop: 'column1', 606 | type: String, 607 | required: false 608 | }, 609 | COLUMN_2: { 610 | prop: 'column2', 611 | type: String, 612 | required: false 613 | }, 614 | COLUMN_4: { 615 | prop: 'column4', 616 | type: String, 617 | required: false 618 | }, 619 | COLUMN_5: { 620 | prop: 'column5', 621 | type: String, 622 | required: false 623 | } 624 | }) 625 | 626 | errors.should.deep.equal([]) 627 | 628 | // Legacy behavior. 629 | rows.should.deep.equal([{ 630 | column2: '12', 631 | column4: '14' 632 | }, { 633 | column2: '22', 634 | column4: null 635 | }]) 636 | }) 637 | 638 | it('should handle missing columns / empty cells (`schemaPropertyValueForMissingColumn: null`) (`required: false`)', () => { 639 | const { rows, errors } = mapToObjects([ 640 | [ 641 | 'COLUMN_2', 642 | 'COLUMN_3', 643 | 'COLUMN_4' 644 | ], [ 645 | '12', 646 | '13', 647 | '14' 648 | ], [ 649 | '22', 650 | '23', 651 | null 652 | ] 653 | ], { 654 | COLUMN_1: { 655 | prop: 'column1', 656 | type: String, 657 | required: false 658 | }, 659 | COLUMN_2: { 660 | prop: 'column2', 661 | type: String, 662 | required: false 663 | }, 664 | COLUMN_4: { 665 | prop: 'column4', 666 | type: String, 667 | required: false 668 | }, 669 | COLUMN_5: { 670 | prop: 'column5', 671 | type: String, 672 | required: false 673 | } 674 | }, { 675 | schemaPropertyValueForMissingColumn: null 676 | }) 677 | 678 | errors.should.deep.equal([]) 679 | 680 | rows.should.deep.equal([{ 681 | column1: null, 682 | column2: '12', 683 | column4: '14', 684 | column5: null 685 | }, { 686 | column1: null, 687 | column2: '22', 688 | column4: null, 689 | column5: null 690 | }]) 691 | }) 692 | 693 | it('should handle missing columns / empty cells (`schemaPropertyValueForNullCellValue: null`) (`required: false`)', () => { 694 | const { rows, errors } = mapToObjects([ 695 | [ 696 | 'COLUMN_2', 697 | 'COLUMN_3', 698 | 'COLUMN_4' 699 | ], [ 700 | '12', 701 | '13', 702 | '14' 703 | ], [ 704 | '22', 705 | '23', 706 | null 707 | ] 708 | ], { 709 | COLUMN_1: { 710 | prop: 'column1', 711 | type: String, 712 | required: false 713 | }, 714 | COLUMN_2: { 715 | prop: 'column2', 716 | type: String, 717 | required: false 718 | }, 719 | COLUMN_4: { 720 | prop: 'column4', 721 | type: String, 722 | required: false 723 | }, 724 | COLUMN_5: { 725 | prop: 'column5', 726 | type: String, 727 | required: false 728 | } 729 | }, { 730 | schemaPropertyValueForNullCellValue: null 731 | }) 732 | 733 | errors.should.deep.equal([]) 734 | 735 | rows.should.deep.equal([{ 736 | // column1: undefined, 737 | column2: '12', 738 | column4: '14', 739 | // column5: undefined 740 | }, { 741 | // column1: undefined, 742 | column2: '22', 743 | column4: null, 744 | // column5: undefined 745 | }]) 746 | }) 747 | 748 | it('should handle missing columns / empty cells (`schemaPropertyValueForMissingColumn: null` and `schemaPropertyValueForNullCellValue: null`) (`required: false`)', () => { 749 | const { rows, errors } = mapToObjects([ 750 | [ 751 | 'COLUMN_2', 752 | 'COLUMN_3', 753 | 'COLUMN_4' 754 | ], [ 755 | '12', 756 | '13', 757 | '14' 758 | ], [ 759 | '22', 760 | '23', 761 | null 762 | ] 763 | ], { 764 | COLUMN_1: { 765 | prop: 'column1', 766 | type: String, 767 | required: false 768 | }, 769 | COLUMN_2: { 770 | prop: 'column2', 771 | type: String, 772 | required: false 773 | }, 774 | COLUMN_4: { 775 | prop: 'column4', 776 | type: String, 777 | required: false 778 | }, 779 | COLUMN_5: { 780 | prop: 'column5', 781 | type: String, 782 | required: false 783 | } 784 | }, { 785 | schemaPropertyValueForMissingColumn: null, 786 | schemaPropertyValueForNullCellValue: null 787 | }) 788 | 789 | errors.should.deep.equal([]) 790 | 791 | rows.should.deep.equal([{ 792 | column1: null, 793 | column2: '12', 794 | column4: '14', 795 | column5: null 796 | }, { 797 | column1: null, 798 | column2: '22', 799 | column4: null, 800 | column5: null 801 | }]) 802 | }) 803 | 804 | it('should handle missing columns / empty cells (`schemaPropertyValueForMissingColumn: null` and `schemaPropertyValueForNullCellValue: null` and `schemaPropertyShouldSkipRequiredValidationForMissingColumn()` not specified) (`required: true`)', () => { 805 | const { rows, errors } = mapToObjects([ 806 | [ 807 | 'COLUMN_2', 808 | 'COLUMN_3', 809 | 'COLUMN_4' 810 | ], [ 811 | '12', 812 | '13', 813 | '14' 814 | ], [ 815 | '22', 816 | '23', 817 | null 818 | ] 819 | ], { 820 | COLUMN_1: { 821 | prop: 'column1', 822 | type: String, 823 | required: false 824 | }, 825 | COLUMN_2: { 826 | prop: 'column2', 827 | type: String, 828 | required: false 829 | }, 830 | COLUMN_4: { 831 | prop: 'column4', 832 | type: String, 833 | required: false 834 | }, 835 | COLUMN_5: { 836 | prop: 'column5', 837 | type: String, 838 | required: true 839 | } 840 | }, { 841 | schemaPropertyValueForMissingColumn: null, 842 | schemaPropertyValueForNullCellValue: null 843 | }) 844 | 845 | errors.should.deep.equal([{ 846 | column: 'COLUMN_5', 847 | error: 'required', 848 | row: 2, 849 | type: String, 850 | value: null 851 | }, { 852 | column: 'COLUMN_5', 853 | error: 'required', 854 | row: 3, 855 | type: String, 856 | value: null 857 | }]) 858 | 859 | rows.should.deep.equal([{ 860 | column1: null, 861 | column2: '12', 862 | column4: '14', 863 | column5: null 864 | }, { 865 | column1: null, 866 | column2: '22', 867 | column4: null, 868 | column5: null 869 | }]) 870 | }) 871 | 872 | it('should handle missing columns / empty cells (`schemaPropertyValueForMissingColumn: null` and `schemaPropertyValueForNullCellValue: null` and `schemaPropertyShouldSkipRequiredValidationForMissingColumn: () => false`) (`required: true`)', () => { 873 | const { rows, errors } = mapToObjects([ 874 | [ 875 | 'COLUMN_2', 876 | 'COLUMN_3', 877 | 'COLUMN_4' 878 | ], [ 879 | '12', 880 | '13', 881 | '14' 882 | ], [ 883 | '22', 884 | '23', 885 | null 886 | ] 887 | ], { 888 | COLUMN_1: { 889 | prop: 'column1', 890 | type: String, 891 | required: false 892 | }, 893 | COLUMN_2: { 894 | prop: 'column2', 895 | type: String, 896 | required: false 897 | }, 898 | COLUMN_4: { 899 | prop: 'column4', 900 | type: String, 901 | required: false 902 | }, 903 | COLUMN_5: { 904 | prop: 'column5', 905 | type: String, 906 | required: true 907 | } 908 | }, { 909 | schemaPropertyValueForMissingColumn: null, 910 | schemaPropertyValueForNullCellValue: null, 911 | schemaPropertyShouldSkipRequiredValidationForMissingColumn: () => false 912 | }) 913 | 914 | errors.should.deep.equal([{ 915 | column: 'COLUMN_5', 916 | error: 'required', 917 | row: 2, 918 | type: String, 919 | value: null 920 | }, { 921 | column: 'COLUMN_5', 922 | error: 'required', 923 | row: 3, 924 | type: String, 925 | value: null 926 | }]) 927 | 928 | rows.should.deep.equal([{ 929 | column1: null, 930 | column2: '12', 931 | column4: '14', 932 | column5: null 933 | }, { 934 | column1: null, 935 | column2: '22', 936 | column4: null, 937 | column5: null 938 | }]) 939 | }) 940 | 941 | it('should handle missing columns / empty cells (`schemaPropertyValueForMissingColumn: null` and `schemaPropertyValueForNullCellValue: null` and `schemaPropertyShouldSkipRequiredValidationForMissingColumn: () => true`) (`required: true`)', () => { 942 | const { rows, errors } = mapToObjects([ 943 | [ 944 | 'COLUMN_2', 945 | 'COLUMN_3', 946 | 'COLUMN_4' 947 | ], [ 948 | '12', 949 | '13', 950 | '14' 951 | ], [ 952 | '22', 953 | '23', 954 | null 955 | ] 956 | ], { 957 | COLUMN_1: { 958 | prop: 'column1', 959 | type: String, 960 | required: false 961 | }, 962 | COLUMN_2: { 963 | prop: 'column2', 964 | type: String, 965 | required: false 966 | }, 967 | COLUMN_4: { 968 | prop: 'column4', 969 | type: String, 970 | required: false 971 | }, 972 | COLUMN_5: { 973 | prop: 'column5', 974 | type: String, 975 | required: true 976 | } 977 | }, { 978 | schemaPropertyValueForMissingColumn: null, 979 | schemaPropertyValueForNullCellValue: null, 980 | schemaPropertyShouldSkipRequiredValidationForMissingColumn: () => true 981 | }) 982 | 983 | errors.should.deep.equal([]) 984 | 985 | rows.should.deep.equal([{ 986 | column1: null, 987 | column2: '12', 988 | column4: '14', 989 | column5: null 990 | }, { 991 | column1: null, 992 | column2: '22', 993 | column4: null, 994 | column5: null 995 | }]) 996 | }) 997 | }) 998 | 999 | // Converts timezone to UTC while preserving the same time 1000 | function convertToUTCTimezone(date) { 1001 | // Doesn't account for leap seconds but I guess that's ok 1002 | // given that javascript's own `Date()` does not either. 1003 | // https://www.timeanddate.com/time/leap-seconds-background.html 1004 | // 1005 | // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset 1006 | // 1007 | return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000) 1008 | } 1009 | -------------------------------------------------------------------------------- /source/read/unpackXlsxFileBrowser.js: -------------------------------------------------------------------------------- 1 | import { unzipSync, strFromU8 } from 'fflate' 2 | 3 | /** 4 | * Reads XLSX file in a browser. 5 | * @param {(File|Blob|ArrayBuffer)} input - A `File` or an `ArrayBuffer`. 6 | * @return {Promise} Resolves to an object holding XLSX file entries. 7 | */ 8 | export default function unpackXlsxFile(input) { 9 | if (input instanceof File) { 10 | return input.arrayBuffer().then(unpackXlsxArrayBuffer) 11 | } 12 | if (input instanceof Blob) { 13 | return input.arrayBuffer().then(unpackXlsxArrayBuffer) 14 | } 15 | return unpackXlsxArrayBuffer(input) 16 | } 17 | 18 | /** 19 | * Reads XLSX file in a browser from an `ArrayBuffer`. 20 | * @param {ArrayBuffer} input 21 | * @return {Promise} Resolves to an object holding XLSX file entries. 22 | */ 23 | function unpackXlsxArrayBuffer(arrayBuffer) { 24 | const archive = new Uint8Array(arrayBuffer) 25 | const contents = unzipSync(archive) 26 | return Promise.resolve(getContents(contents)) 27 | // return new Promise((resolve, reject) => { 28 | // unzip(archive, (error, contents) => { 29 | // if (error) { 30 | // return reject(error) 31 | // } 32 | // return resolve(getContents(contents)) 33 | // }) 34 | // }) 35 | } 36 | 37 | function getContents(contents) { 38 | const unzippedFiles = [] 39 | for (const key of Object.keys(contents)) { 40 | unzippedFiles[key] = strFromU8(contents[key]) 41 | } 42 | return unzippedFiles 43 | } -------------------------------------------------------------------------------- /source/read/unpackXlsxFileNode.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import Stream, { Readable } from 'stream' 3 | 4 | // `unzipper` has a bug when it doesn't include "@aws-sdk/client-s3" package in the `dependencies` 5 | // which causes some "bundlers" throw an error. 6 | // https://github.com/ZJONSSON/node-unzipper/issues/330 7 | // 8 | // One workaround is to install "@aws-sdk/client-s3" package manually, which would still lead to increased bundle size. 9 | // If the code is bundled for server-side-use only, that is will not be used in a web browser, 10 | // then the increased bundle size would not be an issue. 11 | // 12 | // Another workaround could be to "alias" "@aws-sdk/client-s3" package in a "bundler" configuration file 13 | // with a path to a `*.js` file containing just "export default null". But that kind of a workaround would also 14 | // result in errors when using other packages that `import` anything from "@aws-sdk/client-s3" package, 15 | // so it's not really a workaround but more of a ticking bomb. 16 | // 17 | import unzip from 'unzipper' 18 | 19 | /** 20 | * Reads XLSX file in Node.js. 21 | * @param {(string|Stream)} input - A Node.js readable stream or a path to a file. 22 | * @return {Promise} Resolves to an object holding XLSX file entries. 23 | */ 24 | export default function unpackXlsxFile(input) { 25 | // XLSX file is a zip archive. 26 | // The `entries` object stores the files 27 | // and their contents from this XLSX zip archive. 28 | const entries = {} 29 | 30 | const stream = input instanceof Stream 31 | ? input 32 | : ( 33 | input instanceof Buffer 34 | ? createReadableStreamFromBuffer(input) 35 | : fs.createReadStream(input) 36 | ) 37 | 38 | return new Promise((resolve, reject) => { 39 | const entryPromises = [] 40 | 41 | stream 42 | // This first "error" listener is for the original stream errors. 43 | .on('error', reject) 44 | .pipe(unzip.Parse()) 45 | // This second "error" listener is for the unzip stream errors. 46 | .on('error', reject) 47 | .on('finish', () => { 48 | }) 49 | .on('close', () => { 50 | Promise.all(entryPromises).then(() => resolve(entries)) 51 | }) 52 | .on('entry', (entry) => { 53 | let contents = '' 54 | // To ignore an entry: `entry.autodrain()`. 55 | entryPromises.push(new Promise((resolve) => { 56 | // It's not clear what encoding are the files inside XLSX in. 57 | // https://stackoverflow.com/questions/45194771/are-xlsx-files-utf-8-encoded-by-definition 58 | // For example, for XML files, encoding is specified at the top node: 59 | // ``. 60 | // 61 | // `unzipper` supports setting encoding when reading an `entry`. 62 | // https://github.com/ZJONSSON/node-unzipper/issues/35 63 | // https://gitlab.com/catamphetamine/read-excel-file/-/issues/54 64 | // 65 | // If the `entry.setEncoding('utf8')` line would be commented out, 66 | // there's a `nonAsciiCharacterEncoding` test that wouldn't pass. 67 | // 68 | entry.setEncoding('utf8') 69 | // 70 | entry 71 | .on('data', data => contents += data.toString()) 72 | .on('end', () => resolve(entries[entry.path] = contents)) 73 | })) 74 | }) 75 | }) 76 | } 77 | 78 | // Creates a readable stream from a `Buffer`. 79 | function createReadableStreamFromBuffer(buffer) { 80 | // Node.js seems to have a bug in `Readable.from()` function: 81 | // it doesn't correctly handle empty buffers, i.e. it doesn't return a correct stream. 82 | // https://gitlab.com/catamphetamine/read-excel-file/-/issues/106 83 | if (buffer.length === 0) { 84 | throw new Error('No data') 85 | } 86 | return Readable.from(buffer) 87 | } -------------------------------------------------------------------------------- /source/types/Boolean.js: -------------------------------------------------------------------------------- 1 | import InvalidError from './InvalidError.js' 2 | 3 | export default function BooleanType(value) { 4 | if (typeof value === 'boolean') { 5 | return value 6 | } 7 | throw new InvalidError('not_a_boolean') 8 | } -------------------------------------------------------------------------------- /source/types/Date.js: -------------------------------------------------------------------------------- 1 | import parseDate from '../read/parseDate.js' 2 | import InvalidError from './InvalidError.js' 3 | 4 | export default function DateType(value, { properties }) { 5 | // XLSX has no specific format for dates. 6 | // Sometimes a date can be heuristically detected. 7 | // https://github.com/catamphetamine/read-excel-file/issues/3#issuecomment-395770777 8 | if (value instanceof Date) { 9 | if (isNaN(value.valueOf())) { 10 | throw new InvalidError('out_of_bounds') 11 | } 12 | return value 13 | } 14 | if (typeof value === 'number') { 15 | if (isNaN(value)) { 16 | throw new InvalidError('invalid_number') 17 | } 18 | if (!isFinite(value)) { 19 | throw new InvalidError('out_of_bounds') 20 | } 21 | const date = parseDate(value, properties) 22 | if (isNaN(date.valueOf())) { 23 | throw new InvalidError('out_of_bounds') 24 | } 25 | return date 26 | } 27 | throw new InvalidError('not_a_date') 28 | } -------------------------------------------------------------------------------- /source/types/Email.js: -------------------------------------------------------------------------------- 1 | import InvalidError from './InvalidError.js' 2 | 3 | export default function Email(value) { 4 | if (typeof value === 'string') { 5 | if (isEmail(value)) { 6 | return value 7 | } 8 | throw new InvalidError('not_an_email') 9 | } 10 | throw new InvalidError('not_a_string') 11 | } 12 | 13 | const regexp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i 14 | 15 | export function isEmail(value) { 16 | return regexp.test(value) 17 | } -------------------------------------------------------------------------------- /source/types/Email.test.js: -------------------------------------------------------------------------------- 1 | import { isEmail } from './Email.js' 2 | 3 | describe('Email', () => { 4 | it('should validate an Email', () => { 5 | isEmail('123').should.equal(false) 6 | isEmail('vladimir.putin@kremlin.ru').should.equal(true) 7 | }) 8 | }) -------------------------------------------------------------------------------- /source/types/Integer.js: -------------------------------------------------------------------------------- 1 | import InvalidError from './InvalidError.js' 2 | import NumberType from './Number.js' 3 | 4 | export default function Integer(value) { 5 | value = NumberType(value) 6 | if (!isInteger(value)) { 7 | throw new InvalidError('not_an_integer') 8 | } 9 | return value 10 | } 11 | 12 | export function isInteger(x) { 13 | // https://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript 14 | return (x | 0) === x 15 | } -------------------------------------------------------------------------------- /source/types/Integer.test.js: -------------------------------------------------------------------------------- 1 | import { isInteger } from './Integer.js' 2 | 3 | describe('Integer', () => { 4 | it('should validate an Integer', () => { 5 | // isInteger('1.2').should.equal(false) 6 | // isInteger('1').should.equal(true) 7 | isInteger(1.2).should.equal(false) 8 | isInteger(1).should.equal(true) 9 | }) 10 | }) -------------------------------------------------------------------------------- /source/types/InvalidError.js: -------------------------------------------------------------------------------- 1 | export default class InvalidError extends Error { 2 | constructor(reason) { 3 | super('invalid') 4 | this.reason = reason 5 | } 6 | } -------------------------------------------------------------------------------- /source/types/Number.js: -------------------------------------------------------------------------------- 1 | import InvalidError from './InvalidError.js' 2 | 3 | export default function NumberType(value) { 4 | // An XLSX file editing software might not always correctly 5 | // detect numeric values in string-type cells. Users won't bother 6 | // manually selecting a cell type, so the editing software has to guess 7 | // based on the user's input. One can assume that such auto-detection 8 | // might not always work. 9 | // 10 | // So, if a cell is supposed to be a numeric one, convert a string value to a number. 11 | // 12 | if (typeof value === 'string') { 13 | const stringifiedValue = value 14 | value = Number(value) 15 | if (String(value) !== stringifiedValue) { 16 | throw new InvalidError('not_a_number') 17 | } 18 | } 19 | if (typeof value !== 'number') { 20 | throw new InvalidError('not_a_number') 21 | } 22 | if (isNaN(value)) { 23 | throw new InvalidError('invalid_number') 24 | } 25 | // At this point, `value` can only be a number. 26 | // 27 | // The global `isFinite()` function filters out: 28 | // * NaN 29 | // * -Infinity 30 | // * Infinity 31 | // 32 | // All other values pass (including non-numbers). 33 | // 34 | if (!isFinite(value)) { 35 | throw new InvalidError('out_of_bounds') 36 | } 37 | return value 38 | } -------------------------------------------------------------------------------- /source/types/String.js: -------------------------------------------------------------------------------- 1 | import InvalidError from './InvalidError.js' 2 | 3 | export default function StringType(value) { 4 | if (typeof value === 'string') { 5 | return value 6 | } 7 | // Excel tends to perform a forced automatic convertion of string-type values 8 | // to number-type ones when the user has input them. Otherwise, users wouldn't 9 | // be able to perform formula calculations on those cell values because users 10 | // won't bother manually choosing a "numeric" cell type for each cell, and 11 | // even if they did, choosing a "numeric" cell type every time wouldn't be an 12 | // acceptable "user experience". 13 | // 14 | // So, if a cell value is supposed to be a string and Excel has automatically 15 | // converted it to a number, perform a backwards conversion. 16 | // 17 | if (typeof value === 'number') { 18 | if (isNaN(value)) { 19 | throw new InvalidError('invalid_number') 20 | } 21 | // The global `isFinite()` function filters out: 22 | // * NaN 23 | // * -Infinity 24 | // * Infinity 25 | // 26 | // All other values pass (including non-numbers). 27 | // 28 | if (!isFinite(value)) { 29 | throw new InvalidError('out_of_bounds') 30 | } 31 | return String(value) 32 | } 33 | throw new InvalidError('not_a_string') 34 | } -------------------------------------------------------------------------------- /source/types/URL.js: -------------------------------------------------------------------------------- 1 | import InvalidError from './InvalidError.js' 2 | 3 | export default function URL(value) { 4 | if (typeof value === 'string') { 5 | if (isURL(value)) { 6 | return value 7 | } 8 | throw new InvalidError('not_a_url') 9 | } 10 | throw new InvalidError('not_a_string') 11 | } 12 | 13 | // URL regexp explanation: 14 | // 15 | // /^ 16 | // 17 | // (?: 18 | // // Matches optional "http(s):" or "ftp:": 19 | // (?: 20 | // (?:https?|ftp): 21 | // )? 22 | // 23 | // // Matches "//" (required): 24 | // \/\/ 25 | // ) 26 | // 27 | // // Matches a valid non-local IP address: 28 | // (?: 29 | // (?:[1-9]\d?|1\d\d|2[01]\d|22[0-3]) 30 | // (?: 31 | // \. 32 | // (?:1?\d{1,2}|2[0-4]\d|25[0-5]) 33 | // ){2} 34 | // (?: 35 | // \. 36 | // (?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]) 37 | // ) 38 | // 39 | // // Or, 40 | // | 41 | // 42 | // // Matches an alpha-numeric domain name. 43 | // (?: 44 | // (?: 45 | // [a-z0-9\u00a1-\uffff] 46 | // [a-z0-9\u00a1-\uffff_-]{0,62} 47 | // )? 48 | // [a-z0-9\u00a1-\uffff] 49 | // \. 50 | // )* 51 | // (?: 52 | // // Domain zone: "com", "net", etc (required): 53 | // [a-z\u00a1-\uffff]{2,} 54 | // ) 55 | // ) 56 | // 57 | // // Matches a colon and a port number: 58 | // (?::\d{2,5})? 59 | // 60 | // // Matches everything after the "origin": 61 | // // * pathname 62 | // // * query 63 | // // * hash 64 | // (?:[/?#]\S*)? 65 | // 66 | // $/i 67 | 68 | const regexp = /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}))(?::\d{2,5})?(?:[/?#]\S*)?$/i 69 | 70 | // https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url 71 | export function isURL(value) { 72 | return regexp.test(value) 73 | } -------------------------------------------------------------------------------- /source/types/URL.test.js: -------------------------------------------------------------------------------- 1 | import { isURL } from './URL.js' 2 | 3 | describe('URL', () => { 4 | it('should validate a URL', () => { 5 | isURL('123').should.equal(false) 6 | isURL('https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url').should.equal(true) 7 | }) 8 | }) -------------------------------------------------------------------------------- /source/xml/dom.js: -------------------------------------------------------------------------------- 1 | export function findChild(node, tagName) { 2 | let i = 0 3 | while (i < node.childNodes.length) { 4 | const childNode = node.childNodes[i] 5 | // `nodeType: 1` means "Element". 6 | // https://www.w3schools.com/xml/prop_element_nodetype.asp 7 | if (childNode.nodeType === 1 && getTagName(childNode) === tagName) { 8 | return childNode 9 | } 10 | i++ 11 | } 12 | } 13 | 14 | export function findChildren(node, tagName) { 15 | const results = [] 16 | let i = 0 17 | while (i < node.childNodes.length) { 18 | const childNode = node.childNodes[i] 19 | // `nodeType: 1` means "Element". 20 | // https://www.w3schools.com/xml/prop_element_nodetype.asp 21 | if (childNode.nodeType === 1 && getTagName(childNode) === tagName) { 22 | results.push(childNode) 23 | } 24 | i++ 25 | } 26 | return results 27 | } 28 | 29 | export function forEach(node, tagName, func) { 30 | // if (typeof tagName === 'function') { 31 | // func = tagName 32 | // tagName = undefined 33 | // } 34 | let i = 0 35 | while (i < node.childNodes.length) { 36 | const childNode = node.childNodes[i] 37 | if (tagName) { 38 | // `nodeType: 1` means "Element". 39 | // https://www.w3schools.com/xml/prop_element_nodetype.asp 40 | if (childNode.nodeType === 1 && getTagName(childNode) === tagName) { 41 | func(childNode, i) 42 | } 43 | } else { 44 | func(childNode, i) 45 | } 46 | i++ 47 | } 48 | } 49 | 50 | export function map(node, tagName, func) { 51 | const results = [] 52 | forEach(node, tagName, (node, i) => { 53 | results.push(func(node, i)) 54 | }) 55 | return results 56 | } 57 | 58 | const NAMESPACE_REG_EXP = /.+\:/ 59 | export function getTagName(element) { 60 | // For some weird reason, if an element is declared as, 61 | // for example, ``, then its `.tagName` will be 62 | // "x:sheets" instead of just "sheets". 63 | // https://gitlab.com/catamphetamine/read-excel-file/-/issues/25 64 | // Its not clear how to tell it to ignore any namespaces 65 | // when getting `.tagName`, so just replacing anything 66 | // before a colon, if any. 67 | return element.tagName.replace(NAMESPACE_REG_EXP, '') 68 | } 69 | 70 | // This function is only used for occasional debug messages. 71 | export function getOuterXml(node) { 72 | // `nodeType: 1` means "Element". 73 | // https://www.w3schools.com/xml/prop_element_nodetype.asp 74 | if (node.nodeType !== 1) { 75 | return node.textContent 76 | } 77 | 78 | let xml = '<' + getTagName(node) 79 | 80 | let j = 0 81 | while (j < node.attributes.length) { 82 | xml += ' ' + node.attributes[j].name + '=' + '"' + node.attributes[j].value + '"' 83 | j++ 84 | } 85 | 86 | xml += '>' 87 | 88 | let i = 0 89 | while (i < node.childNodes.length) { 90 | xml += getOuterXml(node.childNodes[i]) 91 | i++ 92 | } 93 | 94 | xml += '' 95 | 96 | return xml 97 | } -------------------------------------------------------------------------------- /source/xml/xlsx.js: -------------------------------------------------------------------------------- 1 | import { findChild, findChildren, forEach, map, getTagName } from './dom.js' 2 | 3 | // Returns an array of cells, 4 | // each element being an XML DOM element representing a cell. 5 | export function getCells(document) { 6 | const worksheet = document.documentElement 7 | const sheetData = findChild(worksheet, 'sheetData') 8 | 9 | const cells = [] 10 | forEach(sheetData, 'row', (row) => { 11 | forEach(row, 'c', (cell) => { 12 | cells.push(cell) 13 | }) 14 | }) 15 | return cells 16 | } 17 | 18 | export function getMergedCells(document) { 19 | const worksheet = document.documentElement 20 | const mergedCells = findChild(worksheet, 'mergeCells') 21 | const mergedCellsInfo = [] 22 | if (mergedCells) { 23 | forEach(mergedCells, 'mergeCell', (mergedCell) => { 24 | mergedCellsInfo.push(mergedCell.getAttribute('ref')) 25 | }) 26 | } 27 | return mergedCellsInfo 28 | } 29 | 30 | export function getCellValue(document, node) { 31 | return findChild(node, 'v') 32 | } 33 | 34 | export function getCellInlineStringValue(document, node) { 35 | if ( 36 | node.firstChild && 37 | getTagName(node.firstChild) === 'is' && 38 | node.firstChild.firstChild && 39 | getTagName(node.firstChild.firstChild) === 't' 40 | ) { 41 | return node.firstChild.firstChild.textContent 42 | } 43 | } 44 | 45 | export function getDimensions(document) { 46 | const worksheet = document.documentElement 47 | const dimensions = findChild(worksheet, 'dimension') 48 | if (dimensions) { 49 | return dimensions.getAttribute('ref') 50 | } 51 | } 52 | 53 | export function getBaseStyles(document) { 54 | const styleSheet = document.documentElement 55 | const cellStyleXfs = findChild(styleSheet, 'cellStyleXfs') 56 | if (cellStyleXfs) { 57 | return findChildren(cellStyleXfs, 'xf') 58 | } 59 | return [] 60 | } 61 | 62 | export function getCellStyles(document) { 63 | const styleSheet = document.documentElement 64 | const cellXfs = findChild(styleSheet, 'cellXfs') 65 | if (!cellXfs) { 66 | return [] 67 | } 68 | return findChildren(cellXfs, 'xf') 69 | } 70 | 71 | export function getNumberFormats(document) { 72 | const styleSheet = document.documentElement 73 | let numberFormats = [] 74 | const numFmts = findChild(styleSheet, 'numFmts') 75 | if (numFmts) { 76 | return findChildren(numFmts, 'numFmt') 77 | } 78 | return [] 79 | } 80 | 81 | export function getSharedStrings(document) { 82 | // An `` element can contain a `` (simplest case) or a set of `` ("rich formatting") elements having ``. 83 | // https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sharedstringitem?redirectedfrom=MSDN&view=openxml-2.8.1 84 | // http://www.datypic.com/sc/ooxml/e-ssml_si-1.html 85 | 86 | const sst = document.documentElement 87 | return map(sst, 'si', string => { 88 | const t = findChild(string, 't') 89 | if (t) { 90 | return t.textContent 91 | } 92 | let value = '' 93 | forEach(string, 'r', (r) => { 94 | value += findChild(r, 't').textContent 95 | }) 96 | return value 97 | }) 98 | } 99 | 100 | export function getWorkbookProperties(document) { 101 | const workbook = document.documentElement 102 | return findChild(workbook, 'workbookPr') 103 | } 104 | 105 | export function getRelationships(document) { 106 | const relationships = document.documentElement 107 | return findChildren(relationships, 'Relationship') 108 | } 109 | 110 | export function getSheets(document) { 111 | const workbook = document.documentElement 112 | const sheets = findChild(workbook, 'sheets') 113 | return findChildren(sheets, 'sheet') 114 | } -------------------------------------------------------------------------------- /source/xml/xml.js: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom' 2 | 3 | export default { 4 | createDocument(content) { 5 | return new DOMParser().parseFromString(content) 6 | } 7 | } -------------------------------------------------------------------------------- /source/xml/xmlBrowser.js: -------------------------------------------------------------------------------- 1 | export default { 2 | createDocument(content) { 3 | // if (!content) { 4 | // throw new Error('No *.xml content') 5 | // } 6 | // A weird bug: it won't parse XML unless it's trimmed. 7 | // https://github.com/catamphetamine/read-excel-file/issues/21 8 | return new DOMParser().parseFromString(content.trim(), 'text/xml') 9 | } 10 | } -------------------------------------------------------------------------------- /source/xml/xpath/README.md: -------------------------------------------------------------------------------- 1 | `xlsx-xpath.js` is an "alternative" implementation of `./xml/xlsx.js` functions using the [`XPath`](https://www.w3schools.com/xml/xpath_syntax.asp) XML document query language. 2 | 3 | `XPath` is no longer used in this project and has been substituted with a simpler set of functions defined in `./xml/dom.js` that're used in `./xml/xlsx.js`. 4 | 5 | The reason is that `xpathBrowser.js` turned out to be [not supported](https://github.com/catamphetamine/read-excel-file/issues/26) in Internet Explorer 11, and including a [polyfill](https://www.npmjs.com/package/xpath) for `XPath` (`xpathNode.js`) would increase the bundle size by about 100 kilobytes. -------------------------------------------------------------------------------- /source/xml/xpath/xlsx-xpath.js: -------------------------------------------------------------------------------- 1 | // This file is no longer used. 2 | 3 | // Turns out IE11 doesn't support XPath, so not using `./xpathBrowser` for browsers. 4 | // https://github.com/catamphetamine/read-excel-file/issues/26 5 | // The inclusion of `xpath` package in `./xpathNode` 6 | // increases the bundle size by about 100 kilobytes. 7 | // IE11 is a wide-spread browser and it's unlikely that 8 | // anyone would ignore it for now. 9 | // There could be a separate export `read-excel-file/ie11` 10 | // for using `./xpathNode` instead of `./xpathBrowser` 11 | // but this library has been migrated to not using `xpath` anyway. 12 | // This code is just alternative/historical now, it seems. 13 | import xpath from './xpathNode' 14 | 15 | const namespaces = { 16 | a: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', 17 | // This one seems to be for `r:id` attributes on ``s. 18 | r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', 19 | // This one seems to be for `` file. 20 | rr: 'http://schemas.openxmlformats.org/package/2006/relationships' 21 | } 22 | 23 | export function getCells(document) { 24 | return xpath(document, null, '/a:worksheet/a:sheetData/a:row/a:c', namespaces) 25 | } 26 | 27 | export function getMergedCells(document) { 28 | return xpath(document, null, '/a:worksheet/a:mergedCells/a:mergedCell/@ref', namespaces) 29 | } 30 | 31 | export function getCellValue(document, node) { 32 | return xpath(document, node, './a:v', namespaces)[0] 33 | } 34 | 35 | export function getCellInlineStringValue(document, node) { 36 | return xpath(document, node, './a:is/a:t', namespaces)[0].textContent 37 | } 38 | 39 | export function getDimensions(document) { 40 | const dimensions = xpath(document, null, '/a:worksheet/a:dimension/@ref', namespaces)[0] 41 | if (dimensions) { 42 | return dimensions.textContent 43 | } 44 | } 45 | 46 | export function getBaseStyles(document) { 47 | return xpath(document, null, '/a:styleSheet/a:cellStyleXfs/a:xf', namespaces) 48 | } 49 | 50 | export function getCellStyles(document) { 51 | return xpath(document, null, '/a:styleSheet/a:cellXfs/a:xf', namespaces) 52 | } 53 | 54 | export function getNumberFormats(document) { 55 | return xpath(document, null, '/a:styleSheet/a:numFmts/a:numFmt', namespaces) 56 | } 57 | 58 | export function getSharedStrings(document) { 59 | // An `` element can contain a `` (simplest case) or a set of `` ("rich formatting") elements having ``. 60 | // https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sharedstringitem?redirectedfrom=MSDN&view=openxml-2.8.1 61 | // http://www.datypic.com/sc/ooxml/e-ssml_si-1.html 62 | 63 |   // The ".//a:t[not(ancestor::a:rPh)]" selector means: 64 |   // "select all `` that are not children of ``".  65 | // https://stackoverflow.com/questions/42773772/xpath-span-what-does-the-dot-mean 66 |   // `` seems to be some "phonetic data" added for languages like Japanese that should be ignored. 67 |   // https://github.com/doy/spreadsheet-parsexlsx/issues/72 68 |   return xpath(document, null, '/a:sst/a:si', namespaces) 69 |     .map(string => xpath(document, string, './/a:t[not(ancestor::a:rPh)]', namespaces) 70 |         .map(_ => _.textContent).join('') 71 |     ) 72 | } 73 | 74 | export function getWorkbookProperties(document) { 75 | return xpath(document, null, '/a:workbook/a:workbookPr', namespaces)[0] 76 | } 77 | 78 | export function getRelationships(document) { 79 | return xpath(document, null, '/rr:Relationships/rr:Relationship', namespaces) 80 | } 81 | 82 | export function getSheets(document) { 83 | return xpath(document, null, '/a:workbook/a:sheets/a:sheet', namespaces) 84 | } -------------------------------------------------------------------------------- /source/xml/xpath/xpathBrowser.js: -------------------------------------------------------------------------------- 1 | // This file is no longer used. 2 | 3 | // Turns out IE11 doesn't support XPath, so not using `./xpathBrowser` for browsers. 4 | // https://github.com/catamphetamine/read-excel-file/issues/26 5 | // The inclusion of `xpath` package in `./xpathNode` 6 | // increases the bundle size by about 100 kilobytes. 7 | // IE11 is a wide-spread browser and it's unlikely that 8 | // anyone would ignore it for now. 9 | // There could be a separate export `read-excel-file/ie11` 10 | // for using `./xpathNode` instead of `./xpathBrowser` 11 | // but this library has been migrated to not using `xpath` anyway. 12 | // This code is just alternative/historical now, it seems. 13 | export default function xpath(document, node, path, namespaces = {}) { 14 | const nodes = document.evaluate( 15 | path, 16 | node || document, 17 | prefix => namespaces[prefix], 18 | XPathResult.ANY_TYPE, 19 | null 20 | ) 21 | // Convert iterator to an array. 22 | const results = [] 23 | let result = nodes.iterateNext() 24 | while (result) { 25 | results.push(result) 26 | result = nodes.iterateNext() 27 | } 28 | return results 29 | } -------------------------------------------------------------------------------- /source/xml/xpath/xpathNode.js: -------------------------------------------------------------------------------- 1 | // This file is no longer used. 2 | 3 | import xpath from 'xpath' 4 | 5 | export default function(document, node, path, namespaces = {}) { 6 | const select = xpath.useNamespaces(namespaces) 7 | return select(path, node || document) 8 | } -------------------------------------------------------------------------------- /test/1904.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('1904', () => { 4 | it('should parse 1904 macOS dates', async () => { 5 | const data = await readXlsx('./test/spreadsheets/1904.xlsx') 6 | 7 | expect(data.length).to.equal(6) 8 | 9 | data[0][0].should.equal('Date') 10 | data[1][0].toISOString().should.equal('2018-05-05T00:00:00.000Z') 11 | data[2][0].toISOString().should.equal('2018-05-05T00:00:00.000Z') 12 | data[3][0].toISOString().should.equal('2018-05-05T00:00:00.000Z') 13 | data[4][0].toISOString().should.equal('2018-05-05T00:00:00.000Z') 14 | data[5][0].toISOString().should.equal('2018-05-05T00:00:00.000Z') 15 | }) 16 | 17 | it('should parse 1904 macOS dates', async () => { 18 | const data = await readXlsx('./test/spreadsheets/1904.xlsx', { 19 | schema: { 20 | Date: { 21 | type: Date, 22 | prop: 'date' 23 | } 24 | } 25 | }) 26 | 27 | data.errors.length.should.equal(0) 28 | data.rows.length.should.equal(5) 29 | data.rows[0].date.toISOString().should.equal('2018-05-05T00:00:00.000Z') 30 | data.rows[1].date.toISOString().should.equal('2018-05-05T00:00:00.000Z') 31 | data.rows[2].date.toISOString().should.equal('2018-05-05T00:00:00.000Z') 32 | data.rows[3].date.toISOString().should.equal('2018-05-05T00:00:00.000Z') 33 | data.rows[4].date.toISOString().should.equal('2018-05-05T00:00:00.000Z') 34 | }) 35 | 36 | it('should list sheet names in sheet not found error', async () => { 37 | // By id. 38 | try { 39 | await readXlsx('./test/spreadsheets/1904.xlsx', { sheet: 2 }) 40 | } catch (error) { 41 | error.message.should.equal('Sheet #2 not found in the *.xlsx file. Available sheets: "sheet 1" (#1).') 42 | } 43 | // By name. 44 | try { 45 | await readXlsx('./test/spreadsheets/1904.xlsx', { sheet: 'sheet 2' }) 46 | } catch (error) { 47 | error.message.should.equal('Sheet "sheet 2" not found in the *.xlsx file. Available sheets: "sheet 1" (#1).') 48 | } 49 | }) 50 | }) -------------------------------------------------------------------------------- /test/boolean.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('boolean', () => { 4 | it('should parse booleans', async () => { 5 | const data = await readXlsx('./test/spreadsheets/boolean.xlsx') 6 | 7 | expect(data).to.deep.equal([ 8 | ['Boolean'], 9 | [true], 10 | [false], 11 | [1], 12 | [0] 13 | ]) 14 | }) 15 | }) -------------------------------------------------------------------------------- /test/buffer.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import readXlsx from '../source/read/readXlsxFileNode.js' 4 | 5 | describe('buffer', () => { 6 | it('should read an excel file from a buffer', async () => { 7 | const spreadsheetContents = fs.readFileSync('./test/spreadsheets/inline-string.xlsx') 8 | 9 | const contentsBuffer = Buffer.from(spreadsheetContents) 10 | 11 | const data = await readXlsx(contentsBuffer) 12 | 13 | expect(data).to.deep.equal([ 14 | // ['String'], 15 | ['Test 123'] 16 | ]) 17 | }) 18 | 19 | it('should handle empty buffer input', async () => { 20 | expect(() => readXlsx(Buffer.alloc(0))).to.throw('No data') 21 | }) 22 | }) -------------------------------------------------------------------------------- /test/date.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('date', () => { 4 | it('should parse dates', async () => { 5 | const data = await readXlsx('./test/spreadsheets/date.xlsx') 6 | 7 | expect(data).to.deep.equal([ 8 | // ['Date'], 9 | [new Date('2021-06-10T00:47:45.700Z')] 10 | ]) 11 | }) 12 | }) -------------------------------------------------------------------------------- /test/exports.test.js: -------------------------------------------------------------------------------- 1 | import readXlsxFileBrowser, { parseExcelDate, readSheetNames } from '../index.js' 2 | import readXlsxFileNode, { parseExcelDate as parseExcelDateNode, readSheetNames as readSheetNamesNode } from '../node/index.js' 3 | import readXlsxFileWebWorker, { parseExcelDate as parseExcelDateWebWorker, readSheetNames as readSheetNamesWebWorker } from '../web-worker/index.js' 4 | 5 | import Read from '../index.cjs' 6 | import WebWorker from '../web-worker/index.cjs' 7 | import Node from '../node/index.cjs' 8 | 9 | describe(`exports`, () => { 10 | it(`should export ES6`, () => { 11 | // Browser 12 | readXlsxFileBrowser.should.be.a('function') 13 | parseExcelDate.should.be.a('function') 14 | readSheetNames.should.be.a('function') 15 | 16 | // Web Worker 17 | readXlsxFileWebWorker.should.be.a('function') 18 | parseExcelDateWebWorker.should.be.a('function') 19 | readSheetNamesWebWorker.should.be.a('function') 20 | 21 | // Node.js 22 | readXlsxFileNode.should.be.a('function') 23 | parseExcelDateNode.should.be.a('function') 24 | readSheetNamesNode.should.be.a('function') 25 | }) 26 | 27 | it(`should export CommonJS`, () => { 28 | // Browser 29 | 30 | Read.should.be.a('function') 31 | Read.default.should.be.a('function') 32 | Read.parseExcelDate.should.be.a('function') 33 | Read.readSheetNames.should.be.a('function') 34 | 35 | // Web Worker. 36 | 37 | WebWorker.should.be.a('function') 38 | WebWorker.default.should.be.a('function') 39 | WebWorker.parseExcelDate.should.be.a('function') 40 | WebWorker.readSheetNames.should.be.a('function') 41 | 42 | // Node.js 43 | 44 | Node.should.be.a('function') 45 | Node.default.should.be.a('function') 46 | Node.parseExcelDate.should.be.a('function') 47 | Node.readSheetNames.should.be.a('function') 48 | }) 49 | }) -------------------------------------------------------------------------------- /test/inline-string.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('inline string', () => { 4 | it('should parse inline strings', async () => { 5 | const data = await readXlsx('./test/spreadsheets/inline-string.xlsx') 6 | 7 | expect(data).to.deep.equal([ 8 | // ['String'], 9 | ['Test 123'] 10 | ]) 11 | }) 12 | }) -------------------------------------------------------------------------------- /test/merged-cells.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('merged cells', () => { 4 | it('should parse inline strings', async () => { 5 | const data = await readXlsx('./test/spreadsheets/merged-cells.xlsx') 6 | 7 | expect(data).to.deep.equal([ 8 | ['A1', 'B1', 'C1', 'D1'], 9 | ['A2', 'B2', 'C2', 'D2'] 10 | ]) 11 | }) 12 | }) -------------------------------------------------------------------------------- /test/nonAsciiCharacterEncoding.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('nonAsciiCharacterEncoding', () => { 4 | it('should correctly read non-ASCII characters', async () => { 5 | const data = await readXlsx('./test/spreadsheets/nonAsciiCharacterEncoding.xlsx') 6 | 7 | const row = data.find((row) => { 8 | return row[1] === 'Песчано-гравийные породы, строительный камень' && 9 | row[2] === '"К9, Даргинский", Амурский район'; 10 | }); 11 | 12 | expect(row[14]).to.equal('ООО "Транснефть-Дальний Восток, лицензия АМУ00432ТЭ') 13 | }) 14 | }) -------------------------------------------------------------------------------- /test/parseNumber.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import readXlsx from '../source/read/readXlsxFileNode.js' 4 | 5 | describe('read-excel-file', () => { 6 | it('should support custom `parseNumber` function', () => { 7 | const schema = { 8 | 'START DATE': { 9 | prop: 'date', 10 | type: Date 11 | }, 12 | 'NUMBER OF STUDENTS': { 13 | prop: 'numberOfStudents', 14 | type: Number, 15 | required: true 16 | }, 17 | 'COST': { 18 | prop: 'cost', 19 | type: (any) => any 20 | } 21 | } 22 | 23 | return readXlsx(path.resolve('./test/spreadsheets/course.xlsx'), { 24 | schema, 25 | parseNumber: (string) => string 26 | }).then(({ rows, errors }) => { 27 | rows[0].date = rows[0].date.getTime() 28 | rows.should.deep.equal([{ 29 | date: convertToUTCTimezone(new Date(2018, 2, 24)).getTime(), 30 | numberOfStudents: 123, 31 | cost: '210.45' 32 | }]) 33 | errors.should.deep.equal([]) 34 | }) 35 | }) 36 | }) 37 | 38 | // Converts timezone to UTC while preserving the same time 39 | function convertToUTCTimezone(date) { 40 | // Doesn't account for leap seconds but I guess that's ok 41 | // given that javascript's own `Date()` does not either. 42 | // https://www.timeanddate.com/time/leap-seconds-background.html 43 | // 44 | // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset 45 | // 46 | return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000) 47 | } 48 | -------------------------------------------------------------------------------- /test/requiredFunction.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import readXlsx from '../source/read/readXlsxFileNode.js' 4 | 5 | describe('read-excel-file', () => { 6 | it('should support `required` function (returns `true`)', () => { 7 | const schema = { 8 | 'COURSE TITLE': { 9 | prop: 'courseTitle', 10 | type: String 11 | }, 12 | 'NOT EXISTS': { 13 | prop: 'notExists', 14 | type: Number, 15 | required: (row) => row.courseTitle === 'Chemistry' 16 | } 17 | } 18 | 19 | return readXlsx(path.resolve('./test/spreadsheets/course.xlsx'), { 20 | schema 21 | }).then(({ rows, errors }) => { 22 | rows.should.deep.equal([{ 23 | courseTitle: 'Chemistry' 24 | }]) 25 | errors.should.deep.equal([{ 26 | error: 'required', 27 | row: 2, 28 | column: 'NOT EXISTS', 29 | value: undefined, 30 | // value: null, 31 | type: Number 32 | }]) 33 | }) 34 | }) 35 | 36 | it('should support `required` function (returns `false`)', () => { 37 | const schema = { 38 | 'COURSE TITLE': { 39 | prop: 'courseTitle', 40 | type: String 41 | }, 42 | 'NOT EXISTS': { 43 | prop: 'notExists', 44 | type: Number, 45 | required: (row) => row.courseTitle !== 'Chemistry' 46 | } 47 | } 48 | 49 | return readXlsx(path.resolve('./test/spreadsheets/course.xlsx'), { 50 | schema 51 | }).then(({ rows, errors }) => { 52 | rows.should.deep.equal([{ 53 | courseTitle: 'Chemistry' 54 | }]) 55 | errors.should.deep.equal([]) 56 | }) 57 | }) 58 | }) -------------------------------------------------------------------------------- /test/schemaEmptyRows.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import readXlsx from '../source/read/readXlsxFileNode.js' 4 | 5 | describe('read-excel-file', function() { 6 | it('should ignore empty rows by default', async function() { 7 | const rowMap = [] 8 | const { rows, errors } = await readXlsx(path.resolve('./test/spreadsheets/schemaEmptyRows.xlsx'), { schema, rowMap }) 9 | rows.should.deep.equal([{ 10 | date: new Date(Date.UTC(2018, 2, 24)), 11 | numberOfStudents: 123, 12 | course: { 13 | isFree: false, 14 | cost: 210.45, 15 | title: 'Chemistry' 16 | }, 17 | contact: '+11234567890' 18 | }, { 19 | date: new Date(Date.UTC(2018, 2, 24)), 20 | numberOfStudents: 123, 21 | course: { 22 | isFree: false, 23 | cost: 210.45, 24 | title: 'Chemistry' 25 | }, 26 | contact: '+11234567890' 27 | }]) 28 | errors.should.deep.equal([]) 29 | rowMap.should.deep.equal([0, 1, 2, 3]) 30 | }) 31 | 32 | it('should ignore empty rows by default (throws error)', async function() { 33 | const rowMap = [] 34 | const { rows, errors } = await readXlsx(path.resolve('./test/spreadsheets/schemaEmptyRows.xlsx'), { schema: schemaThrowsError, rowMap }) 35 | errors.should.deep.equal([{ 36 | error: 'invalid', 37 | reason: 'not_a_boolean', 38 | value: '(123) 456-7890', 39 | row: 2, 40 | column: 'CONTACT', 41 | type: Boolean 42 | }, { 43 | error: 'invalid', 44 | reason: 'not_a_boolean', 45 | value: '(123) 456-7890', 46 | row: 4, 47 | column: 'CONTACT', 48 | type: Boolean 49 | }]) 50 | rowMap.should.deep.equal([0, 1, 2, 3]) 51 | }) 52 | 53 | it('should not ignore empty rows when `ignoreEmptyRows: false` flag is passed', async function() { 54 | const rowMap = [] 55 | const { rows, errors } = await readXlsx(path.resolve('./test/spreadsheets/schemaEmptyRows.xlsx'), { schema, rowMap, ignoreEmptyRows: false }) 56 | rows.should.deep.equal([{ 57 | date: new Date(Date.UTC(2018, 2, 24)), 58 | numberOfStudents: 123, 59 | course: { 60 | isFree: false, 61 | cost: 210.45, 62 | title: 'Chemistry' 63 | }, 64 | contact: '+11234567890' 65 | }, null, { 66 | date: new Date(Date.UTC(2018, 2, 24)), 67 | numberOfStudents: 123, 68 | course: { 69 | isFree: false, 70 | cost: 210.45, 71 | title: 'Chemistry' 72 | }, 73 | contact: '+11234567890' 74 | }]) 75 | errors.should.deep.equal([]) 76 | rowMap.should.deep.equal([0, 1, 2, 3]) 77 | }) 78 | 79 | it('should not ignore empty rows when `ignoreEmptyRows: false` flag is passed (throws error)', async function() { 80 | const rowMap = [] 81 | const { rows, errors } = await readXlsx(path.resolve('./test/spreadsheets/schemaEmptyRows.xlsx'), { schema: schemaThrowsError, rowMap }) 82 | errors.should.deep.equal([{ 83 | error: 'invalid', 84 | reason: 'not_a_boolean', 85 | value: '(123) 456-7890', 86 | row: 2, 87 | column: 'CONTACT', 88 | type: Boolean 89 | }, { 90 | error: 'invalid', 91 | reason: 'not_a_boolean', 92 | value: '(123) 456-7890', 93 | row: 4, 94 | column: 'CONTACT', 95 | type: Boolean 96 | }]) 97 | rowMap.should.deep.equal([0, 1, 2, 3]) 98 | }) 99 | }) 100 | 101 | const schema = { 102 | 'START DATE': { 103 | prop: 'date', 104 | type: Date 105 | }, 106 | 'NUMBER OF STUDENTS': { 107 | prop: 'numberOfStudents', 108 | type: Number 109 | }, 110 | 'COURSE': { 111 | prop: 'course', 112 | type: { 113 | 'IS FREE': { 114 | prop: 'isFree', 115 | type: Boolean 116 | // Excel stored booleans as numbers: 117 | // `1` is `true` and `0` is `false`. 118 | // Such numbers are parsed to booleans. 119 | }, 120 | 'COST': { 121 | prop: 'cost', 122 | type: Number 123 | }, 124 | 'COURSE TITLE': { 125 | prop: 'title', 126 | type: String 127 | } 128 | } 129 | }, 130 | 'CONTACT': { 131 | prop: 'contact', 132 | type(value) { 133 | return '+11234567890' 134 | } 135 | } 136 | } 137 | 138 | const schemaThrowsError = { 139 | ...schema, 140 | 'CONTACT': { 141 | prop: 'contact', 142 | type: Boolean 143 | } 144 | } 145 | 146 | // Converts timezone to UTC while preserving the same time 147 | function convertToUTCTimezone(date) { 148 | // Doesn't account for leap seconds but I guess that's ok 149 | // given that javascript's own `Date()` does not either. 150 | // https://www.timeanddate.com/time/leap-seconds-background.html 151 | // 152 | // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset 153 | // 154 | return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000) 155 | } 156 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | 3 | global.expect = expect 4 | chai.should() -------------------------------------------------------------------------------- /test/sharedStrings.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('sharedStrings', () => { 4 | it('should parse sharedStrings (in case of ) and not include "phonetic" ', async () => { 5 | // Parsing `` in `sharedStrings.xml`. 6 | // https://github.com/doy/spreadsheet-parsexlsx/issues/72 7 | const data = await readXlsx('./test/spreadsheets/sharedStrings.r.t.xlsx') 8 | 9 | expect(data).to.deep.equal([ 10 | ['Str'] 11 | ]) 12 | }) 13 | }) -------------------------------------------------------------------------------- /test/sheet.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('sheet', () => { 4 | it('should read sheet by name (first)', async () => { 5 | const data = await readXlsx('./test/spreadsheets/multiple-sheets.xlsx', { sheet: 'sheet 1' }) 6 | expect(data.length).to.equal(1) 7 | data[0][0].should.equal('First sheet') 8 | }) 9 | 10 | it('should read sheet by name (second)', async () => { 11 | const data = await readXlsx('./test/spreadsheets/multiple-sheets.xlsx', { sheet: 'sheet 2' }) 12 | expect(data.length).to.equal(1) 13 | data[0][0].should.equal('Second sheet') 14 | }) 15 | 16 | it('should read sheet by index (first) (default)', async () => { 17 | const data = await readXlsx('./test/spreadsheets/multiple-sheets.xlsx') 18 | expect(data.length).to.equal(1) 19 | data[0][0].should.equal('First sheet') 20 | }) 21 | 22 | it('should read sheet by index (first)', async () => { 23 | const data = await readXlsx('./test/spreadsheets/multiple-sheets.xlsx', { sheet: 1 }) 24 | expect(data.length).to.equal(1) 25 | data[0][0].should.equal('First sheet') 26 | }) 27 | 28 | it('should read sheet by name (second)', async () => { 29 | const data = await readXlsx('./test/spreadsheets/multiple-sheets.xlsx', { sheet: 2 }) 30 | expect(data.length).to.equal(1) 31 | data[0][0].should.equal('Second sheet') 32 | }) 33 | 34 | it('should list sheets', async () => { 35 | const sheets = await readXlsx('./test/spreadsheets/multiple-sheets.xlsx', { getSheets: true }) 36 | expect(sheets).to.deep.equal([ 37 | { 38 | name: 'sheet 1' 39 | }, 40 | { 41 | name: 'sheet 2' 42 | } 43 | ]) 44 | }) 45 | }) -------------------------------------------------------------------------------- /test/spreadsheets/1904.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/1904.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/boolean.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/boolean.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/course.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/course.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/date.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/date.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/excel_mac_2011-basic.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/excel_mac_2011-basic.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/excel_mac_2011-formatting.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/excel_mac_2011-formatting.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/excel_multiple_text_nodes.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/excel_multiple_text_nodes.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/inline-string.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/inline-string.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/merged-cells.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/merged-cells.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/multiple-sheets.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/multiple-sheets.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/nonAsciiCharacterEncoding.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/nonAsciiCharacterEncoding.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/schemaEmptyRows.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/schemaEmptyRows.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/sharedStrings.r.t.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/sharedStrings.r.t.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/string-formula.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/string-formula.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/trim.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/trim.xlsx -------------------------------------------------------------------------------- /test/spreadsheets/workbook-xml-namespace.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/read-excel-file/fac18e6d28d66be30043ce435f2d7ae8d4214e99/test/spreadsheets/workbook-xml-namespace.xlsx -------------------------------------------------------------------------------- /test/string-formula.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('string formula', () => { 4 | it('should return of string cells having a formula', async () => { 5 | const data = await readXlsx('./test/spreadsheets/string-formula.xlsx') 6 | expect(data.length).to.equal(7) 7 | data[4][2].should.equal('Value2') 8 | data[4][3].should.equal('Value3') 9 | // The empty row with index `5` is preserved. 10 | data[6][1].should.equal('Value2Value3') 11 | // Just a check for numeric formula alue. 12 | data[6][2].should.equal(0.909297426825682) 13 | }) 14 | }) -------------------------------------------------------------------------------- /test/test.test.js: -------------------------------------------------------------------------------- 1 | import parseXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | const sheetsDir = './test/spreadsheets' 4 | 5 | const sheets = { 6 | 'excel_mac_2011-basic.xlsx': [ [ 'One', 'Two' ], [ 'Three', 'Four' ] ], 7 | 'excel_mac_2011-formatting.xlsx': [ [ 'Hey', 'now', 'so' ], [ 'cool', null, null ] ], 8 | 'excel_multiple_text_nodes.xlsx': [ [ 'id', 'memo' ], [ 1, 'abc def ghi' ], [ 2, 'pqr stu' ] ] 9 | } 10 | 11 | describe('read-excel-file', function() { 12 | for (const filename in sheets) { 13 | // Creates a javascript "closure". 14 | // Otherwise, in every test, `expected` variable value would be equal 15 | // to the last `for` cycle's `expected` variable value. 16 | (function(filename, expected) { 17 | describe(filename + ' basic test', function() { 18 | it('should return the right value', async function() { 19 | const result = await parseXlsx(sheetsDir + '/' + filename) 20 | expect(result).to.deep.equal(expected) 21 | }) 22 | it('should return the right value with the sheet specified', async function() { 23 | const result = await parseXlsx(sheetsDir + '/' + filename, '1') 24 | expect(result).to.deep.equal(expected) 25 | }) 26 | }) 27 | })(filename, sheets[filename]) 28 | } 29 | }) -------------------------------------------------------------------------------- /test/trim.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | describe('inline string', () => { 4 | it('should parse inline strings', async () => { 5 | const data = await readXlsx('./test/spreadsheets/trim.xlsx', { trim: false }) 6 | 7 | expect(data).to.deep.equal([ 8 | [' text '] 9 | ]) 10 | }) 11 | }) -------------------------------------------------------------------------------- /test/workbook-xml-namespace.test.js: -------------------------------------------------------------------------------- 1 | import readXlsx from '../source/read/readXlsxFileNode.js' 2 | 3 | // https://gitlab.com/catamphetamine/read-excel-file/-/issues/25 4 | describe('workbook.xml:namespace', () => { 5 | it('should parse *.xlsx files where workbook.xml content tags have a namespace', async () => { 6 | const data = await readXlsx('./test/spreadsheets/workbook-xml-namespace.xlsx') 7 | 8 | expect(data[0][0]).to.equal('Phrase') 9 | }) 10 | }) -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export function Integer(): void; 2 | export function URL(): void; 3 | export function Email(): void; 4 | 5 | export type CellValue = string | number | boolean | typeof Date 6 | export type Row = CellValue[] 7 | 8 | type BasicType = 9 | | string 10 | | number 11 | | boolean 12 | | typeof Date 13 | | typeof Integer 14 | | typeof URL 15 | | typeof Email; 16 | 17 | // A cell "type" is a function that receives a "raw" value and returns a "parsed" value or `undefined`. 18 | export type Type = (value: CellValue) => ParsedValue | undefined; 19 | 20 | type SchemaEntryRequiredProperty = boolean | ((row: Object) => boolean); 21 | 22 | interface SchemaEntryForValue { 23 | prop: Key; 24 | type?: BasicType | Type; 25 | oneOf?: Object[Key][]; 26 | required?: SchemaEntryRequiredProperty; 27 | validate?(value: Object[Key]): void; 28 | } 29 | 30 | // Legacy versions of this library supported supplying a custom `parse()` function. 31 | // Since then, the `parse()` function has been renamed to `type()` function. 32 | interface SchemaEntryForValueLegacy { 33 | prop: Key; 34 | parse: (value: CellValue) => Object[Key] | undefined; 35 | oneOf?: Object[Key][]; 36 | required?: SchemaEntryRequiredProperty; 37 | validate?(value: Object[Key]): void; 38 | } 39 | 40 | // Implementing recursive types in TypeScript: 41 | // https://dev.to/busypeoples/notes-on-typescript-recursive-types-and-immutability-5ck1 42 | interface SchemaEntryRecursive { 43 | prop: Key; 44 | type: Record>; 45 | required?: SchemaEntryRequiredProperty; 46 | } 47 | 48 | type SchemaEntry = 49 | SchemaEntryForValue | 50 | SchemaEntryForValueLegacy | 51 | SchemaEntryRecursive 52 | 53 | export type Schema, ColumnTitle extends string = string> = Record> 54 | 55 | export interface Error { 56 | error: string; 57 | reason?: string; 58 | row: number; 59 | column: string; 60 | value?: CellValue_; 61 | type?: Type; 62 | } 63 | 64 | export interface ParsedObjectsResult { 65 | rows: Object[]; 66 | errors: Error[]; 67 | } 68 | 69 | interface ParseCommonOptions { 70 | sheet?: number | string; 71 | trim?: boolean; 72 | parseNumber?: (string: string) => any; 73 | } 74 | 75 | export interface ParseWithSchemaOptions extends ParseCommonOptions, MappingParametersReadExcelFile { 76 | schema: Schema; 77 | transformData?: (rows: Row[]) => Row[]; 78 | ignoreEmptyRows?: boolean; 79 | // `includeNullValues: true` parameter is deprecated. 80 | // It could be replaced with the following combination of parameters: 81 | // * `schemaPropertyValueForMissingColumn: null` 82 | // * `schemaPropertyValueForEmptyCell: null` 83 | // * `getEmptyObjectValue = () => null` 84 | includeNullValues?: boolean; 85 | } 86 | 87 | type MapProperty = string; 88 | type MapObject = { 89 | [key: string]: MapProperty | MapObject; 90 | }; 91 | type Map = MapObject; 92 | 93 | export interface ParseWithMapOptions extends ParseCommonOptions { 94 | map: Map; 95 | transformData?: (rows: Row[]) => Row[]; 96 | dateFormat?: string; 97 | } 98 | 99 | export interface ParseWithoutSchemaOptions extends ParseCommonOptions { 100 | dateFormat?: string; 101 | } 102 | 103 | interface MappingParametersCommon { 104 | schemaPropertyValueForMissingColumn?: any; 105 | schemaPropertyShouldSkipRequiredValidationForMissingColumn?(column: string, parameters: { object: Record }): boolean; 106 | getEmptyObjectValue?(object: Record, parameters: { path?: string }): any; 107 | getEmptyArrayValue?(array: any[], parameters: { path: string }): any; 108 | } 109 | 110 | interface MappingParametersReadExcelFile extends MappingParametersCommon { 111 | schemaPropertyValueForEmptyCell?: null | undefined; 112 | } 113 | 114 | export interface MappingParameters extends MappingParametersCommon { 115 | schemaPropertyValueForUndefinedCellValue?: any; 116 | schemaPropertyValueForNullCellValue?: any; 117 | isColumnOriented?: boolean; 118 | rowIndexMap?: Record; 119 | } -------------------------------------------------------------------------------- /web-worker/index.cjs: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('../commonjs/read/readXlsxFileWebWorker.js').default 2 | exports['default'] = require('../commonjs/read/readXlsxFileWebWorker.js').default 3 | exports.readSheetNames = require('../commonjs/read/readSheetNamesWebWorker.js').default 4 | exports.parseExcelDate = require('../commonjs/read/parseDate.js').default 5 | exports.Integer = require('../commonjs/types/Integer.js').default 6 | exports.Email = require('../commonjs/types/Email.js').default 7 | exports.URL = require('../commonjs/types/URL.js').default -------------------------------------------------------------------------------- /web-worker/index.cjs.js: -------------------------------------------------------------------------------- 1 | // This file is deprecated. 2 | // It's the same as `index.cjs`, just with an added `*.js` extension. 3 | // It fixes the issue when some software doesn't see files with `*.cjs` file extensions 4 | // when used as the `main` property value in `package.json`. 5 | 6 | exports = module.exports = require('../commonjs/read/readXlsxFileWebWorker.js').default 7 | exports['default'] = require('../commonjs/read/readXlsxFileWebWorker.js').default 8 | exports.readSheetNames = require('../commonjs/read/readSheetNamesWebWorker.js').default 9 | exports.parseExcelDate = require('../commonjs/read/parseDate.js').default 10 | exports.Integer = require('../commonjs/types/Integer.js').default 11 | exports.Email = require('../commonjs/types/Email.js').default 12 | exports.URL = require('../commonjs/types/URL.js').default -------------------------------------------------------------------------------- /web-worker/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ParseWithSchemaOptions, 3 | ParseWithMapOptions, 4 | ParseWithoutSchemaOptions, 5 | ParsedObjectsResult, 6 | Row 7 | } from '../types.d.js'; 8 | 9 | export { 10 | Schema, 11 | ParsedObjectsResult, 12 | Error, 13 | CellValue, 14 | Row, 15 | Integer, 16 | Email, 17 | URL 18 | } from '../types.d.js'; 19 | 20 | export function parseExcelDate(excelSerialDate: number) : typeof Date; 21 | 22 | type Input = File | Blob | ArrayBuffer; 23 | 24 | export function readXlsxFile(input: Input, options: ParseWithSchemaOptions) : Promise>; 25 | export function readXlsxFile(input: Input, options: ParseWithMapOptions) : Promise>; 26 | export function readXlsxFile(input: Input, options?: ParseWithoutSchemaOptions) : Promise; 27 | 28 | export function readSheetNames(input: Input) : Promise; 29 | 30 | export default readXlsxFile; -------------------------------------------------------------------------------- /web-worker/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from '../modules/read/readXlsxFileWebWorker.js' 2 | export { default as readSheetNames } from '../modules/read/readSheetNamesWebWorker.js' 3 | export { default as parseExcelDate } from '../modules/read/parseDate.js' 4 | export { default as Integer } from '../modules/types/Integer.js' 5 | export { default as Email } from '../modules/types/Email.js' 6 | export { default as URL } from '../modules/types/URL.js' 7 | -------------------------------------------------------------------------------- /web-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "read-excel-file/web-worker", 4 | "version": "1.0.0", 5 | "main": "index.cjs", 6 | "module": "index.js", 7 | "types": "./index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./index.d.ts", 12 | "import": "./index.js", 13 | "require": "./index.cjs" 14 | } 15 | }, 16 | "sideEffects": false 17 | } 18 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | read-excel-file 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | read-excel-file 166 | 167 | 168 |
169 | 170 |
171 | Read small to medium *.xlsx files in a browser or Node.js. 172 |
173 |
174 | Parse file data to an array of JSON objects using a schema. 175 |
176 | 177 |
178 | 179 | 182 | 183 | 184 |
185 |
186 |
187 | 188 |
189 |
190 | 191 |
192 |

193 | File Data 194 |

195 | 196 |
197 | Also supports parsing file data to an array of JSON objects using a schema. Read more 198 |
199 | 200 |
201 | 202 |
203 | 204 |
205 |
206 | 207 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /website/lib/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.23.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML 5 | * Based on https://github.com/chriskempson/tomorrow-theme 6 | * @author Rose Pritchard 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: #ccc; 12 | background: none; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | font-size: 1em; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*="language-"] { 35 | padding: 1em; 36 | margin: .5em 0; 37 | overflow: auto; 38 | } 39 | 40 | :not(pre) > code[class*="language-"], 41 | pre[class*="language-"] { 42 | background: #2d2d2d; 43 | } 44 | 45 | /* Inline code */ 46 | :not(pre) > code[class*="language-"] { 47 | padding: .1em; 48 | border-radius: .3em; 49 | white-space: normal; 50 | } 51 | 52 | .token.comment, 53 | .token.block-comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: #999; 58 | } 59 | 60 | .token.punctuation { 61 | color: #ccc; 62 | } 63 | 64 | .token.tag, 65 | .token.attr-name, 66 | .token.namespace, 67 | .token.deleted { 68 | color: #e2777a; 69 | } 70 | 71 | .token.function-name { 72 | color: #6196cc; 73 | } 74 | 75 | .token.boolean, 76 | .token.number, 77 | .token.function { 78 | color: #f08d49; 79 | } 80 | 81 | .token.property, 82 | .token.class-name, 83 | .token.constant, 84 | .token.symbol { 85 | color: #f8c555; 86 | } 87 | 88 | .token.selector, 89 | .token.important, 90 | .token.atrule, 91 | .token.keyword, 92 | .token.builtin { 93 | color: #cc99cd; 94 | } 95 | 96 | .token.string, 97 | .token.char, 98 | .token.attr-value, 99 | .token.regex, 100 | .token.variable { 101 | color: #7ec699; 102 | } 103 | 104 | .token.operator, 105 | .token.entity, 106 | .token.url { 107 | color: #67cdcc; 108 | } 109 | 110 | .token.important, 111 | .token.bold { 112 | font-weight: bold; 113 | } 114 | .token.italic { 115 | font-style: italic; 116 | } 117 | 118 | .token.entity { 119 | cursor: help; 120 | } 121 | 122 | .token.inserted { 123 | color: green; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /website/lib/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.23.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,e={},M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=l.reach);y+=m.value.length,m=m.next){var b=m.value;if(t.length>n.length)return;if(!(b instanceof W)){var k,x=1;if(h){if(!(k=z(v,y,n,f)))break;var w=k.index,A=k.index+k[0].length,P=y;for(P+=m.value.length;P<=w;)m=m.next,P+=m.value.length;if(P-=m.value.length,y=P,m.value instanceof W)continue;for(var E=m;E!==t.tail&&(Pl.reach&&(l.reach=N);var j=m.prev;O&&(j=I(t,j,O),y+=O.length),q(t,j,x);var C=new W(o,g?M.tokenize(S,g):S,d,S);if(m=I(t,j,C),L&&I(t,m,L),1l.reach&&(l.reach=_.reach)}}}}}}(e,a,n,a.head,0),function(e){var n=[],t=e.head.next;for(;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=M.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=M.hooks.all[e];if(t&&t.length)for(var r,a=0;r=t[a++];)r(n)}},Token:W};function W(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function z(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function i(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function I(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function q(e,n,t){for(var r=n.next,a=0;a"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var t=M.util.currentScript();function r(){M.manual||M.highlightAll()}if(t&&(M.filename=t.src,t.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var a=document.readyState;"loading"===a||"interactive"===a&&t&&t.defer?document.addEventListener("DOMContentLoaded",r):window.requestAnimationFrame?window.requestAnimationFrame(r):window.setTimeout(r,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/,name:/[^\s<>'"]+/}},cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,function(){return a}),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; 5 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; 8 | -------------------------------------------------------------------------------- /website/lib/promise-polyfill.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";function e(e){var t=this.constructor;return this.then(function(n){return t.resolve(e()).then(function(){return n})},function(n){return t.resolve(e()).then(function(){return t.reject(n)})})}function t(e){return new this(function(t,n){function o(e,n){if(n&&("object"==typeof n||"function"==typeof n)){var f=n.then;if("function"==typeof f)return void f.call(n,function(t){o(e,t)},function(n){r[e]={status:"rejected",reason:n},0==--i&&t(r)})}r[e]={status:"fulfilled",value:n},0==--i&&t(r)}if(!e||"undefined"==typeof e.length)return n(new TypeError(typeof e+" "+e+" is not iterable(cannot read property Symbol(Symbol.iterator))"));var r=Array.prototype.slice.call(e);if(0===r.length)return t([]);for(var i=r.length,f=0;r.length>f;f++)o(f,r[f])})}function n(e){return!(!e||"undefined"==typeof e.length)}function o(){}function r(e){if(!(this instanceof r))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],l(e,this)}function i(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,r._immediateFn(function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var o;try{o=n(e._value)}catch(r){return void u(t.promise,r)}f(t.promise,o)}else(1===e._state?f:u)(t.promise,e._value)})):e._deferreds.push(t)}function f(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof r)return e._state=3,e._value=t,void c(e);if("function"==typeof n)return void l(function(e,t){return function(){e.apply(t,arguments)}}(n,t),e)}e._state=1,e._value=t,c(e)}catch(o){u(e,o)}}function u(e,t){e._state=2,e._value=t,c(e)}function c(e){2===e._state&&0===e._deferreds.length&&r._immediateFn(function(){e._handled||r._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;n>t;t++)i(e,e._deferreds[t]);e._deferreds=null}function l(e,t){var n=!1;try{e(function(e){n||(n=!0,f(t,e))},function(e){n||(n=!0,u(t,e))})}catch(o){if(n)return;n=!0,u(t,o)}}var a=setTimeout;r.prototype["catch"]=function(e){return this.then(null,e)},r.prototype.then=function(e,t){var n=new this.constructor(o);return i(this,new function(e,t,n){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof t?t:null,this.promise=n}(e,t,n)),n},r.prototype["finally"]=e,r.all=function(e){return new r(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(t){r(e,t)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},r.allSettled=t,r.resolve=function(e){return e&&"object"==typeof e&&e.constructor===r?e:new r(function(t){t(e)})},r.reject=function(e){return new r(function(t,n){n(e)})},r.race=function(e){return new r(function(t,o){if(!n(e))return o(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)r.resolve(e[i]).then(t,o)})},r._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},r._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var s=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"function"!=typeof s.Promise?s.Promise=r:s.Promise.prototype["finally"]?s.Promise.allSettled||(s.Promise.allSettled=t):s.Promise.prototype["finally"]=e}); 2 | --------------------------------------------------------------------------------