diff --git a/.gitignore b/.gitignore index 7afe557..0437aca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .*.swp node_modules -lib tmp diff --git a/README.md b/README.md index 99a3c3d..43a24b3 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ by enclosing it within an object with the keys `value, hyperlink`. "A String Column" : "A String Value", "A Number Column" : 12345, "A Date Column" : new Date(1999,11,31) + "A DateTime Column": {value: new Date(), formatAsDateTime: true}, "A String column with a hyperlink" : {value: "A String Value", hyperlink: "http://www.google.com"} "A Number column with a hyperlink" : {value: 12345, hyperlink: "http://www.google.com"} "A Date column with a hyperlink" : {value: new Date(1999,11,31), hyperlink: "http://www.google.com"} diff --git a/lib/blobs.js b/lib/blobs.js new file mode 100644 index 0000000..6e39968 --- /dev/null +++ b/lib/blobs.js @@ -0,0 +1,70 @@ +module.exports = { + contentTypes: "\n\n \n \n \n \n \n \n".replace(/\n\s*/g, ''), + rels: "\n\n \n".replace(/\n\s*/g, ''), + workbook: "\n\n \n \n \n \n \n \n \n \n \n".replace(/\n\s*/g, ''), + workbookRels: "\n\n \n \n \n".replace(/\n\s*/g, ''), + styles: "\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n".replace(/\n\s*/g, ''), + stringsHeader: function(count) { + return ("\n").replace(/\n\s*/g, ''); + }, + string: function(string) { + return ("\n " + string + "\n").replace(/\n\s*/g, ''); + }, + stringsFooter: "".replace(/\n\s*/g, ''), + sheetHeader: "\n\n \n \n \n ".replace(/\n\s*/g, ''), + startColumns: "", + column: function(width, index) { + return ""; + }, + endColumns: "", + startRow: function(row, ht) { + var out; + out = " 0) { + out += " customHeight=\"true\" ht=\"" + ht + "\" "; + } + out += ">"; + console.log(out); + return out; + }, + endRow: "", + cell: function(index, cell) { + return "" + index + ""; + }, + dateCell: function(value, cell) { + return "" + value + ""; + }, + dateTimeCell: function(value, cell) { + return "" + value + ""; + }, + numberCell: function(value, cell) { + return "" + value + ""; + }, + sheetDataHeader: "", + sheetDataFooter: "", + sheetFooter: "".replace(/\n\s*/g, ''), + worksheetRels: function(relationships) { + var i, out, rel, _i, _len; + if (relationships.length > 0) { + out = ""; + for (i = _i = 0, _len = relationships.length; _i < _len; i = ++_i) { + rel = relationships[i]; + out += ""; + } + out += ""; + return out; + } else { + return ""; + } + }, + externalWorksheetRels: function(relationships) { + var i, out, rel, _i, _len; + out = "\n"; + for (i = _i = 0, _len = relationships.length; _i < _len; i = ++_i) { + rel = relationships[i]; + out += ""; + } + out += ""; + return out.replace(/\n\s*/g, ''); + } +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..ffe1743 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,343 @@ +var Archiver, XlsxWriter, blobs, fs, _extend; + +fs = require('fs'); + +blobs = require('./blobs'); + +Archiver = require('archiver'); + +module.exports = XlsxWriter = (function() { + XlsxWriter.write = function(out, data, cb) { + var writer; + writer = new XlsxWriter({ + out: out + }); + writer.addRows(data); + return writer.writeToFile(cb); + }; + + function XlsxWriter(options) { + var defaults, zipOptions; + if (options == null) { + options = {}; + } + if (typeof options === 'string') { + options = { + out: options + }; + } + defaults = { + defaultWidth: 15, + zip: { + forceUTC: true + }, + columns: [] + }; + this.options = _extend(defaults, options); + this._resetSheet(); + this.defineColumns(this.options.columns); + zipOptions = this.options.zip || {}; + zipOptions.forceUTC = true; + this.zip = Archiver('zip', zipOptions); + this.zip.catchEarlyExitAttached = true; + this.zip.append(this.sheetStream, { + name: 'xl/worksheets/sheet1.xml' + }); + } + + XlsxWriter.prototype._addSheetDataHeader = function() { + if (!this.haveHeader) { + this._write(blobs.sheetDataHeader); + return this.haveHeader = true; + } + }; + + XlsxWriter.prototype.addRow = function(row) { + var col, key, _i, _len, _ref; + if (!this.haveHeader) { + this._write(blobs.sheetDataHeader); + this._startRow(); + col = 1; + for (key in row) { + this._addCell(key, col); + this.cellMap.push(key); + col += 1; + } + this._endRow(); + this.haveHeader = true; + } + this._startRow(); + _ref = this.cellMap; + for (col = _i = 0, _len = _ref.length; _i < _len; col = ++_i) { + key = _ref[col]; + this._addCell(row[key] || "", col + 1); + } + return this._endRow(); + }; + + XlsxWriter.prototype.addRows = function(rows) { + var row, _i, _len, _results; + _results = []; + for (_i = 0, _len = rows.length; _i < _len; _i++) { + row = rows[_i]; + _results.push(this.addRow(row)); + } + return _results; + }; + + XlsxWriter.prototype.defineColumns = function(columns) { + if (this.haveHeader) { + throw new Error("Columns cannot be added after rows! Unfortunately Excel will crash\nif column definitions come after sheet data. Please move your `defineColumns()`\ncall before any `addRow()` calls, or define options.columns in the XlsxWriter\nconstructor."); + } + this.options.columns = columns; + return this._write(this._generateColumnDefinition()); + }; + + XlsxWriter.prototype.writeToFile = function(fileName, cb) { + var fileStream, zip; + if (fileName instanceof Function) { + cb = fileName; + fileName = this.options.out; + } + if (!fileName) { + throw new Error("Filename required. Supply one in writeToFile() or in options.out."); + } + zip = this.createReadStream(fileName); + fileStream = fs.createWriteStream(fileName); + fileStream.once('finish', cb); + zip.pipe(fileStream); + return this.finalize(); + }; + + XlsxWriter.prototype.createReadStream = function() { + return this.getReadStream(); + }; + + XlsxWriter.prototype.getReadStream = function() { + return this.zip; + }; + + XlsxWriter.prototype.finalize = function() { + if (this.finalized) { + throw new Error("This XLSX was already finalized."); + } + this.finalized = true; + if (this.haveHeader) { + this._write(blobs.sheetDataFooter); + } + this._write(blobs.worksheetRels(this.relationships)); + this._generateStrings(); + this._generateRelationships(); + this.sheetStream.end(blobs.sheetFooter); + return this._finalizeZip(); + }; + + XlsxWriter.prototype.dispose = function() { + if (this.disposed) { + return; + } + this.sheetStream.end(); + this.sheetStream.unpipe(); + this.zip.unpipe(); + while (this.zip.read()) { + 1; + } + delete this.zip; + delete this.sheetStream; + return this.disposed = true; + }; + + XlsxWriter.prototype._addCell = function(value, col) { + var cell, date, index, row; + if (value == null) { + value = ''; + } + row = this.currentRow; + cell = this._getCellIdentifier(row, col); + if (Object.prototype.toString.call(value) === '[object Object]') { + if (value.formatAsDateTime) { + date = this._dateToOADate(value.value); + this.rowBuffer += blobs.dateTimeCell(date, cell); + return; + } + if (!value.value || !value.hyperlink) { + throw new Error("A hyperlink cell must have both 'value' and 'hyperlink' keys."); + } + this._addCell(value.value, col); + this._createRelationship(cell, value.hyperlink); + return; + } + if (typeof value === 'number') { + return this.rowBuffer += blobs.numberCell(value, cell); + } else if (value instanceof Date) { + date = this._dateToOADate(value); + return this.rowBuffer += blobs.dateCell(date, cell); + } else { + index = this._lookupString(value); + return this.rowBuffer += blobs.cell(index, cell); + } + }; + + XlsxWriter.prototype._startRow = function(ht) { + this.rowBuffer = blobs.startRow(this.currentRow, ht); + return this.currentRow += 1; + }; + + XlsxWriter.prototype._endRow = function() { + return this._write(this.rowBuffer + blobs.endRow); + }; + + XlsxWriter.prototype._getCellIdentifier = function(row, col) { + var a, colIndex, input; + colIndex = ''; + if (this.cellLabelMap[col]) { + colIndex = this.cellLabelMap[col]; + } else { + if (col === 0) { + row = 1; + col = 1; + } + input = (+col - 1).toString(26); + while (input.length) { + a = input.charCodeAt(input.length - 1); + colIndex = String.fromCharCode(a + (a >= 48 && a <= 57 ? 17 : -22)) + colIndex; + input = input.length > 1 ? (parseInt(input.substr(0, input.length - 1), 26) - 1).toString(26) : ""; + } + this.cellLabelMap[col] = colIndex; + } + return colIndex + row; + }; + + XlsxWriter.prototype._generateColumnDefinition = function() { + var column, columnDefinition, idx, index, _ref; + if (!this.options.columns || !this.options.columns.length) { + return ''; + } + columnDefinition = ''; + columnDefinition += blobs.startColumns; + idx = 1; + _ref = this.options.columns; + for (index in _ref) { + column = _ref[index]; + columnDefinition += blobs.column(column.width || this.options.defaultWidth, idx); + idx += 1; + } + columnDefinition += blobs.endColumns; + return columnDefinition; + }; + + XlsxWriter.prototype._generateStrings = function() { + var string, stringTable, _i, _len, _ref; + stringTable = ''; + _ref = this.strings; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + string = _ref[_i]; + stringTable += blobs.string(this.escapeXml(string)); + } + return this.stringsData = blobs.stringsHeader(this.strings.length) + stringTable + blobs.stringsFooter; + }; + + XlsxWriter.prototype._lookupString = function(value) { + if (!this.stringMap[value]) { + this.stringMap[value] = this.stringIndex; + this.strings.push(value); + this.stringIndex += 1; + } + return this.stringMap[value]; + }; + + XlsxWriter.prototype._createRelationship = function(cell, target) { + return this.relationships.push({ + cell: cell, + target: target + }); + }; + + XlsxWriter.prototype._generateRelationships = function() { + return this.relsData = blobs.externalWorksheetRels(this.relationships); + }; + + XlsxWriter.prototype._dateToOADate = function(date) { + var dec, epoch, msPerDay, v; + epoch = new Date(1899, 11, 30); + msPerDay = 8.64e7; + v = -1 * (epoch - date) / msPerDay; + dec = v - Math.floor(v); + if (v < 0 && dec) { + v = Math.floor(v) - dec; + } + return v; + }; + + XlsxWriter.prototype._OADateToDate = function(oaDate) { + var dec, epoch, msPerDay; + epoch = new Date(1899, 11, 30); + msPerDay = 8.64e7; + dec = oaDate - Math.floor(oaDate); + if (oaDate < 0 && dec) { + oaDate = Math.floor(oaDate) - dec; + } + return new Date(oaDate * msPerDay + +epoch); + }; + + XlsxWriter.prototype._resetSheet = function() { + var PassThrough; + this.sheetData = ''; + this.strings = []; + this.stringMap = {}; + this.stringIndex = 0; + this.stringData = null; + this.currentRow = 0; + this.cellMap = []; + this.cellLabelMap = {}; + this.columns = []; + this.relData = ''; + this.relationships = []; + this.haveHeader = false; + this.finalized = false; + PassThrough = require('stream').PassThrough; + this.sheetStream = new PassThrough(); + return this._write(blobs.sheetHeader); + }; + + XlsxWriter.prototype._finalizeZip = function() { + return this.zip.append(blobs.contentTypes, { + name: '[Content_Types].xml' + }).append(blobs.rels, { + name: '_rels/.rels' + }).append(blobs.workbook, { + name: 'xl/workbook.xml' + }).append(blobs.styles, { + name: 'xl/styles.xml' + }).append(blobs.workbookRels, { + name: 'xl/_rels/workbook.xml.rels' + }).append(this.relsData, { + name: 'xl/worksheets/_rels/sheet1.xml.rels' + }).append(this.stringsData, { + name: 'xl/sharedStrings.xml' + }).finalize(); + }; + + XlsxWriter.prototype._write = function(data) { + return this.sheetStream.write(data); + }; + + XlsxWriter.prototype.escapeXml = function(str) { + if (str == null) { + str = ''; + } + return str.replace(/&/g, '&').replace(//g, '>'); + }; + + return XlsxWriter; + +})(); + +_extend = function(dest, src) { + var key, val; + for (key in src) { + val = src[key]; + dest[key] = val; + } + return dest; +}; diff --git a/src/blobs.coffee b/src/blobs.coffee index fc129c4..cda05ca 100644 --- a/src/blobs.coffee +++ b/src/blobs.coffee @@ -50,6 +50,9 @@ module.exports = styles: """ + + + @@ -81,7 +84,8 @@ module.exports = - + + @@ -132,11 +136,19 @@ module.exports = column: (width, index) -> """""" endColumns: """""" - startRow: (row) -> """""" + #startRow: (row) -> """""" + startRow: (row, ht) -> + out = """0 + out += """ customHeight="true" ht="#{ht}" """ + out += ">" + return out + endRow: """""" cell: (index, cell) -> """#{index}""" dateCell: (value, cell) -> """#{value}""" + dateTimeCell: (value, cell) -> """#{value}""" numberCell: (value, cell) -> """#{value}""" sheetDataHeader: """""" diff --git a/src/index.litcoffee b/src/index.litcoffee index 5073fbc..eb45f3f 100644 --- a/src/index.litcoffee +++ b/src/index.litcoffee @@ -79,6 +79,23 @@ When constructing a writer, pass it an optional file path and customization opti # Hook this passthrough into the zip stream. @zip.append(@sheetStream, {name: 'xl/worksheets/sheet1.xml'}) +#### Add SheetDataHeader + +Simply add known header to resulting stream. +Should be used in conjunction with low-level api: _startRow, _addCell, endRow + +##### _addSheetDataHeader: () + +Add SheetDataHeader to resulting stream. + + # @example (javascript) + # writer._addSheetDataHeader() + _addSheetDataHeader: () -> + if !@haveHeader + @_write(blobs.sheetDataHeader) + @haveHeader = true + + #### Adding rows Rows are easy to add one by one or all at once. Data types within the sheet will @@ -93,6 +110,7 @@ Add a single row. # "A String Column" : "A String Value", # "A Number Column" : 12345, # "A Date Column" : new Date(1999,11,31) + # "A DateTime Column": {value: new Date(), formatAsDateTime: true}, # }) addRow: (row) -> @@ -115,6 +133,8 @@ Add a single row. @_addCell(row[key] || "", col + 1) @_endRow() + + ##### addRows(rows: Array) Rows can be added in batch. @@ -261,6 +281,11 @@ Adds a cell to the row in progress. # Hyperlink support if Object.prototype.toString.call(value) == '[object Object]' + if value.formatAsDateTime + date = @_dateToOADate(value.value) + @rowBuffer += blobs.dateTimeCell(date, cell) + return + if !value.value || !value.hyperlink throw new Error("A hyperlink cell must have both 'value' and 'hyperlink' keys.") @_addCell(value.value, col) @@ -281,8 +306,8 @@ Adds a cell to the row in progress. Begins a row. Call this before starting any row. Will start a buffer for all proceeding cells, until @_endRow is called. - _startRow: () -> - @rowBuffer = blobs.startRow(@currentRow) + _startRow: (ht) -> + @rowBuffer = blobs.startRow(@currentRow, ht) @currentRow += 1 Ends a row. Will write the row to the sheet.