vapir-firefox 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ require 'vapir-firefox/firefox_socket/base'
2
+
3
+ # A JsshSocket represents a connection to Firefox over a socket opened to the JSSH extension.
4
+ class JsshSocket < FirefoxSocket
5
+ @configuration_parent = FirefoxSocket.config
6
+ config.update_hash({
7
+ :port => 9997,
8
+ })
9
+
10
+ # returns an array of command line flags that should be used to invoke firefox for jssh
11
+ def self.command_line_flags(options={})
12
+ options = config.defined_hash.merge(options)
13
+ ['-jssh', '-jssh-port', options['port']]
14
+ end
15
+
16
+ def eat_welcome_message
17
+ @prompt="\n> "
18
+ welcome="Welcome to the Mozilla JavaScript Shell!\n"
19
+ read=read_value
20
+ if !read
21
+ @expecting_extra_maybe=true
22
+ raise FirefoxSocketUnableToStart, "Something went wrong initializing - no response"
23
+ elsif read != welcome
24
+ @expecting_extra_maybe=true
25
+ raise FirefoxSocketUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}"
26
+ end
27
+ end
28
+ def initialize_environment
29
+ # set up objects that are needed: nativeJSON_encode_length, VapirTemp, and Vapir
30
+ ret=send_and_read(%Q((function()
31
+ { nativeJSON_encode_length=function(object)
32
+ { var encoded=JSON.stringify(object);
33
+ return encoded.length.toString()+"\\n"+encoded;
34
+ }
35
+ VapirTemp = {};
36
+ Vapir = {};
37
+ return 'done!';
38
+ })()))
39
+ if ret != "done!"
40
+ @expecting_extra_maybe=true
41
+ raise FirefoxSocketError, "Something went wrong initializing environment - message #{ret.inspect}"
42
+ end
43
+ end
44
+
45
+ # returns a JavascriptObject representing the return value of JSSH's builtin getWindows() function.
46
+ def getWindows
47
+ root.getWindows
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ require 'vapir-firefox/firefox_socket/base'
2
+
3
+ # A MozreplSocket represents a connection to Firefox over a socket opened to the MozRepl extension.
4
+ class MozreplSocket < FirefoxSocket
5
+ @configuration_parent = FirefoxSocket.config
6
+ config.update_hash({
7
+ :port => 4242,
8
+ })
9
+
10
+ # returns an array of command line flags that should be used to invoke firefox for mozrepl
11
+ def self.command_line_flags(options={})
12
+ options = config.defined_hash.merge(options)
13
+ ['-repl', options['port']]
14
+ end
15
+
16
+ def eat_welcome_message
17
+ read=read_value
18
+ if !read
19
+ @expecting_extra_maybe=true
20
+ raise FirefoxSocketUnableToStart, "Something went wrong initializing - no response"
21
+ elsif read !~ /Welcome to MozRepl/
22
+ @expecting_extra_maybe=true
23
+ raise FirefoxSocketUnableToStart, "Something went wrong initializing - message #{read.inspect}"
24
+ end
25
+ if read =~ /yours will be named "([^"]+)"/
26
+ @replname=$1
27
+ else
28
+ @replname='repl'
29
+ end
30
+ @prompt="#{@replname}> "
31
+ @expecting_prompt = read !~ /#{Regexp.escape(@prompt)}\z/
32
+ end
33
+ def initialize_environment
34
+ # change the prompt mode to something less decorative
35
+ #send_and_read("#{@replname}.home()")
36
+ send_and_read("#{@replname}.setenv('printPrompt', false)")
37
+ @prompt="\n"
38
+ @expecting_prompt=false
39
+ send_and_read("#{@replname}.setenv('inputMode', 'multiline')")
40
+ @input_terminator = "--end-remote-input\n"
41
+
42
+ # set up objects that are needed: nativeJSON_encode_length, VapirTemp, and Vapir
43
+ ret=send_and_read(%Q((function(the_repl, context)
44
+ { context.nativeJSON_encode_length=function(object)
45
+ { var encoded=JSON.stringify(object);
46
+ the_repl.print(encoded.length.toString()+"\\n"+encoded, false);
47
+ }
48
+ context.VapirTemp = {};
49
+ context.Vapir = {};
50
+ return 'done!';
51
+ })(#{@replname}, this)))
52
+ if ret !~ /done!/
53
+ @expecting_extra_maybe=true
54
+ raise FirefoxSocketError, "Something went wrong initializing environment - message #{ret.inspect}"
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,736 @@
1
+ # represents a javascript object in ruby.
2
+ class JavascriptObject
3
+ # the reference to the javascript object this JavascriptObject represents
4
+ attr_reader :ref
5
+ # the FirefoxSocket this JavascriptObject is on
6
+ attr_reader :firefox_socket
7
+ # whether this represents the result of a function call (if it does, then FirefoxSocket#typeof won't be called on it)
8
+ attr_reader :function_result
9
+ # this tracks the origins of this object - what calls were made along the way to get it.
10
+ attr_reader :debug_name
11
+ # :stopdoc:
12
+ # def logger
13
+ # firefox_socket.logger
14
+ # end
15
+
16
+ # :startdoc:
17
+
18
+ public
19
+ # initializes a JavascriptObject with a string of javascript containing a reference to
20
+ # the object, and a FirefoxSocket that the object is defined on.
21
+ def initialize(ref, firefox_socket, other={})
22
+ other={:debug_name => ref, :function_result => false}.merge(other)
23
+ raise ArgumentError, "Empty object reference!" if !ref || ref==''
24
+ raise ArgumentError, "Reference must be a string - got #{ref.inspect} (#{ref.class.name})" unless ref.is_a?(String)
25
+ raise ArgumentError, "Not given a FirefoxSocket, instead given #{firefox_socket.inspect} (#{firefox_socket.class.name})" unless firefox_socket.is_a?(FirefoxSocket)
26
+ @ref=ref
27
+ @firefox_socket=firefox_socket
28
+ @debug_name=other[:debug_name]
29
+ @function_result=other[:function_result]
30
+ # logger.info { "#{self.class} initialized: #{debug_name} (type #{type})" }
31
+ end
32
+
33
+ # returns the value, via FirefoxSocket#value_json
34
+ def val
35
+ firefox_socket.value_json(ref, :error_on_undefined => !function_result)
36
+ end
37
+
38
+ # whether JavascriptObject shall try to dynamically define methods on initialization, using
39
+ # #define_methods! default is false.
40
+ def self.always_define_methods
41
+ unless class_variable_defined?('@@always_define_methods')
42
+ # if not defined, set the default.
43
+ @@always_define_methods=false
44
+ end
45
+ @@always_define_methods
46
+ end
47
+ # set whether JavascriptObject shall try to dynamically define methods in #val_or_object, using
48
+ # #define_methods!
49
+ #
50
+ # I find this useful to set to true in irb, for tab-completion of methods. it may cause
51
+ # operations to be considerably slower, however.
52
+ #
53
+ # for always setting this in irb, I set this beforehand, overriding the default,
54
+ # by including in my .irbrc the following (which doesn't require this file to be
55
+ # required):
56
+ #
57
+ # class JavascriptObject
58
+ # @@always_define_methods=true
59
+ # end
60
+ def self.always_define_methods=(val)
61
+ @@always_define_methods = val
62
+ end
63
+
64
+ # returns the value just as a string with no attempt to deal with type using json. via FirefoxSocket#value
65
+ #
66
+ # note that this can be slow if it evaluates to a blank string. for example, if ref is just ""
67
+ # then FirefoxSocket#value will wait DEFAULT_SOCKET_TIMEOUT seconds for data that is not to come.
68
+ # this also happens with functions that return undefined. if ref="function(){do_some_stuff;}"
69
+ # (with no return), it will also wait DEFAULT_SOCKET_TIMEOUT.
70
+ def val_str
71
+ firefox_socket.value(ref)
72
+ end
73
+
74
+ # returns javascript typeof this object
75
+ def type
76
+ if function_result # don't get type for function results, causes function evaluations when you probably didn't want that.
77
+ nil
78
+ else
79
+ # logger.add(-1) { "retrieving type for #{debug_name}" }
80
+ @type||= firefox_socket.typeof(ref)
81
+ end
82
+ end
83
+
84
+ # calls the javascript instanceof operator on this object and the given interface (expected to
85
+ # be a JavascriptObject) note that the javascript instanceof operator is not to be confused with
86
+ # ruby's #instance_of? method - this takes a javascript interface; #instance_of? takes a ruby
87
+ # module.
88
+ #
89
+ # example:
90
+ # window.instanceof(window.firefox_socket.Components.interfaces.nsIDOMChromeWindow)
91
+ # => true
92
+ def instanceof(interface)
93
+ firefox_socket.instanceof(self.ref, interface.ref)
94
+ end
95
+ # returns an array of interfaces which this object is an instance of. this is achieved
96
+ # by looping over each value of Components.interfaces (see https://developer.mozilla.org/en/Components.interfaces )
97
+ # and calling the #instanceof operator with this and the interface.
98
+ #
99
+ # this may be rather slow.
100
+ def implemented_interfaces
101
+ firefox_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
102
+ list << interface if (instanceof(interface) rescue false)
103
+ list
104
+ end
105
+ end
106
+
107
+ # returns the type of object that is reported by the javascript toString() method, which
108
+ # returns such as "[object Object]" or "[object XPCNativeWrapper [object HTMLDocument]]"
109
+ # This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
110
+ # Raises an error if this JavascriptObject points to something other than a javascript 'object'
111
+ # type ('function' or 'number' or whatever)
112
+ #
113
+ # this isn't used, doesn't seem useful, and may go away in the future.
114
+ def object_type
115
+ @object_type ||= begin
116
+ case type
117
+ when 'object'
118
+ self.toString! =~ /\A\[object\s+(.*)\]\Z/
119
+ $1
120
+ else
121
+ raise FirefoxSocketJavascriptError, "Type is #{type}, not object"
122
+ end
123
+ end
124
+ end
125
+
126
+ # checks the type of this object, and if it is a type that can be simply converted to a ruby
127
+ # object via json, returns the ruby value. that occurs if the type is one of:
128
+ #
129
+ # 'boolean','number','string','null'
130
+ #
131
+ # otherwise - if the type is something else (probably 'function' or 'object'; or maybe something else)
132
+ # then this JavascriptObject is returned.
133
+ #
134
+ # if the object this refers to is undefined in javascript, then behavor depends on the options
135
+ # hash. if :error_on_undefined is true, then nil is returned; otherwise FirefoxSocketUndefinedValueError
136
+ # is raised.
137
+ #
138
+ # if this is a function result, this will store the result in a temporary location (thereby
139
+ # calling the function to acquire the result) before making the above decision.
140
+ #
141
+ # this method also calls #define_methods! on this if JavascriptObject.always_define_methods is true.
142
+ # this can be overridden in the options hash using the :define_methods key (true or false).
143
+ def val_or_object(options={})
144
+ options={:error_on_undefined=>true, :define_methods => self.class.always_define_methods}.merge(options)
145
+ if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
146
+ store_rand_object_key(firefox_socket.temp_object).val_or_object(options.merge(:error_on_undefined => false))
147
+ else
148
+ # if we don't know our type, stick everything into one call to avoid multiple socket calls
149
+ types_to_convert = ['boolean', 'number', 'string', 'null']
150
+ if !@type
151
+ type_and_value = firefox_socket.value_json(%Q((function()
152
+ { var result={};
153
+ var object;
154
+ try
155
+ { result.type=(function(object){ return (object===null) ? 'null' : (typeof object); })(object=#{ref});
156
+ }
157
+ catch(e)
158
+ { if(e.name=='ReferenceError')
159
+ { result.type='undefined';
160
+ }
161
+ else
162
+ { throw(e);
163
+ };
164
+ }
165
+ if($A(#{FirefoxSocket.to_javascript(types_to_convert)}).include(result.type))
166
+ { result.value = object;
167
+ }
168
+ return result;
169
+ })()))
170
+ @type = type_and_value['type']
171
+ end
172
+
173
+ if type=='undefined'
174
+ if !options[:error_on_undefined]
175
+ nil
176
+ else
177
+ raise FirefoxSocketUndefinedValueError, "undefined expression represented by #{self.inspect} (javascript reference is #{@ref})"
178
+ end
179
+ elsif types_to_convert.include?(type)
180
+ if type_and_value
181
+ raise "internal error - type_and_value had no value key; was #{type_and_value.inspect}" unless type_and_value.key?('value') # this shouldn't happen
182
+ type_and_value['value']
183
+ else
184
+ val
185
+ end
186
+ else # 'function','object', or anything else
187
+ if options[:define_methods] && type=='object'
188
+ define_methods!
189
+ end
190
+ self
191
+ end
192
+ end
193
+ end
194
+ # does the work of #method_missing to determine whether to call a function what to return based
195
+ # on the defined behavior of the given suffix. see #method_missing for more. information.
196
+ def assign_or_call_or_val_or_object_by_suffix(suffix, *args)
197
+ if suffix=='='
198
+ assign(*args)
199
+ else
200
+ obj = if !args.empty? || type=='function'
201
+ pass(*args)
202
+ else
203
+ self
204
+ end
205
+ case suffix
206
+ when nil
207
+ obj.val_or_object
208
+ when '?'
209
+ obj.val_or_object(:error_on_undefined => false)
210
+ when '!'
211
+ obj.val
212
+ else
213
+ raise ArgumentError, "suffix should be one of: nil, '?', '!', '='; got: #{suffix.inspect}"
214
+ end
215
+ end
216
+ end
217
+
218
+ # returns a JavascriptObject representing the given attribute. Checks the type, and if it is a
219
+ # function, calls the function with any arguments given (which are converted to javascript)
220
+ # and returns the return value of the function (or nil if the function returns undefined).
221
+ #
222
+ # If the attribute is undefined, raises an error (if you want an attribute even if it's
223
+ # undefined, use #invoke? or #attr).
224
+ def invoke(attribute, *args)
225
+ attr(attribute).assign_or_call_or_val_or_object_by_suffix(nil, *args)
226
+ end
227
+ # same as #invoke, but returns nil for undefined attributes rather than raising an
228
+ # error.
229
+ def invoke?(attribute, *args)
230
+ attr(attribute).assign_or_call_or_val_or_object_by_suffix('?', *args)
231
+ end
232
+
233
+ # returns a JavascriptObject referencing the given attribute of this object
234
+ def attr(attribute)
235
+ unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
236
+ raise FirefoxSocketSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
237
+ end
238
+ JavascriptObject.new("#{ref}.#{attribute}", firefox_socket, :debug_name => "#{debug_name}.#{attribute}")
239
+ end
240
+
241
+ # assigns the given ruby value (converted to javascript) to the reference
242
+ # for this object. returns self.
243
+ def assign(val)
244
+ @debug_name="(#{debug_name}=#{val.is_a?(JavascriptObject) ? val.debug_name : FirefoxSocket.to_javascript(val)})"
245
+ result=assign_expr(FirefoxSocket.to_javascript(val))
246
+ # logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
247
+ result
248
+ end
249
+ # assigns the given javascript expression (string) to the reference for this object
250
+ def assign_expr(javascript_expression)
251
+ # don't want to use FirefoxSocket#assign_json because converting the result of the assignment
252
+ # (that is, the expression assigned) to json is error-prone and we don't really care about the
253
+ # result.
254
+ #
255
+ # don't want to use FirefoxSocket#assign because the result can be blank and cause send_and_read
256
+ # to wait for data that's not coming - also using a json function is better because it catches
257
+ # errors much more elegantly.
258
+ #
259
+ # so, wrap it in its own function, whose return value is unrelated to the actual assignment.
260
+ # for efficiency, it returns the type (it used to just return nil and uncache type for later
261
+ # retrieval), as this is desirable information, and since there's no other information we
262
+ # particularly desire from this call, that is a good thing to return.
263
+ @type=firefox_socket.value_json("(function(val){#{ref}=val; return (val===null) ? 'null' : (typeof val);}(#{javascript_expression}))")
264
+ self
265
+ end
266
+
267
+ # returns a JavascriptObject for the result of calling the function represented by this object, passing
268
+ # the given arguments, which are converted to javascript. if this is not a function, javascript will raise an error.
269
+ def pass(*args)
270
+ JavascriptObject.new("#{ref}(#{args.map{|arg| FirefoxSocket.to_javascript(arg)}.join(', ')})", firefox_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JavascriptObject) ? arg.debug_name : FirefoxSocket.to_javascript(arg)}.join(', ')})")
271
+ end
272
+
273
+ # returns the value (via FirefoxSocket#value_json) or a JavascriptObject (see #val_or_object) of the return
274
+ # value of this function (assumes this object is a function) passing it the given arguments (which
275
+ # are converted to javascript).
276
+ #
277
+ # simply, it just calls self.pass(*args).val_or_object
278
+ def call(*args)
279
+ pass(*args).val_or_object
280
+ end
281
+
282
+ # assuming the javascript object represented is a constructor, this returns a new
283
+ # instance passing the given arguments.
284
+ #
285
+ # date_class = firefox_socket.object('Date')
286
+ # => #<JavascriptObject:0x0118eee8 type=function, debug_name=Date>
287
+ # date = date_class.new
288
+ # => #<JavascriptObject:0x01188a84 type=object, debug_name=new Date()>
289
+ # date.getFullYear
290
+ # => 2010
291
+ # date_class.new('october 4, 1978').getFullYear
292
+ # => 1978
293
+ def new(*args)
294
+ JavascriptObject.new("new #{ref}", firefox_socket, :debug_name => "new #{debug_name}").call(*args)
295
+ end
296
+
297
+ # sets the given javascript variable to this object, and returns a JavascriptObject referring
298
+ # to the variable.
299
+ #
300
+ # >> foo=document.getElementById('guser').store('foo')
301
+ # => #<JavascriptObject:0x2dff870 @ref="foo" ...>
302
+ # >> foo.tagName
303
+ # => "DIV"
304
+ #
305
+ # the second argument is only used internally and shouldn't be used.
306
+ def store(js_variable, somewhere_meaningful=true)
307
+ stored=JavascriptObject.new(js_variable, firefox_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
308
+ stored.assign_expr(self.ref)
309
+ stored
310
+ end
311
+
312
+ private
313
+ # takes a block which, when yielded a random key, should result in a random reference. this checks
314
+ # that the reference is not already in use and stores this object in that reference, and returns
315
+ # a JavascriptObject referring to the stored object.
316
+ def store_rand_named(&name_proc)
317
+ base=36
318
+ length=32
319
+ begin
320
+ name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
321
+ end #while JavascriptObject.new(name,firefox_socket).type!='undefined'
322
+ # okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
323
+ store(name, false)
324
+ end
325
+ public
326
+
327
+ # stores this object in a random key of the given object and returns the stored object.
328
+ def store_rand_object_key(object)
329
+ raise ArgumentError("Object is not a JavascriptObject: got #{object.inspect}") unless object.is_a?(JavascriptObject)
330
+ store_rand_named do |r|
331
+ object.sub(r).ref
332
+ end
333
+ end
334
+
335
+ # stores this object in a random key of the designated temporary object for this socket and returns the stored object.
336
+ def store_rand_temp
337
+ store_rand_object_key(firefox_socket.temp_object)
338
+ end
339
+
340
+ # returns a JavascriptObject referring to a subscript of this object, specified as a ruby object converted to
341
+ # javascript.
342
+ #
343
+ # similar to [], but [] calls #val_or_object; this always returns a JavascriptObject.
344
+ def sub(key)
345
+ JavascriptObject.new("#{ref}[#{FirefoxSocket.to_javascript(key)}]", firefox_socket, :debug_name => "#{debug_name}[#{key.is_a?(JavascriptObject) ? key.debug_name : FirefoxSocket.to_javascript(key)}]")
346
+ end
347
+
348
+ # returns a JavascriptObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
349
+ #
350
+ # subscript is specified as ruby (converted to javascript).
351
+ def [](key)
352
+ sub(key).val_or_object(:error_on_undefined => false)
353
+ end
354
+
355
+ # assigns the given ruby value (which is converted to javascript) to the given subscript
356
+ # (the key is also converted to javascript).
357
+ def []=(key, value)
358
+ self.sub(key).assign(value)
359
+ end
360
+
361
+ # calls a binary operator (in javascript) with self and another operand.
362
+ #
363
+ # the operator should be string of javascript; the operand will be converted to javascript.
364
+ def binary_operator(operator, operand)
365
+ JavascriptObject.new("(#{ref}#{operator}#{FirefoxSocket.to_javascript(operand)})", firefox_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JavascriptObject) ? operand.debug_name : FirefoxSocket.to_javascript(operand)})").val_or_object
366
+ end
367
+ # addition, using the + operator in javascript
368
+ def +(operand)
369
+ binary_operator('+', operand)
370
+ end
371
+ # subtraction, using the - operator in javascript
372
+ def -(operand)
373
+ binary_operator('-', operand)
374
+ end
375
+ # division, using the / operator in javascript
376
+ def /(operand)
377
+ binary_operator('/', operand)
378
+ end
379
+ # multiplication, using the * operator in javascript
380
+ def *(operand)
381
+ binary_operator('*', operand)
382
+ end
383
+ # modulus, using the % operator in javascript
384
+ def %(operand)
385
+ binary_operator('%', operand)
386
+ end
387
+ # returns true if the javascript object represented by this is equal to the given operand.
388
+ def ==(operand)
389
+ operand.is_a?(JavascriptObject) && binary_operator('==', operand)
390
+ end
391
+ # javascript triple-equals (===) operator. very different from ruby's tripl-equals operator -
392
+ # in javascript this means "really really equal"; in ruby it means "sort of equal-ish"
393
+ def triple_equals(operand)
394
+ operand.is_a?(JavascriptObject) && binary_operator('===', operand)
395
+ end
396
+ # inequality, using the > operator in javascript
397
+ def >(operand)
398
+ binary_operator('>', operand)
399
+ end
400
+ # inequality, using the < operator in javascript
401
+ def <(operand)
402
+ binary_operator('<', operand)
403
+ end
404
+ # inequality, using the >= operator in javascript
405
+ def >=(operand)
406
+ binary_operator('>=', operand)
407
+ end
408
+ # inequality, using the <= operator in javascript
409
+ def <=(operand)
410
+ binary_operator('<=', operand)
411
+ end
412
+
413
+ # method_missing handles unknown method calls in a way that makes it possible to write
414
+ # javascript-like syntax in ruby, to some extent.
415
+ #
416
+ # method_missing checks the attribute of the represented javascript object with with the name of the given method. if that
417
+ # attribute refers to a function, then that function is called with any given arguments
418
+ # (like #invoke does). If that attribute is undefined, an error will be raised, unless a '?'
419
+ # suffix is used (see below).
420
+ #
421
+ # method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
422
+ # special characters, only alphanumeric/underscores, starting with alpha or underscore - with
423
+ # the exception of three special behaviors:
424
+ #
425
+ # If the method ends with an equals sign (=), it does assignment - it calls #assign on the given
426
+ # attribute, with the given (single) argument, to do the assignment and returns the assigned
427
+ # value.
428
+ #
429
+ # If the method ends with a bang (!), then it will attempt to get the value of the reference,
430
+ # using JavascriptObject#val, which converts the javascript to json and then to ruby. For simple types
431
+ # (null, string, boolean, number), this is what gets returned anyway. With other types (usually
432
+ # the 'object' type), attempting to convert to json can raise errors or cause infinite
433
+ # recursion, so is not attempted. but if you have an object or an array that you know you can
434
+ # json-ize, you can use ! to force that.
435
+ #
436
+ # If the method ends with a question mark (?), then if the attribute is undefined, no error is
437
+ # raised (as usually happens) - instead nil is just returned.
438
+ #
439
+ # otherwise, method_missing behaves like #invoke, and returns a JavascriptObject, a string, a boolean,
440
+ # a number, or null.
441
+ #
442
+ # Since method_missing returns a JavascriptObject for javascript objects, this means that you can
443
+ # string together method_missings and the result looks rather like javascript.
444
+ #--
445
+ # $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
446
+ # methods to arrays and hashes - see http://www.prototypejs.org/
447
+ # You can use these methods with method_missing just like any other:
448
+ #
449
+ # >> js_hash=firefox_socket.object('$H')
450
+ # => #<JavascriptObject:0x2beb598 @ref="$H" ...>
451
+ # >> js_arr=firefox_socket.object('$A')
452
+ # => #<JavascriptObject:0x2be40e0 @ref="$A" ...>
453
+ #
454
+ # >> js_arr.call(document.body.childNodes).pluck! :tagName
455
+ # => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
456
+ # >> js_arr.call(document.body.childNodes).pluck! :id
457
+ # => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
458
+ # >> js_hash.call(document.getElementById('tbd')).keys!
459
+ # => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", ...]
460
+ def method_missing(method, *args)
461
+ method=method.to_s
462
+ if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
463
+ method = $1
464
+ suffix = $2
465
+ attr(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
466
+ else
467
+ # don't deal with any special character crap
468
+ super
469
+ end
470
+ end
471
+ # calls define_method for each key of this object as a hash. useful for tab-completing attributes
472
+ # in irb, mostly.
473
+ def define_methods! # :nodoc:
474
+ metaclass=(class << self; self; end)
475
+ keys=firefox_socket.object("function(obj) { var keys=[]; for(var key in obj) { keys.push(key); } return keys; }").pass(self).val
476
+
477
+ keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
478
+ metaclass.send(:define_method, key) do |*args|
479
+ invoke(key, *args)
480
+ end
481
+ end
482
+ end
483
+ # returns true if this object responds to the given method (that is, it's a defined ruby method)
484
+ # or if #method_missing will handle it
485
+ def respond_to?(method, include_private = false)
486
+ super || object_respond_to?(method)
487
+ end
488
+ # returns true if the javascript object this represents responds to the given method. this does not pay attention
489
+ # to any defined ruby methods, just javascript.
490
+ def object_respond_to?(method)
491
+ method=method.to_s
492
+ if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
493
+ method = $1
494
+ suffix = $2
495
+ else # don't deal with any special character crap
496
+ return false
497
+ end
498
+
499
+ if self.type=='undefined'
500
+ return false
501
+ elsif suffix=='='
502
+ if self.type=='object'
503
+ return true # yeah, you can generally assign attributes to objects
504
+ else
505
+ return false # no, you can't generally assign attributes to (boolean, number, string, null)
506
+ end
507
+ else
508
+ attr=attr(method)
509
+ return attr.type!='undefined'
510
+ end
511
+ end
512
+
513
+ # undefine Object#id, and... anything else I think of that needs undef'ing in the future
514
+ [:id, :display].each do |method_name|
515
+ if method_defined?(method_name)
516
+ undef_method(method_name)
517
+ end
518
+ end
519
+
520
+ # returns this object passed through the $A function of the prototype javascript library.
521
+ def to_js_array
522
+ firefox_socket.object('$A').call(self)
523
+ end
524
+ # returns this object passed through the $H function of the prototype javascript library.
525
+ def to_js_hash
526
+ firefox_socket.object('$H').call(self)
527
+ end
528
+ # returns this object passed through a javascript function which copies each key onto a blank object and rescues any errors.
529
+ def to_js_hash_safe
530
+ firefox_socket.object('$_H').call(self)
531
+ end
532
+ # returns a JavascriptArray representing this object
533
+ def to_array
534
+ JavascriptArray.new(self.ref, self.firefox_socket, :debug_name => debug_name)
535
+ end
536
+ # returns a JavascriptHash representing this object
537
+ def to_hash
538
+ JavascriptHash.new(self.ref, self.firefox_socket, :debug_name => debug_name)
539
+ end
540
+ # returns a JavascriptDOMNode representing this object
541
+ def to_dom
542
+ JavascriptDOMNode.new(self.ref, self.firefox_socket, :debug_name => debug_name)
543
+ end
544
+ # returns a JavascriptFunction representing this object
545
+ def to_function
546
+ JavascriptFunction.new(self.ref, self.firefox_socket, :debug_name => debug_name)
547
+ end
548
+
549
+ # returns a ruby Hash. each key/value pair of this object
550
+ # is represented in the returned hash.
551
+ #
552
+ # if an error is encountered trying to access the value for an attribute, then in the
553
+ # returned hash, that attribute is set to the error that was encountered rather than
554
+ # the actual value (since the value wasn't successfully retrieved).
555
+ #
556
+ # options may be specified. the only option currently supported is:
557
+ # * :recurse => a number or nil. if it's a number, then this will recurse to that
558
+ # depth. If it's nil, this won't recurse at all.
559
+ #
560
+ # below the specified recursion level, this will return this JavascriptObject rather than recursing
561
+ # down into it.
562
+ #
563
+ # this function isn't expected to raise any errors, since encountered errors are set as
564
+ # attribute values.
565
+ def to_ruby_hash(options={})
566
+ options={:recurse => 1}.merge(options)
567
+ return self if !options[:recurse] || options[:recurse]==0
568
+ return self if self.type!='object'
569
+ next_options=options.merge(:recurse => options[:recurse]-1)
570
+ begin
571
+ keys=self.to_hash.keys
572
+ rescue FirefoxSocketError
573
+ return self
574
+ end
575
+ keys.inject({}) do |hash, key|
576
+ val=begin
577
+ self[key]
578
+ rescue FirefoxSocketError
579
+ $!
580
+ end
581
+ hash[key]=if val.is_a?(JavascriptObject)
582
+ val.to_ruby_hash(next_options)
583
+ else
584
+ val
585
+ end
586
+ hash
587
+ end
588
+ end
589
+
590
+ # returns an Array in which each element is the #val_or_Object of each element of this javascript array.
591
+ def to_ruby_array
592
+ self.to_array.to_a
593
+ end
594
+
595
+ # represents this javascript object in one line, displaying the type and debug name.
596
+ def inspect
597
+ "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
598
+ end
599
+ def pretty_print(pp) # :nodoc:
600
+ pp.object_address_group(self) do
601
+ pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
602
+ pp.breakable ' '
603
+ pp.group(0) do
604
+ pp.text attr.to_s
605
+ pp.text ': '
606
+ #pp.breakable
607
+ pp.text send(attr)
608
+ end
609
+ end
610
+ end
611
+ end
612
+ end
613
+
614
+ # represents a node on the DOM. not substantially from JavascriptObject, but #inspect
615
+ # is more informative, and #dump is defined for extensive debug info.
616
+ #
617
+ # This class is mostly useful for debug, not used anywhere in production at the moment.
618
+ class JavascriptDOMNode < JavascriptObject
619
+ def inspect_stuff # :nodoc:
620
+ [:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map do |attrn|
621
+ attr=attr(attrn)
622
+ if ['undefined','null'].include?(attr.type)
623
+ nil
624
+ else
625
+ [attrn, attr.val_or_object(:error_on_undefined => false)]
626
+ end
627
+ end.compact
628
+ end
629
+ # returns a string with a bunch of information about this dom node
630
+ def inspect
631
+ "\#<#{self.class.name} #{inspect_stuff.map{|(k,v)| "#{k}=#{v.inspect}"}.join(', ')}>"
632
+ end
633
+ def pretty_print(pp) # :nodoc:
634
+ pp.object_address_group(self) do
635
+ pp.seplist(inspect_stuff, lambda { pp.text ',' }) do |attr_val|
636
+ pp.breakable ' '
637
+ pp.group(0) do
638
+ pp.text attr_val.first.to_s
639
+ pp.text ': '
640
+ #pp.breakable
641
+ pp.text attr_val.last.inspect
642
+ end
643
+ end
644
+ end
645
+ end
646
+ # returns a string (most useful when written to STDOUT or to a file) consisting of this dom node
647
+ # and its child nodes, recursively. each node is one line and depth is indicated by spacing.
648
+ #
649
+ # call #dump(:recurse => n) to recurse down only n levels. default is to recurse all the way down the dom tree.
650
+ def dump(options={})
651
+ options={:recurse => nil, :level => 0}.merge(options)
652
+ next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
653
+ result=(" "*options[:level]*2)+self.inspect+"\n"
654
+ if options[:recurse]==0
655
+ result+=(" "*next_options[:level]*2)+"...\n"
656
+ else
657
+ self.childNodes.to_array.each do |child|
658
+ result+=child.to_dom.dump(next_options)
659
+ end
660
+ end
661
+ result
662
+ end
663
+ end
664
+
665
+ # this class represents a javascript array - that is, a javascript object that has a 'length'
666
+ # attribute which is a non-negative integer, and returns elements at each subscript from 0
667
+ # to less than than that length.
668
+ class JavascriptArray < JavascriptObject
669
+ # yields the element at each subscript of this javascript array, from 0 to self.length.
670
+ def each
671
+ length=self.length
672
+ raise FirefoxSocketJavascriptError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
673
+ for i in 0...length
674
+ element=self[i]
675
+ if element.is_a?(JavascriptObject)
676
+ # yield a more permanent reference than the array subscript
677
+ element=element.store_rand_temp
678
+ end
679
+ yield element
680
+ end
681
+ end
682
+ include Enumerable
683
+ end
684
+
685
+ # this class represents a hash, or 'object' type in javascript.
686
+ class JavascriptHash < JavascriptObject
687
+ # returns an array of keys of this javascript object
688
+ def keys
689
+ @keys=firefox_socket.call_function(:obj => self){ "var keys=[]; for(var key in obj) { keys.push(key); } return keys;" }.val
690
+ end
691
+ # returns whether the given key is a defined key of this javascript object
692
+ def key?(key)
693
+ firefox_socket.call_function(:obj => self, :key => key){ "return key in obj;" }
694
+ end
695
+ # yields each key and value
696
+ def each(&block) # :yields: key, value
697
+ keys.each do |key|
698
+ if block.arity==1
699
+ yield [key, self[key]]
700
+ else
701
+ yield key, self[key]
702
+ end
703
+ end
704
+ end
705
+ # yields each key and value for this object
706
+ def each_pair
707
+ each do |key,value|
708
+ yield key,value
709
+ end
710
+ end
711
+
712
+ include Enumerable
713
+ end
714
+
715
+ # represents a javascript function
716
+ class JavascriptFunction < JavascriptObject
717
+ def to_proc
718
+ @the_proc ||= begin
719
+ javascript_function = self
720
+ the_proc = proc do |*args|
721
+ javascript_function.call(*args)
722
+ end
723
+ # this makes it so you can recover the javascript_function from the proc
724
+ # this method returns - js_func.to_proc.javascript_function returns js_func.
725
+ # unfortunately it's not so useful because when you pass the proc as a block,
726
+ # it's reinstantated without the metaclass or instance variable defined here.
727
+ # still, may have some potential use, so leaving this here.
728
+ the_proc_metaclass = (class << the_proc; self; end)
729
+ the_proc_metaclass.send(:define_method, :javascript_function) do
730
+ javascript_function
731
+ end
732
+ the_proc.instance_variable_set('@javascript_function', javascript_function)
733
+ the_proc
734
+ end
735
+ end
736
+ end