/* global jQuery, _ */ var CSV = {}; // Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file) (function(my) { "use strict"; my.__type__ = "csv"; // use either jQuery or Underscore Deferred depending on what is available var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || (typeof _ !== "undefined" && _.Deferred) || function() { var resolve, reject; var promise = new Promise(function(res, rej) { resolve = res; reject = rej; }); return { resolve: resolve, reject: reject, promise: function() { return promise; } }; }; my.fetch = function(dataset) { var dfd = new Deferred(); if (dataset.file) { var reader = new FileReader(); var encoding = dataset.encoding || "UTF-8"; reader.onload = function(e) { var out = my.extractFields(my.parse(e.target.result, dataset), dataset); out.useMemoryStore = true; out.metadata = { filename: dataset.file.name }; dfd.resolve(out); }; reader.onerror = function(e) { dfd.reject({ error: { message: "Failed to load file. Code: " + e.target.error.code } }); }; reader.readAsText(dataset.file, encoding); } else if (dataset.data) { var out = my.extractFields(my.parse(dataset.data, dataset), dataset); out.useMemoryStore = true; dfd.resolve(out); } else if (dataset.url) { var fetch = window.fetch || function(url) { var jq = jQuery.get(url); var promiseResult = { then: function(res) { jq.done(res); return promiseResult; }, catch: function(rej) { jq.fail(rej); return promiseResult; } }; return promiseResult; }; fetch(dataset.url) .then(function(response) { if (response.text) { return response.text(); } else { return response; } }) .then(function(data) { var out = my.extractFields(my.parse(data, dataset), dataset); out.useMemoryStore = true; dfd.resolve(out); }) .catch(function(req, status) { dfd.reject({ error: { message: "Failed to load file. " + req.statusText + ". Code: " + req.status, request: req } }); }); } return dfd.promise(); }; // Convert array of rows in { records: [ ...] , fields: [ ... ] } // @param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data. my.extractFields = function(rows, noFields) { if (noFields.noHeaderRow !== true && rows.length > 0) { return { fields: rows[0], records: rows.slice(1) }; } else { return { records: rows }; } }; my.normalizeDialectOptions = function(options) { // note lower case compared to CSV DDF var out = { delimiter: ",", doublequote: true, lineterminator: "\n", quotechar: '"', skipinitialspace: true, skipinitialrows: 0 }; for (var key in options) { if (key === "trim") { out["skipinitialspace"] = options.trim; } else { out[key.toLowerCase()] = options[key]; } } return out; }; // ## parse // // For docs see the README // // Heavily based on uselesscode's JS CSV parser (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.parse = function(s, dialect) { // When line terminator is not provided then we try to guess it // and normalize it across the file. if (!dialect || (dialect && !dialect.lineterminator)) { s = my.normalizeLineTerminator(s, dialect); } // Get rid of any trailing \n var options = my.normalizeDialectOptions(dialect); s = chomp(s, options.lineterminator); var cur = "", // The character we are currently processing. inQuote = false, fieldQuoted = false, field = "", // Buffer for building up the current field row = [], out = [], i, processField; processField = function(field) { if (fieldQuoted !== true) { // If field is empty set to null if (field === "") { field = null; // If the field was not quoted and we are trimming fields, trim it } else if (options.skipinitialspace === true) { field = trim(field); } // Convert unquoted numbers to their appropriate types if (rxIsInt.test(field)) { field = parseInt(field, 10); } else if (rxIsFloat.test(field)) { field = parseFloat(field, 10); } } return field; }; for (i = 0; i < s.length; i += 1) { cur = s.charAt(i); // If we are at a EOF or EOR if ( inQuote === false && (cur === options.delimiter || cur === options.lineterminator) ) { field = processField(field); // Add the current field to the current row row.push(field); // If this is EOR append row to output and flush row if (cur === options.lineterminator) { out.push(row); row = []; } // Flush the field buffer field = ""; fieldQuoted = false; } else { // If it's not a quotechar, add it to the field buffer if (cur !== options.quotechar) { field += cur; } else { if (!inQuote) { // We are not in a quote, start a quote inQuote = true; fieldQuoted = true; } else { // Next char is quotechar, this is an escaped quotechar if (s.charAt(i + 1) === options.quotechar) { field += options.quotechar; // Skip the next char i += 1; } else { // It's not escaping, so end quote inQuote = false; } } } } } // Add the last field field = processField(field); row.push(field); out.push(row); // Expose the ability to discard initial rows if (options.skipinitialrows) out = out.slice(options.skipinitialrows); return out; }; my.normalizeLineTerminator = function(csvString, dialect) { dialect = dialect || {}; // Try to guess line terminator if it's not provided. if (!dialect.lineterminator) { return csvString.replace(/(\r\n|\n|\r)/gm, "\n"); } // if not return the string untouched. return csvString; }; my.objectToArray = function(dataToSerialize) { var a = []; var fieldNames = []; var fieldIds = []; for (var ii = 0; ii < dataToSerialize.fields.length; ii++) { var id = dataToSerialize.fields[ii].id; fieldIds.push(id); var label = dataToSerialize.fields[ii].label ? dataToSerialize.fields[ii].label : id; fieldNames.push(label); } a.push(fieldNames); for (var ii = 0; ii < dataToSerialize.records.length; ii++) { var tmp = []; var record = dataToSerialize.records[ii]; for (var jj = 0; jj < fieldIds.length; jj++) { tmp.push(record[fieldIds[jj]]); } a.push(tmp); } return a; }; // ## serialize // // See README for docs // // Heavily based on uselesscode's JS CSV serializer (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.serialize = function(dataToSerialize, dialect) { var a = null; if (dataToSerialize instanceof Array) { a = dataToSerialize; } else { a = my.objectToArray(dataToSerialize); } var options = my.normalizeDialectOptions(dialect); var cur = "", // The character we are currently processing. field = "", // Buffer for building up the current field row = "", out = "", i, j, processField; processField = function(field) { if (field === null) { // If field is null set to empty string field = ""; } else if (typeof field === "string" && rxNeedsQuoting.test(field)) { if (options.doublequote) { field = field.replace(/"/g, '""'); } // Convert string to delimited string field = options.quotechar + field + options.quotechar; } else if (typeof field === "number") { // Convert number to string field = field.toString(10); } return field; }; for (i = 0; i < a.length; i += 1) { cur = a[i]; for (j = 0; j < cur.length; j += 1) { field = processField(cur[j]); // If this is EOR append row to output and flush row if (j === cur.length - 1) { row += field; out += row + options.lineterminator; row = ""; } else { // Add the current field to the current row row += field + options.delimiter; } // Flush the field buffer field = ""; } } return out; }; var rxIsInt = /^\d+$/, rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/, // If a string has leading or trailing space, // contains a comma double quote or a newline // it needs to be quoted in CSV output rxNeedsQuoting = /^\s|\s$|,|"|\n/, trim = (function() { // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists if (String.prototype.trim) { return function(s) { return s.trim(); }; } else { return function(s) { return s.replace(/^\s*/, "").replace(/\s*$/, ""); }; } })(); function chomp(s, lineterminator) { if (s.charAt(s.length - lineterminator.length) !== lineterminator) { // Does not end with \n, just return string return s; } else { // Remove the \n return s.substring(0, s.length - lineterminator.length); } } })(CSV); // backwards compatability for use in Recline var recline = recline || {}; recline.Backend = recline.Backend || {}; recline.Backend.CSV = CSV; if (typeof module !== 'undefined' && module.exports) { module.exports = CSV; }