diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7afe557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.*.swp +node_modules +lib +tmp diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..8d07c37 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +.npmignore +Gruntfile.coffee +src +test diff --git a/Gruntfile.coffee b/Gruntfile.coffee new file mode 100644 index 0000000..1039b61 --- /dev/null +++ b/Gruntfile.coffee @@ -0,0 +1,54 @@ +module.exports = (grunt) -> + @loadNpmTasks('grunt-contrib-clean') + @loadNpmTasks('grunt-contrib-coffee') + @loadNpmTasks('grunt-contrib-watch') + @loadNpmTasks('grunt-mocha-cli') + @loadNpmTasks('grunt-mkdir') + + @initConfig + coffee: + all: + options: + bare: true + expand: true, + cwd: 'src', + src: ['*.coffee'], + dest: 'lib', + ext: '.js' + + clean: + all: ['lib', 'tmp'] + + mkdir: + all: + options: + create: ['tmp'] + + watch: + all: + files: ['src/**.coffee', 'test/**.coffee'] + tasks: ['test'] + + mochacli: + options: + files: 'test/*_test.coffee' + compilers: ['coffee:coffee-script'] + spec: + options: + reporter: 'spec' + + @registerTask 'npmPack', 'Create NPM package.', -> + done = @async() + + grunt.util.spawn + cmd: 'npm' + args: ['pack'] + , (error, result, code) -> + grunt.log.writeln(result.stderr) if result.stderr + grunt.log.writeln(result.stdout) if result.stdout + done(!error) + + @registerTask 'default', ['test'] + @registerTask 'build', ['clean', 'coffee'] + @registerTask 'package', ['build', 'npmPack'] + @registerTask 'test', ['build', 'mkdir', 'mochacli'] diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..c669ca8 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,22 @@ +Copyright (c) 2013 Ruben Vermeersch + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7da7b06 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# XLSX writer + + Simple XLSX writer. Reverse-engineered from sample XLSX files. + +## Usage + + You can install the latest version via npm: + + $ npm install --save xslx-writer + + Require the module: + + var xlsx = require('xslx-writer'); + + Write a spreadsheet: + + var data = [ + { + "Name": "Bob", + "Location": "Sweden" + }, + { + "Name": "Alice", + "Location": "France" + } + ]; + + xlsx.write('mySpreadsheet.xlsx', data, function (err) { + // Error handling here + }); + + This will write a spreadsheet like this: + + Name | Location + --------+--------- + Bob | Sweden + Alice | France + + In other words: The key names are used for the first row (headers), + The values are used for the columns. All field names should be present + in the first row. + +## License + + (The MIT License) + + Copyright (C) 2013 by Ruben Vermeersch + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..b7c7734 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "xlsx-writer", + "description": "Simple XLSX writer.", + "version": "0.0.1", + "homepage": "https://github.com/rubenv/node-xlsx-writer", + "author": { + "name": "Ruben Vermeersch", + "email": "ruben@savanne.be", + "url": "http://savanne.be/" + }, + "repository": { + "type": "git", + "url": "git://github.com/rubenv/node-xlsx-writer.git" + }, + "bugs": { + "url": "https://github.com/rubenv/node-xlsx-writer/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/rubenv/node-xlsx-writer/blob/master/LICENSE-MIT" + } + ], + "main": "lib/index.js", + "engines": { + "node": ">= 0.8.0" + }, + "scripts": { + "test": "grunt test" + }, + "devDependencies": { + "grunt-contrib-watch": "~0.3.1", + "grunt-contrib-coffee": "~0.6.0", + "grunt-contrib-clean": "~0.4.0", + "grunt": "~0.4.0", + "grunt-mocha-cli": "~1.0.1", + "coffee-script": "~1.6.1", + "grunt-mkdir": "~0.1.0", + "excel": "0.0.1", + "underscore": "~1.4.4" + }, + "keywords": [ + "xlsx", + "excel", + "spreadsheet" + ], + "dependencies": { + "temp": "~0.5.0", + "async": "~0.2.6", + "zipper": "~0.1.2" + } +} diff --git a/src/blobs.coffee b/src/blobs.coffee new file mode 100644 index 0000000..fa173d1 --- /dev/null +++ b/src/blobs.coffee @@ -0,0 +1,122 @@ +module.exports = + contentTypes: """ + + + + + + + + + + """.replace(/\n\s*/g, '') + + rels: """ + + + + + """.replace(/\n\s*/g, '') + + workbook: """ + + + + + + + + + + + + + """.replace(/\n\s*/g, '') + + workbookRels: """ + + + + + + + """.replace(/\n\s*/g, '') + + styles: """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """.replace(/\n\s*/g, '') + + strings: """ + + + + Ruben + + + """.replace(/\n\s*/g, '') + + sheet: """ + + + + + + + + + + + 100 + + + 0 + + + + + """.replace(/\n\s*/g, '') + diff --git a/src/index.coffee b/src/index.coffee new file mode 100644 index 0000000..caf6e38 --- /dev/null +++ b/src/index.coffee @@ -0,0 +1,44 @@ +fs = require('fs') +temp = require('temp') +path = require('path') +async = require('async') +zipper = require('zipper') + +blobs = require('./blobs') + +module.exports = + write: (out, data, cb) -> + tempPath = '' + + filename = (folder, name) -> + parts = Array::slice.call(arguments) + parts.unshift(tempPath) + return path.join.apply(@, parts) + + zipfile = new zipper.Zipper(out) + + async.series [ + (cb) -> temp.mkdir 'xlsx', (err, p) -> + tempPath = p + # Debug: + #tempPath = 'tmp' + cb(err) + (cb) -> fs.mkdir(filename('_rels'), cb) + (cb) -> fs.mkdir(filename('xl'), cb) + (cb) -> fs.mkdir(filename('xl', '_rels'), cb) + (cb) -> fs.mkdir(filename('xl', 'worksheets'), cb) + (cb) -> fs.writeFile(filename('[Content_Types].xml'), blobs.contentTypes, cb) + (cb) -> fs.writeFile(filename('_rels', '.rels'), blobs.rels, cb) + (cb) -> fs.writeFile(filename('xl', 'workbook.xml'), blobs.workbook, cb) + (cb) -> fs.writeFile(filename('xl', 'styles.xml'), blobs.styles, cb) + (cb) -> fs.writeFile(filename('xl', 'sharedStrings.xml'), blobs.strings, cb) + (cb) -> fs.writeFile(filename('xl', '_rels', 'workbook.xml.rels'), blobs.workbookRels, cb) + (cb) -> fs.writeFile(filename('xl', 'worksheets', 'sheet1.xml'), blobs.sheet, cb) + (cb) -> zipfile.addFile(filename('[Content_Types].xml'), '[Content_Types].xml', cb) + (cb) -> zipfile.addFile(filename('_rels', '.rels'), '_rels/.rels', cb) + (cb) -> zipfile.addFile(filename('xl', 'workbook.xml'), 'xl/workbook.xml', cb) + (cb) -> zipfile.addFile(filename('xl', 'styles.xml'), 'xl/styles.xml', cb) + (cb) -> zipfile.addFile(filename('xl', 'sharedStrings.xml'), 'xl/sharedStrings.xml', cb) + (cb) -> zipfile.addFile(filename('xl', '_rels', 'workbook.xml.rels'), 'xl/_rels/workbook.xml.rels', cb) + (cb) -> zipfile.addFile(filename('xl', 'worksheets', 'sheet1.xml'), 'xl/worksheets/sheet1.xml', cb) + ], cb diff --git a/test/common.coffee b/test/common.coffee new file mode 100644 index 0000000..6c6b227 --- /dev/null +++ b/test/common.coffee @@ -0,0 +1,38 @@ +xlsx = require('..') + +_ = require('underscore') +assert = require('assert') +fs = require('fs') +parser = require('excel') + +module.exports = (name, data) -> + describe name, -> + filename = "tmp/#{name}.xlsx" + result = [] + + before (done) -> + xlsx.write filename, data, (err) -> + return done(err) if err + + parser filename, (workbook) -> + assert.notEqual(workbook, null) + + result = workbook + done() + + it 'Should create XLSX file', -> + assert(fs.existsSync(filename), 'file needs to exist') + + it 'Should have header row', -> + assert(result.length >= 1, "Should have header row") + + for key, index in _.keys(data[0]) + assert.equal(result[0][index], key) + + it 'Should contain right values', -> + assert.equal(result.length, data.length + 1) + + for row, rowNr in result + continue if rowNr == 0 # Header + for key, index in _.keys(data[0]) + assert.equal(row[index], data[rowNr - 1][key]) diff --git a/test/simple_test.coffee b/test/simple_test.coffee new file mode 100644 index 0000000..f497503 --- /dev/null +++ b/test/simple_test.coffee @@ -0,0 +1,13 @@ +test = require('./common') + +test 'simple-test', [ + { + Name: 'Bob' + Location: 'Sweden' + } + { + Name: 'Alice' + Location: 'France' + } +] +