├── .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 | })();
--------------------------------------------------------------------------------