├── .gitignore ├── README.md ├── bower.json ├── csv.html ├── csv.js ├── csv.txt ├── package.json └── unit ├── index.html └── lib └── ok.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | *~ 4 | github/ 5 | node_modules/ 6 | local/ 7 | idea 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csv-js 2 | A Comma-Separated Values parser for JavaScript 3 | 4 | ----- 5 | 6 | Built to the rfc4180 standard, with adjustable strictness: 7 | 8 | - optional carriage returns for non-microsoft sources 9 | - automatically type-cast numeric an boolean values 10 | - An optional "relaxed" mode which: 11 | - ignores blank lines 12 | - ignores garbage following quoted tokens 13 | - does not enforce a consistent record length 14 | 15 | Example: 16 | ---- 17 | [Live Example](http://gkindel.github.io/CSV-JS/csv.html) 18 | 19 | 20 | Install 21 | ---- 22 | `npm install csv-js --save-dev` 23 | 24 | 25 | Use: 26 | ---- 27 | Simple: 28 | 29 | var rows = CSV.parse("one,two,three\n4,5,6") 30 | // rows equals [["one","two","three"],[4,5,6]] 31 | 32 | jQuery AJAX suggestion: 33 | 34 | $.get("csv.txt") 35 | .pipe( CSV.parse ) 36 | .done( function(rows) { 37 | for( var i =0; i < rows.length; i++){ 38 | console.log(rows[i]) 39 | } 40 | }); 41 | 42 | Options: 43 | ---- 44 | 45 | CSV.RELAXED 46 | 47 | > Try this first if you're having problems with data parsing. Enables a "relaxed" strictness mode. Default: `false` 48 | 49 | - Ignores blank lines; 50 | - Ignores garbage characters following a close quote 51 | - Ignores inconsistent records lengths 52 | - Ignore whitespace around quoted strings. 53 | 54 | CSV.IGNORE\_RECORD\_LENGTH 55 | 56 | > If relaxed mode is not already enabled, ignores inconsistent records lengths Default: `false` 57 | 58 | CSV.IGNORE\_QUOTES 59 | 60 | > Treats all values as literal, including surrounding quotes. For use if CSV isn't well formatted. This will disable escape sequences. Default: `false` 61 | 62 | CSV.LINE\_FEED\_OK 63 | 64 | > Suppress exception for missing carriage returns (specification requires CRLF line endings). Default: `true` 65 | 66 | CSV.CARRIAGE\_RETURN\_OK 67 | 68 | > Suppress exception for missing line feeds (specification requires CRLF line endings). Default: `true` 69 | 70 | CSV.DETECT\_TYPES 71 | 72 | > Automatically type-cast numeric and boolean values such as "false", "null", and "0.1", but not "abcd", "Null", or ".1". Customizable by overriding CSV.resolve_type(str) which returns value. Default: `true` 73 | 74 | CSV.IGNORE\_QUOTE\_WHITESPACE 75 | 76 | > Detects and ignores whitespace before a quoted string which, per spec, should be treated as the start of an unescaped value. Default: `true` 77 | 78 | CSV.DEBUG 79 | 80 | > Enables debug logging to console. Default: `false` 81 | 82 | CSV.COLUMN\_SEPARATOR 83 | 84 | > Split columns by this character. Default "," (comma). 85 | 86 | Exceptions Thrown: 87 | ---- 88 | 89 | *"UNEXPECTED\_END\_OF\_FILE"* or `CSV.ERROR_EOF` 90 | 91 | > Fired when file ends unexpectedly. Eg. File ends during an open escape sequence. Example: 92 | 93 | >`Uncaught UNEXPECTED_END_OF_FILE at char 72 : ption,Price\n1997,Ford,E350,"ac, abs, moon,3000.00` 94 | 95 | *"UNEXPECTED_CHARACTER"* or `CSV.ERROR_CHAR` 96 | 97 | > Fired when an invalid character is detected. Eg. A non-comma after the close of an quoted value. Example: 98 | 99 | > `Uncaught UNEXPECTED_CHARACTER at char 250 : rand Cherokee,"MUST SELL!\nair, moon roof, loaded"z` 100 | 101 | 102 | *"UNEXPECTED_END_OF_RECORD"* or `CSV.ERROR_EOL` 103 | 104 | > Fired when a record ends before the expected number of fields is read (as determined by first row). Example: 105 | 106 | > `Uncaught UNEXPECTED_END_OF_RECORD at char 65 : ,Description,Price\n1997,Ford,E350,"ac, abs, moon"\n ` 107 | 108 | Warnings: 109 | ---- 110 | 111 | *"UNEXPECTED\_WHITESPACE"* or `CSV.WARN_WHITESPACE` 112 | 113 | > Appears when whitespace is encountered outside of a quoted value, only if CSV.IGNORE_QUOTE_WHITESPACE is disabled. Example: 114 | 115 | >`UNEXPECTED_WHITESPACE at char 330 : e,false,123,45.6\n.7,8.,9.1.2,null,undefined\nNull, "` 116 | 117 | Unit Test: 118 | ---- 119 | [Unit Test](http://gkindel.github.io/CSV-JS/unit/) 120 | 121 | License: 122 | ---- 123 | Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 124 | 125 | Permission is hereby granted, free of charge, to any person obtaining a copy 126 | of this software and associated documentation files (the "Software"), to deal 127 | in the Software without restriction, including without limitation the rights 128 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 129 | copies of the Software, and to permit persons to whom the Software is 130 | furnished to do so, subject to the following conditions: 131 | 132 | The above copyright notice and this permission notice shall be included in 133 | all copies or substantial portions of the Software. 134 | 135 | Author: 136 | ---- 137 | Greg Kindel (twitter @gkindel), 2017 138 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-js", 3 | "version": "1.1.1", 4 | "description": "A Comma-Separated Values parser for JavaScript. Standards-based, stand alone, and no regular expressions.", 5 | "main": "csv.js", 6 | "keywords": [ 7 | "csv", 8 | "comma", 9 | "separated", 10 | "values" 11 | ], 12 | "authors": [ 13 | "Greg Kindel (twitter @gkindel)", 14 | "2014" 15 | ], 16 | "license": "MIT", 17 | "homepage": "https://github.com/gkindel/csv-js", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "unit", 23 | "test", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /csv.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CSV.parse() Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 50 | 76 | 77 | 78 |

JavaScript CSV parser

79 | 80 |

Use:

81 |

 82 | 
 83 |     // simple
 84 |     var rows = CSV.parse( csv_text );
 85 | 
 86 |     // w/ jQuery
 87 |     $.ajax("csv.txt")
 88 |         .pipe( CSV.parse )
 89 |         .done( function(rows) {
 90 |             //... do something
 91 |         });
92 | 93 |

Example:

94 | 95 |

Input (link):

96 |

 97 | 
 98 | 

Output as HTML Table:

99 |
100 | 101 |

Output as JSON:

102 |
103 | 104 |

About

105 |
106 | Author: gkindel 107 |
108 | License: MIT 109 |
110 | Further Reading: 111 | 115 |
116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /csv.js: -------------------------------------------------------------------------------- 1 | /* 2 | CSV-JS - A Comma-Separated Values parser for JS 3 | 4 | Built to rfc4180 standard, with options for adjusting strictness: 5 | - optional carriage returns for non-microsoft sources 6 | - automatically type-cast numeric an boolean values 7 | - relaxed mode which: ignores blank lines, ignores gargabe following quoted tokens, does not enforce a consistent record length 8 | 9 | Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | Author Greg Kindel (twitter @gkindel), 2014 22 | */ 23 | 24 | (function (global) { 25 | 'use strict'; 26 | /** 27 | * @name CSV 28 | * @namespace 29 | */ 30 | // implemented as a singleton because JS is single threaded 31 | var CSV = {}; 32 | CSV.RELAXED = false; 33 | CSV.IGNORE_RECORD_LENGTH = false; 34 | CSV.IGNORE_QUOTES = false; 35 | CSV.LINE_FEED_OK = true; 36 | CSV.CARRIAGE_RETURN_OK = true; 37 | CSV.DETECT_TYPES = true; 38 | CSV.IGNORE_QUOTE_WHITESPACE = true; 39 | CSV.DEBUG = false; 40 | 41 | CSV.COLUMN_SEPARATOR = ","; 42 | 43 | CSV.ERROR_EOF = "UNEXPECTED_END_OF_FILE"; 44 | CSV.ERROR_CHAR = "UNEXPECTED_CHARACTER"; 45 | CSV.ERROR_EOL = "UNEXPECTED_END_OF_RECORD"; 46 | CSV.WARN_SPACE = "UNEXPECTED_WHITESPACE"; // not per spec, but helps debugging 47 | 48 | var QUOTE = "\"", 49 | CR = "\r", 50 | LF = "\n", 51 | SPACE = " ", 52 | TAB = "\t"; 53 | 54 | // states 55 | var PRE_TOKEN = 0, 56 | MID_TOKEN = 1, 57 | POST_TOKEN = 2, 58 | POST_RECORD = 4; 59 | /** 60 | * @name CSV.parse 61 | * @function 62 | * @description rfc4180 standard csv parse 63 | * with options for strictness and data type conversion 64 | * By default, will automatically type-cast numeric an boolean values. 65 | * @param {String} str A CSV string 66 | * @return {Array} An array records, each of which is an array of scalar values. 67 | * @example 68 | * // simple 69 | * var rows = CSV.parse("one,two,three\nfour,five,six") 70 | * // rows equals [["one","two","three"],["four","five","six"]] 71 | * @example 72 | * // Though not a jQuery plugin, it is recommended to use with the $.ajax pipe() method: 73 | * $.get("csv.txt") 74 | * .pipe( CSV.parse ) 75 | * .done( function(rows) { 76 | * for( var i =0; i < rows.length; i++){ 77 | * console.log(rows[i]) 78 | * } 79 | * }); 80 | * @see http://www.ietf.org/rfc/rfc4180.txt 81 | */ 82 | CSV.parse = function (str) { 83 | var result = CSV.result = []; 84 | CSV.offset = 0; 85 | CSV.str = str; 86 | CSV.record_begin(); 87 | 88 | CSV.debug("parse()", str); 89 | 90 | var c; 91 | while( 1 ){ 92 | // pull char 93 | c = str[CSV.offset++]; 94 | CSV.debug("c", c); 95 | 96 | // detect eof 97 | if (c == null) { 98 | if( CSV.escaped ) 99 | CSV.error(CSV.ERROR_EOF); 100 | 101 | if( CSV.record ){ 102 | CSV.token_end(); 103 | CSV.record_end(); 104 | } 105 | 106 | CSV.debug("...bail", c, CSV.state, CSV.record); 107 | CSV.reset(); 108 | break; 109 | } 110 | 111 | if( CSV.record == null ){ 112 | // if relaxed mode, ignore blank lines 113 | if( CSV.RELAXED && (c == LF || c == CR && str[CSV.offset + 1] == LF) ){ 114 | continue; 115 | } 116 | CSV.record_begin(); 117 | } 118 | 119 | // pre-token: look for start of escape sequence 120 | if (CSV.state == PRE_TOKEN) { 121 | 122 | if ( (c === SPACE || c === TAB) && CSV.next_nonspace() == QUOTE ){ 123 | if( CSV.RELAXED || CSV.IGNORE_QUOTE_WHITESPACE ) { 124 | continue; 125 | } 126 | else { 127 | // not technically an error, but ambiguous and hard to debug otherwise 128 | CSV.warn(CSV.WARN_SPACE); 129 | } 130 | } 131 | 132 | if (c == QUOTE && ! CSV.IGNORE_QUOTES) { 133 | CSV.debug("...escaped start", c); 134 | CSV.escaped = true; 135 | CSV.state = MID_TOKEN; 136 | continue; 137 | } 138 | CSV.state = MID_TOKEN; 139 | } 140 | 141 | // mid-token and escaped, look for sequences and end quote 142 | if (CSV.state == MID_TOKEN && CSV.escaped) { 143 | if (c == QUOTE) { 144 | if (str[CSV.offset] == QUOTE) { 145 | CSV.debug("...escaped quote", c); 146 | CSV.token += QUOTE; 147 | CSV.offset++; 148 | } 149 | else { 150 | CSV.debug("...escaped end", c); 151 | CSV.escaped = false; 152 | CSV.state = POST_TOKEN; 153 | } 154 | } 155 | else { 156 | CSV.token += c; 157 | CSV.debug("...escaped add", c, CSV.token); 158 | } 159 | continue; 160 | } 161 | 162 | // fall-through: mid-token or post-token, not escaped 163 | if (c == CR ) { 164 | if( str[CSV.offset] == LF ) 165 | CSV.offset++; 166 | else if( ! CSV.CARRIAGE_RETURN_OK ) 167 | CSV.error(CSV.ERROR_CHAR); 168 | CSV.token_end(); 169 | CSV.record_end(); 170 | } 171 | else if (c == LF) { 172 | if( ! (CSV.LINE_FEED_OK || CSV.RELAXED) ) 173 | CSV.error(CSV.ERROR_CHAR); 174 | CSV.token_end(); 175 | CSV.record_end(); 176 | } 177 | else if (c == CSV.COLUMN_SEPARATOR) { 178 | CSV.token_end(); 179 | } 180 | else if( CSV.state == MID_TOKEN ){ 181 | CSV.token += c; 182 | CSV.debug("...add", c, CSV.token); 183 | } 184 | else if ( c === SPACE || c === TAB) { 185 | if (! CSV.IGNORE_QUOTE_WHITESPACE ) 186 | CSV.error(CSV.WARN_SPACE ); 187 | } 188 | else if( ! CSV.RELAXED ){ 189 | CSV.error(CSV.ERROR_CHAR); 190 | } 191 | } 192 | return result; 193 | }; 194 | 195 | /** 196 | * @name CSV.stream 197 | * @function 198 | * @description stream a CSV file 199 | * @example 200 | * node -e "c=require('CSV-JS');require('fs').createReadStream('csv.txt').pipe(c.stream()).pipe(c.stream.json()).pipe(process.stdout)" 201 | */ 202 | CSV.stream = function () { 203 | var stream = require('stream'); 204 | var s = new stream.Transform({objectMode: true}); 205 | s.EOL = '\n'; 206 | s.prior = ""; 207 | s.emitter = function(s) { 208 | return function(e) { 209 | s.push(CSV.parse(e+s.EOL)) 210 | } 211 | }(s); 212 | 213 | s._transform = function(chunk, encoding, done) { 214 | var lines = (this.prior == "") ? 215 | chunk.toString().split(this.EOL) : 216 | (this.prior + chunk.toString()).split(this.EOL); 217 | this.prior = lines.pop(); 218 | lines.forEach(this.emitter); 219 | done() 220 | }; 221 | 222 | s._flush = function(done) { 223 | if (this.prior != "") { 224 | this.emitter(this.prior) 225 | this.prior = "" 226 | } 227 | done() 228 | }; 229 | return s 230 | }; 231 | 232 | CSV.stream.json = function () { 233 | var os = require('os'); 234 | var stream = require('stream'); 235 | var s = new streamTransform({objectMode: true}); 236 | s._transform = function(chunk, encoding, done) { 237 | s.push(JSON.stringify(chunk.toString())+os.EOL); 238 | done() 239 | }; 240 | return s 241 | }; 242 | 243 | CSV.reset = function () { 244 | CSV.state = null; 245 | CSV.token = null; 246 | CSV.escaped = null; 247 | CSV.record = null; 248 | CSV.offset = null; 249 | CSV.result = null; 250 | CSV.str = null; 251 | }; 252 | 253 | CSV.next_nonspace = function () { 254 | var i = CSV.offset; 255 | var c; 256 | while( i < CSV.str.length ) { 257 | c = CSV.str[i++]; 258 | if( !( c == SPACE || c === TAB ) ){ 259 | return c; 260 | } 261 | } 262 | return null; 263 | }; 264 | 265 | CSV.record_begin = function () { 266 | CSV.escaped = false; 267 | CSV.record = []; 268 | CSV.token_begin(); 269 | CSV.debug("record_begin"); 270 | }; 271 | 272 | CSV.record_end = function () { 273 | CSV.state = POST_RECORD; 274 | if( ! (CSV.IGNORE_RECORD_LENGTH || CSV.RELAXED) && CSV.result.length > 0 && CSV.record.length != CSV.result[0].length ){ 275 | CSV.error(CSV.ERROR_EOL); 276 | } 277 | CSV.result.push(CSV.record); 278 | CSV.debug("record end", CSV.record); 279 | CSV.record = null; 280 | }; 281 | 282 | CSV.resolve_type = function (token) { 283 | if( token.match(/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/) ){ 284 | token = parseFloat(token); 285 | } 286 | else if( token.match(/^(true|false)$/i) ){ 287 | token = Boolean( token.match(/true/i) ); 288 | } 289 | else if(token === "undefined" ){ 290 | token = undefined; 291 | } 292 | else if(token === "null" ){ 293 | token = null; 294 | } 295 | return token; 296 | }; 297 | 298 | CSV.token_begin = function () { 299 | CSV.state = PRE_TOKEN; 300 | // considered using array, but http://www.sitepen.com/blog/2008/05/09/string-performance-an-analysis/ 301 | CSV.token = ""; 302 | }; 303 | 304 | CSV.token_end = function () { 305 | if( CSV.DETECT_TYPES ) { 306 | CSV.token = CSV.resolve_type(CSV.token); 307 | } 308 | CSV.record.push(CSV.token); 309 | CSV.debug("token end", CSV.token); 310 | CSV.token_begin(); 311 | }; 312 | 313 | CSV.debug = function (){ 314 | if( CSV.DEBUG ) 315 | console.log(arguments); 316 | }; 317 | 318 | CSV.dump = function (msg) { 319 | return [ 320 | msg , "at char", CSV.offset, ":", 321 | CSV.str.substr(CSV.offset- 50, 50) 322 | .replace(/\r/mg,"\\r") 323 | .replace(/\n/mg,"\\n") 324 | .replace(/\t/mg,"\\t") 325 | ].join(" "); 326 | }; 327 | 328 | CSV.error = function (err){ 329 | var msg = CSV.dump(err); 330 | CSV.reset(); 331 | throw msg; 332 | }; 333 | 334 | CSV.warn = function (err){ 335 | var msg = CSV.dump(err); 336 | try { 337 | console.warn( msg ); 338 | return; 339 | } catch (e) {} 340 | 341 | try { 342 | console.log( msg ); 343 | } catch (e) {} 344 | 345 | }; 346 | 347 | 348 | // Node, PhantomJS, etc 349 | // eg. var CSV = require("CSV"); CSV.parse(...); 350 | if ( typeof module != 'undefined' && module.exports) { 351 | module.exports = CSV; 352 | } 353 | 354 | // CommonJS http://wiki.commonjs.org/wiki/Modules 355 | // eg. var CSV = require("CSV").CSV; CSV.parse(...); 356 | else if (typeof exports != 'undefined' ) { 357 | exports.CSV = CSV; 358 | } 359 | 360 | // AMD https://github.com/amdjs/amdjs-api/wiki/AMD 361 | // eg. require(['./csv.js'], function (CSV) { CSV.parse(...); } ); 362 | else if (typeof define == 'function' && typeof define.amd == 'object'){ 363 | define([], function () { 364 | return CSV; 365 | }); 366 | } 367 | 368 | // standard js global 369 | // eg. CSV.parse(...); 370 | else if( global ){ 371 | global.CSV = CSV; 372 | } 373 | 374 | })(this); 375 | -------------------------------------------------------------------------------- /csv.txt: -------------------------------------------------------------------------------- 1 | Year,Make,Model,Description,Price 2 | 1997,Ford,E350,"ac, abs, moon",3000.00 3 | 1999,Chevy,"Venture ""Extended Edition""","",4900.00 4 | 1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00 5 | 1996,Jeep,Grand Cherokee,"MUST SELL! 6 | air, moon roof, loaded",4799.00 7 | "weird""""quotes ",true,false,123,45.6 8 | .7,8.,9.1.2,null,undefined 9 | Null, "ok whitespace outside quotes" ,trailing unquoted , both , leading 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-js", 3 | "version": "1.0.0", 4 | "description": "A Comma-Separated Values parser for JavaScript. Standards-based, stand alone, and no regular expressions.", 5 | "main": "csv.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/gkindel/csv-js.git" 9 | }, 10 | "author": "Greg Kindel (twitter @gkindel)", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/gkindel/csv-js/issues" 14 | }, 15 | "homepage": "https://github.com/gkindel/csv-js#readme", 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "keywords": [ 20 | "comma", 21 | "parse", 22 | "csv" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /unit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

CSV-JS Test Harness

11 | 12 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /unit/lib/ok.js: -------------------------------------------------------------------------------- 1 | /* 2 | ojks - a tiny asynchronous-friendly JS unit test framework 3 | 4 | Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | Available on GitHub: https://github.com/gkindel/okjs 17 | 18 | Author: Greg Kindel 19 | Created: "Unit.js" Spring 2011 20 | Updated; added JSON.stringify for object/array comparison, Aug 2013 21 | */ 22 | 23 | (function () { 24 | 25 | var defaults = { 26 | verbose : true, 27 | exceptions: false, 28 | timeout : 5000 29 | }; 30 | 31 | var okjs = function (options) { 32 | if( !(this instanceof okjs) ) 33 | return new okjs( options ); 34 | 35 | this.options = merge(defaults, options); 36 | this.location = window.location.toString(); 37 | 38 | if( window.frameElement && window.frameElement.__OK_PARENT__ ){ 39 | this._parent = window.frameElement.__OK_PARENT__; 40 | } 41 | 42 | this.logger = this.options.logger; 43 | 44 | if( ! this.logger ) { 45 | DefaultLogger(this); 46 | } 47 | 48 | this._init(); 49 | }; 50 | 51 | window.okjs = okjs; 52 | 53 | okjs.prototype = { 54 | 55 | // public 56 | 57 | assert : function (message, result, expected) { 58 | var error = ""; 59 | var hasExpected = arguments.length == 3; 60 | 61 | // if expected defined, then do strict compare 62 | if( hasExpected && ! compare(result, expected) ) 63 | error = "Expected: " + expected + ", got " + result; 64 | 65 | // else test for true-ish 66 | if( ! hasExpected && ! Boolean(result) ) 67 | error = "Expected: " + true + ", got " + result; 68 | 69 | this.report(message, error); 70 | }, 71 | 72 | forbid : function (message, result, forbid) { 73 | var error = ""; 74 | var hasForbid = arguments.length == 3; 75 | 76 | // if expected defined, then do strict compare 77 | if( hasForbid && compare(result, forbid) ) 78 | error = "Forbidden Result: " + result; 79 | 80 | // else test for false-ish 81 | if( ! hasForbid && Boolean(result) ) 82 | error = "Expected: " + false + ", got " + result; 83 | 84 | this.report(message, error); 85 | }, 86 | 87 | test : function (message, code, expectErrors) { 88 | var test = { 89 | message: message, 90 | code : code, 91 | expectErrors : expectErrors 92 | }; 93 | 94 | // if subtest, schedule immediately 95 | if( this._running ) 96 | this._subtests.push(test); 97 | 98 | // else tag on end 99 | else 100 | this._queue.push(test); 101 | }, 102 | 103 | callback : function (message, callback, scope, timeout) { 104 | 105 | // legacy syntax: function (message, timeout, callback, scope) 106 | if( typeof callback == "number") { 107 | timeout = arguments[1]; 108 | callback = arguments[2]; 109 | scope = arguments[3]; 110 | } 111 | 112 | if( timeout == null ) 113 | timeout = this.options.timeout; 114 | 115 | if( callback && ! (callback instanceof Function) ) 116 | throw "okjs.callback() invalid function: " + callback; 117 | 118 | this._hold(); 119 | 120 | // dead man's trigger 121 | var active = true; 122 | var timer = setTimeout( this._bind( function () { 123 | active = false; 124 | this.report(message, "Timeout: " + timeout + "ms"); 125 | this._release(); 126 | }), timeout); 127 | 128 | // wrap callback in exception-catching environment 129 | return this._bind( function () { 130 | clearTimeout(timer); 131 | if(! active ) 132 | return; 133 | active = false; 134 | this._eval(message, callback, scope, arguments); 135 | this._release(); 136 | }) 137 | }, 138 | 139 | event : function (message, object, type, callback, scope, timeout) { 140 | if( callback && ! (callback instanceof Function) ) 141 | throw "okjs.listen() invalid function: " + callback; 142 | 143 | // optional form: function (message, object, type, callback, timeout) { 144 | if( typeof scope == "number"){ 145 | timeout = scope; 146 | scope = null; 147 | } 148 | 149 | var wrapped = this.callback(message, callback, scope, timeout); 150 | var onEvent = function () { 151 | object.removeEventListener(type, onEvent); 152 | wrapped.apply(this, arguments); 153 | }; 154 | object.addEventListener( type, onEvent ); 155 | }, 156 | 157 | exception : function (message, callback, scope) { 158 | var error = "Exception not fired"; 159 | try { 160 | callback && callback.apply(scope, callback); 161 | } 162 | catch (e) { 163 | error = ""; 164 | } 165 | this.report(message, error); 166 | }, 167 | 168 | url : function (message, url) { 169 | var test = { 170 | message: message, 171 | url : url 172 | }; 173 | 174 | if( this._running ) 175 | this._queue.unshift(test); 176 | else 177 | this._queue.push(test); 178 | }, 179 | 180 | log : function ( message) { 181 | this.logger.onInfo({ 182 | message: message 183 | }); 184 | }, 185 | 186 | skip : function ( message) { 187 | this.skipped++; 188 | this.logger.onSkip({ 189 | message: message 190 | }); 191 | }, 192 | 193 | report : function ( message, error) { 194 | 195 | if( error && ! this._expectErrors ) { 196 | this.failed++; 197 | this.logger.onFail({ 198 | message: message, 199 | error: error 200 | }); 201 | } 202 | else { 203 | this.passed++; 204 | this.logger.onSuccess({ 205 | message: message 206 | }); 207 | } 208 | }, 209 | 210 | start : function () { 211 | this._running = true; 212 | this._started = now(); 213 | this._resume(); 214 | }, 215 | 216 | // logger utilities 217 | 218 | time : function () { 219 | if(! this._started ) 220 | return null; 221 | return now() - this._started; 222 | }, 223 | 224 | 225 | // "private" 226 | 227 | _init : function () { 228 | this._queue = []; 229 | this._subtests = []; 230 | this._blocks = 0; 231 | this.skipped = 0; 232 | this.failed = 0; 233 | this.passed = 0; 234 | this._running = false; 235 | }, 236 | 237 | _hold : function () { 238 | this._blocks++; 239 | }, 240 | 241 | _release : function () { 242 | this._blocks--; 243 | // resume, asynchronously 244 | setTimeout(this._bind( function () { 245 | this._resume(); 246 | }), 0); 247 | }, 248 | 249 | _eval : function (message, code, scope, args){ 250 | if( this.options.exceptions ) { 251 | code && code.apply(scope, args); 252 | this.report(message); 253 | return; 254 | } 255 | 256 | var error; 257 | try { 258 | code && code.apply(scope, args); 259 | } 260 | catch(e){ 261 | error = "Exception: " + e; 262 | } 263 | this.report(message, error); 264 | }, 265 | 266 | _resume : function () { 267 | if( ! this._running ) { 268 | return; 269 | } 270 | 271 | // waiting for some async 272 | if( this._blocks ) { 273 | return; 274 | } 275 | // no more tests, we're done 276 | if( ! this._queue.length ){ 277 | this._finish(); 278 | return; 279 | } 280 | 281 | // onto the next test 282 | this._subtests = []; 283 | var test = this._queue.shift(); 284 | this._expectErrors = test.expectErrors; 285 | this.logger.onTest( test ); 286 | 287 | this._hold(); 288 | 289 | if( test.code ) 290 | this._eval(test.message, test.code); 291 | 292 | if( test.url ) 293 | this._url(test.url); 294 | 295 | this._release(); 296 | 297 | // queue up any subtests generated during run 298 | this._queue = this._subtests.concat( this._queue); 299 | }, 300 | 301 | _url : function (url){ 302 | this._hold(); 303 | var el = _okframe(true); 304 | el.src = url; 305 | el.__OK_PARENT__ = this; 306 | this.logger.onInfo({ 307 | url: url 308 | }); 309 | }, 310 | 311 | _remote : function (unit) { 312 | if( ! this._running ) 313 | return; 314 | this.logger.onRemote(unit); 315 | this.passed += unit.passed; 316 | this.failed += unit.failed; 317 | this._release(); 318 | }, 319 | 320 | _finish : function () { 321 | this._running = false; 322 | 323 | if( this._parent ) 324 | this._parent._remote(this); 325 | 326 | var el = _el("okframe"); 327 | if( el ){ 328 | // el.src = "about:blank"; 329 | // el.style.display = "none" 330 | } 331 | this.logger.onFinish(); 332 | }, 333 | 334 | // utility: scope binding to this object 335 | _bind : function ( callback ) { 336 | var self = this; 337 | return function (){ 338 | callback.apply(self, arguments); 339 | } 340 | } 341 | 342 | }; 343 | 344 | /** 345 | * Default Logger 346 | */ 347 | var DefaultLogger = function ( okjs ) { 348 | 349 | if( !(this instanceof DefaultLogger) ) 350 | return new DefaultLogger( okjs ); 351 | 352 | defaultCSS(); 353 | this.unit = okjs; 354 | this.unit.logger = this; 355 | 356 | this._started = now(); 357 | 358 | // set up output div 359 | this.output = _el('output'); 360 | 361 | if( ! this.output ) { 362 | this.output = document.createElement('div'); 363 | this.output.setAttribute('id', 'output'); 364 | var body = document.getElementsByTagName('body')[0]; 365 | body.appendChild(this.output); 366 | } 367 | 368 | }; 369 | 370 | DefaultLogger.prototype = { 371 | _log : function (type, message) { 372 | this.output.appendChild( div( "item " + type, 373 | this._timestamp(), '::', type, "::", message 374 | )); 375 | this._scroll(); 376 | }, 377 | 378 | _scroll : function (){ 379 | var large = Math.pow(2, 30); 380 | try { 381 | window.scrollY = window.pageYOffset = large; 382 | } 383 | catch(e) {} 384 | window.scroll && window.scroll(0, large); // crx 385 | }, 386 | 387 | _timestamp : function () { 388 | return ( this.unit.time() / 1000 ) + 's'; 389 | }, 390 | 391 | _summary : function () { 392 | 393 | var type = "summary"; 394 | if( this.unit.failed ) 395 | type += " error"; 396 | else if( this.unit.skipped ) 397 | type += " skip"; 398 | else 399 | type += " success"; 400 | 401 | this.output.appendChild(div( type, 402 | this.unit.passed + " test" + (this.unit.passed == 1 ? '' : 's') + " completed. " 403 | + (this.unit.skipped ? this.unit.skipped + " groups skipped. " : '') 404 | + this.unit.failed + " error" +(this.unit.failed == 1 ? '' : 's')+ ". " 405 | + this._timestamp() 406 | )); 407 | }, 408 | 409 | onTest : function ( group ) { 410 | this.output.appendChild( div("group", group.message )); 411 | this._scroll(); 412 | }, 413 | 414 | onSuccess : function (result) { 415 | this._log("ok", result.message); 416 | }, 417 | 418 | onFail : function (result) { 419 | this._log("fail", result.message + ". " + result.error); 420 | }, 421 | 422 | onInfo : function ( info ) { 423 | this._log("info", link( info.url, info.message) ); 424 | }, 425 | 426 | onSkip : function ( info ) { 427 | this._log("skip", info.message); 428 | }, 429 | 430 | onRemote : function (unit) { 431 | this._log( 432 | unit.failed ? 'fail' : 'ok', 433 | unit.passed + " test" + (unit.passed == 1 ? '' : 's') + " completed. " 434 | + (unit.skipped ? unit.skipped + " skipped. " : '') 435 | + unit.failed + " error" +(unit.failed == 1 ? '' : 's')+ ". " 436 | + this._timestamp() 437 | ); 438 | }, 439 | 440 | onFinish : function ( unit ) { 441 | this.output.appendChild( div("summary", "Test Complete") ); 442 | this._summary(); 443 | this._scroll(); 444 | } 445 | }; 446 | 447 | /* 448 | * Utilities 449 | */ 450 | 451 | function defaultCSS () { 452 | var css = [ 453 | ".error, .fail { color: red; }", 454 | ".success, .ok{ color: gray; }", 455 | ".test, .group, .summary { font-weight: bold; font-size: 1.1em; }", 456 | ".info { font-weight: bolder; color: gray; }", 457 | ".skip { color: purple; }", 458 | ".prompt { font-weight: bolder; color: green; }", 459 | ".summary { margin-top: 10px; } ", 460 | ".summary.success { color: green; }", 461 | ".item { margin-left: 15px; font-family: monospace;}", 462 | "#okframe { background: white; z-index: 100; border: 5px solid gray; border-radius: 5px; position: absolute; top: 10px; bottom: 10px; right: 15px; width: 50%; height: 95%; }" 463 | ]; 464 | var ss = document.createElement("style"); 465 | ss.text = ss.innerHTML = css.join("\n"); 466 | var head = document.getElementsByTagName('head')[0]; 467 | head.insertBefore(ss, head.children[0] ); 468 | } 469 | 470 | function _el ( id ) { 471 | return document.getElementById(id); 472 | } 473 | 474 | function _okframe ( create ) { 475 | var id = "okframe"; 476 | var el = _el(id); 477 | if( ! el && create) { 478 | el = document.createElement("iframe"); 479 | el.setAttribute('id', id); 480 | var body = document.getElementsByTagName('body')[0]; 481 | body.appendChild(el); 482 | } 483 | return el; 484 | } 485 | 486 | function div ( className, message1, message2, etc ) { 487 | var el = document.createElement('div'); 488 | 489 | if( className ) 490 | el.setAttribute('class', className); 491 | 492 | for(var i = 1; i < arguments.length; i++) { 493 | 494 | if( typeof arguments[i] == "string") 495 | el.appendChild( document.createTextNode(arguments[i]) ); 496 | else 497 | el.appendChild( arguments[i] ); 498 | 499 | el.appendChild( document.createTextNode(' ') ); 500 | } 501 | 502 | return el; 503 | } 504 | 505 | function link (href, message, target ) { 506 | var el; 507 | 508 | message = message || href || ''; 509 | 510 | if( href ) { 511 | el = document.createElement('a'); 512 | el.setAttribute('href', href); 513 | el.setAttribute('target', target || '_blank'); 514 | el.appendChild( document.createTextNode(message) ); 515 | } 516 | else 517 | el = document.createTextNode(message); 518 | 519 | return el; 520 | } 521 | 522 | function now () { 523 | return ( new Date() ).getTime(); 524 | } 525 | 526 | function merge (a, b) { 527 | var k, c = {}; 528 | for( k in a ) { 529 | if( a.hasOwnProperty(k) ) 530 | c[k] = a[k]; 531 | } 532 | for( k in b ) { 533 | if( b.hasOwnProperty(k) ) 534 | c[k] = b[k]; 535 | } 536 | return c; 537 | } 538 | 539 | function compare (got, expected) { 540 | if( expected instanceof Function ){ 541 | expected = expected.call(null, got); 542 | } 543 | 544 | if( expected instanceof Array || expected instanceof Object ) { 545 | if( expected.equals instanceof Function ){ 546 | return expected.equals(got); 547 | } 548 | else { 549 | // poor man's deep compare: use JSON to compare objects 550 | var e, g; 551 | try { 552 | e = JSON.stringify(expected); 553 | g = JSON.stringify(got); 554 | return e === g; 555 | } 556 | catch(e) { 557 | return (expected === got); 558 | } 559 | } 560 | } 561 | else { 562 | return(expected === got); 563 | } 564 | } 565 | 566 | })(); --------------------------------------------------------------------------------