UNPKG

csvwriter

Version:

Create CSV from complex JSON objects with CLI or API

314 lines (264 loc) 11.5 kB
//The MIT License (MIT) // //Copyright (c) 2016 Sebastian Maurer // //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. var jsonpath = require('JSONPath'); var table = require("table/dist/table"); var chalk = require('chalk'); module.exports = csvwriter; /** * @typedef {object} csvParameters * @property {string} arrayDelimiter - delimiting character for arrays of primitives (strings, booleans, numbers), set to empty string ("") to disable flatting out primitive arrays (,) * @property {boolean} crlf - use line feed (\n) or carriage return + line feed (\r\n) as line separator (true) * @property {string} delimiter - delimiting character of the csv (,) * @property {string} decimalSeparator - the decimal mark to use for numbers (.) * @property {string} encoding - encoding used for the input and output (utf8) * @property {string} escape - character used to escape the delimiter, newlines and the escape character itself if quoting is disabled * @property {string} fields - specify a comma (,) separated list of fields to convert * @property {boolean} header - include a header as first line (true) * @property {string} path - jsonpath to apply on the object * @property {boolean} lineNumbers - insert a column of line numbers at the front of the output, useful when piping to grep or as a simple primary key (false) * @property {boolean} suppressLineBreaks - remove line breaks (\n) from field values (false) * @property {string} nestingDelimiter - delimiter used for nested fields of the input (.) * @property {number} maxDepth - maximum depth of the json object, fields below max-depth will not be included in the csv, use -1 (default) to include all fields, 0 will not include nested objects * @property {string} output - write to file, use - to write to stdout (default) * @property {string} quote - character used to quote strings in the csv (") * @property {boolean} doubleQuote - insert another quote to escape the quote character (true) * @property {string} nullString - string to use for writing null or undefined values * @property {boolean} table - create a neat looking table for the console (false) * @property {string} headerColor - color of the table header, one of: black, red, green, yellow, blue, magenta, cyan, white, gray (red) * @property {number} quoteMode - quoting style used in the csv: 0 = quote minimal (default), 1 = quote all, 2 = quote non-numeric, 3 = quote none * @property {boolean} utfBom - write utf bom (0xFEFF or 0xEFBBBF) in file if encoding is set to utf (true) * @property {boolean} zero - when interpreting or displaying column numbers, use zero-based numbering instead of the default 1-based numbering (false) */ /** * @callback csvCallback * @param {error} err - Error object or null if no error occurred. * @param {string} csv - The generated CSV as string. */ /** * Convert any JSON string to CSV with support for nested objects, filtering, many different CSV variations, CLI, ... * * @param {(string|object)} data - The source json data which should be converted. Can be a string or a javascript object. * @param {csvParameters} [params] - Configuration of the CSV generation. * @param {csvCallback} callback - Callback to handle the generated CSV string. */ function csvwriter(data, params, callback) { if (typeof params === 'function') { callback = params; params = undefined; } params = applyDefaults(params); if (typeof data !== 'object' && !(data instanceof Array)) { try { data = JSON.parse(data); } catch (err) { callback(err); return; } } if (params.path) { try { data = jsonpath({path:params.path, json:data}); } catch (err) { callback(err); return; } } if (!(data instanceof Array)) { data = [data]; } var columns = []; var rows = []; data.forEach(function (d) { rows.push(flatten(d, columns, params)); }); columns = params.fields ? params.fields.split(',') : columns; callback(null, params.table ? createCLITable(rows, columns, params) : createCSV(rows, columns, params)); } function applyDefaults(params) { params = params || {}; params.tabs = params.tabs || false; params.delimiter = params.tabs ? '\t' : (typeof params.delimiter === 'string' ? params.delimiter : ','); params.delimiterRegExp = escapeRegExp(params.delimiter); params.decimalSeparator = typeof params.decimalSeparator === 'string' ? params.decimalSeparator : '.'; params.arrayDelimiter = typeof params.arrayDelimiter === 'string' ? params.arrayDelimiter : ','; params.nestingDelimiter = typeof params.nestingDelimiter === 'string' ? params.nestingDelimiter : '.'; params.nullString = typeof params.nullString === 'string' ? params.nullString : ''; params.quote = typeof params.quote === 'string' ? params.quote : '"'; params.quoteRegExp = escapeRegExp(params.quote); params.escape = params.escape || null; params.escapeRegExp = params.escape !== null ? escapeRegExp(params.escape) : null; params.doubleQuote = params.doubleQuote !== false; params.suppressLineBreaks = params.suppressLineBreaks || false; params.quoteMode = params.quoteMode >= 0 && params.quoteMode <= 3 ? params.quoteMode : 0; params.lineNumbers = params.lineNumbers || false; params.zero = params.zero || false; params.path = params.path || null; params.fields = params.fields || null; params.maxDepth = typeof params.maxDepth === 'number' ? params.maxDepth : -1; params.header = params.header !== false; params.table = params.table || false; params.headerColor = typeof params.headerColor === 'string' ? params.headerColor : ''; params.crlf = params.crlf !== false; return params; } function escapeRegExp(string){ return new RegExp(string.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1'), 'g'); } function createCLITable(rows, columns, params) { if (params.lineNumbers) { columns = [''].concat(columns); } var tableData = []; if(params.header) { var colorFn = chalk[params.headerColor]; if(typeof colorFn === 'function') { tableData.push(columns.map(function(column) { return colorFn(column); })); } else { tableData.push(columns); } } rows.forEach(function (row, rowNum) { tableData.push(columns.map(function (column, colNum) { if (colNum === 0 && params.lineNumbers) { return params.zero ? rowNum : rowNum + 1; } return row.hasOwnProperty(column) ? row[column] : params.nullString; })); }); return table(tableData); } function createCSV(rows, columns, params) { var newline = params.crlf === false ? '\n' : '\r\n'; var csv = ''; var i, ii; if (columns.length && params.header) { if (params.lineNumbers) { csv += quote('', params) + params.delimiter; } for (i = 0; i < columns.length; i++) { if (i > 0) { csv += params.delimiter; } csv += quote(columns[i], params); } csv += newline; } for (i = 0; i < rows.length; i++) { if (params.lineNumbers) { csv += quote(params.zero ? i : i + 1, params) + params.delimiter; } for (ii = 0; ii < columns.length; ii++) { if (ii > 0) { csv += params.delimiter; } csv += quote(rows[i][columns[ii]], params); } csv += newline; } return csv; } function flatten(data, columns, params, path, row) { path = path || []; row = row || {}; if (params.maxDepth >= 0 && path.length > (params.maxDepth + 1)) { return row; } if (data instanceof Array) { flattenArray(data, columns, params, path, row); } else if (data instanceof Date) { addField(data.toISOString(), columns, params, path, row); } else if (typeof data === 'object') { flattenObject(data, columns, params, path, row); } else { addField(data, columns, params, path, row); } return row; } function flattenArray(data, columns, params, path, row) { if (params.arrayDelimiter && data.length > 0 && typeof data[0] !== 'object' && !(data[0] instanceof Array)) { flatten(data.join(params.arrayDelimiter), columns, params, path, row); } else { var i; for (i = 0; i < data.length; i++) { flatten(data[i], columns, params, path.concat(i), row); } } } function flattenObject(data, columns, params, path, row) { for (var key in data) { if (data.hasOwnProperty(key)) { flatten(data[key], columns, params, path.concat(key), row); } } } function addField(data, columns, params, path, row) { var field = path.join(params.nestingDelimiter); row[field] = data; if (columns.indexOf(field) === -1) { columns.push(field); } } function quote(field, params) { var str = escapeFieldValue(field, params); var needsQuoting = false; if (params.quoteMode === 1) { needsQuoting = true; } else if (params.quoteMode === 2) { needsQuoting = typeof field !== 'number'; } else if (params.quoteMode !== 3) { needsQuoting = str.indexOf(params.delimiter) !== -1 || str.indexOf(params.quote) !== -1 || str.indexOf('\r') !== -1 || str.indexOf('\n') !== -1; } return needsQuoting ? params.quote + str + params.quote : str; } function escapeFieldValue(field, params) { if (field === null || field === undefined) { return params.nullString; } var str = field.toString(); if (typeof field === 'number') { str = str.replace('.', params.decimalSeparator); } if (params.quoteMode !== 3) { if (params.doubleQuote) { str = str.replace(params.quoteRegExp, params.quote + params.quote); } } else if (params.escape) { str = str .replace(params.escapeRegExp, params.escape + params.escape) .replace(params.delimiterRegExp, params.escape + params.delimiter) .replace(/(\r?\n)/g, params.escape + '$1'); } if (params.suppressLineBreaks) { str = str .replace(/\r/g, '') .replace(/\n/g, ''); } return str; }