--- /dev/null
+/*! JavaScript Hooker - v0.2.3 - 1/29/2012
+* http://github.com/cowboy/javascript-hooker
+* Copyright (c) 2012 "Cowboy" Ben Alman; Licensed MIT */
+
+(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));