1 /*
  2  * Dual-licensed under the MIT License & the Academic Free License v. 2.1.
  3  * See the file LICENSE for more information.
  4  *
  5  * (c) 2007-2008 by Per Cederberg & Dynabyte AB. All rights reserved.
  6  */
  7 
  8 // Check for loaded MochiKit
  9 if (typeof(MochiKit) == "undefined") {
 10     throw new ReferenceError("MochiKit must be loaded before loading this script");
 11 }
 12 
 13 /**
 14  * @name MochiKit.Base
 15  * @namespace Provides functional programming and useful comparisons.
 16  */
 17 // Check for loaded MochiKit.Base
 18 if (typeof(MochiKit.Base) == "undefined") {
 19     throw new ReferenceError("MochiKit.Base must be loaded before loading this script");
 20 }
 21 
 22 /**
 23  * Returns the first function argument that is not undefined.
 24  *
 25  * @param {Object} [...] the values to check
 26  *
 27  * @return {Object} the first non-undefined argument, or
 28  *         undefined if all arguments were undefined 
 29  */
 30 MochiKit.Base.defaultValue = function(/* ... */) {
 31     for (var i = 0; i < arguments.length; i++) {
 32         if (typeof(arguments[i]) != "undefined") {
 33             return arguments[i];
 34         }
 35     }
 36     return undefined;
 37 }
 38 
 39 /**
 40  * Creates a dictionary object from a list of keys and values. It
 41  * can be used either as a reverse of items(), or as a reverse of
 42  * keys() and values(). That is, either the function take a single
 43  * list where each element contains both key and value, or it takes
 44  * two separate lists, one with keys and the other with values. If
 45  * a key is specified twice, only the last value will be used.
 46  *
 47  * @code dict([['a', 1], ['b', 2]]) --> { a: 1, b: 2 }
 48  *       dict(['a','b'], [1, 2]) --> { a: 1, b: 2 }
 49  *
 50  * @param {Array} itemsOrKeys the list of items or keys
 51  * @param {Array} [values] the optional list of values
 52  *
 53  * @return {Object} a dictionary object with all the keys set to the
 54  *         corresponding value
 55  */
 56 MochiKit.Base.dict = function(itemsOrKeys, values) {
 57     var o = {};
 58     if (!MochiKit.Base.isArrayLike(itemsOrKeys)) {
 59         throw new TypeError("First argument must be array-like");
 60     }
 61     if (MochiKit.Base.isArrayLike(values) && itemsOrKeys.length !== values.length) {
 62         throw new TypeError("Both arrays must be of same length");
 63     }
 64     for (var i = 0; i < itemsOrKeys.length; i++) {
 65         var k = itemsOrKeys[i];
 66         if (k === null || k === undefined) {
 67             throw new TypeError("Key at index " + i + " is null or undefined");
 68         } else if (MochiKit.Base.isArrayLike(k)) {
 69             o[k[0]] = k[1];
 70         } else if (MochiKit.Base.isArrayLike(values)) {
 71             o[k] = values[i];
 72         } else {
 73             o[k] = values;
 74         }
 75     }
 76     return o;
 77 }
 78 
 79 /**
 80  * Creates a new object by copying keys and values from another
 81  * object. A list of key names (or an object whose property names
 82  * will be used as keys) must be specified as an argument. The
 83  * returned object will only contain properties that were defined in
 84  * the source object, keeping the source object values. The source
 85  * object will be left unmodified.
 86  *
 87  * @param {Object} src the source object to select values from
 88  * @param {Array/Object} keys the list of keys to select, or an
 89  *            object with the keys to select
 90  *
 91  * @return {Object} a new object containing the matching keys and
 92  *             values found in the source object
 93  */
 94 MochiKit.Base.select = function(src, keys) {
 95     var res = {};
 96     if (!MochiKit.Base.isArrayLike(keys)) {
 97         keys = MochiKit.Base.keys(keys);
 98     }
 99     for (var i = 0; i < keys.length; i++) {
100         var k = keys[i];
101         if (k in src) {
102             res[k] = src[k];
103         }
104     }
105     return res;
106 }
107 
108 /**
109  * Filters an object by removing a list of keys. A list of key names
110  * (or an object whose property names will be used as keys) must be
111  * specified as an argument. A new object containing the source
112  * object values for the specified keys will be returned. The source
113  * object will be modified by removing all the specified keys.
114  *
115  * @param {Object} src the source object to select and modify
116  * @param {Array/Object} keys the list of keys to remove, or an
117  *            object with the keys to remove
118  *
119  * @return {Object} a new object containing the matching keys and
120  *             values found in the source object
121  */
122 MochiKit.Base.mask = function(src, keys) {
123     var res = {};
124     if (!MochiKit.Base.isArrayLike(keys)) {
125         keys = MochiKit.Base.keys(keys);
126     }
127     for (var i = 0; i < keys.length; i++) {
128         var k = keys[i];
129         if (k in src) {
130             res[k] = src[k];
131             delete src[k];
132         }
133     }
134     return res;
135 }
136 
137 /**
138  * Returns the name of a function. This is often useful for debugging
139  * or logging purposes. If the function is anonymous or the
140  * JavaScript environment doesn't provide function <code>name</code>
141  * properties, any registered function name or undefined will be
142  * returned.
143  *
144  * @param {Function} func the function to name
145  *
146  * @return {String} the function name, or undefined if not available
147  *
148  * @see MochiKit.Base.registerFunctionNames
149  */
150 MochiKit.Base.functionName = function(func) {
151     if (func == null) {
152         return null;
153     } else if (func.name != null && func.name != "") {
154         return func.name;
155     } else {
156         return func.NAME;
157     }
158 }
159 
160 /**
161  * Registers function names for debugging or logging. This is useful
162  * when using anonymous functions or inside JavaScript environments
163  * that do not provide function <code>name</code> properties. This
164  * function will add the specified name as a new <code>NAME</code>
165  * property to any function that doesn't already have a name. This
166  * function will also process any properties or prototype properties
167  * recursively adding names like <code>name.[property name]</code>.
168  *
169  * @param {Object} obj the function or object to register
170  * @param {String} name the function or object (class) name
171  * @param {Array} [stack] the object stack to avoid circular recursion
172  *
173  * @see MochiKit.Base.functionName
174  */
175 MochiKit.Base.registerFunctionNames = function(obj, name, stack) {
176    if (typeof(obj) === "function" &&
177        (obj.name == null || obj.name == "") &&
178        typeof(obj.NAME) === "undefined") {
179        obj.NAME = name;
180    }
181    stack = stack || [];
182    if (obj != null && name != null &&
183        (typeof(obj) === "object" || typeof(obj) === "function") &&
184        obj !== Object.prototype && obj !== Function.prototype &&
185        typeof(obj.nodeType) !== "number" &&
186        MochiKit.Base.findIdentical(stack, obj) < 0) {
187 
188        stack.push(obj);
189        for (var prop in obj) {
190            var str = name + "." + prop;
191            MochiKit.Base.registerFunctionNames(obj[prop], str, stack);
192        }
193        var str = name + ".prototype";
194        MochiKit.Base.registerFunctionNames(obj.prototype, str, stack);
195        stack.pop();
196    }
197 }
198 
199 /**
200  * Returns the current execution stack trace. The stack trace is an
201  * array of function names with the innermost function at the lowest
202  * index (0). Due to limitations in the JavaScript API:s, the stack
203  * trace will be cut if recursion is detected. The stack trace will
204  * also be cut if the call depth exceeds the maximum depth or if any
205  * function in the chain has an injected stack trace.
206  *
207  * @param {Number} [maxDepth] the maximum call depth, defaults to 20
208  *
209  * @return {Array} the stack trace array of function names
210  *
211  * @see MochiKit.Base.functionName
212  * @see MochiKit.Base.injectStackTrace
213  */
214 MochiKit.Base.stackTrace = function(maxDepth) {
215     var func = arguments.callee.caller;
216     var visited = [];
217     var res = [];
218     maxDepth = maxDepth || 20;
219     while (func != null) {
220         if (MochiKit.Base.findIdentical(visited, func) >= 0) {
221             res.push("...recursion...");
222             break;
223         }
224         if (func.$stackTrace != null) {
225             res = res.concat(func.$stackTrace);
226             break;
227         }
228         var name = MochiKit.Base.functionName(func);
229         if (name === null) {
230             // Skip stack trace when null (but not when undefined)
231         } else {
232             res.push(name || "<anonymous>");
233         }
234         visited.push(func);
235         if (visited.length >= maxDepth) {
236             res.push("...");
237             break;
238         }
239         func = func.caller;
240     }
241     return res;
242 }
243 
244 /**
245  * Injects a stack trace for a function. This method is useful for
246  * creating a fake stack trace in anonymous or callback functions. A
247  * null value can be used to clear any previously injected stack
248  * trace for the calling function.
249  *
250  * @param {Array} stackTrace the stack trace, or null to clear
251  * @param {Function} [func] the function to modify, or null for the
252  *            currently executing function (i.e. the caller)
253  */
254 MochiKit.Base.injectStackTrace = function(stackTrace, func) {
255     func = func || arguments.callee.caller;
256     if (func != null) {
257         if (stackTrace) {
258             func.$stackTrace = stackTrace;
259         } else {
260             delete func.$stackTrace;
261         }
262     }
263 }
264 
265 /**
266  * Resolves a relative URI to an absolute URI. This function will
267  * return absolute URI:s directly and traverse any "../" directory
268  * paths in the specified URI. The base URI provided must be
269  * absolute.
270  *
271  * @param {String} uri the relative URI to resolve
272  * @param {String} base the absolute base URI
273  *
274  * @return {String} the resolved absolute URI
275  */
276 MochiKit.Base.resolveURI = function (uri, base) {
277     if (uri.indexOf("://") > 0) {
278         return uri;
279     } else if (uri.indexOf("#") == 0) {
280         var pos = base.lastIndexOf("#");
281         if (pos >= 0) {
282             base = base.substring(0, pos);
283         }
284         return base + uri;
285     } else if (uri.indexOf("/") == 0) {
286         var pos = base.indexOf("://");
287         base = base.substring(0, pos + 2);
288         return base + uri;
289     } else if (uri.indexOf("../") == 0) {
290         var pos = base.lastIndexOf("/");
291         base = base.substring(0, pos);
292         uri = uri.substring(3);
293         return MochiKit.Base.resolveURI(uri, base);
294     } else {
295         var pos = base.lastIndexOf("/");
296         base = base.substring(0, pos + 1);
297         return base + uri;
298     }
299 }
300