├── examples
├── complex.xlsx
├── simple.xlsx
├── simple.js
├── test.js
├── complex.js
└── simple.html
├── test
├── expect
│ ├── complex.xlsx
│ ├── simple.xlsx
│ ├── simple2.xlsx
│ ├── simple3.xlsx
│ ├── simple4.xlsx
│ └── simple5.xlsx
├── xmlSharedStrings.test.js
├── reftable.test.js
├── xmlWorkbook.test.js
├── xmlWorksheet.test.js
├── xmlContentTypes.test.js
├── lib.test.js
├── xmlStyle.test.js
└── file.test.js
├── .travis.yml
├── docs
└── style.css
├── .npmignore
├── .eslintrc
├── .editorconfig
├── src
├── index.js
├── xmlSharedStrings.js
├── reftable.js
├── row.js
├── col.js
├── xmlContentTypes.js
├── node.js
├── lib.js
├── file.js
├── xmlWorkbook.js
├── cell.js
├── xmlWorksheet.js
├── style.js
├── xmlStyle.js
├── sheet.js
└── templates.js
├── .gitignore
├── .esdoc.json
├── .babelrc
├── rollup.config.js
├── package.json
└── README.md
/examples/complex.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/examples/complex.xlsx
--------------------------------------------------------------------------------
/examples/simple.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/examples/simple.xlsx
--------------------------------------------------------------------------------
/test/expect/complex.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/test/expect/complex.xlsx
--------------------------------------------------------------------------------
/test/expect/simple.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/test/expect/simple.xlsx
--------------------------------------------------------------------------------
/test/expect/simple2.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/test/expect/simple2.xlsx
--------------------------------------------------------------------------------
/test/expect/simple3.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/test/expect/simple3.xlsx
--------------------------------------------------------------------------------
/test/expect/simple4.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/test/expect/simple4.xlsx
--------------------------------------------------------------------------------
/test/expect/simple5.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/better-xlsx/HEAD/test/expect/simple5.xlsx
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - 10
5 | - 12
6 | - 14
7 | - 16
8 |
9 | after_success:
10 | - npm run coveralls
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | a[href="source.html"],
2 | a[href^="file/src/"] {
3 | display: none;
4 | }
5 | .import-path {
6 | display: none;
7 | }
8 | .header-notice {
9 | display: none;
10 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | tmp
4 | node_modules
5 | examples
6 | coverage
7 | test
8 | esdoc
9 | docs
10 | .nyc_output
11 | .babelrc
12 | .editorconfig
13 | .esdoc.json
14 | .eslintrc
15 | .travis.yml
16 | constructor-name.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | parser: babel-eslint
2 | extends: standard
3 |
4 | env:
5 | node: true
6 | mocha: true
7 |
8 | rules:
9 | camelcase: 0
10 | semi: [2, always]
11 | object-curly-spacing: [2, always]
12 | no-unused-expressions: 0
13 | lines-between-class-members: 0
14 | dot-notation: 0
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as cell from './cell';
2 | import * as col from './col';
3 | import * as file from './file';
4 | import * as lib from './lib';
5 | import * as row from './row';
6 | import * as sheet from './sheet';
7 | import * as style from './style';
8 | import Zip from 'jszip';
9 |
10 | export default { ...cell, ...col, ...file, ...lib, ...row, ...sheet, ...style, Zip };
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea/
3 | .ipr
4 | .iws
5 | *~
6 | ~*
7 | *.diff
8 | *.patch
9 | *.bak
10 | .DS_Store
11 | Thumbs.db
12 | .project
13 | .*proj
14 | .svn/
15 | *.swp
16 | *.swo
17 | *.log
18 | *.sublime-project
19 | *.sublime-workspace
20 |
21 | npm-debug.log
22 | package-lock.json
23 | node_modules
24 | tmp/
25 |
26 | .buildpath
27 | .settings
28 | coverage
29 | .nyc_output
30 | lib/
31 | esdoc/
32 |
--------------------------------------------------------------------------------
/src/xmlSharedStrings.js:
--------------------------------------------------------------------------------
1 | import { props, Node, HEAD } from './node';
2 |
3 | export @props('xmlns', 'count', 'uniqueCount')
4 | class Xsst extends Node {
5 | constructor ({ xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }, children) {
6 | super({ xmlns }, children);
7 | this[HEAD] = '';
8 | }
9 | }
10 |
11 | export class Xsi extends Node {}
12 |
13 | export @props('xml:space')
14 | class Xt extends Node {}
15 |
16 | export class Xr extends Node {}
17 |
--------------------------------------------------------------------------------
/.esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./src",
3 | "destination": "./esdoc",
4 | "plugins": [{
5 | "name": "esdoc-standard-plugin",
6 | "option": {
7 | "undocumentIdentifier": {
8 | "enable": false
9 | },
10 | "includeSource": {
11 | "enable": false
12 | }
13 | }
14 | }, {
15 | "name": "esdoc-ecmascript-proposal-plugin",
16 | "option": {
17 | "all": true
18 | }
19 | }, {
20 | "name": "esdoc-inject-style-plugin",
21 | "option": {
22 | "styles": ["./docs/style.css"]
23 | }
24 | }]
25 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "targets": { "node": 4 }
5 | }]
6 | ],
7 | "plugins": [
8 | ["class-properties", {
9 | "superClasses": ["Node"],
10 | "props": [{
11 | "key": "name",
12 | "static": true
13 | }]
14 | }],
15 | ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": false }],
16 | "@babel/plugin-proposal-class-properties",
17 | "@babel/plugin-transform-runtime",
18 | "add-module-exports"
19 | ],
20 | "env": {
21 | "test": {
22 | "plugins": [
23 | "istanbul"
24 | ]
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test/xmlSharedStrings.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import { Xsst, Xsi, Xt, Xr } from '../src/xmlSharedStrings';
5 |
6 | describe('Test: xmlSharedStrings.js', () => {
7 | it('should Xsst render', () => {
8 | const sst = new Xsst({});
9 | const si1 = new Xsi();
10 | const si2 = new Xsi();
11 | sst.children = [si1, si2];
12 | si1.children = [new Xt({ 'xml:space': 'preserve' })];
13 | si2.children = [new Xr()];
14 | expect(sst.render()).to.equal('');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/examples/simple.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const xlsx = require('../lib');
3 |
4 | const file = new xlsx.File();
5 |
6 | const sheet = file.addSheet('Sheet1');
7 | const row = sheet.addRow();
8 | const cell = row.addCell();
9 |
10 | cell.value = 'I am a cell!';
11 | cell.hMerge = 2;
12 | cell.vMerge = 1;
13 |
14 | const style = new xlsx.Style();
15 |
16 | style.fill.patternType = 'solid';
17 | style.fill.fgColor = '00FF0000';
18 | style.fill.bgColor = 'FF000000';
19 | style.align.h = 'center';
20 | style.align.v = 'center';
21 |
22 | cell.style = style;
23 |
24 | file
25 | .saveAs()
26 | .pipe(fs.createWriteStream(__dirname + '/simple.xlsx'))
27 | .on('finish', () => console.log('Done.'));
--------------------------------------------------------------------------------
/src/reftable.js:
--------------------------------------------------------------------------------
1 | import { Xsst, Xsi, Xt } from './xmlSharedStrings';
2 |
3 | export class RefTable {
4 | constructor () {
5 | this.strings = [];
6 | this.known = {};
7 | }
8 | makeXsst () {
9 | const len = this.strings.length;
10 | const sst = new Xsst({
11 | count: len,
12 | uniqueCount: len
13 | });
14 | for (const str of this.strings) {
15 | const si = new Xsi({}, [new Xt({}, [str])]);
16 | sst.children.push(si);
17 | }
18 | return sst;
19 | }
20 | addString (str) {
21 | if (this.known[str] === undefined) {
22 | const index = this.strings.length;
23 | this.strings.push(str);
24 | this.known[str] = index;
25 | return index;
26 | }
27 | return this.known[str];
28 | }
29 | getString (index) {
30 | return this.strings[index];
31 | }
32 | length () {
33 | return this.strings.length;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/reftable.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import { RefTable } from '../src/reftable';
5 |
6 | describe('Test: reftable.js', () => {
7 | it('should reftable work ok', () => {
8 | const rt = new RefTable();
9 | // addString
10 | expect(rt.addString('foo')).to.equal(0);
11 | expect(rt.addString('bar')).to.equal(1);
12 | expect(rt.addString('foo')).to.equal(0);
13 | expect(rt.addString('bar')).to.equal(1);
14 | // getString
15 | expect(rt.getString(0)).to.equal('foo');
16 | expect(rt.getString(1)).to.equal('bar');
17 | // length
18 | expect(rt.length()).to.equal(2);
19 | // makeXsst
20 | const sst = rt.makeXsst();
21 | expect(sst.render()).to.equal('foobar');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/examples/test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yazl = require('yazl');
3 | const xlsx = require('../lib');
4 |
5 | const file = new xlsx.File();
6 |
7 | const sheet = file.addSheet('Sheet1');
8 | const row = sheet.addRow();
9 | const cell = row.addCell();
10 |
11 | cell.value = 'I am a cell!';
12 | cell.hMerge = 2;
13 | cell.vMerge = 1;
14 |
15 | const style = new xlsx.Style();
16 |
17 | style.fill.patternType = 'solid';
18 | style.fill.fgColor = '00FF0000';
19 | style.fill.bgColor = 'FF000000';
20 | style.align.h = 'center';
21 | style.align.v = 'center';
22 |
23 | cell.style = style;
24 |
25 | const zip = new yazl.ZipFile();
26 | const parts = file.makeParts();
27 | for (const key of Object.keys(parts)) {
28 | zip.addBuffer(parts[key], key, {
29 | mtime: new Date()
30 | });
31 | }
32 | zip.end();
33 |
34 | zip.outputStream.pipe(fs.createWriteStream(__dirname + '/simple.xlsx'))
35 | .on('finish', () => console.log('Done.'));
--------------------------------------------------------------------------------
/src/row.js:
--------------------------------------------------------------------------------
1 | import { Cell } from './cell';
2 |
3 | /**
4 | * Row of the sheet.
5 | * ```js
6 | * const row = sheet.addRow();
7 | * row.setHeightCM(0.8);
8 | * ```
9 | */
10 | export class Row {
11 | cells = [];
12 | hidden = false;
13 | /**
14 | * Row height
15 | * @type {Number}
16 | */
17 | height = 0;
18 | outlineLevel = 0;
19 | isCustom = false;
20 |
21 | constructor ({ sheet }) {
22 | this.sheet = sheet;
23 | }
24 | /**
25 | * Set height of the Row with `cm` unit.
26 | * @param {Number} ht Height with `cm` unit
27 | */
28 | setHeightCM (ht) {
29 | this.height = ht * 28.3464567;
30 | this.isCustom = true;
31 | }
32 | /**
33 | * Create a cell and add it into the Row.
34 | * @return {Cell}
35 | */
36 | addCell () {
37 | const cell = new Cell({ row: this });
38 | this.cells.push(cell);
39 | this.sheet.maybeAddCol(this.cells.length);
40 | return cell;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/col.js:
--------------------------------------------------------------------------------
1 | import { NumFmt } from './lib';
2 | import { Style } from './style';
3 |
4 | /**
5 | * The column of the Sheet.
6 | *
7 | * ```js
8 | * const col = sheet.col(0);
9 | * col.width = 20;
10 | * col.style.fill.patternType = 'solid';
11 | * col.style.fill.fgColor = '00FF0000';
12 | * col.style.fill.bgColor = 'FF000000';
13 | * col.style.align.h = 'center';
14 | * col.style.align.v = 'center';
15 | * ```
16 | */
17 | export class Col {
18 | outlineLevel = 0;
19 | /**
20 | * Number format for all column @see {@link NumFmt}
21 | * @type {String}
22 | */
23 | numFmt = '';
24 |
25 | constructor ({ min, max, hidden = false, collapsed = false, width = 0 }) {
26 | this.min = min;
27 | this.max = max;
28 | this.hidden = hidden;
29 | this.collapsed = collapsed;
30 | /**
31 | * Column width [default 9.5]
32 | * @type {Number}
33 | */
34 | this.width = width;
35 | /**
36 | * Style of the column.
37 | * @type {Style}
38 | */
39 | this.style = new Style();
40 | }
41 | setType (cellType) {
42 | this.numFmt = NumFmt[cellType];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import nodeResolve from '@rollup/plugin-node-resolve';
4 |
5 | export default {
6 | output: {
7 | file: 'dist/xlsx.js',
8 | format: 'umd',
9 | name: 'xlsx',
10 | globals: {
11 | jszip: 'JSZip'
12 | }
13 | },
14 | input: 'src/index.js',
15 | external: ['jszip'],
16 | plugins: [
17 | nodeResolve(),
18 | commonjs({
19 | include: 'node_modules/**'
20 | }),
21 | babel({
22 | babelrc: false,
23 | presets: [
24 | ['@babel/preset-env', { modules: false }]
25 | ],
26 | plugins: [
27 | ['class-properties', {
28 | superClasses: ['Node'],
29 | props: [{
30 | key: 'name',
31 | static: true
32 | }]
33 | }],
34 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: false }],
35 | '@babel/plugin-proposal-class-properties',
36 | '@babel/plugin-external-helpers',
37 | '@babel/plugin-transform-runtime'
38 | ],
39 | runtimeHelpers: true,
40 | externalHelpers: true,
41 | exclude: 'node_modules/**'
42 | })
43 | ]
44 | };
45 |
--------------------------------------------------------------------------------
/test/xmlWorkbook.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import { makeXworkbook, Xworkbook, makeWorkbookRels, XRelationships } from '../src/xmlWorkbook';
5 |
6 | describe('Test: xmlWorkbook.js', () => {
7 | it('should makeXworkbook return Xworkbook', () => {
8 | const worksheet = makeXworkbook();
9 | expect(worksheet instanceof Xworkbook).to.be.true;
10 | expect(worksheet.render()).to.equal('');
11 | });
12 |
13 | it('should makeWorkbookRels return XRelationships', () => {
14 | const rels = makeWorkbookRels(1);
15 | expect(rels instanceof XRelationships).to.be.true;
16 | expect(rels.render()).to.equal('');
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/xmlContentTypes.js:
--------------------------------------------------------------------------------
1 | import { props, Node, HEAD } from './node';
2 |
3 | export @props('xmlns')
4 | class XTypes extends Node {
5 | constructor ({ xmlns = 'http://schemas.openxmlformats.org/package/2006/content-types' }, children) {
6 | super({ xmlns }, children);
7 | this[HEAD] = '';
8 | }
9 | }
10 |
11 | export @props('Extension', 'ContentType')
12 | class XDefault extends Node {}
13 |
14 | export @props('PartName', 'ContentType')
15 | class XOverride extends Node {}
16 |
17 | export function makeXTypes (types = new XTypes({})) {
18 | const defaults = [{
19 | Extension: 'rels',
20 | ContentType: 'application/vnd.openxmlformats-package.relationships+xml'
21 | }, {
22 | Extension: 'xml',
23 | ContentType: 'application/xml'
24 | }];
25 |
26 | for (const item of defaults) {
27 | types.children.push(new XDefault(item));
28 | }
29 |
30 | const overrides = [{
31 | PartName: '/_rels/.rels',
32 | ContentType: 'application/vnd.openxmlformats-package.relationships+xml'
33 | }, {
34 | PartName: '/docProps/app.xml',
35 | ContentType: 'application/vnd.openxmlformats-officedocument.extended-properties+xml'
36 | }, {
37 | PartName: '/docProps/core.xml',
38 | ContentType: 'application/vnd.openxmlformats-package.core-properties+xml'
39 | }, {
40 | PartName: '/xl/_rels/workbook.xml.rels',
41 | ContentType: 'application/vnd.openxmlformats-package.relationships+xml'
42 | }, {
43 | PartName: '/xl/sharedStrings.xml',
44 | ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'
45 | }, {
46 | PartName: '/xl/styles.xml',
47 | ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'
48 | }, {
49 | PartName: '/xl/workbook.xml',
50 | ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'
51 | }, {
52 | PartName: '/xl/theme/theme1.xml',
53 | ContentType: 'application/vnd.openxmlformats-officedocument.theme+xml'
54 | }];
55 |
56 | for (const override of overrides) {
57 | types.children.push(new XOverride(override));
58 | }
59 | return types;
60 | }
61 |
--------------------------------------------------------------------------------
/src/node.js:
--------------------------------------------------------------------------------
1 | function attrEscape (str) {
2 | return str.replace(/&/g, '&')
3 | .replace(//g, '>')
13 | .replace(/\r/g, '
');
14 | }
15 |
16 | export const HEAD = Symbol('head');
17 |
18 | export function props (...keys) {
19 | return (target) => {
20 | for (const key of keys) {
21 | target.elements.push({
22 | key,
23 | kind: 'method',
24 | placement: 'prototype',
25 | descriptor: {
26 | get () {
27 | if (this.attributes) {
28 | return this.attributes[key];
29 | }
30 | },
31 | set (value) {
32 | if (this.attributes === undefined) {
33 | this.attributes = {};
34 | }
35 | this.attributes[key] = value;
36 | },
37 | configurable: true,
38 | enumerable: true
39 | }
40 | });
41 | }
42 | return target;
43 | };
44 | }
45 |
46 | export class Node {
47 | constructor (attributes = {}, children = [], name) {
48 | for (const key of Object.keys(attributes)) {
49 | this[key] = attributes[key];
50 | }
51 | this.children = children;
52 | this.__name = name || this.constructor.name.substring(1);
53 | }
54 | render () {
55 | function walk (tree) {
56 | const name = tree.__name;
57 | const { attributes, children } = tree;
58 | const tokens = [];
59 |
60 | if (tree[HEAD]) {
61 | tokens.push(tree[HEAD]);
62 | }
63 | tokens.push(`<${name}`);
64 |
65 | for (const key of Object.keys(attributes || {})) {
66 | let v = attributes[key];
67 | if (v === undefined) continue;
68 | if (typeof v === 'string') {
69 | v = attrEscape(v);
70 | }
71 | if (typeof v === 'boolean') {
72 | v = v ? 1 : 0;
73 | }
74 | tokens.push(` ${key}="${v}"`);
75 | }
76 |
77 | if (!children.length) {
78 | tokens.push('/>');
79 | return tokens;
80 | }
81 | tokens.push('>');
82 | for (const child of children) {
83 | if (child instanceof Node) {
84 | tokens.push(child.render());
85 | } else if (typeof child === 'string') {
86 | tokens.push(escape(child));
87 | } else {
88 | tokens.push(child.toString());
89 | }
90 | }
91 | tokens.push(`${name}>`);
92 | return tokens;
93 | }
94 | return walk(this).join('');
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/test/xmlWorksheet.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import { makeXworksheet, Xworksheet, XsheetData, Xrow, Xc, Xf } from '../src/xmlWorksheet';
5 |
6 | describe('Test: xmlWorksheet.js', () => {
7 | it('should makeXworksheet return Xworksheet', () => {
8 | const worksheet = makeXworksheet();
9 | expect(worksheet instanceof Xworksheet).to.be.true;
10 | expect(worksheet.render()).to.equal('&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P');
11 | });
12 |
13 | it('should XsheetData render', () => {
14 | const sheetData = new XsheetData();
15 | const row1 = new Xrow({
16 | r: 1,
17 | ht: 13,
18 | customHeight: 1
19 | });
20 | const row2 = new Xrow({
21 | r: 2,
22 | ht: 30,
23 | customHeight: 1
24 | });
25 | const c1 = new Xc({ r: 'A1', s: 2, t: 's' });
26 | c1.v = '3';
27 | const c2 = new Xc({ r: 'A2', s: 3, t: 's' });
28 | c2.v = 4;
29 | const c3 = new Xc({ r: 'B2', s: 3, t: 's' });
30 | c3.f = new Xf({}, ['SUM(B5:B13)']);
31 |
32 | row1.children = [c1];
33 | row2.children = [c2, c3];
34 |
35 | sheetData.children = [row1, row2];
36 | expect(sheetData.render()).to.equal('3
4SUM(B5:B13)
');
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/lib.js:
--------------------------------------------------------------------------------
1 | export const NumFmtsCount = 163;
2 | /**
3 | * Number format table
4 | *
5 | * ```js
6 | * {
7 | * 0: 'general',
8 | * 1: '0',
9 | * 2: '0.00',
10 | * 3: '#,##0',
11 | * 4: '#,##0.00',
12 | * 9: '0%',
13 | * 10: '0.00%',
14 | * 11: '0.00e+00',
15 | * 12: '# ?/?',
16 | * 13: '# ??/??',
17 | * 14: 'mm-dd-yy',
18 | * 15: 'd-mmm-yy',
19 | * 16: 'd-mmm',
20 | * 17: 'mmm-yy',
21 | * 18: 'h:mm am/pm',
22 | * 19: 'h:mm:ss am/pm',
23 | * 20: 'h:mm',
24 | * 21: 'h:mm:ss',
25 | * 22: 'm/d/yy h:mm',
26 | * 37: '#,##0 ;(#,##0)',
27 | * 38: '#,##0 ;[red](#,##0)',
28 | * 39: '#,##0.00;(#,##0.00)',
29 | * 40: '#,##0.00;[red](#,##0.00)',
30 | * 41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)',
31 | * 42: '_("$"* #,##0_);_("$* (#,##0);_("$"* "-"_);_(@_)',
32 | * 43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)',
33 | * 44: '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)',
34 | * 45: 'mm:ss',
35 | * 46: '[h]:mm:ss',
36 | * 47: 'mmss.0',
37 | * 48: '##0.0e+0',
38 | * 49: '@'
39 | * }
40 | * ```
41 | *
42 | * @type {Object}
43 | */
44 | export const NumFmt = {
45 | 0: 'general',
46 | 1: '0',
47 | 2: '0.00',
48 | 3: '#,##0',
49 | 4: '#,##0.00',
50 | 9: '0%',
51 | 10: '0.00%',
52 | 11: '0.00e+00',
53 | 12: '# ?/?',
54 | 13: '# ??/??',
55 | 14: 'mm-dd-yy',
56 | 15: 'd-mmm-yy',
57 | 16: 'd-mmm',
58 | 17: 'mmm-yy',
59 | 18: 'h:mm am/pm',
60 | 19: 'h:mm:ss am/pm',
61 | 20: 'h:mm',
62 | 21: 'h:mm:ss',
63 | 22: 'm/d/yy h:mm',
64 | 37: '#,##0 ;(#,##0)',
65 | 38: '#,##0 ;[red](#,##0)',
66 | 39: '#,##0.00;(#,##0.00)',
67 | 40: '#,##0.00;[red](#,##0.00)',
68 | 41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)',
69 | 42: '_("$"* #,##0_);_("$* (#,##0);_("$"* "-"_);_(@_)',
70 | 43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)',
71 | 44: '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)',
72 | 45: 'mm:ss',
73 | 46: '[h]:mm:ss',
74 | 47: 'mmss.0',
75 | 48: '##0.0e+0',
76 | 49: '@'
77 | };
78 |
79 | export const NumFmtInv = {};
80 | for (const k of Object.keys(NumFmt)) {
81 | NumFmtInv[NumFmt[k]] = k;
82 | }
83 | // AA => 26
84 | export function col2num (colstr) {
85 | let d = 0;
86 | for (let i = 0; i !== colstr.length; ++i) {
87 | d = 26 * d + colstr.charCodeAt(i) - 64;
88 | }
89 | return d - 1;
90 | }
91 | // 26 => AA
92 | export function num2col (col) {
93 | let s = '';
94 | for (++col; col; col = Math.floor((col - 1) / 26)) {
95 | s = String.fromCharCode(((col - 1) % 26) + 65) + s;
96 | }
97 | return s;
98 | }
99 | // B3 => {x: 1, y: 2}
100 | export function cid2coord (cid) {
101 | const temp = cid.match(/([A-Z]+)(\d+)/);
102 | return {
103 | x: col2num(temp[1]),
104 | y: parseInt(temp[2], 10) - 1
105 | };
106 | }
107 |
108 | export function toExcelTime (d) {
109 | const unix = d.getTime() / 1000;
110 | return unix / 86400.0 + 25569.0;
111 | }
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "better-xlsx",
3 | "version": "0.7.6",
4 | "description": "A better xlsx lib for read / write / toTable / from Table",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "lint": "eslint --ext .js src test",
8 | "build": "npm run build:node && npm run build:dist",
9 | "build:node": "rimraf lib && NODE_ENV=production babel --out-dir=lib src",
10 | "build:dist": "rimraf dist && NODE_ENV=production rollup -c && uglifyjs -m -o dist/xlsx.min.js dist/xlsx.js",
11 | "prepare": "npm run build",
12 | "test": "NODE_ENV=test nyc mocha -t 0",
13 | "report": "nyc report --reporter=html",
14 | "coveralls": "nyc report --reporter=text-lcov | coveralls",
15 | "doc": "rimraf esdoc && esdoc",
16 | "pages": "npm run doc && gh-pages -d esdoc"
17 | },
18 | "nyc": {
19 | "all": true,
20 | "include": [
21 | "src/**/*.js"
22 | ],
23 | "require": [
24 | "@babel/register"
25 | ],
26 | "sourceMap": false,
27 | "instrument": false
28 | },
29 | "pre-commit": [
30 | "lint"
31 | ],
32 | "devDependencies": {
33 | "@babel/cli": "^7.8.4",
34 | "@babel/core": "^7.8.4",
35 | "@babel/plugin-external-helpers": "^7.8.3",
36 | "@babel/plugin-proposal-class-properties": "^7.8.3",
37 | "@babel/plugin-proposal-decorators": "^7.8.3",
38 | "@babel/plugin-transform-runtime": "^7.8.3",
39 | "@babel/preset-env": "^7.8.4",
40 | "@babel/register": "^7.8.3",
41 | "@rollup/plugin-commonjs": "^19.0.2",
42 | "@rollup/plugin-node-resolve": "^13.0.4",
43 | "babel-eslint": "^10.0.3",
44 | "babel-plugin-add-module-exports": "^1.0.2",
45 | "babel-plugin-class-properties": "^1.0.0",
46 | "babel-plugin-istanbul": "^6.0.0",
47 | "chai": "^4.2.0",
48 | "coveralls": "^3.0.9",
49 | "esdoc": "^1.1.0",
50 | "esdoc-ecmascript-proposal-plugin": "^1.0.0",
51 | "esdoc-inject-style-plugin": "^1.0.0",
52 | "esdoc-standard-plugin": "^1.0.0",
53 | "eslint": "^7.31.0",
54 | "eslint-config-standard": "^16.0.3",
55 | "eslint-plugin-import": "^2.20.1",
56 | "eslint-plugin-node": "^11.0.0",
57 | "eslint-plugin-promise": "^5.1.0",
58 | "gh-pages": "^3.2.3",
59 | "mocha": "^9.0.3",
60 | "nyc": "^15.0.0",
61 | "pre-commit": "^1.1.3",
62 | "rimraf": "^3.0.2",
63 | "rollup": "^2.0.0",
64 | "rollup-plugin-babel": "^4.3.3",
65 | "stream-equal": "^2.0.1",
66 | "uglify-js": "^3.7.7"
67 | },
68 | "repository": {
69 | "type": "git",
70 | "url": "git+https://github.com/d-band/better-xlsx.git"
71 | },
72 | "engines": {
73 | "node": ">= 4"
74 | },
75 | "keywords": [
76 | "xlsx",
77 | "excel",
78 | "html",
79 | "read",
80 | "write"
81 | ],
82 | "author": "d-band",
83 | "license": "MIT",
84 | "bugs": {
85 | "url": "https://github.com/d-band/better-xlsx/issues"
86 | },
87 | "homepage": "https://github.com/d-band/better-xlsx#readme",
88 | "dependencies": {
89 | "@babel/runtime": "^7.8.4",
90 | "jszip": "^3.2.2",
91 | "kind-of": "^6.0.3"
92 | },
93 | "collective": {
94 | "type": "opencollective",
95 | "url": "https://opencollective.com/better-xlsx"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/test/xmlContentTypes.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import { makeXTypes, XTypes, XDefault } from '../src/xmlContentTypes';
5 |
6 | describe('Test: xmlContentTypes.js', () => {
7 | it('should makeXTypes return XTypes', () => {
8 | const types = makeXTypes();
9 | expect(types instanceof XTypes).to.be.true;
10 | expect(types.render()).to.equal('');
11 | });
12 |
13 | it('should makeXTypes return XTypes with Default', () => {
14 | const types = makeXTypes(new XTypes({}, [
15 | new XDefault({ Extension: 'pdf', ContentType: 'application/pdf' })
16 | ]));
17 | expect(types instanceof XTypes).to.be.true;
18 | expect(types.render()).to.equal('');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/examples/complex.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const xlsx = require('../lib');
3 |
4 | const file = new xlsx.File();
5 | const sheet = file.addSheet('Sheet1');
6 | const data = [
7 | ['Auto', 200, 90, 'B2-C2'],
8 | ['Entertainment', 200, 32, 'B3-C3'],
9 | ['Food', 350, 205.75, 'B4-C4'],
10 | ['Home', 300, 250, 'B5-C5'],
11 | ['Medical', 100, 35, 'B6-C6'],
12 | ['Personal Items', 300, 80, 'B7-C7'],
13 | ['Travel', 500, 350, 'B8-C8'],
14 | ['Utilities', 200, 100, 'B9-C9'],
15 | ['Other', 50, 60, 'B10-C10']
16 | ];
17 |
18 | function border(cell, top, left, bottom, right) {
19 | const light = 'ffded9d4';
20 | const dark = 'ff7e6a54';
21 | cell.style.border.top = 'thin';
22 | cell.style.border.topColor = top ? dark : light;
23 | cell.style.border.left = 'thin';
24 | cell.style.border.leftColor = left ? dark : light;
25 | cell.style.border.bottom = 'thin';
26 | cell.style.border.bottomColor = bottom ? dark : light;
27 | cell.style.border.right = 'thin';
28 | cell.style.border.rightColor = right ? dark : light;
29 | }
30 |
31 | function fill(cell, type) {
32 | type = type || 0;
33 | const colors = ['ffffffff', 'ffa2917d', 'ffe4e2de', 'fffff8df', 'fff1eeec'];
34 | // 1: header, 2: first col, 3: second col, 4: gray, 0: white
35 | cell.style.fill.patternType = 'solid';
36 | cell.style.fill.fgColor = colors[type];
37 | cell.style.fill.bgColor = 'ffffffff';
38 | }
39 |
40 | const header = sheet.addRow();
41 | header.setHeightCM(0.8);
42 | const headers = ['Category', 'Budget', 'Actual', 'Difference'];
43 | for (let i = 0; i < headers.length; i++) {
44 | const hc = header.addCell();
45 | hc.value = headers[i];
46 | hc.style.align.v = 'center';
47 | if (i > 0) hc.style.align.h = 'right';
48 | hc.style.font.color = 'ffffffff';
49 | border(hc, 0, 0, 1, 0);
50 | fill(hc, 1);
51 | }
52 |
53 | const len = data.length;
54 | for (let i = 0; i < len; i++) {
55 | const line = data[i];
56 | const row = sheet.addRow();
57 | row.setHeightCM(0.8);
58 | // Col 1
59 | const cell1 = row.addCell();
60 | cell1.value = line[0];
61 | cell1.style.align.v = 'center';
62 | if (i === 0) {
63 | border(cell1, 1, 0, 0, 1);
64 | } else if (i === len - 1) {
65 | border(cell1, 0, 0, 1, 1);
66 | } else {
67 | border(cell1, 0, 0, 0, 1);
68 | }
69 | fill(cell1, 2);
70 | // Col 2
71 | const cell2 = row.addCell();
72 | cell2.value = line[1];
73 | cell2.numFmt = '$#,##0.00';
74 | cell2.cellType = 'TypeNumeric';
75 | cell2.style.align.v = 'center';
76 | if (i === 0) {
77 | border(cell2, 1, 1, 0, 0);
78 | } else if (i === len - 1) {
79 | border(cell2, 0, 1, 1, 0);
80 | } else {
81 | border(cell2, 0, 1, 0, 0);
82 | }
83 | fill(cell2, 3);
84 | // Col 3
85 | const cell3 = row.addCell();
86 | cell3.value = line[2];
87 | cell3.numFmt = '$#,##0.00';
88 | cell3.cellType = 'TypeNumeric';
89 | cell3.style.align.v = 'center';
90 | if (i === 0) {
91 | border(cell3, 1, 0, 0, 0);
92 | } else if (i === len - 1) {
93 | border(cell3, 0, 0, 1, 0);
94 | } else {
95 | border(cell3, 0, 0, 0, 0);
96 | }
97 | fill(cell3, i % 2 === 0 ? 0 : 4);
98 | // Col 4
99 | const cell4 = row.addCell();
100 | cell4.formula = line[3];
101 | cell4.numFmt = '$#,##0.00';
102 | cell4.cellType = 'TypeFormula';
103 | cell4.style.align.v = 'center';
104 | if (i === 0) {
105 | border(cell4, 1, 0, 0, 0);
106 | } else if (i === len - 1) {
107 | border(cell4, 0, 0, 1, 0);
108 | } else {
109 | border(cell4, 0, 0, 0, 0);
110 | }
111 | fill(cell4, i % 2 === 0 ? 0 : 4);
112 | }
113 |
114 | for (let i = 0; i < 4; i++) {
115 | sheet.col(i).width = 20;
116 | }
117 |
118 | file
119 | .saveAs()
120 | .pipe(fs.createWriteStream(__dirname + '/complex.xlsx'))
121 | .on('finish', () => console.log('Done.'));
--------------------------------------------------------------------------------
/src/file.js:
--------------------------------------------------------------------------------
1 | import { Sheet } from './sheet';
2 | import * as templates from './templates';
3 | import { RefTable } from './reftable';
4 | import { makeXworkbook, Xsheets, Xsheet, makeWorkbookRels } from './xmlWorkbook';
5 | import { makeXTypes, XOverride } from './xmlContentTypes';
6 | import { XstyleSheet } from './xmlStyle';
7 | import Zip from 'jszip';
8 |
9 | /**
10 | * This is the main class, use it:
11 | *
12 | * ```js
13 | * import { File } from 'better-xlsx';
14 | * const file = new File();
15 | * const sheet = file.addSheet('Sheet-1');
16 | * ```
17 | *
18 | * @class File
19 | */
20 | export class File {
21 | /**
22 | * @private
23 | */
24 | sheet = {};
25 | /**
26 | * @private
27 | */
28 | sheets = [];
29 | /**
30 | * @private
31 | */
32 | definedNames = [];
33 |
34 | constructor () {
35 | /**
36 | * @private
37 | */
38 | this.styles = new XstyleSheet({});
39 | }
40 | /**
41 | * Add a new Sheet, with the provided name, to a File
42 | * @param {String} name Name of the Sheet
43 | * @return {Sheet}
44 | */
45 | addSheet (name) {
46 | if (this.sheet[name]) {
47 | throw new Error(`duplicate sheet name ${name}.`);
48 | }
49 | const sheet = new Sheet({
50 | name,
51 | file: this,
52 | selected: this.sheets.length === 0
53 | });
54 | this.sheet[name] = sheet;
55 | this.sheets.push(sheet);
56 | return sheet;
57 | }
58 | /**
59 | * Save the File to an xlsx file.
60 | * @param {String} [type='nodebuffer'] For Node.js use `nodebuffer` and browser use `blob` or `base64`.
61 | * @param {Boolean} [compress=false] For file compression.
62 | * @return {Promise|stream} For Node.js return `stream` and browser return Promise.
63 | */
64 | saveAs (type = 'nodebuffer', compress = false) {
65 | const parts = this.makeParts();
66 | const zip = new Zip();
67 | for (const key of Object.keys(parts)) {
68 | zip.file(key, parts[key]);
69 | }
70 | const compression = compress ? 'DEFLATE' : 'STORE';
71 | if (type === 'blob' || type === 'base64') {
72 | return zip.generateAsync({ type, compression });
73 | } else {
74 | return zip.generateNodeStream({ type: 'nodebuffer', compression });
75 | }
76 | }
77 | /**
78 | * @private
79 | * @return {Object} XML files mapping object
80 | */
81 | makeParts () {
82 | const parts = {};
83 | const refTable = new RefTable();
84 | const types = makeXTypes();
85 | const workbook = makeXworkbook();
86 |
87 | this.styles.reset();
88 |
89 | let i = 1;
90 | const sheets = new Xsheets();
91 | for (const sheet of this.sheets) {
92 | const xSheet = sheet.makeXSheet(refTable, this.styles);
93 | types.children.push(new XOverride({
94 | PartName: `/xl/worksheets/sheet${i}.xml`,
95 | ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'
96 | }));
97 | sheets.children.push(new Xsheet({
98 | name: sheet.name,
99 | sheetId: i,
100 | 'r:id': `rId${i}`,
101 | state: 'visible'
102 | }));
103 | parts[`xl/worksheets/sheet${i}.xml`] = xSheet.render();
104 | i++;
105 | }
106 | workbook.sheets = sheets;
107 |
108 | parts['xl/workbook.xml'] = workbook.render();
109 | parts['_rels/.rels'] = templates.DOT_RELS;
110 | parts['docProps/app.xml'] = templates.DOCPROPS_APP;
111 | parts['docProps/core.xml'] = templates.DOCPROPS_CORE;
112 | parts['xl/theme/theme1.xml'] = templates.XL_THEME_THEME;
113 |
114 | parts['xl/sharedStrings.xml'] = refTable.makeXsst().render();
115 | parts['xl/_rels/workbook.xml.rels'] = makeWorkbookRels(this.sheets.length).render();
116 | parts['[Content_Types].xml'] = types.render();
117 | parts['xl/styles.xml'] = this.styles.render();
118 |
119 | return parts;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/xmlWorkbook.js:
--------------------------------------------------------------------------------
1 | import { props, Node, HEAD } from './node';
2 |
3 | export @props('xmlns')
4 | class XRelationships extends Node {
5 | constructor ({ xmlns = 'http://schemas.openxmlformats.org/package/2006/relationships' }, children) {
6 | super({ xmlns }, children);
7 | this[HEAD] = '';
8 | }
9 | }
10 |
11 | export @props('Id', 'Target', 'Type')
12 | class XRelationship extends Node {}
13 |
14 | export @props('xmlns', 'xmlns:r')
15 | class Xworkbook extends Node {
16 | fileVersion = null;
17 | workbookPr = null;
18 | bookViews = null;
19 | sheets = null;
20 | calcPr = null;
21 |
22 | constructor (attrs = {}, children) {
23 | attrs['xmlns'] = attrs['xmlns'] || 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
24 | attrs['xmlns:r'] = attrs['xmlns:r'] || 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
25 | super(attrs, children);
26 | this[HEAD] = '';
27 | }
28 | render () {
29 | this.children = [];
30 | if (this.fileVersion) this.children.push(this.fileVersion);
31 | if (this.workbookPr) this.children.push(this.workbookPr);
32 | if (this.bookViews) this.children.push(this.bookViews);
33 | if (this.sheets) this.children.push(this.sheets);
34 | if (this.calcPr) this.children.push(this.calcPr);
35 | return super.render();
36 | }
37 | }
38 |
39 | export @props('appName', 'lastEdited', 'lowestEdited', 'rupBuild')
40 | class XfileVersion extends Node {}
41 |
42 | export @props('defaultThemeVersion', 'backupFile', 'showObjects', 'date1904')
43 | class XworkbookPr extends Node {}
44 |
45 | export class XworkbookProtection extends Node {}
46 |
47 | export class XbookViews extends Node {}
48 |
49 | export @props('activeTab', 'firstSheet', 'showHorizontalScroll', 'showVerticalScroll', 'showSheetTabs', 'tabRatio', 'windowHeight', 'windowWidth', 'xWindow', 'yWindow')
50 | class XworkbookView extends Node {}
51 |
52 | export class Xsheets extends Node {}
53 |
54 | export @props('name', 'sheetId', 'r:id', 'state')
55 | class Xsheet extends Node {}
56 |
57 | export @props('calcId', 'iterateCount', 'refMode', 'iterate', 'iterateDelta')
58 | class XcalcPr extends Node {}
59 |
60 | export function makeWorkbookRels (sheetCount) {
61 | const rels = new XRelationships({});
62 | for (let i = 1; i <= sheetCount; i++) {
63 | rels.children.push(new XRelationship({
64 | Id: `rId${i}`,
65 | Target: `worksheets/sheet${i}.xml`,
66 | Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'
67 | }));
68 | }
69 | rels.children.push(new XRelationship({
70 | Id: `rId${sheetCount + 1}`,
71 | Target: 'sharedStrings.xml',
72 | Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'
73 | }));
74 | rels.children.push(new XRelationship({
75 | Id: `rId${sheetCount + 2}`,
76 | Target: 'theme/theme1.xml',
77 | Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'
78 | }));
79 | rels.children.push(new XRelationship({
80 | Id: `rId${sheetCount + 3}`,
81 | Target: 'styles.xml',
82 | Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'
83 | }));
84 | return rels;
85 | }
86 |
87 | export function makeXworkbook () {
88 | const workbook = new Xworkbook();
89 | workbook.fileVersion = new XfileVersion({ appName: 'JS XLSX' });
90 | workbook.workbookPr = new XworkbookPr({ showObjects: 'all' });
91 | workbook.bookViews = new XbookViews({}, [
92 | new XworkbookView({
93 | showHorizontalScroll: true,
94 | showSheetTabs: true,
95 | showVerticalScroll: true,
96 | tabRatio: 204,
97 | windowHeight: 8192,
98 | windowWidth: 16384,
99 | xWindow: 0,
100 | yWindow: 0
101 | })
102 | ]);
103 | workbook.calcPr = new XcalcPr({
104 | iterateCount: 100,
105 | iterate: false,
106 | iterateDelta: 0.001,
107 | refMode: 'A1'
108 | });
109 |
110 | return workbook;
111 | }
112 |
--------------------------------------------------------------------------------
/src/cell.js:
--------------------------------------------------------------------------------
1 | import { Style } from './style';
2 | import { toExcelTime, NumFmt } from './lib';
3 | import kind from 'kind-of';
4 |
5 | export const CellType = {
6 | TypeString: 49,
7 | TypeFormula: 0,
8 | TypeNumeric: 1,
9 | TypeBool: 0,
10 | TypeInline: 0,
11 | TypeError: 0,
12 | TypeDate: 14,
13 | TypeGeneral: 0
14 | };
15 |
16 | /**
17 | * Cell intended to provide user access to the contents of Cell within an xlsx.Row.
18 | *
19 | * ```js
20 | * const cell = row.addCell();
21 | * cell.value = 'I am a cell!';
22 | * cell.hMerge = 2;
23 | * cell.vMerge = 1;
24 | * cell.style.fill.patternType = 'solid';
25 | * cell.style.fill.fgColor = '00FF0000';
26 | * cell.style.fill.bgColor = 'FF000000';
27 | * cell.style.align.h = 'center';
28 | * cell.style.align.v = 'center';
29 | * ```
30 | *
31 | * Set the cell value
32 | *
33 | * ```js
34 | * const cell = row.addCell();
35 | * // Date type
36 | * cell.setDate(new Date());
37 | * // Number type
38 | * cell.setNumber(123456);
39 | * cell.numFmt = '$#,##0.00';
40 | * ```
41 | */
42 | export class Cell {
43 | _value = '';
44 | _style = null;
45 | formula = '';
46 | /**
47 | * Number format @see {@link NumFmt}
48 | * @type {String}
49 | */
50 | numFmt = '';
51 | date1904 = false;
52 | /**
53 | * Hide the cell.
54 | * @type {Boolean}
55 | */
56 | hidden = false;
57 | /**
58 | * Horizontal merge with other cells.
59 | * @type {Number}
60 | */
61 | hMerge = 0;
62 | /**
63 | * Vertical merge with other cells.
64 | * @type {Number}
65 | */
66 | vMerge = 0;
67 | cellType = 'TypeString';
68 |
69 | /**
70 | * Create a cell and add it to a row.
71 | * @private
72 | * @param {Object} options.row Row of add to
73 | */
74 | constructor ({ row }) {
75 | this.row = row;
76 | }
77 | /**
78 | * Get the cell style.
79 | * @return {Style}
80 | */
81 | get style () {
82 | if (this._style === null) {
83 | this._style = new Style();
84 | }
85 | return this._style;
86 | }
87 | /**
88 | * Set the style of the cell.
89 | * @param {Style} s
90 | */
91 | set style (s) {
92 | this._style = s;
93 | }
94 | /**
95 | * Get the cell value.
96 | */
97 | get value () {
98 | return this._value;
99 | }
100 | /**
101 | * Set the cell value.
102 | * @param {String|Date|Number|Boolean} v
103 | */
104 | set value (v) {
105 | const t = kind(v);
106 | if (t === 'null' || t === 'undefined') {
107 | return this.setString('');
108 | }
109 | if (t === 'date') {
110 | return this.setDateTime(v);
111 | }
112 | if (t === 'number') {
113 | return this.setNumber(v);
114 | }
115 | if (t === 'string') {
116 | return this.setString(v);
117 | }
118 | if (t === 'boolean') {
119 | return this.setBool(v);
120 | }
121 | return this.setString(v.toString());
122 | }
123 | /**
124 | * Set cell value with String type.
125 | * @param {String} v
126 | */
127 | setString (v) {
128 | this._value = v;
129 | this.formula = '';
130 | this.cellType = 'TypeString';
131 | }
132 | /**
133 | * Set cell value with Date type.
134 | * @param {Date} v
135 | */
136 | setDate (v) {
137 | this._value = parseInt(toExcelTime(v));
138 | this.formula = '';
139 | this.numFmt = NumFmt[14];
140 | this.cellType = 'TypeDate';
141 | }
142 | /**
143 | * Set cell value with DateTime type.
144 | * @param {Date} v
145 | */
146 | setDateTime (v) {
147 | this._value = toExcelTime(v);
148 | this.formula = '';
149 | this.numFmt = NumFmt[22];
150 | this.cellType = 'TypeDate';
151 | }
152 | /**
153 | * Set cell value with Number type.
154 | * @param {Number} v
155 | */
156 | setNumber (v) {
157 | this._value = v;
158 | this.formula = '';
159 | this.numFmt = NumFmt[0];
160 | this.cellType = 'TypeNumeric';
161 | }
162 | /**
163 | * Set cell value with Boolean type.
164 | * @param {Boolean} v
165 | */
166 | setBool (v) {
167 | this._value = v ? 1 : 0;
168 | this.cellType = 'TypeBool';
169 | }
170 | /**
171 | * Set cell formula.
172 | * @param {String} f - Formula like `B2*C2-D2`.
173 | */
174 | setFormula (f) {
175 | this.formula = f;
176 | this.cellType = 'TypeFormula';
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/test/lib.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import { Style, handleStyle, handleNumFmtId } from '../src/style';
5 | import { XstyleSheet } from '../src/xmlStyle';
6 | import { col2num, num2col, cid2coord } from '../src/lib';
7 |
8 | describe('Test: lib.js', () => {
9 | it('should col2num ok', () => {
10 | expect(col2num('B')).to.equal(1);
11 | expect(col2num('AA')).to.equal(26);
12 | expect(col2num('AB')).to.equal(27);
13 | });
14 |
15 | it('should num2col ok', () => {
16 | expect(num2col(1)).to.equal('B');
17 | expect(num2col(26)).to.equal('AA');
18 | expect(num2col(27)).to.equal('AB');
19 | });
20 |
21 | it('should cid2coord ok', () => {
22 | expect(cid2coord('A1')).to.deep.equal({ x: 0, y: 0 });
23 | expect(cid2coord('AA2')).to.deep.equal({ x: 26, y: 1 });
24 | expect(cid2coord('AB3')).to.deep.equal({ x: 27, y: 2 });
25 | });
26 |
27 | it('should handleStyle ok', () => {
28 | const style = new Style();
29 | const styles = new XstyleSheet({});
30 |
31 | styles.reset();
32 | handleStyle(style, 0, styles);
33 | expect(styles.render()).to.equal('');
34 |
35 | styles.reset();
36 | handleStyle(style, 3, styles);
37 | expect(styles.render()).to.equal('');
38 | });
39 |
40 | it('should handleNumFmtId ok', () => {
41 | const styles = new XstyleSheet({});
42 |
43 | styles.reset();
44 | handleNumFmtId(0, styles);
45 | expect(styles.render()).to.equal('');
46 |
47 | styles.reset();
48 | handleNumFmtId(3, styles);
49 | expect(styles.render()).to.equal('');
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | better-xlsx
2 | ===========
3 |
4 | > A better xlsx lib for read / write / toTable / from Table
5 |
6 | [](https://www.npmjs.com/package/better-xlsx)
7 | [](https://www.npmjs.com/package/better-xlsx)
8 | [](https://travis-ci.org/d-band/better-xlsx)
9 | [](https://coveralls.io/github/d-band/better-xlsx?branch=master)
10 | [](https://david-dm.org/d-band/better-xlsx)
11 | [](https://greenkeeper.io/)
12 | [](#backers)
13 | [](#sponsors)
14 |
15 | ---
16 |
17 | ## Install
18 |
19 | ```bash
20 | $ npm install better-xlsx
21 | ```
22 |
23 | ## Usage
24 |
25 | - [More Examples](examples)
26 |
27 | ```javascript
28 | const fs = require('fs');
29 | const xlsx = require('better-xlsx');
30 |
31 | const file = new xlsx.File();
32 |
33 | const sheet = file.addSheet('Sheet1');
34 | const row = sheet.addRow();
35 | const cell = row.addCell();
36 |
37 | cell.value = 'I am a cell!';
38 | cell.hMerge = 2;
39 | cell.vMerge = 1;
40 |
41 | const style = new xlsx.Style();
42 |
43 | style.fill.patternType = 'solid';
44 | style.fill.fgColor = '00FF0000';
45 | style.fill.bgColor = 'FF000000';
46 | style.align.h = 'center';
47 | style.align.v = 'center';
48 |
49 | cell.style = style;
50 |
51 | file
52 | .saveAs()
53 | .pipe(fs.createWriteStream('test.xlsx'))
54 | .on('finish', () => console.log('Done.'));
55 | ```
56 |
57 | ## Todo
58 |
59 | - [ ] xlsx parser
60 | - [ ] read excel file
61 | - [x] write excel file
62 | - [x] transform html table to excel file [html2xlsx](https://github.com/d-band/html2xlsx)
63 |
64 | ## Report a issue
65 |
66 | * [All issues](https://github.com/d-band/better-xlsx/issues)
67 | * [New issue](https://github.com/d-band/better-xlsx/issues/new)
68 |
69 | ## Reference
70 |
71 | - http://www.ecma-international.org/publications/standards/Ecma-376.htm
72 | - https://github.com/tealeg/xlsx
73 |
74 | ## Contributors
75 |
76 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
77 |
78 |
79 |
80 | ## Backers
81 |
82 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/better-xlsx#backer)]
83 |
84 |
85 |
86 |
87 | ## Sponsors
88 |
89 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/better-xlsx#sponsor)]
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | ## License
105 |
106 | better-xlsx is available under the terms of the MIT License.
--------------------------------------------------------------------------------
/examples/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Simple for frontend
6 |
7 |
8 |
9 |
10 |
11 |
12 |
135 |
136 |
--------------------------------------------------------------------------------
/src/xmlWorksheet.js:
--------------------------------------------------------------------------------
1 | import { props, Node, HEAD } from './node';
2 |
3 | export @props('xmlns', 'xmlns:r')
4 | class Xworksheet extends Node {
5 | sheetPr = null;
6 | sheetViews = null;
7 | sheetFormatPr = null;
8 | printOptions = null;
9 | pageMargins = null;
10 | pageSetup = null;
11 | headerFooter = null;
12 | mergeCells = null;
13 | dimension = null;
14 | cols = null;
15 | sheetData = null;
16 |
17 | constructor (attrs = {}, children) {
18 | attrs['xmlns'] = attrs['xmlns'] || 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
19 | attrs['xmlns:r'] = attrs['xmlns:r'] || 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
20 | super(attrs, children);
21 | this[HEAD] = '';
22 | }
23 | render () {
24 | this.children = [];
25 | if (this.sheetPr) this.children.push(this.sheetPr);
26 | if (this.dimension) this.children.push(this.dimension);
27 | if (this.sheetViews) this.children.push(this.sheetViews);
28 | if (this.sheetFormatPr) this.children.push(this.sheetFormatPr);
29 | if (this.cols) this.children.push(this.cols);
30 | if (this.sheetData) this.children.push(this.sheetData);
31 | if (this.mergeCells) this.children.push(this.mergeCells);
32 | if (this.printOptions) this.children.push(this.printOptions);
33 | if (this.pageMargins) this.children.push(this.pageMargins);
34 | if (this.pageSetup) this.children.push(this.pageSetup);
35 | if (this.headerFooter) this.children.push(this.headerFooter);
36 | return super.render();
37 | }
38 | }
39 |
40 | export @props('filterMode')
41 | class XsheetPr extends Node {}
42 |
43 | export @props('fitToPage')
44 | class XpageSetUpPr extends Node {}
45 |
46 | export @props('ref')
47 | class Xdimension extends Node {}
48 |
49 | export class XsheetViews extends Node {}
50 |
51 | export @props('windowProtection', 'showFormulas', 'showGridLines', 'showRowColHeaders', 'showZeros', 'rightToLeft', 'tabSelected', 'showOutlineSymbols', 'defaultGridColor', 'view', 'topLeftCell', 'colorId', 'zoomScale', 'zoomScaleNormal', 'zoomScalePageLayoutView', 'workbookViewId')
52 | class XsheetView extends Node {}
53 |
54 | export @props('pane', 'activeCell', 'activeCellId', 'sqref')
55 | class Xselection extends Node {}
56 |
57 | export @props('xSplit', 'ySplit', 'topLeftCell', 'activePane', 'state')
58 | class Xpane extends Node {}
59 |
60 | export @props('defaultColWidth', 'defaultRowHeight', 'outlineLevelCol', 'outlineLevelRow')
61 | class XsheetFormatPr extends Node {}
62 |
63 | export class Xcols extends Node {}
64 |
65 | export @props('collapsed', 'hidden', 'max', 'min', 'style', 'width', 'customWidth', 'outlineLevel')
66 | class Xcol extends Node {}
67 |
68 | export class XsheetData extends Node {}
69 |
70 | export @props('r', 'spans', 'hidden', 'ht', 'customHeight', 'outlineLevel')
71 | class Xrow extends Node {}
72 |
73 | export @props('r', 's', 't')
74 | class Xc extends Node {
75 | constructor (attrs, children) {
76 | super(attrs, children);
77 | this.f = null;
78 | this.v = null;
79 | }
80 | render () {
81 | if (this.f !== null) this.children.push(this.f);
82 | if (this.v !== null) this.children.push(new Node({}, [this.v], 'v'));
83 | return super.render();
84 | }
85 | }
86 |
87 | export @props('t', 'ref', 'si')
88 | class Xf extends Node {}
89 |
90 | export @props('count')
91 | class XmergeCells extends Node {}
92 |
93 | export @props('ref')
94 | class XmergeCell extends Node {}
95 |
96 | export @props('headings', 'gridLines', 'gridLinesSet', 'horizontalCentered', 'verticalCentered')
97 | class XprintOptions extends Node {}
98 |
99 | export @props('left', 'right', 'top', 'bottom', 'header', 'footer')
100 | class XpageMargins extends Node {}
101 |
102 | export @props('paperSize', 'scale', 'firstPageNumber', 'fitToWidth', 'fitToHeight', 'pageOrder', 'orientation', 'usePrinterDefaults', 'blackAndWhite', 'draft', 'cellComments', 'useFirstPageNumber', 'horizontalDpi', 'verticalDpi', 'copies')
103 | class XpageSetup extends Node {}
104 |
105 | export @props('differentFirst', 'differentOddEven')
106 | class XheaderFooter extends Node {
107 | constructor (attrs, children) {
108 | super(attrs, children);
109 | this.oddHeader = null;
110 | this.oddFooter = null;
111 | }
112 | render () {
113 | if (this.oddHeader !== null) this.children.push(new Node({}, [this.oddHeader], 'oddHeader'));
114 | if (this.oddFooter !== null) this.children.push(new Node({}, [this.oddFooter], 'oddFooter'));
115 | return super.render();
116 | }
117 | }
118 |
119 | export function makeXworksheet (sheet = new Xworksheet()) {
120 | sheet.sheetPr = new XsheetPr({ filterMode: false }, [
121 | new XpageSetUpPr({ fitToPage: false })
122 | ]);
123 | sheet.sheetViews = new XsheetViews({}, [
124 | new XsheetView({
125 | colorId: 64,
126 | defaultGridColor: true,
127 | rightToLeft: false,
128 | showFormulas: false,
129 | showGridLines: true,
130 | showOutlineSymbols: true,
131 | showRowColHeaders: true,
132 | showZeros: true,
133 | tabSelected: false,
134 | topLeftCell: 'A1',
135 | view: 'normal',
136 | windowProtection: false,
137 | workbookViewId: 0,
138 | zoomScale: 100,
139 | zoomScaleNormal: 100,
140 | zoomScalePageLayoutView: 100
141 | }, [
142 | new Xselection({
143 | activeCell: 'A1',
144 | activeCellId: 0,
145 | pane: 'topLeft',
146 | sqref: 'A1'
147 | })
148 | ])
149 | ]);
150 | sheet.sheetFormatPr = new XsheetFormatPr({ defaultRowHeight: '12.85' });
151 | sheet.printOptions = new XprintOptions({
152 | gridLines: false,
153 | gridLinesSet: true,
154 | headings: false,
155 | horizontalCentered: false,
156 | verticalCentered: false
157 | });
158 | sheet.pageMargins = new XpageMargins({
159 | left: 0.7875,
160 | right: 0.7875,
161 | top: 1.05277777777778,
162 | bottom: 1.05277777777778,
163 | header: 0.7875,
164 | footer: 0.7875
165 | });
166 | sheet.pageSetup = new XpageSetup({
167 | blackAndWhite: false,
168 | cellComments: 'none',
169 | copies: 1,
170 | draft: false,
171 | firstPageNumber: 1,
172 | fitToHeight: 1,
173 | fitToWidth: 1,
174 | horizontalDpi: 300,
175 | orientation: 'portrait',
176 | pageOrder: 'downThenOver',
177 | paperSize: 9,
178 | scale: 100,
179 | useFirstPageNumber: true,
180 | usePrinterDefaults: false,
181 | verticalDpi: 300
182 | });
183 | const headerFooter = new XheaderFooter();
184 | headerFooter.oddHeader = '&C&"Times New Roman,Regular"&12&A';
185 | headerFooter.oddFooter = '&C&"Times New Roman,Regular"&12Page &P';
186 |
187 | sheet.headerFooter = headerFooter;
188 | return sheet;
189 | }
190 |
--------------------------------------------------------------------------------
/src/style.js:
--------------------------------------------------------------------------------
1 | import { Xfont, Xfill, XpatternFill, Xborder, Xxf, Xalignment } from './xmlStyle';
2 |
3 | export function handleStyle (style, numFmtId, styles) {
4 | const { xFont, xFill, xBorder, xXf } = style.makeXStyleElements();
5 |
6 | const fontId = styles.addFont(xFont);
7 | const fillId = styles.addFill(xFill);
8 |
9 | // HACK - adding light grey fill, as in OO and Google
10 | const greyfill = new Xfill({
11 | patternFill: new XpatternFill({ patternType: 'lightGray' })
12 | });
13 | styles.addFill(greyfill);
14 |
15 | const borderId = styles.addBorder(xBorder);
16 | xXf.fontId = fontId;
17 | xXf.fillId = fillId;
18 | xXf.borderId = borderId;
19 | xXf.numFmtId = numFmtId;
20 | // apply the numFmtId when it is not the default cellxf
21 | if (xXf.numFmtId > 0) {
22 | xXf.applyNumberFormat = true;
23 | }
24 |
25 | xXf.alignment.horizontal = style.align.h;
26 | xXf.alignment.indent = style.align.indent;
27 | xXf.alignment.shrinkToFit = style.align.shrinkToFit;
28 | xXf.alignment.textRotation = style.align.textRotation;
29 | xXf.alignment.vertical = style.align.v;
30 | xXf.alignment.wrapText = style.align.wrapText;
31 |
32 | return styles.addCellXf(xXf);
33 | }
34 |
35 | export function handleNumFmtId (numFmtId, styles) {
36 | const xf = new Xxf({ numFmtId });
37 | if (numFmtId > 0) {
38 | xf.applyNumberFormat = true;
39 | }
40 | return styles.addCellXf(xf);
41 | }
42 |
43 | /**
44 | * Style class for set Cell styles.
45 | */
46 | export class Style {
47 | applyBorder = false;
48 | applyFill = false;
49 | applyFont = false;
50 | applyAlignment = false;
51 | namedStyleIndex = null;
52 |
53 | constructor () {
54 | /**
55 | * Cell border
56 | * @type {Border}
57 | */
58 | this.border = new Border({});
59 | /**
60 | * Cell fill background or foreground
61 | * @type {Fill}
62 | */
63 | this.fill = new Fill({});
64 | /**
65 | * Cell font
66 | * @type {Font}
67 | */
68 | this.font = new Font({});
69 | /**
70 | * Cell alignment
71 | * @type {Alignment}
72 | */
73 | this.align = new Alignment({});
74 | }
75 | makeXStyleElements () {
76 | const xFont = new Xfont({
77 | sz: this.font.size,
78 | name: this.font.name,
79 | family: this.font.family,
80 | charset: this.font.charset,
81 | color: this.font.color,
82 | b: this.font.bold,
83 | i: this.font.italic,
84 | u: this.font.underline
85 | });
86 | const xFill = new Xfill({
87 | patternFill: new XpatternFill({
88 | patternType: this.fill.patternType,
89 | fgColor: this.fill.fgColor,
90 | bgColor: this.fill.bgColor
91 | })
92 | });
93 | const xBorder = new Xborder({
94 | left: { style: this.border.left, color: this.border.leftColor },
95 | right: { style: this.border.right, color: this.border.rightColor },
96 | top: { style: this.border.top, color: this.border.topColor },
97 | bottom: { style: this.border.bottom, color: this.border.bottomColor }
98 | });
99 | const xXf = new Xxf({
100 | numFmtId: 0,
101 | applyBorder: this.applyBorder,
102 | applyFill: this.applyFill,
103 | applyFont: this.applyFont,
104 | applyAlignment: this.applyAlignment
105 | });
106 |
107 | xXf.alignment = new Xalignment({
108 | horizontal: this.align.h,
109 | indent: this.align.indent,
110 | shrinkToFit: this.align.shrinkToFit,
111 | textRotation: this.align.textRotation,
112 | vertical: this.align.v,
113 | wrapText: this.align.wrapText
114 | });
115 |
116 | if (this.namedStyleIndex !== null) {
117 | xXf.xfId = this.namedStyleIndex;
118 | }
119 |
120 | return { xFont, xFill, xBorder, xXf };
121 | }
122 | }
123 |
124 | /**
125 | * Border of the Style and border type have: `none`, `thin`, `medium`, `thick`, `dashed`, `dotted`, `double`
126 | *
127 | */
128 | export class Border {
129 | /**
130 | * left border color
131 | * @type {String}
132 | */
133 | leftColor = undefined;
134 | /**
135 | * right border color
136 | * @type {String}
137 | */
138 | rightColor = undefined;
139 | /**
140 | * top border color
141 | * @type {String}
142 | */
143 | topColor = undefined;
144 | /**
145 | * bottom border color
146 | * @type {String}
147 | */
148 | bottomColor = undefined;
149 |
150 | constructor ({ left = 'none', right = 'none', top = 'none', bottom = 'none' }) {
151 | /**
152 | * left border type
153 | * @type {String}
154 | */
155 | this.left = left;
156 | /**
157 | * right border type
158 | * @type {String}
159 | */
160 | this.right = right;
161 | /**
162 | * top border type
163 | * @type {String}
164 | */
165 | this.top = top;
166 | /**
167 | * bottom border type
168 | * @type {String}
169 | */
170 | this.bottom = bottom;
171 | }
172 | }
173 | /**
174 | * Fill of the Style
175 | */
176 | export class Fill {
177 | constructor ({ patternType = 'none', fgColor = 'FFFFFFFF', bgColor = '00000000' }) {
178 | /**
179 | * pattern type of the fill
180 | * @type {String}
181 | */
182 | this.patternType = patternType;
183 | /**
184 | * foreground color of the fill
185 | * @type {String}
186 | */
187 | this.fgColor = fgColor;
188 | /**
189 | * background color of the fill
190 | * @type {String}
191 | */
192 | this.bgColor = bgColor;
193 | }
194 | }
195 | /**
196 | * Font of the Style
197 | */
198 | export class Font {
199 | family = 0;
200 | charset = 0;
201 | /**
202 | * font color
203 | * @type {String}
204 | */
205 | color = undefined;
206 | /**
207 | * Is bold style
208 | * @type {Boolean}
209 | */
210 | bold = false;
211 | /**
212 | * Is italic style
213 | * @type {Boolean}
214 | */
215 | italic = false;
216 | /**
217 | * IS underline style
218 | * @type {Boolean}
219 | */
220 | underline = false;
221 |
222 | constructor ({ size = 12, name = 'Verdana' }) {
223 | /**
224 | * font size [default 12]
225 | * @type {Number}
226 | */
227 | this.size = size;
228 | this.name = name;
229 | }
230 | }
231 | /**
232 | * Alignment of the Style.
233 | */
234 | export class Alignment {
235 | indent = 0;
236 | shrinkToFit = false;
237 | textRotation = 0;
238 | wrapText = false;
239 |
240 | constructor ({ h = 'general', v = 'bottom' }) {
241 | /**
242 | * Horizontal align: `general`, `center`, `left`, `right`
243 | * @type {String}
244 | */
245 | this.h = h;
246 | /**
247 | * Vertical align: `general`, `top`, `bottom`, `center`
248 | * @type {String}
249 | */
250 | this.v = v;
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/test/xmlStyle.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import * as style from '../src/xmlStyle';
5 |
6 | const { XstyleSheet, XnumFmts, Xfonts, Xfills, Xborders, XcellStyles, XcellStyleXfs, XcellXfs } = style;
7 | const { XnumFmt, Xfont, Xfill, XpatternFill, Xborder, XcellStyle, Xxf, Xalignment } = style;
8 |
9 | describe('Test: xmlSharedStrings.js', () => {
10 | it('should XstyleSheet render', () => {
11 | const styleSheet = new XstyleSheet({});
12 | const numFmts = new XnumFmts({ count: 0 });
13 | const fonts = new Xfonts({ count: 0 });
14 | const fills = new Xfills({ count: 0 });
15 | const borders = new Xborders({ count: 0 });
16 | const cellStyles = new XcellStyles({ count: 0 });
17 | const cellStyleXfs = new XcellStyleXfs({ count: 0 });
18 | const cellXfs = new XcellXfs({ count: 0 });
19 |
20 | styleSheet.fonts = fonts;
21 | styleSheet.fills = fills;
22 | styleSheet.borders = borders;
23 | styleSheet.cellStyles = cellStyles;
24 | styleSheet.cellStyleXfs = cellStyleXfs;
25 | styleSheet.cellXfs = cellXfs;
26 | styleSheet.numFmts = numFmts;
27 | expect(styleSheet.render()).to.equal('');
28 |
29 | // numFmts
30 | numFmts.children = [
31 | new XnumFmt({ formatCode: 'General', numFmtId: 0 })
32 | ];
33 | numFmts.count = 1;
34 | expect(numFmts.render()).to.equal('');
35 | // fonts
36 | fonts.children = [
37 | new Xfont({ sz: 1, color: 'ff000000', name: 'Avenir Next', b: true, i: true }),
38 | new Xfont({ sz: 2, color: 'ff000000', name: 'Avenir Next', b: true, u: true }),
39 | new Xfont({ sz: 3, color: 'ff000000', name: 'Avenir Next', u: true, i: true }),
40 | new Xfont({ sz: 3, color: 'ff000000', name: 'Avenir Next', family: '0' }),
41 | new Xfont({ sz: 3, color: 'ff000000', name: 'Avenir Next', charset: '0' })
42 | ];
43 | fonts.count = 3;
44 | expect(fonts.render()).to.equal('');
45 | // fills
46 | fills.children = [
47 | new Xfill({ patternFill: new XpatternFill({ patternType: 'solid', fgColor: 'ff000000', bgColor: 'ff000000' }) })
48 | ];
49 | fills.count = 1;
50 | expect(fills.render()).to.equal('');
51 | // borders
52 | borders.children = [
53 | new Xborder({
54 | left: { style: 'thin', color: 'ff000000' },
55 | right: { style: 'thin', color: 'ffffffff' }
56 | }),
57 | new Xborder({
58 | top: { style: 'thin', color: 'ff000000' },
59 | bottom: { style: 'thin', color: 'ffffffff' }
60 | })
61 | ];
62 | borders.count = 2;
63 | expect(borders.render()).to.equal('');
64 | // cellStyles
65 | cellStyles.children = [
66 | new XcellStyle({ builtinId: 0, name: 'Normal', xfId: 0 })
67 | ];
68 | cellStyles.count = 1;
69 | expect(cellStyles.render()).to.equal('');
70 | // xfs
71 | const xf = new Xxf({
72 | applyAlignment: 1,
73 | applyBorder: 1,
74 | applyFill: 0,
75 | applyFont: 1,
76 | applyNumberFormat: 1,
77 | applyProtection: 0,
78 | borderId: 7,
79 | fontId: 0,
80 | numFmtId: 0
81 | });
82 | xf.alignment = new Xalignment({
83 | vertical: 'top',
84 | wrapText: 1
85 | });
86 | // cellStyleXfs
87 | cellStyleXfs.children = [xf];
88 | cellStyleXfs.count = 1;
89 | expect(cellStyleXfs.render()).to.equal('');
90 | // cellXfs
91 | cellXfs.children = [xf];
92 | cellXfs.count = 1;
93 | expect(cellXfs.render()).to.equal('');
94 | });
95 |
96 | it('should XstyleSheet add children', () => {
97 | const styleSheet = new XstyleSheet({});
98 | styleSheet.reset();
99 | expect(styleSheet.render()).to.equal('');
100 |
101 | const font1 = new Xfont({ sz: 3, color: 'ff000000', name: 'Avenir Next', family: '0' });
102 | const font2 = new Xfont({ sz: 3, color: 'ff000000', name: 'Avenir Next', family: '0', b: true });
103 | expect(styleSheet.addFont(font1)).to.equal(0);
104 | expect(styleSheet.addFont(font2)).to.equal(1);
105 | expect(styleSheet.addFont(font1)).to.equal(0);
106 | expect(styleSheet.addFont(font2)).to.equal(1);
107 |
108 | const fill1 = new Xfill({ patternFill: new XpatternFill({ patternType: 'solid', fgColor: 'ff000000', bgColor: 'ff000000' }) });
109 | const fill2 = new Xfill({ patternFill: new XpatternFill({ patternType: 'solid', fgColor: 'ffffffff', bgColor: 'ff000000' }) });
110 | expect(styleSheet.addFill(fill1)).to.equal(0);
111 | expect(styleSheet.addFill(fill2)).to.equal(1);
112 | expect(styleSheet.addFill(fill1)).to.equal(0);
113 | expect(styleSheet.addFill(fill2)).to.equal(1);
114 |
115 | const border1 = new Xborder({
116 | left: { style: 'thin', color: 'ff000000' },
117 | right: { style: 'thin', color: 'ffffffff' }
118 | });
119 | const border2 = new Xborder({
120 | left: { style: 'thin', color: 'ff000000' },
121 | top: { style: 'thin', color: 'ff000000' },
122 | bottom: { style: 'thin', color: 'ffffffff' }
123 | });
124 | expect(styleSheet.addBorder(border1)).to.equal(1);
125 | expect(styleSheet.addBorder(border2)).to.equal(2);
126 | expect(styleSheet.addBorder(border1)).to.equal(1);
127 | expect(styleSheet.addBorder(border2)).to.equal(2);
128 |
129 | const xf1 = new Xxf({
130 | applyAlignment: 1,
131 | applyBorder: 1,
132 | applyFill: 0,
133 | applyFont: 1,
134 | applyNumberFormat: 1,
135 | applyProtection: 0,
136 | borderId: 7,
137 | fontId: 0,
138 | numFmtId: 0
139 | });
140 | xf1.alignment = new Xalignment({
141 | vertical: 'top',
142 | wrapText: 1
143 | });
144 | const xf2 = new Xxf({
145 | applyAlignment: 1,
146 | applyBorder: 1,
147 | applyFill: 0,
148 | applyFont: 1,
149 | applyNumberFormat: 1,
150 | applyProtection: 0,
151 | borderId: 7,
152 | fontId: 0,
153 | numFmtId: 0
154 | });
155 | xf2.alignment = new Xalignment({
156 | vertical: 'bottom',
157 | wrapText: 1
158 | });
159 | expect(styleSheet.addCellXf(xf1)).to.equal(1);
160 | expect(styleSheet.addCellXf(xf2)).to.equal(2);
161 | expect(styleSheet.addCellXf(xf1)).to.equal(1);
162 | expect(styleSheet.addCellXf(xf2)).to.equal(2);
163 |
164 | expect(styleSheet.newNumFmt().numFmtId).to.equal(0);
165 | expect(styleSheet.newNumFmt('@').numFmtId).to.equal('49');
166 | expect(styleSheet.newNumFmt('test').numFmtId).to.equal(164);
167 | });
168 | });
169 |
--------------------------------------------------------------------------------
/src/xmlStyle.js:
--------------------------------------------------------------------------------
1 | import { props, Node, HEAD } from './node';
2 | import { NumFmtInv, NumFmtsCount } from './lib';
3 |
4 | export
5 | @props('xmlns')
6 | class XstyleSheet extends Node {
7 | fonts = null;
8 | fills = null;
9 | borders = null;
10 | cellStyles = null;
11 | cellStyleXfs = null;
12 | cellXfs = null;
13 | numFmts = null;
14 | numFmtRefTable = {};
15 |
16 | constructor ({ xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }, children) {
17 | super({ xmlns }, children);
18 | this[HEAD] = '';
19 | }
20 | render () {
21 | this.children = [];
22 | if (this.numFmts) this.children.push(this.numFmts);
23 | if (this.fonts) this.children.push(this.fonts);
24 | if (this.fills) this.children.push(this.fills);
25 | if (this.borders) this.children.push(this.borders);
26 | if (this.cellStyleXfs) this.children.push(this.cellStyleXfs);
27 | if (this.cellXfs) this.children.push(this.cellXfs);
28 | if (this.cellStyles) this.children.push(this.cellStyles);
29 | return super.render();
30 | }
31 | reset () {
32 | this.children = [];
33 | this.fonts = new Xfonts();
34 | this.fills = new Xfills();
35 | this.borders = new Xborders();
36 | this.cellXfs = new XcellXfs({ count: 1 }, [new Xxf()]);
37 | this.numFmts = new XnumFmts();
38 | this.addBorder(new Xborder({
39 | left: { style: 'none' },
40 | right: { style: 'none' },
41 | top: { style: 'none' },
42 | bottom: { style: 'none' }
43 | }));
44 | }
45 | addFont (xFont) {
46 | if (!xFont.name) return 0;
47 | const list = this.fonts.children;
48 | const len = list.length;
49 | for (let i = 0; i < list.length; i++) {
50 | if (xFont.equals(list[i])) return i;
51 | }
52 | list.push(xFont);
53 | this.fonts.count = list.length;
54 | return len;
55 | }
56 | addFill (xFill) {
57 | const list = this.fills.children;
58 | const len = list.length;
59 | for (let i = 0; i < list.length; i++) {
60 | if (xFill.equals(list[i])) return i;
61 | }
62 | list.push(xFill);
63 | this.fills.count = list.length;
64 | return len;
65 | }
66 | addBorder (xBorder) {
67 | const list = this.borders.children;
68 | const len = list.length;
69 | for (let i = 0; i < list.length; i++) {
70 | if (xBorder.equals(list[i])) return i;
71 | }
72 | list.push(xBorder);
73 | this.borders.count = list.length;
74 | return len;
75 | }
76 | addCellXf (xXf) {
77 | const list = this.cellXfs.children;
78 | const len = list.length;
79 | for (let i = 0; i < list.length; i++) {
80 | if (xXf.equals(list[i])) return i;
81 | }
82 | list.push(xXf);
83 | this.cellXfs.count = list.length;
84 | return len;
85 | }
86 | addNumFmt (xNumFmt) {
87 | if (xNumFmt.numFmtId <= NumFmtsCount) return;
88 | if (this.numFmtRefTable[xNumFmt.numFmtId] === undefined) {
89 | this.numFmts.children.push(xNumFmt);
90 | this.numFmts.count = this.numFmts.children.length;
91 | this.numFmtRefTable[xNumFmt.numFmtId] = xNumFmt;
92 | }
93 | }
94 | newNumFmt (formatCode) {
95 | if (!formatCode) return new XnumFmt({ numFmtId: 0, formatCode: 'general' });
96 | let numFmtId = NumFmtInv[formatCode];
97 | if (numFmtId !== undefined) {
98 | return new XnumFmt({ numFmtId, formatCode });
99 | }
100 | for (const numFmt of this.numFmts.children) {
101 | if (formatCode === numFmt.formatCode) return numFmt;
102 | }
103 |
104 | numFmtId = NumFmtsCount + 1;
105 | do {
106 | if (this.numFmtRefTable[numFmtId]) {
107 | numFmtId++;
108 | } else {
109 | this.addNumFmt(new XnumFmt({ numFmtId, formatCode }));
110 | break;
111 | }
112 | } while (1);
113 | return new XnumFmt({ numFmtId, formatCode });
114 | }
115 | }
116 |
117 | export @props('count')
118 | class XnumFmts extends Node {
119 | render () {
120 | if (this.count) return super.render();
121 | return '';
122 | }
123 | }
124 |
125 | export @props('numFmtId', 'formatCode')
126 | class XnumFmt extends Node {}
127 |
128 | export @props('count')
129 | class Xfonts extends Node {
130 | render () {
131 | if (this.count) return super.render();
132 | return '';
133 | }
134 | }
135 |
136 | export @props('sz', 'name', 'family', 'charset', 'color', 'b', 'i', 'u')
137 | class Xfont extends Node {
138 | render () {
139 | let str = '';
140 | if (this.sz) str += ``;
141 | if (this.name) str += ``;
142 | if (this.family) str += ``;
143 | if (this.charset) str += ``;
144 | if (this.color) str += ``;
145 | if (this.b) str += '';
146 | if (this.i) str += '';
147 | if (this.u) str += '';
148 | return str + '';
149 | }
150 | equals (o) {
151 | return this.sz === o.sz &&
152 | this.name === o.name &&
153 | this.family === o.family &&
154 | this.charset === o.charset &&
155 | this.color === o.color &&
156 | this.b === o.b &&
157 | this.i === o.i &&
158 | this.u === o.u;
159 | }
160 | }
161 |
162 | export @props('count')
163 | class Xfills extends Node {
164 | render () {
165 | if (this.count) return super.render();
166 | return '';
167 | }
168 | }
169 |
170 | export @props('patternFill')
171 | class Xfill extends Node {
172 | render () {
173 | return `${this.patternFill.render()}`;
174 | }
175 | equals (o) {
176 | const pf1 = this.patternFill;
177 | const pf2 = o.patternFill;
178 | if (pf1 && pf2) {
179 | return pf1.patternType === pf2.patternType &&
180 | pf1.fgColor === pf2.fgColor &&
181 | pf1.bgColor === pf2.bgColor;
182 | }
183 | return !pf1 && !pf2;
184 | }
185 | }
186 |
187 | export @props('patternType', 'fgColor', 'bgColor')
188 | class XpatternFill extends Node {
189 | render () {
190 | let str = ``;
191 | if (this.fgColor) str += ``;
192 | if (this.bgColor) str += ``;
193 | return str + '';
194 | }
195 | }
196 |
197 | export @props('count')
198 | class Xborders extends Node {
199 | render () {
200 | if (this.count) return super.render();
201 | return '';
202 | }
203 | }
204 |
205 | export @props('left', 'right', 'top', 'bottom')
206 | class Xborder extends Node {
207 | _renderLine (pos) {
208 | const posVal = this[pos];
209 | if (!posVal) return '';
210 |
211 | let str = `<${pos} style="${posVal.style}">`;
212 | if (posVal.color) str += ``;
213 | return str + `${pos}>`;
214 | }
215 | render () {
216 | let str = '';
217 | str += this._renderLine('left');
218 | str += this._renderLine('right');
219 | str += this._renderLine('top');
220 | str += this._renderLine('bottom');
221 | return str + '';
222 | }
223 | equals (o) {
224 | const check = (a, b) => {
225 | if (a && b) {
226 | return a.style === b.style && a.color === b.color;
227 | }
228 | return !a && !b;
229 | };
230 | return check(this.left, o.left) &&
231 | check(this.right, o.right) &&
232 | check(this.top, o.top) &&
233 | check(this.bottom, o.bottom);
234 | }
235 | }
236 |
237 | export @props('count')
238 | class XcellStyles extends Node {
239 | render () {
240 | if (this.count) return super.render();
241 | return '';
242 | }
243 | }
244 |
245 | export @props('builtInId', 'customBuiltIn', 'hidden', 'iLevel', 'name', 'xfId')
246 | class XcellStyle extends Node {}
247 |
248 | export @props('count')
249 | class XcellStyleXfs extends Node {
250 | render () {
251 | if (this.count) return super.render();
252 | return '';
253 | }
254 | }
255 |
256 | export @props('count')
257 | class XcellXfs extends Node {
258 | render () {
259 | if (this.count) return super.render();
260 | return '';
261 | }
262 | }
263 |
264 | export @props('applyAlignment', 'applyBorder', 'applyFont', 'applyFill', 'applyNumberFormat', 'applyProtection', 'borderId', 'fillId', 'fontId', 'numFmtId', 'xfId')
265 | class Xxf extends Node {
266 | constructor (attrs, children) {
267 | const defaults = {
268 | applyAlignment: false,
269 | applyBorder: false,
270 | applyFont: false,
271 | applyFill: false,
272 | applyNumberFormat: false,
273 | applyProtection: false,
274 | borderId: 0,
275 | fillId: 0,
276 | fontId: 0,
277 | numFmtId: 0
278 | };
279 | super({ ...defaults, ...attrs }, children);
280 | this.alignment = new Xalignment();
281 | }
282 | render () {
283 | if (this.alignment) {
284 | this.children = [this.alignment];
285 | }
286 | return super.render();
287 | }
288 | equals (o) {
289 | return this.applyAlignment === o.applyAlignment &&
290 | this.applyBorder === o.applyBorder &&
291 | this.applyFont === o.applyFont &&
292 | this.applyFill === o.applyFill &&
293 | this.applyProtection === o.applyProtection &&
294 | this.borderId === o.borderId &&
295 | this.fillId === o.fillId &&
296 | this.fontId === o.fontId &&
297 | this.numFmtId === o.numFmtId &&
298 | this.xfId === o.xfId &&
299 | this.alignment.equals(o.alignment);
300 | }
301 | }
302 |
303 | export @props('horizontal', 'indent', 'shrinkToFit', 'textRotation', 'vertical', 'wrapText')
304 | class Xalignment extends Node {
305 | constructor (attrs, children = []) {
306 | const defaults = {
307 | horizontal: 'general',
308 | indent: 0,
309 | shrinkToFit: false,
310 | textRotation: 0,
311 | vertical: 'bottom',
312 | wrapText: false
313 | };
314 | super({ ...defaults, ...attrs }, children);
315 | }
316 | equals (o) {
317 | return this.horizontal === o.horizontal &&
318 | this.indent === o.indent &&
319 | this.shrinkToFit === o.shrinkToFit &&
320 | this.textRotation === o.textRotation &&
321 | this.vertical === o.vertical &&
322 | this.wrapText === o.wrapText;
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/test/file.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { expect } from 'chai';
4 | import fs from 'fs';
5 | import { tmpdir } from 'os';
6 | import { join } from 'path';
7 | import streamEqual from 'stream-equal';
8 | import { File } from '../src/file';
9 | import { Style } from '../src/style';
10 | import Zip from 'jszip';
11 |
12 | const DATE = new Date(Date.UTC(2016, 10, 23, 0, 0, 0));
13 | Zip.defaults.date = DATE;
14 |
15 | function border (cell, top, left, bottom, right) {
16 | const light = 'ffded9d4';
17 | const dark = 'ff7e6a54';
18 | cell.style.border.top = 'thin';
19 | cell.style.border.topColor = top ? dark : light;
20 | cell.style.border.left = 'thin';
21 | cell.style.border.leftColor = left ? dark : light;
22 | cell.style.border.bottom = 'thin';
23 | cell.style.border.bottomColor = bottom ? dark : light;
24 | cell.style.border.right = 'thin';
25 | cell.style.border.rightColor = right ? dark : light;
26 | }
27 |
28 | function fill (cell, type) {
29 | type = type || 0;
30 | const colors = ['ffffffff', 'ffa2917d', 'ffe4e2de', 'fffff8df', 'fff1eeec'];
31 | // 1: header, 2: first col, 3: second col, 4: gray, 0: white
32 | cell.style.fill.patternType = 'solid';
33 | cell.style.fill.fgColor = colors[type];
34 | cell.style.fill.bgColor = 'ffffffff';
35 | }
36 |
37 | describe('Test: file.js', () => {
38 | it('should saveAs simple excel file', (done) => {
39 | const file = new File();
40 | const sheet = file.addSheet('hello');
41 | const row = sheet.addRow();
42 |
43 | expect(sheet.row(0) === row).to.be.true;
44 |
45 | const cell = row.addCell();
46 | cell.value = 'I am a cell!';
47 | cell.hMerge = 2;
48 | cell.vMerge = 2;
49 |
50 | const style = new Style();
51 | style.fill.patternType = 'solid';
52 | style.fill.fgColor = '00FF0000';
53 | style.fill.bgColor = 'FF000000';
54 | style.align.h = 'center';
55 | style.align.v = 'center';
56 |
57 | cell.style = style;
58 | border(cell, 1, 1, 1, 1);
59 |
60 | const tmpfile = join(tmpdir(), 'simple.xlsx');
61 | const expfile = join(__dirname, 'expect/simple.xlsx');
62 | file
63 | .saveAs()
64 | .pipe(fs.createWriteStream(tmpfile))
65 | .on('finish', () => {
66 | const expectFile = fs.createReadStream(expfile);
67 | const actualFile = fs.createReadStream(tmpfile);
68 | streamEqual(expectFile, actualFile).then(ok => {
69 | expect(ok).to.be.true;
70 | done();
71 | });
72 | });
73 | });
74 | it('should saveAs simple excel file with colWidth', (done) => {
75 | const file = new File();
76 | const sheet = file.addSheet('sheet1');
77 | sheet.setColWidth(0, 1, 20);
78 |
79 | const row = sheet.addRow();
80 | const cell = row.addCell();
81 | cell.value = 'I am a cell!';
82 |
83 | const tmpfile = join(tmpdir(), 'simple2.xlsx');
84 | const expfile = join(__dirname, 'expect/simple2.xlsx');
85 | file
86 | .saveAs()
87 | .pipe(fs.createWriteStream(tmpfile))
88 | .on('finish', () => {
89 | const expectFile = fs.createReadStream(expfile);
90 | const actualFile = fs.createReadStream(tmpfile);
91 | streamEqual(expectFile, actualFile).then(ok => {
92 | expect(ok).to.be.true;
93 | done();
94 | });
95 | });
96 | });
97 | it('should saveAs simple excel file with multi types', (done) => {
98 | const file = new File();
99 | const sheet = file.addSheet('sheet1');
100 | sheet.setColWidth(0, 5, 20);
101 |
102 | const data1 = [null, DATE, 123, 'abc', true, [1, 2, 3]];
103 | const data2 = [undefined, DATE, 123.456, 'abc', false, { foo: 'bar' }];
104 | const data3 = ['0.00e+00', '#,##0', '#,##0.00', '0%', '0.00%', '$#,##0.00'];
105 |
106 | const row1 = sheet.addRow();
107 | for (const v of data1) {
108 | const cell = row1.addCell();
109 | cell.value = v;
110 | }
111 | const row2 = sheet.addRow();
112 | for (const v of data2) {
113 | const cell = row2.addCell();
114 | cell.value = v;
115 | }
116 | const row3 = sheet.addRow();
117 | for (const v of data3) {
118 | const cell = row3.addCell();
119 | cell.value = 12345678.87654321;
120 | cell.numFmt = v;
121 | }
122 | const row4 = sheet.addRow();
123 | const c1 = row4.addCell();
124 | c1.setDate(DATE);
125 | const c2 = row4.addCell();
126 | c2.setFormula('SUM(C1:C3)');
127 |
128 | const tmpfile = join(tmpdir(), 'simple3.xlsx');
129 | const expfile = join(__dirname, 'expect/simple3.xlsx');
130 | file
131 | .saveAs()
132 | .pipe(fs.createWriteStream(tmpfile))
133 | .on('finish', () => {
134 | const expectFile = fs.createReadStream(expfile);
135 | const actualFile = fs.createReadStream(tmpfile);
136 | streamEqual(expectFile, actualFile).then(ok => {
137 | expect(ok).to.be.true;
138 | done();
139 | });
140 | });
141 | });
142 | it('should saveAs simple excel file with col style', (done) => {
143 | const file = new File();
144 | const sheet = file.addSheet('sheet1');
145 |
146 | for (let i = 10000; i <= 20000; i += 2000) {
147 | const row = sheet.addRow();
148 | const cell = row.addCell();
149 | cell.value = i;
150 | }
151 |
152 | sheet.col(0).setType(3);
153 |
154 | const tmpfile = join(tmpdir(), 'simple4.xlsx');
155 | const expfile = join(__dirname, 'expect/simple4.xlsx');
156 | file
157 | .saveAs()
158 | .pipe(fs.createWriteStream(tmpfile))
159 | .on('finish', () => {
160 | const expectFile = fs.createReadStream(expfile);
161 | const actualFile = fs.createReadStream(tmpfile);
162 | streamEqual(expectFile, actualFile).then(ok => {
163 | expect(ok).to.be.true;
164 | done();
165 | });
166 | });
167 | });
168 | it('should saveAs simple excel file with page setup', (done) => {
169 | const file = new File();
170 | const sheet = file.addSheet('hello');
171 | sheet.afterMake = (st) => {
172 | st.sheetPr.children.forEach(v => {
173 | v.fitToPage = true;
174 | });
175 | st.pageSetup.fitToHeight = 3;
176 | st.pageSetup.orientation = 'landscape';
177 | st.pageSetup.usePrinterDefaults = true;
178 | st.headerFooter.oddHeader = '';
179 | };
180 | for (let i = 0; i < 100; i++) {
181 | for (let j = 0; j < 50; j++) {
182 | const cell = sheet.cell(i, j);
183 | cell.value = `${i}-${j}`;
184 | }
185 | }
186 |
187 | const tmpfile = join(tmpdir(), 'simple5.xlsx');
188 | const expfile = join(__dirname, 'expect/simple5.xlsx');
189 | file
190 | .saveAs()
191 | .pipe(fs.createWriteStream(tmpfile))
192 | .on('finish', () => {
193 | const expectFile = fs.createReadStream(expfile);
194 | const actualFile = fs.createReadStream(tmpfile);
195 | streamEqual(expectFile, actualFile).then(ok => {
196 | expect(ok).to.be.true;
197 | done();
198 | });
199 | });
200 | });
201 | it('should saveAs complex excel file', (done) => {
202 | const file = new File();
203 | const sheet = file.addSheet('Sheet1');
204 | const data = [
205 | ['Auto', 200, 90, 'B2-C2'],
206 | ['Entertainment', 200, 32, 'B3-C3'],
207 | ['Food', 350, 205.75, 'B4-C4'],
208 | ['Home', 300, 250, 'B5-C5'],
209 | ['Medical', 100, 35, 'B6-C6'],
210 | ['Personal Items', 300, 80, 'B7-C7'],
211 | ['Travel', 500, 350, 'B8-C8'],
212 | ['Utilities', 200, 100, 'B9-C9'],
213 | ['Other', 50, 60, 'B10-C10']
214 | ];
215 |
216 | const header = sheet.addRow();
217 | header.setHeightCM(0.8);
218 | const headers = ['Category', 'Budget', 'Actual', 'Difference'];
219 | for (let i = 0; i < headers.length; i++) {
220 | const hc = header.addCell();
221 | hc.value = headers[i];
222 | hc.style.align.v = 'center';
223 | if (i > 0) hc.style.align.h = 'right';
224 | hc.style.font.color = 'ffffffff';
225 | border(hc, 0, 0, 1, 0);
226 | fill(hc, 1);
227 | }
228 |
229 | const len = data.length;
230 | for (let i = 0; i < len; i++) {
231 | const line = data[i];
232 | const row = sheet.addRow();
233 | row.setHeightCM(0.8);
234 | // Col 1
235 | const cell1 = row.addCell();
236 | cell1.value = line[0];
237 | cell1.style.align.v = 'center';
238 | if (i === 0) {
239 | border(cell1, 1, 0, 0, 1);
240 | } else if (i === len - 1) {
241 | border(cell1, 0, 0, 1, 1);
242 | } else {
243 | border(cell1, 0, 0, 0, 1);
244 | }
245 | fill(cell1, 2);
246 | // Col 2
247 | const cell2 = row.addCell();
248 | cell2.value = line[1];
249 | cell2.numFmt = '$#,##0.00';
250 | cell2.cellType = 'TypeNumeric';
251 | cell2.style.align.v = 'center';
252 | if (i === 0) {
253 | border(cell2, 1, 1, 0, 0);
254 | } else if (i === len - 1) {
255 | border(cell2, 0, 1, 1, 0);
256 | } else {
257 | border(cell2, 0, 1, 0, 0);
258 | }
259 | fill(cell2, 3);
260 | // Col 3
261 | const cell3 = row.addCell();
262 | cell3.value = line[2];
263 | cell3.numFmt = '$#,##0.00';
264 | cell3.cellType = 'TypeNumeric';
265 | cell3.style.align.v = 'center';
266 | if (i === 0) {
267 | border(cell3, 1, 0, 0, 0);
268 | } else if (i === len - 1) {
269 | border(cell3, 0, 0, 1, 0);
270 | } else {
271 | border(cell3, 0, 0, 0, 0);
272 | }
273 | fill(cell3, i % 2 === 0 ? 0 : 4);
274 | // Col 4
275 | const cell4 = row.addCell();
276 | cell4.formula = line[3];
277 | cell4.numFmt = '$#,##0.00';
278 | cell4.cellType = 'TypeFormula';
279 | cell4.style.align.v = 'center';
280 | if (i === 0) {
281 | border(cell4, 1, 0, 0, 0);
282 | } else if (i === len - 1) {
283 | border(cell4, 0, 0, 1, 0);
284 | } else {
285 | border(cell4, 0, 0, 0, 0);
286 | }
287 | fill(cell4, i % 2 === 0 ? 0 : 4);
288 | }
289 |
290 | for (let i = 0; i < 4; i++) {
291 | sheet.col(i).width = 20;
292 | }
293 |
294 | const tmpfile = join(tmpdir(), 'complex.xlsx');
295 | const expfile = join(__dirname, 'expect/complex.xlsx');
296 | file
297 | .saveAs()
298 | .pipe(fs.createWriteStream(tmpfile))
299 | .on('finish', () => {
300 | const expectFile = fs.createReadStream(expfile);
301 | const actualFile = fs.createReadStream(tmpfile);
302 | streamEqual(expectFile, actualFile).then(ok => {
303 | expect(ok).to.be.true;
304 | done();
305 | });
306 | });
307 | });
308 | });
309 |
--------------------------------------------------------------------------------
/src/sheet.js:
--------------------------------------------------------------------------------
1 | import { Row } from './row';
2 | import { Col } from './col';
3 | import { handleStyle, handleNumFmtId, Border } from './style';
4 | import { num2col } from './lib';
5 | import { makeXworksheet, XsheetData, Xpane, Xcols, Xcol, Xrow, Xdimension, Xc, Xf, XmergeCells, XmergeCell } from './xmlWorksheet';
6 |
7 | /**
8 | * Sheet of the xlsx file.
9 | * ```js
10 | * import { File } from 'better-xlsx';
11 | * const file = new File();
12 | * const sheet = file.addSheet('Sheet-1');
13 | * const row = sheet.addRow();
14 | * const cell = row.addCell();
15 | * ```
16 | */
17 | export class Sheet {
18 | rows = [];
19 | cols = [];
20 | maxRow = 0;
21 | maxCol = 0;
22 | hidden = false;
23 | sheetViews = [];
24 | sheetFormat = {
25 | defaultColWidth: 0,
26 | defaultRowHeight: 0,
27 | outlineLevelCol: 0,
28 | outlineLevelRow: 0
29 | };
30 | constructor ({ name, file, selected }) {
31 | this.name = name;
32 | this.file = file;
33 | this.selected = selected;
34 | }
35 | /**
36 | * Create a Row and add it into the Sheet.
37 | * @return {Row}
38 | */
39 | addRow () {
40 | const row = new Row({ sheet: this });
41 | this.rows.push(row);
42 | if (this.rows.length > this.maxRow) {
43 | this.maxRow = this.rows.length;
44 | }
45 | return row;
46 | }
47 | maybeAddCol (cellCount) {
48 | if (cellCount > this.maxCol) {
49 | const col = new Col({
50 | min: cellCount,
51 | max: cellCount,
52 | hidden: false,
53 | collapsed: false
54 | });
55 | this.cols.push(col);
56 | this.maxCol = cellCount;
57 | }
58 | }
59 | /**
60 | * Get Col of the sheet with index and create cols when `index > maxCol`.
61 | * @param {Number} idx Index of the Col [from 0].
62 | * @return {Col}
63 | */
64 | col (idx) {
65 | this.maybeAddCol(idx + 1);
66 | return this.cols[idx];
67 | }
68 | /**
69 | * Get Row of the sheet with index and create rows when `index > maxRow`.
70 | * @param {Number} idx Index of the Row [from 0].
71 | * @return {Row}
72 | */
73 | row (idx) {
74 | for (let len = this.rows.length; len <= idx; len++) {
75 | this.addRow();
76 | }
77 | return this.rows[idx];
78 | }
79 | /**
80 | * Get Cell of the sheet with `(row, col)` and create cell when out of range.
81 | * @param {Number} row
82 | * @param {Number} col
83 | * @return {Cell}
84 | */
85 | cell (row, col) {
86 | for (let len = this.rows.length; len <= row; len++) {
87 | this.addRow();
88 | }
89 | const r = this.rows[row];
90 | for (let len = r.cells.length; len <= col; len++) {
91 | r.addCell();
92 | }
93 | return r.cells[col];
94 | }
95 | /**
96 | * Set columns width from `startcol` to `endcol`.
97 | * @param {Number} startcol
98 | * @param {Number} endcol
99 | * @param {Number} width
100 | */
101 | setColWidth (startcol, endcol, width) {
102 | if (startcol > endcol) {
103 | throw new Error(`Could not set width for range ${startcol}-${endcol}: startcol must be less than endcol.`);
104 | }
105 | const col = new Col({
106 | min: startcol + 1,
107 | max: endcol + 1,
108 | hidden: false,
109 | collapsed: false,
110 | width: width
111 | });
112 | this.cols.push(col);
113 | if (endcol + 1 > this.maxCol) {
114 | this.maxCol = endcol + 1;
115 | }
116 | }
117 | handleMerged () {
118 | const merged = [];
119 | for (let r = 0; r < this.rows.length; r++) {
120 | const row = this.rows[r];
121 | for (let c = 0; c < row.cells.length; c++) {
122 | const cell = row.cells[c];
123 | if (cell.hMerge > 0 || cell.vMerge > 0) {
124 | merged.push({ r, c, cell });
125 | }
126 | }
127 | }
128 | for (const { r, c, cell } of merged) {
129 | const { border } = cell.style;
130 |
131 | cell.style.border = new Border({});
132 |
133 | for (let rownum = 0; rownum <= cell.vMerge; rownum++) {
134 | for (let colnum = 0; colnum <= cell.hMerge; colnum++) {
135 | const tmpcell = this.cell(r + rownum, c + colnum);
136 | const arr = [];
137 | if (rownum === 0) {
138 | arr.push('top');
139 | }
140 | if (rownum === cell.vMerge) {
141 | arr.push('bottom');
142 | }
143 | if (colnum === 0) {
144 | arr.push('left');
145 | }
146 | if (colnum === cell.hMerge) {
147 | arr.push('right');
148 | }
149 | if (arr.length) {
150 | tmpcell.style.applyBorder = true;
151 | arr.forEach(k => {
152 | const ck = `${k}Color`;
153 | tmpcell.style.border[k] = border[k];
154 | tmpcell.style.border[ck] = border[ck];
155 | });
156 | }
157 | }
158 | }
159 | }
160 | }
161 | makeXSheet (refTable, styles) {
162 | const sheet = makeXworksheet();
163 | const xSheet = new XsheetData();
164 | let maxRow = 0;
165 | let maxCell = 0;
166 | let maxLevelCol;
167 | let maxLevelRow;
168 |
169 | this.handleMerged();
170 |
171 | for (let i = 0; i < this.sheetViews.length; i++) {
172 | const view = this.sheetViews[i];
173 | if (view && view.pane) {
174 | sheet.sheetViews.children[i].children.push(new Xpane({
175 | xSplit: view.pane.xSplit,
176 | ySplit: view.pane.ySplit,
177 | topLeftCell: view.pane.topLeftCell,
178 | activePane: view.pane.activePane,
179 | state: view.pane.state
180 | }));
181 | }
182 | }
183 | if (this.selected) {
184 | sheet.sheetViews.children[0].tabSelected = true;
185 | }
186 | if (this.sheetFormat.defaultRowHeight !== 0) {
187 | sheet.sheetFormatPr.defaultRowHeight = this.sheetFormat.defaultRowHeight;
188 | }
189 | if (this.sheetFormat.defaultColWidth !== 0) {
190 | sheet.sheetFormatPr.defaultColWidth = this.sheetFormat.defaultColWidth;
191 | }
192 |
193 | const fIdList = [];
194 | sheet.cols = new Xcols();
195 | for (let c = 0; c < this.cols.length; c++) {
196 | const col = this.cols[c];
197 | col.min = col.min || 1;
198 | col.max = col.max || 1;
199 | const xNumFmt = styles.newNumFmt(col.numFmt);
200 | const fId = handleStyle(col.style, xNumFmt.numFmtId, styles);
201 |
202 | fIdList.push(fId);
203 |
204 | let customWidth = 0;
205 | if (col.width === 0) {
206 | col.width = 9.5;
207 | } else {
208 | customWidth = 1;
209 | }
210 | sheet.cols.children.push(new Xcol({
211 | min: col.min,
212 | max: col.max,
213 | hidden: col.hidden,
214 | width: col.width,
215 | customWidth: customWidth,
216 | collapsed: col.collapsed,
217 | outlineLevel: col.outlineLevel,
218 | style: fId
219 | }));
220 |
221 | if (col.outlineLevel > maxLevelCol) {
222 | maxLevelCol = col.outlineLevel;
223 | }
224 | }
225 | for (let r = 0; r < this.rows.length; r++) {
226 | const row = this.rows[r];
227 | if (r > maxRow) maxRow = r;
228 | const xRow = new Xrow({ r: r + 1 });
229 | if (row.isCustom) {
230 | xRow.customHeight = true;
231 | xRow.ht = row.height;
232 | }
233 | xRow.outlineLevel = row.outlineLevel;
234 | if (row.outlineLevel > maxLevelRow) {
235 | maxLevelRow = row.outlineLevel;
236 | }
237 | for (let c = 0; c < row.cells.length; c++) {
238 | let fId = fIdList[c];
239 | const cell = row.cells[c];
240 | const xNumFmt = styles.newNumFmt(cell.numFmt);
241 | const style = cell.style;
242 | if (style !== null) {
243 | fId = handleStyle(style, xNumFmt.numFmtId, styles);
244 | } else if (cell.numFmt && this.cols[c].numFmt !== cell.numFmt) {
245 | fId = handleNumFmtId(xNumFmt.NumFmtId, styles);
246 | }
247 |
248 | if (c > maxCell) maxCell = c;
249 |
250 | const xC = new Xc({ r: `${num2col(c)}${r + 1}` });
251 | switch (cell.cellType) {
252 | case 'TypeString':
253 | if (cell.value) {
254 | xC.v = refTable.addString(cell.value);
255 | }
256 | xC.t = 's';
257 | xC.s = fId;
258 | break;
259 | case 'TypeBool':
260 | xC.v = cell.value;
261 | xC.t = 'b';
262 | xC.s = fId;
263 | break;
264 | case 'TypeNumeric':
265 | xC.v = cell.value;
266 | xC.s = fId;
267 | break;
268 | case 'TypeDate':
269 | xC.v = cell.value;
270 | xC.s = fId;
271 | break;
272 | case 'TypeFormula':
273 | xC.v = cell.value;
274 | xC.f = new Xf({}, [cell.formula]);
275 | xC.s = fId;
276 | break;
277 | case 'TypeError':
278 | xC.v = cell.value;
279 | xC.f = new Xf({}, [cell.formula]);
280 | xC.t = 'e';
281 | xC.s = fId;
282 | break;
283 | case 'TypeGeneral':
284 | xC.v = cell.value;
285 | xC.s = fId;
286 | break;
287 | }
288 | xRow.children.push(xC);
289 | if (cell.hMerge > 0 || cell.vMerge > 0) {
290 | // r == rownum, c == colnum
291 | const start = `${num2col(c)}${r + 1}`;
292 | const endcol = c + cell.hMerge;
293 | const endrow = r + cell.vMerge + 1;
294 | const end = `${num2col(endcol)}${endrow}`;
295 | const mc = new XmergeCell({ ref: start + ':' + end });
296 | if (sheet.mergeCells === null) {
297 | sheet.mergeCells = new XmergeCells();
298 | }
299 | sheet.mergeCells.children.push(mc);
300 | }
301 | }
302 | xSheet.children.push(xRow);
303 | }
304 | // Update sheet format with the freshly determined max levels
305 | this.sheetFormat.outlineLevelCol = maxLevelCol;
306 | this.sheetFormat.outlineLevelRow = maxLevelRow;
307 | // .. and then also apply this to the xml worksheet
308 | sheet.sheetFormatPr.outlineLevelCol = this.sheetFormat.outlineLevelCol;
309 | sheet.sheetFormatPr.outlineLevelRow = this.sheetFormat.outlineLevelRow;
310 |
311 | if (sheet.mergeCells !== null) {
312 | sheet.mergeCells.count = sheet.mergeCells.children.length;
313 | }
314 |
315 | sheet.sheetData = xSheet;
316 |
317 | const dimension = new Xdimension({
318 | ref: `A1:${num2col(maxCell)}${maxRow + 1}`
319 | });
320 | if (dimension.ref === 'A1:A1') {
321 | dimension.ref = 'A1';
322 | }
323 | sheet.dimension = dimension;
324 | if (this.afterMake) {
325 | this.afterMake(sheet);
326 | }
327 | return sheet;
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/src/templates.js:
--------------------------------------------------------------------------------
1 | export const DOT_RELS = `
2 |
3 |
4 |
5 |
6 | `;
7 |
8 | export const DOCPROPS_APP = `
9 |
10 | 0
11 | JS XLSX
12 | `;
13 |
14 | export const DOCPROPS_CORE = `
15 | `;
16 |
17 | export const XL_THEME_THEME = `
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 | `;
335 |
--------------------------------------------------------------------------------