judge 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. data/.gitignore +8 -0
  2. data/.travis.yml +2 -1
  3. data/Gemfile +1 -10
  4. data/Rakefile +9 -44
  5. data/judge.gemspec +17 -164
  6. data/lib/generators/judge/templates/json2.js +46 -42
  7. data/lib/generators/judge/templates/judge.js +42 -41
  8. data/lib/generators/judge/templates/underscore.js +219 -97
  9. data/lib/judge.rb +6 -4
  10. data/lib/judge/form_builder.rb +75 -0
  11. data/lib/judge/{utils.rb → message_collection.rb} +45 -28
  12. data/lib/judge/validator.rb +21 -0
  13. data/lib/judge/validator_collection.rb +28 -0
  14. data/lib/judge/version.rb +3 -0
  15. data/spec/javascripts/JudgeSpec.js +25 -23
  16. data/test/expected_elements.rb +235 -0
  17. data/test/factories.rb +23 -0
  18. data/test/setup.rb +70 -0
  19. data/test/test_form_builder.rb +69 -0
  20. data/test/test_helper.rb +8 -20
  21. data/test/test_message_collection.rb +84 -0
  22. data/test/test_validator.rb +37 -0
  23. data/test/test_validator_collection.rb +19 -0
  24. metadata +46 -137
  25. data/.document +0 -5
  26. data/Gemfile.lock +0 -116
  27. data/VERSION +0 -1
  28. data/docs/docco.css +0 -196
  29. data/docs/judge.html +0 -280
  30. data/lib/judge/form.rb +0 -59
  31. data/test/dummy/Gemfile +0 -3
  32. data/test/dummy/Gemfile.lock +0 -75
  33. data/test/dummy/Rakefile +0 -7
  34. data/test/dummy/app/controllers/application_controller.rb +0 -3
  35. data/test/dummy/app/controllers/foos_controller.rb +0 -11
  36. data/test/dummy/app/helpers/application_helper.rb +0 -2
  37. data/test/dummy/app/models/city.rb +0 -5
  38. data/test/dummy/app/models/continent.rb +0 -4
  39. data/test/dummy/app/models/country.rb +0 -6
  40. data/test/dummy/app/models/fake.rb +0 -2
  41. data/test/dummy/app/models/foo.rb +0 -19
  42. data/test/dummy/app/views/foos/new.html.erb +0 -71
  43. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  44. data/test/dummy/config.ru +0 -4
  45. data/test/dummy/config/application.rb +0 -45
  46. data/test/dummy/config/boot.rb +0 -10
  47. data/test/dummy/config/database.yml +0 -22
  48. data/test/dummy/config/environment.rb +0 -5
  49. data/test/dummy/config/environments/development.rb +0 -26
  50. data/test/dummy/config/environments/production.rb +0 -49
  51. data/test/dummy/config/environments/test.rb +0 -35
  52. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  53. data/test/dummy/config/initializers/inflections.rb +0 -10
  54. data/test/dummy/config/initializers/mime_types.rb +0 -5
  55. data/test/dummy/config/initializers/secret_token.rb +0 -7
  56. data/test/dummy/config/initializers/session_store.rb +0 -8
  57. data/test/dummy/config/locales/en.yml +0 -12
  58. data/test/dummy/config/routes.rb +0 -3
  59. data/test/dummy/db/development.sqlite3 +0 -0
  60. data/test/dummy/db/migrate/20110624115516_create_foos.rb +0 -26
  61. data/test/dummy/db/migrate/20110724201117_create_fake_collections.rb +0 -14
  62. data/test/dummy/db/migrate/20110724201548_rename_fake_collection_to_fake.rb +0 -9
  63. data/test/dummy/db/migrate/20110725082530_create_continent_country_and_city_tables.rb +0 -24
  64. data/test/dummy/db/schema.rb +0 -55
  65. data/test/dummy/db/test.sqlite3 +0 -0
  66. data/test/dummy/log/server.log +0 -0
  67. data/test/dummy/public/404.html +0 -26
  68. data/test/dummy/public/422.html +0 -26
  69. data/test/dummy/public/500.html +0 -26
  70. data/test/dummy/public/favicon.ico +0 -0
  71. data/test/dummy/public/javascripts/application.js +0 -2
  72. data/test/dummy/public/javascripts/controls.js +0 -965
  73. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  74. data/test/dummy/public/javascripts/effects.js +0 -1123
  75. data/test/dummy/public/javascripts/prototype.js +0 -6001
  76. data/test/dummy/public/javascripts/rails.js +0 -175
  77. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  78. data/test/dummy/script/rails +0 -6
  79. data/test/judge_test.rb +0 -186
@@ -2,6 +2,8 @@
2
2
  // You can find a guide and some more traditional API documentation at <http://joecorcoran.github.com/judge/>.
3
3
  // Hopefully this page will help you understand what's happening under the hood.
4
4
 
5
+ /* http://raw.github.com/joecorcoran/judge/master/LICENSE.txt */
6
+
5
7
  /*jshint curly: true, evil: true, newcap: true, noarg: true, strict: false */
6
8
  /*global _: false, JSON: false */
7
9
 
@@ -41,7 +43,7 @@ judge.Watcher = function (element) {
41
43
  }
42
44
 
43
45
  // Convenient access to this Watcher.
44
- var instance = this;
46
+ var watcher = this;
45
47
 
46
48
  // Watcher instance properties.
47
49
  this.element = element;
@@ -50,15 +52,15 @@ judge.Watcher = function (element) {
50
52
  // The `validate` instance method returns the validity of the watched element,
51
53
  // represented as an object containing the element itself and some validity information.
52
54
  this.validate = function() {
53
- instance.errorMessages = [];
54
- var validators = instance.validators,
55
+ watcher.errorMessages = [];
56
+ var validators = watcher.validators,
55
57
  validity = true,
56
58
  messages = [];
57
59
  _(validators).each(function(validator) {
58
60
  var options = validator.options,
59
61
  msgs = validator.messages;
60
- if (instance.element.value.length || options.allow_blank !== true) {
61
- var result = instance.validates()[validator.kind](options, msgs);
62
+ if (watcher.element.value.length || options.allow_blank !== true) {
63
+ var result = watcher.validates()[validator.kind](options, msgs);
62
64
  if (!result.valid && result.hasOwnProperty('messages')) {
63
65
  validity = false;
64
66
  messages.push(result.messages);
@@ -68,7 +70,7 @@ judge.Watcher = function (element) {
68
70
  return {
69
71
  valid: validity,
70
72
  messages: _(messages).flatten(),
71
- element: instance.element
73
+ element: watcher.element
72
74
  };
73
75
  };
74
76
 
@@ -76,14 +78,14 @@ judge.Watcher = function (element) {
76
78
 
77
79
  // Watcher prototype methods.
78
80
  judge.Watcher.prototype.validates = function() {
79
- var instance = this,
81
+ var watcher = this,
80
82
  extendedMethods = judge.customValidators,
81
83
  methods = {
82
84
 
83
85
  // Presence validator ported as closely as possible
84
86
  // from [ActiveModel::Validations::PresenceValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/PresenceValidator.html).
85
87
  presence: function(options, messages) {
86
- if (instance.element.value.length) {
88
+ if (watcher.element.value.length) {
87
89
  return { valid:true };
88
90
  } else{
89
91
  return { valid:false, messages:[messages.blank] };
@@ -94,7 +96,7 @@ judge.Watcher.prototype.validates = function() {
94
96
  // from [ActiveModel::Validations::LengthValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/LengthValidator.html).
95
97
  length: function(options, messages) {
96
98
  var msgs = [],
97
- length = instance.element.value.length,
99
+ length = watcher.element.value.length,
98
100
  types = {
99
101
  minimum: { operator: '<', message: 'too_short' },
100
102
  maximum: { operator: '>', message: 'too_long' },
@@ -113,7 +115,7 @@ judge.Watcher.prototype.validates = function() {
113
115
  // from [ActiveModel::Validations::ExclusionValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/ExclusionValidator.html).
114
116
  exclusion: function(options, messages) {
115
117
  var stringIn = _(options['in']).map(function(o) { return o.toString(); });
116
- if (_(stringIn).include(instance.element.value)) {
118
+ if (_(stringIn).include(watcher.element.value)) {
117
119
  return {
118
120
  valid:false,
119
121
  messages:[messages.exclusion]
@@ -127,7 +129,7 @@ judge.Watcher.prototype.validates = function() {
127
129
  // from [ActiveModel::Validations::InclusionValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/InclusionValidator.html).
128
130
  inclusion: function(options, messages) {
129
131
  var stringIn = _(options['in']).map(function(o) { return o.toString(); });
130
- if (!_(stringIn).include(instance.element.value)) {
132
+ if (!_(stringIn).include(watcher.element.value)) {
131
133
  return {
132
134
  valid:false,
133
135
  messages:[messages.inclusion]
@@ -148,7 +150,7 @@ judge.Watcher.prototype.validates = function() {
148
150
  less_than_or_equal_to: '<='
149
151
  },
150
152
  msgs = [],
151
- value = instance.element.value,
153
+ value = watcher.element.value,
152
154
  parsedValue = parseFloat(value, 10);
153
155
 
154
156
  if (isNaN(Number(value))) {
@@ -170,14 +172,14 @@ judge.Watcher.prototype.validates = function() {
170
172
  }
171
173
  });
172
174
  }
173
- return msgs.length? { valid:false, messages:msgs } : { valid:true };
175
+ return msgs.length ? { valid:false, messages:msgs } : { valid:true };
174
176
  },
175
177
 
176
178
  // Format validator ported as closely as possible
177
179
  // from [ActiveModel::Validations::FormatValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/FormatValidator.html).
178
180
  format: function(options, messages) {
179
181
  var msgs = [],
180
- value = instance.element.value;
182
+ value = watcher.element.value;
181
183
  if (options.hasOwnProperty('with')) {
182
184
  var withReg = judge.utils.convertRegExp(options['with']);
183
185
  if (!withReg.test(value)) {
@@ -196,7 +198,7 @@ judge.Watcher.prototype.validates = function() {
196
198
  // Acceptance validator ported as closely as possible
197
199
  // from [ActiveModel::Validations::AcceptanceValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/AcceptanceValidator.html).
198
200
  acceptance: function(options, messages) {
199
- if (instance.element.checked === true) {
201
+ if (watcher.element.checked === true) {
200
202
  return { valid:true };
201
203
  } else {
202
204
  return {
@@ -209,10 +211,10 @@ judge.Watcher.prototype.validates = function() {
209
211
  // Confirmation validator ported as closely as possible
210
212
  // from [ActiveModel::Validations::ConfirmationValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/ConfirmationValidator.html).
211
213
  confirmation: function(options, messages) {
212
- var id = instance.element.getAttribute('id'),
214
+ var id = watcher.element.getAttribute('id'),
213
215
  confId = id + '_confirmation',
214
216
  confElem = document.getElementById(confId);
215
- if (instance.element.value === confElem.value) {
217
+ if (watcher.element.value === confElem.value) {
216
218
  return { valid:true };
217
219
  } else {
218
220
  return {
@@ -239,7 +241,7 @@ judge.store = (function() {
239
241
 
240
242
  return {
241
243
 
242
- // Stores a Watcher for an element against a user defined key.
244
+ // Stores watcher(s) for element(s) against a user defined key.
243
245
  save: function(key, element) {
244
246
  var elements = judge.utils.isCollection(element) ? element : [element];
245
247
  _(elements).each(function(element) {
@@ -253,25 +255,28 @@ judge.store = (function() {
253
255
  return store;
254
256
  },
255
257
 
256
- // Removes an individual stored Watcher.
258
+ // Removes stored watcher(s).
257
259
  remove: function(key, element) {
258
260
  if (!store.hasOwnProperty(key)) {
259
261
  return null;
260
262
  }
261
263
  var elements = judge.utils.isCollection(element) ? element : [element];
262
264
  store[key] = _(store[key]).reject(function(j) { return _(elements).include(j.element); });
263
- return store;
265
+ if (store[key].length === 0) {
266
+ delete store[key];
267
+ }
268
+ return store[key];
264
269
  },
265
270
 
266
- // Returns the entire store object, or an array of Watchers stored against
271
+ // Returns the entire store object, or an array of watchers stored against
267
272
  // the given key.
268
273
  get: function(key) {
269
274
  if (_(key).isUndefined()) { return store; }
270
275
  return store.hasOwnProperty(key) ? store[key] : null;
271
276
  },
272
277
 
273
- // Returns the entire store object with Watchers converted to elements,
274
- // or all DOM elements stored within all Watchers stored against the given key.
278
+ // Returns the entire store object with watchers converted to elements,
279
+ // or all DOM elements stored within all watchers stored against the given key.
275
280
  getDOM: function(key) {
276
281
  if (_(key).isUndefined()) {
277
282
  var convertedStore = {};
@@ -283,14 +288,21 @@ judge.store = (function() {
283
288
  return store.hasOwnProperty(key) ? _(store[key]).pluck('element') : null;
284
289
  },
285
290
 
286
- // Wipes the entire store object, or wipes all Watchers stored against
291
+ // Shortcut for `judge.validate(judge.store.getDOM(key));`.
292
+ // Returns null if no stored elements are found for the given key.
293
+ validate: function(key) {
294
+ var elements = judge.store.getDOM(key);
295
+ return (key && !_(elements).isNull()) ? judge.validate(elements) : null;
296
+ },
297
+
298
+ // Wipes the entire store object, or wipes all watchers stored against
287
299
  // the given key.
288
300
  clear: function(key) {
289
301
  if (_(key).isUndefined()) {
290
302
  store = {};
291
303
  } else {
292
304
  if (!store.hasOwnProperty(key)) { return null; }
293
- store[key] = [];
305
+ delete store[key];
294
306
  }
295
307
  return store;
296
308
  }
@@ -312,17 +324,6 @@ judge.validate = function(elements) {
312
324
 
313
325
  // The obligatory utilities.
314
326
  judge.utils = {
315
-
316
- // Determines whether an object is a DOM element of the types that judge can work with.
317
- isValidatable: function(object) {
318
- var type = judge.utils.getObjectString(object),
319
- types = [
320
- 'HTMLInputElement',
321
- 'HTMLTextAreaElement',
322
- 'HTMLSelectElement'
323
- ];
324
- return _(types).include(type);
325
- },
326
327
 
327
328
  // A way of checking isArray, but including weird object types that are returned from collection queries.
328
329
  isCollection: function(object) {
@@ -356,11 +357,13 @@ judge.utils = {
356
357
  isEven: function(value) { return (value % 2 === 0) ? true : false; },
357
358
  isOdd: function(value) { return !judge.utils.isEven(value); },
358
359
 
359
- // Converts a ruby flag-first formatted regular expression, given as a string, into JavaScript.
360
+ // Converts a Ruby regular expression, given as a string, into JavaScript.
360
361
  // This is rudimentary at best, as there are many, many differences between Ruby
361
362
  // and JavaScript when it comes to regexp-fu. Basically, if you validate your records
362
363
  // using complex regular expressions AND you hope that this will "just work"
363
364
  // on the client-side too&hellip; you are going to be pretty disappointed.
365
+ // If you know of a better way (or an existing library) to convert regular expressions
366
+ // from Ruby to JavaScript, I would really love to hear from you.
364
367
  convertRegExp: function(string) {
365
368
  var p = string.slice(1, -1).split(':'),
366
369
  o = p.shift(),
@@ -368,8 +371,8 @@ judge.utils = {
368
371
  return new RegExp(r, judge.utils.convertFlags(o));
369
372
  },
370
373
  convertFlags: function(string) {
371
- var off = new RegExp('-'),
372
- multi = new RegExp('m');
374
+ var off = new RegExp('-'),
375
+ multi = new RegExp('m');
373
376
  string = string.replace('?', '');
374
377
  if (off.test(string) || !multi.test(string)) {
375
378
  return '';
@@ -378,5 +381,3 @@ judge.utils = {
378
381
  }
379
382
  }
380
383
  };
381
-
382
-
@@ -1,4 +1,4 @@
1
- // Underscore.js 1.1.7
1
+ // Underscore.js 1.2.1
2
2
  // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
3
3
  // Underscore is freely distributable under the MIT license.
4
4
  // Portions of Underscore are inspired or borrowed from Prototype,
@@ -48,19 +48,26 @@
48
48
  // Create a safe reference to the Underscore object for use below.
49
49
  var _ = function(obj) { return new wrapper(obj); };
50
50
 
51
- // Export the Underscore object for **CommonJS**, with backwards-compatibility
52
- // for the old `require()` API. If we're not in CommonJS, add `_` to the
53
- // global object.
54
- if (typeof module !== 'undefined' && module.exports) {
55
- module.exports = _;
56
- _._ = _;
51
+ // Export the Underscore object for **Node.js** and **"CommonJS"**, with
52
+ // backwards-compatibility for the old `require()` API. If we're not in
53
+ // CommonJS, add `_` to the global object.
54
+ if (typeof exports !== 'undefined') {
55
+ if (typeof module !== 'undefined' && module.exports) {
56
+ exports = module.exports = _;
57
+ }
58
+ exports._ = _;
59
+ } else if (typeof define === 'function' && define.amd) {
60
+ // Register as a named module with AMD.
61
+ define('underscore', function() {
62
+ return _;
63
+ });
57
64
  } else {
58
65
  // Exported as a string, for Closure Compiler "advanced" mode.
59
66
  root['_'] = _;
60
67
  }
61
68
 
62
69
  // Current version.
63
- _.VERSION = '1.1.7';
70
+ _.VERSION = '1.2.1';
64
71
 
65
72
  // Collection Functions
66
73
  // --------------------
@@ -198,8 +205,8 @@
198
205
  var found = false;
199
206
  if (obj == null) return found;
200
207
  if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
201
- any(obj, function(value) {
202
- if (found = value === target) return true;
208
+ found = any(obj, function(value) {
209
+ return value === target;
203
210
  });
204
211
  return found;
205
212
  };
@@ -220,6 +227,7 @@
220
227
  // Return the maximum element or (element-based computation).
221
228
  _.max = function(obj, iterator, context) {
222
229
  if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
230
+ if (!iterator && _.isEmpty(obj)) return -Infinity;
223
231
  var result = {computed : -Infinity};
224
232
  each(obj, function(value, index, list) {
225
233
  var computed = iterator ? iterator.call(context, value, index, list) : value;
@@ -231,6 +239,7 @@
231
239
  // Return the minimum element (or element-based computation).
232
240
  _.min = function(obj, iterator, context) {
233
241
  if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
242
+ if (!iterator && _.isEmpty(obj)) return Infinity;
234
243
  var result = {computed : Infinity};
235
244
  each(obj, function(value, index, list) {
236
245
  var computed = iterator ? iterator.call(context, value, index, list) : value;
@@ -239,6 +248,21 @@
239
248
  return result.value;
240
249
  };
241
250
 
251
+ // Shuffle an array.
252
+ _.shuffle = function(obj) {
253
+ var shuffled = [], rand;
254
+ each(obj, function(value, index, list) {
255
+ if (index == 0) {
256
+ shuffled[0] = value;
257
+ } else {
258
+ rand = Math.floor(Math.random() * (index + 1));
259
+ shuffled[index] = shuffled[rand];
260
+ shuffled[rand] = value;
261
+ }
262
+ });
263
+ return shuffled;
264
+ };
265
+
242
266
  // Sort the object's values by a criterion produced by an iterator.
243
267
  _.sortBy = function(obj, iterator, context) {
244
268
  return _.pluck(_.map(obj, function(value, index, list) {
@@ -252,9 +276,11 @@
252
276
  }), 'value');
253
277
  };
254
278
 
255
- // Groups the object's values by a criterion produced by an iterator
256
- _.groupBy = function(obj, iterator) {
279
+ // Groups the object's values by a criterion. Pass either a string attribute
280
+ // to group by, or a function that returns the criterion.
281
+ _.groupBy = function(obj, val) {
257
282
  var result = {};
283
+ var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
258
284
  each(obj, function(value, index) {
259
285
  var key = iterator(value, index);
260
286
  (result[key] || (result[key] = [])).push(value);
@@ -298,6 +324,20 @@
298
324
  return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
299
325
  };
300
326
 
327
+ // Returns everything but the last entry of the array. Especcialy useful on
328
+ // the arguments object. Passing **n** will return all the values in
329
+ // the array, excluding the last N. The **guard** check allows it to work with
330
+ // `_.map`.
331
+ _.initial = function(array, n, guard) {
332
+ return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
333
+ };
334
+
335
+ // Get the last element of an array. Passing **n** will return the last N
336
+ // values in the array. The **guard** check allows it to work with `_.map`.
337
+ _.last = function(array, n, guard) {
338
+ return (n != null) && !guard ? slice.call(array, array.length - n) : array[array.length - 1];
339
+ };
340
+
301
341
  // Returns everything but the first entry of the array. Aliased as `tail`.
302
342
  // Especially useful on the arguments object. Passing an **index** will return
303
343
  // the rest of the values in the array from that index onward. The **guard**
@@ -306,20 +346,15 @@
306
346
  return slice.call(array, (index == null) || guard ? 1 : index);
307
347
  };
308
348
 
309
- // Get the last element of an array.
310
- _.last = function(array) {
311
- return array[array.length - 1];
312
- };
313
-
314
349
  // Trim out all falsy values from an array.
315
350
  _.compact = function(array) {
316
351
  return _.filter(array, function(value){ return !!value; });
317
352
  };
318
353
 
319
354
  // Return a completely flattened version of an array.
320
- _.flatten = function(array) {
355
+ _.flatten = function(array, shallow) {
321
356
  return _.reduce(array, function(memo, value) {
322
- if (_.isArray(value)) return memo.concat(_.flatten(value));
357
+ if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
323
358
  memo[memo.length] = value;
324
359
  return memo;
325
360
  }, []);
@@ -333,17 +368,23 @@
333
368
  // Produce a duplicate-free version of the array. If the array has already
334
369
  // been sorted, you have the option of using a faster algorithm.
335
370
  // Aliased as `unique`.
336
- _.uniq = _.unique = function(array, isSorted) {
337
- return _.reduce(array, function(memo, el, i) {
338
- if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
371
+ _.uniq = _.unique = function(array, isSorted, iterator) {
372
+ var initial = iterator ? _.map(array, iterator) : array;
373
+ var result = [];
374
+ _.reduce(initial, function(memo, el, i) {
375
+ if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
376
+ memo[memo.length] = el;
377
+ result[result.length] = array[i];
378
+ }
339
379
  return memo;
340
380
  }, []);
381
+ return result;
341
382
  };
342
383
 
343
384
  // Produce an array that contains the union: each distinct element from all of
344
385
  // the passed-in arrays.
345
386
  _.union = function() {
346
- return _.uniq(_.flatten(arguments));
387
+ return _.uniq(_.flatten(arguments, true));
347
388
  };
348
389
 
349
390
  // Produce an array that contains every item shared between all the
@@ -391,7 +432,6 @@
391
432
  return -1;
392
433
  };
393
434
 
394
-
395
435
  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
396
436
  _.lastIndexOf = function(array, item) {
397
437
  if (array == null) return -1;
@@ -426,15 +466,25 @@
426
466
  // Function (ahem) Functions
427
467
  // ------------------
428
468
 
469
+ // Reusable constructor function for prototype setting.
470
+ var ctor = function(){};
471
+
429
472
  // Create a function bound to a given object (assigning `this`, and arguments,
430
473
  // optionally). Binding with arguments is also known as `curry`.
431
474
  // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
432
475
  // We check for `func.bind` first, to fail fast when `func` is undefined.
433
- _.bind = function(func, obj) {
476
+ _.bind = function bind(func, context) {
477
+ var bound, args;
434
478
  if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
435
- var args = slice.call(arguments, 2);
436
- return function() {
437
- return func.apply(obj, args.concat(slice.call(arguments)));
479
+ if (!_.isFunction(func)) throw new TypeError;
480
+ args = slice.call(arguments, 2);
481
+ return bound = function() {
482
+ if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
483
+ ctor.prototype = func.prototype;
484
+ var self = new ctor;
485
+ var result = func.apply(self, args.concat(slice.call(arguments)));
486
+ if (Object(result) === result) return result;
487
+ return self;
438
488
  };
439
489
  };
440
490
 
@@ -470,31 +520,43 @@
470
520
  return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
471
521
  };
472
522
 
473
- // Internal function used to implement `_.throttle` and `_.debounce`.
474
- var limit = function(func, wait, debounce) {
475
- var timeout;
523
+ // Returns a function, that, when invoked, will only be triggered at most once
524
+ // during a given window of time.
525
+ _.throttle = function(func, wait) {
526
+ var context, args, timeout, throttling, more;
527
+ var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
476
528
  return function() {
477
- var context = this, args = arguments;
478
- var throttler = function() {
529
+ context = this; args = arguments;
530
+ var later = function() {
479
531
  timeout = null;
480
- func.apply(context, args);
532
+ if (more) func.apply(context, args);
533
+ whenDone();
481
534
  };
482
- if (debounce) clearTimeout(timeout);
483
- if (debounce || !timeout) timeout = setTimeout(throttler, wait);
535
+ if (!timeout) timeout = setTimeout(later, wait);
536
+ if (throttling) {
537
+ more = true;
538
+ } else {
539
+ func.apply(context, args);
540
+ }
541
+ whenDone();
542
+ throttling = true;
484
543
  };
485
544
  };
486
545
 
487
- // Returns a function, that, when invoked, will only be triggered at most once
488
- // during a given window of time.
489
- _.throttle = function(func, wait) {
490
- return limit(func, wait, false);
491
- };
492
-
493
546
  // Returns a function, that, as long as it continues to be invoked, will not
494
547
  // be triggered. The function will be called after it stops being called for
495
548
  // N milliseconds.
496
549
  _.debounce = function(func, wait) {
497
- return limit(func, wait, true);
550
+ var timeout;
551
+ return function() {
552
+ var context = this, args = arguments;
553
+ var later = function() {
554
+ timeout = null;
555
+ func.apply(context, args);
556
+ };
557
+ clearTimeout(timeout);
558
+ timeout = setTimeout(later, wait);
559
+ };
498
560
  };
499
561
 
500
562
  // Returns a function that will be executed at most one time, no matter how
@@ -538,7 +600,6 @@
538
600
  };
539
601
  };
540
602
 
541
-
542
603
  // Object Functions
543
604
  // ----------------
544
605
 
@@ -588,6 +649,7 @@
588
649
 
589
650
  // Create a (shallow-cloned) duplicate of an object.
590
651
  _.clone = function(obj) {
652
+ if (!_.isObject(obj)) return obj;
591
653
  return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
592
654
  };
593
655
 
@@ -599,47 +661,93 @@
599
661
  return obj;
600
662
  };
601
663
 
602
- // Perform a deep comparison to check if two objects are equal.
603
- _.isEqual = function(a, b) {
604
- // Check object identity.
605
- if (a === b) return true;
606
- // Different types?
607
- var atype = typeof(a), btype = typeof(b);
608
- if (atype != btype) return false;
609
- // Basic equality test (watch out for coercions).
610
- if (a == b) return true;
611
- // One is falsy and the other truthy.
612
- if ((!a && b) || (a && !b)) return false;
664
+ // Internal recursive comparison function.
665
+ function eq(a, b, stack) {
666
+ // Identical objects are equal. `0 === -0`, but they aren't identical.
667
+ // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
668
+ if (a === b) return a !== 0 || 1 / a == 1 / b;
669
+ // A strict comparison is necessary because `null == undefined`.
670
+ if ((a == null) || (b == null)) return a === b;
613
671
  // Unwrap any wrapped objects.
614
672
  if (a._chain) a = a._wrapped;
615
673
  if (b._chain) b = b._wrapped;
616
- // One of them implements an isEqual()?
617
- if (a.isEqual) return a.isEqual(b);
618
- if (b.isEqual) return b.isEqual(a);
619
- // Check dates' integer values.
620
- if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
621
- // Both are NaN?
622
- if (_.isNaN(a) && _.isNaN(b)) return false;
623
- // Compare regular expressions.
624
- if (_.isRegExp(a) && _.isRegExp(b))
625
- return a.source === b.source &&
626
- a.global === b.global &&
627
- a.ignoreCase === b.ignoreCase &&
628
- a.multiline === b.multiline;
629
- // If a is not an object by this point, we can't handle it.
630
- if (atype !== 'object') return false;
631
- // Check for different array lengths before comparing contents.
632
- if (a.length && (a.length !== b.length)) return false;
633
- // Nothing else worked, deep compare the contents.
634
- var aKeys = _.keys(a), bKeys = _.keys(b);
635
- // Different object sizes?
636
- if (aKeys.length != bKeys.length) return false;
637
- // Recursive comparison of contents.
638
- for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
639
- return true;
674
+ // Invoke a custom `isEqual` method if one is provided.
675
+ if (_.isFunction(a.isEqual)) return a.isEqual(b);
676
+ if (_.isFunction(b.isEqual)) return b.isEqual(a);
677
+ // Compare object types.
678
+ var typeA = typeof a;
679
+ if (typeA != typeof b) return false;
680
+ // Optimization; ensure that both values are truthy or falsy.
681
+ if (!a != !b) return false;
682
+ // `NaN` values are equal.
683
+ if (_.isNaN(a)) return _.isNaN(b);
684
+ // Compare string objects by value.
685
+ var isStringA = _.isString(a), isStringB = _.isString(b);
686
+ if (isStringA || isStringB) return isStringA && isStringB && String(a) == String(b);
687
+ // Compare number objects by value.
688
+ var isNumberA = _.isNumber(a), isNumberB = _.isNumber(b);
689
+ if (isNumberA || isNumberB) return isNumberA && isNumberB && +a == +b;
690
+ // Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0.
691
+ var isBooleanA = _.isBoolean(a), isBooleanB = _.isBoolean(b);
692
+ if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b;
693
+ // Compare dates by their millisecond values.
694
+ var isDateA = _.isDate(a), isDateB = _.isDate(b);
695
+ if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime();
696
+ // Compare RegExps by their source patterns and flags.
697
+ var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b);
698
+ if (isRegExpA || isRegExpB) {
699
+ // Ensure commutative equality for RegExps.
700
+ return isRegExpA && isRegExpB &&
701
+ a.source == b.source &&
702
+ a.global == b.global &&
703
+ a.multiline == b.multiline &&
704
+ a.ignoreCase == b.ignoreCase;
705
+ }
706
+ // Ensure that both values are objects.
707
+ if (typeA != 'object') return false;
708
+ // Arrays or Arraylikes with different lengths are not equal.
709
+ if (a.length !== b.length) return false;
710
+ // Objects with different constructors are not equal.
711
+ if (a.constructor !== b.constructor) return false;
712
+ // Assume equality for cyclic structures. The algorithm for detecting cyclic
713
+ // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
714
+ var length = stack.length;
715
+ while (length--) {
716
+ // Linear search. Performance is inversely proportional to the number of
717
+ // unique nested structures.
718
+ if (stack[length] == a) return true;
719
+ }
720
+ // Add the first object to the stack of traversed objects.
721
+ stack.push(a);
722
+ var size = 0, result = true;
723
+ // Deep compare objects.
724
+ for (var key in a) {
725
+ if (hasOwnProperty.call(a, key)) {
726
+ // Count the expected number of properties.
727
+ size++;
728
+ // Deep compare each member.
729
+ if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break;
730
+ }
731
+ }
732
+ // Ensure that both objects contain the same number of properties.
733
+ if (result) {
734
+ for (key in b) {
735
+ if (hasOwnProperty.call(b, key) && !(size--)) break;
736
+ }
737
+ result = !size;
738
+ }
739
+ // Remove the first object from the stack of traversed objects.
740
+ stack.pop();
741
+ return result;
742
+ }
743
+
744
+ // Perform a deep comparison to check if two objects are equal.
745
+ _.isEqual = function(a, b) {
746
+ return eq(a, b, []);
640
747
  };
641
748
 
642
- // Is a given array or object empty?
749
+ // Is a given array, string, or object empty?
750
+ // An "empty" object has no enumerable own-properties.
643
751
  _.isEmpty = function(obj) {
644
752
  if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
645
753
  for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
@@ -654,7 +762,7 @@
654
762
  // Is a given value an array?
655
763
  // Delegates to ECMA5's native Array.isArray
656
764
  _.isArray = nativeIsArray || function(obj) {
657
- return toString.call(obj) === '[object Array]';
765
+ return toString.call(obj) == '[object Array]';
658
766
  };
659
767
 
660
768
  // Is a given variable an object?
@@ -663,44 +771,50 @@
663
771
  };
664
772
 
665
773
  // Is a given variable an arguments object?
666
- _.isArguments = function(obj) {
667
- return !!(obj && hasOwnProperty.call(obj, 'callee'));
668
- };
774
+ if (toString.call(arguments) == '[object Arguments]') {
775
+ _.isArguments = function(obj) {
776
+ return toString.call(obj) == '[object Arguments]';
777
+ };
778
+ } else {
779
+ _.isArguments = function(obj) {
780
+ return !!(obj && hasOwnProperty.call(obj, 'callee'));
781
+ };
782
+ }
669
783
 
670
784
  // Is a given value a function?
671
785
  _.isFunction = function(obj) {
672
- return !!(obj && obj.constructor && obj.call && obj.apply);
786
+ return toString.call(obj) == '[object Function]';
673
787
  };
674
788
 
675
789
  // Is a given value a string?
676
790
  _.isString = function(obj) {
677
- return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
791
+ return toString.call(obj) == '[object String]';
678
792
  };
679
793
 
680
794
  // Is a given value a number?
681
795
  _.isNumber = function(obj) {
682
- return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
796
+ return toString.call(obj) == '[object Number]';
683
797
  };
684
798
 
685
- // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
686
- // that does not equal itself.
799
+ // Is the given value `NaN`?
687
800
  _.isNaN = function(obj) {
801
+ // `NaN` is the only value for which `===` is not reflexive.
688
802
  return obj !== obj;
689
803
  };
690
804
 
691
805
  // Is a given value a boolean?
692
806
  _.isBoolean = function(obj) {
693
- return obj === true || obj === false;
807
+ return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
694
808
  };
695
809
 
696
810
  // Is a given value a date?
697
811
  _.isDate = function(obj) {
698
- return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
812
+ return toString.call(obj) == '[object Date]';
699
813
  };
700
814
 
701
815
  // Is the given value a regular expression?
702
816
  _.isRegExp = function(obj) {
703
- return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
817
+ return toString.call(obj) == '[object RegExp]';
704
818
  };
705
819
 
706
820
  // Is a given value equal to null?
@@ -733,6 +847,11 @@
733
847
  for (var i = 0; i < n; i++) iterator.call(context, i);
734
848
  };
735
849
 
850
+ // Escape a string for HTML interpolation.
851
+ _.escape = function(string) {
852
+ return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
853
+ };
854
+
736
855
  // Add your own custom functions to the Underscore object, ensuring that
737
856
  // they're correctly added to the OOP wrapper as well.
738
857
  _.mixin = function(obj) {
@@ -753,7 +872,8 @@
753
872
  // following template settings to use alternative delimiters.
754
873
  _.templateSettings = {
755
874
  evaluate : /<%([\s\S]+?)%>/g,
756
- interpolate : /<%=([\s\S]+?)%>/g
875
+ interpolate : /<%=([\s\S]+?)%>/g,
876
+ escape : /<%-([\s\S]+?)%>/g
757
877
  };
758
878
 
759
879
  // JavaScript micro-templating, similar to John Resig's implementation.
@@ -765,6 +885,9 @@
765
885
  'with(obj||{}){__p.push(\'' +
766
886
  str.replace(/\\/g, '\\\\')
767
887
  .replace(/'/g, "\\'")
888
+ .replace(c.escape, function(match, code) {
889
+ return "',_.escape(" + code.replace(/\\'/g, "'") + "),'";
890
+ })
768
891
  .replace(c.interpolate, function(match, code) {
769
892
  return "'," + code.replace(/\\'/g, "'") + ",'";
770
893
  })
@@ -776,8 +899,8 @@
776
899
  .replace(/\n/g, '\\n')
777
900
  .replace(/\t/g, '\\t')
778
901
  + "');}return __p.join('');";
779
- var func = new Function('obj', tmpl);
780
- return data ? func(data) : func;
902
+ var func = new Function('obj', '_', tmpl);
903
+ return data ? func(data, _) : function(data) { return func(data, _) };
781
904
  };
782
905
 
783
906
  // The OOP Wrapper
@@ -836,5 +959,4 @@
836
959
  return this._wrapped;
837
960
  };
838
961
 
839
- })();
840
-
962
+ }).call(this);