From 42d707fe4a180b8990cb753efc8b868da78e4842 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 21 Jun 2014 13:59:48 -0600 Subject: [PATCH] Core parser for 3.0 rewrite complete with all new tests --- Parser.js | 306 ++++++++++++++ test-runner.js | 172 ++++++++ tests.css | 159 +++++++ tests.html | 128 +----- tests.js | 1071 +++++++++++++++++------------------------------- 5 files changed, 1035 insertions(+), 801 deletions(-) create mode 100644 Parser.js create mode 100644 test-runner.js create mode 100644 tests.css diff --git a/Parser.js b/Parser.js new file mode 100644 index 0000000..c0ae88c --- /dev/null +++ b/Parser.js @@ -0,0 +1,306 @@ +/** + 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/test-runner.js b/test-runner.js new file mode 100644 index 0000000..7640f17 --- /dev/null +++ b/test-runner.js @@ -0,0 +1,172 @@ +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 "+passCount+" test"+(passCount == 1 ? "" : "s")+" passed"); + else + $('#status').addClass('status-fail').html(""+failCount+" test"+(failCount == 1 ? "" : "s")+" failed; "+passCount+" passed"); +}); + +function runTest(test, num) +{ + var parser = new Parser(test.config); + var actual = parser.parse(test.input); + + var results = compare(actual.data, actual.errors, test.expected); + + var testDescription = (test.description || ""); + if (testDescription.length > 0) + testDescription += '
'; + if (test.notes) + testDescription += '' + test.notes + ''; + + var tr = '' + + '+' + + '' + testDescription + '' + + passOrFailTd(results.data) + + passOrFailTd(results.errors) + + '
condensed
' + + '
condensed
' + + '
condensed
' + + '
condensed
' + + ''; + + $('#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 'OK'; + else + return 'FAIL'; +} + +function revealChars(txt) +{ + // Make spaces and tabs more obvious when glancing + txt = txt.replace(/( |\t)/ig, '$1'); + + txt = txt.replace(/(\r\n|\n\r|\r|\n)/ig, '$1$1'); + + // Now make the line breaks within the spans actually appear on the page + txt = txt.replace(/">\r\n<\/span>/ig, '">\\r\\n'); + txt = txt.replace(/">\n\r<\/span>/ig, '">\\n\\r'); + txt = txt.replace(/">\r<\/span>/ig, '">\\r'); + txt = txt.replace(/">\n<\/span>/ig, '">\\n'); + + return txt; +} \ No newline at end of file diff --git a/tests.css b/tests.css new file mode 100644 index 0000000..9fba093 --- /dev/null +++ b/tests.css @@ -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; +} \ No newline at end of file diff --git a/tests.html b/tests.html index acf8998..d8a2579 100644 --- a/tests.html +++ b/tests.html @@ -1,127 +1,33 @@ - Parser Tests - - + Tests - Papa Parse + + + - - - - SUMMARY -   - - PASS -   - - FAIL -

- - + + + +
InputTest CaseDataErrors ConfigInput Expected Actual
-
\ No newline at end of file diff --git a/tests.js b/tests.js index 713a387..10753e8 100644 --- a/tests.js +++ b/tests.js @@ -1,693 +1,384 @@ -var passCount = 0, failCount = 0; -var passing = "passing"; -var failing = "failing"; +var RECORD_SEP = String.fromCharCode(30); +var UNIT_SEP = String.fromCharCode(31); -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); +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: 'aDELIMbDELIMc', + config: { delimiter: "DELIM" }, + description: "Bad delimiter", + notes: "Should silently default to comma", + expected: { + data: [['aDELIMbDELIMc']], + 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 }, + description: "Input with only a commented line (comments: true)", + expected: { + data: [], + errors: [] + } + }, + { + input: '#commented line', + description: "Input with comment without comments enabled", + expected: { + data: [['#commented line']], + errors: [] + } + }, + { + input: 'a\r\n b\r\nc', + 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: [] + } + }, + { + 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: [] + } + }, + { + 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: [] } } - - $('#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 = '' + - ''+count+'' + - '
'+string(input)+'
' + - '
'+string(config)+'
' + - '
'+string(expected)+'
' + - '
'+string(actual)+'
' + - ''; - $('#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); -} \ No newline at end of file +]; \ No newline at end of file