ba-hooker.js
6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/*! 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));