/js/libs/jquery.csv.js
https://github.com/mcoulthurst/Alpha · JavaScript · 848 lines · 721 code · 30 blank · 97 comment · 27 complexity · 05bf9cc74a6ad4ef4096268db055f8f5 MD5 · raw file
- /**
- * jQuery-csv (jQuery Plugin)
- * version: 0.71 (2012-11-19)
- *
- * 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'
- /**
- * 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|\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 (m0 === '\n') {
- endOfValue();
- endOfEntry();
- break;
- }
- // phantom carriage return
- if (/^\r$/.test(m0)) {
- 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 (m0 === '\n') {
- endOfValue();
- endOfEntry();
- break;
- }
- // phantom carriage return
- if (/^\r$/.test(m0)) {
- 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 (m0 === '\n') {
- endOfValue();
- endOfEntry();
- break;
- }
- // phantom carriage return
- if (/^\r$/.test(m0)) {
- 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;
- }
- },
- /**
- * $.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,
- onParseEntry: options.onParseEntry,
- onParseValue: options.onParseValue,
- start: options.start,
- end: options.end,
- state: {
- rowNum: 1,
- colNum: 1
- }
- };
- // break the data down to lines
- data = $.csv.parsers.parse(csv, options);
- // 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,
- onParseEntry: options.onParseEntry,
- onParseValue: options.onParseValue,
- start: options.start,
- end: options.end,
- state: {
- rowNum: 1,
- colNum: 1
- },
- match: false
- };
- // fetch the headers
- var headerOptions = {
- delimiter: config.delimiter,
- separator: config.separator,
- start: 1,
- end: 1,
- state: {
- rowNum:1,
- colNum:1
- }
- }
- 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<len; i++) {
- var entry = $.csv.toArray(lines[i], options);
- var object = {};
- for(var j in headers) {
- object[headers[j]] = entry[j];
- }
- data.push(object);
-
- // update row state
- options.state.rowNum++;
- }
- // 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} array 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) {
- 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.escaper = 'escaper' in options ? options.escaper : $.csv.defaults.escaper;
- config.experimental = 'experimental' in options ? options.experimental : false;
- if(!config.experimental) {
- throw new Error('not implemented');
- }
- var output = [];
- for(i in arrays) {
- output.push(arrays[i]);
- }
- // 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(").
- *
- * 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.
- */
- fromObjects2CSV: 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.experimental = 'experimental' in options ? options.experimental : false;
- if(!config.experimental) {
- throw new Error('not implemented');
- }
- var output = [];
- for(i in objects) {
- output.push(arrays[i]);
- }
- // push the value to a callback if one is defined
- if(!config.callback) {
- return output;
- } else {
- config.callback('', output);
- }
- }
- };
- // Maintenance code to maintain backward-compatibility
- // Will be removed in release 1.0
- $.csvEntry2Array = $.csv.toArray;
- $.csv2Array = $.csv.toArrays;
- $.csv2Dictionary = $.csv.toObjects;
- })( jQuery );