├── .babelrc ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── changelog.txt ├── package.json ├── sample.js ├── sampleFiles ├── invoiceData.json ├── logo.png ├── thumbs-up.jpg └── thumbsUp.jpg ├── source ├── index.js └── lib │ ├── cell │ ├── cell.js │ └── index.js │ ├── classes │ ├── comment.js │ ├── ctMarker.js │ ├── definedNameCollection.js │ ├── emu.js │ └── point.js │ ├── column │ ├── column.js │ └── index.js │ ├── drawing │ ├── drawing.js │ ├── index.js │ └── picture.js │ ├── logger.js │ ├── row │ ├── index.js │ └── row.js │ ├── style │ ├── classes │ │ ├── alignment.js │ │ ├── border.js │ │ ├── ctColor.js │ │ ├── fill.js │ │ ├── font.js │ │ └── numberFormat.js │ ├── index.js │ └── style.js │ ├── types │ ├── alignment.js │ ├── borderStyle.js │ ├── cellComment.js │ ├── colorScheme.js │ ├── excelColor.js │ ├── fillPattern.js │ ├── fontFamily.js │ ├── index.js │ ├── orientation.js │ ├── pageOrder.js │ ├── pane.js │ ├── paneState.js │ ├── paperSize.js │ ├── positiveUniversalMeasure.js │ └── printError.js │ ├── utils.js │ ├── workbook │ ├── builder.js │ ├── dxfCollection.js │ ├── index.js │ ├── mediaCollection.js │ └── workbook.js │ └── worksheet │ ├── builder.js │ ├── cf │ ├── cf_rule.js │ ├── cf_rule_types.js │ └── cf_rules_collection.js │ ├── classes │ ├── dataValidation.js │ └── hyperlink.js │ ├── index.js │ ├── optsValidator.js │ ├── sheet_default_params.js │ └── worksheet.js ├── tests ├── cell.test.js ├── cf_rule.test.js ├── column.test.js ├── dataValidations.test.js ├── hyperlink.test.js ├── image.test.js ├── library.test.js ├── row.test.js ├── style.test.js ├── unicodestring.test.js ├── workbook.test.js └── worksheet.test.js └── validate.sh /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "env": { 4 | "test": { 5 | "plugins": ["istanbul"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | 'rules': { 7 | // TODO add a logger lib to the project 8 | // 'no-console': 2 9 | 'brace-style': [2, '1tbs'], 10 | 'camelcase': 1, 11 | 'comma-dangle': [2, 'never'], 12 | 'comma-spacing': [2, { 'before': false, 'after': true }], 13 | 'comma-style': [2, 'last'], 14 | 'eqeqeq': 2, 15 | 'indent': [2, 4], 16 | 'key-spacing': [2, { 'beforeColon': false, 'afterColon': true }], 17 | 'keyword-spacing': 2, 18 | 'linebreak-style': [2, 'unix'], 19 | 'no-case-declarations': 0, 20 | 'no-console': 0, 21 | 'no-redeclare': 0, 22 | 'no-underscore-dangle': 0, 23 | 'no-unused-vars': 0, 24 | 'object-curly-spacing': [2, 'always'], 25 | 'quotes': [2, 'single'], 26 | 'semi': [2, 'always'], 27 | 'semi-spacing': [2, { 'before': false, 'after': true }], 28 | 'space-before-blocks': [2, 'always'], 29 | 'space-before-function-paren': [2, { 'anonymous': 'always', 'named': 'never' }], 30 | 'space-in-parens': [2, 'never'], 31 | 'space-infix-ops': 2, 32 | 'strict': 0 33 | }, 34 | 'env': { 35 | 'node': true, 36 | 'es6': true 37 | }, 38 | 'extends': 'eslint:recommended' 39 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Add link to [gist](https://gist.github.com) that will replicate issue here 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Environment (please complete the following information):** 17 | - OS: [e.g. Ubuntu 18.04] 18 | - Node Version: [e.g. 8.11.14] 19 | - excel4node Version: [e.g. 1.5.0] 20 | - Application: [e.g. LibreOffice, Microsoft Excel, Office 365 Online, Google Sheets] 21 | - Application Version (if applicable): [e.g. [check version](https://support.office.com/en-us/article/about-office-what-version-of-office-am-i-using-932788b8-a3ce-44bf-bb09-e334518b8b19)] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. Log entries related to the issue are good things. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | distribution 4 | .DS_Store 5 | npm-debug.log 6 | Excel.xlsx 7 | dev.js 8 | dev.xlsx 9 | dev 10 | docs 11 | output 12 | tmp 13 | references 14 | coverage 15 | .nyc_output 16 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .nyc_output 3 | .npmrc 4 | .prettierrc 5 | .eslintrc.js 6 | .babelrc 7 | .travis.yml 8 | coverage 9 | docs 10 | references 11 | npm-debug.log 12 | output 13 | tmp 14 | *.xlsx -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": false 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | - "5" 7 | - "4" 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | By participating in this project, you 4 | agree to abide by the thoughtbot [code of conduct]. 5 | 6 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 7 | 8 | - Fork, then clone the repo: 9 | 10 | ``` 11 | git clone git@github.com:your-username/excel4node.git 12 | ``` 13 | - Install package dependencies 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | - Make sure the tests pass: 20 | 21 | ``` 22 | npm run test 23 | ``` 24 | 25 | - Make your change. Add tests for your change. Make the tests pass: 26 | 27 | ``` 28 | npm run test 29 | ``` 30 | 31 | - Validate generated sample Excel workbook against the xlsx-validator 32 | 33 | __This requires Docker be installed on your system to run the xlsx-validator Docker image__ 34 | 35 | ``` 36 | npm run build 37 | node sample.js 38 | ./validate.sh Excel.xlsx 39 | ``` 40 | 41 | - All library code is contained in the source directory. Running 'npm run watch' will start a babel watch process and transpile output to the distribution directory. 42 | 43 | - Document your change in code using [jsdoc] conventions 44 | - Update the README.md file with instructions on how how use your change 45 | 46 | - Push to your fork and [submit a pull request][pr]. 47 | 48 | 49 | Please follow the [style guide][style]. 50 | 51 | [pr]: https://github.com/natergj/excel4node/compare 52 | [style]: https://github.com/natergj/excel4node/blob/master/.eslintrc.js 53 | [jsdoc]: http://usejsdoc.org/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 nater@iamnater.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://img.shields.io/npm/v/excel4node.svg)](https://www.npmjs.com/package/excel4node) 2 | [![License](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) 3 | [![npm](https://img.shields.io/npm/dt/excel4node.svg)](https://www.npmjs.com/package/excel4node) 4 | [![node](https://img.shields.io/node/v/excel4node.svg)](https://nodejs.org/en/download/) 5 | [![Build Status](https://travis-ci.org/natergj/excel4node.svg?branch=master)](https://travis-ci.org/natergj/excel4node) 6 | [![dependencies Status](https://david-dm.org/natergj/excel4node/status.svg)](https://david-dm.org/natergj/excel4node) 7 | [![devDependency Status](https://david-dm.org/natergj/excel4node/dev-status.svg)](https://david-dm.org/natergj/excel4node#info=devDependencies) 8 | 9 | # excel4node 10 | 11 | ## Note on this library 12 | 13 | I started this library back in 2014 as a side project to fulfill a need for a 14 | project I was working on where no other solution existed. After I was finished 15 | being a part of that project I tried to continue to maintain this library, but 16 | life circumstances change and I have accepted that I do not have the time 17 | available to maintain this library at the level it needs. 18 | 19 | I am grateful for the devs at avisr.io who have agreed to take ownership of the 20 | [excel4node package on NPM](https://www.npmjs.com/package/excel4node) and all 21 | future work will occur on their fork at 22 | [advisr-io/excel4node](https://github.com/advisr-io/excel4node). 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "excel4node", 3 | "version": "1.7.2", 4 | "description": "Library to create Formatted Excel Files.", 5 | "engines": { 6 | "node": ">4.0.0" 7 | }, 8 | "keywords": [ 9 | "excel", 10 | "spreadsheet", 11 | "xlsx", 12 | "formatted", 13 | "styled", 14 | "report", 15 | "workbook", 16 | "ooxml" 17 | ], 18 | "main": "./distribution/index.js", 19 | "author": { 20 | "name": "Nater", 21 | "email": "nater@iamnater.com" 22 | }, 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/natergj/excel4node.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/natergj/excel4node/labels/bug" 30 | }, 31 | "scripts": { 32 | "test": "NODE_ENV=test ./node_modules/tape/bin/tape -r babel-register ./tests/*.test.js", 33 | "cover": "NODE_ENV=test nyc tape -r babel-register ./tests/*.test.js", 34 | "build": "./node_modules/babel-cli/bin/babel.js source --presets babel-preset-env -s --out-dir distribution", 35 | "watch": "./node_modules/babel-cli/bin/babel.js source -w --presets babel-preset-env -s --out-dir distribution", 36 | "document": "jsdoc ./source -r -d docs", 37 | "prepublish": "npm run build; npm run test" 38 | }, 39 | "dependencies": { 40 | "deepmerge": "3.2.0", 41 | "image-size": "0.7.2", 42 | "jszip": "3.2.1", 43 | "lodash.get": "4.4.2", 44 | "lodash.isequal": "4.5.0", 45 | "lodash.isundefined": "3.0.1", 46 | "lodash.reduce": "4.6.0", 47 | "lodash.uniqueid": "4.0.1", 48 | "mime": "2.4.0", 49 | "uuid": "3.3.2", 50 | "xmlbuilder": "11.0.1" 51 | }, 52 | "devDependencies": { 53 | "babel-cli": "6.26.0", 54 | "babel-plugin-istanbul": "4.1.6", 55 | "babel-preset-env": "1.7.0", 56 | "babel-register": "6.26.0", 57 | "jsdoc": "3.5.5", 58 | "nyc": "12.0.2", 59 | "source-map-support": "0.5.11", 60 | "tape": "4.10.1", 61 | "tape-promise": "2.0.1", 62 | "xmldom": "0.1.27", 63 | "xpath.js": "1.1.0" 64 | }, 65 | "nyc": { 66 | "instrument": false, 67 | "sourceMap": false, 68 | "reporter": [ 69 | "text-summary", 70 | "html" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /sampleFiles/invoiceData.json: -------------------------------------------------------------------------------- 1 | { 2 | "items" : [ 3 | { 4 | "description": "material", 5 | "unitCost": 500, 6 | "units": 2 7 | }, 8 | { 9 | "description": "labor", 10 | "unitCost": 150, 11 | "units": 4 12 | } 13 | ], 14 | "company": "iAmNater.com", 15 | "logoFile": "logo.png" 16 | } -------------------------------------------------------------------------------- /sampleFiles/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natergj/excel4node/4d560c6be37e89a3fe7486db0105d1ba17596a34/sampleFiles/logo.png -------------------------------------------------------------------------------- /sampleFiles/thumbs-up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natergj/excel4node/4d560c6be37e89a3fe7486db0105d1ba17596a34/sampleFiles/thumbs-up.jpg -------------------------------------------------------------------------------- /sampleFiles/thumbsUp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natergj/excel4node/4d560c6be37e89a3fe7486db0105d1ba17596a34/sampleFiles/thumbsUp.jpg -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | /* REFERENCES 2 | http://www.ecma-international.org/news/TC45_current_work/OpenXML%20White%20Paper.pdf 3 | http://www.ecma-international.org/publications/standards/Ecma-376.htm 4 | http://www.openoffice.org/sc/excelfileformat.pdf 5 | http://officeopenxml.com/anatomyofOOXML-xlsx.php 6 | */ 7 | 8 | /* 9 | Code references specifications sections from ECMA-376 2nd edition doc 10 | ECMA-376, Second Edition, Part 1 - Fundamentals And Markup Language Reference.pdf 11 | found in ECMA-376 2nd edition Part 1 download at http://www.ecma-international.org/publications/standards/Ecma-376.htm 12 | Sections are referenced in code comments with § 13 | */ 14 | 15 | const utils = require('./lib/utils.js'); 16 | const types = require('./lib/types/index.js'); 17 | 18 | module.exports = { 19 | Workbook: require('./lib/workbook/index.js'), 20 | getExcelRowCol: utils.getExcelRowCol, 21 | getExcelAlpha: utils.getExcelAlpha, 22 | getExcelTS: utils.getExcelTS, 23 | getExcelCellRef: utils.getExcelCellRef, 24 | PaperSize: types.paperSize, 25 | CellComment: types.cellComments, 26 | PrintError: types.printError, 27 | PageOrder: types.pageOrder, 28 | Orientation: types.orientation, 29 | Pane: types.pane, 30 | PaneState: types.paneState, 31 | HorizontalAlignment: types.alignment.horizontal, 32 | VerticalAlignment: types.alignment.vertical, 33 | BorderStyle: types.borderStyle, 34 | PresetColorVal: types.excelColor, 35 | PatternType: types.fillPattern, 36 | PositiveUniversalMeasure: types.positiveUniversalMeasure 37 | }; 38 | -------------------------------------------------------------------------------- /source/lib/cell/cell.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | const Comment = require('../classes/comment'); 3 | 4 | // §18.3.1.4 c (Cell) 5 | class Cell { 6 | /** 7 | * Create an Excel Cell 8 | * @private 9 | * @param {Number} row Row of cell. 10 | * @param {Number} col Column of cell 11 | */ 12 | constructor(row, col) { 13 | this.r = `${utils.getExcelAlpha(col)}${row}`; // 'r' attribute 14 | this.s = 0; // 's' attribute refering to style index 15 | this.t = null; // 't' attribute stating Cell data type - §18.18.11 ST_CellType (Cell Type) 16 | this.f = null; // 'f' child element used for formulas 17 | this.v = null; // 'v' child element for values 18 | this.row = row; // used internally throughout code. Does not go into XML 19 | this.col = col; // used internally throughout code. Does not go into XML 20 | } 21 | 22 | get comment() { 23 | return this.comments[this.r]; 24 | } 25 | 26 | string(index) { 27 | this.t = 's'; 28 | this.v = index; 29 | this.f = null; 30 | } 31 | 32 | number(val) { 33 | this.t = 'n'; 34 | this.v = val; 35 | this.f = null; 36 | } 37 | 38 | formula(formula) { 39 | this.t = null; 40 | this.v = null; 41 | this.f = formula; 42 | } 43 | 44 | bool(val) { 45 | this.t = 'b'; 46 | this.v = val; 47 | this.f = null; 48 | } 49 | 50 | date(dt) { 51 | this.t = null; 52 | this.v = utils.getExcelTS(dt); 53 | this.f = null; 54 | } 55 | 56 | style(sId) { 57 | this.s = sId; 58 | } 59 | 60 | addToXMLele(ele) { 61 | if (this.v === null && this.is === null) { 62 | return; 63 | } 64 | 65 | let cEle = ele.ele('c').att('r', this.r).att('s', this.s); 66 | if (this.t !== null) { 67 | cEle.att('t', this.t); 68 | } 69 | if (this.f !== null) { 70 | cEle.ele('f').txt(this.f).up(); 71 | } 72 | if (this.v !== null) { 73 | cEle.ele('v').txt(this.v).up(); 74 | } 75 | cEle.up(); 76 | } 77 | } 78 | 79 | module.exports = Cell; 80 | 81 | -------------------------------------------------------------------------------- /source/lib/cell/index.js: -------------------------------------------------------------------------------- 1 | const deepmerge = require('deepmerge'); 2 | const Cell = require('./cell.js'); 3 | const Row = require('../row/row.js'); 4 | const Comment = require('../classes/comment'); 5 | const Column = require('../column/column.js'); 6 | const Style = require('../style/style.js'); 7 | const utils = require('../utils.js'); 8 | const util = require('util'); 9 | 10 | const validXmlRegex = /[\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/u; 11 | 12 | /** 13 | * The list of valid characters is 14 | * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 15 | * 16 | * We need to test codepoints numerically, instead of regex characters above 65536 (0x10000), 17 | */ 18 | function removeInvalidXml(str) { 19 | return Array.from(str).map(c => { 20 | const cp = c.codePointAt(0); 21 | if (cp >= 65536 && cp <= 1114111) { 22 | return c 23 | } else if (c.match(validXmlRegex)) { 24 | return c; 25 | } else { 26 | return ''; 27 | } 28 | }).join(''); 29 | } 30 | 31 | function stringSetter(val) { 32 | let logger = this.ws.wb.logger; 33 | 34 | if (typeof (val) !== 'string') { 35 | logger.warn('Value sent to String function of cells %s was not a string, it has type of %s', 36 | JSON.stringify(this.excelRefs), 37 | typeof (val)); 38 | val = ''; 39 | } 40 | val = removeInvalidXml(val); 41 | 42 | if (!this.merged) { 43 | this.cells.forEach((c) => { 44 | c.string(this.ws.wb.getStringIndex(val)); 45 | }); 46 | } else { 47 | let c = this.cells[0]; 48 | c.string(this.ws.wb.getStringIndex(val)); 49 | } 50 | return this; 51 | } 52 | 53 | function complexStringSetter(val) { 54 | if (!this.merged) { 55 | this.cells.forEach((c) => { 56 | c.string(this.ws.wb.getStringIndex(val)); 57 | }); 58 | } else { 59 | let c = this.cells[0]; 60 | c.string(this.ws.wb.getStringIndex(val)); 61 | } 62 | return this; 63 | } 64 | 65 | function numberSetter(val) { 66 | if (val === undefined || parseFloat(val) !== val) { 67 | throw new TypeError(util.format('Value sent to Number function of cells %s was not a number, it has type of %s and value of %s', 68 | JSON.stringify(this.excelRefs), 69 | typeof (val), 70 | val 71 | )); 72 | } 73 | val = parseFloat(val); 74 | 75 | if (!this.merged) { 76 | this.cells.forEach((c, i) => { 77 | c.number(val); 78 | }); 79 | } else { 80 | var c = this.cells[0]; 81 | c.number(val); 82 | } 83 | return this; 84 | } 85 | 86 | function booleanSetter(val) { 87 | if (val === undefined || typeof (val.toString().toLowerCase() === 'true' || ((val.toString().toLowerCase() === 'false') ? false : val)) !== 'boolean') { 88 | throw new TypeError(util.format('Value sent to Bool function of cells %s was not a bool, it has type of %s and value of %s', 89 | JSON.stringify(this.excelRefs), 90 | typeof (val), 91 | val 92 | )); 93 | } 94 | val = val.toString().toLowerCase() === 'true'; 95 | 96 | if (!this.merged) { 97 | this.cells.forEach((c, i) => { 98 | c.bool(val.toString()); 99 | }); 100 | } else { 101 | var c = this.cells[0]; 102 | c.bool(val.toString()); 103 | } 104 | return this; 105 | } 106 | 107 | function formulaSetter(val) { 108 | if (typeof (val) !== 'string') { 109 | throw new TypeError(util.format('Value sent to Formula function of cells %s was not a string, it has type of %s', JSON.stringify(this.excelRefs), typeof (val))); 110 | } 111 | if (this.merged !== true) { 112 | this.cells.forEach((c, i) => { 113 | c.formula(val); 114 | }); 115 | } else { 116 | var c = this.cells[0]; 117 | c.formula(val); 118 | } 119 | 120 | return this; 121 | } 122 | 123 | function dateSetter(val) { 124 | let thisDate = new Date(val); 125 | if (isNaN(thisDate.getTime())) { 126 | throw new TypeError(util.format('Invalid date sent to date function of cells. %s could not be converted to a date.', val)); 127 | } 128 | if (this.merged !== true) { 129 | this.cells.forEach((c, i) => { 130 | c.date(thisDate); 131 | }); 132 | } else { 133 | var c = this.cells[0]; 134 | c.date(thisDate); 135 | } 136 | const dtStyle = new Style(this.ws.wb, { 137 | numberFormat: '[$-409]' + this.ws.wb.opts.dateFormat 138 | }); 139 | return styleSetter.bind(this)(dtStyle); 140 | } 141 | 142 | function styleSetter(val) { 143 | let thisStyle; 144 | if (val instanceof Style) { 145 | thisStyle = val.toObject(); 146 | } else if (val instanceof Object) { 147 | thisStyle = val; 148 | } else { 149 | throw new TypeError(util.format('Parameter sent to Style function must be an instance of a Style or a style configuration object')); 150 | } 151 | 152 | let borderEdges = {}; 153 | if (thisStyle.border && thisStyle.border.outline) { 154 | borderEdges.left = this.firstCol; 155 | borderEdges.right = this.lastCol; 156 | borderEdges.top = this.firstRow; 157 | borderEdges.bottom = this.lastRow; 158 | } 159 | 160 | this.cells.forEach((c) => { 161 | if (thisStyle.border && thisStyle.border.outline) { 162 | let thisCellsBorder = {}; 163 | if (c.row === borderEdges.top && thisStyle.border.top) { 164 | thisCellsBorder.top = thisStyle.border.top; 165 | } 166 | if (c.row === borderEdges.bottom && thisStyle.border.bottom) { 167 | thisCellsBorder.bottom = thisStyle.border.bottom; 168 | } 169 | if (c.col === borderEdges.left && thisStyle.border.left) { 170 | thisCellsBorder.left = thisStyle.border.left; 171 | } 172 | if (c.col === borderEdges.right && thisStyle.border.right) { 173 | thisCellsBorder.right = thisStyle.border.right; 174 | } 175 | thisStyle.border = thisCellsBorder; 176 | } 177 | 178 | if (c.s === 0) { 179 | let thisCellStyle = this.ws.wb.createStyle(thisStyle); 180 | c.style(thisCellStyle.ids.cellXfs); 181 | } else { 182 | let curStyle = this.ws.wb.styles[c.s]; 183 | let newStyleOpts = deepmerge(curStyle.toObject(), thisStyle); 184 | let mergedStyle = this.ws.wb.createStyle(newStyleOpts); 185 | c.style(mergedStyle.ids.cellXfs); 186 | } 187 | }); 188 | return this; 189 | } 190 | 191 | function hyperlinkSetter(url, displayStr, tooltip) { 192 | this.excelRefs.forEach((ref) => { 193 | displayStr = typeof displayStr === 'string' ? displayStr : url; 194 | this.ws.hyperlinkCollection.add({ 195 | location: url, 196 | display: displayStr, 197 | tooltip: tooltip, 198 | ref: ref 199 | }); 200 | }); 201 | stringSetter.bind(this)(displayStr); 202 | return styleSetter.bind(this)({ 203 | font: { 204 | color: 'Blue', 205 | underline: true 206 | } 207 | }); 208 | } 209 | 210 | function commentSetter(comment, options) { 211 | if (this.merged !== true) { 212 | this.cells.forEach((c, i) => { 213 | this.ws.comments[c.r] = new Comment(c.r, comment, options) 214 | }); 215 | } else { 216 | var c = this.cells[0]; 217 | this.ws.comments[c.r] = new Comment(c.r, comment, options) 218 | } 219 | return this; 220 | } 221 | 222 | function mergeCells(cellBlock) { 223 | let excelRefs = cellBlock.excelRefs; 224 | if (excelRefs instanceof Array && excelRefs.length > 0) { 225 | excelRefs.sort(utils.sortCellRefs); 226 | 227 | let cellRange = excelRefs[0] + ':' + excelRefs[excelRefs.length - 1]; 228 | let rangeCells = excelRefs; 229 | 230 | let okToMerge = true; 231 | cellBlock.ws.mergedCells.forEach((cr) => { 232 | // Check to see if currently merged cells contain cells in new merge request 233 | let curCells = utils.getAllCellsInExcelRange(cr); 234 | let intersection = utils.arrayIntersectSafe(rangeCells, curCells); 235 | if (intersection.length > 0) { 236 | okToMerge = false; 237 | cellBlock.ws.wb.logger.error(`Invalid Range for: ${cellRange}. Some cells in this range are already included in another merged cell range: ${cr}.`); 238 | } 239 | }); 240 | if (okToMerge) { 241 | cellBlock.ws.mergedCells.push(cellRange); 242 | } 243 | } else { 244 | throw new TypeError(util.format('excelRefs variable sent to mergeCells function must be an array with length > 0')); 245 | } 246 | } 247 | 248 | /** 249 | * @class cellBlock 250 | */ 251 | class cellBlock { 252 | 253 | constructor() { 254 | this.ws; 255 | this.cells = []; 256 | this.excelRefs = []; 257 | this.merged = false; 258 | } 259 | 260 | get matrix() { 261 | let matrix = []; 262 | let tmpObj = {}; 263 | this.cells.forEach((c) => { 264 | if (!tmpObj[c.row]) { 265 | tmpObj[c.row] = []; 266 | } 267 | tmpObj[c.row].push(c); 268 | }); 269 | let rows = Object.keys(tmpObj); 270 | rows.forEach((r) => { 271 | tmpObj[r].sort((a, b) => { 272 | return a.col - b.col; 273 | }); 274 | matrix.push(tmpObj[r]); 275 | }); 276 | return matrix; 277 | } 278 | 279 | get firstRow() { 280 | let firstRow; 281 | this.cells.forEach((c) => { 282 | if (c.row < firstRow || firstRow === undefined) { 283 | firstRow = c.row; 284 | } 285 | }); 286 | return firstRow; 287 | } 288 | 289 | get lastRow() { 290 | let lastRow; 291 | this.cells.forEach((c) => { 292 | if (c.row > lastRow || lastRow === undefined) { 293 | lastRow = c.row; 294 | } 295 | }); 296 | return lastRow; 297 | } 298 | 299 | get firstCol() { 300 | let firstCol; 301 | this.cells.forEach((c) => { 302 | if (c.col < firstCol || firstCol === undefined) { 303 | firstCol = c.col; 304 | } 305 | }); 306 | return firstCol; 307 | } 308 | 309 | get lastCol() { 310 | let lastCol; 311 | this.cells.forEach((c) => { 312 | if (c.col > lastCol || lastCol === undefined) { 313 | lastCol = c.col; 314 | } 315 | }); 316 | return lastCol; 317 | } 318 | } 319 | 320 | /** 321 | * Module repesenting a Cell Accessor 322 | * @alias Worksheet.cell 323 | * @namespace 324 | * @func Worksheet.cell 325 | * @desc Access a range of cells in order to manipulate values 326 | * @param {Number} row1 Row of top left cell 327 | * @param {Number} col1 Column of top left cell 328 | * @param {Number} row2 Row of bottom right cell (optional) 329 | * @param {Number} col2 Column of bottom right cell (optional) 330 | * @param {Boolean} isMerged Merged the cell range into a single cell 331 | * @returns {cellBlock} 332 | */ 333 | function cellAccessor(row1, col1, row2, col2, isMerged) { 334 | let theseCells = new cellBlock(); 335 | theseCells.ws = this; 336 | 337 | row2 = row2 ? row2 : row1; 338 | col2 = col2 ? col2 : col1; 339 | 340 | if (row2 > this.lastUsedRow) { 341 | this.lastUsedRow = row2; 342 | } 343 | 344 | if (col2 > this.lastUsedCol) { 345 | this.lastUsedCol = col2; 346 | } 347 | 348 | for (let r = row1; r <= row2; r++) { 349 | for (let c = col1; c <= col2; c++) { 350 | let ref = `${utils.getExcelAlpha(c)}${r}`; 351 | if (!this.cells[ref]) { 352 | this.cells[ref] = new Cell(r, c); 353 | } 354 | if (!this.rows[r]) { 355 | this.rows[r] = new Row(r, this); 356 | } 357 | if (this.rows[r].cellRefs.indexOf(ref) < 0) { 358 | this.rows[r].cellRefs.push(ref); 359 | } 360 | 361 | theseCells.cells.push(this.cells[ref]); 362 | theseCells.excelRefs.push(ref); 363 | } 364 | } 365 | if (isMerged) { 366 | theseCells.merged = true; 367 | mergeCells(theseCells); 368 | } 369 | 370 | return theseCells; 371 | } 372 | 373 | /** 374 | * @alias cellBlock.string 375 | * @func cellBlock.string 376 | * @param {String} val Value of String 377 | * @returns {cellBlock} Block of cells with attached methods 378 | */ 379 | cellBlock.prototype.string = function (val) { 380 | if (val instanceof Array) { 381 | return complexStringSetter.bind(this)(val); 382 | } else { 383 | return stringSetter.bind(this)(val); 384 | } 385 | }; 386 | 387 | /** 388 | * @alias cellBlock.style 389 | * @func cellBlock.style 390 | * @param {Object} style One of a Style instance or an object with Style parameters 391 | * @returns {cellBlock} Block of cells with attached methods 392 | */ 393 | cellBlock.prototype.style = styleSetter; 394 | 395 | /** 396 | * @alias cellBlock.number 397 | * @func cellBlock.number 398 | * @param {Number} val Value of Number 399 | * @returns {cellBlock} Block of cells with attached methods 400 | */ 401 | cellBlock.prototype.number = numberSetter; 402 | 403 | /** 404 | * @alias cellBlock.bool 405 | * @func cellBlock.bool 406 | * @param {Boolean} val Value of Boolean 407 | * @returns {cellBlock} Block of cells with attached methods 408 | */ 409 | cellBlock.prototype.bool = booleanSetter; 410 | 411 | /** 412 | * @alias cellBlock.formula 413 | * @func cellBlock.formula 414 | * @param {String} val Excel style formula as string 415 | * @returns {cellBlock} Block of cells with attached methods 416 | */ 417 | cellBlock.prototype.formula = formulaSetter; 418 | 419 | /** 420 | * @alias cellBlock.date 421 | * @func cellBlock.date 422 | * @param {Date} val Value of Date 423 | * @returns {cellBlock} Block of cells with attached methods 424 | */ 425 | cellBlock.prototype.date = dateSetter; 426 | 427 | /** 428 | * @alias cellBlock.link 429 | * @func cellBlock.link 430 | * @param {String} url Value of Hyperlink URL 431 | * @param {String} displayStr Value of String representation of URL 432 | * @param {String} tooltip Value of text to display as hover 433 | * @returns {cellBlock} Block of cells with attached methods 434 | */ 435 | cellBlock.prototype.link = hyperlinkSetter; 436 | 437 | cellBlock.prototype.comment = commentSetter; 438 | 439 | module.exports = cellAccessor; -------------------------------------------------------------------------------- /source/lib/classes/comment.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v4'); 2 | const utils = require('../utils'); 3 | 4 | // §18.7.3 Comment 5 | class Comment { 6 | constructor(ref, comment, options = {}) { 7 | this.ref = ref; 8 | this.comment = comment; 9 | this.uuid = '{' + uuid().toUpperCase() + '}'; 10 | this.row = utils.getExcelRowCol(ref).row; 11 | this.col = utils.getExcelRowCol(ref).col; 12 | this.marginLeft = options.marginLeft || ((this.col) * 88 + 8) + 'pt'; 13 | this.marginTop = options.marginTop || ((this.row - 1) * 16 + 8) + 'pt'; 14 | this.width = options.width || '104pt'; 15 | this.height = options.height || '69pt'; 16 | this.position = options.position || 'absolute'; 17 | this.zIndex = options.zIndex || '1'; 18 | this.fillColor = options.fillColor || '#ffffe1'; 19 | this.visibility = options.visibility || 'hidden'; 20 | } 21 | 22 | } 23 | 24 | module.exports = Comment; 25 | -------------------------------------------------------------------------------- /source/lib/classes/ctMarker.js: -------------------------------------------------------------------------------- 1 | let EMU = require('./emu.js'); 2 | 3 | class CTMarker { 4 | /** 5 | * Element representing an Excel position marker 6 | * @param {Number} colId Column Number 7 | * @param {String} colOffset Offset stating how far right to shift the start edge 8 | * @param {Number} rowId Row Number 9 | * @param {String} rowOffset Offset stating how far down to shift the start edge 10 | * @property {Number} col Column number 11 | * @property {EMU} colOff EMUs of right shift 12 | * @property {Number} row Row number 13 | * @property {EMU} rowOff EMUs of top shift 14 | * @returns {CTMarker} Excel CTMarker 15 | */ 16 | constructor(colId, colOffset, rowId, rowOffset) { 17 | this._col = colId; 18 | this._colOff = new EMU(colOffset); 19 | this._row = rowId; 20 | this._rowOff = new EMU(rowOffset); 21 | } 22 | 23 | get col() { 24 | return this._col; 25 | } 26 | set col(val) { 27 | if (parseInt(val, 10) !== val || val < 0) { 28 | throw new TypeError('CTMarker column must be a positive integer'); 29 | } 30 | this._col = val; 31 | } 32 | 33 | get row() { 34 | return this._row; 35 | } 36 | set row(val) { 37 | if (parseInt(val, 10) !== val || val < 0) { 38 | throw new TypeError('CTMarker row must be a positive integer'); 39 | } 40 | this._row = val; 41 | } 42 | 43 | get colOff() { 44 | return this._colOff.value; 45 | } 46 | set colOff(val) { 47 | this._colOff = new EMU(val); 48 | } 49 | 50 | get rowOff() { 51 | return this._rowOff.value; 52 | } 53 | set rowOff(val) { 54 | this._rowOff = new EMU(val); 55 | } 56 | } 57 | 58 | module.exports = CTMarker; -------------------------------------------------------------------------------- /source/lib/classes/definedNameCollection.js: -------------------------------------------------------------------------------- 1 | class DefinedName { //§18.2.5 definedName (Defined Name) 2 | constructor(opts) { 3 | opts.refFormula !== undefined ? this.refFormula = opts.refFormula : null; 4 | opts.name !== undefined ? this.name = opts.name : null; 5 | opts.comment !== undefined ? this.comment = opts.comment : null; 6 | opts.customMenu !== undefined ? this.customMenu = opts.customMenu : null; 7 | opts.description !== undefined ? this.description = opts.description : null; 8 | opts.help !== undefined ? this.help = opts.help : null; 9 | opts.statusBar !== undefined ? this.statusBar = opts.statusBar : null; 10 | opts.localSheetId !== undefined ? this.localSheetId = opts.localSheetId : null; 11 | opts.hidden !== undefined ? this.hidden = opts.hidden : null; 12 | opts['function'] !== undefined ? this['function'] = opts['function'] : null; 13 | opts.vbProcedure !== undefined ? this.vbProcedure = opts.vbProcedure : null; 14 | opts.xlm !== undefined ? this.xlm = opts.xlm : null; 15 | opts.functionGroupId !== undefined ? this.functionGroupId = opts.functionGroupId : null; 16 | opts.shortcutKey !== undefined ? this.shortcutKey = opts.shortcutKey : null; 17 | opts.publishToServer !== undefined ? this.publishToServer = opts.publishToServer : null; 18 | opts.workbookParameter !== undefined ? this.workbookParameter = opts.workbookParameter : null; 19 | } 20 | 21 | addToXMLele(ele) { 22 | let dEle = ele.ele('definedName'); 23 | this.comment !== undefined ? dEle.att('comment', this.comment) : null; 24 | this.customMenu !== undefined ? dEle.att('customMenu', this.customMenu) : null; 25 | this.description !== undefined ? dEle.att('description', this.description) : null; 26 | this.help !== undefined ? dEle.att('help', this.help) : null; 27 | this.statusBar !== undefined ? dEle.att('statusBar', this.statusBar) : null; 28 | this.hidden !== undefined ? dEle.att('hidden', this.hidden) : null; 29 | this.localSheetId !== undefined ? dEle.att('localSheetId', this.localSheetId) : null; 30 | this.name !== undefined ? dEle.att('name', this.name) : null; 31 | this['function'] !== undefined ? dEle.att('function', this['function']) : null; 32 | this.vbProcedure !== undefined ? dEle.att('vbProcedure', this.vbProcedure) : null; 33 | this.xlm !== undefined ? dEle.att('xlm', this.xlm) : null; 34 | this.functionGroupId !== undefined ? dEle.att('functionGroupId', this.functionGroupId) : null; 35 | this.shortcutKey !== undefined ? dEle.att('shortcutKey', this.shortcutKey) : null; 36 | this.publishToServer !== undefined ? dEle.att('publishToServer', this.publishToServer) : null; 37 | this.workbookParameter !== undefined ? dEle.att('workbookParameter', this.workbookParameter) : null; 38 | 39 | this.refFormula !== undefined ? dEle.text(this.refFormula) : null; 40 | } 41 | } 42 | 43 | 44 | class DefinedNameCollection { // §18.2.6 definedNames (Defined Names) 45 | constructor() { 46 | this.items = []; 47 | } 48 | 49 | get length() { 50 | return this.items.length; 51 | } 52 | 53 | get isEmpty() { 54 | if (this.items.length === 0) { 55 | return true; 56 | } else { 57 | return false; 58 | } 59 | } 60 | 61 | addDefinedName(opts) { 62 | let item = new DefinedName(opts); 63 | let newLength = this.items.push(item); 64 | return this.items[newLength - 1]; 65 | } 66 | 67 | addToXMLele(ele) { 68 | let dnEle = ele.ele('definedNames'); 69 | this.items.forEach((dn) => { 70 | dn.addToXMLele(dnEle); 71 | }); 72 | } 73 | } 74 | module.exports = DefinedNameCollection; -------------------------------------------------------------------------------- /source/lib/classes/emu.js: -------------------------------------------------------------------------------- 1 | class EMU { 2 | 3 | /** 4 | * The EMU was created in order to be able to evenly divide in both English and Metric units 5 | * @class EMU 6 | * @param {String} Number of EMUs or string representation of length in mm, cm or in. i.e. '10.5mm' 7 | * @property {Number} value Number of EMUs 8 | * @returns {EMU} Number of EMUs 9 | */ 10 | constructor(val) { 11 | this._value; 12 | this.value = val; 13 | } 14 | 15 | get value() { 16 | return this._value; 17 | } 18 | 19 | set value(val) { 20 | if (val === undefined) { 21 | this._value = 0; 22 | } else if (typeof val === 'number') { 23 | this._value = val ? parseInt(val) : 0; 24 | } else if (typeof val === 'string') { 25 | let re = new RegExp('[0-9]+(\.[0-9]+)?(mm|cm|in)'); 26 | if (re.test(val) === true) { 27 | let measure = parseFloat(/[0-9]+(\.[0-9]+)?/.exec(val)[0]); 28 | let unit = /(mm|cm|in)/.exec(val)[0]; 29 | 30 | switch (unit) { 31 | case 'mm': 32 | this._value = parseInt(measure * 36000); 33 | break; 34 | 35 | case 'cm': 36 | this._value = parseInt(measure * 360000); 37 | break; 38 | 39 | case 'in': 40 | this._value = parseInt(measure * 914400); 41 | break; 42 | } 43 | } else { 44 | throw new TypeError('EMUs must be specified as whole integer EMUs or Floats immediately followed by unit of measure in cm, mm, or in. i.e. "1.5in"'); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * @alias EMU.toInt 51 | * @desc Returns the number of EMUs as integer 52 | * @func EMU.toInt 53 | * @returns {Number} Number of EMUs 54 | */ 55 | toInt() { 56 | return this._value; 57 | } 58 | 59 | /** 60 | * @alias EMU.toInch 61 | * @desc Returns the number of Inches for the EMUs 62 | * @func EMU.toInch 63 | * @returns {Number} Number of Inches for the EMUs 64 | */ 65 | toInch() { 66 | return this._value / 914400; 67 | } 68 | 69 | /** 70 | * @alias EMU.toCM 71 | * @desc Returns the number of Centimeters for the EMUs 72 | * @func EMU.toCM 73 | * @returns {Number} Number of Centimeters for the EMUs 74 | */ 75 | toCM() { 76 | return this._value / 360000; 77 | } 78 | } 79 | 80 | module.exports = EMU; 81 | 82 | /* 83 | M.4.1.1 EMU Unit of Measurement 84 | 85 | 1 emu = 1/914400 in = 1/360000 cm 86 | 87 | Throughout ECMA-376, the EMU is used as a unit of measurement for length. An EMU is defined as follows: 88 | The EMU was created in order to be able to evenly divide in both English and Metric units, in order to 89 | avoid rounding errors during the calculation. The usage of EMUs also facilitates a more seamless system 90 | switch and interoperability between different locales utilizing different units of measurement. 91 | EMUs define an integer based, high precision coordinate system. 92 | */ -------------------------------------------------------------------------------- /source/lib/classes/point.js: -------------------------------------------------------------------------------- 1 | class Point { 2 | /** 3 | * An XY coordinate point on the Worksheet with 0.0 being top left corner 4 | * @class Point 5 | * @property {Number} x X coordinate of Point 6 | * @property {Number} y Y coordinate of Point 7 | * @returns {Point} Excel Point 8 | */ 9 | constructor(x, y) { 10 | this.x = x; 11 | this.y = y; 12 | } 13 | } 14 | 15 | module.exports = Point; -------------------------------------------------------------------------------- /source/lib/column/column.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | 3 | class Column { 4 | /** 5 | * Element representing an Excel Column 6 | * @param {Number} col Column of cell 7 | * @param {Worksheet} Worksheet that contains column 8 | * @property {Worksheet} ws Worksheet that contains the specified Column 9 | * @property {Boolean} collapsed States whether the column is collapsed if part of a group 10 | * @property {Boolean} customWidth States whether or not the column as a width that is not default 11 | * @property {Boolean} hidden States whether or not the specified column is hiddent 12 | * @property {Number} max The greatest column if part of a range 13 | * @property {Number} min The least column if part of a range 14 | * @property {Number} outlineLevel The grouping leve of the Column 15 | * @property {Number} style ID of style 16 | * @property {Number} width Width of the Column 17 | */ 18 | constructor(col, ws) { 19 | this.ws = ws; 20 | this.collapsed = null; 21 | this.customWidth = null; 22 | this.hidden = null; 23 | this.max = col; 24 | this.min = col; 25 | this.outlineLevel = null; 26 | this.style = null; 27 | this.colWidth = null; 28 | } 29 | 30 | get width() { 31 | return this.colWidth; 32 | } 33 | 34 | set width(w) { 35 | if (typeof w === 'number') { 36 | this.colWidth = w; 37 | this.customWidth = true; 38 | } else { 39 | throw new TypeError('Column width must be a number'); 40 | } 41 | return this.colWidth; 42 | } 43 | 44 | /** 45 | * @alias Column.setWidth 46 | * @desc Sets teh width of a column 47 | * @func Column.setWidth 48 | * @param {Number} val New Width of column 49 | * @returns {Column} Excel Column with attached methods 50 | */ 51 | setWidth(w) { 52 | this.width = w; 53 | return this; 54 | } 55 | 56 | /** 57 | * @alias Column.hide 58 | * @desc Sets a Column to be hidden 59 | * @func Column.hide 60 | * @returns {Column} Excel Column with attached methods 61 | */ 62 | hide() { 63 | this.hidden = true; 64 | return this; 65 | } 66 | 67 | /** 68 | * @alias Column.group 69 | * @desc Adds column to the specified group 70 | * @func Column.group 71 | * @param {Number} level Level of excel grouping 72 | * @param {Boolean} collapsed States wheter column grouping level should be collapsed by default 73 | * @returns {Column} Excel Column with attached methods 74 | */ 75 | group(level, collapsed) { 76 | if (parseInt(level) === level) { 77 | this.outlineLevel = level; 78 | } else { 79 | throw new TypeError('Column group level must be a positive integer'); 80 | } 81 | 82 | if (collapsed === undefined) { 83 | return this; 84 | } 85 | 86 | if (typeof collapsed === 'boolean') { 87 | this.collapsed = collapsed; 88 | this.hidden = collapsed; 89 | } else { 90 | throw new TypeError('Column group collapse flag must be a boolean'); 91 | } 92 | 93 | return this; 94 | } 95 | 96 | /** 97 | * @alias Column.freeze 98 | * @desc Creates an Excel pane at the specificed column and Freezes that column from scolling 99 | * @func Column.freeze 100 | * @param {Number} jumptTo Specifies the column that the active pane will be scrolled to by default 101 | * @returns {Column} Excel Column with attached methods 102 | */ 103 | freeze(jumpTo) { 104 | let o = this.ws.opts.sheetView.pane; 105 | jumpTo = typeof jumpTo === 'number' && jumpTo > this.min ? jumpTo : this.min + 1; 106 | o.state = 'frozen'; 107 | o.xSplit = this.min; 108 | o.activePane = 'bottomRight'; 109 | o.ySplit === null ? 110 | o.topLeftCell = utils.getExcelCellRef(1, jumpTo) : 111 | o.topLeftCell = utils.getExcelCellRef(utils.getExcelRowCol(o.topLeftCell).row, jumpTo); 112 | return this; 113 | } 114 | } 115 | 116 | module.exports = Column; -------------------------------------------------------------------------------- /source/lib/column/index.js: -------------------------------------------------------------------------------- 1 | const Cell = require('../cell/cell.js'); 2 | const Row = require('../row/row.js'); 3 | const Column = require('../column/column.js'); 4 | const utils = require('../utils.js'); 5 | 6 | /** 7 | * Module repesenting a Column Accessor 8 | * @alias Worksheet.column 9 | * @namespace 10 | * @func Worksheet.column 11 | * @desc Access a column in order to manipulate values 12 | * @param {Number} col Column of top left cell 13 | * @returns {Column} 14 | */ 15 | let colAccessor = (ws, col) => { 16 | if (!(ws.cols[col] instanceof Column)) { 17 | ws.cols[col] = new Column(col, ws); 18 | } 19 | return ws.cols[col]; 20 | }; 21 | 22 | module.exports = colAccessor; -------------------------------------------------------------------------------- /source/lib/drawing/drawing.js: -------------------------------------------------------------------------------- 1 | const CTMarker = require('../classes/ctMarker.js'); 2 | const Point = require('../classes/point.js'); 3 | const EMU = require('../classes/emu.js'); 4 | 5 | class Drawing { 6 | /** 7 | * Element representing an Excel Drawing superclass 8 | * @property {String} anchorType Proprty for type of anchor. One of 'absoluteAnchor', 'oneCellAnchor', 'twoCellAnchor' 9 | * @property {CTMarker} anchorFrom Property for the top left corner position of drawing 10 | * @property {CTMarker} anchorTo Property for the bottom left corner position of drawing 11 | * @property {String} editAs Property that states how to interact with the Drawing in Excel. One of 'absolute', 'oneCell', 'twoCell' 12 | * @property {Point} _position Internal property for position on Excel Worksheet when drawing type is absoluteAnchor 13 | * @returns {Drawing} Excel Drawing 14 | */ 15 | constructor() { 16 | this._anchorType = null; 17 | this._anchorFrom = null; 18 | this._anchorTo = null; 19 | this._editAs = null; 20 | this._position = null; 21 | } 22 | 23 | get anchorType() { 24 | return this._anchorType; 25 | } 26 | set anchorType(type) { 27 | let types = ['absoluteAnchor', 'oneCellAnchor', 'twoCellAnchor']; 28 | if (types.indexOf(type) < 0) { 29 | throw new TypeError('Invalid option for anchor type. anchorType must be one of ' + types.join(', ')); 30 | } 31 | this._anchorType = type; 32 | } 33 | 34 | get editAs() { 35 | return this._editAs; 36 | } 37 | set editAs(val) { 38 | let types = ['absolute', 'oneCell', 'twoCell']; 39 | if (types.indexOf(val) < 0) { 40 | throw new TypeError('Invalid option for editAs. editAs must be one of ' + types.join(', ')); 41 | } 42 | this._editAs = val; 43 | } 44 | 45 | get anchorFrom() { 46 | return this._anchorFrom; 47 | } 48 | set anchorFrom(obj) { 49 | if (obj !== undefined && obj instanceof Object) { 50 | this._anchorFrom = new CTMarker(obj.col - 1, obj.colOff, obj.row - 1, obj.rowOff); 51 | } 52 | } 53 | 54 | get anchorTo() { 55 | return this._anchorTo; 56 | } 57 | set anchorTo(obj) { 58 | if (obj !== undefined && obj instanceof Object) { 59 | this._anchorTo = new CTMarker(obj.col - 1, obj.colOff, obj.row - 1, obj.rowOff); 60 | } 61 | } 62 | 63 | /** 64 | * @alias Drawing.achor 65 | * @desc Sets the postion and anchor properties of the Drawing 66 | * @func Drawing.achor 67 | * @param {String} type Anchor type of drawing 68 | * @param {Object} from Properties for achorFrom property 69 | * @param {Number} from.col Left edge of drawing will align with left edge of this column 70 | * @param {String} from.colOff Offset. Drawing will be shifted to the right the specified amount. Float followed by measure [0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi). i.e '10.5mm' 71 | * @param {Number} from.row Top edge of drawing will align with top edge of this row 72 | * @param {String} from.rowOff Offset. Drawing will be shifted down the specified amount. Float followed by measure [0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi). i.e '10.5mm' 73 | * @param {Object} to Properties for anchorTo property 74 | * @param {Number} to.col Left edge of drawing will align with left edge of this column 75 | * @param {String} to.colOff Offset. Drawing will be shifted to the right the specified amount. Float followed by measure [0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi). i.e '10.5mm' 76 | * @param {Number} to.row Top edge of drawing will align with top edge of this row 77 | * @param {String} to.rowOff Offset. Drawing will be shifted down the specified amount. Float followed by measure [0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi). i.e '10.5mm' 78 | * @returns {Drawing} Excel Drawing with attached methods 79 | */ 80 | anchor(type, from, to) { 81 | if (type === 'twoCellAnchor') { 82 | if (from === undefined || to === undefined) { 83 | throw new TypeError('twoCellAnchor requires both from and two markers'); 84 | } 85 | this.editAs = 'oneCell'; 86 | } 87 | this.anchorType = type; 88 | this.anchorFrom = from; 89 | this.anchorTo = to; 90 | return this; 91 | } 92 | 93 | /** 94 | * @alias Drawing.position 95 | * @desc The position of the top left corner of the image on the Worksheet 96 | * @func Drawing.position 97 | * @param {ST_PositiveUniversalMeasure} cx Postion from left of Worksheet edge 98 | * @param {ST_PositiveUniversalMeasure} cy Postion from top of Worksheet edge 99 | */ 100 | position(cx, cy) { 101 | this.anchorType = 'absoluteAnchor'; 102 | let thisCx = new EMU(cx); 103 | let thisCy = new EMU(cy); 104 | this._position = new Point(thisCx.value, thisCy.value); 105 | } 106 | } 107 | 108 | module.exports = Drawing; -------------------------------------------------------------------------------- /source/lib/drawing/index.js: -------------------------------------------------------------------------------- 1 | let Drawing = require('./drawing.js'); 2 | let Picture = require('./picture.js'); 3 | 4 | class DrawingCollection { 5 | constructor() { 6 | this.drawings = []; 7 | } 8 | 9 | get length() { 10 | return this.drawings.length; 11 | } 12 | 13 | add(opts) { 14 | switch (opts.type) { 15 | case 'picture': 16 | let newPic = new Picture(opts); 17 | this.drawings.push(newPic); 18 | return newPic; 19 | 20 | default: 21 | throw new TypeError('this option is not yet supported'); 22 | } 23 | } 24 | 25 | get isEmpty() { 26 | if (this.drawings.length === 0) { 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | } 32 | } 33 | 34 | module.exports = { DrawingCollection, Drawing, Picture }; 35 | -------------------------------------------------------------------------------- /source/lib/drawing/picture.js: -------------------------------------------------------------------------------- 1 | const Drawing = require('./drawing.js'); 2 | const path = require('path'); 3 | const imgsz = require('image-size'); 4 | const mime = require('mime'); 5 | const uniqueId = require('lodash.uniqueid'); 6 | 7 | const EMU = require('../classes/emu.js'); 8 | const xmlbuilder = require('xmlbuilder'); 9 | 10 | class Picture extends Drawing { 11 | /** 12 | * Element representing an Excel Picture subclass of Drawing 13 | * @property {String} kind Kind of picture (currently only image is supported) 14 | * @property {String} type ooxml schema 15 | * @property {String} imagePath Filesystem path to image 16 | * @property {Buffer} image Buffer with image 17 | * @property {String} contentType Mime type of image 18 | * @property {String} description Description of image 19 | * @property {String} title Title of image 20 | * @property {String} id ID of image 21 | * @property {String} noGrp pickLocks property 22 | * @property {String} noSelect pickLocks property 23 | * @property {String} noRot pickLocks property 24 | * @property {String} noChangeAspect pickLocks property 25 | * @property {String} noMove pickLocks property 26 | * @property {String} noResize pickLocks property 27 | * @property {String} noEditPoints pickLocks property 28 | * @property {String} noAdjustHandles pickLocks property 29 | * @property {String} noChangeArrowheads pickLocks property 30 | * @property {String} noChangeShapeType pickLocks property 31 | * @returns {Picture} Excel Picture pickLocks property 32 | */ 33 | constructor(opts) { 34 | super(); 35 | this.kind = 'image'; 36 | this.type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'; 37 | this.imagePath = opts.path; 38 | this.image = opts.image; 39 | 40 | this._name = this.image ? 41 | opts.name || uniqueId('image-') : 42 | opts.name || path.basename(this.imagePath); 43 | 44 | const size = imgsz(this.imagePath || this.image); 45 | 46 | this._pxWidth = size.width; 47 | this._pxHeight = size.height; 48 | 49 | this._extension = this.image ? 50 | size.type : 51 | path.extname(this.imagePath).substr(1); 52 | 53 | this.contentType = mime.getType(this._extension); 54 | 55 | this._descr = null; 56 | this._title = null; 57 | this._id; 58 | // picLocks §20.1.2.2.31 picLocks (Picture Locks) 59 | this.noGrp; 60 | this.noSelect; 61 | this.noRot; 62 | this.noChangeAspect = true; 63 | this.noMove; 64 | this.noResize; 65 | this.noEditPoints; 66 | this.noAdjustHandles; 67 | this.noChangeArrowheads; 68 | this.noChangeShapeType; 69 | if (['oneCellAnchor', 'twoCellAnchor'].indexOf(opts.position.type) >= 0) { 70 | this.anchor(opts.position.type, opts.position.from, opts.position.to); 71 | } else if (opts.position.type === 'absoluteAnchor') { 72 | this.position(opts.position.x, opts.position.y); 73 | } else { 74 | throw new TypeError('Invalid option for anchor type. anchorType must be one of oneCellAnchor, twoCellAnchor, or absoluteAnchor'); 75 | } 76 | } 77 | 78 | get name() { 79 | return this._name; 80 | } 81 | set name(newName) { 82 | this._name = newName; 83 | } 84 | get id() { 85 | return this._id; 86 | } 87 | set id(id) { 88 | this._id = id; 89 | } 90 | 91 | get rId() { 92 | return 'rId' + this._id; 93 | } 94 | 95 | get description() { 96 | return this._descr !== null ? this._descr : this._name; 97 | } 98 | set description(desc) { 99 | this._descr = desc; 100 | } 101 | 102 | get title() { 103 | return this._title !== null ? this._title : this._name; 104 | } 105 | set title(title) { 106 | this._title = title; 107 | } 108 | 109 | get extension() { 110 | return this._extension; 111 | } 112 | 113 | get width() { 114 | let inWidth = this._pxWidth / 96; 115 | let emu = new EMU(inWidth + 'in'); 116 | return emu.value; 117 | } 118 | 119 | get height() { 120 | let inHeight = this._pxHeight / 96; 121 | let emu = new EMU(inHeight + 'in'); 122 | return emu.value; 123 | } 124 | 125 | /** 126 | * @alias Picture.addToXMLele 127 | * @desc When generating Workbook output, attaches pictures to the drawings xml file 128 | * @func Picture.addToXMLele 129 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 130 | */ 131 | addToXMLele(ele) { 132 | 133 | let anchorEle = ele.ele('xdr:' + this.anchorType); 134 | 135 | if (this.editAs !== null) { 136 | anchorEle.att('editAs', this.editAs); 137 | } 138 | 139 | if (this.anchorType === 'absoluteAnchor') { 140 | anchorEle.ele('xdr:pos').att('x', this._position.x).att('y', this._position.y); 141 | } 142 | 143 | if (this.anchorType !== 'absoluteAnchor') { 144 | let af = this.anchorFrom; 145 | let afEle = anchorEle.ele('xdr:from'); 146 | afEle.ele('xdr:col').text(af.col); 147 | afEle.ele('xdr:colOff').text(af.colOff); 148 | afEle.ele('xdr:row').text(af.row); 149 | afEle.ele('xdr:rowOff').text(af.rowOff); 150 | } 151 | 152 | if (this.anchorTo && this.anchorType === 'twoCellAnchor') { 153 | let at = this.anchorTo; 154 | let atEle = anchorEle.ele('xdr:to'); 155 | atEle.ele('xdr:col').text(at.col); 156 | atEle.ele('xdr:colOff').text(at.colOff); 157 | atEle.ele('xdr:row').text(at.row); 158 | atEle.ele('xdr:rowOff').text(at.rowOff); 159 | } 160 | 161 | if (this.anchorType === 'oneCellAnchor' || this.anchorType === 'absoluteAnchor') { 162 | anchorEle.ele('xdr:ext').att('cx', this.width).att('cy', this.height); 163 | } 164 | 165 | let picEle = anchorEle.ele('xdr:pic'); 166 | let nvPicPrEle = picEle.ele('xdr:nvPicPr'); 167 | let cNvPrEle = nvPicPrEle.ele('xdr:cNvPr'); 168 | cNvPrEle.att('descr', this.description); 169 | cNvPrEle.att('id', this.id + 1); 170 | cNvPrEle.att('name', this.name); 171 | cNvPrEle.att('title', this.title); 172 | let cNvPicPrEle = nvPicPrEle.ele('xdr:cNvPicPr'); 173 | 174 | this.noGrp === true ? cNvPicPrEle.ele('a:picLocks').att('noGrp', 1) : null; 175 | this.noSelect === true ? cNvPicPrEle.ele('a:picLocks').att('noSelect', 1) : null; 176 | this.noRot === true ? cNvPicPrEle.ele('a:picLocks').att('noRot', 1) : null; 177 | this.noChangeAspect === true ? cNvPicPrEle.ele('a:picLocks').att('noChangeAspect', 1) : null; 178 | this.noMove === true ? cNvPicPrEle.ele('a:picLocks').att('noMove', 1) : null; 179 | this.noResize === true ? cNvPicPrEle.ele('a:picLocks').att('noResize', 1) : null; 180 | this.noEditPoints === true ? cNvPicPrEle.ele('a:picLocks').att('noEditPoints', 1) : null; 181 | this.noAdjustHandles === true ? cNvPicPrEle.ele('a:picLocks').att('noAdjustHandles', 1) : null; 182 | this.noChangeArrowheads === true ? cNvPicPrEle.ele('a:picLocks').att('noChangeArrowheads', 1) : null; 183 | this.noChangeShapeType === true ? cNvPicPrEle.ele('a:picLocks').att('noChangeShapeType', 1) : null; 184 | 185 | let blipFillEle = picEle.ele('xdr:blipFill'); 186 | blipFillEle.ele('a:blip').att('r:embed', this.rId).att('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); 187 | blipFillEle.ele('a:stretch').ele('a:fillRect'); 188 | 189 | let spPrEle = picEle.ele('xdr:spPr'); 190 | let xfrmEle = spPrEle.ele('a:xfrm'); 191 | xfrmEle.ele('a:off').att('x', 0).att('y', 0); 192 | xfrmEle.ele('a:ext').att('cx', this.width).att('cy', this.height); 193 | 194 | let prstGeom = spPrEle.ele('a:prstGeom').att('prst', 'rect'); 195 | prstGeom.ele('a:avLst'); 196 | 197 | anchorEle.ele('xdr:clientData'); 198 | } 199 | } 200 | 201 | module.exports = Picture; -------------------------------------------------------------------------------- /source/lib/logger.js: -------------------------------------------------------------------------------- 1 | class SimpleLogger { 2 | constructor(opts) { 3 | this.logLevel = opts.logLevel || 5; 4 | } 5 | 6 | debug() { 7 | if (this.logLevel >= 5) { 8 | console.debug(...arguments); 9 | } 10 | } 11 | 12 | log() { 13 | if (this.logLevel >= 4) { 14 | console.log(...arguments); 15 | } 16 | } 17 | 18 | inspect() { 19 | if (this.logLevel >= 4) { 20 | console.log(...arguments); 21 | } 22 | } 23 | 24 | info() { 25 | if (this.logLevel >= 3) { 26 | console.info(...arguments); 27 | } 28 | } 29 | 30 | warn() { 31 | if (this.logLevel >= 2) { 32 | console.warn(...arguments); 33 | } 34 | } 35 | 36 | error() { 37 | if (this.logLevel >= 1) { 38 | console.error(...arguments); 39 | } 40 | } 41 | 42 | } 43 | 44 | module.exports = SimpleLogger; -------------------------------------------------------------------------------- /source/lib/row/index.js: -------------------------------------------------------------------------------- 1 | const Row = require('../row/row.js'); 2 | 3 | /** 4 | * Module repesenting a Row Accessor 5 | * @alias Worksheet.row 6 | * @namespace 7 | * @func Worksheet.row 8 | * @desc Access a row in order to manipulate values 9 | * @param {Number} row Row of top left cell 10 | * @returns {Row} 11 | */ 12 | let rowAccessor = function (ws, row) { 13 | 14 | if (typeof row !== 'number') { 15 | throw new TypeError('Row sent to row accessor was not a number.'); 16 | } 17 | 18 | if (!(ws.rows[row] instanceof Row)) { 19 | ws.rows[row] = new Row(row, ws); 20 | } 21 | 22 | return ws.rows[row]; 23 | }; 24 | 25 | 26 | 27 | module.exports = rowAccessor; -------------------------------------------------------------------------------- /source/lib/row/row.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | 3 | class Row { 4 | /** 5 | * Element representing an Excel Row 6 | * @param {Number} row Row of cell 7 | * @param {Worksheet} Worksheet that contains row 8 | * @property {Worksheet} ws Worksheet that contains the specified Row 9 | * @property {Array.String} cellRefs Array of excel cell references 10 | * @property {Boolean} collapsed States whether row is collapsed when grouped 11 | * @property {Boolean} customFormat States whether the row has a custom format 12 | * @property {Boolean} customHeight States whether the row's height is different than default 13 | * @property {Boolean} hidden States whether the row is hidden 14 | * @property {Number} ht Height of the row (internal property) 15 | * @property {Number} outlineLevel Grouping level of row 16 | * @property {Number} r Row index 17 | * @property {Number} s Style index 18 | * @property {Boolean} thickBot States whether row has a thick bottom border 19 | * @property {Boolean} thickTop States whether row has a thick top border 20 | * @property {Number} height Height of row 21 | * @property {String} spans String representation of excel cell range i.e. A1:A10 22 | * @property {Number} firstColumn Index of the first column of the row containg data 23 | * @property {String} firstColumnAlpha Alpha representation of the first column of the row containing data 24 | * @property {Number} lastColumn Index of the last column of the row cotaining data 25 | * @property {String} lastColumnAlpha Alpha representation of the last column of the row containing data 26 | */ 27 | constructor(row, ws) { 28 | this.ws = ws; 29 | this.cellRefs = []; 30 | this.collapsed = null; 31 | this.customFormat = null; 32 | this.customHeight = null; 33 | this.hidden = null; 34 | this.ht = null; 35 | this.outlineLevel = null; 36 | this.r = row; 37 | this.s = null; 38 | this.thickBot = null; 39 | this.thickTop = null; 40 | } 41 | 42 | set height(h) { 43 | if (typeof h === 'number') { 44 | this.ht = h; 45 | this.customHeight = true; 46 | } else { 47 | throw new TypeError('Row height must be a number'); 48 | } 49 | return this.ht; 50 | } 51 | get height() { 52 | return this.ht; 53 | } 54 | 55 | /** 56 | * @alias Row.setHeight 57 | * @desc Sets the height of a row 58 | * @func Row.setHeight 59 | * @param {Number} val New Height of row 60 | * @returns {Row} Excel Row with attached methods 61 | */ 62 | setHeight(h) { 63 | if (typeof h === 'number') { 64 | this.ht = h; 65 | this.customHeight = true; 66 | } else { 67 | throw new TypeError('Row height must be a number'); 68 | } 69 | return this; 70 | } 71 | 72 | get spans() { 73 | if (this.cellRefs.length > 0) { 74 | const startCol = utils.getExcelRowCol(this.cellRefs[0]).col; 75 | const endCol = utils.getExcelRowCol(this.cellRefs[this.cellRefs.length - 1]).col; 76 | return `${startCol}:${endCol}`; 77 | } else { 78 | return null; 79 | } 80 | } 81 | 82 | get firstColumn() { 83 | if (this.cellRefs instanceof Array && this.cellRefs.length > 0) { 84 | return utils.getExcelRowCol(this.cellRefs[0]).col; 85 | } else { 86 | return 1; 87 | } 88 | } 89 | 90 | get firstColumnAlpha() { 91 | if (this.cellRefs instanceof Array && this.cellRefs.length > 0) { 92 | return utils.getExcelAlpha(utils.getExcelRowCol(this.cellRefs[0]).col); 93 | } else { 94 | return 'A'; 95 | } 96 | } 97 | 98 | get lastColumn() { 99 | if (this.cellRefs instanceof Array && this.cellRefs.length > 0) { 100 | return utils.getExcelRowCol(this.cellRefs[this.cellRefs.length - 1]).col; 101 | } else { 102 | return 1; 103 | } 104 | } 105 | 106 | get lastColumnAlpha() { 107 | if (this.cellRefs instanceof Array && this.cellRefs.length > 0) { 108 | return utils.getExcelAlpha(utils.getExcelRowCol(this.cellRefs[this.cellRefs.length - 1]).col); 109 | } else { 110 | return 'A'; 111 | } 112 | } 113 | 114 | /** 115 | * @alias Row.filter 116 | * @desc Add autofilter dropdowns to the items of the row 117 | * @func Row.filter 118 | * @param {Object} opts Object containing options for the fitler. 119 | * @param {Number} opts.lastRow Last row in which the filter show effect filtered results (optional) 120 | * @param {Number} opts.startCol First column that a filter dropdown should be added (optional) 121 | * @param {Number} opts.lastCol Last column that a filter dropdown should be added (optional) 122 | * @param {Array.DefinedName} opts.filters Array of filter paramaters 123 | * @returns {Row} Excel Row with attached methods 124 | */ 125 | filter(opts = {}) { 126 | 127 | let theseFilters = opts.filters instanceof Array ? opts.filters : []; 128 | 129 | let o = this.ws.opts.autoFilter; 130 | o.startRow = this.r; 131 | if (typeof opts.lastRow === 'number') { 132 | o.endRow = opts.lastRow; 133 | } 134 | 135 | if (typeof opts.firstColumn === 'number' && typeof opts.lastColumn === 'number') { 136 | o.startCol = opts.firstColumn; 137 | o.endCol = opts.lastColumn; 138 | } 139 | 140 | // Programmer Note: DefinedName class is added to workbook during workbook write process for filters 141 | 142 | this.ws.opts.autoFilter.filters = theseFilters; 143 | } 144 | 145 | /** 146 | * @alias Row.hide 147 | * @desc Hides the row 148 | * @func Row.hide 149 | * @returns {Row} Excel Row with attached methods 150 | */ 151 | hide() { 152 | this.hidden = true; 153 | return this; 154 | } 155 | 156 | /** 157 | * @alias Row.group 158 | * @desc Hides the row 159 | * @func Row.group 160 | * @param {Number} level Group level of row 161 | * @param {Boolean} collapsed States whether group should be collapsed or expanded by default 162 | * @returns {Row} Excel Row with attached methods 163 | */ 164 | group(level, collapsed) { 165 | if (parseInt(level) === level) { 166 | this.outlineLevel = level; 167 | } else { 168 | throw new TypeError('Row group level must be a positive integer'); 169 | } 170 | 171 | if (collapsed === undefined) { 172 | return this; 173 | } 174 | 175 | if (typeof collapsed === 'boolean') { 176 | this.collapsed = collapsed; 177 | this.hidden = collapsed; 178 | } else { 179 | throw new TypeError('Row group collapse flag must be a boolean'); 180 | } 181 | 182 | return this; 183 | } 184 | 185 | /** 186 | * @alias Row.freeze 187 | * @desc Creates Worksheet panes and freezes the top pane 188 | * @func Row.freeze 189 | * @param {Number} jumpTo Row that the bottom pane should be scrolled to by default 190 | * @returns {Row} Excel Row with attached methods 191 | */ 192 | freeze(jumpTo) { 193 | let o = this.ws.opts.sheetView.pane; 194 | jumpTo = typeof jumpTo === 'number' && jumpTo > this.r ? jumpTo : this.r + 1; 195 | o.state = 'frozen'; 196 | o.ySplit = this.r; 197 | o.activePane = 'bottomRight'; 198 | o.xSplit === null ? 199 | o.topLeftCell = utils.getExcelCellRef(jumpTo, 1) : 200 | o.topLeftCell = utils.getExcelCellRef(jumpTo, utils.getExcelRowCol(o.topLeftCell).col); 201 | return this; 202 | } 203 | } 204 | 205 | module.exports = Row; -------------------------------------------------------------------------------- /source/lib/style/classes/alignment.js: -------------------------------------------------------------------------------- 1 | const types = require('../../types/index.js'); 2 | const xmlbuilder = require('xmlbuilder'); 3 | 4 | class Alignment { // §18.8.1 alignment (Alignment) 5 | /** 6 | * @class Alignment 7 | * @param {Object} opts Properties of Alignment object 8 | * @param {String} opts.horizontal Horizontal Alignment property of text. 9 | * @param {String} opts.vertical Vertical Alignment property of text. 10 | * @param {String} opts.readingOrder Reading order for language of text. 11 | * @param {Number} opts.indent How much text should be indented. Setting indent to 1 will indent text 3 spaces 12 | * @param {Boolean} opts.justifyLastLine Specifies whether to justify last line of text 13 | * @param {Number} opts.relativeIndent Used in conditional formatting to state how much more text should be indented if rule passes 14 | * @param {Boolean} opts.shrinkToFit Indicates if text should be shrunk to fit into cell 15 | * @param {Number} opts.textRotation Number of degrees to rotate text counterclockwise 16 | * @param {Boolean} opts.wrapText States whether text with newline characters should wrap 17 | * @returns {Alignment} 18 | */ 19 | constructor(opts) { 20 | 21 | if (opts.horizontal !== undefined) { 22 | this.horizontal = types.alignment.horizontal.validate(opts.horizontal) === true ? opts.horizontal : null; 23 | } 24 | 25 | if (opts.vertical !== undefined) { 26 | this.vertical = types.alignment.vertical.validate(opts.vertical) === true ? opts.vertical : null; 27 | } 28 | 29 | if (opts.readingOrder !== undefined) { 30 | this.readingOrder = types.alignment.readingOrder.validate(opts.readingOrder) === true ? opts.readingOrder : null; 31 | } 32 | 33 | if (opts.indent !== undefined) { 34 | if (typeof opts.indent === 'number' && parseInt(opts.indent) === opts.indent && opts.indent > 0) { 35 | this.indent = opts.indent; 36 | } else { 37 | throw new TypeError('alignment indent must be a positive integer.'); 38 | } 39 | } 40 | 41 | if (opts.justifyLastLine !== undefined) { 42 | if (typeof opts.justifyLastLine === 'boolean') { 43 | this.justifyLastLine = opts.justifyLastLine; 44 | } else { 45 | throw new TypeError('justifyLastLine alignment option must be of type boolean'); 46 | } 47 | } 48 | 49 | if (opts.relativeIndent !== undefined) { 50 | if (typeof opts.relativeIndent === 'number' && parseInt(opts.relativeIndent) === opts.relativeIndent && opts.relativeIndent > 0) { 51 | this.relativeIndent = opts.relativeIndent; 52 | } else { 53 | throw new TypeError('alignment indent must be a positive integer.'); 54 | } 55 | } 56 | 57 | if (opts.shrinkToFit !== undefined) { 58 | if (typeof opts.shrinkToFit === 'boolean') { 59 | this.shrinkToFit = opts.shrinkToFit; 60 | } else { 61 | throw new TypeError('justifyLastLine alignment option must be of type boolean'); 62 | } 63 | } 64 | 65 | if (opts.textRotation !== undefined) { 66 | if (typeof opts.textRotation === 'number' && parseInt(opts.textRotation) === opts.textRotation) { 67 | this.textRotation = opts.textRotation; 68 | } else if (opts.textRotation !== undefined) { 69 | throw new TypeError('alignment indent must be an integer.'); 70 | } 71 | } 72 | 73 | if (opts.wrapText !== undefined) { 74 | if (typeof opts.wrapText === 'boolean') { 75 | this.wrapText = opts.wrapText; 76 | } else { 77 | throw new TypeError('justifyLastLine alignment option must be of type boolean'); 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * @func Alignment.toObject 84 | * @desc Converts the Alignment instance to a javascript object 85 | * @returns {Object} 86 | */ 87 | toObject() { 88 | let obj = {}; 89 | 90 | this.horizontal !== undefined ? obj.horizontal = this.horizontal : null; 91 | this.indent !== undefined ? obj.indent = this.indent : null; 92 | this.justifyLastLine !== undefined ? obj.justifyLastLine = this.justifyLastLine : null; 93 | this.readingOrder !== undefined ? obj.readingOrder = this.readingOrder : null; 94 | this.relativeIndent !== undefined ? obj.relativeIndent = this.relativeIndent : null; 95 | this.shrinkToFit !== undefined ? obj.shrinkToFit = this.shrinkToFit : null; 96 | this.textRotation !== undefined ? obj.textRotation = this.textRotation : null; 97 | this.vertical !== undefined ? obj.vertical = this.vertical : null; 98 | this.wrapText !== undefined ? obj.wrapText = this.wrapText : null; 99 | 100 | return obj; 101 | } 102 | 103 | /** 104 | * @alias Alignment.addToXMLele 105 | * @desc When generating Workbook output, attaches style to the styles xml file 106 | * @func Alignment.addToXMLele 107 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 108 | */ 109 | addToXMLele(ele) { 110 | let thisEle = ele.ele('alignment'); 111 | this.horizontal !== undefined ? thisEle.att('horizontal', this.horizontal) : null; 112 | this.indent !== undefined ? thisEle.att('indent', this.indent) : null; 113 | this.justifyLastLine === true ? thisEle.att('justifyLastLine', 1) : null; 114 | this.readingOrder !== undefined ? thisEle.att('readingOrder', this.readingOrder) : null; 115 | this.relativeIndent !== undefined ? thisEle.att('relativeIndent', this.relativeIndent) : null; 116 | this.shrinkToFit === true ? thisEle.att('shrinkToFit', 1) : null; 117 | this.textRotation !== undefined ? thisEle.att('textRotation', this.textRotation) : null; 118 | this.vertical !== undefined ? thisEle.att('vertical', this.vertical) : null; 119 | this.wrapText === true ? thisEle.att('wrapText', 1) : null; 120 | } 121 | } 122 | 123 | module.exports = Alignment; -------------------------------------------------------------------------------- /source/lib/style/classes/border.js: -------------------------------------------------------------------------------- 1 | const types = require('../../types/index.js'); 2 | const xmlbuilder = require('xmlbuilder'); 3 | const CTColor = require('./ctColor.js'); 4 | 5 | class BorderOrdinal { 6 | constructor(opts) { 7 | opts = opts ? opts : {}; 8 | if (opts.color !== undefined) { 9 | this.color = new CTColor(opts.color); 10 | } 11 | if (opts.style !== undefined) { 12 | this.style = types.borderStyle.validate(opts.style) === true ? opts.style : null; 13 | } 14 | } 15 | 16 | toObject() { 17 | let obj = {}; 18 | if (this.color !== undefined) { 19 | obj.color = this.color.toObject(); 20 | } 21 | if (this.style !== undefined) { 22 | obj.style = this.style; 23 | } 24 | return obj; 25 | } 26 | } 27 | 28 | class Border { 29 | /** 30 | * @class Border 31 | * @desc Border object for Style 32 | * @param {Object} opts Options for Border object 33 | * @param {Object} opts.left Options for left side of Border 34 | * @param {String} opts.left.color HEX represenation of color 35 | * @param {String} opts.left.style Border style 36 | * @param {Object} opts.right Options for right side of Border 37 | * @param {String} opts.right.color HEX represenation of color 38 | * @param {String} opts.right.style Border style 39 | * @param {Object} opts.top Options for top side of Border 40 | * @param {String} opts.top.color HEX represenation of color 41 | * @param {String} opts.top.style Border style 42 | * @param {Object} opts.bottom Options for bottom side of Border 43 | * @param {String} opts.bottom.color HEX represenation of color 44 | * @param {String} opts.bottom.style Border style 45 | * @param {Object} opts.diagonal Options for diagonal side of Border 46 | * @param {String} opts.diagonal.color HEX represenation of color 47 | * @param {String} opts.diagonal.style Border style 48 | * @param {Boolean} opts.outline States whether borders should be applied only to the outside borders of a cell range 49 | * @param {Boolean} opts.diagonalDown States whether diagonal border should go from top left to bottom right 50 | * @param {Boolean} opts.diagonalUp States whether diagonal border should go from bottom left to top right 51 | * @returns {Border} 52 | */ 53 | constructor(opts) { 54 | opts = opts ? opts : {}; 55 | this.left; 56 | this.right; 57 | this.top; 58 | this.bottom; 59 | this.diagonal; 60 | this.outline; 61 | this.diagonalDown; 62 | this.diagonalUp; 63 | 64 | Object.keys(opts).forEach((opt) => { 65 | if (['outline', 'diagonalDown', 'diagonalUp'].indexOf(opt) >= 0) { 66 | if (typeof opts[opt] === 'boolean') { 67 | this[opt] = opts[opt]; 68 | } else { 69 | throw new TypeError('Border outline option must be of type Boolean'); 70 | } 71 | } else if (['left', 'right', 'top', 'bottom', 'diagonal'].indexOf(opt) < 0) { //TODO: move logic to types folder 72 | throw new TypeError(`Invalid key for border declaration ${opt}. Must be one of left, right, top, bottom, diagonal`); 73 | } else { 74 | this[opt] = new BorderOrdinal(opts[opt]); 75 | } 76 | }); 77 | } 78 | 79 | /** 80 | * @func Border.toObject 81 | * @desc Converts the Border instance to a javascript object 82 | * @returns {Object} 83 | */ 84 | toObject() { 85 | let obj = {}; 86 | obj.left; 87 | obj.right; 88 | obj.top; 89 | obj.bottom; 90 | obj.diagonal; 91 | 92 | if (this.left !== undefined) { 93 | obj.left = this.left.toObject(); 94 | } 95 | if (this.right !== undefined) { 96 | obj.right = this.right.toObject(); 97 | } 98 | if (this.top !== undefined) { 99 | obj.top = this.top.toObject(); 100 | } 101 | if (this.bottom !== undefined) { 102 | obj.bottom = this.bottom.toObject(); 103 | } 104 | if (this.diagonal !== undefined) { 105 | obj.diagonal = this.diagonal.toObject(); 106 | } 107 | typeof this.outline === 'boolean' ? obj.outline = this.outline : null; 108 | typeof this.diagonalDown === 'boolean' ? obj.diagonalDown = this.diagonalDown : null; 109 | typeof this.diagonalUp === 'boolean' ? obj.diagonalUp = this.diagonalUp : null; 110 | 111 | return obj; 112 | } 113 | 114 | /** 115 | * @alias Border.addToXMLele 116 | * @desc When generating Workbook output, attaches style to the styles xml file 117 | * @func Border.addToXMLele 118 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 119 | */ 120 | addToXMLele(borderXML) { 121 | let bXML = borderXML.ele('border'); 122 | if (this.outline === true) { 123 | bXML.att('outline', '1'); 124 | } 125 | if (this.diagonalUp === true) { 126 | bXML.att('diagonalUp', '1'); 127 | } 128 | if (this.diagonalDown === true) { 129 | bXML.att('diagonalDown', '1'); 130 | } 131 | 132 | ['left', 'right', 'top', 'bottom', 'diagonal'].forEach((ord) => { 133 | let thisOEle = bXML.ele(ord); 134 | if (this[ord] !== undefined) { 135 | if (this[ord].style !== undefined) { 136 | thisOEle.att('style', this[ord].style); 137 | } 138 | if (this[ord].color instanceof CTColor) { 139 | this[ord].color.addToXMLele(thisOEle); 140 | } 141 | } 142 | }); 143 | } 144 | } 145 | 146 | module.exports = Border; -------------------------------------------------------------------------------- /source/lib/style/classes/ctColor.js: -------------------------------------------------------------------------------- 1 | const types = require('../../types/index.js'); 2 | const xmlbuilder = require('xmlbuilder'); 3 | 4 | class CTColor { //§18.8.3 && §18.8.19 5 | /** 6 | * @class CTColor 7 | * @desc Excel color representation 8 | * @param {String} color Excel Color scheme or Excel Color name or HEX value of Color 9 | * @properties {String} type Type of color object. defaults to rgb 10 | * @properties {String} rgb ARGB representation of Color 11 | * @properties {String} theme Excel Color Scheme 12 | * @returns {CTColor} 13 | */ 14 | constructor(color) { 15 | this.type; 16 | this.rgb; 17 | this.theme; //§20.1.6.2 clrScheme (Color Scheme) : types.colorSchemes 18 | 19 | if (typeof color === 'string') { 20 | if (types.colorScheme[color.toLowerCase()] !== undefined) { 21 | this.theme = color; 22 | this.type = 'theme'; 23 | } else { 24 | try { 25 | this.rgb = types.excelColor.getColor(color); 26 | this.type = 'rgb'; 27 | } catch (e) { 28 | throw new TypeError(`Fill color must be an RGB value, Excel color (${types.excelColor.opts.join(', ')}) or Excel theme (${types.colorScheme.opts.join(', ')})`); 29 | } 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * @func CTColor.toObject 36 | * @desc Converts the CTColor instance to a javascript object 37 | * @returns {Object} 38 | */ 39 | toObject() { 40 | return this[this.type]; 41 | } 42 | 43 | /** 44 | * @alias CTColor.addToXMLele 45 | * @desc When generating Workbook output, attaches style to the styles xml file 46 | * @func CTColor.addToXMLele 47 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 48 | */ 49 | addToXMLele(ele) { 50 | let colorEle = ele.ele('color'); 51 | colorEle.att(this.type, this[this.type]); 52 | } 53 | } 54 | 55 | module.exports = CTColor; -------------------------------------------------------------------------------- /source/lib/style/classes/fill.js: -------------------------------------------------------------------------------- 1 | const types = require('../../types/index.js'); 2 | const xmlbuilder = require('xmlbuilder'); 3 | const CTColor = require('./ctColor.js'); 4 | 5 | class Stop { //§18.8.38 6 | /** 7 | * @class Stop 8 | * @desc Stops for Gradient fills 9 | * @param {Object} opts Options for Stop 10 | * @param {String} opts.color Color of Stop 11 | * @param {Number} opts.position Order of Stop with first stop being 0 12 | * @returns {Stop} 13 | */ 14 | constructor(opts, position) { 15 | this.color = new CTColor(opts.color); 16 | this.position = position; 17 | } 18 | 19 | /** 20 | * @func Stop.toObject 21 | * @desc Converts the Stop instance to a javascript object 22 | * @returns {Object} 23 | */ 24 | toObject() { 25 | let obj = {}; 26 | this.color !== undefined ? obj.color = this.color.toObject() : null; 27 | this.position !== undefined ? obj.position = this.position : null; 28 | return obj; 29 | } 30 | } 31 | 32 | class Fill { //§18.8.20 fill (Fill) 33 | 34 | /** 35 | * @class Fill 36 | * @desc Excel Fill 37 | * @param {Object} opts 38 | * @param {String} opts.type Type of Excel fill (gradient or pattern) 39 | * @param {Number} opts.bottom If Gradient fill, the position of the bottom edge of the inner rectange as a percentage in decimal form. (must be between 0 and 1) 40 | * @param {Number} opts.top If Gradient fill, the position of the top edge of the inner rectange as a percentage in decimal form. (must be between 0 and 1) 41 | * @param {Number} opts.left If Gradient fill, the position of the left edge of the inner rectange as a percentage in decimal form. (must be between 0 and 1) 42 | * @param {Number} opts.right If Gradient fill, the position of the right edge of the inner rectange as a percentage in decimal form. (must be between 0 and 1) 43 | * @param {Number} opts.degree Angle of the Gradient 44 | * @param {Array.Stop} opts.stops Array of position stops for gradient 45 | * @returns {Fill} 46 | */ 47 | constructor(opts) { 48 | 49 | if (['gradient', 'pattern', 'none'].indexOf(opts.type) >= 0) { 50 | this.type = opts.type; 51 | } else { 52 | throw new TypeError('Fill type must be one of gradient, pattern or none.'); 53 | } 54 | 55 | switch (this.type) { 56 | case 'gradient': //§18.8.24 57 | if (opts.bottom !== undefined) { 58 | if (opts.bottom < 0 || opts.bottom > 1) { 59 | throw new TypeError('Values for gradient fill bottom attribute must be a decimal between 0 and 1'); 60 | } else { 61 | this.bottom = opts.bottom; 62 | } 63 | } 64 | 65 | if (opts.degree !== undefined) { 66 | if (typeof opts.degree === 'number') { 67 | this.degree = opts.degree; 68 | } else { 69 | throw new TypeError('Values of gradient fill degree must be of type number.'); 70 | } 71 | } 72 | 73 | 74 | if (opts.left !== undefined) { 75 | if (opts.left < 0 || opts.left > 1) { 76 | throw new TypeError('Values for gradient fill left attribute must be a decimal between 0 and 1'); 77 | } else { 78 | this.left = opts.left; 79 | } 80 | } 81 | 82 | if (opts.right !== undefined) { 83 | if (opts.right < 0 || opts.right > 1) { 84 | throw new TypeError('Values for gradient fill right attribute must be a decimal between 0 and 1'); 85 | } else { 86 | this.right = opts.right; 87 | } 88 | } 89 | 90 | if (opts.top !== undefined) { 91 | if (opts.top < 0 || opts.top > 1) { 92 | throw new TypeError('Values for gradient fill top attribute must be a decimal between 0 and 1'); 93 | } else { 94 | this.top = opts.top; 95 | } 96 | } 97 | 98 | if (opts.stops !== undefined) { 99 | if (opts.stops instanceof Array) { 100 | opts.stops.forEach((s, i) => { 101 | this.stops.push(new Stop(s, i)); 102 | }); 103 | } else { 104 | throw new TypeError('Stops for gradient fills must be sent as an Array'); 105 | } 106 | } 107 | 108 | break; 109 | 110 | case 'pattern': //§18.8.32 111 | if (opts.bgColor !== undefined) { 112 | this.bgColor = new CTColor(opts.bgColor); 113 | } 114 | 115 | if (opts.fgColor !== undefined) { 116 | this.fgColor = new CTColor(opts.fgColor); 117 | } 118 | 119 | if (opts.patternType !== undefined) { 120 | types.fillPattern.validate(opts.patternType) === true ? this.patternType = opts.patternType : null; 121 | } 122 | break; 123 | 124 | case 'none': 125 | this.patternType = 'none'; 126 | break; 127 | } 128 | } 129 | 130 | /** 131 | * @func Fill.toObject 132 | * @desc Converts the Fill instance to a javascript object 133 | * @returns {Object} 134 | */ 135 | toObject() { 136 | let obj = {}; 137 | 138 | this.type !== undefined ? obj.type = this.type : null; 139 | this.bottom !== undefined ? obj.bottom = this.bottom : null; 140 | this.degree !== undefined ? obj.degree = this.degree : null; 141 | this.left !== undefined ? obj.left = this.left : null; 142 | this.right !== undefined ? obj.right = this.right : null; 143 | this.top !== undefined ? obj.top = this.top : null; 144 | this.bgColor !== undefined ? obj.bgColor = this.bgColor.toObject() : null; 145 | this.fgColor !== undefined ? obj.fgColor = this.fgColor.toObject() : null; 146 | this.patternType !== undefined ? obj.patternType = this.patternType : null; 147 | 148 | if (this.stops !== undefined) { 149 | obj.stop = []; 150 | this.stops.forEach((s) => { 151 | obj.stops.push(s.toObject()); 152 | }); 153 | } 154 | 155 | return obj; 156 | } 157 | 158 | /** 159 | * @alias Fill.addToXMLele 160 | * @desc When generating Workbook output, attaches style to the styles xml file 161 | * @func Fill.addToXMLele 162 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 163 | */ 164 | addToXMLele(fXML) { 165 | let pFill = fXML.ele('patternFill').att('patternType', this.patternType); 166 | 167 | if (this.fgColor instanceof CTColor) { 168 | pFill.ele('fgColor').att(this.fgColor.type, this.fgColor[this.fgColor.type]); 169 | } 170 | 171 | if (this.bgColor instanceof CTColor) { 172 | pFill.ele('bgColor').att(this.bgColor.type, this.bgColor[this.bgColor.type]); 173 | } 174 | } 175 | } 176 | 177 | module.exports = Fill; -------------------------------------------------------------------------------- /source/lib/style/classes/font.js: -------------------------------------------------------------------------------- 1 | const xmlbuilder = require('xmlbuilder'); 2 | const types = require('../../types/index.js'); 3 | 4 | class Font { 5 | /** 6 | * @class Font 7 | * @desc Instance of Font with properties 8 | * @param {Object} opts Options for Font 9 | * @param {String} opts.color HEX color of font 10 | * @param {String} opts.name Name of Font. i.e. Calibri 11 | * @param {String} opts.scheme Font Scheme. defaults to major 12 | * @param {Number} opts.size Pt size of Font 13 | * @param {String} opts.family Font Family. defaults to roman 14 | * @param {String} opts.vertAlign Specifies font as subscript or superscript 15 | * @param {Number} opts.charset Character set of font as defined in §18.4.1 charset (Character Set) or standard 16 | * @param {Boolean} opts.condense Macintosh compatibility settings to squeeze text together when rendering 17 | * @param {Boolean} opts.extend Stretches out the text when rendering 18 | * @param {Boolean} opts.bold States whether font should be bold 19 | * @param {Boolean} opts.italics States whether font should be in italics 20 | * @param {Boolean} opts.outline States whether font should be outlined 21 | * @param {Boolean} opts.shadow States whether font should have a shadow 22 | * @param {Boolean} opts.strike States whether font should have a strikethrough 23 | * @param {Boolean} opts.underline States whether font should be underlined 24 | * @retuns {Font} 25 | */ 26 | constructor(opts) { 27 | opts = opts ? opts : {}; 28 | 29 | typeof opts.color === 'string' ? this.color = types.excelColor.getColor(opts.color) : null; 30 | typeof opts.name === 'string' ? this.name = opts.name : null; 31 | typeof opts.scheme === 'string' ? this.scheme = opts.scheme : null; 32 | typeof opts.size === 'number' ? this.size = opts.size : null; 33 | typeof opts.family === 'string' && types.fontFamily.validate(opts.family) === true ? this.family = opts.family : null; 34 | 35 | typeof opts.vertAlign === 'string' ? this.vertAlign = opts.vertAlign : null; 36 | typeof opts.charset === 'number' ? this.charset = opts.charset : null; 37 | 38 | typeof opts.condense === 'boolean' ? this.condense = opts.condense : null; 39 | typeof opts.extend === 'boolean' ? this.extend = opts.extend : null; 40 | typeof opts.bold === 'boolean' ? this.bold = opts.bold : null; 41 | typeof opts.italics === 'boolean' ? this.italics = opts.italics : null; 42 | typeof opts.outline === 'boolean' ? this.outline = opts.outline : null; 43 | typeof opts.shadow === 'boolean' ? this.shadow = opts.shadow : null; 44 | typeof opts.strike === 'boolean' ? this.strike = opts.strike : null; 45 | typeof opts.underline === 'boolean' ? this.underline = opts.underline : null; 46 | } 47 | 48 | /** 49 | * @func Font.toObject 50 | * @desc Converts the Font instance to a javascript object 51 | * @returns {Object} 52 | */ 53 | toObject() { 54 | let obj = {}; 55 | 56 | typeof this.charset === 'number' ? obj.charset = this.charset : null; 57 | typeof this.color === 'string' ? obj.color = this.color : null; 58 | typeof this.family === 'string' ? obj.family = this.family : null; 59 | typeof this.name === 'string' ? obj.name = this.name : null; 60 | typeof this.scheme === 'string' ? obj.scheme = this.scheme : null; 61 | typeof this.size === 'number' ? obj.size = this.size : null; 62 | typeof this.vertAlign === 'string' ? obj.vertAlign = this.vertAlign : null; 63 | 64 | typeof this.condense === 'boolean' ? obj.condense = this.condense : null; 65 | typeof this.extend === 'boolean' ? obj.extend = this.extend : null; 66 | typeof this.bold === 'boolean' ? obj.bold = this.bold : null; 67 | typeof this.italics === 'boolean' ? obj.italics = this.italics : null; 68 | typeof this.outline === 'boolean' ? obj.outline = this.outline : null; 69 | typeof this.shadow === 'boolean' ? obj.shadow = this.shadow : null; 70 | typeof this.strike === 'boolean' ? obj.strike = this.strike : null; 71 | typeof this.underline === 'boolean' ? obj.underline = this.underline : null; 72 | 73 | return obj; 74 | } 75 | 76 | /** 77 | * @alias Font.addToXMLele 78 | * @desc When generating Workbook output, attaches style to the styles xml file 79 | * @func Font.addToXMLele 80 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 81 | */ 82 | addToXMLele(fontXML) { 83 | let fEle = fontXML.ele('font'); 84 | 85 | // Place styling elements first to avoid validation errors with .NET validator 86 | this.condense === true ? fEle.ele('condense') : null; 87 | this.extend === true ? fEle.ele('extend') : null; 88 | this.bold === true ? fEle.ele('b') : null; 89 | this.italics === true ? fEle.ele('i') : null; 90 | this.outline === true ? fEle.ele('outline') : null; 91 | this.shadow === true ? fEle.ele('shadow') : null; 92 | this.strike === true ? fEle.ele('strike') : null; 93 | this.underline === true ? fEle.ele('u') : null; 94 | this.vertAlign === true ? fEle.ele('vertAlign') : null; 95 | 96 | fEle.ele('sz').att('val', this.size !== undefined ? this.size : 12); 97 | fEle.ele('color').att('rgb', this.color !== undefined ? this.color : 'FF000000'); 98 | fEle.ele('name').att('val', this.name !== undefined ? this.name : 'Calibri'); 99 | if (this.family !== undefined) { 100 | fEle.ele('family').att('val', types.fontFamily[this.family.toLowerCase()]); 101 | } 102 | if (this.scheme !== undefined) { 103 | fEle.ele('scheme').att('val', this.scheme); 104 | } 105 | 106 | 107 | return true; 108 | } 109 | 110 | 111 | } 112 | 113 | module.exports = Font; -------------------------------------------------------------------------------- /source/lib/style/classes/numberFormat.js: -------------------------------------------------------------------------------- 1 | class NumberFormat { 2 | /** 3 | * @class NumberFormat 4 | * @param {String} fmt Format of the Number 5 | * @returns {NumberFormat} 6 | */ 7 | constructor(fmt) { 8 | this.formatCode = fmt; 9 | this.id; 10 | } 11 | 12 | get numFmtId() { 13 | return this.id; 14 | } 15 | set numFmtId(id) { 16 | this.id = id; 17 | } 18 | 19 | /** 20 | * @alias NumberFormat.addToXMLele 21 | * @desc When generating Workbook output, attaches style to the styles xml file 22 | * @func NumberFormat.addToXMLele 23 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 24 | */ 25 | addToXMLele(ele) { 26 | if (this.formatCode !== undefined) { 27 | ele.ele('numFmt') 28 | .att('formatCode', this.formatCode) 29 | .att('numFmtId', this.numFmtId); 30 | } 31 | } 32 | } 33 | 34 | module.exports = NumberFormat; -------------------------------------------------------------------------------- /source/lib/style/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./style.js'); -------------------------------------------------------------------------------- /source/lib/style/style.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | const deepmerge = require('deepmerge'); 3 | 4 | const Alignment = require('./classes/alignment.js'); 5 | const Border = require('./classes/border.js'); 6 | const Fill = require('./classes/fill.js'); 7 | const Font = require('./classes/font.js'); 8 | const NumberFormat = require('./classes/numberFormat.js'); 9 | 10 | let _getFontId = (wb, font = {}) => { 11 | 12 | // Create the Font and lookup key 13 | font = deepmerge(wb.opts.defaultFont, font); 14 | const thisFont = new Font(font); 15 | const lookupKey = JSON.stringify(thisFont.toObject()); 16 | 17 | // Find an existing entry, creating a new one if it does not exist 18 | let id = wb.styleDataLookup.fonts[lookupKey]; 19 | if (id === undefined) { 20 | id = wb.styleData.fonts.push(thisFont) - 1; 21 | wb.styleDataLookup.fonts[lookupKey] = id; 22 | } 23 | 24 | return id; 25 | }; 26 | 27 | let _getFillId = (wb, fill) => { 28 | if (fill === undefined) { 29 | return null; 30 | } 31 | 32 | // Create the Fill and lookup key 33 | const thisFill = new Fill(fill); 34 | const lookupKey = JSON.stringify(thisFill.toObject()); 35 | 36 | // Find an existing entry, creating a new one if it does not exist 37 | let id = wb.styleDataLookup.fills[lookupKey]; 38 | if (id === undefined) { 39 | id = wb.styleData.fills.push(thisFill) - 1; 40 | wb.styleDataLookup.fills[lookupKey] = id; 41 | } 42 | 43 | return id; 44 | }; 45 | 46 | let _getBorderId = (wb, border) => { 47 | if (border === undefined) { 48 | return null; 49 | } 50 | 51 | // Create the Border and lookup key 52 | const thisBorder = new Border(border); 53 | const lookupKey = JSON.stringify(thisBorder.toObject()); 54 | 55 | // Find an existing entry, creating a new one if it does not exist 56 | let id = wb.styleDataLookup.borders[lookupKey]; 57 | if (id === undefined) { 58 | id = wb.styleData.borders.push(thisBorder) - 1; 59 | wb.styleDataLookup.borders[lookupKey] = id; 60 | } 61 | 62 | return id; 63 | }; 64 | 65 | let _getNumFmt = (wb, val) => { 66 | let fmt; 67 | wb.styleData.numFmts.forEach((f) => { 68 | if (f.formatCode === val) { 69 | fmt = f; 70 | } 71 | }); 72 | 73 | if (fmt === undefined) { 74 | let fmtId = wb.styleData.numFmts.length + 164; 75 | fmt = new NumberFormat(val); 76 | fmt.numFmtId = fmtId; 77 | wb.styleData.numFmts.push(fmt); 78 | } 79 | 80 | return fmt; 81 | }; 82 | 83 | 84 | /* 85 | Style Opts 86 | { 87 | alignment: { // §18.8.1 88 | horizontal: ['center', 'centerContinuous', 'distributed', 'fill', 'general', 'justify', 'left', 'right'], 89 | indent: integer, // Number of spaces to indent = indent value * 3 90 | justifyLastLine: boolean, 91 | readingOrder: ['contextDependent', 'leftToRight', 'rightToLeft'], 92 | relativeIndent: integer, // number of additional spaces to indent 93 | shrinkToFit: boolean, 94 | textRotation: integer, // number of degrees to rotate text counter-clockwise 95 | vertical: ['bottom', 'center', 'distributed', 'justify', 'top'], 96 | wrapText: boolean 97 | }, 98 | font: { // §18.8.22 99 | bold: boolean, 100 | charset: integer, 101 | color: string, 102 | condense: boolean, 103 | extend: boolean, 104 | family: string, 105 | italics: boolean, 106 | name: string, 107 | outline: boolean, 108 | scheme: string, // §18.18.33 ST_FontScheme (Font scheme Styles) 109 | shadow: boolean, 110 | strike: boolean, 111 | size: integer, 112 | underline: boolean, 113 | vertAlign: string // §22.9.2.17 ST_VerticalAlignRun (Vertical Positioning Location) 114 | }, 115 | border: { // §18.8.4 border (Border) 116 | left: { 117 | style: string, 118 | color: string 119 | }, 120 | right: { 121 | style: string, 122 | color: string 123 | }, 124 | top: { 125 | style: string, 126 | color: string 127 | }, 128 | bottom: { 129 | style: string, 130 | color: string 131 | }, 132 | diagonal: { 133 | style: string, 134 | color: string 135 | }, 136 | diagonalDown: boolean, 137 | diagonalUp: boolean, 138 | outline: boolean 139 | }, 140 | fill: { // §18.8.20 fill (Fill) 141 | type: 'pattern', 142 | patternType: 'solid', 143 | color: 'Yellow' 144 | }, 145 | numberFormat: integer or string // §18.8.30 numFmt (Number Format) 146 | } 147 | */ 148 | class Style { 149 | constructor(wb, opts) { 150 | /** 151 | * Excel Style object 152 | * @class Style 153 | * @desc Style object for formatting Excel Cells 154 | * @param {Workbook} wb Excel Workbook object 155 | * @param {Object} opts Options for style 156 | * @param {Object} opts.alignment Options for creating an Alignment instance 157 | * @param {Object} opts.font Options for creating a Font instance 158 | * @param {Object} opts.border Options for creating a Border instance 159 | * @param {Object} opts.fill Options for creating a Fill instance 160 | * @param {String} opts.numberFormat 161 | * @property {Alignment} alignment Alignment instance associated with Style 162 | * @property {Border} border Border instance associated with Style 163 | * @property {Number} borderId ID of Border instance in the Workbook 164 | * @property {Fill} fill Fill instance associated with Style 165 | * @property {Number} fillId ID of Fill instance in the Workbook 166 | * @property {Font} font Font instance associated with Style 167 | * @property {Number} fontId ID of Font instance in the Workbook 168 | * @property {String} numberFormat String represenation of the way a number should be formatted 169 | * @property {Number} xf XF id of the Style in the Workbook 170 | * @returns {Style} 171 | */ 172 | opts = opts ? opts : {}; 173 | opts = deepmerge(wb.styles[0] ? wb.styles[0] : {}, opts); 174 | 175 | if (opts.alignment !== undefined) { 176 | this.alignment = new Alignment(opts.alignment); 177 | } 178 | 179 | if (opts.border !== undefined) { 180 | this.borderId = _getBorderId(wb, opts.border); // attribute 0 based index 181 | this.border = wb.styleData.borders[this.borderId]; 182 | } 183 | if (opts.fill !== undefined) { 184 | this.fillId = _getFillId(wb, opts.fill); // attribute 0 based index 185 | this.fill = wb.styleData.fills[this.fillId]; 186 | } 187 | 188 | if (opts.font !== undefined) { 189 | this.fontId = _getFontId(wb, opts.font); // attribute 0 based index 190 | this.font = wb.styleData.fonts[this.fontId]; 191 | } 192 | 193 | if (opts.numberFormat !== undefined) { 194 | if (typeof opts.numberFormat === 'number' && opts.numberFormat <= 164) { 195 | this.numFmtId = opts.numberFormat; 196 | } else if (typeof opts.numberFormat === 'string') { 197 | this.numFmt = _getNumFmt(wb, opts.numberFormat); 198 | } 199 | } 200 | 201 | if (opts.pivotButton !== undefined) { 202 | this.pivotButton = null; // attribute boolean 203 | } 204 | 205 | if (opts.quotePrefix !== undefined) { 206 | this.quotePrefix = null; // attribute boolean 207 | } 208 | 209 | this.ids = {}; 210 | } 211 | 212 | get xf() { 213 | let thisXF = {}; 214 | 215 | if (typeof this.fontId === 'number') { 216 | thisXF.applyFont = 1; 217 | thisXF.fontId = this.fontId; 218 | } 219 | 220 | if (typeof this.fillId === 'number') { 221 | thisXF.applyFill = 1; 222 | thisXF.fillId = this.fillId; 223 | } 224 | 225 | if (typeof this.borderId === 'number') { 226 | thisXF.applyBorder = 1; 227 | thisXF.borderId = this.borderId; 228 | } 229 | 230 | if (typeof this.numFmtId === 'number') { 231 | thisXF.applyNumberFormat = 1; 232 | thisXF.numFmtId = this.numFmtId; 233 | } else if (this.numFmt !== undefined && this.numFmt !== null) { 234 | thisXF.applyNumberFormat = 1; 235 | thisXF.numFmtId = this.numFmt.numFmtId; 236 | } 237 | 238 | if (this.alignment instanceof Alignment) { 239 | thisXF.applyAlignment = 1; 240 | thisXF.alignment = this.alignment; 241 | } 242 | 243 | return thisXF; 244 | } 245 | 246 | 247 | /** 248 | * @func Style.toObject 249 | * @desc Converts the Style instance to a javascript object 250 | * @returns {Object} 251 | */ 252 | toObject() { 253 | let obj = {}; 254 | 255 | if (typeof this.fontId === 'number') { 256 | obj.font = this.font.toObject(); 257 | } 258 | 259 | if (typeof this.fillId === 'number') { 260 | obj.fill = this.fill.toObject(); 261 | } 262 | 263 | if (typeof this.borderId === 'number') { 264 | obj.border = this.border.toObject(); 265 | } 266 | 267 | if (typeof this.numFmtId === 'number' && this.numFmtId < 164) { 268 | obj.numberFormat = this.numFmtId; 269 | } else if (this.numFmt !== undefined && this.numFmt !== null) { 270 | obj.numberFormat = this.numFmt.formatCode; 271 | } 272 | 273 | if (this.alignment instanceof Alignment) { 274 | obj.alignment = this.alignment.toObject(); 275 | } 276 | 277 | if (this.pivotButton !== undefined) { 278 | obj.pivotButton = this.pivotButton; 279 | } 280 | 281 | if (this.quotePrefix !== undefined) { 282 | obj.quotePrefix = this.quotePrefix; 283 | } 284 | 285 | return obj; 286 | } 287 | 288 | /** 289 | * @alias Style.addToXMLele 290 | * @desc When generating Workbook output, attaches style to the styles xml file 291 | * @func Style.addToXMLele 292 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 293 | */ 294 | addXFtoXMLele(ele) { 295 | let thisEle = ele.ele('xf'); 296 | let thisXF = this.xf; 297 | Object.keys(thisXF).forEach((a) => { 298 | if (a === 'alignment') { 299 | thisXF[a].addToXMLele(thisEle); 300 | } else { 301 | thisEle.att(a, thisXF[a]); 302 | } 303 | }); 304 | } 305 | 306 | /** 307 | * @alias Style.addDXFtoXMLele 308 | * @desc When generating Workbook output, attaches style to the styles xml file as a dxf for use with conditional formatting rules 309 | * @func Style.addDXFtoXMLele 310 | * @param {xmlbuilder.Element} ele Element object of the xmlbuilder module 311 | */ 312 | addDXFtoXMLele(ele) { 313 | let thisEle = ele.ele('dxf'); 314 | 315 | if (this.font instanceof Font) { 316 | this.font.addToXMLele(thisEle); 317 | } 318 | 319 | if (this.numFmt instanceof NumberFormat) { 320 | this.numFmt.addToXMLele(thisEle); 321 | } 322 | 323 | if (this.fill instanceof Fill) { 324 | this.fill.addToXMLele(thisEle.ele('fill')); 325 | } 326 | 327 | if (this.alignment instanceof Alignment) { 328 | this.alignment.addToXMLele(thisEle); 329 | } 330 | 331 | if (this.border instanceof Border) { 332 | this.border.addToXMLele(thisEle); 333 | } 334 | } 335 | } 336 | 337 | module.exports = Style; -------------------------------------------------------------------------------- /source/lib/types/alignment.js: -------------------------------------------------------------------------------- 1 | function horizontalAlignments() { 2 | this.opts = [ // §18.18.40 ST_HorizontalAlignment (Horizontal Alignment Type) 3 | 'center', 4 | 'centerContinuous', 5 | 'distributed', 6 | 'fill', 7 | 'general', 8 | 'justify', 9 | 'left', 10 | 'right' 11 | ]; 12 | this.opts.forEach((o, i) => { 13 | this[o] = i + 1; 14 | }); 15 | } 16 | 17 | function verticalAlignments() { 18 | this.opts = [ //§18.18.88 ST_VerticalAlignment (Vertical Alignment Types) 19 | 'bottom', 20 | 'center', 21 | 'distributed', 22 | 'justify', 23 | 'top' 24 | ]; 25 | this.opts.forEach((o, i) => { 26 | this[o] = i + 1; 27 | }); 28 | } 29 | 30 | function readingOrders() { 31 | this['contextDependent'] = 0; 32 | this['leftToRight'] = 1; 33 | this['rightToLeft'] = 2; 34 | this.opts = ['contextDependent', 'leftToRight', 'rightToLeft']; 35 | } 36 | 37 | horizontalAlignments.prototype.validate = function (val) { 38 | if (this[val] === undefined) { 39 | let opts = []; 40 | for (let name in this) { 41 | if (this.hasOwnProperty(name)) { 42 | opts.push(name); 43 | } 44 | } 45 | throw new TypeError(`Invalid value for alignment.horizontal ${val}; Value must be one of ${this.opts.join(', ')}`); 46 | } else { 47 | return true; 48 | } 49 | }; 50 | 51 | verticalAlignments.prototype.validate = function (val) { 52 | if (this[val] === undefined) { 53 | let opts = []; 54 | for (let name in this) { 55 | if (this.hasOwnProperty(name)) { 56 | opts.push(name); 57 | } 58 | } 59 | throw new TypeError(`Invalid value for alignment.vertical ${val}; Value must be one of ${this.opts.join(', ')}`); 60 | } else { 61 | return true; 62 | } 63 | }; 64 | 65 | readingOrders.prototype.validate = function (val) { 66 | if (this[val] === undefined) { 67 | let opts = []; 68 | for (let name in this) { 69 | if (this.hasOwnProperty(name)) { 70 | opts.push(name); 71 | } 72 | } 73 | throw new TypeError(`Invalid value for alignment.readingOrder ${val}; Value must be one of ${this.opts.join(', ')}`); 74 | } else { 75 | return true; 76 | } 77 | }; 78 | 79 | module.exports.vertical = new verticalAlignments(); 80 | module.exports.horizontal = new horizontalAlignments(); 81 | module.exports.readingOrder = new readingOrders(); -------------------------------------------------------------------------------- /source/lib/types/borderStyle.js: -------------------------------------------------------------------------------- 1 | function items() { 2 | this.opts = [//§18.18.3 ST_BorderStyle (Border Line Styles) 3 | 'none', 4 | 'thin', 5 | 'medium', 6 | 'dashed', 7 | 'dotted', 8 | 'thick', 9 | 'double', 10 | 'hair', 11 | 'mediumDashed', 12 | 'dashDot', 13 | 'mediumDashDot', 14 | 'dashDotDot', 15 | 'mediumDashDotDot', 16 | 'slantDashDot' 17 | ]; 18 | this.opts.forEach((o, i) => { 19 | this[o] = i + 1; 20 | }); 21 | } 22 | 23 | 24 | items.prototype.validate = function (val) { 25 | if (this[val] === undefined) { 26 | let opts = []; 27 | for (let name in this) { 28 | if (this.hasOwnProperty(name)) { 29 | opts.push(name); 30 | } 31 | } 32 | throw new TypeError('Invalid value for ST_BorderStyle; Value must be one of ' + this.opts.join(', ')); 33 | } else { 34 | return true; 35 | } 36 | }; 37 | 38 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/cellComment.js: -------------------------------------------------------------------------------- 1 | //§18.18.5 ST_CellComments (Cell Comments) 2 | 3 | function items() { 4 | this.opts = ['none', 'asDisplayed', 'atEnd']; 5 | this.opts.forEach((o, i) => { 6 | this[o] = i + 1; 7 | }); 8 | } 9 | 10 | 11 | items.prototype.validate = function (val) { 12 | if (this[val] === undefined) { 13 | let opts = []; 14 | for (let name in this) { 15 | if (this.hasOwnProperty(name)) { 16 | opts.push(name); 17 | } 18 | } 19 | throw new TypeError('Invalid value for ST_CellComments; Value must be one of ' + this.opts.join(', ')); 20 | } else { 21 | return true; 22 | } 23 | }; 24 | 25 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/colorScheme.js: -------------------------------------------------------------------------------- 1 | function items() { 2 | this.opts = [//§20.1.6.2 clrScheme (Color Scheme) 3 | 'dark 1', 4 | 'light 1', 5 | 'dark 2', 6 | 'light 2', 7 | 'accent 1', 8 | 'accent 2', 9 | 'accent 3', 10 | 'accent 4', 11 | 'accent 5', 12 | 'accent 6', 13 | 'hyperlink', 14 | 'followed hyperlink' 15 | ]; 16 | this.opts.forEach((o, i) => { 17 | this[o] = i + 1; 18 | }); 19 | } 20 | 21 | 22 | items.prototype.validate = function (val) { 23 | if (this[val.toLowerCase()] === undefined) { 24 | let opts = []; 25 | for (let name in this) { 26 | if (this.hasOwnProperty(name)) { 27 | opts.push(name); 28 | } 29 | } 30 | throw new TypeError('Invalid value for clrScheme; Value must be one of ' + this.opts.join(', ')); 31 | } else { 32 | return true; 33 | } 34 | }; 35 | 36 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/excelColor.js: -------------------------------------------------------------------------------- 1 | function items() { 2 | // subset of §20.1.10.48 ST_PresetColorVal (Preset Color Value) 3 | this['aqua'] = 'FF33CCCC'; 4 | this['black'] = 'FF000000'; 5 | this['blue'] = 'FF0000FF'; 6 | this['blue-gray'] = 'FF666699'; 7 | this['bright green'] = 'FF00FF00'; 8 | this['brown'] = 'FF993300'; 9 | this['dark blue'] = 'FF000080'; 10 | this['dark green'] = 'FF003300'; 11 | this['dark red'] = 'FF800000'; 12 | this['dark teal'] = 'FF003366'; 13 | this['dark yellow'] = 'FF808000'; 14 | this['gold'] = 'FFFFCC00'; 15 | this['gray-25'] = 'FFC0C0C0'; 16 | this['gray-40'] = 'FF969696'; 17 | this['gray-50'] = 'FF808080'; 18 | this['gray-80'] = 'FF333333'; 19 | this['green'] = 'FF008000'; 20 | this['indigo'] = 'FF333399'; 21 | this['lavender'] = 'FFCC99FF'; 22 | this['light blue'] = 'FF3366FF'; 23 | this['light green'] = 'FFCCFFCC'; 24 | this['light orange'] = 'FFFF9900'; 25 | this['light turquoise'] = 'FFCCFFFF'; 26 | this['light yellow'] = 'FFFFFF99'; 27 | this['lime'] = 'FF99CC00'; 28 | this['olive green'] = 'FF333300'; 29 | this['orange'] = 'FFFF6600'; 30 | this['pale blue'] = 'FF99CCFF'; 31 | this['pink'] = 'FFFF00FF'; 32 | this['plum'] = 'FF993366'; 33 | this['red'] = 'FFFF0000'; 34 | this['rose'] = 'FFFF99CC'; 35 | this['sea green'] = 'FF339966'; 36 | this['sky blue'] = 'FF00CCFF'; 37 | this['tan'] = 'FFFFCC99'; 38 | this['teal'] = 'FF008080'; 39 | this['turquoise'] = 'FF00FFFF'; 40 | this['violet'] = 'FF800080'; 41 | this['white'] = 'FFFFFFFF'; 42 | this['yellow'] = 'FFFFFF00'; 43 | 44 | this.opts = []; 45 | Object.keys(this).forEach((k) => { 46 | if (typeof this[k] === 'string') { 47 | this.opts.push(k); 48 | } 49 | }); 50 | } 51 | 52 | 53 | items.prototype.validate = function (val) { 54 | if (this[val.toLowerCase()] === undefined) { 55 | let opts = []; 56 | for (let name in this) { 57 | if (this.hasOwnProperty(name)) { 58 | opts.push(name); 59 | } 60 | } 61 | throw new TypeError('Invalid value for ST_PresetColorVal; Value must be one of ' + this.opts.join(', ')); 62 | } else { 63 | return true; 64 | } 65 | }; 66 | 67 | items.prototype.getColor = function (val) { 68 | // check for RGB, RGBA or Excel Color Names and return RGBA 69 | 70 | if (typeof this[val.toLowerCase()] === 'string') { 71 | // val was a named color that matches predefined list. return corresponding color 72 | return this[val.toLowerCase()]; 73 | } else if (val.length === 8 && /^[a-fA-F0-9()]+$/.test(val)) { 74 | // val is already a properly formatted color string, return upper case version of itself 75 | return val.toUpperCase(); 76 | } else if (val.length === 6 && /^[a-fA-F0-9()]+$/.test(val)) { 77 | // val is color code without Alpha, add it and return 78 | return 'FF' + val.toUpperCase(); 79 | } else if (val.length === 7 && val.substr(0, 1) === '#' && /^[a-fA-F0-9()]+$/.test(val.substr(1))) { 80 | // val was sent as html style hex code, remove # and add alpha 81 | return 'FF' + val.substr(1).toUpperCase(); 82 | } else { 83 | // I don't know what this is, return valid color and console.log error 84 | throw new TypeError('valid color options are html style hex codes, ARGB strings or these colors by name: %s', this.opts.join(', ')); 85 | } 86 | }; 87 | 88 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/fillPattern.js: -------------------------------------------------------------------------------- 1 | function items() { 2 | this.opts = [//§18.18.55 ST_PatternType (Pattern Type) 3 | 'darkDown', 4 | 'darkGray', 5 | 'darkGrid', 6 | 'darkHorizontal', 7 | 'darkTrellis', 8 | 'darkUp', 9 | 'darkVerical', 10 | 'gray0625', 11 | 'gray125', 12 | 'lightDown', 13 | 'lightGray', 14 | 'lightGrid', 15 | 'lightHorizontal', 16 | 'lightTrellis', 17 | 'lightUp', 18 | 'lightVertical', 19 | 'mediumGray', 20 | 'none', 21 | 'solid' 22 | ]; 23 | this.opts.forEach((o, i) => { 24 | this[o] = i + 1; 25 | }); 26 | } 27 | 28 | 29 | items.prototype.validate = function (val) { 30 | if (this[val] === undefined) { 31 | let opts = []; 32 | for (let name in this) { 33 | if (this.hasOwnProperty(name)) { 34 | opts.push(name); 35 | } 36 | } 37 | throw new TypeError('Invalid value for ST_PatternType; Value must be one of ' + this.opts.join(', ')); 38 | } else { 39 | return true; 40 | } 41 | }; 42 | 43 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/fontFamily.js: -------------------------------------------------------------------------------- 1 | function items() { 2 | this.opts = [//§18.8.18 family (Font Family) 3 | 'n/a', 4 | 'roman', 5 | 'swiss', 6 | 'modern', 7 | 'script', 8 | 'decorative' 9 | ]; 10 | this.opts.forEach((o, i) => { 11 | this[o] = i; 12 | }); 13 | } 14 | 15 | 16 | items.prototype.validate = function (val) { 17 | if (typeof val !== 'string') { 18 | throw new TypeError(`Invalid value for Font Family ${val}; Value must be one of ${this.opts.join(', ')}`); 19 | } 20 | 21 | if (this[val.toLowerCase()] === undefined) { 22 | let opts = []; 23 | for (let name in this) { 24 | if (this.hasOwnProperty(name)) { 25 | opts.push(name); 26 | } 27 | } 28 | throw new TypeError(`Invalid value for Font Family ${val}; Value must be one of ${this.opts.join(', ')}`); 29 | } else { 30 | return true; 31 | } 32 | }; 33 | 34 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.alignment = require('./alignment'); 4 | exports.borderStyle = require('./borderStyle'); 5 | exports.cellComment = require('./cellComment'); 6 | exports.colorScheme = require('./colorScheme'); 7 | exports.excelColor = require('./excelColor'); 8 | exports.fillPattern = require('./fillPattern'); 9 | exports.fontFamily = require('./fontFamily'); 10 | exports.orientation = require('./orientation'); 11 | exports.pageOrder = require('./pageOrder'); 12 | exports.pane = require('./pane'); 13 | exports.paneState = require('./paneState'); 14 | exports.paperSize = require('./paperSize'); 15 | exports.positiveUniversalMeasure = require('./positiveUniversalMeasure'); 16 | exports.printError = require('./printError'); 17 | -------------------------------------------------------------------------------- /source/lib/types/orientation.js: -------------------------------------------------------------------------------- 1 | //§18.18.50 ST_Orientation (Orientation) 2 | 3 | function items() { 4 | let opts = ['default', 'portrait', 'landscape']; 5 | opts.forEach((o, i) => { 6 | this[o] = i + 1; 7 | }); 8 | } 9 | 10 | 11 | items.prototype.validate = function (val) { 12 | if (this[val.toLowerCase()] === undefined) { 13 | let opts = []; 14 | for (let name in this) { 15 | if (this.hasOwnProperty(name)) { 16 | opts.push(name); 17 | } 18 | } 19 | throw new TypeError('Invalid value for pageSetup.orientation; Value must be one of ' + opts.join(', ')); 20 | } else { 21 | return true; 22 | } 23 | }; 24 | 25 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/pageOrder.js: -------------------------------------------------------------------------------- 1 | //§18.18.51 ST_PageOrder (Page Order) 2 | 3 | function items() { 4 | let opts = ['downThenOver', 'overThenDown']; 5 | opts.forEach((o, i) => { 6 | this[o] = i + 1; 7 | }); 8 | } 9 | 10 | 11 | items.prototype.validate = function (val) { 12 | if (this[val] === undefined) { 13 | let opts = []; 14 | for (let name in this) { 15 | if (this.hasOwnProperty(name)) { 16 | opts.push(name); 17 | } 18 | } 19 | throw new TypeError('Invalid value for pageSetup.pageOrder; Value must be one of ' + opts.join(', ')); 20 | } else { 21 | return true; 22 | } 23 | }; 24 | 25 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/pane.js: -------------------------------------------------------------------------------- 1 | //§18.18.52 ST_Pane (Pane Types) 2 | 3 | function items() { 4 | let opts = ['bottomLeft', 'bottomRight', 'topLeft', 'topRight']; 5 | opts.forEach((o, i) => { 6 | this[o] = i + 1; 7 | }); 8 | } 9 | 10 | 11 | items.prototype.validate = function (val) { 12 | if (this[val] === undefined) { 13 | let opts = []; 14 | for (let name in this) { 15 | if (this.hasOwnProperty(name)) { 16 | opts.push(name); 17 | } 18 | } 19 | throw new TypeError('Invalid value for sheetview.pane.activePane; Value must be one of ' + opts.join(', ')); 20 | } else { 21 | return true; 22 | } 23 | }; 24 | 25 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/paneState.js: -------------------------------------------------------------------------------- 1 | //§ST_PaneState (Pane State) 2 | 3 | function items() { 4 | let opts = ['split', 'frozen', 'frozenSplit']; 5 | opts.forEach((o, i) => { 6 | this[o] = i + 1; 7 | }); 8 | } 9 | 10 | 11 | items.prototype.validate = function (val) { 12 | if (this[val] === undefined) { 13 | let opts = []; 14 | for (let name in this) { 15 | if (this.hasOwnProperty(name)) { 16 | opts.push(name); 17 | } 18 | } 19 | throw new TypeError('Invalid value for sheetView.pane.state; Value must be one of ' + opts.join(', ')); 20 | } else { 21 | return true; 22 | } 23 | }; 24 | 25 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/paperSize.js: -------------------------------------------------------------------------------- 1 | function items() { // As defined in §18.3.1.63 pageSetup (Page Setup Settings) 2 | this.LETTER_PAPER = 1; // Letter paper (8.5 in. by 11 in.) 3 | this.LETTER_SMALL_PAPER = 2; // Letter small paper (8.5 in. by 11 in.) 4 | this.TABLOID_PAPER = 3; // Tabloid paper (11 in. by 17 in.) 5 | this.LEDGER_PAPER = 4; // Ledger paper (17 in. by 11 in.) 6 | this.LEGAL_PAPER = 5; // Legal paper (8.5 in. by 14 in.) 7 | this.STATEMENT_PAPER = 6; // Statement paper (5.5 in. by 8.5 in.) 8 | this.EXECUTIVE_PAPER = 7; // Executive paper (7.25 in. by 10.5 in.) 9 | this.A3_PAPER = 8; // A3 paper (297 mm by 420 mm) 10 | this.A4_PAPER = 9; // A4 paper (210 mm by 297 mm) 11 | this.A4_SMALL_PAPER = 10; // A4 small paper (210 mm by 297 mm) 12 | this.A5_PAPER = 11; // A5 paper (148 mm by 210 mm) 13 | this.B4_PAPER = 12; // B4 paper (250 mm by 353 mm) 14 | this.B5_PAPER = 13; // B5 paper (176 mm by 250 mm) 15 | this.FOLIO_PAPER = 14; // Folio paper (8.5 in. by 13 in.) 16 | this.QUARTO_PAPER = 15; // Quarto paper (215 mm by 275 mm) 17 | this.STANDARD_PAPER_10_BY_14_IN = 16; // Standard paper (10 in. by 14 in.) 18 | this.STANDARD_PAPER_11_BY_17_IN = 17; // Standard paper (11 in. by 17 in.) 19 | this.NOTE_PAPER = 18; // Note paper (8.5 in. by 11 in.) 20 | this.NUMBER_9_ENVELOPE = 19; // #9 envelope (3.875 in. by 8.875 in.) 21 | this.NUMBER_10_ENVELOPE = 20; // #10 envelope (4.125 in. by 9.5 in.) 22 | this.NUMBER_11_ENVELOPE = 21; // #11 envelope (4.5 in. by 10.375 in.) 23 | this.NUMBER_12_ENVELOPE = 22; // #12 envelope (4.75 in. by 11 in.) 24 | this.NUMBER_14_ENVELOPE = 23; // #14 envelope (5 in. by 11.5 in.) 25 | this.C_PAPER = 24; // C paper (17 in. by 22 in.) 26 | this.D_PAPER = 25; // D paper (22 in. by 34 in.) 27 | this.E_PAPER = 26; // E paper (34 in. by 44 in.) 28 | this.DL_PAPER = 27; // DL envelope (110 mm by 220 mm) 29 | this.C5_ENVELOPE = 28; // C5 envelope (162 mm by 229 mm) 30 | this.C3_ENVELOPE = 29; // C3 envelope (324 mm by 458 mm) 31 | this.C4_ENVELOPE = 30; // C4 envelope (229 mm by 324 mm) 32 | this.C6_ENVELOPE = 31; // C6 envelope (114 mm by 162 mm) 33 | this.C65_ENVELOPE = 32; // C65 envelope (114 mm by 229 mm) 34 | this.B4_ENVELOPE = 33; // B4 envelope (250 mm by 353 mm) 35 | this.B5_ENVELOPE = 34; // B5 envelope (176 mm by 250 mm) 36 | this.B6_ENVELOPE = 35; // B6 envelope (176 mm by 125 mm) 37 | this.ITALY_ENVELOPE = 36; // Italy envelope (110 mm by 230 mm) 38 | this.MONARCH_ENVELOPE = 37; // Monarch envelope (3.875 in. by 7.5 in.). 39 | this.SIX_THREE_QUARTERS_ENVELOPE = 38; // 6 3/4 envelope (3.625 in. by 6.5 in.) 40 | this.US_STANDARD_FANFOLD = 39; // US standard fanfold (14.875 in. by 11 in.) 41 | this.GERMAN_STANDARD_FANFOLD = 40; // German standard fanfold (8.5 in. by 12 in.) 42 | this.GERMAN_LEGAL_FANFOLD = 41; // German legal fanfold (8.5 in. by 13 in.) 43 | this.ISO_B4 = 42; // ISO B4 (250 mm by 353 mm) 44 | this.JAPANESE_DOUBLE_POSTCARD = 43; // Japanese double postcard (200 mm by 148 mm) 45 | this.STANDARD_PAPER_9_BY_11_IN = 44; // Standard paper (9 in. by 11 in.) 46 | this.STANDARD_PAPER_10_BY_11_IN = 45; // Standard paper (10 in. by 11 in.) 47 | this.STANDARD_PAPER_15_BY_11_IN = 46; // Standard paper (15 in. by 11 in.) 48 | this.INVITE_ENVELOPE = 47; // Invite envelope (220 mm by 220 mm) 49 | this.LETTER_EXTRA_PAPER = 50; // Letter extra paper (9.275 in. by 12 in.) 50 | this.LEGAL_EXTRA_PAPER = 51; // Legal extra paper (9.275 in. by 15 in.) 51 | this.TABLOID_EXTRA_PAPER = 52; // Tabloid extra paper (11.69 in. by 18 in.) 52 | this.A4_EXTRA_PAPER = 53; // A4 extra paper (236 mm by 322 mm) 53 | this.LETTER_TRANSVERSE_PAPER = 54; // Letter transverse paper (8.275 in. by 11 in.) 54 | this.A4_TRANSVERSE_PAPER = 55; // A4 transverse paper (210 mm by 297 mm) 55 | this.LETTER_EXTRA_TRANSVERSE_PAPER = 56; // Letter extra transverse paper (9.275 in. by 12 in.) 56 | this.SUPER_A_SUPER_A_A4_PAPER = 57; // SuperA/SuperA/A4 paper (227 mm by 356 mm) 57 | this.SUPER_B_SUPER_B_A3_PAPER = 58; // SuperB/SuperB/A3 paper (305 mm by 487 mm) 58 | this.LETTER_PLUS_PAPER = 59; // Letter plus paper (8.5 in. by 12.69 in.) 59 | this.A4_PLUS_PAPER = 60; // A4 plus paper (210 mm by 330 mm) 60 | this.A5_TRANSVERSE_PAPER = 61; // A5 transverse paper (148 mm by 210 mm) 61 | this.JIS_B5_TRANSVERSE_PAPER = 62; // JIS B5 transverse paper (182 mm by 257 mm) 62 | this.A3_EXTRA_PAPER = 63; // A3 extra paper (322 mm by 445 mm) 63 | this.A5_EXTRA_PAPER = 64; // A5 extra paper (174 mm by 235 mm) 64 | this.ISO_B5_EXTRA_PAPER = 65; // ISO B5 extra paper (201 mm by 276 mm) 65 | this.A2_PAPER = 66; // A2 paper (420 mm by 594 mm) 66 | this.A3_TRANSVERSE_PAPER = 67; // A3 transverse paper (297 mm by 420 mm) 67 | this.A3_EXTRA_TRANSVERSE_PAPER = 68; // A3 extra transverse paper (322 mm by 445 mm) 68 | 69 | this.opts = []; 70 | Object.keys(this).forEach((k) => { 71 | if (typeof this[k] === 'number') { 72 | this.opts.push(k); 73 | } 74 | }); 75 | } 76 | 77 | 78 | items.prototype.validate = function (val) { 79 | if (this[val.toUpperCase()] === undefined) { 80 | let opts = []; 81 | for (let name in this) { 82 | if (this.hasOwnProperty(name)) { 83 | opts.push(name); 84 | } 85 | } 86 | throw new TypeError('Invalid value for PAPER_SIZE; Value must be one of ' + this.opts.join(', ')); 87 | } else { 88 | return true; 89 | } 90 | }; 91 | 92 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/types/positiveUniversalMeasure.js: -------------------------------------------------------------------------------- 1 | //§22.9.2.12 ST_PositiveUniversalMeasure (Positive Universal Measurement) 2 | 3 | function measure() { 4 | } 5 | 6 | measure.prototype.validate = function (val) { 7 | let re = new RegExp('[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)'); 8 | if (re.test(val) !== true) { 9 | throw new TypeError('Invalid value for universal positive measure. Value must a positive Float immediately followed by unit of measure from list mm, cm, in, pt, pc, pi. i.e. 10.5cm'); 10 | } else { 11 | return true; 12 | } 13 | }; 14 | 15 | module.exports = new measure(); -------------------------------------------------------------------------------- /source/lib/types/printError.js: -------------------------------------------------------------------------------- 1 | //§18.18.60 ST_PrintError (Print Errors) 2 | function items() { 3 | let opts = ['displayed', 'blank', 'dash', 'NA']; 4 | opts.forEach((o, i) => { 5 | this[o] = i + 1; 6 | }); 7 | } 8 | 9 | 10 | items.prototype.validate = function (val) { 11 | if (this[val] === undefined) { 12 | let opts = []; 13 | for (let name in this) { 14 | if (this.hasOwnProperty(name)) { 15 | opts.push(name); 16 | } 17 | } 18 | throw new TypeError('Invalid value for pageSetup.errors; Value must be one of ' + opts.join(', ')); 19 | } else { 20 | return true; 21 | } 22 | }; 23 | 24 | module.exports = new items(); -------------------------------------------------------------------------------- /source/lib/utils.js: -------------------------------------------------------------------------------- 1 | let types = require('./types/index.js'); 2 | 3 | let _bitXOR = (a, b) => { 4 | let maxLength = a.length > b.length ? a.length : b.length; 5 | 6 | let padString = ''; 7 | for (let i = 0; i < maxLength; i++) { 8 | padString += '0'; 9 | } 10 | 11 | a = String(padString + a).substr(-maxLength); 12 | b = String(padString + b).substr(-maxLength); 13 | 14 | let response = ''; 15 | for (let i = 0; i < a.length; i++) { 16 | response += a[i] === b[i] ? 0 : 1; 17 | } 18 | return response; 19 | }; 20 | 21 | let generateRId = () => { 22 | let text = 'R'; 23 | let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 24 | for (let i = 0; i < 16; i++) { 25 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 26 | } 27 | return text; 28 | }; 29 | 30 | let _rotateBinary = (bin) => { 31 | return bin.substr(1, bin.length - 1) + bin.substr(0, 1); 32 | }; 33 | 34 | let _getHashForChar = (char, hash) => { 35 | hash = hash ? hash : '0000'; 36 | let charCode = char.charCodeAt(0); 37 | let hashBin = parseInt(hash, 16).toString(2); 38 | let charBin = parseInt(charCode, 10).toString(2); 39 | hashBin = String('000000000000000' + hashBin).substr(-15); 40 | charBin = String('000000000000000' + charBin).substr(-15); 41 | let nextHash = _bitXOR(hashBin, charBin); 42 | nextHash = _rotateBinary(nextHash); 43 | nextHash = parseInt(nextHash, 2).toString(16); 44 | 45 | return nextHash; 46 | }; 47 | 48 | // http://www.openoffice.org/sc/excelfileformat.pdf section 4.18.4 49 | let getHashOfPassword = (str) => { 50 | let curHash = '0000'; 51 | for (let i = str.length - 1; i >= 0; i--) { 52 | curHash = _getHashForChar(str[i], curHash); 53 | } 54 | let curHashBin = parseInt(curHash, 16).toString(2); 55 | let charCountBin = parseInt(str.length, 10).toString(2); 56 | let saltBin = parseInt('CE4B', 16).toString(2); 57 | 58 | let firstXOR = _bitXOR(curHashBin, charCountBin); 59 | let finalHashBin = _bitXOR(firstXOR, saltBin); 60 | let finalHash = String('0000' + parseInt(finalHashBin, 2).toString(16).toUpperCase()).slice(-4); 61 | 62 | return finalHash; 63 | }; 64 | 65 | /** 66 | * Translates a column number into the Alpha equivalent used by Excel 67 | * @function getExcelAlpha 68 | * @param {Number} colNum Column number that is to be transalated 69 | * @returns {String} The Excel alpha representation of the column number 70 | * @example 71 | * // returns B 72 | * getExcelAlpha(2); 73 | */ 74 | let getExcelAlpha = (colNum) => { 75 | let remaining = colNum; 76 | let aCharCode = 65; 77 | let columnName = ''; 78 | while (remaining > 0) { 79 | let mod = (remaining - 1) % 26; 80 | columnName = String.fromCharCode(aCharCode + mod) + columnName; 81 | remaining = (remaining - 1 - mod) / 26; 82 | } 83 | return columnName; 84 | }; 85 | 86 | /** 87 | * Translates a column number into the Alpha equivalent used by Excel 88 | * @function getExcelAlpha 89 | * @param {Number} rowNum Row number that is to be transalated 90 | * @param {Number} colNum Column number that is to be transalated 91 | * @returns {String} The Excel alpha representation of the column number 92 | * @example 93 | * // returns B1 94 | * getExcelCellRef(1, 2); 95 | */ 96 | let getExcelCellRef = (rowNum, colNum) => { 97 | let remaining = colNum; 98 | let aCharCode = 65; 99 | let columnName = ''; 100 | while (remaining > 0) { 101 | let mod = (remaining - 1) % 26; 102 | columnName = String.fromCharCode(aCharCode + mod) + columnName; 103 | remaining = (remaining - 1 - mod) / 26; 104 | } 105 | return columnName + rowNum; 106 | }; 107 | 108 | /** 109 | * Translates a Excel cell represenation into row and column numerical equivalents 110 | * @function getExcelRowCol 111 | * @param {String} str Excel cell representation 112 | * @returns {Object} Object keyed with row and col 113 | * @example 114 | * // returns {row: 2, col: 3} 115 | * getExcelRowCol('C2') 116 | */ 117 | let getExcelRowCol = (str) => { 118 | let numeric = str.split(/\D/).filter(function (el) { 119 | return el !== ''; 120 | })[0]; 121 | let alpha = str.split(/\d/).filter(function (el) { 122 | return el !== ''; 123 | })[0]; 124 | let row = parseInt(numeric, 10); 125 | let col = alpha.toUpperCase().split('').reduce(function (a, b, index, arr) { 126 | return a + (b.charCodeAt(0) - 64) * Math.pow(26, arr.length - index - 1); 127 | }, 0); 128 | return { row: row, col: col }; 129 | }; 130 | 131 | /** 132 | * Translates a date into Excel timestamp 133 | * @function getExcelTS 134 | * @param {Date} date Date to translate 135 | * @returns {Number} Excel timestamp 136 | * @example 137 | * // returns 29810.958333333332 138 | * getExcelTS(new Date('08/13/1981')); 139 | */ 140 | let getExcelTS = (date) => { 141 | 142 | let thisDt = new Date(date); 143 | thisDt.setDate(thisDt.getDate() + 1); 144 | 145 | let epoch = new Date('1900-01-01T00:00:00.0000Z'); 146 | 147 | // Handle legacy leap year offset as described in §18.17.4.1 148 | const legacyLeapDate = new Date('1900-02-28T23:59:59.999Z'); 149 | if (thisDt - legacyLeapDate > 0) { 150 | thisDt.setDate(thisDt.getDate() + 1); 151 | } 152 | 153 | // Get milliseconds between date sent to function and epoch 154 | let diff2 = thisDt.getTime() - epoch.getTime(); 155 | 156 | let ts = diff2 / (1000 * 60 * 60 * 24); 157 | 158 | return parseFloat(ts.toFixed(7)); 159 | }; 160 | 161 | let sortCellRefs = (a, b) => { 162 | let aAtt = getExcelRowCol(a); 163 | let bAtt = getExcelRowCol(b); 164 | if (aAtt.col === bAtt.col) { 165 | return aAtt.row - bAtt.row; 166 | } else { 167 | return aAtt.col - bAtt.col; 168 | } 169 | }; 170 | 171 | let arrayIntersectSafe = (a, b) => { 172 | 173 | if (a instanceof Array && b instanceof Array) { 174 | var ai = 0, bi = 0; 175 | var result = new Array(); 176 | 177 | while (ai < a.length && bi < b.length) { 178 | if (a[ai] < b[bi]) { 179 | ai++; 180 | } else if (a[ai] > b[bi]) { 181 | bi++; 182 | } else { 183 | result.push(a[ai]); 184 | ai++; 185 | bi++; 186 | } 187 | } 188 | return result; 189 | } else { 190 | throw new TypeError('Both variables sent to arrayIntersectSafe must be arrays'); 191 | } 192 | }; 193 | 194 | let getAllCellsInExcelRange = (range) => { 195 | var cells = range.split(':'); 196 | var cell1props = getExcelRowCol(cells[0]); 197 | var cell2props = getExcelRowCol(cells[1]); 198 | return getAllCellsInNumericRange(cell1props.row, cell1props.col, cell2props.row, cell2props.col); 199 | }; 200 | 201 | let getAllCellsInNumericRange = (row1, col1, row2, col2) => { 202 | var response = []; 203 | row2 = row2 ? row2 : row1; 204 | col2 = col2 ? col2 : col1; 205 | for (var i = row1; i <= row2; i++) { 206 | for (var j = col1; j <= col2; j++) { 207 | response.push(getExcelAlpha(j) + i); 208 | } 209 | } 210 | return response.sort(sortCellRefs); 211 | }; 212 | 213 | let boolToInt = (bool) => { 214 | if (bool === true) { 215 | return 1; 216 | } 217 | if (bool === false) { 218 | return 0; 219 | } 220 | if (parseInt(bool) === 1) { 221 | return 1; 222 | } 223 | if (parseInt(bool) === 0) { 224 | return 0; 225 | } 226 | throw new TypeError('Value sent to boolToInt must be true, false, 1 or 0'); 227 | }; 228 | 229 | /* 230 | * Helper Functions 231 | */ 232 | 233 | module.exports = { 234 | generateRId, 235 | getHashOfPassword, 236 | getExcelAlpha, 237 | getExcelCellRef, 238 | getExcelRowCol, 239 | getExcelTS, 240 | sortCellRefs, 241 | arrayIntersectSafe, 242 | getAllCellsInExcelRange, 243 | getAllCellsInNumericRange, 244 | boolToInt 245 | }; -------------------------------------------------------------------------------- /source/lib/workbook/dxfCollection.js: -------------------------------------------------------------------------------- 1 | const _isEqual = require('lodash.isequal'); 2 | const Style = require('../style'); 3 | const util = require('util'); 4 | 5 | class DXFItem { // §18.8.14 dxf (Formatting) 6 | constructor(style, wb) { 7 | this.wb = wb; 8 | this.style = style; 9 | this.id; 10 | } 11 | get dxfId() { 12 | return this.id; 13 | } 14 | 15 | addToXMLele(ele) { 16 | this.style.addDXFtoXMLele(ele); 17 | } 18 | } 19 | 20 | class DXFCollection { // §18.8.15 dxfs (Formats) 21 | constructor(wb) { 22 | this.wb = wb; 23 | this.items = []; 24 | } 25 | 26 | add(style) { 27 | if (!(style instanceof Style)) { 28 | style = this.wb.Style(style); 29 | } 30 | 31 | let thisItem; 32 | this.items.forEach((item) => { 33 | if (_isEqual(item.style.toObject(), style.toObject())) { 34 | return thisItem = item; 35 | } 36 | }); 37 | if (!thisItem) { 38 | thisItem = new DXFItem(style, this.wb); 39 | this.items.push(thisItem); 40 | thisItem.id = this.items.length - 1; 41 | } 42 | return thisItem; 43 | } 44 | 45 | get length() { 46 | return this.items.length; 47 | } 48 | 49 | addToXMLele(ele) { 50 | let dxfXML = ele 51 | .ele('dxfs') 52 | .att('count', this.length); 53 | 54 | this.items.forEach((item) => { 55 | item.addToXMLele(dxfXML); 56 | }); 57 | } 58 | } 59 | 60 | module.exports = DXFCollection; -------------------------------------------------------------------------------- /source/lib/workbook/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./workbook.js'); -------------------------------------------------------------------------------- /source/lib/workbook/mediaCollection.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | class MediaCollection { 4 | constructor() { 5 | this.items = []; 6 | } 7 | 8 | add(item) { 9 | if (typeof item === 'string') { 10 | fs.accessSync(item, fs.R_OK); 11 | } 12 | 13 | this.items.push(item); 14 | return this.items.length; 15 | } 16 | 17 | get isEmpty() { 18 | if (this.items.length === 0) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | } 24 | } 25 | 26 | module.exports = MediaCollection; -------------------------------------------------------------------------------- /source/lib/workbook/workbook.js: -------------------------------------------------------------------------------- 1 | const _isUndefined = require('lodash.isundefined'); 2 | const deepmerge = require('deepmerge'); 3 | const fs = require('fs'); 4 | const utils = require('../utils.js'); 5 | const Worksheet = require('../worksheet'); 6 | const Style = require('../style'); 7 | const Border = require('../style/classes/border.js'); 8 | const Fill = require('../style/classes/fill.js'); 9 | const Font = require('../style/classes/font'); 10 | const DXFCollection = require('./dxfCollection.js'); 11 | const MediaCollection = require('./mediaCollection.js'); 12 | const DefinedNameCollection = require('../classes/definedNameCollection.js'); 13 | const types = require('../types/index.js'); 14 | const builder = require('./builder.js'); 15 | const http = require('http'); 16 | const SimpleLogger = require('../logger'); 17 | 18 | /* Available options for Workbook 19 | { 20 | jszip : { 21 | compression : 'DEFLATE' 22 | }, 23 | defaultFont : { 24 | size : 12, 25 | family : 'Calibri', 26 | color : 'FFFFFFFF' 27 | } 28 | } 29 | */ 30 | // Default Options for Workbook 31 | let workbookDefaultOpts = { 32 | jszip: { 33 | compression: 'DEFLATE' 34 | }, 35 | defaultFont: { 36 | 'color': 'FF000000', 37 | 'name': 'Calibri', 38 | 'size': 12, 39 | 'family': 'roman' 40 | }, 41 | dateFormat: 'm/d/yy' 42 | }; 43 | 44 | 45 | class Workbook { 46 | 47 | /** 48 | * @class Workbook 49 | * @param {Object} opts Workbook settings 50 | * @param {Object} opts.jszip 51 | * @param {String} opts.jszip.compression JSZip compression type. defaults to 'DEFLATE' 52 | * @param {Object} opts.defaultFont 53 | * @param {String} opts.defaultFont.color HEX value of default font color. defaults to #000000 54 | * @param {String} opts.defaultFont.name Font name. defaults to Calibri 55 | * @param {Number} opts.defaultFont.size Font size. defaults to 12 56 | * @param {String} opts.defaultFont.family Font family. defaults to roman 57 | * @param {String} opts.dataFormat Specifies the format for dates in the Workbook. defaults to 'm/d/yy' 58 | * @param {Number} opts.workbookView.activeTab Specifies an unsignedInt that contains the index to the active sheet in this book view. 59 | * @param {Boolean} opts.workbookView.autoFilterDateGrouping Specifies a boolean value that indicates whether to group dates when presenting the user with filtering options in the user interface. 60 | * @param {Number} opts.workbookView.firstSheet Specifies the index to the first sheet in this book view. 61 | * @param {Boolean} opts.workbookView.minimized Specifies a boolean value that indicates whether the workbook window is minimized. 62 | * @param {Boolean} opts.workbookView.showHorizontalScroll Specifies a boolean value that indicates whether to display the horizontal scroll bar in the user interface. 63 | * @param {Boolean} opts.workbookView.showSheetTabs Specifies a boolean value that indicates whether to display the sheet tabs in the user interface. 64 | * @param {Boolean} opts.workbookView.showVerticalScroll Specifies a boolean value that indicates whether to display the vertical scroll bar. 65 | * @param {Number} opts.workbookView.tabRatio Specifies ratio between the workbook tabs bar and the horizontal scroll bar. 66 | * @param {String} opts.workbookView.visibility Specifies visible state of the workbook window. ('hidden', 'veryHidden', 'visible') (§18.18.89) 67 | * @param {Number} opts.workbookView.windowHeight Specifies the height of the workbook window. The unit of measurement for this value is twips. 68 | * @param {Number} opts.workbookView.windowWidth Specifies the width of the workbook window. The unit of measurement for this value is twips.. 69 | * @param {Number} opts.workbookView.xWindow Specifies the X coordinate for the upper left corner of the workbook window. The unit of measurement for this value is twips. 70 | * @param {Number} opts.workbookView.yWindow Specifies the Y coordinate for the upper left corner of the workbook window. The unit of measurement for this value is twips. 71 | * @param {Boolean} opts.workbookView 72 | * @param {Object} opts.logger Logger that supports warn and error method, defaults to console 73 | * @param {String} opts.author Name displayed as document's author 74 | * @returns {Workbook} 75 | */ 76 | constructor(opts = {}) { 77 | 78 | const hasCustomLogger = opts.logger !== undefined; 79 | const hasValidCustomLogger = hasCustomLogger && typeof opts.logger.warn === 'function' && typeof opts.logger.error === 'function'; 80 | 81 | this.logger = hasValidCustomLogger ? opts.logger : new SimpleLogger({ 82 | logLevel: Number.isNaN(parseInt(opts.logLevel)) ? 0 : parseInt(opts.logLevel) 83 | }); 84 | if (hasCustomLogger && !hasValidCustomLogger) { 85 | this.logger.log('opts.logger is not a valid logger'); 86 | } 87 | 88 | this.opts = deepmerge(workbookDefaultOpts, opts); 89 | this.author = this.opts.author || 'Microsoft Office User'; 90 | 91 | this.sheets = []; 92 | this.sharedStrings = []; 93 | this.sharedStringLookup = new Map(); 94 | this.styles = []; 95 | this.stylesLookup = new Map(); 96 | this.dxfCollection = new DXFCollection(this); 97 | this.mediaCollection = new MediaCollection(); 98 | this.definedNameCollection = new DefinedNameCollection(); 99 | this.styleData = { 100 | 'numFmts': [], 101 | 'fonts': [], 102 | 'fills': [new Fill({ 103 | type: 'pattern', 104 | patternType: 'none' 105 | }), new Fill({ 106 | type: 'pattern', 107 | patternType: 'gray125' 108 | })], 109 | 'borders': [new Border()], 110 | 'cellXfs': [{ 111 | 'borderId': null, 112 | 'fillId': null, 113 | 'fontId': 0, 114 | 'numFmtId': null 115 | }] 116 | }; 117 | 118 | // Lookups for style components to quickly find existing entries 119 | // - Lookup keys are stringified JSON of a style's toObject result 120 | // - Lookup values are the indexes for the actual entry in the styleData arrays 121 | this.styleDataLookup = { 122 | 'fonts': {}, 123 | 'fills': this.styleData.fills.reduce((ret, fill, index) => { 124 | ret[JSON.stringify(fill.toObject())] = index; 125 | return ret; 126 | }, {}), 127 | 'borders': this.styleData.borders.reduce((ret, border, index) => { 128 | ret[JSON.stringify(border.toObject())] = index; 129 | return ret; 130 | }, {}) 131 | }; 132 | 133 | // Set Default Font and Style 134 | this.createStyle({ 135 | font: this.opts.defaultFont 136 | }); 137 | } 138 | 139 | /** 140 | * setSelectedTab 141 | * @param {Number} tab number of sheet that should be displayed when workbook opens. tabs are indexed starting with 1 142 | **/ 143 | setSelectedTab(id) { 144 | this.sheets.forEach((s) => { 145 | if (s.sheetId === id) { 146 | s.opts.sheetView.tabSelected = 1; 147 | } else { 148 | s.opts.sheetView.tabSelected = 0; 149 | } 150 | }); 151 | } 152 | 153 | /** 154 | * writeToBuffer 155 | * Writes Excel data to a node Buffer. 156 | */ 157 | writeToBuffer() { 158 | return builder.writeToBuffer(this); 159 | } 160 | 161 | /** 162 | * Generate .xlsx file. 163 | * @param {String} fileName Name of Excel workbook with .xslx extension 164 | * @param {http.response | callback} http response object or callback function (optional). 165 | * If http response object is given, file is written to http response. Useful for web applications. 166 | * If callback is given, callback called with (err, fs.Stats) passed 167 | */ 168 | write(fileName, handler) { 169 | 170 | builder.writeToBuffer(this) 171 | .then((buffer) => { 172 | switch (typeof handler) { 173 | // handler passed as http response object. 174 | 175 | case 'object': 176 | if (handler instanceof http.ServerResponse) { 177 | handler.writeHead(200, { 178 | 'Content-Length': buffer.length, 179 | 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 180 | 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"; filename*=utf-8''${encodeURIComponent(fileName)};`, 181 | }); 182 | handler.end(buffer); 183 | } else { 184 | throw new TypeError('Unknown object sent to write function.'); 185 | } 186 | break; 187 | 188 | // handler passed as callback function 189 | case 'function': 190 | fs.writeFile(fileName, buffer, function (err) { 191 | if (err) { 192 | handler(err); 193 | } else { 194 | fs.stat(fileName, handler); 195 | } 196 | }); 197 | break; 198 | 199 | // no handler passed, write file to FS. 200 | default: 201 | 202 | fs.writeFile(fileName, buffer, function (err) { 203 | if (err) { 204 | throw err; 205 | } 206 | }); 207 | break; 208 | } 209 | }) 210 | .catch((e) => { 211 | if (handler instanceof http.ServerResponse) { 212 | this.logger.error(e.stack); 213 | handler.status = 500; 214 | handler.setHeader('Content-Type', 'text/plain'); 215 | handler.end('500 Server Error'); 216 | } else if (typeof handler === 'function') { 217 | handler(e.stack); 218 | } else { 219 | this.logger.error(e.stack); 220 | } 221 | }); 222 | } 223 | 224 | /** 225 | * Add a worksheet to the Workbook 226 | * @param {String} name Name of the Worksheet 227 | * @param {Object} opts Options for Worksheet. See Worksheet class definition 228 | * @returns {Worksheet} 229 | */ 230 | addWorksheet(name, opts) { 231 | let newLength = this.sheets.push(new Worksheet(this, name, opts)); 232 | return this.sheets[newLength - 1]; 233 | } 234 | 235 | /** 236 | * Add a Style to the Workbook 237 | * @param {Object} opts Options for the style. See Style class definition 238 | * @returns {Style} 239 | */ 240 | createStyle(opts) { 241 | const thisStyle = new Style(this, opts); 242 | const lookupKey = JSON.stringify(thisStyle.toObject()); 243 | 244 | // Use existing style if one exists 245 | if (this.stylesLookup.get(lookupKey)) { 246 | return this.stylesLookup.get(lookupKey); 247 | } 248 | 249 | this.stylesLookup.set(lookupKey, thisStyle); 250 | const index = this.styles.push(thisStyle) - 1; 251 | this.styles[index].ids.cellXfs = index; 252 | return this.styles[index]; 253 | } 254 | 255 | /** 256 | * Gets the index of a string from the shared string array if exists and adds the string if it does not and returns the new index 257 | * @param {String} val Text of string 258 | * @returns {Number} index of the string in the shared strings array 259 | */ 260 | getStringIndex(val) { 261 | const lookupKey = typeof val === "string" ? val : JSON.stringify(val); 262 | const target = this.sharedStringLookup.get(lookupKey); 263 | if (_isUndefined(target)) { 264 | const index = this.sharedStrings.push(val) - 1; 265 | this.sharedStringLookup.set(lookupKey, index); 266 | return index; 267 | } else { 268 | return target; 269 | } 270 | } 271 | 272 | /** 273 | * @func Workbook._generateXML 274 | * @desc used for testing the Workbook XML generated by the builder 275 | * @return {Promise} resolves with Workbook XML 276 | */ 277 | _generateXML() { 278 | return builder.workbookXML(this); 279 | } 280 | } 281 | 282 | module.exports = Workbook; -------------------------------------------------------------------------------- /source/lib/worksheet/cf/cf_rule.js: -------------------------------------------------------------------------------- 1 | const _reduce = require('lodash.reduce'); 2 | const _get = require('lodash.get'); 3 | const CF_RULE_TYPES = require('./cf_rule_types'); 4 | 5 | class CfRule { // §18.3.1.10 cfRule (Conditional Formatting Rule) 6 | constructor(ruleConfig) { 7 | this.type = ruleConfig.type; 8 | this.priority = ruleConfig.priority; 9 | this.formula = ruleConfig.formula; 10 | this.dxfId = ruleConfig.dxfId; 11 | 12 | let foundType = CF_RULE_TYPES[this.type]; 13 | 14 | if (!foundType) { 15 | throw new TypeError('"' + this.type + '" is not a valid conditional formatting rule type'); 16 | } 17 | 18 | if (!foundType.supported) { 19 | throw new TypeError('Conditional formatting type "' + this.type + '" is not yet supported'); 20 | } 21 | 22 | let missingProps = _reduce(foundType.requiredProps, (list, prop) => { 23 | if (_get(this, prop, null) === null) { 24 | list.push(prop); 25 | } 26 | return list; 27 | }, []); 28 | 29 | if (missingProps.length) { 30 | throw new TypeError('Conditional formatting rule is missing required properties: ' + missingProps.join(', ')); 31 | } 32 | } 33 | 34 | addToXMLele(ele) { 35 | let thisRule = ele.ele('cfRule'); 36 | if (this.type !== undefined) { 37 | thisRule.att('type', this.type); 38 | } 39 | if (this.dxfId !== undefined) { 40 | thisRule.att('dxfId', this.dxfId); 41 | } 42 | if (this.priority !== undefined) { 43 | thisRule.att('priority', this.priority); 44 | } 45 | 46 | if (this.formula !== undefined) { 47 | thisRule.ele('formula').text(this.formula); 48 | thisRule.up(); 49 | } 50 | thisRule.up(); 51 | } 52 | } 53 | 54 | 55 | module.exports = CfRule; -------------------------------------------------------------------------------- /source/lib/worksheet/cf/cf_rule_types.js: -------------------------------------------------------------------------------- 1 | // Types from xlsx spec: 2 | // http://download.microsoft.com/download/D/3/3/D334A189-E51B-47FF-B0E8-C0479AFB0E3C/[MS-XLSX].pdf 3 | 4 | module.exports = { 5 | cellIs: { 6 | supported: false 7 | }, 8 | expression: { 9 | supported: true, 10 | requiredProps: ['dxfId', 'priority', 'formula'] 11 | }, 12 | colorScale: { 13 | supported: false 14 | }, 15 | dataBar: { 16 | supported: false 17 | }, 18 | iconSet: { 19 | supported: false 20 | }, 21 | containsText: { 22 | supported: false 23 | }, 24 | notContainsText: { 25 | supported: false 26 | }, 27 | beginsWith: { 28 | supported: false 29 | }, 30 | endsWith: { 31 | supported: false 32 | }, 33 | containsBlanks: { 34 | supported: false 35 | }, 36 | notContainsBlanks: { 37 | supported: false 38 | }, 39 | containsErrors: { 40 | supported: false 41 | }, 42 | notContainsErrors: { 43 | supported: false 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /source/lib/worksheet/cf/cf_rules_collection.js: -------------------------------------------------------------------------------- 1 | const CfRule = require('./cf_rule'); 2 | 3 | // ----------------------------------------------------------------------------- 4 | 5 | class CfRulesCollection { // §18.3.1.18 conditionalFormatting (Conditional Formatting) 6 | constructor() { 7 | // rules are indexed by cell refs 8 | this.rulesBySqref = {}; 9 | } 10 | 11 | get count() { 12 | return Object.keys(this.rulesBySqref).length; 13 | } 14 | 15 | add(sqref, ruleConfig) { 16 | let rules = this.rulesBySqref[sqref] || []; 17 | let newRule = new CfRule(ruleConfig); 18 | rules.push(newRule); 19 | this.rulesBySqref[sqref] = rules; 20 | return this; 21 | } 22 | 23 | addToXMLele(ele) { 24 | Object.keys(this.rulesBySqref).forEach((sqref) => { 25 | let thisEle = ele.ele('conditionalFormatting').att('sqref', sqref); 26 | this.rulesBySqref[sqref].forEach((rule) => { 27 | rule.addToXMLele(thisEle); 28 | }); 29 | thisEle.up(); 30 | }); 31 | } 32 | } 33 | 34 | 35 | module.exports = CfRulesCollection; -------------------------------------------------------------------------------- /source/lib/worksheet/classes/dataValidation.js: -------------------------------------------------------------------------------- 1 | const myUtils = require('../../utils.js'); 2 | 3 | let cleanFormula = (f) => { 4 | if (typeof f === 'number' || f.substr(0, 1) === '=') { 5 | return f; 6 | } else { 7 | return '"' + f + '"'; 8 | } 9 | }; 10 | 11 | class DataValidation { // §18.3.1.32 dataValidation (Data Validation) 12 | constructor(opts) { 13 | opts = opts ? opts : {}; 14 | if (opts.sqref === undefined) { 15 | throw new TypeError('sqref must be specified when creating a DataValidation instance.'); 16 | } 17 | this.sqref = opts.sqref; 18 | if (opts.formulas instanceof Array) { 19 | opts.formulas[0] !== undefined ? this.formula1 = opts.formulas[0] : null; 20 | opts.formulas[1] !== undefined ? this.formula2 = opts.formulas[1] : null; 21 | } 22 | 23 | if (opts.allowBlank !== undefined) { 24 | if (parseInt(opts.allowBlank) === 1) { 25 | opts.allowBlank = true; 26 | } 27 | if (parseInt(opts.allowBlank) === 0) { 28 | opts.allowBlank = false; 29 | } 30 | if (typeof opts.allowBlank !== 'boolean') { 31 | throw new TypeError('DataValidation allowBlank must be true, false, 1 or 0'); 32 | } 33 | this.allowBlank = opts.allowBlank; 34 | } 35 | 36 | if (opts.errorStyle !== undefined) { 37 | let enums = ['stop', 'warning', 'information']; 38 | if (enums.indexOf(opts.errorStyle) < 0) { 39 | throw new TypeError('DataValidation errorStyle must be one of ' + enums.join(', ')); 40 | } 41 | this.errorStyle = opts.errorStyle; 42 | } 43 | 44 | if (opts.error !== undefined) { 45 | if (typeof opts.error !== 'string') { 46 | throw new TypeError('DataValidation error must be a string'); 47 | } 48 | this.error = opts.error; 49 | this.showErrorMessage = opts.showErrorMessage = true; 50 | } 51 | 52 | if (opts.errorTitle !== undefined) { 53 | if (typeof opts.errorTitle !== 'string') { 54 | throw new TypeError('DataValidation errorTitle must be a string'); 55 | } 56 | this.errorTitle = opts.errorTitle; 57 | this.showErrorMessage = opts.showErrorMessage = true; 58 | } 59 | 60 | if (opts.imeMode !== undefined) { 61 | let enums = ['noControl', 'off', 'on', 'disabled', 'hiragana', 'fullKatakana', 'halfKatakana', 'fullAlpha', 'halfAlpha', 'fullHangul', 'halfHangul']; 62 | if (enums.indexOf(opts.imeMode) < 0) { 63 | throw new TypeError('DataValidation imeMode must be one of ' + enums.join(', ')); 64 | } 65 | this.imeMode = opts.imeMode; 66 | } 67 | 68 | if (opts.operator !== undefined) { 69 | let enums = ['between', 'notBetween', 'equal', 'notEqual', 'lessThan', 'lessThanOrEqual', 'greaterThan', 'greaterThanOrEqual']; 70 | if (enums.indexOf(opts.operator) < 0) { 71 | throw new TypeError('DataValidation operator must be one of ' + enums.join(', ')); 72 | } 73 | this.operator = opts.operator; 74 | } 75 | 76 | if (opts.prompt !== undefined) { 77 | if (typeof opts.prompt !== 'string') { 78 | throw new TypeError('DataValidation prompt must be a string'); 79 | } 80 | this.prompt = opts.prompt; 81 | this.showInputMessage = opts.showInputMessage = true; 82 | } 83 | 84 | if (opts.promptTitle !== undefined) { 85 | if (typeof opts.promptTitle !== 'string') { 86 | throw new TypeError('DataValidation promptTitle must be a string'); 87 | } 88 | this.promptTitle = opts.promptTitle; 89 | this.showInputMessage = opts.showInputMessage = true; 90 | } 91 | 92 | if (opts.showDropDown !== undefined) { 93 | if (parseInt(opts.showDropDown) === 1) { 94 | opts.showDropDown = true; 95 | } 96 | if (parseInt(opts.showDropDown) === 0) { 97 | opts.showDropDown = false; 98 | } 99 | if (typeof opts.showDropDown !== 'boolean') { 100 | throw new TypeError('DataValidation showDropDown must be true, false, 1 or 0'); 101 | } 102 | this.showDropDown = opts.showDropDown; 103 | } 104 | 105 | if (opts.showErrorMessage !== undefined) { 106 | if (parseInt(opts.showErrorMessage) === 1) { 107 | opts.showErrorMessage = true; 108 | } 109 | if (parseInt(opts.showErrorMessage) === 0) { 110 | opts.showErrorMessage = false; 111 | } 112 | if (typeof opts.showErrorMessage !== 'boolean') { 113 | throw new TypeError('DataValidation showErrorMessage must be true, false, 1 or 0'); 114 | } 115 | this.showErrorMessage = opts.showErrorMessage; 116 | } 117 | 118 | if (opts.showInputMessage !== undefined) { 119 | if (parseInt(opts.showInputMessage) === 1) { 120 | opts.showInputMessage = true; 121 | } 122 | if (parseInt(opts.showInputMessage) === 0) { 123 | opts.showInputMessage = false; 124 | } 125 | if (typeof opts.showInputMessage !== 'boolean') { 126 | throw new TypeError('DataValidation showInputMessage must be true, false, 1 or 0'); 127 | } 128 | this.showInputMessage = opts.showInputMessage; 129 | } 130 | 131 | if (opts.type !== undefined) { 132 | let enums = ['none', 'whole', 'decimal', 'list', 'date', 'time', 'textLength', 'custom']; 133 | if (enums.indexOf(opts.type) < 0) { 134 | throw new TypeError('DataValidation type must be one of ' + enums.join(', ')); 135 | } 136 | this.type = opts.type; 137 | } 138 | } 139 | 140 | addToXMLele(ele) { 141 | let valEle = ele.ele('dataValidation'); 142 | this.type !== undefined ? valEle.att('type', this.type) : null; 143 | this.errorStyle !== undefined ? valEle.att('errorStyle', this.errorStyle) : null; 144 | this.imeMode !== undefined ? valEle.att('imeMode', this.imeMode) : null; 145 | this.operator !== undefined ? valEle.att('operator', this.operator) : null; 146 | this.allowBlank !== undefined ? valEle.att('allowBlank', myUtils.boolToInt(this.allowBlank)) : null; 147 | this.showDropDown === false ? valEle.att('showDropDown', 1) : null; // For some reason, the Excel app sets this property to true if the "In-cell dropdown" option is selected in the data validation screen. 148 | this.showInputMessage !== undefined ? valEle.att('showInputMessage', myUtils.boolToInt(this.showInputMessage)) : null; 149 | this.showErrorMessage !== undefined ? valEle.att('showErrorMessage', myUtils.boolToInt(this.showErrorMessage)) : null; 150 | this.errorTitle !== undefined ? valEle.att('errorTitle', this.errorTitle) : null; 151 | this.error !== undefined ? valEle.att('error', this.error) : null; 152 | this.promptTitle !== undefined ? valEle.att('promptTitle', this.promptTitle) : null; 153 | this.prompt !== undefined ? valEle.att('prompt', this.prompt) : null; 154 | this.sqref !== undefined ? valEle.att('sqref', this.sqref) : null; 155 | if (this.formula1 !== undefined) { 156 | valEle.ele('formula1').text(cleanFormula(this.formula1)); 157 | valEle.up(); 158 | if (this.formula2 !== undefined) { 159 | valEle.ele('formula2').text(cleanFormula(this.formula2)); 160 | valEle.up(); 161 | } 162 | } 163 | valEle.up(); 164 | } 165 | } 166 | 167 | class DataValidationCollection { // §18.3.1.33 dataValidations (Data Validations) 168 | constructor(opts) { 169 | opts = opts ? opts : {}; 170 | this.items = []; 171 | } 172 | 173 | get length() { 174 | return this.items.length; 175 | } 176 | 177 | add(opts) { 178 | let thisValidation = new DataValidation(opts); 179 | this.items.push(thisValidation); 180 | return thisValidation; 181 | } 182 | 183 | addToXMLele(ele) { 184 | let valsEle = ele.ele('dataValidations').att('count', this.length); 185 | this.items.forEach((val) => { 186 | val.addToXMLele(valsEle); 187 | }); 188 | valsEle.up(); 189 | } 190 | } 191 | 192 | module.exports = { DataValidationCollection, DataValidation }; 193 | -------------------------------------------------------------------------------- /source/lib/worksheet/classes/hyperlink.js: -------------------------------------------------------------------------------- 1 | 2 | class Hyperlink { //§18.3.1.47 hyperlink (Hyperlink) 3 | constructor(opts) { 4 | opts = opts ? opts : {}; 5 | 6 | if (opts.ref === undefined) { 7 | throw new TypeError('ref is a required option when creating a hyperlink'); 8 | } 9 | this.ref = opts.ref; 10 | 11 | if (opts.display !== undefined) { 12 | this.display = opts.display; 13 | } else { 14 | this.display = opts.location; 15 | } 16 | if (opts.location !== undefined) { 17 | this.location = opts.location; 18 | } 19 | if (opts.tooltip !== undefined) { 20 | this.tooltip = opts.tooltip; 21 | } else { 22 | this.tooltip = opts.location; 23 | } 24 | this.id; 25 | } 26 | 27 | get rId() { 28 | return 'rId' + this.id; 29 | } 30 | 31 | addToXMLEle(ele) { 32 | let thisEle = ele.ele('hyperlink'); 33 | thisEle.att('ref', this.ref); 34 | thisEle.att('r:id', this.rId); 35 | if (this.display !== undefined) { 36 | thisEle.att('display', this.display); 37 | } 38 | if (this.location !== undefined) { 39 | thisEle.att('address', this.location); 40 | } 41 | if (this.tooltip !== undefined) { 42 | thisEle.att('tooltip', this.tooltip); 43 | } 44 | thisEle.up(); 45 | } 46 | } 47 | 48 | class HyperlinkCollection { //§18.3.1.48 hyperlinks (Hyperlinks) 49 | constructor() { 50 | this.links = []; 51 | } 52 | 53 | get length() { 54 | return this.links.length; 55 | } 56 | 57 | add(opts) { 58 | let thisLink = new Hyperlink(opts); 59 | thisLink.id = this.links.length + 1; 60 | this.links.push(thisLink); 61 | return thisLink; 62 | } 63 | 64 | addToXMLele(ele) { 65 | if (this.length > 0) { 66 | let linksEle = ele.ele('hyperlinks'); 67 | this.links.forEach((l) => { 68 | l.addToXMLEle(linksEle); 69 | }); 70 | linksEle.up(); 71 | } 72 | } 73 | } 74 | 75 | module.exports = { HyperlinkCollection, Hyperlink }; -------------------------------------------------------------------------------- /source/lib/worksheet/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./worksheet.js'); -------------------------------------------------------------------------------- /source/lib/worksheet/optsValidator.js: -------------------------------------------------------------------------------- 1 | const types = require('../types/index.js'); 2 | 3 | const optsTypes = { 4 | 'margins': { 5 | 'bottom': 'Float', 6 | 'footer': 'Float', 7 | 'header': 'Float', 8 | 'left': 'Float', 9 | 'right': 'Float', 10 | 'top': 'Float' 11 | }, 12 | 'printOptions': { 13 | 'centerHorizontal': 'Boolean', 14 | 'centerVertical': 'Boolean', 15 | 'printGridLines': 'Boolean', 16 | 'printHeadings': 'Boolean' 17 | 18 | }, 19 | 'pageSetup': { 20 | 'blackAndWhite': 'Boolean', 21 | 'cellComments': 'CELL_COMMENTS', 22 | 'copies': 'Integer', 23 | 'draft': 'Boolean', 24 | 'errors': 'PRINT_ERROR', 25 | 'firstPageNumber': 'Boolean', 26 | 'fitToHeight': 'Integer', 27 | 'fitToWidth': 'Integer', 28 | 'horizontalDpi': 'Integer', 29 | 'orientation': 'ORIENTATION', 30 | 'pageOrder': 'PAGE_ORDER', 31 | 'paperHeight': 'POSITIVE_UNIVERSAL_MEASURE', 32 | 'paperSize': 'PAPER_SIZE', 33 | 'paperWidth': 'POSITIVE_UNIVERSAL_MEASURE', 34 | 'scale': 'Integer', 35 | 'useFirstPageNumber': 'Boolean', 36 | 'usePrinterDefaults': 'Boolean', 37 | 'verticalDpi': 'Integer' 38 | }, 39 | 'headerFooter': { 40 | 'evenFooter': 'String', 41 | 'evenHeader': 'String', 42 | 'firstFooter': 'String', 43 | 'firstHeader': 'String', 44 | 'oddFooter': 'String', 45 | 'oddHeader': 'String', 46 | 'alignWithMargins': 'Boolean', 47 | 'differentFirst': 'Boolean', 48 | 'differentOddEven': 'Boolean', 49 | 'scaleWithDoc': 'Boolean' 50 | }, 51 | 'sheetView': { 52 | 'pane': { 53 | 'activePane': 'PANE', 54 | 'state': 'PANE_STATE', 55 | 'topLeftCell': null, 56 | 'xSplit': null, 57 | 'ySplit': null 58 | }, 59 | 'tabSelected': null, 60 | 'workbookViewId': null, 61 | 'rightToLeft': null, 62 | 'showGridLines': null, 63 | 'zoomScale': null, 64 | 'zoomScaleNormal': null, 65 | 'zoomScalePageLayoutView': null 66 | }, 67 | 'sheetFormat': { 68 | 'baseColWidth': null, 69 | 'customHeight': null, 70 | 'defaultColWidth': null, 71 | 'defaultRowHeight': null, 72 | 'outlineLevelCol': null, 73 | 'outlineLevelRow': null, 74 | 'thickBottom': null, 75 | 'thickTop': null, 76 | 'zeroHeight': null 77 | }, 78 | 'sheetProtection': { 79 | 'autoFilter': null, 80 | 'deleteColumns': null, 81 | 'deleteRow': null, 82 | 'formatCells': null, 83 | 'formatColumns': null, 84 | 'formatRows': null, 85 | 'hashValue': null, 86 | 'insertColumns': null, 87 | 'insertHyperlinks': null, 88 | 'insertRows': null, 89 | 'objects': null, 90 | 'password': null, 91 | 'pivotTables': null, 92 | 'scenarios': null, 93 | 'selectLockedCells': null, 94 | 'selectUnlockedCell': null, 95 | 'sheet': null, 96 | 'sort': null 97 | }, 98 | 'outline': { 99 | 'summaryBelow': null 100 | }, 101 | 'autoFilter': { 102 | 'startRow': null, 103 | 'endRow': null, 104 | 'startCol': null, 105 | 'endCol': null, 106 | 'filters': null 107 | }, 108 | 'hidden': 'Boolean' 109 | }; 110 | 111 | let getObjItem = (obj, key) => { 112 | let returnObj = obj; 113 | let levels = key.split('.'); 114 | 115 | while (levels.length > 0) { 116 | let thisLevelKey = levels.shift(); 117 | try { 118 | returnObj = returnObj[thisLevelKey]; 119 | } catch (e) { 120 | //returnObj = undefined; 121 | } 122 | } 123 | return returnObj; 124 | }; 125 | 126 | let validator = function (key, val, type) { 127 | switch (type) { 128 | 129 | case 'PAPER_SIZE': 130 | let sizes = Object.keys(types.paperSize); 131 | if (sizes.indexOf(val) < 0) { 132 | throw new TypeError('Invalid value for ' + key + '. Value must be one of ' + sizes.join(', ')); 133 | } 134 | break; 135 | 136 | case 'PAGE_ORDER': 137 | types.pageOrder.validate(val); 138 | break; 139 | 140 | case 'ORIENTATION': 141 | types.orientation.validate(val); 142 | break; 143 | 144 | case 'POSITIVE_UNIVERSAL_MEASURE': 145 | types.positiveUniversalMeasure.validate(val); 146 | break; 147 | 148 | case 'CELL_COMMENTS': 149 | types.cellComment.validate(val); 150 | break; 151 | 152 | case 'PRINT_ERROR': 153 | types.printError.validate(val); 154 | break; 155 | 156 | case 'PANE': 157 | types.pane.validate(val); 158 | break; 159 | 160 | case 'PANE_STATE': 161 | types.paneState.validate(val); 162 | break; 163 | 164 | case 'Boolean': 165 | if ([true, false, 1, 0].indexOf(val) < 0) { 166 | throw new TypeError(key + ' expects value of true, false, 1 or 0'); 167 | } 168 | break; 169 | 170 | case 'Float': 171 | if (parseFloat(val) !== val) { 172 | throw new TypeError(key + ' expects value as a Float number'); 173 | } 174 | break; 175 | 176 | case 'Integer': 177 | if (parseInt(val) !== val) { 178 | throw new TypeError(key + ' expects value as an Integer'); 179 | } 180 | break; 181 | 182 | case 'String': 183 | if (typeof val !== 'string') { 184 | throw new TypeError(key + ' expects value as a String'); 185 | } 186 | break; 187 | 188 | default: 189 | break; 190 | } 191 | }; 192 | 193 | let traverse = function (o, keyParts, func) { 194 | for (let i in o) { 195 | let thisKeyParts = keyParts.concat(i); 196 | let thisKey = thisKeyParts.join('.'); 197 | let thisType = getObjItem(optsTypes, thisKey); 198 | 199 | if (typeof thisType === 'string') { 200 | let thisItem = o[i]; 201 | func(thisKey, thisItem, thisType); 202 | } 203 | if (o[i] !== null && typeof o[i] === 'object') { 204 | traverse(o[i], thisKeyParts, func); 205 | } 206 | } 207 | }; 208 | 209 | module.exports = (opts) => { 210 | traverse(opts, [], validator); 211 | }; -------------------------------------------------------------------------------- /source/lib/worksheet/sheet_default_params.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'margins': { 3 | 'bottom': 0.75, 4 | 'footer': 0.3, 5 | 'header': 0.3, 6 | 'left': 0.7, 7 | 'right': 0.7, 8 | 'top': 0.75 9 | }, 10 | 'printOptions': { 11 | 'centerHorizontal': null, 12 | 'centerVertical': null, 13 | 'printGridLines': null, 14 | 'printHeadings': null 15 | 16 | }, 17 | 'headerFooter': { 18 | 'evenFooter': null, 19 | 'evenHeader': null, 20 | 'firstFooter': null, 21 | 'firstHeader': null, 22 | 'oddFooter': null, 23 | 'oddHeader': null, 24 | 'alignWithMargins': null, 25 | 'differentFirst': null, 26 | 'differentOddEven': null, 27 | 'scaleWithDoc': null 28 | }, 29 | 'pageSetup': { 30 | 'blackAndWhite': null, 31 | 'cellComments': null, 32 | 'copies': null, 33 | 'draft': null, 34 | 'errors': null, 35 | 'firstPageNumber': null, 36 | 'fitToHeight': null, 37 | 'fitToWidth': null, 38 | 'horizontalDpi': null, 39 | 'orientation': null, 40 | 'pageOrder': null, 41 | 'paperHeight': null, 42 | 'paperSize': null, 43 | 'paperWidth': null, 44 | 'scale': null, 45 | 'useFirstPageNumber': null, 46 | 'usePrinterDefaults': null, 47 | 'verticalDpi': null 48 | }, 49 | 'sheetView': { 50 | 'pane': { 51 | 'activePane': null, 52 | 'state': null, 53 | 'topLeftCell': null, 54 | 'xSplit': null, 55 | 'ySplit': null 56 | }, 57 | 'tabSelected': 0, 58 | 'workbookViewId': 0, 59 | 'rightToLeft': 0, 60 | 'showGridLines': 1, 61 | 'zoomScale': 100, 62 | 'zoomScaleNormal': 100, 63 | 'zoomScalePageLayoutView': 100 64 | }, 65 | 'sheetFormat': { 66 | 'baseColWidth': 10, 67 | 'customHeight': null, 68 | 'defaultColWidth': null, 69 | 'defaultRowHeight': null, 70 | 'outlineLevelCol': null, 71 | 'outlineLevelRow': null, 72 | 'thickBottom': null, 73 | 'thickTop': null, 74 | 'zeroHeight': null 75 | }, 76 | 'sheetProtection': { // same as "Protect Sheet" in Review tab of Excel 77 | 'autoFilter': null, 78 | 'deleteColumns': null, 79 | 'deleteRows': null, 80 | 'formatCells': null, 81 | 'formatColumns': null, 82 | 'formatRows': null, 83 | 'hashValue': null, 84 | 'insertColumns': null, 85 | 'insertHyperlinks': null, 86 | 'insertRows': null, 87 | 'objects': null, 88 | 'password': null, 89 | 'pivotTables': null, 90 | 'scenarios': null, 91 | 'selectLockedCells': null, 92 | 'selectUnlockedCells': null, 93 | 'sheet': null, 94 | 'sort': null 95 | }, 96 | 'outline': { 97 | 'summaryBelow': null, 98 | 'summaryRight': null 99 | }, 100 | 'autoFilter': { 101 | 'startRow': null, 102 | 'endRow': null, 103 | 'startCol': null, 104 | 'endCol': null, 105 | 'ref': null, 106 | 'filters': [] 107 | }, 108 | 'disableRowSpansOptimization': false, 109 | 'hidden': false, 110 | }; -------------------------------------------------------------------------------- /tests/cell.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const DOMParser = require('xmldom').DOMParser; 3 | const xl = require('../source/index'); 4 | 5 | test('Cell coverage', (t) => { 6 | t.plan(1); 7 | let wb = new xl.Workbook(); 8 | let ws = wb.addWorksheet('test'); 9 | let cellAccessor = ws.cell(1, 1); 10 | t.ok(cellAccessor, 'Correctly generated cellAccessor object'); 11 | }); 12 | 13 | test('Cell returns correct number of cell references', (t) => { 14 | t.plan(1); 15 | let wb = new xl.Workbook(); 16 | let ws = wb.addWorksheet('test'); 17 | let cellAccessor = ws.cell(1, 1, 5, 2); 18 | t.ok(cellAccessor.excelRefs.length === 10, 'cellAccessor returns correct number of cellRefs'); 19 | }); 20 | 21 | test('Add String to cell', (t) => { 22 | t.plan(3); 23 | let wb = new xl.Workbook(); 24 | let ws = wb.addWorksheet('test'); 25 | let cell = ws.cell(1, 1).string('my test string'); 26 | let thisCell = ws.cells[cell.excelRefs[0]]; 27 | t.ok(thisCell.t === 's', 'cellType set to sharedString'); 28 | t.ok(typeof (thisCell.v) === 'number', 'cell Value is a number'); 29 | t.ok(wb.sharedStrings[thisCell.v] === 'my test string', 'Cell sharedString value is correct'); 30 | }); 31 | 32 | test('Replace null or undefined value with empty string', (t) => { 33 | t.plan(3); 34 | let wb = new xl.Workbook(); 35 | let ws = wb.addWorksheet('test'); 36 | let cell = ws.cell(1, 1).string(null); 37 | let thisCell = ws.cells[cell.excelRefs[0]]; 38 | t.ok(thisCell.t === 's', 'cellType set to sharedString'); 39 | t.ok(typeof (thisCell.v) === 'number', 'cell Value is a number'); 40 | t.ok(wb.sharedStrings[thisCell.v] === '', 'Cell is empty string'); 41 | }); 42 | 43 | test('Add Number to cell', (t) => { 44 | t.plan(3); 45 | let wb = new xl.Workbook(); 46 | let ws = wb.addWorksheet('test'); 47 | let cell = ws.cell(1, 1).number(10); 48 | let thisCell = ws.cells[cell.excelRefs[0]]; 49 | t.ok(thisCell.t === 'n', 'cellType set to number'); 50 | t.ok(typeof (thisCell.v) === 'number', 'cell Value is a number'); 51 | t.ok(thisCell.v === 10, 'Cell value value is correct'); 52 | }); 53 | 54 | test('Add Boolean to cell', (t) => { 55 | t.plan(3); 56 | let wb = new xl.Workbook(); 57 | let ws = wb.addWorksheet('test'); 58 | let cell = ws.cell(1, 1).bool(true); 59 | let thisCell = ws.cells[cell.excelRefs[0]]; 60 | t.ok(thisCell.t === 'b', 'cellType set to boolean'); 61 | t.ok(typeof (thisCell.v) === 'string', 'cell Value is a string'); 62 | t.ok(thisCell.v === 'true' || thisCell.v === 'false', 'Cell value value is correct'); 63 | }); 64 | 65 | test('Add Formula to cell', (t) => { 66 | t.plan(4); 67 | let wb = new xl.Workbook(); 68 | let ws = wb.addWorksheet('test'); 69 | let cell = ws.cell(1, 1).formula('SUM(A1:A10)'); 70 | let thisCell = ws.cells[cell.excelRefs[0]]; 71 | t.ok(thisCell.t === null, 'cellType is not set'); 72 | t.ok(thisCell.v === null, 'cellValue is not set'); 73 | t.ok(typeof (thisCell.f) === 'string', 'cell Formula is a string'); 74 | t.ok(thisCell.f === 'SUM(A1:A10)', 'Cell value value is correct'); 75 | }); 76 | 77 | test('Add Comment to cell', (t) => { 78 | let wb = new xl.Workbook(); 79 | let ws = wb.addWorksheet('test'); 80 | let cell = ws.cell(1, 1).comment('My test comment'); 81 | let ref = cell.excelRefs[0]; 82 | t.ok(ws.comments[ref].comment === 'My test comment'); 83 | ws.generateCommentsXML().then((XML) => { 84 | let doc = new DOMParser().parseFromString(XML); 85 | let testComment = doc.getElementsByTagName('commentList')[0]; 86 | t.ok(testComment.textContent === 'My test comment', 'Verify comment text is correct'); 87 | t.end() 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/cf_rule.test.js: -------------------------------------------------------------------------------- 1 | var deepmerge = require('deepmerge'); 2 | var test = require('tape'); 3 | 4 | var CfRule = require('../source/lib/worksheet/cf/cf_rule'); 5 | 6 | test('CfRule init', function (t) { 7 | t.plan(4); 8 | 9 | var baseConfig = { 10 | type: 'expression', 11 | formula: 'NOT(ISERROR(SEARCH("??", A1)))', 12 | priority: 1, 13 | dxfId: 0 14 | }; 15 | 16 | t.ok(new CfRule(baseConfig), 'init with valid and support type'); 17 | 18 | try { 19 | var cfr = new CfRule(deepmerge(baseConfig, { 20 | type: 'bogusType' 21 | })); 22 | } catch (err) { 23 | t.ok( 24 | err instanceof TypeError, 25 | 'init of CfRule with invalid type should throw an error' 26 | ); 27 | } 28 | 29 | try { 30 | var cfr = new CfRule(deepmerge(baseConfig, { 31 | type: 'dataBar' 32 | })); 33 | } catch (err) { 34 | t.ok( 35 | err instanceof TypeError, 36 | 'init of CfRule with an unsupported type should throw an error' 37 | ); 38 | } 39 | 40 | try { 41 | var cfr = new CfRule(deepmerge(baseConfig, { 42 | formula: null 43 | })); 44 | } catch (err) { 45 | t.ok( 46 | err instanceof TypeError, 47 | 'init of CfRule with missing properties should throw an error' 48 | ); 49 | } 50 | 51 | }); -------------------------------------------------------------------------------- /tests/column.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source/index'); 3 | let Column = require('../source/lib/column/column.js'); 4 | 5 | test('Column Tests', (t) => { 6 | 7 | let wb = new xl.Workbook(); 8 | let ws = wb.addWorksheet(); 9 | 10 | t.ok(ws.column(2) instanceof Column, 'Successfully accessed a column object'); 11 | t.ok(ws.cols['2'] instanceof Column, 'Column was successfully added to worksheet object'); 12 | 13 | ws.column(2).setWidth(40); 14 | t.equals(ws.column(2).width, 40, 'Column width successfully changed to integer'); 15 | 16 | ws.column(2).setWidth(40.5); 17 | t.equals(ws.column(2).width, 40.5, 'Column width successfully changed to float'); 18 | 19 | ws.column(2).freeze(4); 20 | t.equals(ws.opts.sheetView.pane.xSplit, 2, 'Worksheet set to freeze pane at column 2'); 21 | t.equals(ws.opts.sheetView.pane.topLeftCell, 'D1', 'Worksheet set to freeze pane at column 2 and scrollTo column 4'); 22 | 23 | ws.row(4).freeze(); 24 | t.equals(ws.opts.sheetView.pane.topLeftCell, 'D5', 'topLeftCell updated when row was also frozen'); 25 | 26 | t.end(); 27 | }); -------------------------------------------------------------------------------- /tests/dataValidations.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const xl = require('../source/index'); 3 | const DOMParser = require('xmldom').DOMParser; 4 | const DataValidation = require('../source/lib/worksheet/classes/dataValidation.js'); 5 | 6 | test('DataValidation Tests', (t) => { 7 | let wb = new xl.Workbook(); 8 | let ws = wb.addWorksheet('test'); 9 | 10 | let val1 = ws.addDataValidation({ 11 | type: 'whole', 12 | errorStyle: 'warning', 13 | operator: 'greaterThan', 14 | showInputMessage: '1', 15 | showErrorMessage: '1', 16 | errorTitle: 'Invalid Data', 17 | error: 'The value must be a whole number greater than 0.', 18 | promptTitle: 'Whole Number', 19 | prompt: 'Please enter a whole number greater than 0.', 20 | sqref: 'A1:B1', 21 | formulas: [ 22 | 0 23 | ] 24 | }); 25 | 26 | let val2 = ws.addDataValidation({ 27 | type: 'list', 28 | allowBlank: 1, 29 | showInputMessage: 1, 30 | showErrorMessage: 1, 31 | sqref: 'X2:X10', 32 | formulas: [ 33 | 'value1,value2' 34 | ] 35 | }); 36 | 37 | let val3 = ws.addDataValidation({ 38 | type: 'list', 39 | allowBlank: 1, 40 | showInputMessage: 1, 41 | showErrorMessage: 1, 42 | showDropDown: true, 43 | sqref: 'X2:X10', 44 | formulas: [ 45 | 'value1,value2' 46 | ] 47 | }); 48 | 49 | let val4 = ws.addDataValidation({ 50 | type: 'list', 51 | allowBlank: 1, 52 | showInputMessage: 1, 53 | showErrorMessage: 1, 54 | showDropDown: false, 55 | sqref: 'X2:X10', 56 | formulas: [ 57 | 'value1,value2' 58 | ] 59 | }); 60 | 61 | let val5 = ws.addDataValidation({ 62 | type: 'whole', 63 | errorStyle: 'warning', 64 | operator: 'between', 65 | showInputMessage: '1', 66 | showErrorMessage: '1', 67 | errorTitle: 'Invalid Data', 68 | error: 'The value must be a whole number greater than 0.', 69 | promptTitle: 'Whole Number', 70 | prompt: 'Please enter a whole number greater than 0.', 71 | sqref: 'A10:D10', 72 | formulas: [0, 10] 73 | }); 74 | 75 | t.ok( 76 | val1 instanceof DataValidation.DataValidation && 77 | val2 instanceof DataValidation.DataValidation && 78 | val3 instanceof DataValidation.DataValidation && 79 | val4 instanceof DataValidation.DataValidation && 80 | val5 instanceof DataValidation.DataValidation && 81 | ws.dataValidationCollection.length === 5, 82 | 'Data Validations Created' 83 | ); 84 | t.ok(val1.formula1 === 0 && val1.formula2 === undefined, 'formula\'s of first validation correctly set'); 85 | t.ok(val2.formula1 === 'value1,value2' && val2.formula2 === undefined, 'formula\'s of 2nd validation correctly set'); 86 | t.ok(val3.formula1 === 'value1,value2' && val3.formula2 === undefined, 'formula\'s of 3rd validation correctly set'); 87 | t.ok(val5.formula1 === 0 && val5.formula2 === 10, 'formula\'s of 4th validation correctly set'); 88 | try { 89 | let val6 = ws.addDataValidation({ 90 | type: 'list', 91 | allowBlank: 1, 92 | showInputMessage: 1, 93 | showErrorMessage: 1, 94 | //sqref: 'X2:X10', 95 | formulas: [ 96 | 'value1,value2' 97 | ] 98 | }); 99 | t.ok(val6 instanceof DataValidation === false, 'init of DataValidation with missing properties should throw an error'); 100 | } catch (e) { 101 | t.ok( 102 | e instanceof TypeError, 103 | 'init of DataValidation with missing properties should throw an error' 104 | ); 105 | } 106 | 107 | ws.generateXML().then((XML) => { 108 | let doc = new DOMParser().parseFromString(XML); 109 | let dataValidations = doc.getElementsByTagName('dataValidation'); 110 | 111 | t.equals(dataValidations[1].getAttribute('showDropDown'), '', 'showDropDown correclty not set when showDropDown is set to true'); 112 | t.equals(dataValidations[2].getAttribute('showDropDown'), '', 'showDropDown correclty not set when showDropDown is not specified'); 113 | t.equals(dataValidations[3].getAttribute('showDropDown'), '1', 'showDropDown correclty set to 1 when showDropDown is set to false'); 114 | t.end(); 115 | }); 116 | 117 | 118 | }); -------------------------------------------------------------------------------- /tests/hyperlink.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source/index'); 3 | 4 | test('Create Hyperlink', (t) => { 5 | let wb = new xl.Workbook(); 6 | let ws = wb.addWorksheet('test'); 7 | ws.cell(1, 1).link('http://iamnater.com', 'iAmNater', 'iAmNater.com'); 8 | t.ok(ws.hyperlinkCollection.links[0].location === 'http://iamnater.com', 'Link location set correctly'); 9 | t.ok(ws.hyperlinkCollection.links[0].display === 'iAmNater', 'Link display set correctly'); 10 | t.ok(ws.hyperlinkCollection.links[0].tooltip === 'iAmNater.com', 'Link tooltip set correctly'); 11 | t.ok(typeof ws.hyperlinkCollection.links[0].id === 'number', 'ID correctly set'); 12 | t.ok(ws.hyperlinkCollection.links[0].rId === 'rId' + ws.hyperlinkCollection.links[0].id, 'Link Ref ID set correctly'); 13 | t.end(); 14 | }); -------------------------------------------------------------------------------- /tests/image.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const _tape = require('tape-promise').default; 3 | const test = _tape(tape); 4 | const xl = require('../source/index'); 5 | const Picture = require('../source/lib/drawing/picture.js'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | test('Test adding images', (t) => { 10 | var wb = new xl.Workbook(); 11 | var ws = wb.addWorksheet('test 1'); 12 | 13 | ws.addImage({ 14 | path: path.resolve(__dirname, '../sampleFiles/thumbs-up.jpg'), 15 | type: 'picture', 16 | position: { 17 | type: 'absoluteAnchor', 18 | x: '1in', 19 | y: '2in' 20 | } 21 | }); 22 | 23 | ws.addImage({ 24 | path: path.resolve(__dirname, '../sampleFiles/logo.png'), 25 | type: 'picture', 26 | position: { 27 | type: 'oneCellAnchor', 28 | from: { 29 | col: 1, 30 | colOff: '0.5in', 31 | row: 1, 32 | rowOff: 0 33 | } 34 | } 35 | }); 36 | 37 | ws.addImage({ 38 | image: fs.readFileSync(path.resolve(__dirname, '../sampleFiles/logo.png')), 39 | type: 'picture', 40 | fileName: 'logo.png', 41 | position: { 42 | type: 'twoCellAnchor', 43 | from: { 44 | col: 1, 45 | colOff: 0, 46 | row: 10, 47 | rowOff: 0 48 | }, 49 | to: { 50 | col: 4, 51 | colOff: 0, 52 | row: 13, 53 | rowOff: 0 54 | } 55 | } 56 | }); 57 | 58 | let pics = ws.drawingCollection.drawings; 59 | t.ok(pics[0] instanceof Picture && pics[1] instanceof Picture && pics[2] instanceof Picture, '3 new picture successfully created'); 60 | 61 | try { 62 | ws.addImage({ 63 | path: path.resolve(__dirname, '../sampleFiles/logo.png'), 64 | type: 'picture', 65 | position: { 66 | type: 'twoCellAnchor', 67 | from: { 68 | col: 1, 69 | colOff: 0, 70 | row: 10, 71 | rowOff: 0 72 | } 73 | } 74 | }); 75 | t.notOk(pics[3] instanceof Picture, 'Adding twoCellAnchor picture without specifying to position should throw error'); 76 | } catch (e) { 77 | t.ok( 78 | e instanceof TypeError, 79 | 'Adding twoCellAnchor picture without specifying to position should throw error' 80 | ); 81 | } 82 | 83 | t.end(); 84 | }); -------------------------------------------------------------------------------- /tests/library.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source/index'); 3 | 4 | test('Test library functions', (t) => { 5 | t.equals(xl.getExcelRowCol('A1').row, 1, 'Returned correct row from ref lookup'); 6 | t.equals(xl.getExcelRowCol('C10').row, 10, 'Returned correct row from ref lookup'); 7 | t.equals(xl.getExcelRowCol('AA14').row, 14, 'Returned correct row from ref lookup'); 8 | t.equals(xl.getExcelRowCol('ABA999').row, 999, 'Returned correct row from ref lookup'); 9 | t.equals(xl.getExcelRowCol('A1').col, 1, 'Returned correct column from ref lookup'); 10 | t.equals(xl.getExcelRowCol('AA1').col, 27, 'Returned correct column from ref lookup'); 11 | t.equals(xl.getExcelRowCol('ZA1').col, 677, 'Returned correct column from ref lookup'); 12 | t.equals(xl.getExcelRowCol('ABA1').col, 729, 'Returned correct column from ref lookup'); 13 | 14 | t.equals(xl.getExcelAlpha(1), 'A', 'Returned correct column alpha'); 15 | t.equals(xl.getExcelAlpha(27), 'AA', 'Returned correct column alpha'); 16 | t.equals(xl.getExcelAlpha(677), 'ZA', 'Returned correct column alpha'); 17 | t.equals(xl.getExcelAlpha(729), 'ABA', 'Returned correct column alpha'); 18 | 19 | t.equals(xl.getExcelCellRef(1, 1), 'A1', 'Returned correct excel cell reference'); 20 | t.equals(xl.getExcelCellRef(10, 3), 'C10', 'Returned correct excel cell reference'); 21 | t.equals(xl.getExcelCellRef(14, 27), 'AA14', 'Returned correct excel cell reference'); 22 | t.equals(xl.getExcelCellRef(999, 729), 'ABA999', 'Returned correct excel cell reference'); 23 | 24 | /** 25 | * Tests as defined in §18.17.4.3 of ECMA-376, Second Edition, Part 1 - Fundamentals And Markup Language Reference 26 | * The serial value 3687.4207639... represents 1910-02-03T10:05:54Z 27 | * The serial value 1.5000000... represents 1900-01-01T12:00:00Z 28 | * The serial value 2958465.9999884... represents 9999-12-31T23:59:59Z 29 | */ 30 | t.equals(xl.getExcelTS(new Date('1910-02-03T10:05:54Z')), 3687.4207639, 'Correctly translated date 1910-02-03T10:05:54Z'); 31 | t.equals(xl.getExcelTS(new Date('1900-01-01T12:00:00Z')), 1.5000000, 'Correctly translated date 1900-01-01T12:00:00Z'); 32 | t.equals(xl.getExcelTS(new Date('9999-12-31T23:59:59Z')), 2958465.9999884, 'Correctly translated date 9999-12-31T23:59:59Z'); 33 | 34 | /** 35 | * Tests as defined in §18.17.4.1 of ECMA-376, Second Edition, Part 1 - Fundamentals And Markup Language Reference 36 | * The serial value 2.0000000... represents 1900-01-01 37 | * The serial value 3687.0000000... represents 1910-02-03 38 | * The serial value 38749.0000000... represents 2006-02-01 39 | * The serial value 2958465.0000000... represents 9999-12-31 40 | */ 41 | t.equals(xl.getExcelTS(new Date('1900-01-01T00:00:00Z')), 1, 'Correctly translated 1900-01-01'); 42 | t.equals(xl.getExcelTS(new Date('1910-02-03T00:00:00Z')), 3687, 'Correctly translated 1910-02-03'); 43 | t.equals(xl.getExcelTS(new Date('2006-02-01T00:00:00Z')), 38749, 'Correctly translated 2006-02-01'); 44 | t.equals(xl.getExcelTS(new Date('9999-12-31T00:00:00Z')), 2958465, 'Correctly translated 9999-12-31'); 45 | 46 | t.equals(xl.getExcelTS(new Date('2017-06-01T00:00:00.000Z')), 42887, 'Correctly translated 2017-06-01'); 47 | 48 | t.end(); 49 | }); -------------------------------------------------------------------------------- /tests/row.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source/index'); 3 | let Row = require('../source/lib/row/row.js'); 4 | 5 | test('Row Tests', (t) => { 6 | 7 | let rowWb = new xl.Workbook({ logLevel: 5 }); 8 | let rowWS = rowWb.addWorksheet(); 9 | 10 | t.ok(rowWS.row(2) instanceof Row, 'Successfully accessed a row object'); 11 | t.ok(rowWS.rows['2'] instanceof Row, 'Row was successfully added to worksheet object'); 12 | 13 | rowWS.row(2).setHeight(40); 14 | t.equals(rowWS.row(2).height, 40, 'Row height successfully changed'); 15 | 16 | rowWS.row(2).filter(); 17 | t.equals(rowWS.opts.autoFilter.startRow, 2, 'Filters added to row 2'); 18 | 19 | rowWS.row(3).filter({ 20 | firstRow: 1, 21 | firstColumn: 2, 22 | lastRow: 20, 23 | lastColumn: 5 24 | }); 25 | t.equals(rowWS.opts.autoFilter.endRow, 20, 'Manual filters set to end at row 20'); 26 | t.equals(rowWS.opts.autoFilter.endCol, 5, 'Manual filters set to end at column 5'); 27 | t.equals(rowWS.opts.autoFilter.startCol, 2, 'Manual filters set to start at column 2'); 28 | 29 | rowWS.row(2).freeze(4); 30 | t.equals(rowWS.opts.sheetView.pane.ySplit, 2, 'Worksheet set to freeze pane at row 2'); 31 | t.equals(rowWS.opts.sheetView.pane.topLeftCell, 'A4', 'Worksheet set to freeze pane at row 2 and scrollTo row 4'); 32 | 33 | rowWS.column(4).freeze(); 34 | t.equals(rowWS.opts.sheetView.pane.topLeftCell, 'E4', 'topLeftCell updated when column was also frozen'); 35 | 36 | t.end(); 37 | }); -------------------------------------------------------------------------------- /tests/style.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source/index.js'); 3 | let Style = require('../source/lib/style'); 4 | let xmlbuilder = require('xmlbuilder'); 5 | 6 | test('Create New Style', (t) => { 7 | t.plan(1); 8 | let wb = new xl.Workbook(); 9 | let style = wb.createStyle(); 10 | 11 | t.ok(style instanceof Style, 'Correctly generated Style object'); 12 | }); 13 | 14 | test('Set Style Properties', (t) => { 15 | t.plan(); 16 | 17 | let wb = new xl.Workbook(); 18 | let style = wb.createStyle({ 19 | alignment: { 20 | horizontal: 'center', 21 | indent: 1, // Number of spaces to indent = indent value * 3 22 | justifyLastLine: true, 23 | readingOrder: 'leftToRight', 24 | relativeIndent: 1, // number of additional spaces to indent 25 | shrinkToFit: false, 26 | textRotation: 0, // number of degrees to rotate text counter-clockwise 27 | vertical: 'bottom', 28 | wrapText: true 29 | }, 30 | font: { 31 | bold: true, 32 | color: 'Black', 33 | condense: false, 34 | extend: false, 35 | family: 'Roman', 36 | italics: true, 37 | name: 'Courier', 38 | outline: true, 39 | scheme: 'major', // §18.18.33 ST_FontScheme (Font scheme Styles) 40 | shadow: true, 41 | strike: true, 42 | size: 14, 43 | underline: true, 44 | vertAlign: 'subscript' // §22.9.2.17 ST_VerticalAlignRun (Vertical Positioning Location) 45 | }, 46 | border: { // §18.8.4 border (Border) 47 | left: { 48 | style: 'thin', 49 | color: '#444444' 50 | }, 51 | right: { 52 | style: 'thin', 53 | color: '#444444' 54 | }, 55 | top: { 56 | style: 'thin', 57 | color: '#444444' 58 | }, 59 | bottom: { 60 | style: 'thin', 61 | color: '#444444' 62 | }, 63 | diagonal: { 64 | style: 'thin', 65 | color: '#444444' 66 | }, 67 | diagonalDown: true, 68 | outline: true 69 | }, 70 | fill: { // §18.8.20 fill (Fill) 71 | type: 'pattern', 72 | patternType: 'solid', 73 | fgColor: 'Yellow' 74 | }, 75 | numberFormat: '0.00##%' // §18.8.30 numFmt (Number Format) 76 | }); 77 | 78 | t.ok(style instanceof Style, 'Style object successfully created'); 79 | 80 | let styleObj = style.toObject(); 81 | t.ok(styleObj.alignment.horizontal === 'center', 'alignment.horizontal correctly set'); 82 | t.ok(styleObj.alignment.indent === 1, 'alignment.indent correctly set'); 83 | t.ok(styleObj.alignment.justifyLastLine === true, 'alignment.justifyLastLine correctly set'); 84 | t.ok(styleObj.alignment.readingOrder === 'leftToRight', 'alignment.readingOrder correctly set'); 85 | t.ok(styleObj.alignment.relativeIndent === 1, 'alignment.relativeIndent correctly set'); 86 | t.ok(styleObj.alignment.shrinkToFit === false, 'alignment.shrinkToFit correctly set'); 87 | t.ok(styleObj.alignment.textRotation === 0, 'alignment.textRotation correctly set'); 88 | t.ok(styleObj.alignment.vertical === 'bottom', 'alignment.vertical correctly set'); 89 | t.ok(styleObj.alignment.wrapText === true, 'alignment.wrapText correctly set'); 90 | t.ok(styleObj.font.bold === true, 'font.bold correctly set'); 91 | t.ok(styleObj.font.color === 'FF000000', 'font.color correctly set'); 92 | t.ok(styleObj.font.condense === false, 'font.condense correctly set'); 93 | t.ok(styleObj.font.extend === false, 'font.extend correctly set'); 94 | t.ok(styleObj.font.family === 'Roman', 'font.family correctly set'); 95 | t.ok(styleObj.font.italics === true, 'font.italics correctly set'); 96 | t.ok(styleObj.font.name === 'Courier', 'font.name correctly set'); 97 | t.ok(styleObj.font.outline === true, 'font.outline correctly set'); 98 | t.ok(styleObj.font.scheme === 'major', 'font.scheme correctly set'); 99 | t.ok(styleObj.font.shadow === true, 'font.shadow correctly set'); 100 | t.ok(styleObj.font.strike === true, 'font.strike correctly set'); 101 | t.ok(styleObj.font.size === 14, 'font.size correctly set'); 102 | t.ok(styleObj.font.underline === true, 'font.underline correctly set'); 103 | t.ok(styleObj.font.vertAlign === 'subscript', 'font.vertAlign correctly set'); 104 | t.ok(styleObj.border.left.style === 'thin', 'border.left.style correctly set'); 105 | t.ok(styleObj.border.left.color === 'FF444444', 'border.left.color correctly set'); 106 | t.ok(styleObj.border.right.style === 'thin', 'border.right.style correctly set'); 107 | t.ok(styleObj.border.right.color === 'FF444444', 'border.right.color correctly set'); 108 | t.ok(styleObj.border.top.style === 'thin', 'border.top.style correctly set'); 109 | t.ok(styleObj.border.top.color === 'FF444444', 'border.top.color correctly set'); 110 | t.ok(styleObj.border.bottom.style === 'thin', 'border.bottom.style correctly set'); 111 | t.ok(styleObj.border.bottom.color === 'FF444444', 'border.bottom.color correctly set'); 112 | t.ok(styleObj.border.diagonal.style === 'thin', 'border.diagonal.style correctly set'); 113 | t.ok(styleObj.border.diagonal.color === 'FF444444', 'border.diagonal.color correctly set'); 114 | t.ok(styleObj.border.diagonalDown === true, 'border.diagonalDown correctly set'); 115 | t.ok(styleObj.border.diagonalUp === undefined, 'border.diagonalUp correctly not set'); 116 | t.ok(styleObj.border.outline === true, 'border.outline correctly set'); 117 | t.ok(styleObj.fill.type === 'pattern', 'fill.type correctly set'); 118 | t.ok(styleObj.fill.patternType === 'solid', 'fill.patternType correctly set'); 119 | t.ok(styleObj.fill.fgColor === 'FFFFFF00', 'fill.fgColor correctly set'); 120 | t.ok(styleObj.fill.bgColor === undefined, 'fill.bgColor correctly not set'); 121 | 122 | let alignmentXMLele = xmlbuilder.create('test'); 123 | style.alignment.addToXMLele(alignmentXMLele); 124 | let alignmentXMLString = alignmentXMLele.doc().end(); 125 | t.ok(alignmentXMLString === '', 'Alignment XML generated successfully'); 126 | 127 | let fontXMLele = xmlbuilder.create('test'); 128 | style.font.addToXMLele(fontXMLele); 129 | let fontXMLString = fontXMLele.doc().end(); 130 | t.ok(fontXMLString === '', 'font xml created successfully'); 131 | 132 | let fillXMLele = xmlbuilder.create('test'); 133 | style.fill.addToXMLele(fillXMLele); 134 | let fillXMLString = fillXMLele.doc().end(); 135 | t.ok(fillXMLString === '', 'Fill xml created successfully'); 136 | 137 | let borderXMLele = xmlbuilder.create('test'); 138 | style.border.addToXMLele(borderXMLele); 139 | let borderXMLString = borderXMLele.doc().end(); 140 | t.ok(borderXMLString === '', 'Border xml created successfully'); 141 | 142 | t.end(); 143 | }); 144 | 145 | test('Update style on Cell', (t) => { 146 | 147 | let wb = new xl.Workbook({ logLevel: 5 }); 148 | let ws = wb.addWorksheet('Sheet1'); 149 | let style = wb.createStyle({ 150 | font: { 151 | size: 14, 152 | name: 'Helvetica', 153 | underline: true 154 | } 155 | }); 156 | ws.cell(1, 1).string('string').style(style); 157 | let styleID = ws.cell(1, 1).cells[0].s; 158 | let thisStyle = wb.styles[styleID]; 159 | t.equals(thisStyle.toObject().font.name, 'Helvetica', 'Cell correctly set to style font.'); 160 | t.equals(thisStyle.toObject().font.underline, true, 'Cell correctly set to style font underline.'); 161 | 162 | ws.cell(1, 1).style({ 163 | font: { 164 | name: 'Courier', 165 | underline: false 166 | } 167 | }); 168 | let styleID2 = ws.cell(1, 1).cells[0].s; 169 | let thisStyle2 = wb.styles[styleID2]; 170 | t.equal(thisStyle2.toObject().font.name, 'Courier', 'Cell font name correctly updated to new font name'); 171 | t.equal(thisStyle2.toObject().font.size, 14, 'Cell font size correctly did not change'); 172 | t.equal(thisStyle2.toObject().font.underline, false, 'Cell font underline correctly unset'); 173 | 174 | t.end(); 175 | }); 176 | 177 | test('Validate borders on cellBlocks', (t) => { 178 | let wb = new xl.Workbook(); 179 | let ws = wb.addWorksheet('Sheet1'); 180 | let style = wb.createStyle({ 181 | border: { 182 | left: { 183 | style: 'thin', 184 | color: '#444444' 185 | }, 186 | right: { 187 | style: 'thin', 188 | color: '#444444' 189 | }, 190 | top: { 191 | style: 'thin', 192 | color: '#444444' 193 | }, 194 | bottom: { 195 | style: 'thin', 196 | color: '#444444' 197 | } 198 | } 199 | }); 200 | let style2 = wb.createStyle({ 201 | border: { 202 | left: { 203 | style: 'thin', 204 | color: '#444444' 205 | }, 206 | right: { 207 | style: 'thin', 208 | color: '#444444' 209 | }, 210 | top: { 211 | style: 'thin', 212 | color: '#444444' 213 | }, 214 | bottom: { 215 | style: 'thin', 216 | color: '#444444' 217 | }, 218 | outline: true 219 | } 220 | }); 221 | 222 | ws.cell(2, 2, 5, 3).style(style); 223 | ws.cell(2, 5, 5, 6).style(style2); 224 | 225 | t.ok(wb.styles[ws.cell(2,2).cells[0].s].border.left !== undefined, 'Left side of top left cell should have a border if outline is set to false'); 226 | t.ok(wb.styles[ws.cell(2,2).cells[0].s].border.right !== undefined, 'Right side of top left cell should have a border if outline is set to false'); 227 | t.ok(wb.styles[ws.cell(2,2).cells[0].s].border.top !== undefined, 'Top side of top left cell should have a border if outline is set to false'); 228 | t.ok(wb.styles[ws.cell(2,2).cells[0].s].border.bottom !== undefined, 'Bottom side of top left cell should have a border if outline is set to false'); 229 | 230 | t.ok(wb.styles[ws.cell(2,5).cells[0].s].border.left !== undefined, 'Left side of top left cell should have a border if outline is set to true'); 231 | t.ok(wb.styles[ws.cell(2,5).cells[0].s].border.top !== undefined, 'Top side of top left cell should have a border if outline is set to true'); 232 | t.ok(wb.styles[ws.cell(2,5).cells[0].s].border.right === undefined, 'Right side of top left cell should NOT have a border if outline is set to true'); 233 | t.ok(wb.styles[ws.cell(2,5).cells[0].s].border.bottom === undefined, 'Bottom side of top left cell should NOT have a border if outline is set to true'); 234 | 235 | t.end(); 236 | }); 237 | 238 | test('Use Workbook default fonts', (t) => { 239 | let wb = new xl.Workbook({ 240 | dateFormat: 'm/d/yy hh:mm:ss', 241 | defaultFont: { 242 | size: 9, 243 | name: 'Arial', 244 | color: 'FF000000' 245 | } 246 | }); 247 | 248 | let style = wb.createStyle({ 249 | font: { 250 | size: 10 251 | } 252 | }); 253 | 254 | var ws = wb.addWorksheet('Fonts'); 255 | 256 | ws.cell(1, 1).string('Arial 9'); 257 | ws.cell(2, 1).string('Arial 10').style(style); 258 | t.equals(wb.styles[ws.cell(1, 1).cells[0].s].font.name, 'Arial', 'Font of cell with no custom style uses font name set as workbook default'); 259 | t.equals(wb.styles[ws.cell(1, 1).cells[0].s].font.size, 9, 'Font of cell with no custom style uses font size set as workbook default'); 260 | t.equals(wb.styles[ws.cell(2, 1).cells[0].s].font.name, 'Arial', 'Font of cell with custom style specifying only font size uses font name set as workbook default'); 261 | t.equals(wb.styles[ws.cell(2, 1).cells[0].s].font.size, 10, 'Font of cell with custom style specifying only font size uses style font size rather than value set as workbook default'); 262 | 263 | t.end(); 264 | }); 265 | 266 | test('Reuse existing styles', (t) => { 267 | // TODO: Needs tests for remaining style props and should test all iterations 268 | 269 | const fontA = { 270 | size: 14, 271 | name: 'Helvetica', 272 | underline: true 273 | }; 274 | 275 | const fontB = { 276 | size: 20, 277 | name: 'Arial', 278 | underline: true 279 | }; 280 | 281 | const borderA = { 282 | left: { 283 | style: 'thin', 284 | color: '#444444' 285 | }, 286 | right: { 287 | style: 'thin', 288 | color: '#444444' 289 | }, 290 | top: { 291 | style: 'thin', 292 | color: '#444444' 293 | }, 294 | bottom: { 295 | style: 'thin', 296 | color: '#444444' 297 | } 298 | }; 299 | 300 | const borderB = { 301 | left: { 302 | style: 'thin', 303 | color: '#111111' 304 | }, 305 | right: { 306 | style: 'thin', 307 | color: '#222222' 308 | }, 309 | top: { 310 | style: 'thin', 311 | color: '#333333' 312 | }, 313 | bottom: { 314 | style: 'thin', 315 | color: '#444444' 316 | } 317 | }; 318 | 319 | const fillA = { 320 | type: 'pattern', 321 | patternType: 'solid', 322 | fgColor: 'Yellow' 323 | }; 324 | 325 | const fillB = { 326 | type: 'pattern', 327 | patternType: 'lightDown', 328 | fgColor: 'Red' 329 | }; 330 | 331 | function testCombination(isEqual, styleOptsA, styleOptsB, message) { 332 | let wb = new xl.Workbook(); 333 | let prevStyleCount = wb.styles.length; 334 | let styleA = wb.createStyle(JSON.parse(JSON.stringify(styleOptsA))); 335 | let styleB = wb.createStyle(JSON.parse(JSON.stringify(styleOptsB))); 336 | if (isEqual) { 337 | t.equal(styleA, styleB, message + ' return same Style instance'); 338 | t.equal(wb.styles.length, prevStyleCount + 1, message + ' only added one style to workbook'); 339 | } 340 | else { 341 | t.notEqual(styleA, styleB, message + ' return different Style instance'); 342 | t.equal(wb.styles.length, prevStyleCount + 2, message + ' added two styles to workbook'); 343 | } 344 | } 345 | 346 | testCombination(true, { 347 | font: fontA 348 | }, { 349 | font: fontA 350 | }, 'Same font'); 351 | 352 | testCombination(false, { 353 | font: fontA 354 | }, { 355 | font: fontB 356 | }, 'Different fonts'); 357 | 358 | testCombination(true, { 359 | border: borderA 360 | }, { 361 | border: borderA 362 | }, 'Same border'); 363 | 364 | testCombination(false, { 365 | border: borderA 366 | }, { 367 | border: borderB 368 | }, 'Different borders'); 369 | 370 | testCombination(true, { 371 | fill: fillA 372 | }, { 373 | fill: fillA 374 | }, 'Same fill'); 375 | 376 | testCombination(false, { 377 | fill: fillA 378 | }, { 379 | fill: fillB 380 | }, 'Different fills'); 381 | 382 | testCombination(true, { 383 | font: fontA, 384 | border: borderA 385 | }, { 386 | font: fontA, 387 | border: borderA 388 | }, 'Same font and border'); 389 | 390 | testCombination(false, { 391 | font: fontA, 392 | border: borderA 393 | }, { 394 | font: fontA, 395 | border: borderB 396 | }, 'Same font different borders'); 397 | 398 | testCombination(false, { 399 | font: fontA, 400 | border: borderA 401 | }, { 402 | font: fontB, 403 | border: borderA 404 | }, 'Different font same borders'); 405 | 406 | testCombination(false, { 407 | font: fontA, 408 | fill: fillA 409 | }, { 410 | font: fontA, 411 | fill: fillB 412 | }, 'Same font different fills'); 413 | 414 | testCombination(true, { 415 | font: fontA, 416 | border: borderA, 417 | fill: fillA 418 | }, { 419 | font: fontA, 420 | border: borderA, 421 | fill: fillA 422 | }, 'Same font, border and fill'); 423 | 424 | t.end(); 425 | }); 426 | -------------------------------------------------------------------------------- /tests/unicodestring.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source'); 3 | 4 | test('Escape Unicode Cell Values', (t) => { 5 | let wb = new xl.Workbook(); 6 | let ws = wb.addWorksheet('test'); 7 | let cellIndex = 1; 8 | /** 9 | * To test that unicode is escaped properly, provide an unescaped source string, and then our 10 | * expected escaped string. 11 | * 12 | * See the following literature: 13 | * https://stackoverflow.com/questions/43094662/excel-accepts-some-characters-whereas-openxml-has-error/43141040#43141040 14 | * https://stackoverflow.com/questions/43094662/excel-accepts-some-characters-whereas-openxml-has-error 15 | * https://www.ecma-international.org/publications/standards/Ecma-376.htm 16 | */ 17 | function testUnicode(strVal, testVal) { 18 | let cellAccessor = ws.cell(1, cellIndex); 19 | let cell = cellAccessor.string(strVal); 20 | let thisCell = ws.cells[cell.excelRefs[0]]; 21 | cellIndex++; 22 | t.ok(wb.sharedStrings[thisCell.v] === testVal, 'Unicode "' + strVal + '" correctly escaped in cell'); 23 | } 24 | 25 | testUnicode('Hi <>', 'Hi <>'); 26 | testUnicode('😂', '😂'); 27 | testUnicode('hello! 😂', 'hello! 😂'); 28 | testUnicode('☕️', '☕️'); // ☕️ is U+2615 which is within the valid range. 29 | testUnicode('😂☕️', '😂☕️'); 30 | testUnicode('Good 🤞🏼 Luck', 'Good 🤞🏼 Luck'); 31 | testUnicode('Fist 🤜🏻🤛🏿 bump', 'Fist 🤜🏻🤛🏿 bump'); 32 | testUnicode('㭩', '㭩'); 33 | testUnicode('I am the Α and the Ω', 'I am the Α and the Ω'); 34 | testUnicode('𐤶', '𐤶'); // Lydian Letter En U+10936 35 | testUnicode('𠁆', '𠁆'); // Ideograph bik6 36 | testUnicode('\u000b', ''); // tab should be removed 37 | 38 | t.end(); 39 | }); -------------------------------------------------------------------------------- /tests/workbook.test.js: -------------------------------------------------------------------------------- 1 | let test = require('tape'); 2 | let xl = require('../source/index'); 3 | let Font = require('../source/lib/style/classes/font.js'); 4 | 5 | test('Change default workbook options', (t) => { 6 | 7 | let wb = new xl.Workbook(); 8 | let wb2 = new xl.Workbook({ 9 | jszip: { 10 | compression: 'DEFLATE' 11 | }, 12 | defaultFont: { 13 | size: 14, 14 | name: 'Arial', 15 | color: 'FFFFFFFF' 16 | } 17 | }); 18 | 19 | let wb1Font = wb.styleData.fonts[0]; 20 | let wb2Font = wb2.styleData.fonts[0]; 21 | 22 | t.ok(wb1Font instanceof Font, 'Default Font successfully created'); 23 | t.ok(wb2Font instanceof Font, 'Updated Default Font successfully created'); 24 | 25 | t.ok(wb1Font.color === 'FF000000', 'Default font color correctly set'); 26 | t.ok(wb1Font.name === 'Calibri', 'Default font name correctly set'); 27 | t.ok(wb1Font.size === 12, 'Default font size correctly set'); 28 | t.ok(wb1Font.family === 'roman', 'Default font family correctly set'); 29 | 30 | 31 | t.ok(wb2Font.color === 'FFFFFFFF', 'Default font color correctly updated'); 32 | t.ok(wb2Font.name === 'Arial', 'Default font name correctly updated'); 33 | t.ok(wb2Font.size === 14, 'Default font size correctly updated'); 34 | t.ok(wb2Font.family === 'roman', 'Default font family correctly updated'); 35 | 36 | t.end(); 37 | }); -------------------------------------------------------------------------------- /validate.sh: -------------------------------------------------------------------------------- 1 | TESTFILE=`pwd`/$1 2 | echo "Testing file $TESTFILE" 3 | 4 | OUTPUT=`docker run --rm -v $TESTFILE:/TestFile.xlsx vindvaki/xlsx-validator /usr/local/bin/xlsx-validator /TestFile.xlsx` 5 | 6 | if [ -z "$OUTPUT" ] 7 | then 8 | echo "===> Package passes validation" 9 | else 10 | echo "===> Package has errors" 11 | echo "$OUTPUT" 12 | fi 13 | --------------------------------------------------------------------------------