Browse Source

Merging 3.0 into master

pull/62/head 3.0.0
Matthew Holt 11 years ago
parent
commit
1fe3c46993
  1. 5
      .gitignore
  2. 2
      LICENSE
  3. 34
      README.md
  4. 16
      bower.json
  5. 677
      jquery.parse.js
  6. 6
      jquery.parse.min.js
  7. 1261
      papaparse.js
  8. 6
      papaparse.min.js
  9. 14
      parse.jquery.json
  10. 1041
      player/player.css
  11. 53
      player/player.html
  12. 70
      player/player.js
  13. 127
      tests.html
  14. 693
      tests.js
  15. 401
      tests/test-cases.js
  16. 171
      tests/test-runner.js
  17. 159
      tests/tests.css
  18. 34
      tests/tests.html

5
.gitignore vendored

@ -1,2 +1,3 @@ @@ -1,2 +1,3 @@
bower_components/*
node_modules/*
_gitignore/
bower_components/
node_modules/

2
LICENSE

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 Matt
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

34
README.md

@ -3,30 +3,34 @@ Parse CSV with Javascript @@ -3,30 +3,34 @@ Parse CSV with Javascript
[![mholt on Gittip](http://img.shields.io/badge/tips-accepted-brightgreen.svg?style=flat)](https://www.gittip.com/mholt/)
Papa Parse (formerly the jQuery Parse Plugin) is a robust and powerful CSV (character-separated values) parser with these features:
- Parses delimited text strings without any fuss
- Attach to `<input type="file">` elements to load and parse files from disk
- Automatically detects delimiter (or specify a delimiter yourself)
- Supports streaming large inputs
- Utilize the header row, if present
- Gracefully handles malformed data
- Optional dynamic typing so that numeric data is parsed as numbers
- Descriptive and contextual errors
- Easy to use
- Parse CSV files directly (local or over the network)
- Stream large files (even via HTTP)
- Reverse parsing (converts JSON to CSV)
- 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
- Minor jQuery integration to get files from `<input type="file">` elements
All are optional (except for being easy to use).
Demo
[Demo](http://papaparse.com/demo.html)
----
Visit **[PapaParse.com](http://papaparse.com/#demo)** to give Papa a whirl!
Visit **[PapaParse.com/demo.html](http://papaparse.com/demo.html)** to try Papa!
Get Started
-----------
Use [jquery.parse.min.js](https://github.com/mholt/jquery.parse/blob/master/jquery.parse.min.js) for production.
Use [papaparse.min.js](https://github.com/mholt/PapaParse/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).
@ -35,18 +39,18 @@ For usage instructions, see the [homepage](http://papaparse.com) and, for more d @@ -35,18 +39,18 @@ For usage instructions, see the [homepage](http://papaparse.com) and, for more d
Tests
-----
The Parser component is under test. Download this repository and open `tests.html` in your browser to run them.
Papa Parse is under test (especially its core Parser). Download this repository and open `tests/tests.html` in your browser to run them.
Contributing
------------
If you'd like to see a feature or bug fix, pull down the code and submit a pull request. But remember, if you're changing anything in the Parser function, a pull request, *with test*, is best. (All changes to the parser component should be validated with tests.) You may also open issues for discussion or join in on Twitter with [#PapaParse](https://twitter.com/search?q=%23PapaParse&src=typd&f=realtime)
To discuss a new feature or ask a question, open an issue. To fix a bug, submit a pull request to be credited with the [contributors](https://github.com/mholt/PapaParse/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 an experiment by [SmartyStreets](http://smartystreets.com) which matured into a fully-featured, independent jQuery plugin. Wanting to enhance the demo on their homepage, SmartyStreets looked into ways to simulate their list service. This involved processing at least part of a potentially large delimited text file. And what else? They wanted to do it without requiring a file upload (for simplicity and to alleviate privacy concerns). No suitable solutions were found, so they built their own. After finding it successful, the code was brought out into this jQuery plugin, now known as Papa Parse.
Papa Parse is the result of a successful experiment by [SmartyStreets](http://smartystreets.com) which matured into a fully-featured, independent Javascript library.

16
bower.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "Papa-Parse",
"main": "jquery.parse.js",
"main": "papaparse.js",
"homepage": "http://papaparse.com",
"authors": [
"Matthew Holt"
@ -20,7 +20,11 @@ @@ -20,7 +20,11 @@
"pipe",
"file",
"filereader",
"stream"
"stream",
"workers",
"ajax",
"threading",
"multi-threaded"
],
"license": "MIT",
"ignore": [
@ -28,9 +32,7 @@ @@ -28,9 +32,7 @@
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": ">=1.6.0"
}
"tests",
"player"
]
}

677
jquery.parse.js

@ -1,677 +0,0 @@ @@ -1,677 +0,0 @@
/*
Papa Parse
v2.1.4
https://github.com/mholt/jquery.parse
*/
(function($)
{
"use strict";
$.fn.parse = function(options)
{
var config = options.config || {};
var queue = [];
this.each(function(idx)
{
var supported = $(this).prop('tagName').toUpperCase() == "INPUT"
&& $(this).attr('type') == "file"
&& window.FileReader;
if (!supported)
return true; // continue to next input element
var instanceConfig = $.extend({}, config); // This copy is very important
if (!this.files || this.files.length == 0)
{
error("NoFileError", undefined, this);
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
});
if (queue.length > 0)
parseFile(queue[0]);
});
return this;
function parseFile(f)
{
var completeFunc = complete, errorFunc;
if (isFunction(options.error))
errorFunc = function() { options.error(reader.error, f.file, f.inputElem); };
if (isFunction(options.complete))
completeFunc = function(results, file, inputElem, event) { options.complete(results, file, inputElem, event); complete(); };
if (isFunction(options.before))
{
var returned = options.before(f.file, f.inputElem);
if (typeof returned === 'object')
f.instanceConfig = $.extend(f.instanceConfig, returned);
else if (returned === "skip")
return complete(); // Proceeds to next file
else if (returned === false)
{
error("AbortError", f.file, f.inputElem);
return; // Aborts all queued files immediately
}
}
if (f.instanceConfig.step)
{
var streamer = new Streamer(f.file, {
inputElem: f.inputElem,
config: $.extend({}, f.instanceConfig) // This copy is very important
});
streamer.stream(completeFunc, errorFunc);
}
else
{
var reader = new FileReader();
reader.onerror = errorFunc;
reader.onload = function(event)
{
var text = event.target.result;
var results = $.parse(text, f.instanceConfig);
completeFunc(results, f.file, f.inputElem, event);
};
reader.readAsText(f.file, f.instanceConfig.encoding);
}
}
function error(name, file, elem)
{
if (isFunction(options.error))
options.error({name: name}, file, elem);
}
function complete()
{
queue.splice(0, 1);
if (queue.length > 0)
parseFile(queue[0]);
}
};
$.parse = function(input, options)
{
var parser = new Parser(options);
return parser.parse(input);
};
function isFunction(func) { return typeof func === 'function'; }
// Streamer is a wrapper over Parser to handle chunking the input file
function Streamer(file, settings)
{
if (!settings)
settings = {};
if (!settings.chunkSize)
settings.chunkSize = 1024 * 1024 * 5; // 5 MB
if (settings.config.step) // it had better be there...!
{
var userStep = settings.config.step;
settings.config.step = function(data) { return userStep(data, file, settings.inputElem); };
}
var start = 0;
var aggregate = "";
var partialLine = "";
var parser = new Parser(settings.config);
var reader = new FileReader();
reader.onload = blobLoaded;
reader.onerror = blobError;
this.stream = function(completeCallback, fileErrorCallback)
{
settings.onComplete = completeCallback;
settings.onFileError = fileErrorCallback;
nextChunk();
};
function blobLoaded(event)
{
aggregate += partialLine + event.target.result;
partialLine = "";
if (start < file.size)
{
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 = parser.parse(aggregate);
aggregate = "";
if (start >= file.size)
return done(event);
else if (results.errors.abort)
return;
else
nextChunk();
}
function done(event)
{
if (typeof settings.onComplete === 'function')
settings.onComplete(undefined, file, settings.inputElem, event);
}
function blobError()
{
if (typeof settings.onFileError === 'function')
settings.onFileError(reader.error, file, settings.inputElem);
}
function nextChunk()
{
if (start < file.size)
{
var end = Math.min(start + settings.chunkSize, file.size);
if (file.slice) {
reader.readAsText(file.slice(start, end), settings.config.encoding);
}
else if (file.webkitSlice) {
reader.readAsText(file.webkitSlice(start, end), settings.config.encoding);
}
start += settings.chunkSize;
}
};
}
// Parser is the actual parsing component.
// It is under test and does not depend on jQuery.
// You could rip this entire function out of the plugin
// and use it independently (with attribution).
function Parser(config)
{
var self = this;
var _invocations = 0;
var _input = "";
var _chunkOffset = 0;
var _abort = false;
var _config = {};
var _state = freshState();
var _defaultConfig = {
delimiter: "",
header: true,
dynamicTyping: true,
preview: 0,
commentChar: false
};
var _regex = {
floats: /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i,
empty: /^\s*$/
};
config = validConfig(config);
_config = {
delimiter: config.delimiter,
header: config.header,
dynamicTyping: config.dynamicTyping,
preview: config.preview,
step: config.step,
commentChar: config.commentChar
};
this.parse = function(input)
{
if (typeof input !== 'string')
return returnable();
reset(input);
if (!_config.delimiter && !guessDelimiter(input))
{
addError("Delimiter", "UndetectableDelimiter", "Unable to auto-detect delimiting character; defaulted to comma", "config");
_config.delimiter = ",";
}
for (_state.i = 0; _state.i < _input.length; _state.i++)
{
if (_abort || (_config.preview > 0 && _state.lineNum > _config.preview))
break;
_state.ch = _input[_state.i];
if (_config.commentChar) {
// Check if line begins with a commentChar
if (_state.line == "" &&_state.ch == _config.commentChar) {
newRow();
// skip to next row
while (true) {
++_state.i
if (_input[_state.i] == "\r" || _input[_state.i] == "\n")
break;
}
}
_state.ch = _input[_state.i];
}
_state.line += _state.ch;
if (_state.ch == '"')
handleQuote();
else if (_state.inQuotes)
inQuotes();
else
notInQuotes();
}
if (_abort)
addError("Abort", "ParseAbort", "Parsing was aborted by the user's step function", "abort");
else
{
endRow(); // End of input is also end of the last row
if (_state.inQuotes)
addError("Quotes", "MissingQuotes", "Unescaped or mismatched quotes");
}
return returnable();
};
this.getOptions = function()
{
return {
delimiter: _config.delimiter,
header: _config.header,
dynamicTyping: _config.dynamicTyping,
preview: _config.preview,
step: _config.step
};
};
function validConfig(config)
{
if (typeof config !== 'object')
config = {};
if (typeof config.delimiter !== 'string'
|| config.delimiter.length != 1)
config.delimiter = _defaultConfig.delimiter;
if (config.delimiter == '"' || config.delimiter == "\n")
config.delimiter = _defaultConfig.delimiter;
if (typeof config.header !== 'boolean')
config.header = _defaultConfig.header;
if (typeof config.dynamicTyping !== 'boolean')
config.dynamicTyping = _defaultConfig.dynamicTyping;
if (typeof config.preview !== 'number')
config.preview = _defaultConfig.preview;
if (typeof config.step !== 'function')
config.step = _defaultConfig.step;
if (config.commentChar === true)
config.commentChar = '#';
if (typeof config.commentChar !== 'string' && config.commentChar !== false)
config.commentChar = false;
return config;
}
function guessDelimiter(input)
{
var recordSep = String.fromCharCode(30);
var unitSep = String.fromCharCode(31);
var delimiters = [",", "\t", "|", ";", recordSep, unitSep];
var bestDelim, bestDelta, fieldCountPrevRow;
for (var i = 0; i < delimiters.length; i++)
{
var delim = delimiters[i];
var delta = 0, avgFieldCount = 0;
var fieldCountPrevRow = undefined;
var preview = new Parser({
delimiter: delim,
header: false,
dynamicTyping: false,
preview: 10
}).parse(input);
for (var j = 0; j < preview.results.length; j++)
{
var fieldCount = preview.results[j].length;
avgFieldCount += fieldCount;
if (typeof fieldCountPrevRow === 'undefined')
{
fieldCountPrevRow = fieldCount;
continue;
}
else if (fieldCount > 1)
{
delta += Math.abs(fieldCount - fieldCountPrevRow);
fieldCountPrevRow = fieldCount;
}
}
avgFieldCount /= preview.results.length;
if ((typeof bestDelta === 'undefined' || delta < bestDelta)
&& avgFieldCount > 1.99)
{
bestDelta = delta;
bestDelim = delim;
}
}
_config.delimiter = bestDelim;
return !!bestDelim;
}
function handleQuote()
{
var delimBefore = (_state.i > 0 && isBoundary(_state.i-1))
|| _state.i == 0;
var delimAfter = (_state.i < _input.length - 1 && isBoundary(_state.i+1))
|| _state.i == _input.length - 1;
var escaped = _state.i < _input.length - 1
&& _input[_state.i+1] == '"';
if (_state.inQuotes && escaped)
{
_state.fieldVal += '"';
_state.i++;
}
else if (delimBefore || delimAfter)
_state.inQuotes = !_state.inQuotes;
else
addError("Quotes", "UnexpectedQuotes", "Unexpected quotes");
}
function inQuotes()
{
appendCharToField();
}
function appendCharToField()
{
_state.fieldVal += _state.ch;
}
function notInQuotes()
{
if (_state.ch == _config.delimiter)
saveValue();
else if ((_state.ch == "\r" && _state.i < _input.length - 1
&& _input[_state.i+1] == "\n")
|| (_state.ch == "\n" && _state.i < _input.length - 1
&& _input[_state.i+1] == "\r"))
{
newRow();
_state.i++;
}
else if (_state.ch == "\r" || _state.ch == "\n")
newRow();
else
appendCharToField();
}
function isBoundary(i)
{
return _input[i] == _config.delimiter
|| _input[i] == "\n"
|| _input[i] == "\r";
}
function saveValue()
{
if (_config.header)
{
if (_state.lineNum == 1 && _invocations == 1)
_state.parsed.fields.push(_state.fieldVal);
else
{
var currentRow = _state.parsed.rows[_state.parsed.rows.length - 1];
var fieldName = _state.parsed.fields[_state.field];
if (fieldName)
{
if (_config.dynamicTyping)
_state.fieldVal = tryParseFloat(_state.fieldVal);
currentRow[fieldName] = _state.fieldVal;
}
else
{
if (typeof currentRow.__parsed_extra === 'undefined')
currentRow.__parsed_extra = [];
currentRow.__parsed_extra.push(_state.fieldVal);
}
}
}
else
{
if (_config.dynamicTyping)
_state.fieldVal = tryParseFloat(_state.fieldVal);
_state.parsed[_state.parsed.length - 1].push(_state.fieldVal);
}
_state.fieldVal = "";
_state.field ++;
}
function newRow()
{
endRow();
if (streaming())
{
_state.errors = {};
_state.errors.length = 0;
}
if (_config.header)
{
if (_state.lineNum > 0)
{
if (streaming())
_state.parsed.rows = [ {} ];
else
_state.parsed.rows.push({});
}
}
else
{
if (streaming())
_state.parsed = [ [] ];
else if (!_config.header)
_state.parsed.push([]);
}
_state.lineNum++;
_state.line = "";
_state.field = 0;
}
function endRow()
{
if (_abort)
return;
saveValue();
var emptyLine = trimEmptyLine();
if (!emptyLine && _config.header)
inspectFieldCount();
if (streaming() && (!_config.header ||
(_config.header && _state.parsed.rows.length > 0)))
{
var keepGoing = _config.step(returnable());
if (keepGoing === false)
_abort = true;
}
}
function streaming()
{
return typeof _config.step === 'function';
}
function tryParseFloat(num)
{
var isNumber = _regex.floats.test(num);
return isNumber ? parseFloat(num) : num;
}
function trimEmptyLine()
{
if (_regex.empty.test(_state.line))
{
if (_config.header)
{
if (_state.lineNum == 1)
{
_state.parsed.fields = [];
_state.lineNum--;
}
else
_state.parsed.rows.splice(_state.parsed.rows.length - 1, 1);
}
else
_state.parsed.splice(_state.parsed.length - 1, 1);
return true;
}
return false;
}
function inspectFieldCount()
{
if (!_config.header)
return true;
if (_state.parsed.rows.length == 0)
return true;
var expected = _state.parsed.fields.length;
// Actual field count tabulated manually because IE<9 doesn't support Object.keys
var actual = 0;
var lastRow = _state.parsed.rows[_state.parsed.rows.length - 1];
for (var prop in lastRow)
if (lastRow.hasOwnProperty(prop))
actual++;
if (actual < expected)
return addError("FieldMismatch", "TooFewFields", "Too few fields: expected " + expected + " fields but parsed " + actual);
else if (actual > expected)
return addError("FieldMismatch", "TooManyFields", "Too many fields: expected " + expected + " fields but parsed " + actual);
return true;
}
function addError(type, code, msg, errKey)
{
var row = _config.header
? (_state.parsed.rows.length ? _state.parsed.rows.length - 1 : undefined)
: _state.parsed.length - 1;
var key = errKey || row;
if (typeof _state.errors[key] === 'undefined')
_state.errors[key] = [];
_state.errors[key].push({
type: type,
code: code,
message: msg,
line: _state.lineNum,
row: row,
index: _state.i + _chunkOffset
});
_state.errors.length ++;
return false;
}
function returnable()
{
return {
results: _state.parsed,
errors: _state.errors,
meta: {
delimiter: _config.delimiter
}
};
}
function reset(input)
{
_invocations++;
if (_invocations > 1 && streaming())
_chunkOffset += input.length;
_state = freshState();
_input = input;
}
function freshState()
{
// If streaming, and thus parsing the input in chunks, this
// is careful to preserve what we've already got, when necessary.
var parsed;
if (_config.header)
{
parsed = {
fields: streaming() ? _state.parsed.fields || [] : [],
rows: streaming() && _invocations > 1 ? [ {} ] : []
};
}
else
parsed = [ [] ];
return {
i: 0,
lineNum: streaming() ? _state.lineNum : 1,
field: 0,
fieldVal: "",
line: "",
ch: "",
inQuotes: false,
parsed: parsed,
errors: { length: 0 }
};
}
}
})(jQuery);

6
jquery.parse.min.js vendored

File diff suppressed because one or more lines are too long

1261
papaparse.js

File diff suppressed because it is too large Load Diff

6
papaparse.min.js vendored

File diff suppressed because one or more lines are too long

14
parse.jquery.json

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
{
"name": "parse",
"version": "2.1.4",
"version": "3.0.0",
"title": "Papa Parse",
"description": "Papa is a powerful CSV (delimited text) parser that streams large files and gracefully handles malformed input.",
"description": "Powerful CSV parser that converts CSV to JSON and JSON to CSV. Supports web workers and streaming large files. Gracefully handles malformed input.",
"keywords": [
"csv",
"parse",
@ -18,7 +18,11 @@ @@ -18,7 +18,11 @@
"file",
"filereader",
"stream",
"encoding"
"encoding",
"workers",
"multi-threaded",
"threading",
"ajax"
],
"author": {
"name": "Matthew Holt",
@ -33,8 +37,8 @@ @@ -33,8 +37,8 @@
],
"homepage":"http://papaparse.com",
"docs": "http://papaparse.com/docs.html",
"demo": "http://papaparse.com/#demo",
"download": "https://raw.github.com/mholt/jquery.parse/master/jquery.parse.min.js",
"demo": "http://papaparse.com/demo.html",
"download": "https://raw.github.com/mholt/PapaParse/master/papaparse.min.js",
"dependencies": {
"jquery": ">=1.6.0"
}

1041
player/player.css

File diff suppressed because it is too large Load Diff

53
player/player.html

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<title>Papa Parse Player</title>
<meta charset="utf-8">
<link rel="stylesheet" href="player.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="../papaparse.js"></script>
<script src="player.js"></script>
</head>
<body>
<h1><a href="http://papaparse.com">Papa Parse</a> Player</h1>
<div class="grid-container">
<div class="grid-25">
<label><input type="checkbox" id="download"> Download</label>
<label><input type="checkbox" id="stream"> Stream</label>
<label><input type="checkbox" id="worker"> Worker thread</label>
<label><input type="checkbox" id="header"> Header row</label>
<label><input type="checkbox" id="dynamicTyping"> Dynamic typing</label>
<label>Preview: <input type="number" min="0" max="1000" placeholder="default" id="preview"></label>
<label>Encoding: <input type="text" id="encoding" placeholder="default" size="10"></label>
<label>Comment char: <input type="text" size="5" maxlength="1" placeholder="default" id="comments"></label>
<label>Delimiter: <input type="text" size="5" maxlength="1" placeholder="auto" id="delimiter"> <a href="javascript:" id="insert-tab">tab</a></label>
</div>
<div class="grid-75 text-center">
<textarea id="input" placeholder="Input">Column 1,Column 2,Column 3,Column 4
1-1,1-2,1-3,1-4
2-1,2-2,2-3,2-4
3-1,3-2,3-3,3-4
4,5,6,7</textarea>
<br>
<b>or</b>
<br>
<input type="file" id="files" multiple>
<br><br>
<button id="submit">Parse</button>
<br><br>
<i>Open the Console in your browser's inspector tools to see results.</i>
</div>
</div>
</body>
</html>

70
player/player.js

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
var stepped = 0;
var start, end;
$(function()
{
$('#submit').click(function()
{
stepped = 0;
var txt = $('#input').val();
var files = $('#files')[0].files;
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: $('#dynamicTyping').prop('checked'),
preview: parseInt($('#preview').val() || 0),
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);
}

127
tests.html

@ -1,127 +0,0 @@ @@ -1,127 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Parser Tests</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="jquery.parse.js"></script>
<script src="tests.js"></script>
<style>
body {
font-family: sans-serif;
}
#tmp {
white-space: pre;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 10px;
}
#results {
border-collapse: collapse;
width: 100%;
}
#results td {
vertical-align: top;
padding: 10px;
border-bottom: 10px solid white;
}
#results td div {
overflow-x: auto;
}
.count {
background: #333;
color: #DDD;
width: 2em;
text-align: center;
}
.input,
.output {
width: 25%;
}
.input {
background: #DDD;
}
.config {
background: #CCC;
}
.output {
background: #EEE;
}
.input code,
.config code,
.output code {
font: 12px/1.5em 'Menlo', 'Monaco', 'Courier New', monospace;
display: block;
white-space: pre;
}
.clr-green,
.passing {
color: #475B15;
}
.clr-red,
.failing {
color: #AA0000;
}
.passing {
background: #ECF9CC;
color: #475B15;
}
.failing {
background: #FFE8E8;
}
.failing code {
font-weight: bold;
}
hr {
border: 0;
background: 0;
clear: both;
}
.clr-green {
color: #79A01E;
}
.clr-red {
color: #AA0000;
}
#pass-count,
#fail-count {
font-weight: bold;
}
</style>
</head>
<body>
SUMMARY
&nbsp;
<span class="clr-green"><span id="pass-count">-</span> PASS</span>
&nbsp;
<span class="clr-red"><span id="fail-count">-</span> FAIL</span>
<br><br>
<table id="results">
<tr>
<th></th>
<th>Input</th>
<th>Config</th>
<th>Expected</th>
<th>Actual</th>
</tr>
</table>
<div id="output"></div>
</body>
</html>

693
tests.js

@ -1,693 +0,0 @@ @@ -1,693 +0,0 @@
var passCount = 0, failCount = 0;
var passing = "passing";
var failing = "failing";
var recordSep = String.fromCharCode(30);
var unitSep = String.fromCharCode(31);
var resultSet1 = [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": "V3"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: true },
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
2,
"V3"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: false },
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"2",
"V3"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: true, dynamicTyping: false },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": "2",
"F3": "V3"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: "", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": "V3"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
];
var tests = [
{
input: "F1,F2,F3\nV1,2,V3",
cases: resultSet1
},
{
input: "F1,F2,F3\r\nV1,2,V3",
cases: resultSet1
},
{
input: "F1,\"F2\",F3\r\nV1,2,\"V3\"",
cases: resultSet1
},
{
input: "F1,F2,F3\n\nV1,2,V3",
cases: resultSet1
},
{
input: "F1,F2,F3\r\n\r\nV1,2,V3",
cases: resultSet1
},
{
input: "F1,F2,F3\n\rV1,2,V3",
cases: resultSet1
},
{
input: "F1,F2,F3\rV1,2,V3",
cases: resultSet1
},
{
input: "F1,F2,F3\r\n \r\nV1,2,V3",
cases: resultSet1
},
{
input: "\r\nF1,F2,F3\r\nV1,2,V3",
cases: resultSet1
},
{
input: 'F1,F2,"F3"\n"V1","2",V3',
cases: resultSet1
},
{
input: "F1,F2,F3\nV1,2,V3\nV4,V5,V6",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": "V3"
},
{
"F1": "V4",
"F2": "V5",
"F3": "V6"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: true },
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
2,
"V3"
],
[
"V4",
"V5",
"V6"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: false },
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"2",
"V3"
],
[
"V4",
"V5",
"V6"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: true, dynamicTyping: false },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": "2",
"F3": "V3"
},
{
"F1": "V4",
"F2": "V5",
"F3": "V6"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: "F1,F2,F3\n,2,V3\nV4,V5,V6",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "",
"F2": 2,
"F3": "V3"
},
{
"F1": "V4",
"F2": "V5",
"F3": "V6"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: "F1,F2,F3\n,2,V3,V4\nV5,V6,V7",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "",
"F2": 2,
"F3": "V3",
"__parsed_extra": [
"V4"
]
},
{
"F1": "V5",
"F2": "V6",
"F3": "V7"
}
]
},
"errors": {
"0": [
{
"type": "FieldMismatch",
"code": "TooManyFields",
"message": "Too many fields: expected 3 fields but parsed 4",
"line": 2,
"row": 0,
"index": 17
}
],
"length": 1
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: "F1,F2,F3\nV1,2.0,-3.01, V4\n\rV5,\"V\n6\",V7\r,,",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": -3.01,
"__parsed_extra": [
" V4"
]
},
{
"F1": "V5",
"F2": "V\n6",
"F3": "V7"
},
{
"F1": "",
"F2": "",
"F3": ""
}
]
},
"errors": {
"0": [
{
"type": "FieldMismatch",
"code": "TooManyFields",
"message": "Too many fields: expected 3 fields but parsed 4",
"line": 2,
"row": 0,
"index": 25
}
],
"length": 1
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: "F1,F2,F3\nV1,V2,V3\nV5,\"V6,V7",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": "V2",
"F3": "V3"
},
{
"F1": "V5",
"F2": "V6,V7"
}
]
},
"errors": {
"1": [
{
"type": "FieldMismatch",
"code": "TooFewFields",
"message": "Too few fields: expected 3 fields but parsed 2",
"line": 3,
"row": 1,
"index": 27
},
{
"type": "Quotes",
"code": "MissingQuotes",
"message": "Unescaped or mismatched quotes",
"line": 3,
"row": 1,
"index": 27
}
],
"length": 2
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: "F1,F2,F3\n2,-2, 2\n 2. ,.2, .2 \n-2., -2.0e-5, -.4 ",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": 2,
"F2": -2,
"F3": 2
},
{
"F1": 2,
"F2": 0.2,
"F3": 0.2
},
{
"F1": -2,
"F2": -0.00002,
"F3": -0.4
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: "F1\nV1\nV2\nV3\nV4",
cases: [
{
config: { delimiter: "", header: false, dynamicTyping: false },
expected: {
"results": [
[
"F1"
],
[
"V1"
],
[
"V2"
],
[
"V3"
],
[
"V4"
]
],
"errors": {
"length": 1,
"config": [
{
"type": "Delimiter",
"code": "UndetectableDelimiter",
"message": "Unable to auto-detect delimiting character; defaulted to comma",
"line": 1,
"row": 0,
"index": 0
}
]
},
"meta": {
"delimiter": ","
}
}
}
]
},
{
input: ["F1","F2","F3\r\nV1","V2","V3"].join(recordSep),
cases: [
{
config: { delimiter: "", header: false, dynamicTyping: false },
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"V2",
"V3"
],
],
"errors": {
"length": 0
},
"meta": {
"delimiter": "\u001e"
}
}
}
]
},
{
input: ["F1","F2","F3\r\nV1","V2","V3"].join(unitSep),
cases: [
{
config: { delimiter: "", header: false, dynamicTyping: false },
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"V2",
"V3"
],
],
"errors": {
"length": 0
},
"meta": {
"delimiter": "\u001f"
}
}
}
]
}
];
$(function()
{
var counter = 0;
for (var i = 0; i < tests.length; i++)
{
var test = tests[i];
var input = test.input;
for (var j = 0; j < test.cases.length; j++)
{
counter++;
var testCase = test.cases[j];
var actual = doTest(input, testCase.config);
var status = equal(actual, testCase.expected) ? passing : failing;
render(input, testCase.expected, actual, testCase.config, counter, status);
}
}
$('#pass-count').text(passCount);
$('#fail-count').text(failCount);
});
function doTest(input, config)
{
// try
// {
return $.parse(input, config);
// }
// catch (e)
// {
// return {exception: e.message, error: e, note: "See console to inspect stack"};
// }
}
function render(input, expected, actual, config, count, status)
{
if (status == passing)
passCount++;
else
{
console.log("TEST " + count +" FAILED.");
console.log(" Expected:", expected);
console.log(" Actual:", actual);
console.log(" Config:", config);
failCount++;
}
var html = '<tr>' +
'<td class="count">'+count+'</td>' +
'<td class="input"><div><code>'+string(input)+'</code></div></td>' +
'<td class="config"><div><code>'+string(config)+'</code></div></td>' +
'<td class="output"><div><code>'+string(expected)+'</code></div></td>' +
'<td class="output '+status+'"><div><code>'+string(actual)+'</code></div></td>' +
'</tr>';
$('#results').append(html);
}
function string(obj)
{
return typeof obj === "string" ? obj : JSON.stringify(obj, undefined, 2);
}
function equal(actual, expected)
{
return string(actual) === string(expected);
}

401
tests/test-cases.js

@ -0,0 +1,401 @@ @@ -0,0 +1,401 @@
// 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);
var TESTS = [
{
input: 'A,b,c',
description: "One row",
expected: {
data: [['A', 'b', 'c']],
errors: []
}
},
{
input: 'A,b,c\r\nd,E,f',
description: "Two rows",
expected: {
data: [['A', 'b', 'c'], ['d', 'E', 'f']],
errors: []
}
},
{
input: 'A,b,c\rd,E,f',
description: "Two rows, just \\r",
expected: {
data: [['A', 'b', 'c'], ['d', 'E', 'f']],
errors: []
}
},
{
input: 'A,b,c\nd,E,f',
description: "Two rows, just \\n",
expected: {
data: [['A', 'b', 'c'], ['d', 'E', 'f']],
errors: []
}
},
{
input: 'a, b ,c',
description: "Whitespace at edges of unquoted field",
notes: "Extra whitespace should graciously be preserved",
expected: {
data: [['a', ' b ', 'c']],
errors: []
}
},
{
input: 'A,"B",C',
description: "Quoted field",
expected: {
data: [['A', 'B', 'C']],
errors: []
}
},
{
input: 'A," B ",C',
description: "Quoted field with extra whitespace on edges",
expected: {
data: [['A', ' B ', 'C']],
errors: []
}
},
{
input: 'A,"B,B",C',
description: "Quoted field with delimiter",
expected: {
data: [['A', 'B,B', 'C']],
errors: []
}
},
{
input: 'A,"B\r\nB",C',
description: "Quoted field with \\r\\n",
expected: {
data: [['A', 'B\r\nB', 'C']],
errors: []
}
},
{
input: 'A,"B\rB",C',
description: "Quoted field with \\r",
expected: {
data: [['A', 'B\rB', 'C']],
errors: []
}
},
{
input: 'A,"B\nB",C',
description: "Quoted field with \\n",
expected: {
data: [['A', 'B\nB', 'C']],
errors: []
}
},
{
input: 'A,"B""B""B",C',
description: "Quoted field with escaped quotes",
expected: {
data: [['A', 'B"B"B', 'C']],
errors: []
}
},
{
input: 'A,"""B""",C',
description: "Quoted field with escaped quotes at boundaries",
expected: {
data: [['A', '"B"', 'C']],
errors: []
}
},
{
input: 'A, "B" ,C',
description: "Quoted field with whitespace around quotes",
notes: "This is malformed input, but it should be parsed gracefully (with errors)",
expected: {
data: [['A', ' "B" ', 'C']],
errors: [
{"type": "Quotes", "code": "UnexpectedQuotes", "message": "Unexpected quotes", "line": 1, "row": 0, "index": 3},
{"type": "Quotes", "code": "UnexpectedQuotes", "message": "Unexpected quotes", "line": 1, "row": 0, "index": 5}
]
}
},
{
input: 'a\tb\tc\r\nd\te\tf',
config: { delimiter: "\t" },
description: "Tab delimiter",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a|b|c\r\nd|e|f',
config: { delimiter: "|" },
description: "Pipe delimiter",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a'+RECORD_SEP+'b'+RECORD_SEP+'c\r\nd'+RECORD_SEP+'e'+RECORD_SEP+'f',
config: { delimiter: RECORD_SEP },
description: "ASCII 30 delimiter",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a'+UNIT_SEP+'b'+UNIT_SEP+'c\r\nd'+UNIT_SEP+'e'+UNIT_SEP+'f',
config: { delimiter: UNIT_SEP },
description: "ASCII 31 delimiter",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c',
config: { delimiter: "DELIM" },
description: "Bad delimiter",
notes: "Should silently default to comma",
expected: {
data: [['a', 'b', 'c']],
errors: []
}
},
{
input: '# Comment!\r\na,b,c',
config: { comments: true },
description: "Commented line at beginning (comments: true)",
expected: {
data: [['a', 'b', 'c']],
errors: []
}
},
{
input: 'a,b,c\r\n# Comment\r\nd,e,f',
config: { comments: true },
description: "Commented line in middle (comments: true)",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\n# Comment',
config: { comments: true },
description: "Commented line at end (comments: true)",
expected: {
data: [['a', 'b', 'c']],
errors: []
}
},
{
input: 'a,b,c\r\n!Comment goes here\r\nd,e,f',
config: { comments: '!' },
description: "Comment with non-default character (comments: '!')",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\n=N(Comment)\r\nd,e,f',
config: { comments: '=N(' },
description: "Comment, but bad char specified (comments: \"=N(\")",
notes: "Should silently disable comment parsing",
expected: {
data: [['a', 'b', 'c'], ['=N(Comment)'], ['d', 'e', 'f']],
errors: []
}
},
{
input: '#commented line\r\n',
config: { comments: true, delimiter: ',' },
description: "Input with only a commented line (comments: true)",
expected: {
data: [],
errors: []
}
},
{
input: '#commented line',
config: { delimiter: ',' },
description: "Input with comment without comments enabled",
expected: {
data: [['#commented line']],
errors: []
}
},
{
input: 'a\r\n b\r\nc',
config: { delimiter: ',' },
description: "Input without comments with line starting with whitespace",
notes: "\" \" == false, but \" \" !== false, so === comparison is required",
expected: {
data: [['a'], [' b'], ['c']],
errors: []
}
},
{
input: 'a#b#c\r\n# Comment',
config: { delimiter: '#', comments: '#' },
description: "Comment char same as delimiter",
notes: "Comment parsing should automatically be silently disabled in this case",
expected: {
data: [['a', 'b', 'c'], ['', ' Comment']],
errors: []
}
},
{
input: '\r\na,b,c\r\nd,e,f',
description: "Blank line at beginning",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\n\r\nd,e,f',
description: "Blank line in middle",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\nd,e,f\r\n\r\n',
description: "Blank lines at end",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\n \r\nd,e,f',
description: "Blank line in middle with whitespace",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\n,e,f',
description: "First field of a line is empty",
expected: {
data: [['a', 'b', 'c'], ['', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\n,e,f',
description: "First field of a line is empty",
expected: {
data: [['a', 'b', 'c'], ['', 'e', 'f']],
errors: []
}
},
{
input: 'a,,c\r\n,,',
description: "Other fields are empty",
expected: {
data: [['a', '', 'c'], ['', '', '']],
errors: []
}
},
{
input: '',
description: "Empty input string",
expected: {
data: [],
errors: [{
"type": "Delimiter",
"code": "UndetectableDelimiter",
"message": "Unable to auto-detect delimiting character; defaulted to comma"
}]
}
},
{
input: ',',
description: "Input is just the delimiter (2 empty fields)",
expected: {
data: [['', '']],
errors: []
}
},
{
input: 'Abc def',
description: "Input is just a string (a single field)",
expected: {
data: [['Abc def']],
errors: [
{
"type": "Delimiter",
"code": "UndetectableDelimiter",
"message": "Unable to auto-detect delimiting character; defaulted to comma"
}
]
}
},
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 0 },
description: "Preview 0 rows should default to parsing all",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
errors: []
}
},
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 1 },
description: "Preview 1 row",
expected: {
data: [['a', 'b', 'c']],
errors: []
}
},
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 2 },
description: "Preview 2 rows",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 3 },
description: "Preview all (3) rows",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
errors: []
}
},
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 4 },
description: "Preview more rows than input has",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
errors: []
}
},
{
input: 'a,b,c\r\nd,e,"f\r\nf",g,h,i',
config: { preview: 2 },
description: "Preview should count rows, not lines",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f\r\nf', 'g', 'h', 'i']],
errors: []
}
}
];

171
tests/test-runner.js

@ -0,0 +1,171 @@ @@ -0,0 +1,171 @@
var passCount = 0;
var failCount = 0;
$(function()
{
// First, wireup!
$('#results').on('click', 'td.rvl', function()
{
var tr = $(this).closest('tr');
if (tr.hasClass('collapsed'))
{
$('.revealer', tr).hide();
$('.hidden', tr).show();
$(this).html("-");
}
else
{
$('.revealer', tr).show();
$('.hidden', tr).hide();
$(this).html("+");
}
tr.toggleClass('collapsed expanded');
});
$('#expand-all').click(function()
{
$('.collapsed .rvl').click();
});
$('#collapse-all').click(function()
{
$('.expanded .rvl').click();
});
// Next, run tests and render results!
for (var i = 0; i < TESTS.length; i++)
{
var test = TESTS[i];
var passed = runTest(test, i);
if (passed)
passCount++;
else
failCount++;
}
// Finally, show the overall status.
if (failCount == 0)
$('#status').addClass('status-pass').html("All <b>"+passCount+"</b> test"+(passCount == 1 ? "" : "s")+" passed");
else
$('#status').addClass('status-fail').html("<b>"+failCount+"</b> test"+(failCount == 1 ? "" : "s")+" failed; <b>"+passCount+"</b> passed");
});
function runTest(test, num)
{
var actual = Papa.parse(test.input, test.config);
var results = compare(actual.data, actual.errors, test.expected);
var testDescription = (test.description || "");
if (testDescription.length > 0)
testDescription += '<br>';
if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>';
var tr = '<tr class="collapsed" id="test-'+num+'">'
+ '<td class="rvl">+</td>'
+ '<td>' + testDescription + '</td>'
+ passOrFailTd(results.data)
+ passOrFailTd(results.errors)
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + JSON.stringify(test.config, null, 2) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + revealChars(test.input) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(test.expected.data, null, 4) + '\r\nerrors: ' + JSON.stringify(test.expected.errors, null, 4) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(actual.data, null, 4) + '\r\nerrors: ' + JSON.stringify(actual.errors, null, 4) + '</div></td>'
+ '</tr>';
$('#results').append(tr);
if (!results.data.passed || !results.errors.passed)
$('#test-'+num+' td.rvl').click();
return results.data.passed && results.errors.passed
}
function compare(actualData, actualErrors, expected)
{
var data = compareData(actualData, expected.data);
var errors = compareErrors(actualErrors, expected.errors);
return {
data: data,
errors: errors
}
}
function compareData(actual, expected)
{
var passed = true;
if (actual.length != expected.length)
passed = false;
for (var row = 0; row < expected.length; row++)
{
if (actual.length != expected.length)
{
passed = false;
break;
}
for (var col = 0; col < expected[row].length; col++)
{
if (actual[row].length != expected[row].length)
{
passed = false;
break;
}
var expectedVal = expected[row][col];
var actualVal = actual[row][col];
if (actualVal !== expectedVal)
{
passed = false;
break;
}
}
}
// We pass back an object right now, even though it only contains
// one value, because we might add details to the test results later
// (same with compareErrors below)
return {
passed: passed
};
}
function compareErrors(actual, expected)
{
var passed = JSON.stringify(actual) == JSON.stringify(expected);
return {
passed: passed
};
}
function passOrFailTd(result)
{
if (result.passed)
return '<td class="ok">OK</td>';
else
return '<td class="fail">FAIL</td>';
}
function revealChars(txt)
{
// Make spaces and tabs more obvious when glancing
txt = txt.replace(/( |\t)/ig, '<span class="whitespace-char">$1</span>');
txt = txt.replace(/(\r\n|\n\r|\r|\n)/ig, '<span class="whitespace-char special-char">$1</span>$1');
// Now make the line breaks within the spans actually appear on the page
txt = txt.replace(/">\r\n<\/span>/ig, '">\\r\\n</span>');
txt = txt.replace(/">\n\r<\/span>/ig, '">\\n\\r</span>');
txt = txt.replace(/">\r<\/span>/ig, '">\\r</span>');
txt = txt.replace(/">\n<\/span>/ig, '">\\n</span>');
return txt;
}

159
tests/tests.css

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/* Eric Meyer's Reset CSS v2.0 */
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
body {
background: #F0F0F0;
font: 14px 'Helvetica Neue', sans-serif;
color: #333;
padding: 30px 15px;
}
a {
color: rgb(0, 142, 236);
}
a:hover {
color: rgb(82, 186, 255);
}
b {
font-weight: bold;
}
i {
font-style: italic;
}
h1 {
text-align: center;
font-weight: bold;
font-size: 62px;
margin-bottom: 30px;
}
.status-pass,
.status-fail {
padding: 10px;
margin-bottom: 30px;
color: #FFF;
text-align: center;
text-transform: uppercase;
font-size: 18px;
letter-spacing: 1px;
font-weight: 100;
}
.status-pass {
background: rgb(3, 168, 3);
}
.status-fail {
background: #BB0000;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 5px;
}
table th,
table td {
padding: 5px;
border: 1px solid #BBB;
}
table th {
color: #000;
background: #DDD;
font-weight: bold;
padding: 10px 5px;
text-transform: uppercase;
}
table td {
background: #FFF;
color: #555;
font-size: 14px;
}
td.ok,
td.fail {
text-transform: uppercase;
font-weight: 300;
vertical-align: middle;
text-align: center;
width: 80px;
}
td.ok {
background: rgb(204, 250, 144);
}
td.fail {
background: rgb(255, 192, 192);
}
td.rvl {
background: #444;
color: #999;
vertical-align: middle;
text-align: center;
cursor: pointer;
width: 20px;
}
td.rvl:hover {
color: #FFF;
}
tr.collapsed td.revealable {
background: #ECECEC;
vertical-align: middle;
text-align: center;
font-family: 'Helvetica Neue', sans-serif;
text-transform: lowercase;
color: #AAA;
}
tr.expanded .revealer {
font-family: 'Helvetica Neue', sans-serif;
text-transform: lowercase;
font-size: 10px;
background: #FFF;
position: absolute;
display: block;
padding: 3px;
top: -5px;
right: -5px;
}
td .container {
position: relative;
}
.notes {
color: #888;
font-size: 12px;
}
.pre {
font-family: Menlo, Monaco, monospace;
white-space: pre-wrap;
}
td.pre {
font-size: 12px;
}
.hidden {
display: none;
}
.special-char {
color: #78B7E7;
}
.whitespace-char {
background: #D5FCFA;
}

34
tests/tests.html

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>Papa Parse Tests</title>
<meta charset="utf-8">
<link rel="stylesheet" href="tests.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="../papaparse.js"></script>
<script src="test-cases.js"></script>
<script src="test-runner.js"></script>
</head>
<body>
<h1>Papa Parse Tests</h1>
<div id="status"></div>
<a href="javascript:" id="expand-all">Expand all</a>
&middot;
<a href="javascript:" id="collapse-all">Collapse all</a>
<br>
<table id="results">
<tr>
<th colspan="2">Test Case</th>
<th>Data</th>
<th>Errors</th>
<th>Config</th>
<th>Input</th>
<th>Expected</th>
<th>Actual</th>
</tr>
</table>
</body>
</html>
Loading…
Cancel
Save