├── .travis.yml ├── .eslintrc ├── .editorconfig ├── package.json ├── LICENSE ├── save-csv.min.js ├── Makefile ├── README.md └── save-csv.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: eslint-config-silverwind 3 | 4 | rules: 5 | no-var: [0] 6 | prefer-arrow-callback: [0] 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [LICENSE] 13 | indent_size = none 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "save-csv", 3 | "version": "4.1.0", 4 | "description": "Download an array of objects as a CSV file in the browser", 5 | "author": "silverwind", 6 | "repository": "silverwind/save-csv", 7 | "main": "save-csv.js", 8 | "browser": "save-csv.js", 9 | "license": "BSD-2-Clause", 10 | "scripts": { 11 | "test": "make test" 12 | }, 13 | "keywords": [ 14 | "csv", 15 | "save", 16 | "excel", 17 | "export", 18 | "locale-aware", 19 | "download", 20 | "browser", 21 | "json" 22 | ], 23 | "files": [ 24 | "save-csv.js", 25 | "save-csv.min.js" 26 | ], 27 | "devDependencies": { 28 | "eslint": "^5.9.0", 29 | "eslint-config-silverwind": "^2.0.10", 30 | "gzip-size-cli": "^3.0.0", 31 | "semver": "^5.6.0", 32 | "uglify-js": "^3.4.9", 33 | "updates": "^5.3.0", 34 | "ver": "^3.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) silverwind 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /save-csv.min.js: -------------------------------------------------------------------------------- 1 | /*! save-csv v4.1.0 | (c) silverwind | BSD license */ 2 | !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&module.exports?module.exports=t():e.saveCsv=t()}("undefined"!=typeof self?self:this,function(){"use strict";return function(e,i){if(!Array.isArray(e)||!e.length)throw new Error("Expected an array of values, got "+e);(i=i||{}).filename=i.filename||"export.csv",i.sep=i.sep||"auto",i.eol=i.eol||"\r\n",i.bom="boolean"!=typeof i.bom||i.bom,i.quote=i.quote||'"',i.mime=i.mime||"text/csv;charset=utf-8","auto"===i.sep&&("toLocaleString"in Number.prototype?i.sep=","===1.2.toLocaleString().substring(1,2)?";":",":i.sep=",");var o=new RegExp(i.quote,"g"),n=new RegExp(i.sep,"g");i.formatter=i.formatter||function(e){var t=!1;return"string"!=typeof e&&(e=JSON.stringify(e)||""),o.test(e)&&(e=i.quote+e.replace(o,i.quote+i.quote)+i.quote,t=!0),n.test(e)&&!t&&(e=i.quote+e+i.quote),e};var a=[];!function o(n,r,e){e.forEach(function(e){var t=n?n+"."+e:e;"object"==typeof r[e]&&null!==r[e]?o(t,r[e],Object.keys(r[e])):a.push(i.formatter(t))})}(null,e[0],Object.keys(e[0]));var t=a.join(i.sep)+i.eol+e.map(function(e){var n=[];return function t(o,e){e.forEach(function(e){"object"==typeof o[e]&&null!==o[e]?t(o[e],Object.keys(o[e])):n.push(i.formatter(o[e]))})}(e,Object.keys(e)),n.join(i.sep)}).join(i.eol),r=new Blob([i.bom?"\ufeff"+t:t]);if(window.navigator.msSaveOrOpenBlob)window.navigator.msSaveOrOpenBlob(r,i.filename);else{var u=document.createElement("a");u.setAttribute("href",URL.createObjectURL(r,{type:i.mime})),u.setAttribute("download",i.filename),u.setAttribute("target","_blank"),u.style.display="none",document.body.appendChild(u),u.click(),setTimeout(function(){document.body.removeChild(u)},0)}}}); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # os dependencies: jq git npm 2 | 3 | VERSION := $(shell jq -r .version < package.json) 4 | 5 | lint: 6 | npx eslint --color --quiet --ignore-pattern *.min.js . 7 | 8 | test: 9 | $(MAKE) lint 10 | 11 | min: 12 | npx uglifyjs save-csv.js -o save-csv.min.js --mangle --compress --unsafe --comments '/save-csv/' && wc -c save-csv.min.js 13 | cat README.md | sed -E "s/[0-9]+ bytes/$$(npx gzip-size --raw save-csv.min.js) bytes/" > README.md 14 | git diff --exit-code &>/dev/null || git commit -am "rebuild" 15 | 16 | update: 17 | npx updates -u 18 | rm -rf node_modules 19 | npm i --no-package-lock 20 | 21 | publish: 22 | npm publish 23 | git push -u --follow-tags 24 | 25 | patch: 26 | $(MAKE) lint 27 | cat save-csv.min.js | sed -E "s/v[0-9\.]+/v$$(npx semver -i patch $(VERSION))/" > save-csv.min.js 28 | cat save-csv.js | sed -E "s/v[0-9\.]+/v$$(npx semver -i patch $(VERSION))/" > save-csv.js 29 | git diff --exit-code &>/dev/null || git commit -am "bump version" 30 | $(MAKE) min 31 | npx ver patch 32 | $(MAKE) publish 33 | 34 | minor: 35 | $(MAKE) lint 36 | cat save-csv.min.js | sed -E "s/v[0-9\.]+/v$$(npx semver -i minor $(VERSION))/" > save-csv.min.js 37 | cat save-csv.js | sed -E "s/v[0-9\.]+/v$$(npx semver -i minor $(VERSION))/" > save-csv.js 38 | git diff --exit-code &>/dev/null || git commit -am "bump version" 39 | $(MAKE) min 40 | npx ver minor 41 | $(MAKE) publish 42 | 43 | major: 44 | $(MAKE) lint 45 | cat save-csv.min.js | sed -E "s/v[0-9\.]+/v$$(npx semver -i major $(VERSION))/" > save-csv.min.js 46 | cat save-csv.js | sed -E "s/v[0-9\.]+/v$$(npx semver -i major $(VERSION))/" > save-csv.js 47 | git diff --exit-code &>/dev/null || git commit -am "bump version" 48 | $(MAKE) min 49 | npx ver major 50 | $(MAKE) publish 51 | 52 | .PHONY: lint test min publish patch minor major 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # save-csv 2 | [![](https://img.shields.io/npm/v/save-csv.svg?style=flat)](https://www.npmjs.org/package/save-csv) [![](https://img.shields.io/npm/dm/save-csv.svg)](https://www.npmjs.org/package/save-csv) [![](https://api.travis-ci.org/silverwind/save-csv.svg?style=flat)](https://travis-ci.org/silverwind/save-csv) 3 | > Download an array of objects as a CSV file in the browser 4 | 5 | `save-csv` is a tiny library (892 bytes gzipped) that creates an CSV file from a array of objects with matching keys and triggers a download in the browser. Features: 6 | 7 | - Automatically detects the value separator (usually `,`) based on the user's regional settings. 8 | - Saves UTF8 by default and helps Excel to recognize this by adding a [byte order mark](https://en.wikipedia.org/wiki/Byte_order_mark). 9 | - Fully configurable. Every output character can be modified via options. 10 | 11 | ## Example 12 | ```html 13 | 14 | ``` 15 | ```js 16 | saveCsv([ 17 | {a:1, b:2}, 18 | {a:3, b:4}, 19 | ]); 20 | ``` 21 | #### Output 22 | ```csv 23 | a,b 24 | 1,2 25 | 3,4 26 | ``` 27 | 28 | ## API 29 | ### save-csv(array, [options]) 30 | - `array` *Array*: An array containing objects with matching keys. 31 | - `options` *Object* 32 | - `filename` *string*: The filename to save to. Default: `export.csv`. 33 | - `sep` *string*: The value separator (usually `,`). Recognizes the special value `auto` with which automatic detection based on the user's regional settings is attempted (See [#1](https://github.com/silverwind/save-csv/issues/1)). Default: `auto`. 34 | - `eol` *string*: The line separator. Default: `\r\n`. 35 | - `quote` *string*: The quote character to use. Default: `"`. 36 | - `bom` *boolean*: Whether to include a [byte order mark](https://en.wikipedia.org/wiki/Byte_order_mark) in the output. Default: `true`. 37 | - `mime` *string*: The mime type for the file. Default: `text/csv;charset=utf-8`. 38 | - `formatter` *Function*: A custom formatter function for values. The default function handles `sep` in values and uses `JSON.stringify` for complex values. Receives `value`. 39 | 40 | © [silverwind](https://github.com/silverwind), distributed under BSD licence 41 | -------------------------------------------------------------------------------- /save-csv.js: -------------------------------------------------------------------------------- 1 | /*! save-csv v4.1.0 | (c) silverwind | BSD license */ 2 | (function(root, m) { 3 | if (typeof define === "function" && define.amd) { 4 | define([], m); 5 | } else if (typeof module === "object" && module.exports) { 6 | module.exports = m(); 7 | } else { 8 | root.saveCsv = m(); 9 | } 10 | })(typeof self !== "undefined" ? self : this, function() { 11 | "use strict"; 12 | return function(arr, opts) { 13 | if (!Array.isArray(arr) || !arr.length) { 14 | throw new Error("Expected an array of values, got " + arr); 15 | } 16 | 17 | opts = opts || {}; 18 | opts.filename = opts.filename || "export.csv"; 19 | opts.sep = opts.sep || "auto"; 20 | opts.eol = opts.eol || "\r\n"; 21 | opts.bom = typeof opts.bom === "boolean" ? opts.bom : true; 22 | opts.quote = opts.quote || '"'; 23 | opts.mime = opts.mime || "text/csv;charset=utf-8"; 24 | 25 | // This is not ideal, but given that Array.prototype.toLocaleString is 26 | // rather unreliable, it's a good compromise. 27 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1388777 28 | // https://bugs.chromium.org/p/chromium/issues/detail?id=753841 29 | if (opts.sep === "auto") { 30 | if ("toLocaleString" in Number.prototype) { 31 | opts.sep = (1.2).toLocaleString().substring(1, 2) === "," ? ";" : ","; 32 | } else { 33 | opts.sep = ","; 34 | } 35 | } 36 | 37 | var quoteRe = new RegExp(opts.quote, "g"); 38 | var sepRe = new RegExp(opts.sep, "g"); 39 | 40 | opts.formatter = opts.formatter || function(value) { 41 | var quoted = false; 42 | 43 | if (typeof value !== "string") { 44 | value = JSON.stringify(value) || ""; 45 | } 46 | 47 | // escape quotes by doubling the quotes and wrapping in quotes 48 | if (quoteRe.test(value)) { 49 | value = opts.quote + value.replace(quoteRe, opts.quote + opts.quote) + opts.quote; 50 | quoted = true; 51 | } 52 | 53 | // escape separator by wrapping in quotes 54 | if (sepRe.test(value) && !quoted) { 55 | value = opts.quote + value + opts.quote; 56 | } 57 | 58 | return value; 59 | }; 60 | 61 | // build headers from first element in array 62 | var paths = []; 63 | (function scan(prefix, obj, keys) { 64 | keys.forEach(function(key) { 65 | var path = prefix ? prefix + "." + key : key; 66 | if (typeof obj[key] === "object" && obj[key] !== null) { 67 | scan(path, obj[key], Object.keys(obj[key])); 68 | } else { 69 | paths.push(opts.formatter(path)); 70 | } 71 | }); 72 | })(null, arr[0], Object.keys(arr[0])); 73 | var header = paths.join(opts.sep) + opts.eol; 74 | 75 | // build body 76 | var body = arr.map(function(obj) { 77 | var row = []; 78 | (function scan(obj, keys) { 79 | keys.forEach(function(key) { 80 | if (typeof obj[key] === "object" && obj[key] !== null) { 81 | scan(obj[key], Object.keys(obj[key])); 82 | } else { 83 | row.push(opts.formatter(obj[key])); 84 | } 85 | }); 86 | })(obj, Object.keys(obj)); 87 | return row.join(opts.sep); 88 | }).join(opts.eol); 89 | 90 | // build a link and trigger a download 91 | var text = header + body; 92 | var blob = new Blob([opts.bom ? "\ufeff" + text : text]); 93 | 94 | if (window.navigator.msSaveOrOpenBlob) { // compat: ie10 95 | window.navigator.msSaveOrOpenBlob(blob, opts.filename); 96 | } else { 97 | var a = document.createElement("a"); 98 | a.setAttribute("href", URL.createObjectURL(blob, {type: opts.mime})); 99 | a.setAttribute("download", opts.filename); 100 | a.setAttribute("target", "_blank"); 101 | a.style.display = "none"; 102 | document.body.appendChild(a); 103 | a.click(); 104 | setTimeout(function() { 105 | document.body.removeChild(a); 106 | }, 0); 107 | } 108 | }; 109 | }); 110 | --------------------------------------------------------------------------------