master
1
2 'use strict';
3
4 /***
5 * @package Core
6 * @description Internal utility and common methods.
7 ***/
8
9
10 // A few optimizations for Google Closure Compiler will save us a couple kb in the release script.
11 var object = Object, array = Array, regexp = RegExp, date = Date, string = String, number = Number, math = Math, Undefined;
12
13 // The global context
14 var globalContext = typeof global !== 'undefined' ? global : this;
15
16 // Internal toString
17 var internalToString = object.prototype.toString;
18
19 // Internal hasOwnProperty
20 var internalHasOwnProperty = object.prototype.hasOwnProperty;
21
22 // defineProperty exists in IE8 but will error when trying to define a property on
23 // native objects. IE8 does not have defineProperies, however, so this check saves a try/catch block.
24 var definePropertySupport = object.defineProperty && object.defineProperties;
25
26 // Are regexes type function?
27 var regexIsFunction = typeof regexp() === 'function';
28
29 // Do strings have no keys?
30 var noKeysInStringObjects = !('0' in new string('a'));
31
32 // Type check methods need a way to be accessed dynamically.
33 var typeChecks = {};
34
35 // Classes that can be matched by value
36 var matchedByValueReg = /^\[object Date|Array|String|Number|RegExp|Boolean|Arguments\]$/;
37
38 // Class initializers and class helpers
39 var ClassNames = 'Boolean,Number,String,Array,Date,RegExp,Function'.split(',');
40
41 var isBoolean = buildPrimitiveClassCheck('boolean', ClassNames[0]);
42 var isNumber = buildPrimitiveClassCheck('number', ClassNames[1]);
43 var isString = buildPrimitiveClassCheck('string', ClassNames[2]);
44
45 var isArray = buildClassCheck(ClassNames[3]);
46 var isDate = buildClassCheck(ClassNames[4]);
47 var isRegExp = buildClassCheck(ClassNames[5]);
48
49
50 // Wanted to enhance performance here by using simply "typeof"
51 // but Firefox has two major issues that make this impossible,
52 // one fixed, the other not. Despite being typeof "function"
53 // the objects below still report in as [object Function], so
54 // we need to perform a full class check here.
55 //
56 // 1. Regexes can be typeof "function" in FF < 3
57 // https://bugzilla.mozilla.org/show_bug.cgi?id=61911 (fixed)
58 //
59 // 2. HTMLEmbedElement and HTMLObjectElement are be typeof "function"
60 // https://bugzilla.mozilla.org/show_bug.cgi?id=268945 (won't fix)
61 //
62 var isFunction = buildClassCheck(ClassNames[6]);
63
64 function isClass(obj, klass, cached) {
65 var k = cached || className(obj);
66 return k === '[object '+klass+']';
67 }
68
69 function buildClassCheck(klass) {
70 var fn = (klass === 'Array' && array.isArray) || function(obj, cached) {
71 return isClass(obj, klass, cached);
72 };
73 typeChecks[klass] = fn;
74 return fn;
75 }
76
77 function buildPrimitiveClassCheck(type, klass) {
78 var fn = function(obj) {
79 if(isObjectType(obj)) {
80 return isClass(obj, klass);
81 }
82 return typeof obj === type;
83 }
84 typeChecks[klass] = fn;
85 return fn;
86 }
87
88 function className(obj) {
89 return internalToString.call(obj);
90 }
91
92 function initializeClasses() {
93 initializeClass(object);
94 iterateOverObject(ClassNames, function(i,name) {
95 initializeClass(globalContext[name]);
96 });
97 }
98
99 function initializeClass(klass) {
100 if(klass['SugarMethods']) return;
101 defineProperty(klass, 'SugarMethods', {});
102 extend(klass, false, true, {
103 'extend': function(methods, override, instance) {
104 extend(klass, instance !== false, override, methods);
105 },
106 'sugarRestore': function() {
107 return batchMethodExecute(this, klass, arguments, function(target, name, m) {
108 defineProperty(target, name, m.method);
109 });
110 },
111 'sugarRevert': function() {
112 return batchMethodExecute(this, klass, arguments, function(target, name, m) {
113 if(m['existed']) {
114 defineProperty(target, name, m['original']);
115 } else {
116 delete target[name];
117 }
118 });
119 }
120 });
121 }
122
123 // Class extending methods
124
125 function extend(klass, instance, override, methods) {
126 var extendee = instance ? klass.prototype : klass;
127 initializeClass(klass);
128 iterateOverObject(methods, function(name, extendedFn) {
129 var nativeFn = extendee[name],
130 existed = hasOwnProperty(extendee, name);
131 if(isFunction(override) && nativeFn) {
132 extendedFn = wrapNative(nativeFn, extendedFn, override);
133 }
134 if(override !== false || !nativeFn) {
135 defineProperty(extendee, name, extendedFn);
136 }
137 // If the method is internal to Sugar, then
138 // store a reference so it can be restored later.
139 klass['SugarMethods'][name] = {
140 'method': extendedFn,
141 'existed': existed,
142 'original': nativeFn,
143 'instance': instance
144 };
145 });
146 }
147
148 function extendSimilar(klass, instance, override, set, fn) {
149 var methods = {};
150 set = isString(set) ? set.split(',') : set;
151 set.forEach(function(name, i) {
152 fn(methods, name, i);
153 });
154 extend(klass, instance, override, methods);
155 }
156
157 function batchMethodExecute(target, klass, args, fn) {
158 var all = args.length === 0, methods = multiArgs(args), changed = false;
159 iterateOverObject(klass['SugarMethods'], function(name, m) {
160 if(all || methods.indexOf(name) !== -1) {
161 changed = true;
162 fn(m['instance'] ? target.prototype : target, name, m);
163 }
164 });
165 return changed;
166 }
167
168 function wrapNative(nativeFn, extendedFn, condition) {
169 return function(a) {
170 return condition.apply(this, arguments) ?
171 extendedFn.apply(this, arguments) :
172 nativeFn.apply(this, arguments);
173 }
174 }
175
176 function defineProperty(target, name, method) {
177 if(definePropertySupport) {
178 object.defineProperty(target, name, {
179 'value': method,
180 'configurable': true,
181 'enumerable': false,
182 'writable': true
183 });
184 } else {
185 target[name] = method;
186 }
187 }
188
189
190 // Argument helpers
191
192 function multiArgs(args, fn, from) {
193 var result = [], i = from || 0, len;
194 for(len = args.length; i < len; i++) {
195 result.push(args[i]);
196 if(fn) fn.call(args, args[i], i);
197 }
198 return result;
199 }
200
201 function flattenedArgs(args, fn, from) {
202 var arg = args[from || 0];
203 if(isArray(arg)) {
204 args = arg;
205 from = 0;
206 }
207 return multiArgs(args, fn, from);
208 }
209
210 function checkCallback(fn) {
211 if(!fn || !fn.call) {
212 throw new TypeError('Callback is not callable');
213 }
214 }
215
216
217 // General helpers
218
219 function isDefined(o) {
220 return o !== Undefined;
221 }
222
223 function isUndefined(o) {
224 return o === Undefined;
225 }
226
227
228 // Object helpers
229
230 function hasProperty(obj, prop) {
231 return !isPrimitiveType(obj) && prop in obj;
232 }
233
234 function hasOwnProperty(obj, prop) {
235 return !!obj && internalHasOwnProperty.call(obj, prop);
236 }
237
238 function isObjectType(obj) {
239 // 1. Check for null
240 // 2. Check for regexes in environments where they are "functions".
241 return !!obj && (typeof obj === 'object' || (regexIsFunction && isRegExp(obj)));
242 }
243
244 function isPrimitiveType(obj) {
245 var type = typeof obj;
246 return obj == null || type === 'string' || type === 'number' || type === 'boolean';
247 }
248
249 function isPlainObject(obj, klass) {
250 klass = klass || className(obj);
251 try {
252 // Not own constructor property must be Object
253 // This code was borrowed from jQuery.isPlainObject
254 if (obj && obj.constructor &&
255 !hasOwnProperty(obj, 'constructor') &&
256 !hasOwnProperty(obj.constructor.prototype, 'isPrototypeOf')) {
257 return false;
258 }
259 } catch (e) {
260 // IE8,9 Will throw exceptions on certain host objects.
261 return false;
262 }
263 // === on the constructor is not safe across iframes
264 // 'hasOwnProperty' ensures that the object also inherits
265 // from Object, which is false for DOMElements in IE.
266 return !!obj && klass === '[object Object]' && 'hasOwnProperty' in obj;
267 }
268
269 function iterateOverObject(obj, fn) {
270 var key;
271 for(key in obj) {
272 if(!hasOwnProperty(obj, key)) continue;
273 if(fn.call(obj, key, obj[key], obj) === false) break;
274 }
275 }
276
277 function simpleRepeat(n, fn) {
278 for(var i = 0; i < n; i++) {
279 fn(i);
280 }
281 }
282
283 function simpleMerge(target, source) {
284 iterateOverObject(source, function(key) {
285 target[key] = source[key];
286 });
287 return target;
288 }
289
290 // Make primtives types like strings into objects.
291 function coercePrimitiveToObject(obj) {
292 if(isPrimitiveType(obj)) {
293 obj = object(obj);
294 }
295 if(noKeysInStringObjects && isString(obj)) {
296 forceStringCoercion(obj);
297 }
298 return obj;
299 }
300
301 // Force strings to have their indexes set in
302 // environments that don't do this automatically.
303 function forceStringCoercion(obj) {
304 var i = 0, chr;
305 while(chr = obj.charAt(i)) {
306 obj[i++] = chr;
307 }
308 }
309
310 // Hash definition
311
312 function Hash(obj) {
313 simpleMerge(this, coercePrimitiveToObject(obj));
314 };
315
316 Hash.prototype.constructor = object;
317
318 // Math helpers
319
320 var abs = math.abs;
321 var pow = math.pow;
322 var ceil = math.ceil;
323 var floor = math.floor;
324 var round = math.round;
325 var min = math.min;
326 var max = math.max;
327
328 function withPrecision(val, precision, fn) {
329 var multiplier = pow(10, abs(precision || 0));
330 fn = fn || round;
331 if(precision < 0) multiplier = 1 / multiplier;
332 return fn(val * multiplier) / multiplier;
333 }
334
335 // Full width number helpers
336
337 var HalfWidthZeroCode = 0x30;
338 var HalfWidthNineCode = 0x39;
339 var FullWidthZeroCode = 0xff10;
340 var FullWidthNineCode = 0xff19;
341
342 var HalfWidthPeriod = '.';
343 var FullWidthPeriod = '.';
344 var HalfWidthComma = ',';
345
346 // Used here and later in the Date package.
347 var FullWidthDigits = '';
348
349 var NumberNormalizeMap = {};
350 var NumberNormalizeReg;
351
352 function codeIsNumeral(code) {
353 return (code >= HalfWidthZeroCode && code <= HalfWidthNineCode) ||
354 (code >= FullWidthZeroCode && code <= FullWidthNineCode);
355 }
356
357 function buildNumberHelpers() {
358 var digit, i;
359 for(i = 0; i <= 9; i++) {
360 digit = chr(i + FullWidthZeroCode);
361 FullWidthDigits += digit;
362 NumberNormalizeMap[digit] = chr(i + HalfWidthZeroCode);
363 }
364 NumberNormalizeMap[HalfWidthComma] = '';
365 NumberNormalizeMap[FullWidthPeriod] = HalfWidthPeriod;
366 // Mapping this to itself to easily be able to easily
367 // capture it in stringToNumber to detect decimals later.
368 NumberNormalizeMap[HalfWidthPeriod] = HalfWidthPeriod;
369 NumberNormalizeReg = regexp('[' + FullWidthDigits + FullWidthPeriod + HalfWidthComma + HalfWidthPeriod + ']', 'g');
370 }
371
372 // String helpers
373
374 function chr(num) {
375 return string.fromCharCode(num);
376 }
377
378 // WhiteSpace/LineTerminator as defined in ES5.1 plus Unicode characters in the Space, Separator category.
379 function getTrimmableCharacters() {
380 return '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF';
381 }
382
383 function repeatString(str, num) {
384 var result = '', str = str.toString();
385 while (num > 0) {
386 if (num & 1) {
387 result += str;
388 }
389 if (num >>= 1) {
390 str += str;
391 }
392 }
393 return result;
394 }
395
396 // Returns taking into account full-width characters, commas, and decimals.
397 function stringToNumber(str, base) {
398 var sanitized, isDecimal;
399 sanitized = str.replace(NumberNormalizeReg, function(chr) {
400 var replacement = NumberNormalizeMap[chr];
401 if(replacement === HalfWidthPeriod) {
402 isDecimal = true;
403 }
404 return replacement;
405 });
406 return isDecimal ? parseFloat(sanitized) : parseInt(sanitized, base || 10);
407 }
408
409
410 // Used by Number and Date
411
412 function padNumber(num, place, sign, base) {
413 var str = abs(num).toString(base || 10);
414 str = repeatString('0', place - str.replace(/\.\d+/, '').length) + str;
415 if(sign || num < 0) {
416 str = (num < 0 ? '-' : '+') + str;
417 }
418 return str;
419 }
420
421 function getOrdinalizedSuffix(num) {
422 if(num >= 11 && num <= 13) {
423 return 'th';
424 } else {
425 switch(num % 10) {
426 case 1: return 'st';
427 case 2: return 'nd';
428 case 3: return 'rd';
429 default: return 'th';
430 }
431 }
432 }
433
434
435 // RegExp helpers
436
437 function getRegExpFlags(reg, add) {
438 var flags = '';
439 add = add || '';
440 function checkFlag(prop, flag) {
441 if(prop || add.indexOf(flag) > -1) {
442 flags += flag;
443 }
444 }
445 checkFlag(reg.multiline, 'm');
446 checkFlag(reg.ignoreCase, 'i');
447 checkFlag(reg.global, 'g');
448 checkFlag(reg.sticky, 'y');
449 return flags;
450 }
451
452 function escapeRegExp(str) {
453 if(!isString(str)) str = string(str);
454 return str.replace(/([\\/\'*+?|()\[\]{}.^$])/g,'\\$1');
455 }
456
457
458 // Date helpers
459
460 function callDateGet(d, method) {
461 return d['get' + (d._utc ? 'UTC' : '') + method]();
462 }
463
464 function callDateSet(d, method, value) {
465 return d['set' + (d._utc && method != 'ISOWeek' ? 'UTC' : '') + method](value);
466 }
467
468 // Used by Array#unique and Object.equal
469
470 function stringify(thing, stack) {
471 var type = typeof thing,
472 thingIsObject,
473 thingIsArray,
474 klass, value,
475 arr, key, i, len;
476
477 // Return quickly if string to save cycles
478 if(type === 'string') return thing;
479
480 klass = internalToString.call(thing)
481 thingIsObject = isPlainObject(thing, klass);
482 thingIsArray = isArray(thing, klass);
483
484 if(thing != null && thingIsObject || thingIsArray) {
485 // This method for checking for cyclic structures was egregiously stolen from
486 // the ingenious method by @kitcambridge from the Underscore script:
487 // https://github.com/documentcloud/underscore/issues/240
488 if(!stack) stack = [];
489 // Allowing a step into the structure before triggering this
490 // script to save cycles on standard JSON structures and also to
491 // try as hard as possible to catch basic properties that may have
492 // been modified.
493 if(stack.length > 1) {
494 i = stack.length;
495 while (i--) {
496 if (stack[i] === thing) {
497 return 'CYC';
498 }
499 }
500 }
501 stack.push(thing);
502 value = thing.valueOf() + string(thing.constructor);
503 arr = thingIsArray ? thing : object.keys(thing).sort();
504 for(i = 0, len = arr.length; i < len; i++) {
505 key = thingIsArray ? i : arr[i];
506 value += key + stringify(thing[key], stack);
507 }
508 stack.pop();
509 } else if(1 / thing === -Infinity) {
510 value = '-0';
511 } else {
512 value = string(thing && thing.valueOf ? thing.valueOf() : thing);
513 }
514 return type + klass + value;
515 }
516
517 function isEqual(a, b) {
518 if(a === b) {
519 // Return quickly up front when matching by reference,
520 // but be careful about 0 !== -0.
521 return a !== 0 || 1 / a === 1 / b;
522 } else if(objectIsMatchedByValue(a) && objectIsMatchedByValue(b)) {
523 return stringify(a) === stringify(b);
524 }
525 return false;
526 }
527
528 function objectIsMatchedByValue(obj) {
529 // Only known objects are matched by value. This is notably excluding functions, DOM Elements, and instances of
530 // user-created classes. The latter can arguably be matched by value, but distinguishing between these and
531 // host objects -- which should never be compared by value -- is very tricky so not dealing with it here.
532 var klass = className(obj);
533 return matchedByValueReg.test(klass) || isPlainObject(obj, klass);
534 }
535
536
537 // Used by Array#at and String#at
538
539 function getEntriesForIndexes(obj, args, isString) {
540 var result,
541 length = obj.length,
542 argsLen = args.length,
543 overshoot = args[argsLen - 1] !== false,
544 multiple = argsLen > (overshoot ? 1 : 2);
545 if(!multiple) {
546 return entryAtIndex(obj, length, args[0], overshoot, isString);
547 }
548 result = [];
549 multiArgs(args, function(index) {
550 if(isBoolean(index)) return false;
551 result.push(entryAtIndex(obj, length, index, overshoot, isString));
552 });
553 return result;
554 }
555
556 function entryAtIndex(obj, length, index, overshoot, isString) {
557 if(overshoot) {
558 index = index % length;
559 if(index < 0) index = length + index;
560 }
561 return isString ? obj.charAt(index) : obj[index];
562 }
563
564
565 // Object class methods implemented as instance methods
566
567 function buildObjectInstanceMethods(set, target) {
568 extendSimilar(target, true, false, set, function(methods, name) {
569 methods[name + (name === 'equal' ? 's' : '')] = function() {
570 return object[name].apply(null, [this].concat(multiArgs(arguments)));
571 }
572 });
573 }
574
575 initializeClasses();
576 buildNumberHelpers();
577