diff --git a/.github/workflows/npm-grunt.yaml b/.github/workflows/npm-grunt.yaml new file mode 100644 index 0000000..8c6d59d --- /dev/null +++ b/.github/workflows/npm-grunt.yaml @@ -0,0 +1,29 @@ +name: NodeJS with Grunt + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Build + run: | + npm install + grunt + diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml new file mode 100644 index 0000000..c75bc58 --- /dev/null +++ b/.github/workflows/npm-publish.yaml @@ -0,0 +1,38 @@ +name: CI Pipeline - NPM Publish + +permissions: + checks: write + contents: write + +on: + push: + branches: + - "main" + - "master" + +jobs: + read-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.package_version.outputs.version }} # This makes it available to other jobs + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Read version from package.json + id: package_version + run: | + echo "::set-output name=version::$(jq -r '.version' package.json)" + - name: Output the version + run: echo "The version in package.json is ${{ steps.package_version.outputs.version }}" + + publish-pipeline: + needs: read-version + name: "Publish" + uses: brandwatch/bw-workflow-actions/.github/workflows/node-deploy-package.yaml@production + with: + version: ${{ needs.read-version.outputs.version }}-pr.${{ github.event.pull_request.number }} + node-version: 20 + linter: false + test: true + build-step: true + secrets: inherit diff --git a/.gitignore b/.gitignore index 76fc8d7..4848392 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ tmp npm-debug.log node_modules +package-lock.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index 23a4619..0000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = (grunt) -> - @loadNpmTasks('grunt-contrib-clean') - @loadNpmTasks('grunt-contrib-coffee') - @loadNpmTasks('grunt-contrib-watch') - @loadNpmTasks('grunt-mkdir') - @loadNpmTasks('grunt-release') - @loadNpmTasks('grunt-vows-runner') - - @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'] - - vows: - all: - src: 'test/test.coffee' - options: - reporter: 'spec' - - - @registerTask 'default', ['test'] - @registerTask 'build', ['clean', 'coffee'] - @registerTask 'package', ['build', 'release'] - @registerTask 'test', ['build', 'mkdir', 'vows'] diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..2a44964 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,49 @@ +module.exports = function(grunt) { + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + clean: { + all: ['lib', 'tmp'] + }, + coffee: { + all: { + expand: true, + cwd: 'src', + src: ['**/*.coffee'], + dest: 'lib', + ext: '.js' + } + }, + mkdir: { + all: { + options: { + create: ['tmp'] + } + } + }, + shell: { + vows: { + command: 'vows test/test.coffee --spec' + } + }, + watch: { + scripts: { + files: ['src/**/*.coffee'], + tasks: ['coffee'], + options: { + spawn: false + } + } + } + }); + + // Load the plugins. + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-coffee'); + grunt.loadNpmTasks('grunt-mkdir'); + grunt.loadNpmTasks('grunt-shell'); + grunt.loadNpmTasks('grunt-contrib-watch'); + + // Default task(s). + grunt.registerTask('default', ['clean', 'coffee', 'mkdir', 'shell:vows']); +}; diff --git a/lib/index.js b/lib/index.js index bc2b7aa..de610e9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,123 +1,146 @@ -var archiver, duplex, sheetStream, templates, through, utils, xlsxStream, _; +(function() { + // Convert stream of Array to a xlsx file. -through = require('through'); + // * Usage -archiver = require('archiver'); + // out = fs.createWriteStream('out.xlsx') + // stream = xlsxStream() + // stream.pipe out -_ = require('lodash'); + // stream.write(['aaa', 'bbb', 'ccc']) + // stream.write([1, 2, 3]) + // stream.write([new Date, '090-1234-5678', 'これはテストです']) -duplex = require('duplexer'); + // stream.end() + var _, archiver, duplex, sheetStream, templates, through, utils, xlsxStream; -templates = require('./templates'); + through = require('through'); -utils = require("./utils"); + archiver = require('archiver'); -sheetStream = require("./sheet"); + _ = require('lodash'); -module.exports = xlsxStream = function(opts) { - var defaultRepeater, defaultSheet, index, item, proxy, sheets, styles, zip, _i, _len, _ref; - if (opts == null) { - opts = {}; - } - zip = archiver.create('zip', opts); - defaultRepeater = through(); - proxy = duplex(defaultRepeater, zip); - zip.pause(); - process.nextTick(function() { - return zip.resume(); - }); - defaultSheet = null; - sheets = []; - styles = { - numFmts: [ - { - numFmtId: "0", - formatCode: "" - } - ], - cellStyleXfs: [ - { - numFmtId: "0", - formatCode: "" - }, { - numFmtId: "1", - formatCode: "0" - }, { - numFmtId: "14", - formatCode: "m/d/yy" - } - ], - customFormatsCount: 0, - formatCodesToStyleIndex: {} - }; - index = 0; - _ref = styles.cellStyleXfs; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - item = _ref[_i]; - styles.formatCodesToStyleIndex[item.formatCode || ""] = index; - index++; - } - defaultRepeater.once('data', function(data) { - defaultSheet = proxy.sheet('Sheet1'); - defaultSheet.write(data); - defaultRepeater.pipe(defaultSheet); - return defaultRepeater.on('end', proxy.finalize); - }); - proxy.sheet = function(name) { - var sheet; - index = sheets.length + 1; - sheet = { - index: index, - name: name || ("Sheet" + index), - rel: "worksheets/sheet" + index + ".xml", - path: "xl/worksheets/sheet" + index + ".xml", - styles: styles - }; - sheets.push(sheet); - return sheetStream(zip, sheet, opts); - }; - proxy.finalize = function() { - var buffer, func, name, obj, sheet, _j, _len1, _ref1, _ref2, _ref3; - zip.append(templates.styles(styles), { - name: "xl/styles.xml", - store: opts.store + duplex = require('duplexer'); + + templates = require('./templates'); + + utils = require("./utils"); + + sheetStream = require("./sheet"); + + module.exports = xlsxStream = function(opts = {}) { + var defaultRepeater, defaultSheet, i, index, item, len, proxy, ref, sheets, styles, zip; + // archiving into a zip file using archiver (internally using node's zlib built-in module) + zip = archiver.create('zip', opts); + defaultRepeater = through(); + proxy = duplex(defaultRepeater, zip); + // prevent loosing data before listening 'data' event in node v0.8 + zip.pause(); + process.nextTick(function() { + return zip.resume(); }); - _ref1 = templates.statics; - for (name in _ref1) { - buffer = _ref1[name]; - zip.append(buffer, { - name: name, - store: opts.store - }); + defaultSheet = null; + sheets = []; + styles = { + numFmts: [ + { + numFmtId: "0", + formatCode: "" + } + ], + cellStyleXfs: [ + { + numFmtId: "0", + formatCode: "" + }, + { + numFmtId: "1", + formatCode: "0" + }, + { + numFmtId: "14", + formatCode: "m/d/yy" + } + ], + customFormatsCount: 0, + formatCodesToStyleIndex: {} + }; + index = 0; + ref = styles.cellStyleXfs; + for (i = 0, len = ref.length; i < len; i++) { + item = ref[i]; + styles.formatCodesToStyleIndex[item.formatCode || ""] = index; + index++; } - _ref2 = templates.semiStatics; - for (name in _ref2) { - func = _ref2[name]; - zip.append(func(opts), { - name: name, + // writing data without sheet() results in creating a default worksheet named 'Sheet1' + defaultRepeater.once('data', function(data) { + defaultSheet = proxy.sheet('Sheet1'); + defaultSheet.write(data); + defaultRepeater.pipe(defaultSheet); + return defaultRepeater.on('end', proxy.finalize); + }); + // Append a new worksheet to the workbook + proxy.sheet = function(name) { + var sheet; + index = sheets.length + 1; + sheet = { + index: index, + name: name || `Sheet${index}`, + rel: `worksheets/sheet${index}.xml`, + path: `xl/worksheets/sheet${index}.xml`, + styles: styles + }; + sheets.push(sheet); + return sheetStream(zip, sheet, opts); + }; + // finalize the xlsx file + proxy.finalize = function() { + var buffer, func, j, len1, name, obj, ref1, ref2, ref3, sheet; + // styles + zip.append(templates.styles(styles), { + name: "xl/styles.xml", store: opts.store }); - } - _ref3 = templates.sheet_related; - for (name in _ref3) { - obj = _ref3[name]; - buffer = obj.header; - for (_j = 0, _len1 = sheets.length; _j < _len1; _j++) { - sheet = sheets[_j]; - buffer += obj.sheet(sheet); + ref1 = templates.statics; + // static files + for (name in ref1) { + buffer = ref1[name]; + zip.append(buffer, { + name, + store: opts.store + }); } - buffer += obj.footer; - zip.append(buffer, { - name: name, - store: opts.store - }); - } - return zip.finalize(function(e, bytes) { - if (e != null) { - return proxy.emit('error', e); + ref2 = templates.semiStatics; + for (name in ref2) { + func = ref2[name]; + zip.append(func(opts), { + name, + store: opts.store + }); } - return proxy.emit('finalize', bytes); - }); + ref3 = templates.sheet_related; + // files modified by number of sheets + for (name in ref3) { + obj = ref3[name]; + buffer = obj.header; + for (j = 0, len1 = sheets.length; j < len1; j++) { + sheet = sheets[j]; + buffer += obj.sheet(sheet); + } + buffer += obj.footer; + zip.append(buffer, { + name, + store: opts.store + }); + } + return zip.finalize(function(e, bytes) { + if (e != null) { + return proxy.emit('error', e); + } + return proxy.emit('finalize', bytes); + }); + }; + return proxy; }; - return proxy; -}; + +}).call(this); diff --git a/lib/sheet.js b/lib/sheet.js index ced49a2..725f9dc 100644 --- a/lib/sheet.js +++ b/lib/sheet.js @@ -1,75 +1,75 @@ -var sheetStream, template, through, utils, _; +(function() { + var _, sheetStream, template, through, utils, worksheetTemplates; -_ = require("lodash"); + _ = require("lodash"); -through = require('through'); + through = require('through'); -utils = require('./utils'); + utils = require('./utils'); -template = require('./templates'); -worksheetTemplates = template.worksheet; + template = require('./templates'); -module.exports = sheetStream = function(zip, sheet, opts) { - var colChar, converter, nRow, onData, onEnd; - var links = []; - if (opts == null) { - opts = {}; - } - colChar = _.memoize(utils.colChar); - nRow = 0; - onData = function(row) { - var buf, col, i, val, _i, _j, _len, _len1, _ref; - nRow++; - buf = ""; - if (opts.columns != null) { - _ref = opts.columns; - for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { - col = _ref[i]; - buf += utils.buildCell("" + (colChar(i)) + nRow, row[col], sheet.styles); + worksheetTemplates = template.worksheet; + + module.exports = sheetStream = function(zip, sheet, opts = {}) { + var colChar, converter, links, nRow, onData, onEnd; + // 列番号の26進表記(A, B, .., Z, AA, AB, ..) + // 一度計算したらキャッシュしておく。 + colChar = _.memoize(utils.colChar); + links = []; + // 行ごとに変換してxl/worksheets/sheet1.xml に追加 + nRow = 0; + onData = function(row) { + var buf, col, i, j, k, len, len1, ref, val; + nRow++; + buf = ``; + if (opts.columns != null) { + ref = opts.columns; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + col = ref[i]; + buf += utils.buildCell(`${colChar(i)}${nRow}`, row[col], sheet.styles); + } + } else { + for (i = k = 0, len1 = row.length; k < len1; i = ++k) { + val = row[i]; + buf += utils.buildCell(`${colChar(i)}${nRow}`, val, sheet.styles); + } } - } else { - for (i = _j = 0, _len1 = row.length; _j < _len1; i = ++_j) { - val = row[i]; - if (typeof val === 'string') { - if (val.startsWith('http://')) { - links.push(`${colChar(i)}${nRow}-${val}`) - } + buf += ''; + return this.queue(buf); + }; + onEnd = function() { + var converter, func, j, len, link, linkCounter, name, rel; + this.queue(worksheetTemplates.footer); + if (links.length > 0) { + rel = template.rels; + for (name in rel) { + func = rel[name]; + zip.append(func(links), { + name: name + }); } - buf += utils.buildCell("" + (colChar(i)) + nRow, val, sheet.styles); + this.queue(worksheetTemplates.hyperLinkStart); + linkCounter = 0; + for (j = 0, len = links.length; j < len; j++) { + link = links[j]; + linkCounter++; + this.queue(worksheetTemplates.hyperLink(link, linkCounter)); + } + this.queue(worksheetTemplates.hyperLinkEnd); } - } - buf += ''; - return this.queue(buf); + this.queue(worksheetTemplates.endSheet); + this.queue(null); + return converter = colChar = zip = null; + }; + converter = through(onData, onEnd); + zip.append(converter, { + name: sheet.path, + store: opts.store + }); + // ヘッダ部分を追加 + converter.queue(worksheetTemplates.header); + return converter; }; - onEnd = function() { - var converter; - this.queue(worksheetTemplates.footer); - if (links) { - let rel = template.rels; - for (name in rel) { - func = rel[name]; - zip.append(func(links), { - name: name - }); - } - this.queue(worksheetTemplates.hyperLinkStart); - let linkCounter = 0; - links.forEach(link => { - linkCounter++; - this.queue(worksheetTemplates.hyperLink(link, linkCounter)); - }); - this.queue(worksheetTemplates.hyperLinkEnd); - } - this.queue(worksheetTemplates.endSheet); - this.queue(null); - return converter = colChar = zip = null; - }; - converter = through(onData, onEnd); - zip.append(converter, { - name: sheet.path, - store: opts.store - }); - converter.queue(worksheetTemplates.header); - return converter; -}; +}).call(this); diff --git a/lib/templates.js b/lib/templates.js index 2b407de..a670b30 100644 --- a/lib/templates.js +++ b/lib/templates.js @@ -1,90 +1,199 @@ -var esc, utils, xml; +(function() { + var esc, utils, xml; -utils = require('./utils'); + utils = require('./utils'); -xml = utils.compress; + xml = utils.compress; -esc = utils.escapeXML; + esc = utils.escapeXML; -module.exports = { - worksheet: { - header: xml("\n\n \n \n \n \n "), - footer: xml(" \n"), - hyperLinkStart: xml("\n"), - hyperLink: (link, rId) => { - const parts = link.split('-'); - const escapedLink = parts[1].replace(/&/g, '&') - const xmlString = `\n`; - return xml(xmlString) - }, - hyperLinkEnd: xml("\n"), - endSheet: xml("") - }, - sheet_related: { - "[Content_Types].xml": { - header: xml("\n\n \n \n \n \n \n \n "), - sheet: function(sheet) { - return ""; + module.exports = { + worksheet: { + header: xml(` + + + + + + `), + footer: xml(``), + hyperLinkStart: xml(``), + hyperLink: function(link, rId) { + var escapedLink, parts, xmlString; + parts = link.split('-'); + escapedLink = parts[1].replace(/&/g, '&'); + xmlString = ``; + return xml(xmlString); }, - footer: xml("") + hyperLinkEnd: xml(``), + endSheet: xml(``) }, - "xl/_rels/workbook.xml.rels": { - header: xml("\n"), - sheet: function(sheet) { - return ""; + sheet_related: { + "[Content_Types].xml": { + header: xml(` + + + + + + + + `), + sheet: function(sheet) { + return ``; + }, + footer: xml(``) }, - footer: xml(" \n \n") - }, - "xl/workbook.xml": { - header: xml("\n\n \n \n \n \n \n "), - sheet: function(sheet) { - return xml(""); + "xl/_rels/workbook.xml.rels": { + header: xml(` +`), + sheet: function(sheet) { + return ``; + }, + footer: xml(` + +`) }, - footer: xml(" \n \n") - } - }, - styles: function(styl) { - var cellXfItems, cellXfs, item, numFmtItems, numFmts, _i, _j, _len, _len1, _ref, _ref1; - numFmtItems = ""; - _ref = styl.numFmts; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - item = _ref[_i]; - numFmtItems += " \n"; - } - numFmts = numFmtItems ? "\n " + numFmtItems + "" : ""; - cellXfItems = ""; - _ref1 = styl.cellStyleXfs; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - item = _ref1[_j]; - cellXfItems += " \n"; - } - cellXfs = cellXfItems ? "\n " + cellXfItems + "\n" : ""; - return xml("\n\n " + numFmts + "\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 " + cellXfs + "\n \n \n \n \n \n \n \n \n \n \n"); - }, - statics: { - "_rels/.rels": xml("\n \n \n \n \n"), - "xl/sharedStrings.xml": xml("\n"), - "docProps/app.xml": xml("\n\n node-xlsx-stream\n 0\n false\n Microsoft Corporation\n false\n false\n false\n " + (require('../package.json').version) + "\n") - }, - semiStatics: { - "docProps/core.xml": function(opts) { - var today; - today = new Date().toISOString(); - return "\n\n node-xlsx-stream\n node-xlsx-stream\n " + today + "\n " + today + "\n"; - } - }, - rels: { - "xl/worksheets/_rels/sheet1.xml.rels": function (links) { - xmlString = xml("\n"); - let linksCounter = 0; - links.forEach(link => { - linksCounter++; - const parts = link.split('-'); - const escapedLink = parts[1].replace(/&/g, '&') - xmlString += xml(`\n`); - }); - xmlString += xml(""); - return xmlString; + "xl/workbook.xml": { + header: xml(` + + + + + + + `), + sheet: function(sheet) { + return xml(``); + }, + footer: xml(` + +`) + } + }, + styles: function(styl) { + var cellXfItems, cellXfs, i, item, j, len, len1, numFmtItems, numFmts, ref, ref1; + numFmtItems = ""; + ref = styl.numFmts; + for (i = 0, len = ref.length; i < len; i++) { + item = ref[i]; + numFmtItems += ``; + } + numFmts = numFmtItems ? ` + ${numFmtItems} +` : ""; + cellXfItems = ""; + ref1 = styl.cellStyleXfs; + for (j = 0, len1 = ref1.length; j < len1; j++) { + item = ref1[j]; + cellXfItems += ``; + } + cellXfs = cellXfItems ? ` + ${cellXfItems} +` : ""; + return xml(` + + ${numFmts} + + + + + + + + + + + + + + + + + + + + + + + + + + + ${cellXfs} + + + + + + + + + + +`); + }, + statics: { + "_rels/.rels": xml(` + + + + +`), + "xl/sharedStrings.xml": xml(` +`), + "docProps/app.xml": xml(` + + node-xlsx-stream + 0 + false + Microsoft Corporation + false + false + false + ${require('../package.json').version} +`) + }, + semiStatics: { + "docProps/core.xml": function(opts) { + var today; + today = new Date().toISOString(); + return ` + + node-xlsx-stream + node-xlsx-stream + ${today} + ${today} +`; + } + }, + rels: { + "xl/worksheets/_rels/sheet1.xml.rels": function(links) { + var escapedLink, i, len, link, linksCounter, parts, xmlString; + xmlString = xml(` +`); + linksCounter = 0; + for (i = 0, len = links.length; i < len; i++) { + link = links[i]; + linksCounter++; + parts = link.split('-'); + escapedLink = parts[1].replace(/&/g, '&'); + xmlString += xml(``); + } + xmlString += xml(``); + return xmlString; + } } - } -}; + }; + +}).call(this); diff --git a/lib/utils.js b/lib/utils.js index d20b98d..1bf13e6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,149 +1,153 @@ -var compress, escapeXML, _; +(function() { + var _, compress, escapeXML; -_ = require("lodash"); + _ = require("lodash"); -module.exports = { - colChar: function(input) { - var a, colIndex; - input = input.toString(26); - colIndex = ''; - 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) : ""; - } - return colIndex; - }, - escapeXML: escapeXML = function(str) { - return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); - }, - compress: compress = function(str) { - return String(str).replace(/\n\s*/g, ''); - }, - buildCell: function(ref, val, styles) { - var f, getStyle, r, s, t, v; - getStyle = function(nf) { - var getBuiltinNumFmtId, numFmtId, r, s; - if (!nf) { - return; - } - r = styles.formatCodesToStyleIndex[nf]; - if (r) { - return r; + module.exports = { + colChar: function(input) { + var a, colIndex; + input = input.toString(26); + colIndex = ''; + 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) : ""; } - getBuiltinNumFmtId = function(nf) { - var builtin_nfs; - builtin_nfs = { - 'General': 0, - '': 0, - '0': 1, - '0.00': 2, - '#,##0': 3, - '#,##0.00': 4, - '0%': 9, - '0.00%': 10, - '0.00E+00': 11, - '# ?/?': 12, - '# ??/??': 13, - 'm/d/yy': 14, - 'd-mmm-yy': 15, - 'd-mmm': 16, - 'mmm-yy': 17, - 'h:mm AM/PM': 18, - 'h:mm:ss AM/PM': 19, - 'h:mm': 20, - 'h:mm:ss': 21, - 'm/d/yy h:mm': 22, - '[$-404]e/m/d': 27, - '#,##0 ;(#,##0)': 37, - '#,##0 ;[Red](#,##0)': 38, - '#,##0.00;(#,##0.00)': 39, - '#,##0.00;[Red](#,##0.00)': 40, - '_("$"* #,##0.00_);_("$"* \\(#,##0.00\\);_("$"* "-"??_);_(@_)': 44, - 'mm:ss': 45, - '[h]:mm:ss': 46, - 'mmss.0': 47, - '##0.0E+0': 48, - '@': 49, - 't0': 59, - 't0.00': 60, - 't#,##0': 61, - 't#,##0.00': 62, - 't0%': 67, - 't0.00%': 68, - 't# ?/?': 69, - 't# ??/??': 70 + return colIndex; + }, + escapeXML: escapeXML = function(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + }, + compress: compress = function(str) { + return String(str).replace(/\n\s*/g, ''); + }, + buildCell: function(ref, val, styles) { + var f, getStyle, r, s, t, v; + getStyle = function(nf) { + var getBuiltinNumFmtId, numFmtId, r, s; + if (!nf) { + return; + } + r = styles.formatCodesToStyleIndex[nf]; + if (r) { + return r; + } + getBuiltinNumFmtId = function(nf) { + var builtin_nfs; + // ECMA-376 18.8.30 + builtin_nfs = { + 'General': 0, + '': 0, + '0': 1, + '0.00': 2, + '#,##0': 3, + '#,##0.00': 4, + '0%': 9, + '0.00%': 10, + '0.00E+00': 11, + '# ?/?': 12, + '# ??/??': 13, + 'm/d/yy': 14, // also 30 + 'd-mmm-yy': 15, + 'd-mmm': 16, + 'mmm-yy': 17, + 'h:mm AM/PM': 18, + 'h:mm:ss AM/PM': 19, + 'h:mm': 20, + 'h:mm:ss': 21, + 'm/d/yy h:mm': 22, + '[$-404]e/m/d': 27, // also 36, 50, 57 + '#,##0 ;(#,##0)': 37, + '#,##0 ;[Red](#,##0)': 38, + '#,##0.00;(#,##0.00)': 39, + '#,##0.00;[Red](#,##0.00)': 40, + '_("$"* #,##0.00_);_("$"* \\(#,##0.00\\);_("$"* "-"??_);_(@_)': 44, + 'mm:ss': 45, + '[h]:mm:ss': 46, + 'mmss.0': 47, + '##0.0E+0': 48, + '@': 49, + 't0': 59, + 't0.00': 60, + 't#,##0': 61, + 't#,##0.00': 62, + 't0%': 67, + 't0.00%': 68, + 't# ?/?': 69, + 't# ??/??': 70 + }; + r = builtin_nfs[nf]; + return r; }; - r = builtin_nfs[nf]; - return r; - }; - numFmtId = getBuiltinNumFmtId(nf); - if (!numFmtId) { - styles.customFormatsCount++; - numFmtId = 164 + styles.customFormatsCount; - styles.numFmts.push({ + numFmtId = getBuiltinNumFmtId(nf); + if (!numFmtId) { + styles.customFormatsCount++; + numFmtId = 164 + styles.customFormatsCount; + styles.numFmts.push({ + numFmtId: numFmtId, + formatCode: nf + }); + } + s = styles.cellStyleXfs.length; + styles.cellStyleXfs.push({ numFmtId: numFmtId, formatCode: nf }); + styles.formatCodesToStyleIndex[nf] = s; + return s; + }; + if (val == null) { + return ''; } - s = styles.cellStyleXfs.length; - styles.cellStyleXfs.push({ - numFmtId: numFmtId, - formatCode: nf - }); - styles.formatCodesToStyleIndex[nf] = s; - return s; - }; - if (val == null) { - return ''; - } - if (typeof val === 'object' && !_.isDate(val)) { - v = val.v; - t = val.t; - s = val.s; - f = val.f; - if (!s && val.nf) { - s = getStyle(val.nf); + if (typeof val === 'object' && !_.isDate(val)) { + v = val.v; + t = val.t; + s = val.s; + f = val.f; + if (!s && val.nf) { + s = getStyle(val.nf); + } + } else { + v = val; } - } else { - v = val; - } - if (_.isNumber(v) && _.isFinite(v)) { - v = '' + v + ''; - if (val.nf && !t) { - t = 'n'; + if (_.isNumber(v) && _.isFinite(v)) { + v = '' + v + ''; + if (val.nf && !t) { + t = 'n'; + } + } else if (_.isDate(v)) { + t = 'd'; + if (s == null) { + s = '2'; + } + v = '' + v.toISOString() + ''; + } else if (_.isBoolean(v)) { + t = 'b'; + v = '' + (v === true ? '1' : '0') + ''; + } else if (v) { + v = '' + escapeXML(v) + ''; + t = 'inlineStr'; } - } else if (_.isDate(v)) { - t = 'd'; - if (s == null) { - s = '2'; + if (!(v || f)) { + return ''; } - v = '' + v.toISOString() + ''; - } else if (_.isBoolean(v)) { - t = 'b'; - v = '' + (v === true ? '1' : '0') + ''; - } else if (v) { - v = '' + escapeXML(v) + ''; - t = 'inlineStr'; - } - if (!(v || f)) { - return ''; - } - r = ''; - } - if (v) { - r += v; + r = ''; + } + if (v) { + r += v; + } + r += ''; + return r; } - r += ''; - return r; - } -}; + }; + +}).call(this); diff --git a/package.json b/package.json index 2b3c577..778a633 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xlsx-stream", "description": "Creates SpreadsheetML (.xlsx) files in sequence with streaming interface.", - "version": "0.1.2", + "version": "0.2.0", "homepage": "https://github.com/nunukim/node-xlsx-stream", "author": { "name": "Ryota Suzuki", @@ -23,27 +23,31 @@ ], "main": "lib/index.js", "engines": { - "node": "~ 0.8.0" + "node": ">0.8.0" + }, + "scripts": { + "grunt": "grunt", + "test": "grunt", + "build": "npm install" }, - "scripts": {}, "devDependencies": { - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-watch": "~0.5.3", - "grunt-contrib-coffee": "~0.7.0", - "grunt-release": "~0.6.0", - "grunt-vows-runner": "~0.6.0", - "grunt-mkdir": "~0.1.1", - "grunt": "~0.4.0", - "coffee-script": "~1.6.3", - "vows": "~0.7.0", - "excel-parser": "~0.2.1", - "concat-stream": "~1.2.1" + "grunt": "^1.6.1", + "grunt-cli": "^1.5.0", + "grunt-contrib-clean": "^2.0.1", + "grunt-contrib-watch": "^1.1.0", + "grunt-contrib-coffee": "^2.1.0", + "grunt-release": "^0.14.0", + "grunt-shell": "^4.0.0", + "grunt-mkdir": "^1.1.0", + "coffee-script": "^1.12.7", + "vows": "^0.8.3", + "concat-stream": "^2.0.0" }, "dependencies": { - "lodash": "~ 1.3.0", - "through": "~ 2.3.4", - "duplexer": "~ 0.1.1", - "archiver": "~0.4.10" + "lodash": "^4.17.21", + "through": "^2.3.8", + "duplexer": "^0.1.2", + "archiver": "^7.0.1" }, "keywords": [ "xlsx", diff --git a/src/sheet.coffee b/src/sheet.coffee index 4155e9c..3059399 100644 --- a/src/sheet.coffee +++ b/src/sheet.coffee @@ -2,12 +2,14 @@ _ = require "lodash" through = require('through') utils = require('./utils') -template = require('./templates').worksheet +template = require('./templates') +worksheetTemplates = template.worksheet module.exports = sheetStream = (zip, sheet, opts={})-> # 列番号の26進表記(A, B, .., Z, AA, AB, ..) # 一度計算したらキャッシュしておく。 colChar = _.memoize utils.colChar + links = [] # 行ごとに変換してxl/worksheets/sheet1.xml に追加 nRow = 0 @@ -20,9 +22,23 @@ module.exports = sheetStream = (zip, sheet, opts={})-> buf += utils.buildCell("#{colChar(i)}#{nRow}", val, sheet.styles) for val, i in row buf += '' @queue buf + onEnd = -> - # フッタ部分を追加 - @queue template.footer + @queue worksheetTemplates.footer + + if links.length > 0 + rel = template.rels + for name, func of rel + zip.append func(links), name: name + + @queue worksheetTemplates.hyperLinkStart + linkCounter = 0 + for link in links + linkCounter++ + @queue worksheetTemplates.hyperLink(link, linkCounter) + @queue worksheetTemplates.hyperLinkEnd + + @queue worksheetTemplates.endSheet @queue null converter = colChar = zip = null @@ -30,6 +46,6 @@ module.exports = sheetStream = (zip, sheet, opts={})-> zip.append converter, name: sheet.path, store: opts.store # ヘッダ部分を追加 - converter.queue template.header + converter.queue worksheetTemplates.header return converter diff --git a/src/templates.coffee b/src/templates.coffee index 1db321c..80ec3c0 100644 --- a/src/templates.coffee +++ b/src/templates.coffee @@ -4,11 +4,13 @@ xml = utils.compress esc = utils.escapeXML module.exports = - # worksheet worksheet: header: xml """ - + @@ -17,10 +19,24 @@ module.exports = """ footer: xml """ + """ + hyperLinkStart: xml """ + + """ + hyperLink: (link, rId) -> + parts = link.split('-') + escapedLink = parts[1].replace(/&/g, '&') + xmlString = """ + + """ + xml xmlString + hyperLinkEnd: xml """ + + """ + endSheet: xml """ """ - # Static files sheet_related: "[Content_Types].xml": header: xml """ @@ -34,9 +50,10 @@ module.exports = """ - sheet: (sheet)-> """ - - """ + sheet: (sheet) -> + """ + + """ footer: xml """ """ @@ -46,9 +63,10 @@ module.exports = """ - sheet: (sheet)-> """ - - """ + sheet: (sheet) -> + """ + + """ footer: xml """ @@ -58,36 +76,42 @@ module.exports = "xl/workbook.xml": header: xml """ - + - + """ - sheet: (sheet)-> xml """ - - """ + sheet: (sheet) -> + xml """ + + """ footer: xml """ """ - # Styles file - styles: (styl)-> + styles: (styl) -> numFmtItems = "" for item in styl.numFmts - numFmtItems += " \n" + numFmtItems += """ + + """ numFmts = if numFmtItems then """ - #{numFmtItems} + #{numFmtItems} + """ else "" cellXfItems = "" for item in styl.cellStyleXfs - cellXfItems += " \n" + cellXfItems += """ + + """ cellXfs = if cellXfItems then """ #{cellXfItems} @@ -96,7 +120,9 @@ module.exports = xml """ - + #{numFmts} @@ -138,11 +164,10 @@ module.exports = """ - # Static files statics: "_rels/.rels": xml """ - + @@ -166,17 +191,40 @@ module.exports = false #{require('../package.json').version} - """ + """ semiStatics: - "docProps/core.xml": (opts)-> + "docProps/core.xml": (opts) -> today = new Date().toISOString() """ - + node-xlsx-stream node-xlsx-stream #{today} #{today} """ + + rels: + "xl/worksheets/_rels/sheet1.xml.rels": (links) -> + xmlString = xml """ + + + """ + linksCounter = 0 + for link in links + linksCounter++ + parts = link.split('-') + escapedLink = parts[1].replace(/&/g, '&') + xmlString += xml """ + + """ + xmlString += xml """ + + """ + xmlString diff --git a/test/test.coffee b/test/test.coffee index a135907..de6b9c9 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -1,7 +1,7 @@ xlsx_stream = require "../" vows = require "vows" assert = require "assert" -office = require "office" +#office = require "office" fs = require "fs" path = require "path"