/* eslint no-prototype-builtins: 0 */ /** * jQuery-csv (jQuery Plugin) * * This document is licensed as free software under the terms of the * MIT License: http://www.opensource.org/licenses/mit-license.php * * Acknowledgements: * The original design and influence to implement this library as a jquery * plugin is influenced by jquery-json (http://code.google.com/p/jquery-json/). * If you're looking to use native JSON.Stringify but want additional backwards * compatibility for browsers that don't support it, I highly recommend you * check it out. * * A special thanks goes out to rwk@acm.org for providing a lot of valuable * feedback to the project including the core for the new FSM * (Finite State Machine) parsers. If you're looking for a stable TSV parser * be sure to take a look at jquery-tsv (http://code.google.com/p/jquery-tsv/). * For legal purposes I'll include the "NO WARRANTY EXPRESSED OR IMPLIED. * USE AT YOUR OWN RISK.". Which, in 'layman's terms' means, by using this * library you are accepting responsibility if it breaks your code. * * Legal jargon aside, I will do my best to provide a useful and stable core * that can effectively be built on. * * Copyrighted 2012 by Evan Plaice. */ RegExp.escape = function (s) { return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') }; (function () { 'use strict' let $ // to keep backwards compatibility if (typeof jQuery !== 'undefined' && jQuery) { $ = jQuery } else { $ = {} } /** * jQuery.csv.defaults * Encapsulates the method paramater defaults for the CSV plugin module. */ $.csv = { defaults: { separator: ',', delimiter: '"', headers: true }, hooks: { castToScalar: function (value, state) { const hasDot = /\./ if (isNaN(value)) { return value } else { if (hasDot.test(value)) { return parseFloat(value) } else { const integer = parseInt(value) if (isNaN(integer)) { return null } else { return integer } } } } }, parsers: { parse: function (csv, options) { // cache settings const separator = options.separator const delimiter = options.delimiter // set initial state if it's missing if (!options.state.rowNum) { options.state.rowNum = 1 } if (!options.state.colNum) { options.state.colNum = 1 } // clear initial state const data = [] let entry = [] let state = 0 let value = '' let exit = false function endOfEntry () { // reset the state state = 0 value = '' // if 'start' hasn't been met, don't output if (options.start && options.state.rowNum < options.start) { // update global state entry = [] options.state.rowNum++ options.state.colNum = 1 return } if (options.onParseEntry === undefined) { // onParseEntry hook not set data.push(entry) } else { const hookVal = options.onParseEntry(entry, options.state) // onParseEntry Hook // false skips the row, configurable through a hook if (hookVal !== false) { data.push(hookVal) } } // console.log('entry:' + entry); // cleanup entry = [] // if 'end' is met, stop parsing if (options.end && options.state.rowNum >= options.end) { exit = true } // update global state options.state.rowNum++ options.state.colNum = 1 } function endOfValue () { if (options.onParseValue === undefined) { // onParseValue hook not set entry.push(value) } else if (options.headers && options.state.rowNum === 1) { // don't onParseValue object headers entry.push(value) } else { const hook = options.onParseValue(value, options.state) // onParseValue Hook // false skips the row, configurable through a hook if (hook !== false) { entry.push(hook) } } // console.log('value:' + value); // reset the state value = '' state = 0 // update global state options.state.colNum++ } // escape regex-specific control chars const escSeparator = RegExp.escape(separator) const escDelimiter = RegExp.escape(delimiter) // compile the regEx str using the custom delimiter/separator let match = /(D|S|\r\n|\n|\r|[^DS\r\n]+)/ let matchSrc = match.source matchSrc = matchSrc.replace(/S/g, escSeparator) matchSrc = matchSrc.replace(/D/g, escDelimiter) match = new RegExp(matchSrc, 'gm') // put on your fancy pants... // process control chars individually, use look-ahead on non-control chars csv.replace(match, function (m0) { if (exit) { return } switch (state) { // the start of a value case 0: // null last value if (m0 === separator) { value += '' endOfValue() break } // opening delimiter if (m0 === delimiter) { state = 1 break } // null last value if (/^(\r\n|\n|\r)$/.test(m0)) { endOfValue() endOfEntry() break } // un-delimited value value += m0 state = 3 break // delimited input case 1: // second delimiter? check further if (m0 === delimiter) { state = 2 break } // delimited data value += m0 state = 1 break // delimiter found in delimited input case 2: // escaped delimiter? if (m0 === delimiter) { value += m0 state = 1 break } // null value if (m0 === separator) { endOfValue() break } // end of entry if (/^(\r\n|\n|\r)$/.test(m0)) { endOfValue() endOfEntry() break } // broken paser? throw Error('CSVDataError: Illegal State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') // un-delimited input case 3: // null last value if (m0 === separator) { endOfValue() break } // end of entry if (/^(\r\n|\n|\r)$/.test(m0)) { endOfValue() endOfEntry() break } if (m0 === delimiter) { // non-compliant data throw Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') } // broken parser? throw Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') default: // shenanigans throw Error('CSVDataError: Unknown State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') } // console.log('val:' + m0 + ' state:' + state); }) // submit the last entry // ignore null last line if (entry.length !== 0) { endOfValue() endOfEntry() } return data }, // a csv-specific line splitter splitLines: function (csv, options) { if (!csv) { return undefined } options = options || {} // cache settings const separator = options.separator || $.csv.defaults.separator const delimiter = options.delimiter || $.csv.defaults.delimiter // set initial state if it's missing options.state = options.state || {} if (!options.state.rowNum) { options.state.rowNum = 1 } // clear initial state const entries = [] let state = 0 let entry = '' let exit = false function endOfLine () { // reset the state state = 0 // if 'start' hasn't been met, don't output if (options.start && options.state.rowNum < options.start) { // update global state entry = '' options.state.rowNum++ return } if (options.onParseEntry === undefined) { // onParseEntry hook not set entries.push(entry) } else { const hookVal = options.onParseEntry(entry, options.state) // onParseEntry Hook // false skips the row, configurable through a hook if (hookVal !== false) { entries.push(hookVal) } } // cleanup entry = '' // if 'end' is met, stop parsing if (options.end && options.state.rowNum >= options.end) { exit = true } // update global state options.state.rowNum++ } // escape regex-specific control chars const escSeparator = RegExp.escape(separator) const escDelimiter = RegExp.escape(delimiter) // compile the regEx str using the custom delimiter/separator let match = /(D|S|\n|\r|[^DS\r\n]+)/ let matchSrc = match.source matchSrc = matchSrc.replace(/S/g, escSeparator) matchSrc = matchSrc.replace(/D/g, escDelimiter) match = new RegExp(matchSrc, 'gm') // put on your fancy pants... // process control chars individually, use look-ahead on non-control chars csv.replace(match, function (m0) { if (exit) { return } switch (state) { // the start of a value/entry case 0: // null value if (m0 === separator) { entry += m0 state = 0 break } // opening delimiter if (m0 === delimiter) { entry += m0 state = 1 break } // end of line if (m0 === '\n') { endOfLine() break } // phantom carriage return if (/^\r$/.test(m0)) { break } // un-delimit value entry += m0 state = 3 break // delimited input case 1: // second delimiter? check further if (m0 === delimiter) { entry += m0 state = 2 break } // delimited data entry += m0 state = 1 break // delimiter found in delimited input case 2: { // escaped delimiter? const prevChar = entry.substr(entry.length - 1) if (m0 === delimiter && prevChar === delimiter) { entry += m0 state = 1 break } // end of value if (m0 === separator) { entry += m0 state = 0 break } // end of line if (m0 === '\n') { endOfLine() break } // phantom carriage return if (m0 === '\r') { break } // broken paser? throw Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']') } // un-delimited input case 3: // null value if (m0 === separator) { entry += m0 state = 0 break } // end of line if (m0 === '\n') { endOfLine() break } // phantom carriage return if (m0 === '\r') { break } // non-compliant data if (m0 === delimiter) { throw Error('CSVDataError: Illegal quote [Row:' + options.state.rowNum + ']') } // broken parser? throw Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']') default: // shenanigans throw Error('CSVDataError: Unknown state [Row:' + options.state.rowNum + ']') } // console.log('val:' + m0 + ' state:' + state); }) // submit the last entry // ignore null last line if (entry !== '') { endOfLine() } return entries }, // a csv entry parser parseEntry: function (csv, options) { // cache settings const separator = options.separator const delimiter = options.delimiter // set initial state if it's missing if (!options.state.rowNum) { options.state.rowNum = 1 } if (!options.state.colNum) { options.state.colNum = 1 } // clear initial state const entry = [] let state = 0 let value = '' function endOfValue () { if (options.onParseValue === undefined) { // onParseValue hook not set entry.push(value) } else { const hook = options.onParseValue(value, options.state) // onParseValue Hook // false skips the value, configurable through a hook if (hook !== false) { entry.push(hook) } } // reset the state value = '' state = 0 // update global state options.state.colNum++ } // checked for a cached regEx first if (!options.match) { // escape regex-specific control chars const escSeparator = RegExp.escape(separator) const escDelimiter = RegExp.escape(delimiter) // compile the regEx str using the custom delimiter/separator const match = /(D|S|\n|\r|[^DS\r\n]+)/ let matchSrc = match.source matchSrc = matchSrc.replace(/S/g, escSeparator) matchSrc = matchSrc.replace(/D/g, escDelimiter) options.match = new RegExp(matchSrc, 'gm') } // put on your fancy pants... // process control chars individually, use look-ahead on non-control chars csv.replace(options.match, function (m0) { switch (state) { // the start of a value case 0: // null last value if (m0 === separator) { value += '' endOfValue() break } // opening delimiter if (m0 === delimiter) { state = 1 break } // skip un-delimited new-lines if (m0 === '\n' || m0 === '\r') { break } // un-delimited value value += m0 state = 3 break // delimited input case 1: // second delimiter? check further if (m0 === delimiter) { state = 2 break } // delimited data value += m0 state = 1 break // delimiter found in delimited input case 2: // escaped delimiter? if (m0 === delimiter) { value += m0 state = 1 break } // null value if (m0 === separator) { endOfValue() break } // skip un-delimited new-lines if (m0 === '\n' || m0 === '\r') { break } // broken paser? throw Error('CSVDataError: Illegal State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') // un-delimited input case 3: // null last value if (m0 === separator) { endOfValue() break } // skip un-delimited new-lines if (m0 === '\n' || m0 === '\r') { break } // non-compliant data if (m0 === delimiter) { throw Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') } // broken parser? throw Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') default: // shenanigans throw Error('CSVDataError: Unknown State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']') } // console.log('val:' + m0 + ' state:' + state); }) // submit the last value endOfValue() return entry } }, helpers: { /** * $.csv.helpers.collectPropertyNames(objectsArray) * Collects all unique property names from all passed objects. * * @param {Array} objects Objects to collect properties from. * * Returns an array of property names (array will be empty, * if objects have no own properties). */ collectPropertyNames: function (objects) { let o = [] let propName = [] const props = [] for (o in objects) { for (propName in objects[o]) { if ((objects[o].hasOwnProperty(propName)) && (props.indexOf(propName) < 0) && (typeof objects[o][propName] !== 'function')) { props.push(propName) } } } return props } }, /** * $.csv.toArray(csv) * Converts a CSV entry string to a javascript array. * * @param {Array} csv The string containing the CSV data. * @param {Object} [options] An object containing user-defined options. * @param {Character} [separator] An override for the separator character. Defaults to a comma(,). * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote("). * * This method deals with simple CSV strings only. It's useful if you only * need to parse a single entry. If you need to parse more than one line, * use $.csv2Array instead. */ toArray: function (csv, options, callback) { // if callback was passed to options swap callback with options if (options !== undefined && typeof (options) === 'function') { if (callback !== undefined) { return console.error('You cannot 3 arguments with the 2nd argument being a function') } callback = options options = {} } options = (options !== undefined ? options : {}) const config = {} config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false) config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter const state = (options.state !== undefined ? options.state : {}) // setup options = { delimiter: config.delimiter, separator: config.separator, onParseEntry: options.onParseEntry, onParseValue: options.onParseValue, state: state } const entry = $.csv.parsers.parseEntry(csv, options) // push the value to a callback if one is defined if (!config.callback) { return entry } else { config.callback('', entry) } }, /** * $.csv.toArrays(csv) * Converts a CSV string to a javascript array. * * @param {String} csv The string containing the raw CSV data. * @param {Object} [options] An object containing user-defined options. * @param {Character} [separator] An override for the separator character. Defaults to a comma(,). * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote("). * * This method deals with multi-line CSV. The breakdown is simple. The first * dimension of the array represents the line (or entry/row) while the second * dimension contains the values (or values/columns). */ toArrays: function (csv, options, callback) { // if callback was passed to options swap callback with options if (options !== undefined && typeof (options) === 'function') { if (callback !== undefined) { return console.error('You cannot 3 arguments with the 2nd argument being a function') } callback = options options = {} } options = (options !== undefined ? options : {}) const config = {} config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false) config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter // setup let data = [] options = { delimiter: config.delimiter, separator: config.separator, onPreParse: options.onPreParse, onParseEntry: options.onParseEntry, onParseValue: options.onParseValue, onPostParse: options.onPostParse, start: options.start, end: options.end, state: { rowNum: 1, colNum: 1 } } // onPreParse hook if (options.onPreParse !== undefined) { csv = options.onPreParse(csv, options.state) } // parse the data data = $.csv.parsers.parse(csv, options) // onPostParse hook if (options.onPostParse !== undefined) { data = options.onPostParse(data, options.state) } // push the value to a callback if one is defined if (!config.callback) { return data } else { config.callback('', data) } }, /** * $.csv.toObjects(csv) * Converts a CSV string to a javascript object. * @param {String} csv The string containing the raw CSV data. * @param {Object} [options] An object containing user-defined options. * @param {Character} [separator] An override for the separator character. Defaults to a comma(,). * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote("). * @param {Boolean} [headers] Indicates whether the data contains a header line. Defaults to true. * * This method deals with multi-line CSV strings. Where the headers line is * used as the key for each value per entry. */ toObjects: function (csv, options, callback) { // if callback was passed to options swap callback with options if (options !== undefined && typeof (options) === 'function') { if (callback !== undefined) { return console.error('You cannot 3 arguments with the 2nd argument being a function') } callback = options options = {} } options = (options !== undefined ? options : {}) const config = {} config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false) config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter config.headers = 'headers' in options ? options.headers : $.csv.defaults.headers options.start = 'start' in options ? options.start : 1 // account for headers if (config.headers) { options.start++ } if (options.end && config.headers) { options.end++ } // setup let lines = [] let data = [] options = { delimiter: config.delimiter, separator: config.separator, onPreParse: options.onPreParse, onParseEntry: options.onParseEntry, onParseValue: options.onParseValue, onPostParse: options.onPostParse, start: options.start, end: options.end, state: { rowNum: 1, colNum: 1 }, match: false, transform: options.transform } // fetch the headers const headerOptions = { delimiter: config.delimiter, separator: config.separator, start: 1, end: 1, state: { rowNum: 1, colNum: 1 }, headers: true } // onPreParse hook if (options.onPreParse !== undefined) { csv = options.onPreParse(csv, options.state) } // parse the csv const headerLine = $.csv.parsers.splitLines(csv, headerOptions) const headers = $.csv.toArray(headerLine[0], headerOptions) // fetch the data lines = $.csv.parsers.splitLines(csv, options) // reset the state for re-use options.state.colNum = 1 if (headers) { options.state.rowNum = 2 } else { options.state.rowNum = 1 } // convert data to objects for (let i = 0, len = lines.length; i < len; i++) { const entry = $.csv.toArray(lines[i], options) const object = {} for (let j = 0; j < headers.length; j++) { object[headers[j]] = entry[j] } if (options.transform !== undefined) { data.push(options.transform.call(undefined, object)) } else { data.push(object) } // update row state options.state.rowNum++ } // onPostParse hook if (options.onPostParse !== undefined) { data = options.onPostParse(data, options.state) } // push the value to a callback if one is defined if (!config.callback) { return data } else { config.callback('', data) } }, /** * $.csv.fromArrays(arrays) * Converts a javascript array to a CSV String. * * @param {Array} arrays An array containing an array of CSV entries. * @param {Object} [options] An object containing user-defined options. * @param {Character} [separator] An override for the separator character. Defaults to a comma(,). * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote("). * * This method generates a CSV file from an array of arrays (representing entries). */ fromArrays: function (arrays, options, callback) { // if callback was passed to options swap callback with options if (options !== undefined && typeof (options) === 'function') { if (callback !== undefined) { return console.error('You cannot 3 arguments with the 2nd argument being a function') } callback = options options = {} } options = (options !== undefined ? options : {}) const config = {} config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false) config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter let output = '' for (let i = 0; i < arrays.length; i++) { const line = arrays[i] const lineValues = [] for (let j = 0; j < line.length; j++) { let strValue = (line[j] === undefined || line[j] === null) ? '' : line[j].toString() if (strValue.indexOf(config.delimiter) > -1) { strValue = strValue.replace(new RegExp(config.delimiter, 'g'), config.delimiter + config.delimiter) } let escMatcher = '\n|\r|S|D' escMatcher = escMatcher.replace('S', config.separator) escMatcher = escMatcher.replace('D', config.delimiter) if (strValue.search(escMatcher) > -1) { strValue = config.delimiter + strValue + config.delimiter } lineValues.push(strValue) } output += lineValues.join(config.separator) + '\n' } // push the value to a callback if one is defined if (!config.callback) { return output } else { config.callback('', output) } }, /** * $.csv.fromObjects(objects) * Converts a javascript dictionary to a CSV string. * * @param {Object} objects An array of objects containing the data. * @param {Object} [options] An object containing user-defined options. * @param {Character} [separator] An override for the separator character. Defaults to a comma(,). * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote("). * @param {Character} [sortOrder] Sort order of columns (named after * object properties). Use 'alpha' for alphabetic. Default is 'declare', * which means, that properties will _probably_ appear in order they were * declared for the object. But without any guarantee. * @param {Character or Array} [manualOrder] Manually order columns. May be * a strin in a same csv format as an output or an array of header names * (array items won't be parsed). All the properties, not present in * `manualOrder` will be appended to the end in accordance with `sortOrder` * option. So the `manualOrder` always takes preference, if present. * * This method generates a CSV file from an array of objects (name:value pairs). * It starts by detecting the headers and adding them as the first line of * the CSV file, followed by a structured dump of the data. */ fromObjects: function (objects, options, callback) { // if callback was passed to options swap callback with options if (options !== undefined && typeof (options) === 'function') { if (callback !== undefined) { return console.error('You cannot 3 arguments with the 2nd argument being a function') } callback = options options = {} } options = (options !== undefined ? options : {}) const config = {} config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false) config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter config.headers = 'headers' in options ? options.headers : $.csv.defaults.headers config.sortOrder = 'sortOrder' in options ? options.sortOrder : 'declare' config.manualOrder = 'manualOrder' in options ? options.manualOrder : [] config.transform = options.transform if (typeof config.manualOrder === 'string') { config.manualOrder = $.csv.toArray(config.manualOrder, config) } if (config.transform !== undefined) { const origObjects = objects objects = [] for (let i = 0; i < origObjects.length; i++) { objects.push(config.transform.call(undefined, origObjects[i])) } } let props = $.csv.helpers.collectPropertyNames(objects) if (config.sortOrder === 'alpha') { props.sort() } if (config.manualOrder.length > 0) { const propsManual = [].concat(config.manualOrder) for (let p = 0; p < props.length; p++) { if (propsManual.indexOf(props[p]) < 0) { propsManual.push(props[p]) } } props = propsManual } let line const output = [] let propName if (config.headers) { output.push(props) } for (let o = 0; o < objects.length; o++) { line = [] for (let p = 0; p < props.length; p++) { propName = props[p] if (propName in objects[o] && typeof objects[o][propName] !== 'function') { line.push(objects[o][propName]) } else { line.push('') } } output.push(line) } // push the value to a callback if one is defined return $.csv.fromArrays(output, options, config.callback) } } // Maintenance code to maintain backward-compatibility // Will be removed in release 1.0 $.csvEntry2Array = $.csv.toArray $.csv2Array = $.csv.toArrays $.csv2Dictionary = $.csv.toObjects // CommonJS module is defined if (typeof module !== 'undefined' && module.exports) { module.exports = $.csv } }).call(this)