| /* |
| * JavaScript Hooker |
| * http://github.com/cowboy/javascript-hooker |
| * |
| * Copyright (c) 2012 "Cowboy" Ben Alman |
| * Licensed under the MIT license. |
| * http://benalman.com/about/license/ |
| */ |
| |
| (function(exports) { |
| // Get an array from an array-like object with slice.call(arrayLikeObject). |
| var slice = [].slice; |
| // Get an "[object [[Class]]]" string with toString.call(value). |
| var toString = {}.toString; |
| |
| // I can't think of a better way to ensure a value is a specific type other |
| // than to create instances and use the `instanceof` operator. |
| function HookerOverride(v) { this.value = v; } |
| function HookerPreempt(v) { this.value = v; } |
| function HookerFilter(c, a) { this.context = c; this.args = a; } |
| |
| // When a pre- or post-hook returns the result of this function, the value |
| // passed will be used in place of the original function's return value. Any |
| // post-hook override value will take precedence over a pre-hook override |
| // value. |
| exports.override = function(value) { |
| return new HookerOverride(value); |
| }; |
| |
| // When a pre-hook returns the result of this function, the value passed will |
| // be used in place of the original function's return value, and the original |
| // function will NOT be executed. |
| exports.preempt = function(value) { |
| return new HookerPreempt(value); |
| }; |
| |
| // When a pre-hook returns the result of this function, the context and |
| // arguments passed will be applied into the original function. |
| exports.filter = function(context, args) { |
| return new HookerFilter(context, args); |
| }; |
| |
| // Execute callback(s) for properties of the specified object. |
| function forMethods(obj, props, callback) { |
| var prop; |
| if (typeof props === "string") { |
| // A single prop string was passed. Create an array. |
| props = [props]; |
| } else if (props == null) { |
| // No props were passed, so iterate over all properties, building an |
| // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so |
| // this has to be done manually. |
| props = []; |
| for (prop in obj) { |
| if (obj.hasOwnProperty(prop)) { |
| props.push(prop); |
| } |
| } |
| } |
| // Execute callback for every method in the props array. |
| var i = props.length; |
| while (i--) { |
| // If the property isn't a function... |
| if (toString.call(obj[props[i]]) !== "[object Function]" || |
| // ...or the callback returns false... |
| callback(obj, props[i]) === false) { |
| // ...remove it from the props array to be returned. |
| props.splice(i, 1); |
| } |
| } |
| // Return an array of method names for which the callback didn't fail. |
| return props; |
| } |
| |
| // Monkey-patch (hook) a method of an object. |
| exports.hook = function(obj, props, options) { |
| // If the props argument was omitted, shuffle the arguments. |
| if (options == null) { |
| options = props; |
| props = null; |
| } |
| // If just a function is passed instead of an options hash, use that as a |
| // pre-hook function. |
| if (typeof options === "function") { |
| options = {pre: options}; |
| } |
| |
| // Hook the specified method of the object. |
| return forMethods(obj, props, function(obj, prop) { |
| // The original (current) method. |
| var orig = obj[prop]; |
| // The new hooked function. |
| function hooked() { |
| var result, origResult, tmp; |
| |
| // Get an array of arguments. |
| var args = slice.call(arguments); |
| |
| // If passName option is specified, prepend prop to the args array, |
| // passing it as the first argument to any specified hook functions. |
| if (options.passName) { |
| args.unshift(prop); |
| } |
| |
| // If a pre-hook function was specified, invoke it in the current |
| // context with the passed-in arguments, and store its result. |
| if (options.pre) { |
| result = options.pre.apply(this, args); |
| } |
| |
| if (result instanceof HookerFilter) { |
| // If the pre-hook returned hooker.filter(context, args), invoke the |
| // original function with that context and arguments, and store its |
| // result. |
| origResult = result = orig.apply(result.context, result.args); |
| } else if (result instanceof HookerPreempt) { |
| // If the pre-hook returned hooker.preempt(value) just use the passed |
| // value and don't execute the original function. |
| origResult = result = result.value; |
| } else { |
| // Invoke the original function in the current context with the |
| // passed-in arguments, and store its result. |
| origResult = orig.apply(this, arguments); |
| // If the pre-hook returned hooker.override(value), use the passed |
| // value, otherwise use the original function's result. |
| result = result instanceof HookerOverride ? result.value : origResult; |
| } |
| |
| if (options.post) { |
| // If a post-hook function was specified, invoke it in the current |
| // context, passing in the result of the original function as the |
| // first argument, followed by any passed-in arguments. |
| tmp = options.post.apply(this, [origResult].concat(args)); |
| if (tmp instanceof HookerOverride) { |
| // If the post-hook returned hooker.override(value), use the passed |
| // value, otherwise use the previously computed result. |
| result = tmp.value; |
| } |
| } |
| |
| // Unhook if the "once" option was specified. |
| if (options.once) { |
| exports.unhook(obj, prop); |
| } |
| |
| // Return the result! |
| return result; |
| } |
| // Re-define the method. |
| obj[prop] = hooked; |
| // Fail if the function couldn't be hooked. |
| if (obj[prop] !== hooked) { return false; } |
| // Store a reference to the original method as a property on the new one. |
| obj[prop]._orig = orig; |
| }); |
| }; |
| |
| // Get a reference to the original method from a hooked function. |
| exports.orig = function(obj, prop) { |
| return obj[prop]._orig; |
| }; |
| |
| // Un-monkey-patch (unhook) a method of an object. |
| exports.unhook = function(obj, props) { |
| return forMethods(obj, props, function(obj, prop) { |
| // Get a reference to the original method, if it exists. |
| var orig = exports.orig(obj, prop); |
| // If there's no original method, it can't be unhooked, so fail. |
| if (!orig) { return false; } |
| // Unhook the method. |
| obj[prop] = orig; |
| }); |
| }; |
| }(typeof exports === "object" && exports || this)); |