From c5152fc69159715c746bbbab1a771fff087be02f Mon Sep 17 00:00:00 2001 From: Joni Aaltonen Date: Tue, 10 Feb 2015 16:19:06 -0200 Subject: [PATCH] adds jquery.csv.js --- bower.json | 2 +- src/jquery.csv.js | 990 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 991 insertions(+), 1 deletion(-) create mode 100644 src/jquery.csv.js diff --git a/bower.json b/bower.json index e73a449..e19ac7d 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-worldskills-utils", - "version": "0.0.15", + "version": "0.0.16", "homepage": "https://github.com/worldskills/angular-worldskills-utils", "main": "src/angular-worldskills-utils.js", "license": "MIT", diff --git a/src/jquery.csv.js b/src/jquery.csv.js new file mode 100644 index 0000000..3dc0462 --- /dev/null +++ b/src/jquery.csv.js @@ -0,0 +1,990 @@ +/** + * jQuery-csv (jQuery Plugin) + * version: 0.70 (2012-11-04) + * + * 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 (undefined) { + 'use strict' + + var $; + + // 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) { + var hasDot = /\./; + if (isNaN(value)) { + return value; + } else { + if (hasDot.test(value)) { + return parseFloat(value); + } else { + var integer = parseInt(value); + if(isNaN(integer)) { + return null; + } else { + return integer; + } + } + } + } + }, + + parsers: { + parse: function(csv, options) { + // cache settings + var separator = options.separator; + var 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 + var data = []; + var entry = []; + var state = 0; + var value = ''; + var 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 { + var 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 { + var 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 + var escSeparator = RegExp.escape(separator); + var escDelimiter = RegExp.escape(delimiter); + + // compile the regEx str using the custom delimiter/separator + var match = /(D|S|\r\n|\n|\r|[^DS\r\n]+)/; + var matchSrc = match.source; + matchSrc = matchSrc.replace(/S/g, escSeparator); + matchSrc = matchSrc.replace(/D/g, escDelimiter); + match = 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 new 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 new Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']'); + } + // broken parser? + throw new Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']'); + default: + // shenanigans + throw new 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) { + // cache settings + var separator = options.separator; + var delimiter = options.delimiter; + + // set initial state if it's missing + if(!options.state.rowNum) { + options.state.rowNum = 1; + } + + // clear initial state + var entries = []; + var state = 0; + var entry = ''; + var 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 { + var 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 + var escSeparator = RegExp.escape(separator); + var escDelimiter = RegExp.escape(delimiter); + + // compile the regEx str using the custom delimiter/separator + var match = /(D|S|\n|\r|[^DS\r\n]+)/; + var matchSrc = match.source; + matchSrc = matchSrc.replace(/S/g, escSeparator); + matchSrc = matchSrc.replace(/D/g, escDelimiter); + match = 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? + var 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 new 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 new Error('CSVDataError: Illegal quote [Row:' + options.state.rowNum + ']'); + } + // broken parser? + throw new Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']'); + default: + // shenanigans + throw new 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 + var separator = options.separator; + var 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 + var entry = []; + var state = 0; + var value = ''; + + function endOfValue() { + if(options.onParseValue === undefined) { + // onParseValue hook not set + entry.push(value); + } else { + var 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 + var escSeparator = RegExp.escape(separator); + var escDelimiter = RegExp.escape(delimiter); + + // compile the regEx str using the custom delimiter/separator + var match = /(D|S|\n|\r|[^DS\r\n]+)/; + var matchSrc = match.source; + matchSrc = matchSrc.replace(/S/g, escSeparator); + matchSrc = matchSrc.replace(/D/g, escDelimiter); + options.match = 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 new 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 new Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']'); + } + // broken parser? + throw new Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']'); + default: + // shenanigans + throw new 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) { + + var o, propName, 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) { + var options = (options !== undefined ? options : {}); + var 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; + var state = (options.state !== undefined ? options.state : {}); + + // setup + var options = { + delimiter: config.delimiter, + separator: config.separator, + onParseEntry: options.onParseEntry, + onParseValue: options.onParseValue, + state: state + } + + var 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) { + var options = (options !== undefined ? options : {}); + var 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 + var data = []; + var 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) { + options.onPreParse(csv, options.state); + } + + // parse the data + data = $.csv.parsers.parse(csv, options); + + // onPostParse hook + if(options.onPostParse !== undefined) { + 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) { + var options = (options !== undefined ? options : {}); + var 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 + var lines = []; + var data = []; + + var 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 + var headerOptions = { + delimiter: config.delimiter, + separator: config.separator, + start: 1, + end: 1, + state: { + rowNum:1, + colNum:1 + } + } + + // onPreParse hook + if(options.onPreParse !== undefined) { + options.onPreParse(csv, options.state); + } + + // parse the csv + var headerLine = $.csv.parsers.splitLines(csv, headerOptions); + var headers = $.csv.toArray(headerLine[0], options); + + // fetch the data + var 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(var i=0, len=lines.length; i -1) { + var delRegex = new RegExp(config.delimiter, "g"); + strValue = strValue.replace(delRegex, config.delimiter + config.delimiter); + } + + var 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) + '\r\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) { + var options = (options !== undefined ? options : {}); + var 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; + + // added by ERIC, return just the arrays + config.justArrays = 'justArrays' in options ? options.justArrays : false; + + if (typeof config.manualOrder === 'string') { + config.manualOrder = $.csv.toArray(config.manualOrder, config); + } + + if (config.transform !== undefined) { + var origObjects = objects; + objects = []; + + var i; + for (i = 0; i < origObjects.length; i++) { + objects.push(config.transform.call(undefined, origObjects[i])); + } + } + + var props = $.csv.helpers.collectPropertyNames(objects); + + if (config.sortOrder === 'alpha') { + props.sort(); + } // else {} - nothing to do for 'declare' order + + if (config.manualOrder.length > 0) { + + var propsManual = [].concat(config.manualOrder); + var p; + for (p = 0; p < props.length; p++) { + if (propsManual.indexOf( props[p] ) < 0) { + propsManual.push( props[p] ); + } + } + props = propsManual; + } + + var o, p, line, output = [], propName; + if (config.headers) { + output.push(props); + } + + for (o = 0; o < objects.length; o++) { + line = []; + for (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); + } + + // modification by ERIC - just give me the arrays you made out of the object + if (config.justArrays) + return output; + + // 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 ); \ No newline at end of file