diff --git a/.gitignore b/.gitignore index 804eb70..358e9e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -bower_components/* -node_modules/* \ No newline at end of file +_gitignore/ +bower_components/ +node_modules/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0d04dcd --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Matthew Holt + +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. diff --git a/Parser.js b/Parser.js deleted file mode 100644 index c0ae88c..0000000 --- a/Parser.js +++ /dev/null @@ -1,306 +0,0 @@ -/** - Papa Parse 3.0 alpha - core parser function - (c) 2014 Matthew Holt. - Not for use in production or redistribution. - For development of Papa Parse only. -**/ -function Parser(config) -{ - var self = this; - var BYTE_ORDER_MARK = "\ufeff"; - var EMPTY = /^\s*$/; - - // Delimiters that are not allowed - var _badDelimiters = ["\r", "\n", "\"", BYTE_ORDER_MARK]; - - var _input; // The input text being parsed - var _delimiter; // The delimiting character - var _comments; // Comment character (default '#') or boolean - var _step; // The step (streaming) function - var _callback; // The callback to invoke when finished - var _preview; // Maximum number of lines (not rows) to parse - var _ch; // Current character - var _i; // Current character's positional index - var _inQuotes; // Whether in quotes or not - var _lineNum; // Current line number (1-based indexing) - var _data; // Parsed data (results) - var _errors; // Parse errors - var _rowIdx; // Current row index within results (0-based) - var _colIdx; // Current col index within result row (0-based) - var _aborted; // Abort flag - var _paused; // Pause flag - - // Unpack the config object - config = config || {}; - _delimiter = config.delimiter; - _comments = config.comments; - _step = config.step; - _callback = config.complete; - _preview = config.preview; - - // Delimiter integrity check - if (typeof _delimiter !== 'string' - || _delimiter.length != 1 - || _badDelimiters.indexOf(_delimiter) > -1) - _delimiter = ","; - - // Comment character integrity check - if (_comments === true) - _comments = "#"; - else if (typeof _comments !== 'string' - || _comments.length != 1 - || _badDelimiters.indexOf(_comments) > -1 - || _comments == _delimiter) - _comments = false; - - // Parses delimited text input - this.parse = function(input) - { - if (typeof input !== 'string') - throw "Input must be a string"; - reset(input); - return parserLoop(); - }; - - this.pause = function() - { - _paused = true; - }; - - this.resume = function() - { - _paused = false; - if (_i < _input.length) - return parserLoop(); - }; - - this.abort = function() - { - _aborted = true; - }; - - function parserLoop() - { - while (_i < _input.length) - { - if (_aborted) break; - if (_preview > 0 && _rowIdx >= _preview) break; - if (_paused) return; - - if (_ch == '"') - parseQuotes(); - else if (_inQuotes) - parseInQuotes(); - else - parseNotInQuotes(); - - nextChar(); - } - - return finishParsing(); - } - - function nextChar() - { - _i++; - _ch = _input[_i]; - } - - function finishParsing() - { - if (_inQuotes) - addError("Quotes", "MissingQuotes", "Unescaped or mismatched quotes"); - - endRow(); // End of input is also end of the last row - - if (typeof _step !== 'function') - return returnable(); - else if (typeof _callback === 'function') - _callback(); - } - - function parseQuotes() - { - if (quotesOnBoundary() && !quotesEscaped()) - _inQuotes = !_inQuotes; - else - { - saveChar(); - if (_inQuotes && quotesEscaped()) - _i++ - else - addError("Quotes", "UnexpectedQuotes", "Unexpected quotes"); - } - } - - function parseInQuotes() - { - saveChar(); - if (twoCharLineBreak()) - { - nextChar(); - saveChar(); - _lineNum++; - } - else if (oneCharLineBreak()) - _lineNum++; - } - - function parseNotInQuotes() - { - if (_ch == _delimiter) - newField(); - else if (twoCharLineBreak()) - { - newRow(); - nextChar(); - } - else if (oneCharLineBreak()) - newRow(); - else if (isCommentStart()) - skipLine(); - else - saveChar(); - } - - function isCommentStart() - { - var firstCharOfLine = _i == 0 - || oneCharLineBreak(_i-1) - || twoCharLineBreak(_i-2); - return firstCharOfLine && _input[_i] === _comments; - } - - function skipLine() - { - while (!twoCharLineBreak() - && !oneCharLineBreak() - && _i < _input.length) - { - nextChar(); - } - } - - function saveChar() - { - _data[_rowIdx][_colIdx] += _ch; - } - - function newField() - { - _data[_rowIdx].push(""); - _colIdx = _data[_rowIdx].length - 1; - } - - function newRow() - { - endRow(); - - _lineNum++; - _data.push([]); - _rowIdx = _data.length - 1; - newField(); - } - - function endRow() - { - trimEmptyLastRow(); - if (typeof _step === 'function') - { - if (_data[_rowIdx]) - _step(returnable(), self); - clearErrorsAndData(); - } - } - - function trimEmptyLastRow() - { - if (_data[_rowIdx].length == 1 && EMPTY.test(_data[_rowIdx][0])) - { - _data.splice(_rowIdx, 1); - _rowIdx = _data.length - 1; - } - } - - function twoCharLineBreak(i) - { - if (typeof i !== 'number') - i = _i; - return i < _input.length - 1 && - ((_input[i] == "\r" && _input[i+1] == "\n") - || (_input[i] == "\n" && _input[i+1] == "\r")) - } - - function oneCharLineBreak(i) - { - if (typeof i !== 'number') - i = _i; - return _input[i] == "\r" || _input[i] == "\n"; - } - - function quotesEscaped() - { - // Quotes as data cannot be on boundary, for example: ,"", are not escaped quotes - return !quotesOnBoundary() && _i < _input.length - 1 && _input[_i+1] == '"'; - } - - function quotesOnBoundary() - { - return isBoundary(_i-1) || isBoundary(_i+1); - } - - function isBoundary(i) - { - if (typeof i != 'number') - i = _i; - - var ch = _input[i]; - - return (i == -1 || i == _input.length) - || (i < _input.length - && i > -1 - && (ch == _delimiter - || ch == "\r" - || ch == "\n")); - } - - function addError(type, code, msg) - { - _errors.push({ - type: type, - code: code, - message: msg, - line: _lineNum, - row: _rowIdx, - index: _i - }); - } - - function reset(input) - { - _input = input; - _inQuotes = false; - _lineNum = 1; - _i = 0; - clearErrorsAndData(); - _data = [ [""] ]; // starting parsing requires an empty field - _ch = _input[_i]; - } - - function clearErrorsAndData() - { - _data = []; - _errors = []; - _rowIdx = 0; - _colIdx = 0; - } - - function returnable() - { - return { - data: _data, - errors: _errors, - lines: _lineNum - }; - } -} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b85ac4 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +Parse CSV with Javascript +======================================== + +Papa Parse (formerly the jQuery Parse Plugin) is a robust and powerful CSV (character-separated values) parser with these features: + +- Easily parses delimited text strings with any delimiter +- Parse CSV files directly (local or over the network) +- Stream large files (even via HTTP) +- Auto-detects the delimiter +- Worker threads to keep your web page responsive +- Header row support +- Can convert numbers and booleans to their types +- Graceful and robust error handling +- jQuery integration to easily parse files from `` elements + +All are optional (except for being easy to use). + + + +Demo +---- + +Visit **[PapaParse.com#demo](http://papaparse.com/#demo)** to try Papa! + + + +Get Started +----------- + +Use [papaparse.min.js](https://github.com/mholt/jquery.parse/blob/master/papaparse.min.js) for production. + +For usage instructions, see the [homepage](http://papaparse.com) and, for more detail, the [documentation](http://papaparse.com/docs.html). + + + +Tests +----- + +Papa Parse, especially its core Parser, is under test. Download this repository and open `tests/tests.html` in your browser to run them. + + + +Contributing +------------ + +To discuss a new feature or if you have a question, open an issue. To fix a bug, submit a pull request and get credited in the [contributors](https://github.com/mholt/jquery.parse/graphs/contributors)! Remember, a pull request, *with test*, is best. (Especially all changes to the Parser component should be validated with tests.) You may also discuss on Twitter with [#PapaParse](https://twitter.com/search?q=%23PapaParse&src=typd&f=realtime) or directly to me, [@mholt6](https://twitter.com/mholt6). + + + +Origins +------- + +Papa Parse is the result of a successful experiment by [SmartyStreets](http://smartystreets.com) which matured into a fully-featured, independent Javascript library. diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..a9bfa02 --- /dev/null +++ b/bower.json @@ -0,0 +1,34 @@ +{ + "name": "Papa-Parse", + "main": "papaparse.js", + "homepage": "http://papaparse.com", + "authors": [ + "Matthew Holt" + ], + "description": "Papa is a powerful CSV (delimited text) parser", + "keywords": [ + "csv", + "parse", + "parsing", + "parser", + "delimited", + "text", + "data", + "auto-detect", + "comma", + "tab", + "pipe", + "file", + "filereader", + "stream" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "player" + ] +} diff --git a/papaparse.js b/papaparse.js new file mode 100644 index 0000000..6e13287 --- /dev/null +++ b/papaparse.js @@ -0,0 +1,1218 @@ +/* + Papa Parse + v3.0.0 alpha - MAY BE UNSTABLE - STILL IN DEVELOPMENT + https://github.com/mholt/PapaParse +*/ +(function(global) +{ + "use strict"; + + var IS_WORKER = !global.document, SCRIPT_PATH; + var workers = {}, workerIdCounter = 0; + + // A configuration object from which to draw default settings + var DEFAULTS = { + delimiter: "", // empty: auto-detect + header: false, + dynamicTyping: false, + preview: 0, + step: undefined, + encoding: "", // browser should default to "UTF-8" + worker: false, + comments: false, + complete: undefined, + download: false + }; + + global.Papa = {}; + + global.Papa.parse = CsvToJson; + global.Papa.unparse = JsonToCsv; + + global.Papa.RECORD_SEP = String.fromCharCode(30); + global.Papa.UNIT_SEP = String.fromCharCode(31); + global.Papa.BYTE_ORDER_MARK = "\ufeff"; + global.Papa.BAD_DELIMITERS = ["\r", "\n", "\"", global.Papa.BYTE_ORDER_MARK]; + global.Papa.WORKERS_SUPPORTED = !!global.Worker; + + if (global.jQuery) + { + var $ = global.jQuery; + $.fn.parse = function(options) + { + var config = options.config || {}; + var queue = []; + + this.each(function(idx) + { + var supported = $(this).prop('tagName').toUpperCase() == "INPUT" + && $(this).attr('type').toLowerCase() == "file" + && global.FileReader; + + if (!supported) + return true; // continue to next input element + + var instanceConfig = $.extend({}, config); + + if (!this.files || this.files.length == 0) + return true; // continue to next input element + + for (var i = 0; i < this.files.length; i++) + { + queue.push({ + file: this.files[i], + inputElem: this, + instanceConfig: instanceConfig + }); + } + }); + + parseNextFile(); + return this; // chainability + + function parseNextFile() + { + if (queue.length == 0) + { + options.complete(); + return; + } + + var f = queue[0]; + + if (isFunction(options.before)) + { + var returned = options.before(f.file, f.inputElem); + + if (typeof returned === 'object') + { + if (returned.action == "abort") + { + error("AbortError", f.file, f.inputElem, returned.reason); + return; // Aborts all queued files immediately + } + else if (returned.action == "skip") + { + fileComplete(); // parse the next file in the queue, if any + return; + } + else if (typeof returned.config === 'object') + f.instanceConfig = $.extend(f.instanceConfig, returned.config); + } + else if (returned == "skip") + { + fileComplete(); // parse the next file in the queue, if any + return; + } + } + + // Wrap up the user's complete callback, if any, so that ours also gets executed + var userCompleteFunc = f.instanceConfig.complete; + f.instanceConfig.complete = function(results) { + if (isFunction(userCompleteFunc)) + userCompleteFunc(results, f.file, f.inputElem, event); + fileComplete(); + }; + + Papa.parse(f.file, f.instanceConfig); + } + + function error(name, file, elem, reason) + { + if (isFunction(options.error)) + options.error({name: name}, file, elem, reason); + } + + function fileComplete() + { + queue.splice(0, 1); + parseNextFile(); + } + } + } + + + if (IS_WORKER) + global.onmessage = workerThreadReceivedMessage; + else if (Papa.WORKERS_SUPPORTED) + SCRIPT_PATH = getScriptPath(); + + + + + function CsvToJson(_input, _config) + { + var config = IS_WORKER ? _config : copyAndValidateConfig(_config); + var useWorker = config.worker && Papa.WORKERS_SUPPORTED; + + if (useWorker) + { + var w = newWorker(); + + w.userStep = config.step; + w.userComplete = config.complete; + + config.step = isFunction(config.step); + config.complete = isFunction(config.complete); + delete config.worker; // prevent infinite loop + + w.postMessage({ + input: _input, + config: config, + workerId: w.id + }); + } + else + { + if (typeof _input === 'string') + { + if (config.download) + { + var streamer = new NetworkStreamer(config); + streamer.stream(_input); + } + else + { + if (IS_WORKER && config.step) + { + config.step = function(results) + { + global.postMessage({ + workerId: Papa.WORKER_ID, + results: results, + finished: false + }); + }; + } + var ph = new ParserHandle(config); + var results = ph.parse(_input); + if (isFunction(config.complete)) + config.complete(results); + return results; + } + } + else if (_input instanceof File) + { + if (config.step) + { + var streamer = new FileStreamer(config); + streamer.stream(_input); + } + else + { + var ph = new ParserHandle(config); + + if (IS_WORKER) + { + var reader = new FileReaderSync(); + var input = reader.readAsText(_input, config.encoding); + return ph.parse(input); + } + else + { + reader = new FileReader(); + reader.onload = function(event) + { + var ph = new ParserHandle(config); + var results = ph.parse(event.target.result); + if (isFunction(config.complete)) + config.complete(results); + }; + reader.readAsText(_input, config.encoding); + } + } + } + } + } + + + + + + + function JsonToCsv(_input) + { + var _output = ""; + var _fields = []; + var _delimiter = ","; + var _quotes = false; // whether to surround every datum with quotes + var _newline = "\r\n"; + + if (typeof _input === "string") + _input = JSON.parse(_input); + + if (_input instanceof Array) + { + if (!_input.length || _input[0] instanceof Array) + return serialize(null, _input); + else if (typeof _input[0] === 'object') + return serialize(objectKeys(_input[0]), _input); + } + else if (typeof _input === 'object') + { + if (typeof _input.data === 'string') + _input.data = JSON.parse(_input.data); + + if (_input.data instanceof Array) + { + if (typeof _input.delimiter === 'string' + && _input.delimiter.length == 1 + && global.Papa.BAD_DELIMITERS.indexOf(_input.delimiter) == -1) + { + _delimiter = _input.delimiter; + } + + if (typeof _input.quotes === 'boolean') + _quotes = _input.quotes; + + if (typeof _input.newline === 'string') + _newline = _input.newline; + + if (!_input.fields) + _input.fields = _input.data[0] instanceof Array + ? _input.fields + : objectKeys(_input.data[0]); + + if (!(_input.data[0] instanceof Array) && typeof _input.data[0] !== 'object') + _input.data = [_input.data]; // handles input like [1,2,3] or ["asdf"] + + return serialize(_input.fields, _input.data); + } + } + + // Default (any valid paths should return before this) + throw "exception: Unable to serialize unrecognized input"; + + + + // Turns an object's keys into an array + function objectKeys(obj) + { + if (typeof obj !== 'object') + return []; + var keys = []; + for (var key in obj) + keys.push(key); + return keys; + } + + // The double for loop that iterates the data and writes out a CSV string including header row + function serialize(fields, data) + { + var csv = ""; + + if (typeof fields === 'string') + fields = JSON.parse(fields); + if (typeof data === 'string') + data = JSON.parse(data); + + var hasHeader = fields instanceof Array && fields.length > 0; + var dataKeyedByField = !(data[0] instanceof Array); + + // If there a header row, write it first + if (hasHeader) + { + for (var i = 0; i < fields.length; i++) + { + if (i > 0) + csv += _delimiter; + csv += safe(fields[i]); + } + if (data.length > 0) + csv += _newline; + } + + // Then write out the data + for (var row = 0; row < data.length; row++) + { + var maxCol = hasHeader ? fields.length : data[row].length; + + for (var col = 0; col < maxCol; col++) + { + if (col > 0) + csv += _delimiter; + var colIdx = hasHeader && dataKeyedByField ? fields[col] : col; + csv += safe(data[row][colIdx]); + } + + if (row < data.length - 1) + csv += _newline; + } + + return csv; + } + + // Encloses a value around quotes if needed (makes a value safe for CSV insertion) + function safe(str) + { + if (typeof str === "undefined") + return ""; + + str = str.toString().replace(/"/g, '""'); + + var needsQuotes = _quotes + || hasAny(str, global.Papa.BAD_DELIMITERS) + || str.indexOf(_delimiter) > -1 + || str.charAt(0) == ' ' + || str.charAt(str.length - 1) == ' '; + + return needsQuotes ? '"' + str + '"' : str; + } + + function hasAny(str, substrings) + { + for (var i = 0; i < substrings.length; i++) + if (str.indexOf(substrings[i]) > -1) + return true; + return false; + } + } + + + + + + function NetworkStreamer(config) + { + config = config || {}; + if (!config.chunkSize) + config.chunkSize = 1024 * 1024 * 5; // 5 MB + + var start = 0; + var aggregate = ""; + var partialLine = ""; + var xhr, nextChunk; + var handle = new ParserHandle(copy(config)); + + this.stream = function(url) + { + // TODO: Pull this setup out of the streamer and have reader, nextChunk and chunkLoaded passed in? + if (IS_WORKER) + { + nextChunk = function() + { + readChunk(); + chunkLoaded(); + }; + } + else + { + nextChunk = function() + { + readChunk(); + }; + } + + nextChunk(); // Starts streaming + + + function readChunk() + { + xhr = new XMLHttpRequest(); + if (!IS_WORKER) + { + xhr.onload = chunkLoaded; + xhr.onerror = chunkError; + } + xhr.open("GET", url, !IS_WORKER); + if (config.step) + { + var end = start + config.chunkSize - 1; // minus one because byte range is inclusive + xhr.setRequestHeader("Range", "bytes="+start+"-"+end); + } + xhr.send(); + start += config.chunkSize; + return xhr.responseText; + } + + function chunkLoaded() + { + if (xhr.readyState != 4) + return; + + // Rejoin the line we likely just split in two by chunking the file + aggregate += partialLine + xhr.responseText; + partialLine = ""; + + var finishedWithEntireFile = !config.step || xhr.responseText.length < config.chunkSize; + + if (!finishedWithEntireFile) + { + var lastLineEnd = aggregate.lastIndexOf("\n"); + + if (lastLineEnd < 0) + lastLineEnd = aggregate.lastIndexOf("\r"); + + if (lastLineEnd > -1) + { + partialLine = aggregate.substring(lastLineEnd + 1); // skip the line ending character + aggregate = aggregate.substring(0, lastLineEnd); + } + else + { + // For chunk sizes smaller than a line (a line could not fit in a single chunk) + // we simply build our aggregate by reading in the next chunk, until we find a newline + nextChunk(); + return; + } + } + + var results = handle.parse(aggregate); + aggregate = ""; // very important to reset each time we parse an aggregate + + if (IS_WORKER) + { + global.postMessage({ + results: results, + workerId: Papa.WORKER_ID, + finished: finishedWithEntireFile + }); + } + + if (finishedWithEntireFile) + { + if (isFunction(config.complete)) + config.complete(undefined); + } + else if (results.meta.aborted && isFunction(config.complete)) + config.complete(results); + else + nextChunk(); + } + + function chunkError() + { + if (isFunction(config.error)) + config.error(reader.error, file); + } + }; + } + + + + + + + + + + function FileStreamer(config) + { + config = config || {}; + if (!config.chunkSize) + config.chunkSize = 1024 * 1024 * 10; // 10 MB + + var start = 0; + var aggregate = ""; + var partialLine = ""; + var reader, nextChunk, slice; + var handle = new ParserHandle(copy(config)); + + this.stream = function(file) + { + var slice = file.slice || file.webkitSlice || file.mozSlice; // TODO: Why doesn't this work? + + // TODO: Pull this setup out of the streamer and have reader, nextChunk and chunkLoaded passed in? + if (IS_WORKER) + { + reader = new FileReaderSync(); + nextChunk = function() + { + if (start < file.size) + chunkLoaded({ + target: { // simulate the structure of a FileReader event + result: readChunk() + } + }); + }; + } + else + { + reader = new FileReader(); + reader.onload = chunkLoaded; + reader.onerror = chunkError; + + nextChunk = function() + { + if (start < file.size) + readChunk(); + }; + } + + nextChunk(); // Starts streaming + + + + + function readChunk() + { + var end = Math.min(start + config.chunkSize, file.size); + var txt = reader.readAsText(slice.call(file, start, end), config.encoding); + start += config.chunkSize; + return txt; + } + + function chunkLoaded(event) + { + // Rejoin the line we likely just split in two by chunking the file + aggregate += partialLine + event.target.result; + partialLine = ""; + + var finishedWithEntireFile = start >= file.size; + + if (!finishedWithEntireFile) + { + var lastLineEnd = aggregate.lastIndexOf("\n"); + + if (lastLineEnd < 0) + lastLineEnd = aggregate.lastIndexOf("\r"); + + if (lastLineEnd > -1) + { + partialLine = aggregate.substring(lastLineEnd + 1); // skip the line ending character + aggregate = aggregate.substring(0, lastLineEnd); + } + else + { + // For chunk sizes smaller than a line (a line could not fit in a single chunk) + // we simply build our aggregate by reading in the next chunk, until we find a newline + nextChunk(); + return; + } + } + + var results = handle.parse(aggregate); + aggregate = ""; // very important to reset each time we parse an aggregate + + if (IS_WORKER) + { + global.postMessage({ + results: results, + workerId: Papa.WORKER_ID, + finished: finishedWithEntireFile + }); + } + + if (finishedWithEntireFile) + { + if (isFunction(config.complete)) + config.complete(undefined, file); + } + else if (results.meta.aborted && isFunction(config.complete)) + config.complete(results); + else if (!results.meta.paused) + nextChunk(); + } + + function chunkError() + { + if (isFunction(config.error)) + config.error(reader.error, file); + } + }; + } + + + + + + // Use one ParserHandle per entire CSV file or string + function ParserHandle(_config) + { + // One goal is to minimize the use of regular expressions... + var FLOAT = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i; + + var _fields = []; // Fields are from the header row of the input, if there is one + var _results = { // The last results returned from the parser + data: [], + errors: [], + meta: {} + }; + _config = copy(_config); + + this.parse = function(input) + { + if (!_config.delimiter) + { + var delimGuess = guessDelimiter(input); + if (delimGuess.successful) + _config.delimiter = delimGuess.bestDelimiter; + else + { + addError("Delimiter", "UndetectableDelimiter", "Unable to auto-detect delimiting character; defaulted to comma"); + _config.delimiter = ","; + } + _results.meta.delimiter = _config.delimiter; + } + + var parser = new Parser(_config); + + _results = parser.parse(input); + + if (needsHeaderRow()) + fillHeaderFields(); + + var results = applyHeaderAndDynamicTyping(); + + return results; + }; + + function guessDelimiter(input) + { + var delimChoices = [",", "\t", "|", ";", Papa.RECORD_SEP, Papa.UNIT_SEP]; + var bestDelim, bestDelta, fieldCountPrevRow; + + for (var i = 0; i < delimChoices.length; i++) + { + var delim = delimChoices[i]; + var delta = 0, avgFieldCount = 0; + fieldCountPrevRow = undefined; + + var preview = new Parser({ + delimiter: delim, + preview: 10 + }).parse(input); + + for (var j = 0; j < preview.data.length; j++) + { + var fieldCount = preview.data[j].length; + avgFieldCount += fieldCount; + + if (typeof fieldCountPrevRow === 'undefined') + { + fieldCountPrevRow = fieldCount; + continue; + } + else if (fieldCount > 1) + { + delta += Math.abs(fieldCount - fieldCountPrevRow); + fieldCountPrevRow = fieldCount; + } + } + + avgFieldCount /= preview.data.length; + + if ((typeof bestDelta === 'undefined' || delta < bestDelta) + && avgFieldCount > 1.99) + { + bestDelta = delta; + bestDelim = delim; + } + } + + _config.delimiter = bestDelim; + + return { + successful: !!bestDelim, + bestDelimiter: bestDelim + } + } + + function needsHeaderRow() + { + return _config.header && _fields.length == 0; + } + + function fillHeaderFields() + { + if (!_results) + return; + for (var i = 0; needsHeaderRow() && i < _results.data.length; i++) + for (var j = 0; j < _results.data[i].length; j++) + _fields.push(_results.data[i][j]); + _results.data.splice(0, 1); + } + + function applyHeaderAndDynamicTyping() + { + if (!_results) + return; + + for (var i = 0; i < _results.data.length; i++) + { + var row = {}; + for (var j = 0; j < _results.data[i].length; j++) + { + if (_config.dynamicTyping) + { + var value = _results.data[i][j]; + + if (value == "true") + _results.data[i][j] = true; + else if (value == "false") + _results.data[i][j] = false; + else + _results.data[i][j] = tryParseFloat(value); + } + + if (_config.header) + { + if (j >= _fields.length) + { + if (!row["__parsed_extra"]) + row["__parsed_extra"] = []; + row["__parsed_extra"].push(_results.data[i][j]); + } + row[_fields[j]] = _results.data[i][j]; + } + } + + if (_config.header) + { + _results.data[i] = row; + if (j > _fields.length) + addError("FieldMismatch", "TooManyFields", "Too many fields: expected " + _fields.length + " fields but parsed " + j, i); + else if (j < _fields.length) + addError("FieldMismatch", "TooFewFields", "Too few fields: expected " + _fields.length + " fields but parsed " + j, i); + } + } + + return _results; + } + + function tryParseFloat(val) + { + var isNumber = FLOAT.test(val); + return isNumber ? parseFloat(val) : val; + } + + function addError(type, code, msg, row) + { + _results.errors.push({ + type: type, + code: code, + message: msg, + row: row + }); + } + } + + + + + + + + + + + + function Parser(config) + { + var self = this; + var EMPTY = /^\s*$/; + + var _input; // The input text being parsed + var _delimiter; // The delimiting character + var _comments; // Comment character (default '#') or boolean + var _step; // The step (streaming) function + var _callback; // The callback to invoke when finished + var _preview; // Maximum number of lines (not rows) to parse + var _ch; // Current character + var _i; // Current character's positional index + var _inQuotes; // Whether in quotes or not + var _lineNum; // Current line number (1-based indexing) + var _data; // Parsed data (results) + var _errors; // Parse errors + var _rowIdx; // Current row index within results (0-based) + var _colIdx; // Current col index within result row (0-based) + var _aborted = false; // Abort flag + var _paused = false; // Pause flag + + // Unpack the config object + config = config || {}; + _delimiter = config.delimiter; + _comments = config.comments; + _step = config.step; + _preview = config.preview; + + // Delimiter integrity check + if (typeof _delimiter !== 'string' + || _delimiter.length != 1 + || Papa.BAD_DELIMITERS.indexOf(_delimiter) > -1) + _delimiter = ","; + + // Comment character integrity check + if (_comments === true) + _comments = "#"; + else if (typeof _comments !== 'string' + || _comments.length != 1 + || Papa.BAD_DELIMITERS.indexOf(_comments) > -1 + || _comments == _delimiter) + _comments = false; + + // Parses delimited text input + this.parse = function(input) + { + if (typeof input !== 'string') + throw "Input must be a string"; + reset(input); + return parserLoop(); + }; +/* + // TODO: Pause and resume just doesn't work well. + // I suspect this may need to be implemented at a higher-level + // scope than just this core Parser. + this.pause = function() + { + _paused = true; + }; + + this.resume = function() + { + _paused = false; + if (_i < _input.length) + return parserLoop(); + }; +*/ + this.abort = function() + { + _aborted = true; + }; + + function parserLoop() + { + while (_i < _input.length) + { + if (_aborted) break; + if (_preview > 0 && _rowIdx >= _preview) break; + if (_paused) return returnable(); + + if (_ch == '"') + parseQuotes(); + else if (_inQuotes) + parseInQuotes(); + else + parseNotInQuotes(); + + nextChar(); + } + + return finishParsing(); + } + + function nextChar() + { + _i++; + _ch = _input[_i]; + } + + function finishParsing() + { + if (_aborted) + addError("Abort", "ParseAbort", "Parsing was aborted by the user's step function"); + if (_inQuotes) + addError("Quotes", "MissingQuotes", "Unescaped or mismatched quotes"); + endRow(); // End of input is also end of the last row + return returnable(); + } + + function parseQuotes() + { + if (quotesOnBoundary() && !quotesEscaped()) + _inQuotes = !_inQuotes; + else + { + saveChar(); + if (_inQuotes && quotesEscaped()) + _i++ + else + addError("Quotes", "UnexpectedQuotes", "Unexpected quotes"); + } + } + + function parseInQuotes() + { + saveChar(); + } + + function parseNotInQuotes() + { + if (_ch == _delimiter) + newField(); + else if (twoCharLineBreak()) + { + newRow(); + nextChar(); + } + else if (oneCharLineBreak()) + newRow(); + else if (isCommentStart()) + skipLine(); + else + saveChar(); + } + + function isCommentStart() + { + if (!_comments) + return false; + + var firstCharOfLine = _i == 0 + || oneCharLineBreak(_i-1) + || twoCharLineBreak(_i-2); + return firstCharOfLine && _input[_i] === _comments; + } + + function skipLine() + { + while (!twoCharLineBreak() + && !oneCharLineBreak() + && _i < _input.length) + { + nextChar(); + } + } + + function saveChar() + { + _data[_rowIdx][_colIdx] += _ch; + } + + function newField() + { + _data[_rowIdx].push(""); + _colIdx = _data[_rowIdx].length - 1; + } + + function newRow() + { + endRow(); + + _lineNum++; + _data.push([]); + _rowIdx = _data.length - 1; + newField(); + } + + function endRow() + { + trimEmptyLastRow(); + if (typeof _step === 'function') + { + if (_data[_rowIdx]) + _step(returnable(), self); + clearErrorsAndData(); + } + } + + function trimEmptyLastRow() + { + if (_data[_rowIdx].length == 1 && EMPTY.test(_data[_rowIdx][0])) + { + _data.splice(_rowIdx, 1); + _rowIdx = _data.length - 1; + } + } + + function twoCharLineBreak(i) + { + if (typeof i !== 'number') + i = _i; + return i < _input.length - 1 && + ((_input[i] == "\r" && _input[i+1] == "\n") + || (_input[i] == "\n" && _input[i+1] == "\r")) + } + + function oneCharLineBreak(i) + { + if (typeof i !== 'number') + i = _i; + return _input[i] == "\r" || _input[i] == "\n"; + } + + function quotesEscaped() + { + // Quotes as data cannot be on boundary, for example: ,"", are not escaped quotes + return !quotesOnBoundary() && _i < _input.length - 1 && _input[_i+1] == '"'; + } + + function quotesOnBoundary() + { + return isBoundary(_i-1) || isBoundary(_i+1); + } + + function isBoundary(i) + { + if (typeof i != 'number') + i = _i; + + var ch = _input[i]; + + return (i == -1 || i == _input.length) + || (i < _input.length + && i > -1 + && (ch == _delimiter + || ch == "\r" + || ch == "\n")); + } + + function addError(type, code, msg) + { + _errors.push({ + type: type, + code: code, + message: msg, + line: _lineNum, + row: _rowIdx, + index: _i + }); + } + + function reset(input) + { + _input = input; + _inQuotes = false; + _lineNum = 1; + _i = 0; + clearErrorsAndData(); + _data = [ [""] ]; // starting parsing requires an empty field + _ch = _input[_i]; + } + + function clearErrorsAndData() + { + _data = []; + _errors = []; + _rowIdx = 0; + _colIdx = 0; + } + + function returnable() + { + return { + data: _data, + errors: _errors, + meta: { + lines: _lineNum, + delimiter: _delimiter, + aborted: _aborted + } + }; + } + } + + + + function getScriptPath() + { + var id = "worker" + String(Math.random()).substr(2); + document.write(''); + return document.getElementById(id).previousSibling.src; + } + + function newWorker() + { + if (!Papa.WORKERS_SUPPORTED) + return false; + var w = new global.Worker(SCRIPT_PATH); + w.onmessage = mainThreadReceivedMessage; + w.id = workerIdCounter++; + workers[w.id] = w; + return w; + } + + // Callback when main thread receives a message + function mainThreadReceivedMessage(e) + { + var msg = e.data; + var worker = workers[msg.workerId]; + + if (msg.results && msg.results.data && isFunction(worker.userStep)) + { + for (var i = 0; i < msg.results.data.length; i++) + worker.userStep(msg.results.data[i]); + } + + if (msg.finished) + { + if (isFunction(workers[msg.workerId].userComplete)) + workers[msg.workerId].userComplete(msg.results); + workers[msg.workerId].terminate(); + delete workers[msg.workerId]; + } + } + + // Callback when worker thread receives a message + function workerThreadReceivedMessage(e) + { + var msg = e.data; + + if (typeof Papa.WORKER_ID === 'undefined' && msg) + Papa.WORKER_ID = msg.workerId; + + if (typeof msg.input === 'string') + { + global.postMessage({ + workerId: Papa.WORKER_ID, + results: Papa.parse(msg.input, msg.config), + finished: true, + }); + } + else if (msg.input instanceof File) + { + var results = Papa.parse(msg.input, msg.config); + if (results) + global.postMessage({ + workerId: Papa.WORKER_ID, + results: results, + finished: true + }); + } + } + + // Replaces bad config values with good, default ones + function copyAndValidateConfig(origConfig) + { + if (typeof origConfig !== 'object') + origConfig = {}; + + var config = copy(origConfig); + + if (typeof config.delimiter !== 'string' + || config.delimiter.length != 1 + || Papa.BAD_DELIMITERS.indexOf(config.delimiter) > -1) + config.delimiter = DEFAULTS.delimiter; + + if (typeof config.header !== 'boolean') + config.header = DEFAULTS.header; + + if (typeof config.dynamicTyping !== 'boolean') + config.dynamicTyping = DEFAULTS.dynamicTyping; + + if (typeof config.preview !== 'number') + config.preview = DEFAULTS.preview; + + if (typeof config.step !== 'function') + config.step = DEFAULTS.step; + + if (typeof config.complete !== 'function') + config.complete = DEFAULTS.complete; + + if (typeof config.encoding !== 'string') + config.encoding = DEFAULTS.encoding; + + if (typeof config.worker !== 'boolean') + config.worker = DEFAULTS.worker; + + if (typeof config.download !== 'boolean') + config.download = DEFAULTS.download; + + return config; + } + + function copy(obj) + { + if (typeof obj !== 'object') + return obj; + var cpy = obj instanceof Array ? [] : {}; + for (var key in obj) + cpy[key] = copy(obj[key]); + return cpy; + } + + function isFunction(func) + { + return typeof func === 'function'; + } +})(this); \ No newline at end of file diff --git a/player/player.css b/player/player.css new file mode 100644 index 0000000..1683f88 --- /dev/null +++ b/player/player.css @@ -0,0 +1,1041 @@ +body { + font-family: 'Source Sans Pro', sans-serif; +} + +h1 { + text-align: center; +} + +textarea, +button { + font-size: 14px; +} + +textarea { + box-sizing: border-box; + font: 14px/1.25em Menlo, Monaco, 'Courier New', monospace; + width: 100%; + padding: 10px; + max-width: 900px; + height: 200px; +} + +button { + padding: 10px 50px; + font-size: 20px; +} + +label { + display: block; +} + + +.text-center { + text-align: center; +} + + + + + + + + + +/* ============================================ */ +/* This file has a mobile-to-desktop breakpoint */ +/* ============================================ */ +@media screen and (max-width: 400px) { + @-ms-viewport { + width: 320px; + } +} +.clear { + clear: both; + display: block; + overflow: hidden; + visibility: hidden; + width: 0; + height: 0; +} + +.grid-container:before, .clearfix:before, +.grid-container:after, +.clearfix:after { + content: "."; + display: block; + overflow: hidden; + visibility: hidden; + font-size: 0; + line-height: 0; + width: 0; + height: 0; +} + +.grid-container:after, .clearfix:after { + clear: both; +} + +.grid-container { + margin-left: auto; + margin-right: auto; + max-width: 1200px; + padding-left: 10px; + padding-right: 10px; +} + +.grid-5, .mobile-grid-5, .grid-10, .mobile-grid-10, .grid-15, .mobile-grid-15, .grid-20, .mobile-grid-20, .grid-25, .mobile-grid-25, .grid-30, .mobile-grid-30, .grid-35, .mobile-grid-35, .grid-40, .mobile-grid-40, .grid-45, .mobile-grid-45, .grid-50, .mobile-grid-50, .grid-55, .mobile-grid-55, .grid-60, .mobile-grid-60, .grid-65, .mobile-grid-65, .grid-70, .mobile-grid-70, .grid-75, .mobile-grid-75, .grid-80, .mobile-grid-80, .grid-85, .mobile-grid-85, .grid-90, .mobile-grid-90, .grid-95, .mobile-grid-95, .grid-100, .mobile-grid-100, .grid-33, .mobile-grid-33, .grid-66, .mobile-grid-66 { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding-left: 10px; + padding-right: 10px; +} + +.grid-parent { + padding-left: 0; + padding-right: 0; +} + +@media screen and (max-width: 767px) { + .mobile-grid-100:before, + .mobile-grid-100:after { + content: "."; + display: block; + overflow: hidden; + visibility: hidden; + font-size: 0; + line-height: 0; + width: 0; + height: 0; + } + + .mobile-grid-100:after { + clear: both; + } + + .mobile-push-5, .mobile-pull-5, .mobile-push-10, .mobile-pull-10, .mobile-push-15, .mobile-pull-15, .mobile-push-20, .mobile-pull-20, .mobile-push-25, .mobile-pull-25, .mobile-push-30, .mobile-pull-30, .mobile-push-35, .mobile-pull-35, .mobile-push-40, .mobile-pull-40, .mobile-push-45, .mobile-pull-45, .mobile-push-50, .mobile-pull-50, .mobile-push-55, .mobile-pull-55, .mobile-push-60, .mobile-pull-60, .mobile-push-65, .mobile-pull-65, .mobile-push-70, .mobile-pull-70, .mobile-push-75, .mobile-pull-75, .mobile-push-80, .mobile-pull-80, .mobile-push-85, .mobile-pull-85, .mobile-push-90, .mobile-pull-90, .mobile-push-95, .mobile-pull-95, .mobile-push-33, .mobile-pull-33, .mobile-push-66, .mobile-pull-66 { + position: relative; + } + + .hide-on-mobile { + display: none !important; + } + + .mobile-grid-5 { + float: left; + width: 5%; + } + + .mobile-prefix-5 { + margin-left: 5%; + } + + .mobile-suffix-5 { + margin-right: 5%; + } + + .mobile-push-5 { + left: 5%; + } + + .mobile-pull-5 { + left: -5%; + } + + .mobile-grid-10 { + float: left; + width: 10%; + } + + .mobile-prefix-10 { + margin-left: 10%; + } + + .mobile-suffix-10 { + margin-right: 10%; + } + + .mobile-push-10 { + left: 10%; + } + + .mobile-pull-10 { + left: -10%; + } + + .mobile-grid-15 { + float: left; + width: 15%; + } + + .mobile-prefix-15 { + margin-left: 15%; + } + + .mobile-suffix-15 { + margin-right: 15%; + } + + .mobile-push-15 { + left: 15%; + } + + .mobile-pull-15 { + left: -15%; + } + + .mobile-grid-20 { + float: left; + width: 20%; + } + + .mobile-prefix-20 { + margin-left: 20%; + } + + .mobile-suffix-20 { + margin-right: 20%; + } + + .mobile-push-20 { + left: 20%; + } + + .mobile-pull-20 { + left: -20%; + } + + .mobile-grid-25 { + float: left; + width: 25%; + } + + .mobile-prefix-25 { + margin-left: 25%; + } + + .mobile-suffix-25 { + margin-right: 25%; + } + + .mobile-push-25 { + left: 25%; + } + + .mobile-pull-25 { + left: -25%; + } + + .mobile-grid-30 { + float: left; + width: 30%; + } + + .mobile-prefix-30 { + margin-left: 30%; + } + + .mobile-suffix-30 { + margin-right: 30%; + } + + .mobile-push-30 { + left: 30%; + } + + .mobile-pull-30 { + left: -30%; + } + + .mobile-grid-35 { + float: left; + width: 35%; + } + + .mobile-prefix-35 { + margin-left: 35%; + } + + .mobile-suffix-35 { + margin-right: 35%; + } + + .mobile-push-35 { + left: 35%; + } + + .mobile-pull-35 { + left: -35%; + } + + .mobile-grid-40 { + float: left; + width: 40%; + } + + .mobile-prefix-40 { + margin-left: 40%; + } + + .mobile-suffix-40 { + margin-right: 40%; + } + + .mobile-push-40 { + left: 40%; + } + + .mobile-pull-40 { + left: -40%; + } + + .mobile-grid-45 { + float: left; + width: 45%; + } + + .mobile-prefix-45 { + margin-left: 45%; + } + + .mobile-suffix-45 { + margin-right: 45%; + } + + .mobile-push-45 { + left: 45%; + } + + .mobile-pull-45 { + left: -45%; + } + + .mobile-grid-50 { + float: left; + width: 50%; + } + + .mobile-prefix-50 { + margin-left: 50%; + } + + .mobile-suffix-50 { + margin-right: 50%; + } + + .mobile-push-50 { + left: 50%; + } + + .mobile-pull-50 { + left: -50%; + } + + .mobile-grid-55 { + float: left; + width: 55%; + } + + .mobile-prefix-55 { + margin-left: 55%; + } + + .mobile-suffix-55 { + margin-right: 55%; + } + + .mobile-push-55 { + left: 55%; + } + + .mobile-pull-55 { + left: -55%; + } + + .mobile-grid-60 { + float: left; + width: 60%; + } + + .mobile-prefix-60 { + margin-left: 60%; + } + + .mobile-suffix-60 { + margin-right: 60%; + } + + .mobile-push-60 { + left: 60%; + } + + .mobile-pull-60 { + left: -60%; + } + + .mobile-grid-65 { + float: left; + width: 65%; + } + + .mobile-prefix-65 { + margin-left: 65%; + } + + .mobile-suffix-65 { + margin-right: 65%; + } + + .mobile-push-65 { + left: 65%; + } + + .mobile-pull-65 { + left: -65%; + } + + .mobile-grid-70 { + float: left; + width: 70%; + } + + .mobile-prefix-70 { + margin-left: 70%; + } + + .mobile-suffix-70 { + margin-right: 70%; + } + + .mobile-push-70 { + left: 70%; + } + + .mobile-pull-70 { + left: -70%; + } + + .mobile-grid-75 { + float: left; + width: 75%; + } + + .mobile-prefix-75 { + margin-left: 75%; + } + + .mobile-suffix-75 { + margin-right: 75%; + } + + .mobile-push-75 { + left: 75%; + } + + .mobile-pull-75 { + left: -75%; + } + + .mobile-grid-80 { + float: left; + width: 80%; + } + + .mobile-prefix-80 { + margin-left: 80%; + } + + .mobile-suffix-80 { + margin-right: 80%; + } + + .mobile-push-80 { + left: 80%; + } + + .mobile-pull-80 { + left: -80%; + } + + .mobile-grid-85 { + float: left; + width: 85%; + } + + .mobile-prefix-85 { + margin-left: 85%; + } + + .mobile-suffix-85 { + margin-right: 85%; + } + + .mobile-push-85 { + left: 85%; + } + + .mobile-pull-85 { + left: -85%; + } + + .mobile-grid-90 { + float: left; + width: 90%; + } + + .mobile-prefix-90 { + margin-left: 90%; + } + + .mobile-suffix-90 { + margin-right: 90%; + } + + .mobile-push-90 { + left: 90%; + } + + .mobile-pull-90 { + left: -90%; + } + + .mobile-grid-95 { + float: left; + width: 95%; + } + + .mobile-prefix-95 { + margin-left: 95%; + } + + .mobile-suffix-95 { + margin-right: 95%; + } + + .mobile-push-95 { + left: 95%; + } + + .mobile-pull-95 { + left: -95%; + } + + .mobile-grid-33 { + float: left; + width: 33.33333%; + } + + .mobile-prefix-33 { + margin-left: 33.33333%; + } + + .mobile-suffix-33 { + margin-right: 33.33333%; + } + + .mobile-push-33 { + left: 33.33333%; + } + + .mobile-pull-33 { + left: -33.33333%; + } + + .mobile-grid-66 { + float: left; + width: 66.66667%; + } + + .mobile-prefix-66 { + margin-left: 66.66667%; + } + + .mobile-suffix-66 { + margin-right: 66.66667%; + } + + .mobile-push-66 { + left: 66.66667%; + } + + .mobile-pull-66 { + left: -66.66667%; + } + + .mobile-grid-100 { + clear: both; + width: 100%; + } +} +@media screen and (min-width: 768px) { + .grid-100:before, + .grid-100:after { + content: "."; + display: block; + overflow: hidden; + visibility: hidden; + font-size: 0; + line-height: 0; + width: 0; + height: 0; + } + + .grid-100:after { + clear: both; + } + + .push-5, .pull-5, .push-10, .pull-10, .push-15, .pull-15, .push-20, .pull-20, .push-25, .pull-25, .push-30, .pull-30, .push-35, .pull-35, .push-40, .pull-40, .push-45, .pull-45, .push-50, .pull-50, .push-55, .pull-55, .push-60, .pull-60, .push-65, .pull-65, .push-70, .pull-70, .push-75, .pull-75, .push-80, .pull-80, .push-85, .pull-85, .push-90, .pull-90, .push-95, .pull-95, .push-33, .pull-33, .push-66, .pull-66 { + position: relative; + } + + .hide-on-desktop { + display: none !important; + } + + .grid-5 { + float: left; + width: 5%; + } + + .prefix-5 { + margin-left: 5%; + } + + .suffix-5 { + margin-right: 5%; + } + + .push-5 { + left: 5%; + } + + .pull-5 { + left: -5%; + } + + .grid-10 { + float: left; + width: 10%; + } + + .prefix-10 { + margin-left: 10%; + } + + .suffix-10 { + margin-right: 10%; + } + + .push-10 { + left: 10%; + } + + .pull-10 { + left: -10%; + } + + .grid-15 { + float: left; + width: 15%; + } + + .prefix-15 { + margin-left: 15%; + } + + .suffix-15 { + margin-right: 15%; + } + + .push-15 { + left: 15%; + } + + .pull-15 { + left: -15%; + } + + .grid-20 { + float: left; + width: 20%; + } + + .prefix-20 { + margin-left: 20%; + } + + .suffix-20 { + margin-right: 20%; + } + + .push-20 { + left: 20%; + } + + .pull-20 { + left: -20%; + } + + .grid-25 { + float: left; + width: 25%; + } + + .prefix-25 { + margin-left: 25%; + } + + .suffix-25 { + margin-right: 25%; + } + + .push-25 { + left: 25%; + } + + .pull-25 { + left: -25%; + } + + .grid-30 { + float: left; + width: 30%; + } + + .prefix-30 { + margin-left: 30%; + } + + .suffix-30 { + margin-right: 30%; + } + + .push-30 { + left: 30%; + } + + .pull-30 { + left: -30%; + } + + .grid-35 { + float: left; + width: 35%; + } + + .prefix-35 { + margin-left: 35%; + } + + .suffix-35 { + margin-right: 35%; + } + + .push-35 { + left: 35%; + } + + .pull-35 { + left: -35%; + } + + .grid-40 { + float: left; + width: 40%; + } + + .prefix-40 { + margin-left: 40%; + } + + .suffix-40 { + margin-right: 40%; + } + + .push-40 { + left: 40%; + } + + .pull-40 { + left: -40%; + } + + .grid-45 { + float: left; + width: 45%; + } + + .prefix-45 { + margin-left: 45%; + } + + .suffix-45 { + margin-right: 45%; + } + + .push-45 { + left: 45%; + } + + .pull-45 { + left: -45%; + } + + .grid-50 { + float: left; + width: 50%; + } + + .prefix-50 { + margin-left: 50%; + } + + .suffix-50 { + margin-right: 50%; + } + + .push-50 { + left: 50%; + } + + .pull-50 { + left: -50%; + } + + .grid-55 { + float: left; + width: 55%; + } + + .prefix-55 { + margin-left: 55%; + } + + .suffix-55 { + margin-right: 55%; + } + + .push-55 { + left: 55%; + } + + .pull-55 { + left: -55%; + } + + .grid-60 { + float: left; + width: 60%; + } + + .prefix-60 { + margin-left: 60%; + } + + .suffix-60 { + margin-right: 60%; + } + + .push-60 { + left: 60%; + } + + .pull-60 { + left: -60%; + } + + .grid-65 { + float: left; + width: 65%; + } + + .prefix-65 { + margin-left: 65%; + } + + .suffix-65 { + margin-right: 65%; + } + + .push-65 { + left: 65%; + } + + .pull-65 { + left: -65%; + } + + .grid-70 { + float: left; + width: 70%; + } + + .prefix-70 { + margin-left: 70%; + } + + .suffix-70 { + margin-right: 70%; + } + + .push-70 { + left: 70%; + } + + .pull-70 { + left: -70%; + } + + .grid-75 { + float: left; + width: 75%; + } + + .prefix-75 { + margin-left: 75%; + } + + .suffix-75 { + margin-right: 75%; + } + + .push-75 { + left: 75%; + } + + .pull-75 { + left: -75%; + } + + .grid-80 { + float: left; + width: 80%; + } + + .prefix-80 { + margin-left: 80%; + } + + .suffix-80 { + margin-right: 80%; + } + + .push-80 { + left: 80%; + } + + .pull-80 { + left: -80%; + } + + .grid-85 { + float: left; + width: 85%; + } + + .prefix-85 { + margin-left: 85%; + } + + .suffix-85 { + margin-right: 85%; + } + + .push-85 { + left: 85%; + } + + .pull-85 { + left: -85%; + } + + .grid-90 { + float: left; + width: 90%; + } + + .prefix-90 { + margin-left: 90%; + } + + .suffix-90 { + margin-right: 90%; + } + + .push-90 { + left: 90%; + } + + .pull-90 { + left: -90%; + } + + .grid-95 { + float: left; + width: 95%; + } + + .prefix-95 { + margin-left: 95%; + } + + .suffix-95 { + margin-right: 95%; + } + + .push-95 { + left: 95%; + } + + .pull-95 { + left: -95%; + } + + .grid-33 { + float: left; + width: 33.33333%; + } + + .prefix-33 { + margin-left: 33.33333%; + } + + .suffix-33 { + margin-right: 33.33333%; + } + + .push-33 { + left: 33.33333%; + } + + .pull-33 { + left: -33.33333%; + } + + .grid-66 { + float: left; + width: 66.66667%; + } + + .prefix-66 { + margin-left: 66.66667%; + } + + .suffix-66 { + margin-right: 66.66667%; + } + + .push-66 { + left: 66.66667%; + } + + .pull-66 { + left: -66.66667%; + } + + .grid-100 { + clear: both; + width: 100%; + } +} \ No newline at end of file diff --git a/player/player.html b/player/player.html new file mode 100644 index 0000000..0bee2f3 --- /dev/null +++ b/player/player.html @@ -0,0 +1,53 @@ + + + + Papa Parse Player + + + + + + + +

Papa Parse Player

+ +
+ +
+ + + + + + + + + +
+ +
+ + + +
+ or +
+ + + +

+ + + +

+ + Open the Console in your browser's inspector tools to see results. +
+ +
+ + \ No newline at end of file diff --git a/player/player.js b/player/player.js new file mode 100644 index 0000000..d77f4b6 --- /dev/null +++ b/player/player.js @@ -0,0 +1,71 @@ +var stepped = 0; +var start, end; + +$(function() +{ + $('#submit').click(function() + { + var txt = $('#input').val(); + var files = $('#files')[0].files; + stepped = 0; + + var config = buildConfig(); + + if (files.length > 0) + { + start = performance.now(); + + $('#files').parse({ + config: config, + before: function(file, inputElem) + { + console.log("Parsing file:", file); + }, + complete: function() + { + console.log("Done with all files."); + } + }); + } + else + { + start = performance.now(); + var results = Papa.parse(txt, config); + console.log("Synchronous parse results:", results); + } + }); + + $('#insert-tab').click(function() + { + $('#delimiter').val('\t'); + }); +}); + + + +function buildConfig() +{ + return { + delimiter: $('#delimiter').val(), + header: $('#header').prop('checked'), + dynamicTyping: $('#header').prop('checked'), + preview: parseInt($('#preview').val()), + step: $('#stream').prop('checked') ? stepFn : undefined, + encoding: $('#encoding').val(), + worker: $('#worker').prop('checked'), + comments: $('#comments').val(), + complete: completeFn, + download: $('#download').prop('checked') + }; +} + +function stepFn(results, parser) +{ + stepped++; +} + +function completeFn() +{ + end = performance.now(); + console.log("Finished input. Time:", end-start, arguments); +} \ No newline at end of file diff --git a/tests.js b/tests/test-cases.js similarity index 97% rename from tests.js rename to tests/test-cases.js index 10753e8..88871b8 100644 --- a/tests.js +++ b/tests/test-cases.js @@ -1,3 +1,8 @@ +// TODO: Add tests for unparse: +// If fields is omitted, write a CSV string without a header row +// If delimiter is omitted, choose comma by default +// If data is omitted, do nothing... maybe if fields IS specified, write just the header row? + var RECORD_SEP = String.fromCharCode(30); var UNIT_SEP = String.fromCharCode(31); diff --git a/test-runner.js b/tests/test-runner.js similarity index 98% rename from test-runner.js rename to tests/test-runner.js index 7640f17..ec4bb96 100644 --- a/test-runner.js +++ b/tests/test-runner.js @@ -56,8 +56,7 @@ $(function() function runTest(test, num) { - var parser = new Parser(test.config); - var actual = parser.parse(test.input); + var actual = Papa.parse(test.input, test.config); var results = compare(actual.data, actual.errors, test.expected); diff --git a/tests.css b/tests/tests.css similarity index 100% rename from tests.css rename to tests/tests.css diff --git a/tests.html b/tests/tests.html similarity index 80% rename from tests.html rename to tests/tests.html index d8a2579..ed3f304 100644 --- a/tests.html +++ b/tests/tests.html @@ -1,11 +1,12 @@ - Tests - Papa Parse + Papa Parse Tests + - - + +