├── 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(``); 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('34SUM(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 | [![NPM version](https://img.shields.io/npm/v/better-xlsx.svg)](https://www.npmjs.com/package/better-xlsx) 7 | [![NPM downloads](https://img.shields.io/npm/dm/better-xlsx.svg)](https://www.npmjs.com/package/better-xlsx) 8 | [![Build Status](https://travis-ci.org/d-band/better-xlsx.svg?branch=master)](https://travis-ci.org/d-band/better-xlsx) 9 | [![Coverage Status](https://coveralls.io/repos/github/d-band/better-xlsx/badge.svg?branch=master)](https://coveralls.io/github/d-band/better-xlsx?branch=master) 10 | [![Dependency Status](https://david-dm.org/d-band/better-xlsx.svg)](https://david-dm.org/d-band/better-xlsx) 11 | [![Greenkeeper badge](https://badges.greenkeeper.io/d-band/better-xlsx.svg)](https://greenkeeper.io/) 12 | [![Backers on Open Collective](https://opencollective.com/better-xlsx/backers/badge.svg)](#backers) 13 | [![Sponsors on Open Collective](https://opencollective.com/better-xlsx/sponsors/badge.svg)](#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 + ``; 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 | --------------------------------------------------------------------------------