blob: d10a3216b5ffd91891b83f034a2be7bf57c6815b [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001/*! JavaScript Hooker - v0.2.3 - 1/29/2012
2* http://github.com/cowboy/javascript-hooker
3* Copyright (c) 2012 "Cowboy" Ben Alman; Licensed MIT */
4
5(function(exports) {
6 // Get an array from an array-like object with slice.call(arrayLikeObject).
7 var slice = [].slice;
8 // Get an "[object [[Class]]]" string with toString.call(value).
9 var toString = {}.toString;
10
11 // I can't think of a better way to ensure a value is a specific type other
12 // than to create instances and use the `instanceof` operator.
13 function HookerOverride(v) { this.value = v; }
14 function HookerPreempt(v) { this.value = v; }
15 function HookerFilter(c, a) { this.context = c; this.args = a; }
16
17 // When a pre- or post-hook returns the result of this function, the value
18 // passed will be used in place of the original function's return value. Any
19 // post-hook override value will take precedence over a pre-hook override
20 // value.
21 exports.override = function(value) {
22 return new HookerOverride(value);
23 };
24
25 // When a pre-hook returns the result of this function, the value passed will
26 // be used in place of the original function's return value, and the original
27 // function will NOT be executed.
28 exports.preempt = function(value) {
29 return new HookerPreempt(value);
30 };
31
32 // When a pre-hook returns the result of this function, the context and
33 // arguments passed will be applied into the original function.
34 exports.filter = function(context, args) {
35 return new HookerFilter(context, args);
36 };
37
38 // Execute callback(s) for properties of the specified object.
39 function forMethods(obj, props, callback) {
40 var prop;
41 if (typeof props === "string") {
42 // A single prop string was passed. Create an array.
43 props = [props];
44 } else if (props == null) {
45 // No props were passed, so iterate over all properties, building an
46 // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so
47 // this has to be done manually.
48 props = [];
49 for (prop in obj) {
50 if (obj.hasOwnProperty(prop)) {
51 props.push(prop);
52 }
53 }
54 }
55 // Execute callback for every method in the props array.
56 var i = props.length;
57 while (i--) {
58 // If the property isn't a function...
59 if (toString.call(obj[props[i]]) !== "[object Function]" ||
60 // ...or the callback returns false...
61 callback(obj, props[i]) === false) {
62 // ...remove it from the props array to be returned.
63 props.splice(i, 1);
64 }
65 }
66 // Return an array of method names for which the callback didn't fail.
67 return props;
68 }
69
70 // Monkey-patch (hook) a method of an object.
71 exports.hook = function(obj, props, options) {
72 // If the props argument was omitted, shuffle the arguments.
73 if (options == null) {
74 options = props;
75 props = null;
76 }
77 // If just a function is passed instead of an options hash, use that as a
78 // pre-hook function.
79 if (typeof options === "function") {
80 options = {pre: options};
81 }
82
83 // Hook the specified method of the object.
84 return forMethods(obj, props, function(obj, prop) {
85 // The original (current) method.
86 var orig = obj[prop];
87 // The new hooked function.
88 function hooked() {
89 var result, origResult, tmp;
90
91 // Get an array of arguments.
92 var args = slice.call(arguments);
93
94 // If passName option is specified, prepend prop to the args array,
95 // passing it as the first argument to any specified hook functions.
96 if (options.passName) {
97 args.unshift(prop);
98 }
99
100 // If a pre-hook function was specified, invoke it in the current
101 // context with the passed-in arguments, and store its result.
102 if (options.pre) {
103 result = options.pre.apply(this, args);
104 }
105
106 if (result instanceof HookerFilter) {
107 // If the pre-hook returned hooker.filter(context, args), invoke the
108 // original function with that context and arguments, and store its
109 // result.
110 origResult = result = orig.apply(result.context, result.args);
111 } else if (result instanceof HookerPreempt) {
112 // If the pre-hook returned hooker.preempt(value) just use the passed
113 // value and don't execute the original function.
114 origResult = result = result.value;
115 } else {
116 // Invoke the original function in the current context with the
117 // passed-in arguments, and store its result.
118 origResult = orig.apply(this, arguments);
119 // If the pre-hook returned hooker.override(value), use the passed
120 // value, otherwise use the original function's result.
121 result = result instanceof HookerOverride ? result.value : origResult;
122 }
123
124 if (options.post) {
125 // If a post-hook function was specified, invoke it in the current
126 // context, passing in the result of the original function as the
127 // first argument, followed by any passed-in arguments.
128 tmp = options.post.apply(this, [origResult].concat(args));
129 if (tmp instanceof HookerOverride) {
130 // If the post-hook returned hooker.override(value), use the passed
131 // value, otherwise use the previously computed result.
132 result = tmp.value;
133 }
134 }
135
136 // Unhook if the "once" option was specified.
137 if (options.once) {
138 exports.unhook(obj, prop);
139 }
140
141 // Return the result!
142 return result;
143 }
144 // Re-define the method.
145 obj[prop] = hooked;
146 // Fail if the function couldn't be hooked.
147 if (obj[prop] !== hooked) { return false; }
148 // Store a reference to the original method as a property on the new one.
149 obj[prop]._orig = orig;
150 });
151 };
152
153 // Get a reference to the original method from a hooked function.
154 exports.orig = function(obj, prop) {
155 return obj[prop]._orig;
156 };
157
158 // Un-monkey-patch (unhook) a method of an object.
159 exports.unhook = function(obj, props) {
160 return forMethods(obj, props, function(obj, prop) {
161 // Get a reference to the original method, if it exists.
162 var orig = exports.orig(obj, prop);
163 // If there's no original method, it can't be unhooked, so fail.
164 if (!orig) { return false; }
165 // Unhook the method.
166 obj[prop] = orig;
167 });
168 };
169}(typeof exports === "object" && exports || this));