joosy 0.1.0.alpha → 1.0.0.RC1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. data/.codoopts +5 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +15 -2
  4. data/Gemfile.lock +102 -81
  5. data/Guardfile +16 -16
  6. data/LICENSE +22 -0
  7. data/MIT-LICENSE +2 -2
  8. data/README.md +118 -0
  9. data/app/assets/javascripts/joosy/core/application.js.coffee +53 -0
  10. data/app/assets/javascripts/joosy/core/form.js.coffee +338 -0
  11. data/app/assets/javascripts/joosy/core/helpers/form.js.coffee +72 -0
  12. data/app/assets/javascripts/joosy/core/helpers/view.js.coffee +42 -0
  13. data/app/assets/javascripts/joosy/core/helpers/widgets.js.coffee +14 -0
  14. data/app/assets/javascripts/joosy/core/joosy.js.coffee +184 -0
  15. data/app/assets/javascripts/joosy/core/layout.js.coffee +168 -0
  16. data/app/assets/javascripts/joosy/core/modules/container.js.coffee +124 -0
  17. data/app/assets/javascripts/joosy/core/modules/events.js.coffee +122 -0
  18. data/app/assets/javascripts/joosy/core/modules/filters.js.coffee +39 -0
  19. data/app/assets/javascripts/joosy/core/modules/log.js.coffee +36 -0
  20. data/app/assets/javascripts/joosy/core/modules/module.js.coffee +117 -0
  21. data/app/assets/javascripts/joosy/core/modules/renderer.js.coffee +200 -0
  22. data/app/assets/javascripts/joosy/core/modules/time_manager.js.coffee +46 -0
  23. data/app/assets/javascripts/joosy/core/modules/widgets_manager.js.coffee +87 -0
  24. data/app/assets/javascripts/joosy/core/page.js.coffee +387 -0
  25. data/app/assets/javascripts/joosy/core/preloader.js.coffee +13 -0
  26. data/app/assets/javascripts/joosy/core/resource/collection.js.coffee +175 -0
  27. data/app/assets/javascripts/joosy/core/resource/generic.js.coffee +303 -0
  28. data/app/assets/javascripts/joosy/core/resource/rest.js.coffee +244 -0
  29. data/app/assets/javascripts/joosy/core/resource/rest_collection.js.coffee +24 -0
  30. data/app/assets/javascripts/joosy/core/router.js.coffee +201 -0
  31. data/app/assets/javascripts/joosy/core/templaters/rails_jst.js.coffee +37 -0
  32. data/app/assets/javascripts/joosy/core/widget.js.coffee +85 -0
  33. data/app/assets/javascripts/joosy/preloaders/caching.js.coffee +169 -0
  34. data/app/assets/javascripts/joosy/preloaders/inline.js.coffee +56 -0
  35. data/{vendor → app}/assets/javascripts/joosy.js.coffee +0 -1
  36. data/app/helpers/joosy/sprockets_helper.rb +39 -12
  37. data/joosy.gemspec +4 -3
  38. data/lib/joosy/rails/engine.rb +12 -1
  39. data/lib/joosy/rails/version.rb +2 -2
  40. data/lib/joosy.rb +9 -0
  41. data/lib/rails/generators/joosy/application_generator.rb +15 -3
  42. data/lib/rails/generators/joosy/joosy_base.rb +2 -2
  43. data/lib/rails/generators/joosy/layout_generator.rb +8 -1
  44. data/lib/rails/generators/joosy/page_generator.rb +21 -6
  45. data/lib/rails/generators/joosy/preloader_generator.rb +14 -7
  46. data/lib/rails/generators/joosy/resource_generator.rb +29 -0
  47. data/lib/rails/generators/joosy/templates/app/helpers/application.js.coffee +4 -0
  48. data/lib/rails/generators/joosy/templates/app/layouts/application.js.coffee +1 -0
  49. data/lib/rails/generators/joosy/templates/app/layouts/template.js.coffee +1 -1
  50. data/lib/rails/generators/joosy/templates/app/pages/application.js.coffee +1 -1
  51. data/lib/rails/generators/joosy/templates/app/pages/template.js.coffee +3 -3
  52. data/lib/rails/generators/joosy/templates/app/pages/welcome/index.js.coffee +22 -0
  53. data/lib/rails/generators/joosy/templates/app/resources/template.js.coffee +2 -0
  54. data/lib/rails/generators/joosy/templates/app/routes.js.coffee +7 -1
  55. data/lib/rails/generators/joosy/templates/app/templates/layouts/application.jst.hamlc +2 -0
  56. data/lib/rails/generators/joosy/templates/app/templates/pages/welcome/index.jst.hamlc +7 -0
  57. data/lib/rails/generators/joosy/templates/app/widgets/template.js.coffee +2 -2
  58. data/lib/rails/generators/joosy/templates/app.js.coffee +4 -0
  59. data/lib/rails/generators/joosy/templates/app_preloader.js.coffee.erb +9 -12
  60. data/lib/rails/generators/joosy/templates/app_resources_predefiner.js.coffee.erb +11 -0
  61. data/lib/rails/generators/joosy/templates/preload.html.erb +5 -6
  62. data/lib/rails/generators/joosy/templates/preload.html.haml +3 -5
  63. data/lib/rails/generators/joosy/widget_generator.rb +8 -1
  64. data/lib/rails/resources_with_joosy.rb +11 -0
  65. data/spec/javascripts/helpers/spec_helper.js.coffee +25 -0
  66. data/spec/javascripts/joosy/core/application_spec.js.coffee +40 -0
  67. data/spec/javascripts/joosy/core/form_spec.js.coffee +200 -0
  68. data/spec/javascripts/joosy/core/helpers/forms_spec.js.coffee +103 -0
  69. data/spec/javascripts/joosy/core/helpers/view_spec.js.coffee +10 -0
  70. data/spec/javascripts/joosy/core/joosy_spec.js.coffee +97 -0
  71. data/spec/javascripts/joosy/core/layout_spec.js.coffee +50 -0
  72. data/spec/javascripts/joosy/core/modules/container_spec.js.coffee +32 -27
  73. data/spec/javascripts/joosy/core/modules/events_spec.js.coffee +55 -18
  74. data/spec/javascripts/joosy/core/modules/filters_spec.js.coffee +28 -27
  75. data/spec/javascripts/joosy/core/modules/log_spec.js.coffee +3 -3
  76. data/spec/javascripts/joosy/core/modules/module_spec.js.coffee +6 -15
  77. data/spec/javascripts/joosy/core/modules/renderer_spec.js.coffee +203 -0
  78. data/spec/javascripts/joosy/core/modules/time_manager_spec.js.coffee +12 -7
  79. data/spec/javascripts/joosy/core/modules/widget_manager_spec.js.coffee +31 -17
  80. data/spec/javascripts/joosy/core/page_spec.js.coffee +178 -0
  81. data/spec/javascripts/joosy/core/resource/collection_spec.js.coffee +84 -0
  82. data/spec/javascripts/joosy/core/resource/generic_spec.js.coffee +149 -0
  83. data/spec/javascripts/joosy/core/resource/rest_collection_spec.js.coffee +31 -0
  84. data/spec/javascripts/joosy/core/resource/rest_spec.js.coffee +171 -0
  85. data/spec/javascripts/joosy/core/router_spec.js.coffee +143 -0
  86. data/spec/javascripts/joosy/core/templaters/rails_jst_spec.js.coffee +25 -0
  87. data/spec/javascripts/joosy/core/widget_spec.js.coffee +40 -0
  88. data/spec/javascripts/joosy/preloaders/caching_spec.js.coffee +36 -0
  89. data/spec/javascripts/joosy/preloaders/inline_spec.js.coffee +16 -0
  90. data/spec/javascripts/support/assets/coolface.jpg +0 -0
  91. data/spec/javascripts/support/assets/okay.jpg +0 -0
  92. data/spec/javascripts/support/assets/test.js +1 -0
  93. data/spec/javascripts/support/sinon-ie-1.3.1.js +82 -0
  94. data/vendor/assets/javascripts/jquery.form.js +978 -963
  95. data/vendor/assets/javascripts/metamorph.js +409 -0
  96. data/vendor/assets/javascripts/sugar.js +1057 -366
  97. metadata +95 -50
  98. data/README.rdoc +0 -3
  99. data/lib/joosy/forms.rb +0 -47
  100. data/lib/joosy-rails.rb +0 -5
  101. data/lib/rails/generators/joosy/templates/preload.html.slim +0 -21
  102. data/tmp/javascripts/.gitignore +0 -1
  103. data/tmp/spec/javascripts/helpers/.gitignore +0 -1
  104. data/vendor/assets/javascripts/base64.js +0 -135
  105. data/vendor/assets/javascripts/inflection.js +0 -656
  106. data/vendor/assets/javascripts/joosy/core/application.js.coffee +0 -26
  107. data/vendor/assets/javascripts/joosy/core/form.js.coffee +0 -87
  108. data/vendor/assets/javascripts/joosy/core/joosy.js.coffee +0 -62
  109. data/vendor/assets/javascripts/joosy/core/layout.js.coffee +0 -38
  110. data/vendor/assets/javascripts/joosy/core/modules/container.js.coffee +0 -51
  111. data/vendor/assets/javascripts/joosy/core/modules/events.js.coffee +0 -17
  112. data/vendor/assets/javascripts/joosy/core/modules/filters.js.coffee +0 -39
  113. data/vendor/assets/javascripts/joosy/core/modules/log.js.coffee +0 -12
  114. data/vendor/assets/javascripts/joosy/core/modules/module.js.coffee +0 -28
  115. data/vendor/assets/javascripts/joosy/core/modules/time_manager.js.coffee +0 -23
  116. data/vendor/assets/javascripts/joosy/core/modules/widgets_manager.js.coffee +0 -45
  117. data/vendor/assets/javascripts/joosy/core/page.js.coffee +0 -136
  118. data/vendor/assets/javascripts/joosy/core/resource/rest.js.coffee +0 -69
  119. data/vendor/assets/javascripts/joosy/core/resource/rest_collection.js.coffee +0 -42
  120. data/vendor/assets/javascripts/joosy/core/router.js.coffee +0 -75
  121. data/vendor/assets/javascripts/joosy/core/widget.js.coffee +0 -35
  122. data/vendor/assets/javascripts/joosy/preloader/development.js.coffee +0 -27
  123. data/vendor/assets/javascripts/joosy/preloader/production.js.coffee +0 -84
@@ -8,36 +8,42 @@
8
8
  // native objects. IE8 does not have defineProperies, however, so this check saves a try/catch block.
9
9
  var definePropertySupport = object.defineProperty && object.defineProperties;
10
10
 
11
+ // Class extending methods
12
+
11
13
  function extend(klass, instance, override, methods) {
12
- var extendee = instance ? klass.prototype : klass;
14
+ var extendee = instance ? klass.prototype : klass, original;
13
15
  initializeClass(klass, instance, methods);
14
16
  iterateOverObject(methods, function(name, method) {
17
+ original = extendee[name];
15
18
  if(typeof override === 'function') {
16
- defineProperty(extendee, name, wrapNative(extendee[name], method, override));
17
- } else if(override === true || !extendee[name]) {
19
+ method = wrapNative(extendee[name], method, override);
20
+ }
21
+ if(override !== false || !extendee[name]) {
18
22
  defineProperty(extendee, name, method);
19
23
  }
20
24
  // If the method is internal to Sugar, then store a reference so it can be restored later.
21
- klass['SugarMethods'][name] = { instance: instance, method: method };
25
+ klass['SugarMethods'][name] = { instance: instance, method: method, original: original };
22
26
  });
23
27
  }
24
28
 
25
29
  function initializeClass(klass) {
26
30
  if(klass.SugarMethods) return;
27
31
  defineProperty(klass, 'SugarMethods', {});
28
- defineProperty(klass, 'restore', function() {
29
- var all = arguments.length === 0, methods = multiArgs(arguments);
30
- iterateOverObject(klass['SugarMethods'], function(name, m) {
31
- if(all || existsInArray(methods, name)) {
32
- defineProperty(m.instance ? klass.prototype : klass, name, m.method);
32
+ extend(klass, false, false, {
33
+ 'restore': function() {
34
+ var all = arguments.length === 0, methods = multiArgs(arguments);
35
+ iterateOverObject(klass['SugarMethods'], function(name, m) {
36
+ if(all || methods.has(name)) {
37
+ defineProperty(m.instance ? klass.prototype : klass, name, m.method);
38
+ }
39
+ });
40
+ },
41
+ 'extend': function(methods, override, instance) {
42
+ if(klass === object && arguments.length === 0) {
43
+ mapObjectPrototypeMethods();
44
+ } else {
45
+ extend(klass, instance !== false, override, methods);
33
46
  }
34
- });
35
- });
36
- defineProperty(klass, 'extend', function(methods, override, instance) {
37
- if(klass === object && arguments.length === 0) {
38
- mapObjectPrototypeMethods();
39
- } else {
40
- extend(klass, instance !== false, override, methods);
41
47
  }
42
48
  });
43
49
  }
@@ -60,48 +66,18 @@
60
66
  }
61
67
  }
62
68
 
63
- function iterateOverObject(obj, fn) {
64
- var count = 0;
65
- for(var key in obj) {
66
- if(!obj.hasOwnProperty(key)) continue;
67
- fn.call(obj, key, obj[key], count);
68
- count++;
69
- }
69
+ // Object helpers
70
+
71
+ function hasOwnProperty(obj, key) {
72
+ return object.prototype.hasOwnProperty.call(obj, key);
70
73
  }
71
74
 
72
- function equal(a, b, stack) {
73
- var primitive = object.prototype.toString.call(a).match(/\[object (\w+)\]/)[1];
74
- if(a === b) {
75
- return a !== 0 || 1 / a === 1 / b;
76
- } else if(isNull(a) || isUndefined(a) || isNull(b) || isUndefined(b)) {
77
- return false;
78
- } else if(primitive == 'RegExp') {
79
- return a.ignoreCase == b.ignoreCase &&
80
- a.multiline == b.multiline &&
81
- a.source == b.source &&
82
- a.global == b.global;
83
- } else if(primitive == 'Array' || primitive == 'Object') {
84
- // This method for checking for cyclic structures was egregiously stolen from
85
- // the ingenious method by @kitcambridge from the Underscore script... ref:
86
- // https://github.com/documentcloud/underscore/issues/240
87
- var length = stack.length;
88
- while (length--) {
89
- if (stack[length] == a) return true;
90
- }
91
- stack.push(a);
92
- for(var key in a) {
93
- if(!a.hasOwnProperty(key)) continue;
94
- if(!b.hasOwnProperty(key) || !equal(a[key], b[key], stack)) {
95
- return false;
96
- }
97
- }
98
- stack.pop();
99
- return object.keys(a).length === object.keys(b).length &&
100
- a.constructor === b.constructor &&
101
- a.length === b.length;
102
- } else {
103
- return isClass(b, primitive) && a.valueOf() === b.valueOf();
104
- }
75
+ function iterateOverObject(obj, fn) {
76
+ var key;
77
+ for(key in obj) {
78
+ if(!hasOwnProperty(obj, key)) continue;
79
+ fn.call(obj, key, obj[key]);
80
+ }
105
81
  }
106
82
 
107
83
  function multiMatch(el, match, scope, params) {
@@ -128,6 +104,52 @@
128
104
  }
129
105
  }
130
106
 
107
+ function stringify(thing, stack) {
108
+ var value, klass, isObject, isArray, arr, i, key, type = typeof thing;
109
+
110
+ // Return quickly if string to save cycles
111
+ if(type === 'string') return thing;
112
+
113
+ klass = object.prototype.toString.call(thing)
114
+ isObject = klass === '[object Object]';
115
+ isArray = klass === '[object Array]';
116
+
117
+ if(thing != null && isObject || isArray) {
118
+ // This method for checking for cyclic structures was egregiously stolen from
119
+ // the ingenious method by @kitcambridge from the Underscore script:
120
+ // https://github.com/documentcloud/underscore/issues/240
121
+ if(!stack) stack = [];
122
+ // Allowing a step into the structure before triggering this
123
+ // script to save cycles on standard JSON structures and also to
124
+ // try as hard as possible to catch basic properties that may have
125
+ // been modified.
126
+ if(stack.length > 1) {
127
+ i = stack.length;
128
+ while (i--) {
129
+ if (stack[i] === thing) {
130
+ return 'CYC';
131
+ }
132
+ }
133
+ }
134
+ stack.push(thing);
135
+ value = string(thing.constructor);
136
+ arr = isArray ? thing : object.keys(thing).sort();
137
+ for(i = 0; i < arr.length; i++) {
138
+ key = isArray ? i : arr[i];
139
+ value += key + stringify(thing[key], stack);
140
+ }
141
+ stack.pop();
142
+ } else if(1 / thing === -Infinity) {
143
+ value = '-0';
144
+ } else {
145
+ value = string(thing);
146
+ }
147
+ return type + klass + value;
148
+ }
149
+
150
+
151
+ // Argument helpers
152
+
131
153
  function transformArgument(el, map, context, mapArgs) {
132
154
  if(isUndefined(map)) {
133
155
  return el;
@@ -146,11 +168,12 @@
146
168
 
147
169
  function multiArgs(args, fn, flatten, index) {
148
170
  args = getArgs(args);
149
- if(flatten !== false) args = arrayFlatten(args);
171
+ if(flatten === true) args = arrayFlatten(args, 1);
150
172
  arrayEach(args, fn || function(){}, index);
151
173
  return args;
152
174
  }
153
175
 
176
+
154
177
  // Used for both arrays and strings
155
178
 
156
179
  function entryAtIndex(arr, args, str) {
@@ -179,40 +202,10 @@
179
202
  return object.prototype.toString.call(obj) === '[object '+str+']';
180
203
  }
181
204
 
182
- function isObjectPrimitive(o) {
183
- return typeof o == 'object';
184
- }
185
-
186
- function isNull(o) {
187
- return o === null;
188
- }
189
-
190
205
  function isUndefined(o) {
191
206
  return o === Undefined;
192
207
  }
193
208
 
194
- function isDefined(o) {
195
- return o !== Undefined;
196
- }
197
-
198
- function mergeObject(target, source, deep, resolve) {
199
- if(isObjectPrimitive(source)) {
200
- iterateOverObject(source, function(key, val) {
201
- var prop = target[key], conflict = isDefined(prop), isArray = object.isArray(val);
202
- if(deep === true && (isArray || object.isObject(val))) {
203
- if(!prop) prop = isArray ? [] : {};
204
- mergeObject(prop, val, deep);
205
- } else if(conflict && object.isFunction(resolve)) {
206
- prop = resolve.call(source, key, target[key], source[key])
207
- } else if(!conflict || (conflict && resolve !== false)) {
208
- prop = source[key];
209
- }
210
- target[key] = prop;
211
- });
212
- }
213
- return target;
214
- }
215
-
216
209
  function setParamsObject(obj, param, value, deep) {
217
210
  var reg = /^(.+?)(\[.*\])$/, isArray, match, allKeys, key;
218
211
  if(deep !== false && (match = param.match(reg))) {
@@ -247,13 +240,11 @@
247
240
  });
248
241
  }
249
242
 
250
- Hash.prototype.constructor = object;
251
-
252
243
  /***
253
244
  * @method is[Type](<obj>)
254
245
  * @returns Boolean
255
246
  * @short Returns true if <obj> is an object of that type.
256
- * @extra %isObject% will return false on anything that is not an object literal, including instances of inherited classes. Note also that %isNaN% will ONLY return true if the object IS %NaN%. It does not mean the same as browser native %isNaN%, which returns true for anything that is "not a number". Type methods are available as instance methods on extended objects and when using Object.extend().
247
+ * @extra %isObject% will return false on anything that is not an object literal, including instances of inherited classes. Note also that %isNaN% will ONLY return true if the object IS %NaN%. It does not mean the same as browser native %isNaN%, which returns true for anything that is "not a number". Type methods are available as instance methods on extended objects.
257
248
  * @example
258
249
  *
259
250
  * Object.isArray([1,2,3]) -> true
@@ -285,10 +276,15 @@
285
276
  ***/
286
277
 
287
278
 
279
+ var ObjectTypeMethods = ['isObject','isNaN'];
280
+ var ObjectHashMethods = ['keys','values','each','merge','isEmpty','clone','equal','watch','tap','has']
281
+
288
282
  function buildTypeMethods() {
289
- var methods = {};
283
+ var methods = {}, name;
290
284
  arrayEach(['Array','Boolean','Date','Function','Number','String','RegExp'], function(type) {
291
- methods['is' + type] = function(obj) {
285
+ name = 'is' + type;
286
+ ObjectTypeMethods.push(name);
287
+ methods[name] = function(obj) {
292
288
  return isClass(obj, type);
293
289
  }
294
290
  });
@@ -307,11 +303,11 @@
307
303
 
308
304
  function buildObject() {
309
305
  buildTypeMethods();
310
- buildInstanceMethods(['keys','values','each','merge','isEmpty','clone','equal','watch','tap'], Hash);
306
+ buildInstanceMethods(ObjectHashMethods, Hash);
311
307
  }
312
308
 
313
309
  function mapObjectPrototypeMethods() {
314
- buildInstanceMethods(Object.keys(Object['SugarMethods']).remove('extended', 'fromQueryString'), Object);
310
+ buildInstanceMethods(ObjectTypeMethods.concat(ObjectHashMethods), Object);
315
311
  }
316
312
 
317
313
  extend(object, false, true, {
@@ -319,7 +315,7 @@
319
315
  * @method watch(<obj>, <prop>, <fn>)
320
316
  * @returns Nothing
321
317
  * @short Watches a property of <obj> and runs <fn> when it changes.
322
- * @extra <fn> is passed three arguments: the property <prop>, the old value, and the new value. The return value of [fn] will be set as the new value. This method is useful for things such as validating or cleaning the value when it is set. Warning: this method WILL NOT work in browsers that don't support %Object.defineProperty%. This notably includes IE 8 and below, and Opera. This is the only method in Sugar that is not fully compatible with all browsers. %watch% is available as an instance method on extended objects and when using Object.extend().
318
+ * @extra <fn> is passed three arguments: the property <prop>, the old value, and the new value. The return value of [fn] will be set as the new value. This method is useful for things such as validating or cleaning the value when it is set. Warning: this method WILL NOT work in browsers that don't support %Object.defineProperty%. This notably includes IE 8 and below, and Opera. This is the only method in Sugar that is not fully compatible with all browsers. %watch% is available as an instance method on extended objects.
323
319
  * @example
324
320
  *
325
321
  * Object.watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) {
@@ -369,10 +365,11 @@
369
365
  * @set isType
370
366
  ***/
371
367
  'isObject': function(obj) {
372
- if(isNull(obj) || isUndefined(obj)) {
368
+ if(obj == null) {
373
369
  return false;
374
370
  } else {
375
- return isClass(obj, 'Object') && obj.constructor === object;
371
+ // === on the constructor is not safe across iframes
372
+ return isClass(obj, 'Object') && string(obj.constructor) === string(object) || obj.constructor === Hash;
376
373
  }
377
374
  },
378
375
 
@@ -389,7 +386,7 @@
389
386
  * @method each(<obj>, [fn])
390
387
  * @returns Object
391
388
  * @short Iterates over each property in <obj> calling [fn] on each iteration.
392
- * @extra %each% is available as an instance method on extended objects and when using Object.extend().
389
+ * @extra %each% is available as an instance method on extended objects.
393
390
  * @example
394
391
  *
395
392
  * Object.each({ broken:'wear' }, function(key, value) {
@@ -410,29 +407,62 @@
410
407
  },
411
408
 
412
409
  /***
413
- * @method merge(<target>, <source>, [resolve] = true)
410
+ * @method merge(<target>, <source>, [deep] = false, [resolve] = true)
414
411
  * @returns Merged object
415
412
  * @short Merges all the properties of <source> into <target>.
416
- * @extra Properties of <source> will win in the case of conflicts, unless [resolve] is %false%. [resolve] can also be a function that resolves the conflict. In this case it will be passed 3 arguments, %key%, %targetVal%, and %sourceVal%, with the context set to <source>. This will allow you to solve conflict any way you want, ie. adding two numbers together, etc. %merge% is available as an instance method on extended objects and when using Object.extend().
413
+ * @extra Merges are shallow unless [deep] is %true%. Properties of <source> will win in the case of conflicts, unless [resolve] is %false%. [resolve] can also be a function that resolves the conflict. In this case it will be passed 3 arguments, %key%, %targetVal%, and %sourceVal%, with the context set to <source>. This will allow you to solve conflict any way you want, ie. adding two numbers together, etc. %merge% is available as an instance method on extended objects.
417
414
  * @example
418
415
  *
419
416
  * Object.merge({a:1},{b:2}) -> { a:1, b:2 }
420
- * Object.merge({a:1},{a:2}, false) -> { a:1 }
421
- + Object.merge({a:1},{a:2}, function(key, a, b) {
417
+ * Object.merge({a:1},{a:2}, false, false) -> { a:1 }
418
+ + Object.merge({a:1},{a:2}, false, function(key, a, b) {
422
419
  * return a + b;
423
420
  * }); -> { a:3 }
424
421
  * Object.extended({a:1}).merge({b:2}) -> { a:1, b:2 }
425
422
  *
426
423
  ***/
427
- 'merge': function(target, merge, resolve) {
428
- return mergeObject(target, merge, true, resolve);
424
+ 'merge': function(target, source, deep, resolve) {
425
+ var key, val;
426
+ // Strings cannot be reliably merged thanks to
427
+ // their properties not being enumerable in < IE8.
428
+ if(target && typeof source != 'string') {
429
+ for(key in source) {
430
+ if(!hasOwnProperty(source, key) || !target) continue;
431
+ val = source[key];
432
+ // Conflict!
433
+ if(target[key] !== Undefined) {
434
+ // Do not merge.
435
+ if(resolve === false) {
436
+ continue;
437
+ }
438
+ // Use the result of the callback as the result.
439
+ if(object.isFunction(resolve)) {
440
+ val = resolve.call(source, key, target[key], source[key])
441
+ }
442
+ }
443
+ // Deep merging.
444
+ if(deep === true && val && typeof val === 'object') {
445
+ if(object.isDate(val)) {
446
+ val = new Date(val.getTime());
447
+ } else if(object.isRegExp(val)) {
448
+ val = new RegExp(val.source, val.getFlags());
449
+ } else {
450
+ if(!target[key]) target[key] = array.isArray(val) ? [] : {};
451
+ Object.merge(target[key], source[key], deep, resolve);
452
+ continue;
453
+ }
454
+ }
455
+ target[key] = val;
456
+ }
457
+ }
458
+ return target;
429
459
  },
430
460
 
431
461
  /***
432
462
  * @method isEmpty(<obj>)
433
463
  * @returns Boolean
434
464
  * @short Returns true if <obj> is empty.
435
- * @extra %isEmpty% is available as an instance method on extended objects and when using Object.extend().
465
+ * @extra %isEmpty% is available as an instance method on extended objects.
436
466
  * @example
437
467
  *
438
468
  * Object.isEmpty({}) -> true
@@ -441,7 +471,7 @@
441
471
  *
442
472
  ***/
443
473
  'isEmpty': function(obj) {
444
- if(!isObjectPrimitive(obj) || isNull(obj)) return !(obj && obj.length > 0);
474
+ if(obj == null || typeof obj != 'object') return !(obj && obj.length > 0);
445
475
  return object.keys(obj).length == 0;
446
476
  },
447
477
 
@@ -449,7 +479,7 @@
449
479
  * @method equal(<a>, <b>)
450
480
  * @returns Boolean
451
481
  * @short Returns true if <a> and <b> are equal.
452
- * @extra %empty% is available as an instance method as "equals" (note the "s") on extended objects and when using Object.extend().
482
+ * @extra %equal% in Sugar is "egal", meaning the values are equal if they are "not observably distinguishable". Note that on extended objects the name is %equals% for readability.
453
483
  * @example
454
484
  *
455
485
  * Object.equal({a:2}, {a:2}) -> true
@@ -458,14 +488,14 @@
458
488
  *
459
489
  ***/
460
490
  'equal': function(a, b) {
461
- return equal(a, b, []);
491
+ return stringify(a) === stringify(b);
462
492
  },
463
493
 
464
494
  /***
465
495
  * @method values(<obj>, [fn])
466
496
  * @returns Array
467
497
  * @short Returns an array containing the values in <obj>. Optionally calls [fn] for each value.
468
- * @extra Returned values are in no particular order. %values% is available as an instance method on extended objects and when using Object.extend().
498
+ * @extra Returned values are in no particular order. %values% is available as an instance method on extended objects.
469
499
  * @example
470
500
  *
471
501
  * Object.values({ broken: 'wear' }) -> ['wear']
@@ -488,7 +518,7 @@
488
518
  * @method clone(<obj> = {}, [deep] = false)
489
519
  * @returns Cloned object
490
520
  * @short Creates a clone (copy) of <obj>.
491
- * @extra Default is a shallow clone, unless [deep] is true. %clone% is available as an instance method on extended objects and when using Object.extend().
521
+ * @extra Default is a shallow clone, unless [deep] is true. %clone% is available as an instance method on extended objects.
492
522
  * @example
493
523
  *
494
524
  * Object.clone({foo:'bar'}) -> { foo: 'bar' }
@@ -497,9 +527,10 @@
497
527
  *
498
528
  ***/
499
529
  'clone': function(obj, deep) {
500
- if(!isObjectPrimitive(obj) || isNull(obj)) return obj;
501
- var target = Object.isFunction(obj.keys) ? Object.extended() : {};
502
- return mergeObject(target, obj, deep);
530
+ if(obj == null || typeof obj !== 'object') return obj;
531
+ if(array.isArray(obj)) return obj.clone();
532
+ var target = obj.constructor === Hash ? new Hash() : {};
533
+ return object.merge(target, obj, deep);
503
534
  },
504
535
 
505
536
  /***
@@ -539,6 +570,21 @@
539
570
  'tap': function(obj, fn) {
540
571
  transformArgument(obj, fn, obj, [obj]);
541
572
  return obj;
573
+ },
574
+
575
+ /***
576
+ * @method has(<obj>, <key>)
577
+ * @returns Boolean
578
+ * @short Checks if <obj> has <key> using hasOwnProperty from Object.prototype.
579
+ * @extra This method is considered safer than %Object#hasOwnProperty% when using objects as hashes. See %http://www.devthought.com/2012/01/18/an-object-is-not-a-hash/% for more.
580
+ * @example
581
+ *
582
+ * Object.has({ foo: 'bar' }, 'foo') -> true
583
+ * Object.has({ foo: 'bar' }, 'baz') -> false
584
+ * Object.has({ hasOwnProperty: true }, 'foo') -> false
585
+ ***/
586
+ 'has': function (obj, key) {
587
+ return hasOwnProperty(obj, key);
542
588
  }
543
589
 
544
590
  });
@@ -550,7 +596,7 @@
550
596
  * @method keys(<obj>, [fn])
551
597
  * @returns Array
552
598
  * @short Returns an array containing the keys in <obj>. Optionally calls [fn] for each key.
553
- * @extra This method is provided for browsers that don't support it natively, and additionally is enhanced to accept the callback [fn]. Returned keys are in no particular order. %keys% is available as an instance method on extended objects and when using Object.extend().
599
+ * @extra This method is provided for browsers that don't support it natively, and additionally is enhanced to accept the callback [fn]. Returned keys are in no particular order. %keys% is available as an instance method on extended objects.
554
600
  * @example
555
601
  *
556
602
  * Object.keys({ broken: 'wear' }) -> ['broken']
@@ -561,7 +607,7 @@
561
607
  *
562
608
  ***/
563
609
  'keys': function(obj, fn) {
564
- if(isNull(obj) || (!isObjectPrimitive(obj) && !object.isRegExp(obj) && !object.isFunction(obj))) {
610
+ if(obj == null || typeof obj != 'object' && !object.isRegExp(obj) && !object.isFunction(obj)) {
565
611
  throw new TypeError('Object required');
566
612
  }
567
613
  var keys = [];
@@ -587,7 +633,6 @@
587
633
  ***/
588
634
 
589
635
 
590
-
591
636
  // Basic array internal methods
592
637
 
593
638
  function arrayEach(arr, fn, startIndex, loop, sparse) {
@@ -619,19 +664,17 @@
619
664
  return returnIndex ? index : result;
620
665
  }
621
666
 
622
- function existsInArray(arr, obj) {
623
- return arr.any(function(el) {
624
- return object.equal(obj, el);
625
- });
626
- }
627
-
628
667
  function arrayUnique(arr, map) {
629
- var prop, test = function(el) { return transformArgument(el, map, arr, [el]) === prop; };
630
- return arr.reduce(function(result, next, index) {
631
- prop = transformArgument(next, map, arr, [next, index, arr]);
632
- if(result.none(map ? test : next)) result.push(next);
633
- return result;
634
- }, []);
668
+ var result = [], o = {}, stringified, transformed;
669
+ arrayEach(arr, function(el, i) {
670
+ transformed = map ? transformArgument(el, map, arr, [el, i, arr]) : el;
671
+ stringified = stringify(transformed);
672
+ if(!arrayObjectExists(o, stringified, el)) {
673
+ o[stringified] = transformed;
674
+ result.push(el);
675
+ }
676
+ })
677
+ return result;
635
678
  }
636
679
 
637
680
  function arrayFlatten(arr, level, current) {
@@ -649,18 +692,27 @@
649
692
  }
650
693
 
651
694
  function arrayIntersect(arr1, arr2, subtract) {
652
- var result = [];
695
+ var result = [], o = {};
696
+ arr2.each(function(el) {
697
+ o[stringify(el)] = el;
698
+ });
653
699
  arr1.each(function(el) {
700
+ var stringified = stringify(el), exists = arrayObjectExists(o, stringified, el);
654
701
  // Add the result to the array if:
655
702
  // 1. We're subtracting intersections or it doesn't already exist in the result and
656
703
  // 2. It exists in the compared array and we're adding, or it doesn't exist and we're removing.
657
- if((subtract || !existsInArray(result, el)) && subtract != existsInArray(arr2, el)) {
704
+ if(exists != subtract) {
705
+ delete o[stringified];
658
706
  result.push(el);
659
707
  }
660
708
  });
661
709
  return result;
662
710
  }
663
711
 
712
+ function arrayObjectExists(hash, stringified, obj) {
713
+ return stringified in hash && (typeof obj !== 'function' || obj === hash[stringified]);
714
+ }
715
+
664
716
  // ECMA5 methods
665
717
 
666
718
  function arrayIndexOf(arr, search, fromIndex, increment) {
@@ -684,7 +736,7 @@
684
736
  }
685
737
 
686
738
  function arrayReduce(arr, fn, initialValue, fromRight) {
687
- var length = arr.length, count = 0, defined = isDefined(initialValue), result, index;
739
+ var length = arr.length, count = 0, defined = initialValue !== Undefined, result, index;
688
740
  checkCallback(fn);
689
741
  if(length == 0 && !defined) {
690
742
  throw new TypeError('Reduce called on empty array with no initial value');
@@ -754,9 +806,7 @@
754
806
  iterateOverObject(obj, function(key) {
755
807
  var entry = obj[key];
756
808
  var test = transformArgument(entry, map, obj, isArray? [entry, key.toNumber(), obj] : []);
757
- if(isUndefined(test)) {
758
- return;
759
- } else if(test === edge) {
809
+ if(test === edge) {
760
810
  result.push(entry);
761
811
  } else if((max && test > edge) || (min && test < edge)) {
762
812
  result = [entry];
@@ -767,6 +817,74 @@
767
817
  }
768
818
 
769
819
 
820
+ // Alphanumeric collation helpers
821
+
822
+ function collateStrings(a, b) {
823
+ var aValue, bValue, aChar, bChar, aEquiv, bEquiv, index = 0, tiebreaker = 0;
824
+ a = getCollationReadyString(a);
825
+ b = getCollationReadyString(b);
826
+ do {
827
+ aChar = getCollationCharacter(a, index);
828
+ bChar = getCollationCharacter(b, index);
829
+ aValue = getCollationValue(aChar);
830
+ bValue = getCollationValue(bChar);
831
+ if(aValue === -1 || bValue === -1) {
832
+ aValue = a.charCodeAt(index) || null;
833
+ bValue = b.charCodeAt(index) || null;
834
+ }
835
+ aEquiv = aChar !== a.charAt(index);
836
+ bEquiv = bChar !== b.charAt(index);
837
+ if(aEquiv !== bEquiv && tiebreaker === 0) {
838
+ tiebreaker = aEquiv - bEquiv;
839
+ }
840
+ index += 1;
841
+ } while(aValue != null && bValue != null && aValue === bValue);
842
+ if(aValue === bValue) return tiebreaker;
843
+ return aValue < bValue ? -1 : 1;
844
+ }
845
+
846
+ function getCollationReadyString(str) {
847
+ if(array[AlphanumericSortIgnoreCase]) {
848
+ str = str.toLowerCase();
849
+ }
850
+ return str.remove(array[AlphanumericSortIgnore]);
851
+ }
852
+
853
+ function getCollationCharacter(str, index) {
854
+ var chr = str.charAt(index), eq = array[AlphanumericSortEquivalents] || {};
855
+ return eq[chr] || chr;
856
+ }
857
+
858
+ function getCollationValue(chr) {
859
+ if(!chr) {
860
+ return null;
861
+ } else {
862
+ return array[AlphanumericSortOrder].indexOf(chr);
863
+ }
864
+ }
865
+
866
+ var AlphanumericSortOrder = 'AlphanumericSortOrder';
867
+ var AlphanumericSortIgnore = 'AlphanumericSortIgnore';
868
+ var AlphanumericSortIgnoreCase = 'AlphanumericSortIgnoreCase';
869
+ var AlphanumericSortEquivalents = 'AlphanumericSortEquivalents';
870
+
871
+ function buildArray() {
872
+ var order = 'AÁÀÂÃĄBCĆČÇDĎÐEÉÈĚÊËĘFGĞHıIÍÌİÎÏJKLŁMNŃŇÑOÓÒÔPQRŘSŚŠŞTŤUÚÙŮÛÜVWXYÝZŹŻŽÞÆŒØÕÅÄÖ';
873
+ var equiv = 'AÁÀÂÃÄ,CÇ,EÉÈÊË,IÍÌİÎÏ,OÓÒÔÕÖ,Sß,UÚÙÛÜ';
874
+ array[AlphanumericSortOrder] = order.split('').map(function(str) {
875
+ return str + str.toLowerCase();
876
+ }).join('');
877
+ var equivalents = {};
878
+ equiv.split(',').each(function(set) {
879
+ var equivalent = set.charAt(0);
880
+ set.slice(1).chars(function(chr) {
881
+ equivalents[chr] = equivalent;
882
+ equivalents[chr.toLowerCase()] = equivalent.toLowerCase();
883
+ });
884
+ });
885
+ array[AlphanumericSortIgnoreCase] = true;
886
+ array[AlphanumericSortEquivalents] = equivalents;
887
+ }
770
888
 
771
889
  extend(array, false, false, {
772
890
 
@@ -1099,7 +1217,7 @@
1099
1217
  ***/
1100
1218
  'findIndex': function(f, startIndex, loop) {
1101
1219
  var index = arrayFind(this, f, startIndex, loop, true);
1102
- return isDefined(index) ? index : -1;
1220
+ return isUndefined(index) ? -1 : index;
1103
1221
  },
1104
1222
 
1105
1223
  /***
@@ -1164,7 +1282,7 @@
1164
1282
  i++;
1165
1283
  }
1166
1284
  }
1167
- }, false);
1285
+ });
1168
1286
  return arr;
1169
1287
  },
1170
1288
 
@@ -1250,7 +1368,7 @@
1250
1368
  *
1251
1369
  ***/
1252
1370
  'clone': function() {
1253
- return mergeObject([], this);
1371
+ return object.merge([], this);
1254
1372
  },
1255
1373
 
1256
1374
  /***
@@ -1284,8 +1402,8 @@
1284
1402
  ***/
1285
1403
  'union': function() {
1286
1404
  var arr = this;
1287
- multiArgs(arguments, function(a) {
1288
- arr = arr.concat(a);
1405
+ multiArgs(arguments, function(arg) {
1406
+ arr = arr.concat(arg);
1289
1407
  });
1290
1408
  return arrayUnique(arr);
1291
1409
  },
@@ -1301,7 +1419,7 @@
1301
1419
  *
1302
1420
  ***/
1303
1421
  'intersect': function() {
1304
- return arrayIntersect(this, multiArgs(arguments), false);
1422
+ return arrayIntersect(this, multiArgs(arguments, null, true), false);
1305
1423
  },
1306
1424
 
1307
1425
  /***
@@ -1316,7 +1434,7 @@
1316
1434
  *
1317
1435
  ***/
1318
1436
  'subtract': function(a) {
1319
- return arrayIntersect(this, multiArgs(arguments), true);
1437
+ return arrayIntersect(this, multiArgs(arguments, null, true), true);
1320
1438
  },
1321
1439
 
1322
1440
  /***
@@ -1623,7 +1741,7 @@
1623
1741
  result.push(el.compact());
1624
1742
  } else if(all && el) {
1625
1743
  result.push(el);
1626
- } else if(!all && isDefined(el) && !isNull(el) && (!object.isNumber(el) || !isNaN(el))) {
1744
+ } else if(!all && el != null && !object.isNaN(el)) {
1627
1745
  result.push(el);
1628
1746
  }
1629
1747
  });
@@ -1664,7 +1782,7 @@
1664
1782
  * @method sortBy(<map>, [desc] = false)
1665
1783
  * @returns Array
1666
1784
  * @short Sorts the array by <map>.
1667
- * @extra <map> may be a function or a string acting as a shortcut. [desc] will sort the array in descending order.
1785
+ * @extra <map> may be a function, a string acting as a shortcut, or blank (direct comparison of array values). [desc] will sort the array in descending order. When the field being sorted on is a string, the resulting order will be determined by an internal algorithm that is optimized for major Western languages, but can be customized. For more information see @array_sorting.
1668
1786
  * @example
1669
1787
  *
1670
1788
  * ['world','a','new'].sortBy('length') -> ['a','new','world']
@@ -1680,10 +1798,14 @@
1680
1798
  var aProperty, bProperty, comp;
1681
1799
  aProperty = transformArgument(a, map, arr, [a]);
1682
1800
  bProperty = transformArgument(b, map, arr, [b]);
1683
- if(aProperty && aProperty.compare) {
1684
- comp = aProperty.compare(bProperty);
1801
+ if(object.isString(aProperty) && object.isString(bProperty)) {
1802
+ comp = collateStrings(aProperty, bProperty);
1803
+ } else if(aProperty < bProperty) {
1804
+ comp = -1;
1805
+ } else if(aProperty > bProperty) {
1806
+ comp = 1;
1685
1807
  } else {
1686
- comp = aProperty < bProperty;
1808
+ comp = 0;
1687
1809
  }
1688
1810
  return comp * (desc ? -1 : 1);
1689
1811
  });
@@ -1749,7 +1871,6 @@
1749
1871
  return arguments.length > 0 ? result : result[0];
1750
1872
  }
1751
1873
 
1752
-
1753
1874
  });
1754
1875
 
1755
1876
 
@@ -1855,8 +1976,8 @@
1855
1976
  'random': function(n1, n2) {
1856
1977
  var min, max;
1857
1978
  if(arguments.length == 1) n2 = n1, n1 = 0;
1858
- min = Math.min(n1 || 0, isDefined(n2) ? n2 : 1);
1859
- max = Math.max(n1 || 0, isDefined(n2) ? n2 : 1);
1979
+ min = Math.min(n1 || 0, isUndefined(n2) ? 1 : n2);
1980
+ max = Math.max(n1 || 0, isUndefined(n2) ? 1 : n2);
1860
1981
  return round((Math.random() * (max - min)) + min);
1861
1982
  }
1862
1983
 
@@ -1908,7 +2029,7 @@
1908
2029
  *
1909
2030
  ***/
1910
2031
  'metric': function(precision, limit) {
1911
- return abbreviateNumber(this, precision, 'nμm kMGTPE', 4, isDefined(limit) ? limit : 1);
2032
+ return abbreviateNumber(this, precision, 'nμm kMGTPE', 4, isUndefined(limit) ? 1 : limit);
1912
2033
  },
1913
2034
 
1914
2035
  /***
@@ -1925,7 +2046,7 @@
1925
2046
  *
1926
2047
  ***/
1927
2048
  'bytes': function(precision, limit) {
1928
- return abbreviateNumber(this, precision, 'kMGTPE', 0, isDefined(limit) ? limit : 4, true) + 'B';
2049
+ return abbreviateNumber(this, precision, 'kMGTPE', 0, isUndefined(limit) ? 4 : limit, true) + 'B';
1929
2050
  },
1930
2051
 
1931
2052
  /***
@@ -2231,22 +2352,6 @@
2231
2352
  ***/
2232
2353
  'hex': function(pad) {
2233
2354
  return this.pad(pad || 1, false, 16);
2234
- },
2235
-
2236
- /***
2237
- * @method compare(<num>)
2238
- * @returns Number
2239
- * @short Performs a numeric comparison against the number.
2240
- * @extra This method is also defined on %String% and %Date%, and is useful when performing complex sort operations where the type isn't known.
2241
- * @example
2242
- *
2243
- * (255).compare(254) -> 1;
2244
- * (245).compare(254) -> -9;
2245
- * (0).compare(0) -> 0;
2246
- *
2247
- ***/
2248
- 'compare': function(num) {
2249
- return this - Number(num);
2250
2355
  }
2251
2356
 
2252
2357
  });
@@ -2473,8 +2578,13 @@
2473
2578
  return string.Inflector && string.Inflector.acronyms && string.Inflector.acronyms[word];
2474
2579
  }
2475
2580
 
2581
+ var btoa, atob;
2582
+
2476
2583
  function buildBase64(key) {
2477
- if(isDefined(this.btoa)) return;
2584
+ if(this.btoa) {
2585
+ btoa = this.btoa;
2586
+ atob = this.atob;
2587
+ }
2478
2588
  var base64reg = /[^A-Za-z0-9\+\/\=]/g;
2479
2589
  btoa = function(str) {
2480
2590
  var output = '';
@@ -2531,8 +2641,6 @@
2531
2641
  }
2532
2642
  }
2533
2643
 
2534
-
2535
-
2536
2644
  function buildTrim() {
2537
2645
  var support = getTrimmableCharacters().match(/^\s+$/);
2538
2646
  try { string.prototype.trim.call([1]); } catch(e) { support = false; }
@@ -2705,7 +2813,7 @@
2705
2813
  *
2706
2814
  ***/
2707
2815
  'capitalize': function(all) {
2708
- var reg = all ? /\b[a-z]/g : /^[a-z]/;
2816
+ var reg = all ? /^\S|\s\S/g : /^\S/;
2709
2817
  return this.toLowerCase().replace(reg, function(letter) {
2710
2818
  return letter.toUpperCase();
2711
2819
  });
@@ -2769,7 +2877,7 @@
2769
2877
  },
2770
2878
 
2771
2879
  /***
2772
- * @method each([search] = /./g, [fn])
2880
+ * @method each([search] = single character, [fn])
2773
2881
  * @returns Array
2774
2882
  * @short Runs callback [fn] against each occurence of [search].
2775
2883
  * @extra Returns an array of matches. [search] may be either a string or regex, and defaults to every character in the string.
@@ -2785,9 +2893,9 @@
2785
2893
  'each': function(search, fn) {
2786
2894
  if(object.isFunction(search)) {
2787
2895
  fn = search;
2788
- search = /./g;
2896
+ search = /[\s\S]/g;
2789
2897
  } else if(!search) {
2790
- search = /./g
2898
+ search = /[\s\S]/g
2791
2899
  } else if(object.isString(search)) {
2792
2900
  search = regexp(regexp.escape(search), 'gi');
2793
2901
  } else if(object.isRegExp(search)) {
@@ -2856,7 +2964,7 @@
2856
2964
  *
2857
2965
  ***/
2858
2966
  'chars': function(fn) {
2859
- return this.trim().each(fn);
2967
+ return this.each(fn);
2860
2968
  },
2861
2969
 
2862
2970
  /***
@@ -2980,7 +3088,7 @@
2980
3088
  *
2981
3089
  ***/
2982
3090
  'has': function(find) {
2983
- return this.search(find) !== -1;
3091
+ return this.search(object.isRegExp(find) ? find : RegExp.escape(find)) !== -1;
2984
3092
  },
2985
3093
 
2986
3094
 
@@ -3137,8 +3245,9 @@
3137
3245
  *
3138
3246
  ***/
3139
3247
  'compact': function() {
3140
- var str = this.replace(/[\r\n]/g, '');
3141
- return str.trim().replace(/([\s ])+/g, '$1');
3248
+ return this.trim().replace(/([\r\n\s ])+/g, function(match, whitespace){
3249
+ return whitespace === ' ' ? whitespace : ' ';
3250
+ });
3142
3251
  },
3143
3252
 
3144
3253
  /***
@@ -3171,7 +3280,7 @@
3171
3280
  *
3172
3281
  ***/
3173
3282
  'first': function(num) {
3174
- num = isUndefined(num) ? 1 : num;
3283
+ if(isUndefined(num)) num = 1;
3175
3284
  return this.substr(0, num);
3176
3285
  },
3177
3286
 
@@ -3186,7 +3295,7 @@
3186
3295
  *
3187
3296
  ***/
3188
3297
  'last': function(num) {
3189
- num = isUndefined(num) ? 1 : num;
3298
+ if(isUndefined(num)) num = 1;
3190
3299
  var start = this.length - num < 0 ? 0 : this.length - num;
3191
3300
  return this.substr(start);
3192
3301
  },
@@ -3346,28 +3455,51 @@
3346
3455
  },
3347
3456
 
3348
3457
  /***
3349
- * @method truncate(<length>, [append] = '...', [split] = false)
3458
+ * @method truncate(<length>, [split] = true, from = 'right', [ellipsis] = '...')
3350
3459
  * @returns Object
3351
3460
  * @short Truncates a string.
3352
- * @extra Unless [split] is true, %truncate% will not split words up, and instead discard the word where the truncation occurred.
3353
- * @example
3354
- *
3355
- * 'just sittin on the dock of the bay'.truncate(20) -> 'just sittin on the...'
3356
- * 'just sittin on the dock of the bay'.truncate(20, '...', true) -> 'just sittin on the do...'
3357
- * 'just sittin on the dock of the bay'.truncate(20, ' >>>', false) -> 'just sittin on the >>>'
3358
- *
3359
- ***/
3360
- 'truncate': function(length, append, split) {
3361
- var reg, repeatedCharacter;
3362
- append = isUndefined(append) ? '...' : String(append);
3363
- length -= append.length;
3364
- if(this.length <= length) return this.toString();
3365
- repeatedCharacter = append.match(/^(.)\1+$/) ? append.slice(0,1) : '';
3366
- reg = regexp('[^' + getTrimmableCharacters() + repeatedCharacter + '][' + getTrimmableCharacters() + repeatedCharacter + ']');
3367
- while(length > 0 && !reg.test(this.slice(length - 1, length + 1)) && split !== true) {
3368
- length--;
3461
+ * @extra If [split] is %false%, will not split words up, and instead discard the word where the truncation occurred. [from] can also be %"middle"% or %"left"%.
3462
+ * @example
3463
+ *
3464
+ * 'just sittin on the dock of the bay'.truncate(20) -> 'just sittin on the do...'
3465
+ * 'just sittin on the dock of the bay'.truncate(20, false) -> 'just sittin on the...'
3466
+ * 'just sittin on the dock of the bay'.truncate(20, true, 'middle') -> 'just sitt...of the bay'
3467
+ * 'just sittin on the dock of the bay'.truncate(20, true, 'middle') -> '...the dock of the bay'
3468
+ *
3469
+ ***/
3470
+ 'truncate': function(length, split, from, ellipsis) {
3471
+ var pos,
3472
+ prepend = '',
3473
+ append = '',
3474
+ str = this.toString(),
3475
+ chars = '[' + getTrimmableCharacters() + ']+',
3476
+ space = '[^' + getTrimmableCharacters() + ']*',
3477
+ reg = regexp(chars + space + '$');
3478
+ ellipsis = isUndefined(ellipsis) ? '...' : string(ellipsis);
3479
+ if(str.length <= length) {
3480
+ return str;
3481
+ }
3482
+ switch(from) {
3483
+ case 'left':
3484
+ pos = str.length - length;
3485
+ prepend = ellipsis;
3486
+ str = str.slice(pos);
3487
+ reg = regexp('^' + space + chars);
3488
+ break;
3489
+ case 'middle':
3490
+ pos = Math.floor(length / 2);
3491
+ append = ellipsis + str.slice(str.length - pos).trimLeft();
3492
+ str = str.slice(0, pos);
3493
+ break;
3494
+ default:
3495
+ pos = length;
3496
+ append = ellipsis;
3497
+ str = str.slice(0, pos);
3498
+ }
3499
+ if(split === false && this.slice(pos, pos + 1).match(/\S/)) {
3500
+ str = str.remove(reg);
3369
3501
  }
3370
- return this.slice(0, length) + (length > 0 ? append : '');
3502
+ return prepend + str + append;
3371
3503
  },
3372
3504
 
3373
3505
  /***
@@ -3392,32 +3524,8 @@
3392
3524
  }
3393
3525
  });
3394
3526
  return this.replace(/\{(.+?)\}/g, function(m, key) {
3395
- return assign.hasOwnProperty(key) ? assign[key] : m;
3527
+ return hasOwnProperty(assign, key) ? assign[key] : m;
3396
3528
  });
3397
- },
3398
-
3399
- /***
3400
- * @method compare(<str>, [ignore] = false)
3401
- * @returns Number
3402
- * @short Performs a lexical (alphabetic) comparison against the number.
3403
- * @extra This method is also defined on %Number% and %Date%, and is useful when performing complex sort operations where the type isn't known. If [ignore] is %true%, will ignore any non-alphanumeric character when performing comparison. [ignore] can also be a regexp.
3404
- * @example
3405
- *
3406
- * ('a').compare('b') -> -1;
3407
- * ('b').compare('a') -> 1;
3408
- * ('a').compare('a') -> 0;
3409
- * ('a').compare('@a', true) -> 0;
3410
- *
3411
- ***/
3412
- 'compare': function(cmp, ignore) {
3413
- var str = this, cmp = String(cmp);
3414
- if(ignore === true) ignore = /\W/g;
3415
- if(ignore) {
3416
- cmp = cmp.remove(ignore);
3417
- str = str.remove(ignore);
3418
- }
3419
- if(str == cmp) return 0;
3420
- else return str < cmp ? -1 : 1;
3421
3529
  }
3422
3530
 
3423
3531
  });
@@ -3548,7 +3656,7 @@
3548
3656
  ***/
3549
3657
  'escape': function(str) {
3550
3658
  if(!object.isString(str)) str = String(str);
3551
- return str.replace(/([/'*+?|()\[\]{}.^$])/g,'\\$1');
3659
+ return str.replace(/([\\/'*+?|()\[\]{}.^$])/g,'\\$1');
3552
3660
  }
3553
3661
 
3554
3662
  });
@@ -3619,6 +3727,7 @@
3619
3727
 
3620
3728
  function setDelay(fn, ms, after, scope, args) {
3621
3729
  if(!fn.timers) fn.timers = [];
3730
+ if(!object.isNumber(ms)) ms = 0;
3622
3731
  fn.timers.push(setTimeout(function(){
3623
3732
  fn.timers.removeAt(index);
3624
3733
  after.apply(scope, args || []);
@@ -3696,12 +3805,12 @@
3696
3805
  *
3697
3806
  ***/
3698
3807
  'lazy': function(ms, limit) {
3699
- var fn = this, queue = [], lock = false, rounded, perExecution;
3808
+ var fn = this, queue = [], lock = false, execute, rounded, perExecution;
3700
3809
  ms = ms || 1;
3701
3810
  limit = limit || Infinity;
3702
3811
  rounded = ms.ceil();
3703
3812
  perExecution = round(rounded / ms);
3704
- var execute = function() {
3813
+ execute = function() {
3705
3814
  if(lock || queue.length == 0) return;
3706
3815
  var max = Math.max(queue.length - perExecution, 0);
3707
3816
  while(queue.length > max) {
@@ -3738,36 +3847,43 @@
3738
3847
  ***/
3739
3848
  'delay': function(ms) {
3740
3849
  var fn = this;
3741
- if(!object.isNumber(ms)) ms = 0;
3742
3850
  var args = getArgs(arguments, 1);
3743
3851
  setDelay(fn, ms, fn, fn, args);
3744
3852
  return fn;
3745
3853
  },
3746
3854
 
3747
3855
  /***
3748
- * @method debounce(<ms>, [wait] = true)
3856
+ * @method throttle(<ms>)
3749
3857
  * @returns Function
3750
- * @short Calls a function only once per <ms> no matter how many times it is actually called.
3751
- * @extra This method is useful to execute a function after things have "settled down". A good example of this is when a user tabs quickly through form fields, execution of a heavy operation should happen after a few milliseconds when they have "settled" on a field. If [wait] is %false% execution will happen immediately, and all subsequent calls within <ms> will be ignored.
3858
+ * @short Creates a throttled version of the function that will only be executed once per <ms> milliseconds.
3859
+ * @example
3860
+ *
3861
+ * var fn = (function(arg1) {
3862
+ * // called immediately and will wait 50ms until it responds again
3863
+ * }).throttle(50); fn() fn() fn();
3864
+ *
3865
+ ***/
3866
+ 'throttle': function(ms) {
3867
+ return this.lazy(ms, 1);
3868
+ },
3869
+
3870
+ /***
3871
+ * @method debounce(<ms>)
3872
+ * @returns Function
3873
+ * @short Creates a "debounced" function that postpones its execution until after <ms> milliseconds have passed.
3874
+ * @extra This method is useful to execute a function after things have "settled down". A good example of this is when a user tabs quickly through form fields, execution of a heavy operation should happen after a few milliseconds when they have "settled" on a field.
3752
3875
  * @example
3753
3876
  *
3754
3877
  * var fn = (function(arg1) {
3755
3878
  * // called once 50ms later
3756
3879
  * }).debounce(50); fn() fn() fn();
3757
- * var fn = (function(arg1) {
3758
- * // called immediately and will wait 50ms until it responds again
3759
- * }).debounce(50, false); fn() fn() fn();
3760
3880
  *
3761
3881
  ***/
3762
- 'debounce': function(ms, wait) {
3882
+ 'debounce': function(ms) {
3763
3883
  var fn = this;
3764
- if(wait === false) {
3765
- return this.lazy(ms, 1);
3766
- } else {
3767
- return function() {
3768
- fn.cancel();
3769
- setDelay(fn, ms, fn, this, arguments);
3770
- }
3884
+ return function() {
3885
+ fn.cancel();
3886
+ setDelay(fn, ms, fn, this, arguments);
3771
3887
  }
3772
3888
  },
3773
3889
 
@@ -3840,7 +3956,7 @@
3840
3956
  'once': function() {
3841
3957
  var fn = this;
3842
3958
  return function() {
3843
- return fn.hasOwnProperty('memo') ? fn['memo'] : fn['memo'] = fn.apply(this, arguments);
3959
+ return hasOwnProperty(fn, 'memo') ? fn['memo'] : fn['memo'] = fn.apply(this, arguments);
3844
3960
  }
3845
3961
  },
3846
3962
 
@@ -3862,7 +3978,7 @@
3862
3978
  return function() {
3863
3979
  var args = getArgs(arguments);
3864
3980
  arrayEach(curried, function(arg, index) {
3865
- if(isDefined(arg) || index >= args.length) args.splice(index, 0, arg);
3981
+ if(arg != null || index >= args.length) args.splice(index, 0, arg);
3866
3982
  });
3867
3983
  return fn.apply(this, args);
3868
3984
  }
@@ -3876,13 +3992,18 @@
3876
3992
  buildObject();
3877
3993
  buildString();
3878
3994
  buildFunction();
3995
+ buildArray();
3879
3996
  initializeClass(date);
3880
3997
 
3998
+ Object.initializeClass = initializeClass;
3999
+
4000
+
3881
4001
  })();
4002
+ // Google Closure Compiler will output a wrapping function here.
3882
4003
  (function() {
3883
4004
 
3884
-
3885
- var regexp = RegExp, object = Object, date = Date, number = Number, Undefined;
4005
+ // A few optimizations for Google Closure Compiler will save us a couple kb in the release script.
4006
+ var regexp = RegExp, object = Object, date = Date, number = Number, Undefined, English;
3886
4007
 
3887
4008
  function isDefined(o) {
3888
4009
  return o !== Undefined;
@@ -3898,9 +4019,11 @@
3898
4019
  *
3899
4020
  ***/
3900
4021
 
3901
- var TimeFormat = ['hour','minute','second','millisecond','meridian','utc','offset_sign','offset_hours','offset_minutes']
3902
- var RequiredTime = '(\\d{1,2}):?(\\d{2})?:?(\\d{2})?(?:\\.(\\d{1,6}))?(am|pm)?(?:(Z)|(?:([+-])(\\d{2})(?::?(\\d{2}))?)?)?';
4022
+ var TimeFormat = ['hour','minute','second','meridian','utc','offset_sign','offset_hours','offset_minutes']
4023
+ var FloatReg = '\\d{1,2}(?:[,.]\\d+)?';
4024
+ var RequiredTime = '('+FloatReg+'):?('+FloatReg+')?:?('+FloatReg+')?(am|pm)?(?:(Z)|(?:([+-])(\\d{2})(?::?(\\d{2}))?)?)?';
3903
4025
  var OptionalTime = '\\s*(?:(?:t|at |\\s+)' + RequiredTime + ')?';
4026
+
3904
4027
  var LowerAsianDigits = '一二三四五六七八九';
3905
4028
  var UpperAsianDigits = '十百千万';
3906
4029
  var AsianDigitReg = regexp('[' + LowerAsianDigits + UpperAsianDigits + ']', 'g');
@@ -4106,80 +4229,55 @@
4106
4229
 
4107
4230
  var CommonLocales = {
4108
4231
 
4109
- 'en': '2;;January,February,March,April,May,June,July,August,September,October,November,December;Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday;millisecond:|s,second:|s,minute:|s,hour:|s,day:|s,week:|s,month:|s,year:|s;one,two,three,four,five,six,seven,eight,nine,ten;a,an,the;the,st|nd|rd|th,of;{num} {unit} {sign},{num} {unit=4-5} {sign} {day},{weekday?} {month} {date}{2} {year?} {time?},{date} {month} {year},{month} {year},{shift?} {weekday} {time?},{shift} week {weekday} {time?},{shift} {unit=5-7},{1} {edge} of {shift?} {unit=4-7?}{month?}{year?},{weekday} {3} {shift} week,{1} {date}{2} of {month},{1}{month?} {date?}{2} of {shift} {unit=6-7},{day} at {time?},{time} {day};{Month} {d}, {yyyy};,yesterday,today,tomorrow;,ago|before,,from now|after|from;,last,the|this,next;last day,end,,first day|beginning',
4232
+ 'en': '2;;January,February,March,April,May,June,July,August,September,October,November,December;Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday;millisecond:|s,second:|s,minute:|s,hour:|s,day:|s,week:|s,month:|s,year:|s;one,two,three,four,five,six,seven,eight,nine,ten;a,an,the;the,st|nd|rd|th,of;{num} {unit} {sign},{num} {unit=4-5} {sign} {day},{weekday?} {month} {date}{1} {year?} {time?},{date} {month} {year},{month} {year},{shift?} {weekday} {time?},{shift} week {weekday} {time?},{shift} {unit=5-7},{0} {edge} of {shift?} {unit=4-7?}{month?}{year?},{weekday} {2} {shift} week,{0} {date}{1} of {month},{0}{month?} {date?}{1} of {shift} {unit=6-7},{day} at {time?},{time} {day};{Month} {d}, {yyyy};,yesterday,today,tomorrow;,ago|before,,from now|after|from;,last,the|this,next;last day,end,,first day|beginning',
4110
4233
 
4111
4234
  'ja': '1;月;;日曜日,月曜日,火曜日,水曜日,木曜日,金曜日,土曜日;ミリ秒,秒,分,時間,日,週間|週,ヶ月|ヵ月|月,年;;;;{num}{unit}{sign},{shift}{unit=5-7}{weekday?},{year}年{month?}月?{date?}日?,{month}月{date?}日?,{date}日;{yyyy}年{M}月{d}日;一昨日,昨日,今日,明日,明後日;,前,,後;,去|先,,来',
4112
4235
 
4113
4236
  'ko': '1;월;;일요일,월요일,화요일,수요일,목요일,금요일,토요일;밀리초,초,분,시간,일,주,개월|달,년;일|한,이,삼,사,오,육,칠,팔,구,십;;;{num}{unit} {sign},{shift} {unit=5-7},{shift} {unit=5?} {weekday},{year}년{month?}월?{date?}일?,{month}월{date?}일?,{date}일;{yyyy}년{M}월{d}일;그저께,어제,오늘,내일,모레;,전,,후;,지난|작,이번,다음|내',
4114
4237
 
4115
- 'ru': '4;;Январ:я|ь,Феврал:я|ь,Март:а|,Апрел:я|ь,Ма:я|й,Июн:я|ь,Июл:я|ь,Август:а|,Сентябр:я|ь,Октябр:я|ь,Ноябр:я|ь,Декабр:я|ь;Воскресенье,Понедельник,Вторник,Среда,Четверг,Пятница,Суббота;миллисекунд:а|у|ы|,секунд:а|у|ы|,минут:а|у|ы|,час:||а|ов,день|день|дня|дней,недел:я|ю|и|ь|е,месяц:||а|ев|е,год|год|года|лет|году;од:ин|ну,дв:а|е,три,четыре,пять,шесть,семь,восемь,девять,десять;;в|на,года;{num} {unit} {sign},{sign} {num} {unit},{date} {month} {year?} {2},{month} {year},{1} {shift} {unit=5-7};{d} {month} {yyyy} года;позавчера,вчера,сегодня,завтра,послезавтра;,назад,,через;,прошло:й|м,,следующе:й|м',
4238
+ 'ru': '4;;Январ:я|ь,Феврал:я|ь,Март:а|,Апрел:я|ь,Ма:я|й,Июн:я|ь,Июл:я|ь,Август:а|,Сентябр:я|ь,Октябр:я|ь,Ноябр:я|ь,Декабр:я|ь;Воскресенье,Понедельник,Вторник,Среда,Четверг,Пятница,Суббота;миллисекунд:а|у|ы|,секунд:а|у|ы|,минут:а|у|ы|,час:||а|ов,день|день|дня|дней,недел:я|ю|и|ь|е,месяц:||а|ев|е,год|год|года|лет|году;од:ин|ну,дв:а|е,три,четыре,пять,шесть,семь,восемь,девять,десять;;в|на,года;{num} {unit} {sign},{sign} {num} {unit},{date} {month} {year?} {1},{month} {year},{0} {shift} {unit=5-7};{d} {month} {yyyy} года;позавчера,вчера,сегодня,завтра,послезавтра;,назад,,через;,прошло:й|м,,следующе:й|м',
4116
4239
 
4117
- 'es': '6;;enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre;domingo,lunes,martes,miércoles|miercoles,jueves,viernes,sábado|sabado;milisegundo:|s,segundo:|s,minuto:|s,hora:|s,día|días|dia|dias,semana:|s,mes:|es,año|años|ano|anos;uno,dos,tres,cuatro,cinco,seis,siete,ocho,nueve,diez;;el,de;{sign} {num} {unit},{num} {unit} {sign},{date?} {2} {month} {2} {year?},{1} {unit=5-7} {shift},{1} {shift} {unit=5-7};{d} de {month} de {yyyy};anteayer,ayer,hoy,mañana|manana;,hace,,de ahora;,pasad:o|a,,próximo|próxima|proximo|proxima',
4240
+ 'es': '6;;enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre;domingo,lunes,martes,miércoles|miercoles,jueves,viernes,sábado|sabado;milisegundo:|s,segundo:|s,minuto:|s,hora:|s,día|días|dia|dias,semana:|s,mes:|es,año|años|ano|anos;uno,dos,tres,cuatro,cinco,seis,siete,ocho,nueve,diez;;el,de;{sign} {num} {unit},{num} {unit} {sign},{date?} {1} {month} {1} {year?},{0} {unit=5-7} {shift},{0} {shift} {unit=5-7};{d} de {month} de {yyyy};anteayer,ayer,hoy,mañana|manana;,hace,,de ahora;,pasad:o|a,,próximo|próxima|proximo|proxima',
4118
4241
 
4119
- 'pt': '6;;janeiro,fevereiro,março,abril,maio,junho,julho,agosto,setembro,outubro,novembro,dezembro;domingo,segunda-feira,terça-feira,quarta-feira,quinta-feira,sexta-feira,sábado|sabado;milisegundo:|s,segundo:|s,minuto:|s,hora:|s,dia:|s,semana:|s,mês|mêses|mes|meses,ano:|s;um,dois,três|tres,quatro,cinco,seis,sete,oito,nove,dez,uma,duas;;a,de;{num} {unit} {sign},{sign} {num} {unit},{date?} {2} {month} {2} {year?},{1} {unit=5-7} {shift},{1} {shift} {unit=5-7};{d} de {month} de {yyyy};anteontem,ontem,hoje,amanh:ã|a;,atrás|atras|há|ha,,daqui a;,passad:o|a,,próximo|próxima|proximo|proxima',
4242
+ 'pt': '6;;janeiro,fevereiro,março,abril,maio,junho,julho,agosto,setembro,outubro,novembro,dezembro;domingo,segunda-feira,terça-feira,quarta-feira,quinta-feira,sexta-feira,sábado|sabado;milisegundo:|s,segundo:|s,minuto:|s,hora:|s,dia:|s,semana:|s,mês|mêses|mes|meses,ano:|s;um,dois,três|tres,quatro,cinco,seis,sete,oito,nove,dez,uma,duas;;a,de;{num} {unit} {sign},{sign} {num} {unit},{date?} {1} {month} {1} {year?},{0} {unit=5-7} {shift},{0} {shift} {unit=5-7};{d} de {month} de {yyyy};anteontem,ontem,hoje,amanh:ã|a;,atrás|atras|há|ha,,daqui a;,passad:o|a,,próximo|próxima|proximo|proxima',
4120
4243
 
4121
- 'fr': '2;;janvier,février|fevrier,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre|decembre;dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi;milliseconde:|s,seconde:|s,minute:|s,heure:|s,jour:|s,semaine:|s,mois,an:|s|née|nee;un:|e,deux,trois,quatre,cinq,six,sept,huit,neuf,dix;;l\'|la|le;{sign} {num} {unit},{sign} {num} {unit},{1} {date?} {month} {year?},{1} {unit=5-7} {shift};{d} {month} {yyyy};,hier,aujourd\'hui,demain;,il y a,,dans|d\'ici;,derni:er|ère|ere,,prochain:|e',
4244
+ 'fr': '2;;janvier,février|fevrier,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre|decembre;dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi;milliseconde:|s,seconde:|s,minute:|s,heure:|s,jour:|s,semaine:|s,mois,an:|s|née|nee;un:|e,deux,trois,quatre,cinq,six,sept,huit,neuf,dix;;l\'|la|le;{sign} {num} {unit},{sign} {num} {unit},{0} {date?} {month} {year?},{0} {unit=5-7} {shift};{d} {month} {yyyy};,hier,aujourd\'hui,demain;,il y a,,dans|d\'ici;,derni:er|ère|ere,,prochain:|e',
4122
4245
 
4123
- 'it': '2;;Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre;Domenica,Luned:ì|i,Marted:ì|i,Mercoled:ì|i,Gioved:ì|i,Venerd:ì|i,Sabato;millisecond:o|i,second:o|i,minut:o|i,or:a|e,giorn:o|i,settiman:a|e,mes:e|i,ann:o|i;un:|\'|a|o,due,tre,quattro,cinque,sei,sette,otto,nove,dieci;;l\'|la|il;{num} {unit} {sign},{weekday?} {date?} {month} {year?},{1} {unit=5-7} {shift},{1} {shift} {unit=5-7};{d} {month} {yyyy};,ieri,oggi,domani,dopodomani;,fa,,da adesso;,scors:o|a,,prossim:o|a',
4246
+ 'it': '2;;Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre;Domenica,Luned:ì|i,Marted:ì|i,Mercoled:ì|i,Gioved:ì|i,Venerd:ì|i,Sabato;millisecond:o|i,second:o|i,minut:o|i,or:a|e,giorn:o|i,settiman:a|e,mes:e|i,ann:o|i;un:|\'|a|o,due,tre,quattro,cinque,sei,sette,otto,nove,dieci;;l\'|la|il;{num} {unit} {sign},{weekday?} {date?} {month} {year?},{0} {unit=5-7} {shift},{0} {shift} {unit=5-7};{d} {month} {yyyy};,ieri,oggi,domani,dopodomani;,fa,,da adesso;,scors:o|a,,prossim:o|a',
4124
4247
 
4125
4248
  'de': '2;;Januar,Februar,März|Marz,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember;Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag;Millisekunde:|n,Sekunde:|n,Minute:|n,Stunde:|n,Tag:|en,Woche:|n,Monat:|en,Jahr:|en;ein:|e|er|em|en,zwei,drei,vier,fuenf,sechs,sieben,acht,neun,zehn;;der;{sign} {num} {unit},{num} {unit} {sign},{num} {unit} {sign},{sign} {num} {unit},{weekday?} {date?} {month} {year?},{shift} {unit=5-7};{d}. {Month} {yyyy};vorgestern,gestern,heute,morgen,übermorgen|ubermorgen|uebermorgen;,vor:|her,,in;,letzte:|r|n|s,,nächste:|r|n|s+naechste:|r|n|s',
4126
4249
 
4127
- 'zh-TW': '1;月;;日,一,二,三,四,五,六;毫秒,秒鐘,分鐘,小時,天,個星期|週,個月,年;;;日|號;{num}{unit}{sign},星期{weekday},{shift}{unit=5-7},{shift}{unit=5}{weekday},{year}年{month?}月?{date?}{1},{month}月{date?}{1},{date}{1};{yyyy}年{M}月{d}日;前天,昨天,今天,明天,後天;,前,,後;,上|去,這,下|明',
4250
+ 'zh-TW': '1;月;;日,一,二,三,四,五,六;毫秒,秒鐘,分鐘,小時,天,個星期|週,個月,年;;;日|號;{num}{unit}{sign},星期{weekday},{shift}{unit=5-7},{shift}{unit=5}{weekday},{year}年{month?}月?{date?}{0},{month}月{date?}{0},{date}{0};{yyyy}年{M}月{d}日;前天,昨天,今天,明天,後天;,前,,後;,上|去,這,下|明',
4128
4251
 
4129
- 'zh-CN': '1;月;;日,一,二,三,四,五,六;毫秒,秒钟,分钟,小时,天,个星期|周,个月,年;;;日|号;{num}{unit}{sign},星期{weekday},{shift}{unit=5-7},{shift}{unit=5}{weekday},{year}年{month?}月?{date?}{1},{month}月{date?}{1},{date}{1};{yyyy}年{M}月{d}日;前天,昨天,今天,明天,后天;,前,,后;,上|去,这,下|明'
4252
+ 'zh-CN': '1;月;;日,一,二,三,四,五,六;毫秒,秒钟,分钟,小时,天,个星期|周,个月,年;;;日|号;{num}{unit}{sign},星期{weekday},{shift}{unit=5-7},{shift}{unit=5}{weekday},{year}年{month?}月?{date?}{0},{month}月{date?}{0},{date}{0};{yyyy}年{M}月{d}日;前天,昨天,今天,明天,后天;,前,,后;,上|去,这,下|明'
4130
4253
 
4131
4254
  }
4132
4255
 
4133
4256
  function checkLocaleFormatsAdded(loc) {
4134
- var addFormat = date.addFormat, code = loc['code'];
4257
+ var code = loc['code'];
4135
4258
  if(loc.formatsAdded) return;
4136
- addFormat('(' + loc['months'].compact().join('|') + ')', ['month'], code);
4137
- addFormat('(' + loc['weekdays'].compact().join('|') + ')', ['weekday'], code);
4138
- addFormat('(' + loc['modifiers'].filter(function(m){ return m.name === 'day'; }).map('text').join('|') + ')', ['day'], code);
4259
+ addDateInputFormat('(' + loc['months'].compact().join('|') + ')', ['month'], code);
4260
+ addDateInputFormat('(' + loc['weekdays'].compact().join('|') + ')', ['weekday'], code);
4261
+ addDateInputFormat('(' + loc['modifiers'].filter(function(m){ return m.name === 'day'; }).map('src').join('|') + ')', ['day'], code);
4139
4262
  loc['formats'].each(function(src) {
4140
- var to = [];
4141
- src = src.replace(/\s+/g, '[-,. ]*');
4142
- src = src.replace(/\{(.+?)\}/g, function(all, k) {
4143
- var opt = k.match(/\?$/), slice = k.match(/(\d)(?:-(\d))?/), nc = k.match(/^\d+$/), key = k.replace(/[^a-z]+$/, ''), value, arr;
4144
- if(key === 'time') {
4145
- to = to.concat(TimeFormat);
4146
- return opt ? OptionalTime : RequiredTime;
4147
- }
4148
- if(nc) {
4149
- value = loc['optionals'][nc[0] - 1];
4150
- } else if(loc[key]) {
4151
- value = loc[key];
4152
- } else if(loc[key + 's']) {
4153
- value = loc[key + 's'];
4154
- if(slice) {
4155
- // Can't use filter here as Prototype hijacks the method and doesn't
4156
- // pass an index, so use a simple loop instead!
4157
- arr = [];
4158
- value.each(function(m,i) {
4159
- var mod = i % (loc['units'] ? 8 : value.length);
4160
- if(mod >= slice[1] && mod <= (slice[2] || slice[1])) {
4161
- arr.push(m);
4162
- }
4163
- });
4164
- value = arr;
4165
- }
4166
- value = value.compact().join('|');
4167
- }
4168
- if(nc) {
4169
- return '(?:' + value + ')?';
4170
- } else {
4171
- to.push(key);
4172
- return '(' + value + ')' + (opt ? '?' : '');
4173
- }
4174
- });
4175
- addFormat(src, to, code);
4263
+ loc.addFormat(src, code, false);
4176
4264
  });
4177
4265
  loc.formatsAdded = true;
4178
4266
  }
4179
4267
 
4268
+ function addDateInputFormat(format, match, locale, variant, method) {
4269
+ method = method || 'push';
4270
+ DateInputFormats[method]({
4271
+ variant: variant,
4272
+ locale: locale,
4273
+ reg: regexp('^' + format + '$', 'i'),
4274
+ to: match
4275
+ });
4276
+ }
4277
+
4180
4278
  function getLocalization(code, fallback, set) {
4181
4279
  if(fallback && (!object.isString(code) || !code)) code = Date['currentLocale'];
4182
- if(code && !Localizations[code]) initializeLocalization(code, set);
4280
+ if(code && !Localizations[code] || set) initializeLocalization(code, set);
4183
4281
  return Localizations[code];
4184
4282
  }
4185
4283
 
@@ -4203,7 +4301,7 @@
4203
4301
  function setArray(name, abbreviate, multiple) {
4204
4302
  var arr = [];
4205
4303
  if(!set[name]) return;
4206
- set[name].each(function(el, i) {
4304
+ set[name].forEach(function(el, i) {
4207
4305
  eachAlternate(el, function(str, j) {
4208
4306
  arr[j * multiple + i] = str.toLowerCase();
4209
4307
  });
@@ -4230,13 +4328,13 @@
4230
4328
  var arr = [];
4231
4329
  set.modifiersByName = {};
4232
4330
  set['modifiers'].each(function(modifier) {
4233
- eachAlternate(modifier.text, function(t) {
4331
+ eachAlternate(modifier.src, function(t) {
4234
4332
  set.modifiersByName[t] = modifier;
4235
- arr.push({ name: modifier.name, text: t, value: modifier.value });
4333
+ arr.push({ name: modifier.name, src: t, value: modifier.value });
4236
4334
  });
4237
4335
  });
4238
4336
  arr.groupBy('name', function(name, group) {
4239
- group = group.map('text');
4337
+ group = group.map('src');
4240
4338
  if(name === 'day') group = group.concat(set['weekdays']);
4241
4339
  set[name] = group.join('|');
4242
4340
  });
@@ -4276,7 +4374,7 @@
4276
4374
  ['day','sign','shift','edge'].each(function(name, i) {
4277
4375
  if(!pre[i + 10]) return;
4278
4376
  pre[i + 10].split(',').each(function(t, j) {
4279
- if(t) set['modifiers'].push({ name: name, text: t, value: j - 2 });
4377
+ if(t) set['modifiers'].push({ name: name, src: t, value: j - 2 });
4280
4378
  });
4281
4379
  });
4282
4380
  if(bool(1)) {
@@ -4292,6 +4390,7 @@
4292
4390
  set['hasPlural'] = bool(2);
4293
4391
  set['pastRelativeFormat'] = set['formats'][0];
4294
4392
  set['futureRelativeFormat'] = set['formats'][bool(3) ? 1 : 0];
4393
+ set['durationFormat'] = set['formats'][0].replace(/\s*\{sign\}\s*/, '');
4295
4394
  return set;
4296
4395
  }
4297
4396
 
@@ -4340,9 +4439,16 @@
4340
4439
  return English['units'][this['units'].indexOf(n) % 8];
4341
4440
  },
4342
4441
 
4343
- relative: function(num, u, ms) {
4344
- var format, sign, unit, last, mult;
4345
- format = ms > 0 ? this['futureRelativeFormat'] : this['pastRelativeFormat'];
4442
+ relative: function(adu) {
4443
+ return this.convertAdjustedToFormat(adu, adu[2] > 0 ? 'futureRelativeFormat' : 'pastRelativeFormat');
4444
+ },
4445
+
4446
+ duration: function(ms) {
4447
+ return this.convertAdjustedToFormat(getAdjustedUnit(ms), 'durationFormat');
4448
+ },
4449
+
4450
+ convertAdjustedToFormat: function(adu, format) {
4451
+ var num = adu[0], u = adu[1], ms = adu[2], sign, unit, last, mult;
4346
4452
  if(this['code'] == 'ru') {
4347
4453
  last = num.toString().from(-1);
4348
4454
  switch(true) {
@@ -4356,7 +4462,47 @@
4356
4462
  unit = this['units'][mult * 8 + u] || this['units'][u];
4357
4463
  if(this['capitalizeUnit']) unit = unit.capitalize();
4358
4464
  sign = this['modifiers'].find(function(m) { return m.name == 'sign' && m.value == (ms > 0 ? 1 : -1); });
4359
- return format.assign({ 'num': num, 'unit': unit, 'sign': sign.text });
4465
+ return this[format].assign({ 'num': num, 'unit': unit, 'sign': sign.src });
4466
+ },
4467
+
4468
+ addFormat: function(src, code, add) {
4469
+ var to = [], loc = this;
4470
+ if(add !== false) loc.formats.push(src);
4471
+ src = src.replace(/\s+/g, '[-,. ]*');
4472
+ src = src.replace(/\{(.+?)\}/g, function(all, k) {
4473
+ var opt = k.match(/\?$/), slice = k.match(/(\d)(?:-(\d))?/), nc = k.match(/^\d+$/), key = k.replace(/[^a-z]+$/, ''), value, arr;
4474
+ if(key === 'time') {
4475
+ to = to.concat(TimeFormat);
4476
+ return opt ? OptionalTime : RequiredTime;
4477
+ }
4478
+ if(nc) {
4479
+ value = loc['optionals'][nc[0]];
4480
+ } else if(loc[key]) {
4481
+ value = loc[key];
4482
+ } else if(loc[key + 's']) {
4483
+ value = loc[key + 's'];
4484
+ if(slice) {
4485
+ // Can't use filter here as Prototype hijacks the method and doesn't
4486
+ // pass an index, so use a simple loop instead!
4487
+ arr = [];
4488
+ value.forEach(function(m, i) {
4489
+ var mod = i % (loc['units'] ? 8 : value.length);
4490
+ if(mod >= slice[1] && mod <= (slice[2] || slice[1])) {
4491
+ arr.push(m);
4492
+ }
4493
+ });
4494
+ value = arr;
4495
+ }
4496
+ value = value.compact().join('|');
4497
+ }
4498
+ if(nc) {
4499
+ return '(?:' + value + ')?';
4500
+ } else {
4501
+ to.push(key);
4502
+ return '(' + value + ')' + (opt ? '?' : '');
4503
+ }
4504
+ });
4505
+ addDateInputFormat(src, to, code);
4360
4506
  }
4361
4507
 
4362
4508
  });
@@ -4390,8 +4536,7 @@
4390
4536
  if(isUndefined(value) || value === '') return;
4391
4537
  value = convertAsianDigits(value.hankaku('n'), key);
4392
4538
  if(key === 'year') obj.yearAsString = value;
4393
- if(key === 'millisecond') value = value * Math.pow(10, 3 - value.length);
4394
- num = parseFloat(value);
4539
+ num = parseFloat(value.replace(/,/, '.'));
4395
4540
  obj[key] = !isNaN(num) ? num : value.toLowerCase();
4396
4541
  });
4397
4542
  return obj;
@@ -4473,7 +4618,7 @@
4473
4618
  }
4474
4619
 
4475
4620
  // Adjust for timezone offset
4476
- if(set['offset_hours'] || set['offset_minutes']) {
4621
+ if('offset_hours' in set || 'offset_minutes' in set) {
4477
4622
  set['utc'] = true;
4478
4623
  set['offset_minutes'] = set['offset_minutes'] || 0;
4479
4624
  set['offset_minutes'] += set['offset_hours'] * 60;
@@ -4521,9 +4666,18 @@
4521
4666
  // Finally shift the unit.
4522
4667
  set[unit] = (set[unit] || 0) + num;
4523
4668
  }
4669
+
4524
4670
  if(set['year_sign'] === '-') {
4525
4671
  set['year'] *= -1;
4526
4672
  }
4673
+
4674
+ DateUnitsReversed.slice(1,4).each(function(u, i) {
4675
+ var value = set[u.unit], fraction = value % 1;
4676
+ if(fraction) {
4677
+ set[DateUnitsReversed[i].unit] = (fraction * (u.unit === 'second' ? 1000 : 60)).round();
4678
+ set[u.unit] = value | 0;
4679
+ }
4680
+ });
4527
4681
  return false;
4528
4682
  }
4529
4683
  });
@@ -4534,6 +4688,9 @@
4534
4688
  } else if(relative) {
4535
4689
  d.advance(set);
4536
4690
  } else if(set['utc']) {
4691
+ // UTC times can traverse into other days or even months,
4692
+ // so preemtively reset the time here to prevent this.
4693
+ d.resetTime();
4537
4694
  d.setUTC(set, true);
4538
4695
  } else {
4539
4696
  d.set(set, true);
@@ -4569,20 +4726,20 @@
4569
4726
  } else if(Date[f]) {
4570
4727
  f = Date[f];
4571
4728
  } else if(object.isFunction(f)) {
4572
- adu = getAdjustedDateUnit(date);
4729
+ adu = getAdjustedUnit(date.millisecondsFromNow());
4573
4730
  f = f.apply(date, adu.concat(loc));
4574
4731
  }
4575
4732
  if(!f && !relative) {
4576
4733
  f = loc['outputFormat'];
4577
4734
  } else if(!f && relative) {
4578
- adu = adu || getAdjustedDateUnit(date);
4735
+ adu = adu || getAdjustedUnit(date.millisecondsFromNow());
4579
4736
  // Adjust up if time is in ms, as this doesn't
4580
4737
  // look very good for a standard relative date.
4581
4738
  if(adu[1] === 0) {
4582
4739
  adu[1] = 1;
4583
4740
  adu[0] = 1;
4584
4741
  }
4585
- return loc.relative(adu[0],adu[1],adu[2]);
4742
+ return loc.relative(adu);
4586
4743
  }
4587
4744
  DateOutputFormats.each(function(dof) {
4588
4745
  f = f.replace(regexp('\\{('+dof.token+')(\\d)?\\}', dof.word ? 'i' : ''), function(m,t,d) {
@@ -4626,7 +4783,7 @@
4626
4783
  var min = p.date.getTime();
4627
4784
  var max = min + accuracy;
4628
4785
  if(p.set && p.set.specificity == 'week' && new Date(max + 1).getHours() != 0) {
4629
- max -= date['DSTOffset'];
4786
+ max += date['DSTOffset'];
4630
4787
  }
4631
4788
  return t >= (min - loBuffer) && t <= (max + hiBuffer);
4632
4789
  }
@@ -4710,10 +4867,10 @@
4710
4867
  return 1 + (date.daysSince(date.clone().beginningOfYear()) / 7 | 0);
4711
4868
  }
4712
4869
 
4713
- function getAdjustedDateUnit(d) {
4714
- var next, ms = d.millisecondsFromNow(), ams = ms.abs(), value = ams, unit = 0;
4870
+ function getAdjustedUnit(ms) {
4871
+ var next, ams = ms.abs(), value = ams, unit = 0;
4715
4872
  DateUnitsReversed.from(1).each(function(u, i) {
4716
- next = (ams / u.multiplier()).round(1) | 0;
4873
+ next = (ams / u.multiplier() * 10).round() / 10 | 0;
4717
4874
  if(next >= 1) {
4718
4875
  value = next;
4719
4876
  unit = i + 1;
@@ -5087,9 +5244,9 @@
5087
5244
  DateUnitsReversed = DateUnits.clone().reverse();
5088
5245
  var monthReg = '\\d{1,2}|' + English['months'].join('|');
5089
5246
  StaticInputFormats.each(function(f) {
5090
- date.addFormat(f.src.replace(/\{month\}/, monthReg) + (f.time === false ? '' : OptionalTime), f.to.concat(TimeFormat), 'en', f.variant);
5247
+ addDateInputFormat(f.src.replace(/\{month\}/, monthReg) + (f.time === false ? '' : OptionalTime), f.to.concat(TimeFormat), 'en', f.variant);
5091
5248
  });
5092
- date.addFormat(RequiredTime, TimeFormat);
5249
+ addDateInputFormat(RequiredTime, TimeFormat);
5093
5250
  }
5094
5251
 
5095
5252
  /***
@@ -5493,11 +5650,41 @@
5493
5650
  }
5494
5651
 
5495
5652
 
5653
+ /***
5654
+ * @method toISOString()
5655
+ * @returns String
5656
+ * @short Formats the string to ISO8601 format.
5657
+ * @extra This will always format as UTC time. Provided for browsers that do not support this method.
5658
+ * @example
5659
+ *
5660
+ * Date.create().toISOString() -> ex. 2011-07-05 12:24:55.528Z
5661
+ *
5662
+ ***
5663
+ * @method toJSON()
5664
+ * @returns String
5665
+ * @short Returns a JSON representation of the date.
5666
+ * @extra This is effectively an alias for %toISOString%. Will always return the date in UTC time. Implemented for browsers that do not support it.
5667
+ * @example
5668
+ *
5669
+ * Date.create().toJSON() -> ex. 2011-07-05 12:24:55.528Z
5670
+ *
5671
+ ***/
5672
+
5673
+ function buildISOString(name) {
5674
+ var d = new date(date.UTC(1999, 11, 31)), target = '1999-12-31T00:00:00.000Z', methods = {};
5675
+ if(!d[name] || d[name]() !== target) {
5676
+ methods[name] = function() { return formatDate(this.toUTC(), date['ISO8601_DATETIME']); }
5677
+ date.extend(methods, true);
5678
+ }
5679
+ }
5680
+
5496
5681
  function buildDate() {
5497
5682
  English = date.setLocale('en');
5498
5683
  buildDateMethods();
5499
5684
  buildDateInputFormats();
5500
5685
  buildRelativeAliases();
5686
+ buildISOString('toISOString');
5687
+ buildISOString('toJSON');
5501
5688
  setDateProperties();
5502
5689
  }
5503
5690
 
@@ -5574,32 +5761,13 @@
5574
5761
  *
5575
5762
  ***/
5576
5763
  'addFormat': function(format, match, locale, variant) {
5577
- DateInputFormats.push({
5578
- variant: variant,
5579
- locale: locale,
5580
- reg: regexp('^' + format + '$', 'i'),
5581
- to: match
5582
- });
5764
+ addDateInputFormat(format, match, locale, variant, 'unshift');
5583
5765
  }
5584
5766
 
5585
5767
  }, false, false);
5586
5768
 
5587
5769
  date.extend({
5588
5770
 
5589
- /***
5590
- * @method toISOString()
5591
- * @returns String
5592
- * @short Formats the string to ISO8601 format.
5593
- * @extra This will always format as UTC time. Provided for browsers that do not support this method.
5594
- * @example
5595
- *
5596
- * Date.create().toISOString() -> ex. 2011-07-05 12:24:55.528Z
5597
- *
5598
- ***/
5599
- 'toISOString': function() {
5600
- return formatDate(this.toUTC(), date['ISO8601_DATETIME']);
5601
- },
5602
-
5603
5771
  /***
5604
5772
  * @method set(<set>, [reset] = false)
5605
5773
  * @returns Date
@@ -5887,7 +6055,7 @@
5887
6055
  * Date.create().format('{Weekday} {d} {Month}, {yyyy}') -> ex. Monday July 4, 2003
5888
6056
  * Date.create().format('{hh}:{mm}') -> ex. 15:57
5889
6057
  * Date.create().format('{12hr}:{mm}{tt}') -> ex. 3:57pm
5890
- * Date.create().format(Date.ISO8601) -> ex. 2011-07-05 12:24:55.528Z
6058
+ * Date.create().format(Date.ISO8601_DATETIME) -> ex. 2011-07-05 12:24:55.528Z
5891
6059
  * Date.create('last week').format('', 'ja') -> ex. 先週
5892
6060
  * Date.create('yesterday').format(function(value,unit,ms,loc) {
5893
6061
  * // value = 1, unit = 3, ms = -86400000, loc = [current locale object]
@@ -5977,20 +6145,6 @@
5977
6145
  ***/
5978
6146
  'clone': function() {
5979
6147
  return new date(this.getTime());
5980
- },
5981
-
5982
- /***
5983
- * @method compare(<obj>)
5984
- * @returns Number
5985
- * @short Performs a numeric comparison against the date.
5986
- * @extra This method is also defined on %String% and %Number%, and is useful when performing complex sort operations where the type isn't known.
5987
- * @example
5988
- *
5989
- * Date.create('1 day ago').compare('today') -> -864000;
5990
- *
5991
- ***/
5992
- 'compare': function(obj) {
5993
- return this - createDate(arguments);
5994
6148
  }
5995
6149
 
5996
6150
  });
@@ -5999,24 +6153,14 @@
5999
6153
  // Instance aliases
6000
6154
  date.extend({
6001
6155
 
6002
- /***
6003
- * @method toJSON()
6004
- * @returns String
6005
- * @short Returns a JSON representation of the date.
6006
- * @extra This is effectively an alias for %toISOString%. Will always return the date in UTC time. Implemented for browsers that do not support it.
6007
- * @example
6008
- *
6009
- * Date.create().toJSON() -> ex. 2011-07-05 12:24:55.528Z
6010
- *
6011
- ***/
6012
- 'toJSON': date.prototype.toISOString,
6013
-
6014
6156
  /***
6015
6157
  * @method iso()
6016
6158
  * @alias toISOString
6017
6159
  *
6018
6160
  ***/
6019
- 'iso': date.prototype.toISOString,
6161
+ 'iso': function() {
6162
+ return this.toISOString();
6163
+ },
6020
6164
 
6021
6165
  /***
6022
6166
  * @method getWeekday()
@@ -6035,6 +6179,553 @@
6035
6179
  });
6036
6180
 
6037
6181
 
6182
+
6183
+ /***
6184
+ * Number module
6185
+ *
6186
+ ***/
6187
+
6188
+ number.extend({
6189
+
6190
+ /***
6191
+ * @method duration([locale] = currentLocale)
6192
+ * @returns String
6193
+ * @short Takes the number as milliseconds and returns a unit-adjusted localized string.
6194
+ * @extra This method is the same as %Date#relative% without the localized equivalent of "from now" or "ago". [locale] can be passed as the first (and only) parameter. Note that this method is only available when the dates package is included.
6195
+ * @example
6196
+ *
6197
+ * (500).duration() -> '500 milliseconds'
6198
+ * (1200).duration() -> '1 second'
6199
+ * (75).minutes().duration() -> '1 hour'
6200
+ * (75).minutes().duration('es') -> '1 hora'
6201
+ *
6202
+ ***/
6203
+ 'duration': function(code) {
6204
+ return Date.getLocale(code).duration(this);
6205
+ }
6206
+
6207
+ });
6208
+
6038
6209
  buildDate();
6039
6210
 
6040
6211
  })();
6212
+ (function(context) {
6213
+
6214
+ /***
6215
+ * String module
6216
+ *
6217
+ ***/
6218
+
6219
+
6220
+ var globalContext,
6221
+ plurals = [],
6222
+ singulars = [],
6223
+ uncountables = [],
6224
+ humans = [],
6225
+ acronyms = {},
6226
+ Downcased,
6227
+ Normalize,
6228
+ Inflector;
6229
+
6230
+ globalContext = typeof global !== 'undefined' ? global : context;
6231
+
6232
+ function removeFromUncountablesAndAddTo(arr, rule, replacement) {
6233
+ if(Object.isString(rule)) {
6234
+ uncountables.remove(rule);
6235
+ }
6236
+ uncountables.remove(replacement)
6237
+ arr.unshift({ rule: rule, replacement: replacement })
6238
+ }
6239
+
6240
+ function paramMatchesType(param, type) {
6241
+ return param == type || param == 'all' || !param;
6242
+ }
6243
+
6244
+ function isUncountable(word) {
6245
+ return uncountables.any(function(uncountable) {
6246
+ return new RegExp('\\b' + uncountable + '$', 'i').test(word);
6247
+ });
6248
+ }
6249
+
6250
+ function inflect(word, pluralize) {
6251
+ word = Object.isString(word) ? word.toString() : '';
6252
+ if(word.isBlank() || isUncountable(word)) {
6253
+ return word;
6254
+ } else {
6255
+ return runReplacements(word, pluralize ? plurals : singulars);
6256
+ }
6257
+ }
6258
+
6259
+ function runReplacements(word, table) {
6260
+ table.each(function(inflection) {
6261
+ if(word.match(inflection.rule)) {
6262
+ word = word.replace(inflection.rule, inflection.replacement);
6263
+ return false;
6264
+ }
6265
+ });
6266
+ return word;
6267
+ }
6268
+
6269
+ function capitalize(word) {
6270
+ return word.replace(/^\W*[a-z]/, function(w){
6271
+ return w.toUpperCase();
6272
+ });
6273
+ }
6274
+
6275
+ Inflector = {
6276
+
6277
+ /*
6278
+ * Specifies a new acronym. An acronym must be specified as it will appear in a camelized string. An underscore
6279
+ * string that contains the acronym will retain the acronym when passed to %camelize%, %humanize%, or %titleize%.
6280
+ * A camelized string that contains the acronym will maintain the acronym when titleized or humanized, and will
6281
+ * convert the acronym into a non-delimited single lowercase word when passed to String#underscore.
6282
+ *
6283
+ * Examples:
6284
+ * String.Inflector.acronym('HTML')
6285
+ * 'html'.titleize() -> 'HTML'
6286
+ * 'html'.camelize() -> 'HTML'
6287
+ * 'MyHTML'.underscore() -> 'my_html'
6288
+ *
6289
+ * The acronym, however, must occur as a delimited unit and not be part of another word for conversions to recognize it:
6290
+ *
6291
+ * String.Inflector.acronym('HTTP')
6292
+ * 'my_http_delimited'.camelize() -> 'MyHTTPDelimited'
6293
+ * 'https'.camelize() -> 'Https', not 'HTTPs'
6294
+ * 'HTTPS'.underscore() -> 'http_s', not 'https'
6295
+ *
6296
+ * String.Inflector.acronym('HTTPS')
6297
+ * 'https'.camelize() -> 'HTTPS'
6298
+ * 'HTTPS'.underscore() -> 'https'
6299
+ *
6300
+ * Note: Acronyms that are passed to %pluralize% will no longer be recognized, since the acronym will not occur as
6301
+ * a delimited unit in the pluralized result. To work around this, you must specify the pluralized form as an
6302
+ * acronym as well:
6303
+ *
6304
+ * String.Inflector.acronym('API')
6305
+ * 'api'.pluralize().camelize() -> 'Apis'
6306
+ *
6307
+ * String.Inflector.acronym('APIs')
6308
+ * 'api'.pluralize().camelize() -> 'APIs'
6309
+ *
6310
+ * %acronym% may be used to specify any word that contains an acronym or otherwise needs to maintain a non-standard
6311
+ * capitalization. The only restriction is that the word must begin with a capital letter.
6312
+ *
6313
+ * Examples:
6314
+ * String.Inflector.acronym('RESTful')
6315
+ * 'RESTful'.underscore() -> 'restful'
6316
+ * 'RESTfulController'.underscore() -> 'restful_controller'
6317
+ * 'RESTfulController'.titleize() -> 'RESTful Controller'
6318
+ * 'restful'.camelize() -> 'RESTful'
6319
+ * 'restful_controller'.camelize() -> 'RESTfulController'
6320
+ *
6321
+ * String.Inflector.acronym('McDonald')
6322
+ * 'McDonald'.underscore() -> 'mcdonald'
6323
+ * 'mcdonald'.camelize() -> 'McDonald'
6324
+ */
6325
+ 'acronym': function(word) {
6326
+ acronyms[word.toLowerCase()] = word;
6327
+ Inflector.acronymRegExp = new RegExp(Object.values(acronyms).join('|'), 'g');
6328
+ },
6329
+
6330
+ /*
6331
+ * Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
6332
+ * The replacement should always be a string that may include references to the matched data from the rule.
6333
+ */
6334
+ 'plural': function(rule, replacement) {
6335
+ removeFromUncountablesAndAddTo(plurals, rule, replacement);
6336
+ },
6337
+
6338
+ /*
6339
+ * Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
6340
+ * The replacement should always be a string that may include references to the matched data from the rule.
6341
+ */
6342
+ 'singular': function(rule, replacement) {
6343
+ removeFromUncountablesAndAddTo(singulars, rule, replacement);
6344
+ },
6345
+
6346
+ /*
6347
+ * Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used
6348
+ * for strings, not regular expressions. You simply pass the irregular in singular and plural form.
6349
+ *
6350
+ * Examples:
6351
+ * String.Inflector.irregular('octopus', 'octopi')
6352
+ * String.Inflector.irregular('person', 'people')
6353
+ */
6354
+ 'irregular': function(singular, plural) {
6355
+ var singularFirst = singular.first(),
6356
+ singularRest = singular.from(1),
6357
+ pluralFirst = plural.first(),
6358
+ pluralRest = plural.from(1),
6359
+ pluralFirstUpper = pluralFirst.toUpperCase(),
6360
+ pluralFirstLower = pluralFirst.toLowerCase(),
6361
+ singularFirstUpper = singularFirst.toUpperCase(),
6362
+ singularFirstLower = singularFirst.toLowerCase();
6363
+ uncountables.remove(singular)
6364
+ uncountables.remove(plural)
6365
+ if(singularFirstUpper == pluralFirstUpper) {
6366
+ Inflector.plural(new RegExp('({1}){2}$'.assign(singularFirst, singularRest), 'i'), '$1' + pluralRest);
6367
+ Inflector.plural(new RegExp('({1}){2}$'.assign(pluralFirst, pluralRest), 'i'), '$1' + pluralRest);
6368
+ Inflector.singular(new RegExp('({1}){2}$'.assign(pluralFirst, pluralRest), 'i'), '$1' + singularRest);
6369
+ } else {
6370
+ Inflector.plural(new RegExp('{1}{2}$'.assign(singularFirstUpper, singularRest)), pluralFirstUpper + pluralRest);
6371
+ Inflector.plural(new RegExp('{1}{2}$'.assign(singularFirstLower, singularRest)), pluralFirstLower + pluralRest);
6372
+ Inflector.plural(new RegExp('{1}{2}$'.assign(pluralFirstUpper, pluralRest)), pluralFirstUpper + pluralRest);
6373
+ Inflector.plural(new RegExp('{1}{2}$'.assign(pluralFirstLower, pluralRest)), pluralFirstLower + pluralRest);
6374
+ Inflector.singular(new RegExp('{1}{2}$'.assign(pluralFirstUpper, pluralRest)), singularFirstUpper + singularRest);
6375
+ Inflector.singular(new RegExp('{1}{2}$'.assign(pluralFirstLower, pluralRest)), singularFirstLower + singularRest);
6376
+ }
6377
+ },
6378
+
6379
+ /*
6380
+ * Add uncountable words that shouldn't be attempted inflected.
6381
+ *
6382
+ * Examples:
6383
+ * String.Inflector.uncountable('money')
6384
+ * String.Inflector.uncountable('money', 'information')
6385
+ * String.Inflector.uncountable(['money', 'information', 'rice'])
6386
+ */
6387
+ 'uncountable': function() {
6388
+ uncountables.add(Array.create(arguments).flatten());
6389
+ },
6390
+
6391
+ /*
6392
+ * Specifies a humanized form of a string by a regular expression rule or by a string mapping.
6393
+ * When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
6394
+ * When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name')
6395
+ *
6396
+ * Examples:
6397
+ * String.Inflector.human(/_cnt$/i, '_count')
6398
+ * String.Inflector.human('legacy_col_person_name', 'Name')
6399
+ */
6400
+ 'human': function(rule, replacement) {
6401
+ humans.unshift({ rule: rule, replacement: replacement })
6402
+ },
6403
+
6404
+
6405
+ /*
6406
+ * Clears the loaded inflections within a given scope (default is 'all').
6407
+ * Options are: 'all', 'plurals', 'singulars', 'uncountables', 'humans'.
6408
+ *
6409
+ * Examples:
6410
+ * String.Inflector.clear('all')
6411
+ * String.Inflector.clear('plurals')
6412
+ */
6413
+ 'clear': function(type) {
6414
+ if(paramMatchesType(type, 'singulars')) singulars = [];
6415
+ if(paramMatchesType(type, 'plurals')) plurals = [];
6416
+ if(paramMatchesType(type, 'uncountables')) uncountables = [];
6417
+ if(paramMatchesType(type, 'humans')) humans = [];
6418
+ if(paramMatchesType(type, 'acronyms')) acronyms = {};
6419
+ }
6420
+
6421
+ };
6422
+
6423
+ Downcased = [
6424
+ 'and', 'or', 'nor', 'a', 'an', 'the', 'so', 'but', 'to', 'of', 'at',
6425
+ 'by', 'from', 'into', 'on', 'onto', 'off', 'out', 'in', 'over',
6426
+ 'with', 'for'
6427
+ ];
6428
+
6429
+ Normalize = {
6430
+ 'A': /[AⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ]/g,
6431
+ 'B': /[BⒷBḂḄḆɃƂƁ]/g,
6432
+ 'C': /[CⒸCĆĈĊČÇḈƇȻꜾ]/g,
6433
+ 'D': /[DⒹDḊĎḌḐḒḎĐƋƊƉꝹ]/g,
6434
+ 'E': /[EⒺEÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚƐƎ]/g,
6435
+ 'F': /[FⒻFḞƑꝻ]/g,
6436
+ 'G': /[GⒼGǴĜḠĞĠǦĢǤƓꞠꝽꝾ]/g,
6437
+ 'H': /[HⒽHĤḢḦȞḤḨḪĦⱧⱵꞍ]/g,
6438
+ 'I': /[IⒾIÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗ]/g,
6439
+ 'J': /[JⒿJĴɈ]/g,
6440
+ 'K': /[KⓀKḰǨḲĶḴƘⱩꝀꝂꝄꞢ]/g,
6441
+ 'L': /[LⓁLĿĹĽḶḸĻḼḺŁȽⱢⱠꝈꝆꞀ]/g,
6442
+ 'M': /[MⓂMḾṀṂⱮƜ]/g,
6443
+ 'N': /[NⓃNǸŃÑṄŇṆŅṊṈȠƝꞐꞤ]/g,
6444
+ 'O': /[OⓄOÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘǪǬØǾƆƟꝊꝌ]/g,
6445
+ 'P': /[PⓅPṔṖƤⱣꝐꝒꝔ]/g,
6446
+ 'Q': /[QⓆQꝖꝘɊ]/g,
6447
+ 'R': /[RⓇRŔṘŘȐȒṚṜŖṞɌⱤꝚꞦꞂ]/g,
6448
+ 'S': /[SⓈSẞŚṤŜṠŠṦṢṨȘŞⱾꞨꞄ]/g,
6449
+ 'T': /[TⓉTṪŤṬȚŢṰṮŦƬƮȾꞆ]/g,
6450
+ 'U': /[UⓊUÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴɄ]/g,
6451
+ 'V': /[VⓋVṼṾƲꝞɅ]/g,
6452
+ 'W': /[WⓌWẀẂŴẆẄẈⱲ]/g,
6453
+ 'X': /[XⓍXẊẌ]/g,
6454
+ 'Y': /[YⓎYỲÝŶỸȲẎŸỶỴƳɎỾ]/g,
6455
+ 'Z': /[ZⓏZŹẐŻŽẒẔƵȤⱿⱫꝢ]/g,
6456
+ 'a': /[aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ]/g,
6457
+ 'b': /[bⓑbḃḅḇƀƃɓ]/g,
6458
+ 'c': /[cⓒcćĉċčçḉƈȼꜿↄ]/g,
6459
+ 'd': /[dⓓdḋďḍḑḓḏđƌɖɗꝺ]/g,
6460
+ 'e': /[eⓔeèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ]/g,
6461
+ 'f': /[fⓕfḟƒꝼ]/g,
6462
+ 'g': /[gⓖgǵĝḡğġǧģǥɠꞡᵹꝿ]/g,
6463
+ 'h': /[hⓗhĥḣḧȟḥḩḫẖħⱨⱶɥ]/g,
6464
+ 'i': /[iⓘiìíîĩīĭïḯỉǐȉȋịįḭɨı]/g,
6465
+ 'j': /[jⓙjĵǰɉ]/g,
6466
+ 'k': /[kⓚkḱǩḳķḵƙⱪꝁꝃꝅꞣ]/g,
6467
+ 'l': /[lⓛlŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇ]/g,
6468
+ 'm': /[mⓜmḿṁṃɱɯ]/g,
6469
+ 'n': /[nⓝnǹńñṅňṇņṋṉƞɲʼnꞑꞥ]/g,
6470
+ 'o': /[oⓞoòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔꝋꝍɵ]/g,
6471
+ 'p': /[pⓟpṕṗƥᵽꝑꝓꝕ]/g,
6472
+ 'q': /[qⓠqɋꝗꝙ]/g,
6473
+ 'r': /[rⓡrŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ]/g,
6474
+ 's': /[sⓢsśṥŝṡšṧṣṩșşȿꞩꞅẛ]/g,
6475
+ 't': /[tⓣtṫẗťṭțţṱṯŧƭʈⱦꞇ]/g,
6476
+ 'u': /[uⓤuùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ]/g,
6477
+ 'v': /[vⓥvṽṿʋꝟʌ]/g,
6478
+ 'w': /[wⓦwẁẃŵẇẅẘẉⱳ]/g,
6479
+ 'x': /[xⓧxẋẍ]/g,
6480
+ 'y': /[yⓨyỳýŷỹȳẏÿỷẙỵƴɏỿ]/g,
6481
+ 'z': /[zⓩzźẑżžẓẕƶȥɀⱬꝣ]/g,
6482
+ 'AA': /[Ꜳ]/g,
6483
+ 'AE': /[ÆǼǢ]/g,
6484
+ 'AO': /[Ꜵ]/g,
6485
+ 'AU': /[Ꜷ]/g,
6486
+ 'AV': /[ꜸꜺ]/g,
6487
+ 'AY': /[Ꜽ]/g,
6488
+ 'DZ': /[DZDŽ]/g,
6489
+ 'Dz': /[DzDž]/g,
6490
+ 'LJ': /[LJ]/g,
6491
+ 'Lj': /[Lj]/g,
6492
+ 'NJ': /[NJ]/g,
6493
+ 'Nj': /[Nj]/g,
6494
+ 'OI': /[Ƣ]/g,
6495
+ 'OO': /[Ꝏ]/g,
6496
+ 'OU': /[Ȣ]/g,
6497
+ 'TZ': /[Ꜩ]/g,
6498
+ 'VY': /[Ꝡ]/g,
6499
+ 'aa': /[ꜳ]/g,
6500
+ 'ae': /[æǽǣ]/g,
6501
+ 'ao': /[ꜵ]/g,
6502
+ 'au': /[ꜷ]/g,
6503
+ 'av': /[ꜹꜻ]/g,
6504
+ 'ay': /[ꜽ]/g,
6505
+ 'dz': /[dzdž]/g,
6506
+ 'hv': /[ƕ]/g,
6507
+ 'lj': /[lj]/g,
6508
+ 'nj': /[nj]/g,
6509
+ 'oi': /[ƣ]/g,
6510
+ 'ou': /[ȣ]/g,
6511
+ 'oo': /[ꝏ]/g,
6512
+ 'ss': /[ß]/g,
6513
+ 'tz': /[ꜩ]/g,
6514
+ 'vy': /[ꝡ]/
6515
+ };
6516
+
6517
+
6518
+ Inflector.plural(/$/, 's');
6519
+ Inflector.plural(/s$/gi, 's');
6520
+ Inflector.plural(/(ax|test)is$/gi, '$1es');
6521
+ Inflector.plural(/(octop|vir|fung|foc|radi|alumn)(i|us)$/gi, '$1i');
6522
+ Inflector.plural(/(census|alias|status)$/gi, '$1es');
6523
+ Inflector.plural(/(bu)s$/gi, '$1ses');
6524
+ Inflector.plural(/(buffal|tomat)o$/gi, '$1oes');
6525
+ Inflector.plural(/([ti])um$/gi, '$1a');
6526
+ Inflector.plural(/([ti])a$/gi, '$1a');
6527
+ Inflector.plural(/sis$/gi, 'ses');
6528
+ Inflector.plural(/f+e?$/gi, 'ves');
6529
+ Inflector.plural(/(cuff|roof)$/gi, '$1s');
6530
+ Inflector.plural(/([ht]ive)$/gi, '$1s');
6531
+ Inflector.plural(/([^aeiouy]o)$/gi, '$1es');
6532
+ Inflector.plural(/([^aeiouy]|qu)y$/gi, '$1ies');
6533
+ Inflector.plural(/(x|ch|ss|sh)$/gi, '$1es');
6534
+ Inflector.plural(/(matr|vert|ind)(?:ix|ex)$/gi, '$1ices');
6535
+ Inflector.plural(/([ml])ouse$/gi, '$1ice');
6536
+ Inflector.plural(/([ml])ice$/gi, '$1ice');
6537
+ Inflector.plural(/^(ox)$/gi, '$1en');
6538
+ Inflector.plural(/^(oxen)$/gi, '$1');
6539
+ Inflector.plural(/(quiz)$/gi, '$1zes');
6540
+ Inflector.plural(/(phot|cant|hom|zer|pian|portic|pr|quart|kimon)o$/gi, '$1os');
6541
+ Inflector.plural(/(craft)$/gi, '$1');
6542
+ Inflector.plural(/[eo]{2}(th?)$/gi, 'ee$1');
6543
+
6544
+ Inflector.singular(/s$/gi, '');
6545
+ Inflector.singular(/([pst][aiu]s)$/gi, '$1');
6546
+ Inflector.singular(/([aeiouy])ss$/gi, '$1ss');
6547
+ Inflector.singular(/(n)ews$/gi, '$1ews');
6548
+ Inflector.singular(/([ti])a$/gi, '$1um');
6549
+ Inflector.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/gi, '$1$2sis');
6550
+ Inflector.singular(/(^analy)ses$/gi, '$1sis');
6551
+ Inflector.singular(/(i)(f|ves)$/i, '$1fe');
6552
+ Inflector.singular(/([aeolr]f?)(f|ves)$/i, '$1f');
6553
+ Inflector.singular(/([ht]ive)s$/gi, '$1');
6554
+ Inflector.singular(/([^aeiouy]|qu)ies$/gi, '$1y');
6555
+ Inflector.singular(/(s)eries$/gi, '$1eries');
6556
+ Inflector.singular(/(m)ovies$/gi, '$1ovie');
6557
+ Inflector.singular(/(x|ch|ss|sh)es$/gi, '$1');
6558
+ Inflector.singular(/([ml])(ous|ic)e$/gi, '$1ouse');
6559
+ Inflector.singular(/(bus)(es)?$/gi, '$1');
6560
+ Inflector.singular(/(o)es$/gi, '$1');
6561
+ Inflector.singular(/(shoe)s?$/gi, '$1');
6562
+ Inflector.singular(/(cris|ax|test)[ie]s$/gi, '$1is');
6563
+ Inflector.singular(/(octop|vir|fung|foc|radi|alumn)(i|us)$/gi, '$1us');
6564
+ Inflector.singular(/(census|alias|status)(es)?$/gi, '$1');
6565
+ Inflector.singular(/^(ox)(en)?/gi, '$1');
6566
+ Inflector.singular(/(vert|ind)(ex|ices)$/gi, '$1ex');
6567
+ Inflector.singular(/(matr)(ix|ices)$/gi, '$1ix');
6568
+ Inflector.singular(/(quiz)(zes)?$/gi, '$1');
6569
+ Inflector.singular(/(database)s?$/gi, '$1');
6570
+ Inflector.singular(/ee(th?)$/gi, 'oo$1');
6571
+
6572
+ Inflector.irregular('person', 'people');
6573
+ Inflector.irregular('man', 'men');
6574
+ Inflector.irregular('child', 'children');
6575
+ Inflector.irregular('sex', 'sexes');
6576
+ Inflector.irregular('move', 'moves');
6577
+ Inflector.irregular('save', 'saves');
6578
+ Inflector.irregular('save', 'saves');
6579
+ Inflector.irregular('cow', 'kine');
6580
+ Inflector.irregular('goose', 'geese');
6581
+ Inflector.irregular('zombie', 'zombies');
6582
+
6583
+ Inflector.uncountable('equipment,information,rice,money,species,series,fish,sheep,jeans'.split(','));
6584
+
6585
+
6586
+ String.extend({
6587
+
6588
+ /***
6589
+ * @method pluralize()
6590
+ * @returns String
6591
+ * @short Returns the plural form of the word in the string.
6592
+ * @example
6593
+ *
6594
+ * 'post'.pluralize() -> 'posts'
6595
+ * 'octopus'.pluralize() -> 'octopi'
6596
+ * 'sheep'.pluralize() -> 'sheep'
6597
+ * 'words'.pluralize() -> 'words'
6598
+ * 'CamelOctopus'.pluralize() -> 'CamelOctopi'
6599
+ *
6600
+ ***/
6601
+ 'pluralize': function() {
6602
+ return inflect(this, true);
6603
+ },
6604
+
6605
+ /***
6606
+ * @method singularize()
6607
+ * @returns String
6608
+ * @short The reverse of String#pluralize. Returns the singular form of a word in a string.
6609
+ * @example
6610
+ *
6611
+ * 'posts'.singularize() -> 'post'
6612
+ * 'octopi'.singularize() -> 'octopus'
6613
+ * 'sheep'.singularize() -> 'sheep'
6614
+ * 'word'.singularize() -> 'word'
6615
+ * 'CamelOctopi'.singularize() -> 'CamelOctopus'
6616
+ *
6617
+ ***/
6618
+ 'singularize': function() {
6619
+ return inflect(this, false);
6620
+ },
6621
+
6622
+ /***
6623
+ * @method humanize()
6624
+ * @returns String
6625
+ * @short Creates a human readable string.
6626
+ * @extra Capitalizes the first word and turns underscores into spaces and strips a trailing '_id', if any. Like String#titleize, this is meant for creating pretty output.
6627
+ * @example
6628
+ *
6629
+ * 'employee_salary'.humanize() -> 'Employee salary'
6630
+ * 'author_id'.humanize() -> 'Author'
6631
+ *
6632
+ ***/
6633
+ 'humanize': function() {
6634
+ var str = runReplacements(this, humans);
6635
+ str = str.replace(/_id$/g, '');
6636
+ str = str.replace(/(_)?([a-z\d]*)/gi, function(match, _, word){
6637
+ return (_ ? ' ' : '') + (acronyms[word] || word.toLowerCase());
6638
+ });
6639
+ return capitalize(str);
6640
+ },
6641
+
6642
+ /***
6643
+ * @method titleize()
6644
+ * @returns String
6645
+ * @short Creates a title version of the string.
6646
+ * @extra Capitalizes all the words and replaces some characters in the string to create a nicer looking title. String#titleize is meant for creating pretty output.
6647
+ * @example
6648
+ *
6649
+ * 'man from the boondocks'.titleize() -> 'Man from the Boondocks'
6650
+ * 'x-men: the last stand'.titleize() -> 'X Men: The Last Stand'
6651
+ * 'TheManWithoutAPast'.titleize() -> 'The Man Without a Past'
6652
+ * 'raiders_of_the_lost_ark'.titleize() -> 'Raiders of the Lost Ark'
6653
+ *
6654
+ ***/
6655
+ 'titleize': function() {
6656
+ var fullStopPunctuation = /[.:;!]$/, hasPunctuation, lastHadPunctuation, isFirstOrLast;
6657
+ return this.spacify().humanize().words(function(word, index, words) {
6658
+ hasPunctuation = fullStopPunctuation.test(word);
6659
+ isFirstOrLast = index == 0 || index == words.length - 1 || hasPunctuation || lastHadPunctuation;
6660
+ lastHadPunctuation = hasPunctuation;
6661
+ if(isFirstOrLast || !Downcased.any(word)) {
6662
+ return capitalize(word);
6663
+ } else {
6664
+ return word;
6665
+ }
6666
+ }).join(' ');
6667
+ },
6668
+
6669
+ /***
6670
+ * @method namespace()
6671
+ * @returns Mixed
6672
+ * @short Tries to find the namespace or property with the name specified in the string.
6673
+ * @extra Namespacing begins at the global level and operates on every "." in the string. If any level returns %undefined% the result will be %undefined%.
6674
+ * @example
6675
+ *
6676
+ * 'Path.To.Namespace'.namespace() -> Path.To.Namespace
6677
+ *
6678
+ ***/
6679
+ 'namespace': function() {
6680
+ var spaces = this.split('.'), scope = globalContext;
6681
+ spaces.each(function(s) {
6682
+ return !!(scope = scope[s]);
6683
+ });
6684
+ return scope;
6685
+ },
6686
+
6687
+ /***
6688
+ * @method parameterize()
6689
+ * @returns String
6690
+ * @short Replaces special characters in a string so that it may be used as part of a pretty URL.
6691
+ * @example
6692
+ *
6693
+ * 'hell, no!'.parameterize() -> 'hell-no'
6694
+ *
6695
+ ***/
6696
+ 'parameterize': function(separator) {
6697
+ if(separator === undefined) separator = '-';
6698
+ var str = this.normalize();
6699
+ str = str.replace(/[^a-z0-9\-_]+/gi, separator)
6700
+ if(separator) {
6701
+ str = str.replace(new RegExp('^{sep}+|{sep}+$|({sep}){sep}+'.assign({ 'sep': RegExp.escape(separator) }), 'g'), '$1');
6702
+ }
6703
+ return str.toLowerCase();
6704
+ },
6705
+
6706
+ /***
6707
+ * @method normalize()
6708
+ * @returns String
6709
+ * @short Returns the string with accented and non-standard Latin-based characters converted into ASCII approximate equivalents.
6710
+ * @example
6711
+ *
6712
+ * 'á'.normalize() -> 'a'
6713
+ * 'Ménage à trois'.normalize() -> 'Menage a trois'
6714
+ * 'Volkswagen'.normalize() -> 'Volkswagen'
6715
+ * 'FULLWIDTH'.normalize() -> 'FULLWIDTH'
6716
+ *
6717
+ ***/
6718
+ 'normalize': function() {
6719
+ var str = this.toString();
6720
+ Object.each(Normalize, function(base, reg) {
6721
+ str = str.replace(reg, base);
6722
+ });
6723
+ return str;
6724
+ }
6725
+
6726
+ });
6727
+
6728
+ String.Inflector = Inflector;
6729
+ String.Inflector.acronyms = acronyms;
6730
+
6731
+ })(this);