├── .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 | [](https://www.npmjs.com/package/excel4node)
2 | [](https://opensource.org/licenses/MIT)
3 | [](https://www.npmjs.com/package/excel4node)
4 | [](https://nodejs.org/en/download/)
5 | [](https://travis-ci.org/natergj/excel4node)
6 | [](https://david-dm.org/natergj/excel4node)
7 | [](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 |
--------------------------------------------------------------------------------