vapir-firefox 1.7.2 → 1.8.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.
- data/lib/vapir-firefox/{firefox.rb → browser.rb} +358 -145
- data/lib/vapir-firefox/clear_tracks.rb +50 -0
- data/lib/vapir-firefox/config.rb +17 -0
- data/lib/vapir-firefox/container.rb +67 -1
- data/lib/vapir-firefox/element.rb +62 -38
- data/lib/vapir-firefox/jssh_socket.rb +527 -206
- data/lib/vapir-firefox/page_container.rb +44 -19
- data/lib/vapir-firefox/version.rb +1 -1
- data/lib/vapir-firefox/window.rb +2 -2
- data/lib/vapir-firefox.rb +2 -5
- metadata +11 -32
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'json'
|
2
|
-
require 'active_support'
|
3
2
|
require 'socket'
|
4
3
|
require 'timeout'
|
5
4
|
#require 'logger'
|
6
5
|
|
6
|
+
# :stopdoc:
|
7
7
|
#class LoggerWithCallstack < Logger
|
8
8
|
# class TimeElapsedFormatter < Formatter
|
9
9
|
# def initialize
|
@@ -36,27 +36,27 @@ require 'timeout'
|
|
36
36
|
# end
|
37
37
|
#end
|
38
38
|
|
39
|
-
|
40
|
-
# this is like #to_json, but without the conflicting names between ActiveSupport and JSON gem,
|
41
|
-
# and also for JsshObject (which is a reference; not real json; see the overload in that class)
|
42
|
-
def to_jssh
|
43
|
-
ActiveSupport::JSON.encode(self)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
39
|
+
# :startdoc:
|
47
40
|
|
41
|
+
# base exception class for all exceptions raised from Jssh sockets and objects.
|
48
42
|
class JsshError < StandardError;end
|
49
|
-
# this exception covers all connection errors either on startup or during usage
|
43
|
+
# this exception covers all connection errors either on startup or during usage. often it represents an Errno error such as Errno::ECONNRESET.
|
50
44
|
class JsshConnectionError < JsshError;end
|
51
45
|
# This exception is thrown if we are unable to connect to JSSh.
|
52
46
|
class JsshUnableToStart < JsshConnectionError;end
|
47
|
+
# Represents an error encountered on the javascript side, caught in a try/catch block.
|
53
48
|
class JsshJavascriptError < JsshError
|
54
49
|
attr_accessor :source, :js_err, :lineNumber, :stack, :fileName
|
55
50
|
end
|
51
|
+
# represents a syntax error in javascript.
|
56
52
|
class JsshSyntaxError < JsshJavascriptError;end
|
53
|
+
# raised when a javascript value is expected to be defined but is undefined
|
57
54
|
class JsshUndefinedValueError < JsshJavascriptError;end
|
58
55
|
|
56
|
+
# A JsshSocket represents a connection to Firefox over a socket opened to the JSSH extension. It
|
57
|
+
# does the work of interacting with the socket and translating ruby values to javascript and back.
|
59
58
|
class JsshSocket
|
59
|
+
# :stopdoc:
|
60
60
|
# def self.logger
|
61
61
|
# @@logger||=begin
|
62
62
|
# logfile=File.open('c:/tmp/jssh_log.txt', File::WRONLY|File::TRUNC|File::CREAT)
|
@@ -73,24 +73,37 @@ class JsshSocket
|
|
73
73
|
|
74
74
|
PROMPT="\n> "
|
75
75
|
|
76
|
-
# IP Address of the machine where the script is to be executed. Default to localhost.
|
77
|
-
@@default_jssh_ip = "127.0.0.1"
|
78
|
-
@@default_jssh_port = 9997
|
79
76
|
PrototypeFile=File.join(File.dirname(__FILE__), "prototype.functional.js")
|
77
|
+
# :startdoc:
|
78
|
+
|
79
|
+
# default IP Address of the machine where the script is to be executed. Default to localhost.
|
80
|
+
DEFAULT_IP = "127.0.0.1"
|
81
|
+
# default port to connect to.
|
82
|
+
DEFAULT_PORT = 9997
|
80
83
|
|
84
|
+
# maximum time JsshSocket waits for a value to be sent before giving up
|
81
85
|
DEFAULT_SOCKET_TIMEOUT=64
|
86
|
+
# maximum time JsshSocket will wait for additional reads on a socket that is actively sending
|
82
87
|
SHORT_SOCKET_TIMEOUT=(2**-2).to_f
|
88
|
+
# the number of bytes to read from the socket at a time
|
89
|
+
READ_SIZE=4096
|
83
90
|
|
84
|
-
|
91
|
+
# the IP to which this socket is connected
|
92
|
+
attr_reader :ip
|
93
|
+
# the port on which this socket is connected
|
94
|
+
attr_reader :port
|
95
|
+
# whether the prototye javascript library is loaded
|
96
|
+
attr_reader :prototype
|
85
97
|
|
86
98
|
# Connects a new socket to jssh
|
99
|
+
#
|
87
100
|
# Takes options:
|
88
101
|
# * :jssh_ip => the ip to connect to, default 127.0.0.1
|
89
102
|
# * :jssh_port => the port to connect to, default 9997
|
90
103
|
# * :send_prototype => true|false, whether to load and send the Prototype library (the functional programming part of it anyway, and JSON bits)
|
91
104
|
def initialize(options={})
|
92
|
-
@ip=options[:jssh_ip] ||
|
93
|
-
@port=options[:jssh_port] ||
|
105
|
+
@ip=options[:jssh_ip] || DEFAULT_IP
|
106
|
+
@port=options[:jssh_port] || DEFAULT_PORT
|
94
107
|
@prototype=options.key?(:send_prototype) ? options[:send_prototype] : true
|
95
108
|
begin
|
96
109
|
@socket = TCPSocket::new(@ip, @port)
|
@@ -130,7 +143,7 @@ class JsshSocket
|
|
130
143
|
@expecting_extra_maybe=true
|
131
144
|
raise JsshError, "Something went wrong initializing native JSON - message #{ret.inspect}"
|
132
145
|
end
|
133
|
-
|
146
|
+
root.JsshTemp={}
|
134
147
|
end
|
135
148
|
|
136
149
|
private
|
@@ -151,22 +164,25 @@ class JsshSocket
|
|
151
164
|
# perfectly possible that a string the same as the prompt is part of actual data. (even stripping it from the
|
152
165
|
# ends of the string is not entirely certain; data could have it at the ends too, but that's the best that can
|
153
166
|
# be done.) so, read_value should be called after every line, or you can end up with stuff like:
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
167
|
+
#
|
168
|
+
# >> @socket.send "3\n4\n5\n", 0
|
169
|
+
# => 6
|
170
|
+
# >> read_value
|
171
|
+
# => "3\n> 4\n> 5"
|
158
172
|
#
|
159
173
|
# by default, read_value reads until the socket is done being ready. "done being ready" is defined as Kernel.select
|
160
174
|
# saying that the socket isn't ready after waiting for SHORT_SOCKET_TIMEOUT. usually this will be true after a
|
161
175
|
# single read, as most things only take one #recv call to get the whole value. this waiting for SHORT_SOCKET_TIMEOUT
|
162
176
|
# can add up to being slow if you're doing a lot of socket activity.
|
177
|
+
#
|
163
178
|
# to solve this, performance can be improved significantly using the :length_before_value option. with this, you have
|
164
179
|
# to write your javascript to return the length of the value to be sent, followed by a newline, followed by the actual
|
165
180
|
# value (which must be of the length it says it is, or this method will error).
|
181
|
+
#
|
166
182
|
# if this option is set, this doesn't do any SHORT_SOCKET_TIMEOUT waiting once it gets the full value, it returns
|
167
183
|
# immediately.
|
168
184
|
def read_value(options={})
|
169
|
-
options={:timeout => DEFAULT_SOCKET_TIMEOUT, :length_before_value => false, :read_size =>
|
185
|
+
options={:timeout => DEFAULT_SOCKET_TIMEOUT, :length_before_value => false, :read_size => READ_SIZE}.merge(options)
|
170
186
|
received_data = []
|
171
187
|
value_string = ""
|
172
188
|
size_to_read=options[:read_size]
|
@@ -222,7 +238,7 @@ class JsshSocket
|
|
222
238
|
# if all we got was the prompt, just stick it on the value here so that the code below will deal with setting @execting_prompt correctly
|
223
239
|
value_string << cleared_error
|
224
240
|
else
|
225
|
-
raise JsshError, "We finished receiving but the socket was still ready to send! extra data received
|
241
|
+
raise JsshError, "We finished receiving but the socket was still ready to send! extra data received were: #{cleared_error}"
|
226
242
|
end
|
227
243
|
end
|
228
244
|
@expecting_extra_maybe=false
|
@@ -252,6 +268,8 @@ class JsshSocket
|
|
252
268
|
end
|
253
269
|
|
254
270
|
private
|
271
|
+
# returns the number of complete utf-8 encoded characters in the string, without erroring on
|
272
|
+
# partial characters.
|
255
273
|
def utf8_length_safe(string)
|
256
274
|
string=string.dup
|
257
275
|
begin
|
@@ -266,12 +284,12 @@ class JsshSocket
|
|
266
284
|
end
|
267
285
|
end
|
268
286
|
# this should be called when an error occurs and we want to clear the socket of any value remaining on it.
|
269
|
-
#
|
287
|
+
# tries for SHORT_SOCKET_TIMEOUT to see if a value will appear on the socket; if one does, returns it.
|
270
288
|
def clear_error
|
271
289
|
data=""
|
272
290
|
while Kernel.select([@socket], nil, nil, SHORT_SOCKET_TIMEOUT)
|
273
291
|
# clear any other crap left on the socket
|
274
|
-
data << @socket.recv(
|
292
|
+
data << @socket.recv(READ_SIZE)
|
275
293
|
end
|
276
294
|
if data =~ /#{Regexp.escape(PROMPT)}\z/
|
277
295
|
@expecting_prompt=false
|
@@ -279,7 +297,9 @@ class JsshSocket
|
|
279
297
|
data
|
280
298
|
end
|
281
299
|
|
282
|
-
|
300
|
+
# sends the given javascript expression which is evaluated, reads the resulting value from the socket, and returns that value.
|
301
|
+
#
|
302
|
+
# options are passed to #read_value untouched; the only one that probably ought to be used here is :timeout.
|
283
303
|
def send_and_read(js_expr, options={})
|
284
304
|
# logger.add(-1) { "SEND_AND_READ is starting. options=#{options.inspect}" }
|
285
305
|
@last_expression=js_expr
|
@@ -289,6 +309,8 @@ class JsshSocket
|
|
289
309
|
return read_value(options)
|
290
310
|
end
|
291
311
|
|
312
|
+
private
|
313
|
+
# creates a ruby exception from the given information and raises it.
|
292
314
|
def js_error(errclassname, message, source, stuff={})
|
293
315
|
errclass=if errclassname
|
294
316
|
unless JsshError.const_defined?(errclassname)
|
@@ -303,14 +325,38 @@ class JsshSocket
|
|
303
325
|
err.js_err=stuff
|
304
326
|
["lineNumber", "stack", "fileName"].each do |attr|
|
305
327
|
if stuff.key?(attr)
|
306
|
-
err.send(
|
328
|
+
err.send("#{attr}=", stuff[attr])
|
307
329
|
end
|
308
330
|
end
|
309
331
|
raise err
|
310
332
|
end
|
311
|
-
|
333
|
+
public
|
334
|
+
|
335
|
+
def self.to_javascript(object)
|
336
|
+
if ['Array', 'Set'].any?{|klass_name| Object.const_defined?(klass_name) && object.is_a?(Object.const_get(klass_name)) }
|
337
|
+
"["+object.map{|element| to_javascript(element) }.join(", ")+"]"
|
338
|
+
elsif object.is_a?(Hash)
|
339
|
+
"{"+object.map{|(key, value)| to_javascript(key)+": "+to_javascript(value) }.join(", ")+"}"
|
340
|
+
elsif object.is_a?(JsshObject)
|
341
|
+
object.ref
|
342
|
+
elsif [true, false, nil].include?(object) || [Integer, Float, String, Symbol].any?{|klass| object.is_a?(klass) }
|
343
|
+
object.to_json
|
344
|
+
elsif object.is_a?(Regexp)
|
345
|
+
# get the flags javascript recognizes - not the same ones as ruby.
|
346
|
+
js_flags = {Regexp::MULTILINE => 'm', Regexp::IGNORECASE => 'i'}.inject("") do |flags, (bit, flag)|
|
347
|
+
flags + (object.options & bit > 0 ? flag : '')
|
348
|
+
end
|
349
|
+
# "new RegExp("+to_javascript(object.source)+", "+to_javascript(js_flags)+")"
|
350
|
+
js_source = object.source.empty? ? "/(?:)/" : object.inspect
|
351
|
+
js_source.sub!(/\w*\z/, '') # drop ruby flags
|
352
|
+
js_source + js_flags
|
353
|
+
else
|
354
|
+
raise "Unable to represent object as javascript: #{object.inspect} (#{object.class})"
|
355
|
+
end
|
356
|
+
end
|
312
357
|
|
313
358
|
# returns the value of the given javascript expression, as reported by JSSH.
|
359
|
+
#
|
314
360
|
# This will be a string, the given expression's toString.
|
315
361
|
def value(js)
|
316
362
|
# this is wrapped in a function so that ...
|
@@ -379,12 +425,16 @@ class JsshSocket
|
|
379
425
|
val=send_and_read(wrapped_js, options.merge(:length_before_value => true))
|
380
426
|
error_or_val_json(val, js)
|
381
427
|
end
|
428
|
+
private
|
429
|
+
# takes a json value (a string) of the form {errored: boolean, value: anything},
|
430
|
+
# checks if an error is indicated, and creates and raises an appropriate exception
|
431
|
+
# if so.
|
382
432
|
def error_or_val_json(val, js)
|
383
433
|
if !val || val==''
|
384
434
|
@expecting_extra_maybe=true
|
385
435
|
raise JsshError, "received no value! may have timed out waiting for a value that was not coming."
|
386
436
|
end
|
387
|
-
if val
|
437
|
+
if val=~ /\ASyntaxError: /
|
388
438
|
raise JsshSyntaxError, val
|
389
439
|
end
|
390
440
|
errord_and_val=parse_json(val)
|
@@ -406,6 +456,7 @@ class JsshSocket
|
|
406
456
|
val
|
407
457
|
end
|
408
458
|
end
|
459
|
+
public
|
409
460
|
|
410
461
|
# assigns to the javascript reference on the left the object on the right.
|
411
462
|
# Assuming the right object can be converted to JSON, the javascript value will
|
@@ -413,13 +464,13 @@ class JsshSocket
|
|
413
464
|
# the assigned value, converted from its javascript value back to ruby. So, the return
|
414
465
|
# value won't be exactly equivalent if you use symbols for example.
|
415
466
|
#
|
416
|
-
#
|
417
|
-
#
|
467
|
+
# >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
|
468
|
+
# => {"foo"=>["baz", "qux"]}
|
418
469
|
#
|
419
470
|
# Uses #value_json; see its documentation.
|
420
471
|
def assign_json(js_left, rb_right)
|
421
472
|
ensure_prototype
|
422
|
-
js_right=rb_right
|
473
|
+
js_right=JsshSocket.to_javascript(rb_right)
|
423
474
|
value_json("#{js_left}=#{js_right}")
|
424
475
|
end
|
425
476
|
|
@@ -430,12 +481,13 @@ class JsshSocket
|
|
430
481
|
# Uses #value_json; see its documentation.
|
431
482
|
def call_json(js_function, *rb_args)
|
432
483
|
ensure_prototype
|
433
|
-
js_args=rb_args.map{|arg| arg
|
484
|
+
js_args=rb_args.map{|arg| JsshSocket.to_javascript(arg) }
|
434
485
|
value_json("#{js_function}(#{js_args.join(', ')})")
|
435
486
|
end
|
436
487
|
|
437
488
|
# does the same thing as #handle, but with json, calling #assign_json, #value_json,
|
438
489
|
# or #call_json.
|
490
|
+
#
|
439
491
|
# if the given javascript expression ends with an = symbol, #handle_json calls to
|
440
492
|
# #assign_json assuming it is given one argument; if the expression refers to a function,
|
441
493
|
# calls that function with the given arguments using #call_json; if the expression is
|
@@ -468,7 +520,7 @@ class JsshSocket
|
|
468
520
|
# raises error if the prototype library (needed for JSON stuff in javascript) has not been loaded
|
469
521
|
def ensure_prototype
|
470
522
|
unless prototype
|
471
|
-
raise JsshError, "
|
523
|
+
raise JsshError, "This functionality requires the prototype library; cannot be called on a Jssh session that has not loaded the Prototype library"
|
472
524
|
end
|
473
525
|
end
|
474
526
|
|
@@ -489,12 +541,14 @@ class JsshSocket
|
|
489
541
|
error_or_val_json(send_and_read(js, :length_before_value => true),js)
|
490
542
|
end
|
491
543
|
|
544
|
+
# uses the javascript 'instanceof' operator, passing it the given
|
545
|
+
# expression and interface. this should return true or false.
|
492
546
|
def instanceof(js_expression, js_interface)
|
493
547
|
value_json "(#{js_expression}) instanceof (#{js_interface})"
|
494
548
|
end
|
495
549
|
|
496
|
-
# parses the given JSON string using
|
497
|
-
# Raises
|
550
|
+
# parses the given JSON string using JSON.parse
|
551
|
+
# Raises JSON::ParserError if given a blank string, something that is not a string, or
|
498
552
|
# a string that contains invalid JSON
|
499
553
|
def parse_json(json)
|
500
554
|
err_class=JSON::ParserError
|
@@ -512,23 +566,159 @@ class JsshSocket
|
|
512
566
|
end
|
513
567
|
end
|
514
568
|
|
515
|
-
|
516
|
-
|
569
|
+
# takes a reference and returns a new JsshObject representing that reference on this socket.
|
570
|
+
# ref should be a string representing a reference in javascript.
|
571
|
+
def object(ref, other={})
|
572
|
+
JsshObject.new(ref, self, {:debug_name => ref}.merge(other))
|
573
|
+
end
|
574
|
+
# takes a reference and returns a new JsshObject representing that reference on this socket,
|
575
|
+
# stored on this socket's temporary object.
|
576
|
+
def object_in_temp(ref, other={})
|
577
|
+
object(ref, other).store_rand_temp
|
517
578
|
end
|
518
|
-
|
519
|
-
|
579
|
+
|
580
|
+
# represents the root of the space seen by the JsshSocket, and implements #method_missing to
|
581
|
+
# return objects at the root level in a similar manner to JsshObject's #method_missing.
|
582
|
+
#
|
583
|
+
# for example, jssh_socket.root.Components will return the top-level Components object;
|
584
|
+
# jssh_socket.root.ctypes will return the ctypes top-level object if that is defined, or error
|
585
|
+
# if not.
|
586
|
+
#
|
587
|
+
# if the object is a function, then it will be called with any given arguments:
|
588
|
+
# >> jssh_socket.root.getWindows
|
589
|
+
# => #<JsshObject:0x0254d150 type=object, debug_name=getWindows()>
|
590
|
+
# >> jssh_socket.root.eval("3+2")
|
591
|
+
# => 5
|
592
|
+
#
|
593
|
+
# If any arguments are given to an object that is not a function, you will get an error:
|
594
|
+
# >> jssh_socket.root.Components('wat')
|
595
|
+
# ArgumentError: Cannot pass arguments to Javascript object #<JsshObject:0x02545978 type=object, debug_name=Components>
|
596
|
+
#
|
597
|
+
# special behaviors exist for the suffixes !, ?, and =.
|
598
|
+
#
|
599
|
+
# - '?' suffix returns nil if the object does not exist, rather than raising an exception. for
|
600
|
+
# example:
|
601
|
+
# >> jssh_socket.root.foo
|
602
|
+
# JsshUndefinedValueError: undefined expression represented by #<JsshObject:0x024c3ae0 type=undefined, debug_name=foo> (javascript reference is foo)
|
603
|
+
# >> jssh_socket.root.foo?
|
604
|
+
# => nil
|
605
|
+
# - '=' suffix sets the named object to what is given, for example:
|
606
|
+
# >> jssh_socket.root.foo?
|
607
|
+
# => nil
|
608
|
+
# >> jssh_socket.root.foo={:x => ['y', 'z']}
|
609
|
+
# => {:x=>["y", "z"]}
|
610
|
+
# >> jssh_socket.root.foo
|
611
|
+
# => #<JsshObject:0x024a3510 type=object, debug_name=foo>
|
612
|
+
# - '!' suffix tries to convert the value to json in javascrit and back from json to ruby, even
|
613
|
+
# when it might be unsafe (causing infinite rucursion or other errors). for example:
|
614
|
+
# >> jssh_socket.root.foo!
|
615
|
+
# => {"x"=>["y", "z"]}
|
616
|
+
# it can be used with function results that would normally result in a JsshObject:
|
617
|
+
# >> jssh_socket.root.eval!("[1, 2, 3]")
|
618
|
+
# => [1, 2, 3]
|
619
|
+
# and of course it can error if you try to do something you shouldn't:
|
620
|
+
# >> jssh_socket.root.getWindows!
|
621
|
+
# JsshError::NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIJSON.encode]
|
622
|
+
def root
|
623
|
+
jssh_socket=self
|
624
|
+
# @root ||= begin
|
625
|
+
root = Object.new
|
626
|
+
(class << root; self; end).send(:define_method, :method_missing) do |method, *args|
|
627
|
+
method=method.to_s
|
628
|
+
if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
|
629
|
+
method = $1
|
630
|
+
suffix = $2
|
631
|
+
jssh_socket.object(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
632
|
+
else
|
633
|
+
# don't deal with any special character crap
|
634
|
+
super
|
635
|
+
end
|
636
|
+
end
|
637
|
+
root
|
638
|
+
# end
|
520
639
|
end
|
521
640
|
|
641
|
+
# Creates and returns a JsshObject representing a function.
|
642
|
+
#
|
643
|
+
# Takes any number of arguments, which should be strings or symbols, which are arguments to the
|
644
|
+
# javascript function.
|
645
|
+
#
|
646
|
+
# The javascript function is specified as the result of a block which must be given to
|
647
|
+
# #function.
|
648
|
+
#
|
649
|
+
# An example:
|
650
|
+
# jssh_socket.function(:a, :b) do
|
651
|
+
# "return a+b;"
|
652
|
+
# end
|
653
|
+
# => #<JsshObject:0x0248e78c type=function, debug_name=function(a, b){ return a+b; }>
|
654
|
+
#
|
655
|
+
# This is exactly the same as doing
|
656
|
+
# jssh_socket.object("function(a, b){ return a+b; }")
|
657
|
+
# but it is a bit more concise and reads a bit more ruby-like.
|
658
|
+
#
|
659
|
+
# a longer example to return the text of a thing (rather contrived, but, it works):
|
660
|
+
#
|
661
|
+
# jssh_socket.function(:node) do %q[
|
662
|
+
# if(node.nodeType==3)
|
663
|
+
# { return node.data;
|
664
|
+
# }
|
665
|
+
# else if(node.nodeType==1)
|
666
|
+
# { return node.textContent;
|
667
|
+
# }
|
668
|
+
# else
|
669
|
+
# { return "what?";
|
670
|
+
# }
|
671
|
+
# ]
|
672
|
+
# end.call(some_node)
|
673
|
+
def function(*arg_names)
|
674
|
+
unless arg_names.all?{|arg| (arg.is_a?(String) || arg.is_a?(Symbol)) && arg.to_s =~ /\A[a-z_][a-z0-9_]*\z/i }
|
675
|
+
raise ArgumentError, "Arguments to \#function should be strings or symbols representing the names of arguments to the function. got #{arg_names.inspect}"
|
676
|
+
end
|
677
|
+
unless block_given?
|
678
|
+
raise ArgumentError, "\#function should be given a block which results in a string representing the body of a javascript function. no block was given!"
|
679
|
+
end
|
680
|
+
function_body = yield
|
681
|
+
unless function_body.is_a?(String)
|
682
|
+
raise ArgumentError, "The block given to \#function must return a string representing the body of a javascript function! instead got #{function_body.inspect}"
|
683
|
+
end
|
684
|
+
nl = function_body.include?("\n") ? "\n" : ""
|
685
|
+
object("function(#{arg_names.join(", ")})#{nl}{ #{function_body} #{nl}}")
|
686
|
+
end
|
687
|
+
|
688
|
+
# takes a hash of arguments with keys that are strings or symbols that will be variables in the
|
689
|
+
# scope of the function in javascript, and a block which results in a string which should be the
|
690
|
+
# body of a javascript function. calls the given function with the given arguments.
|
691
|
+
#
|
692
|
+
# an example:
|
693
|
+
# jssh_socket.call_function(:x => 3, :y => {:z => 'foobar'}) do
|
694
|
+
# "return x + y['z'].length;"
|
695
|
+
# end
|
696
|
+
#
|
697
|
+
# will return 9.
|
698
|
+
def call_function(arguments_hash={}, &block)
|
699
|
+
argument_names, argument_vals = *arguments_hash.inject([[],[]]) do |(names, vals),(name, val)|
|
700
|
+
[names + [name], vals + [val]]
|
701
|
+
end
|
702
|
+
function(*argument_names, &block).call(*argument_vals)
|
703
|
+
end
|
704
|
+
|
705
|
+
# returns a JsshObject representing a designated top-level object for temporary storage of stuff
|
706
|
+
# on this socket.
|
707
|
+
#
|
708
|
+
# really, temporary values could be stored anywhere. this just gives one nice consistent designated place to stick them.
|
522
709
|
def temp_object
|
523
|
-
@temp_object ||=
|
710
|
+
@temp_object ||= root.JsshTemp
|
524
711
|
end
|
712
|
+
# returns a JsshObject representing the Components top-level javascript object.
|
713
|
+
#
|
714
|
+
# https://developer.mozilla.org/en/Components_object
|
525
715
|
def Components
|
526
|
-
@components ||=
|
716
|
+
@components ||= root.Components
|
527
717
|
end
|
718
|
+
# returns a JsshObject representing the return value of JSSH's builtin getWindows() function.
|
528
719
|
def getWindows
|
529
|
-
|
720
|
+
root.getWindows
|
530
721
|
end
|
531
|
-
|
532
722
|
# raises an informative error if the socket is down for some reason
|
533
723
|
def assert_socket
|
534
724
|
begin
|
@@ -545,18 +735,29 @@ class JsshSocket
|
|
545
735
|
end
|
546
736
|
end
|
547
737
|
|
738
|
+
# returns a string of basic information about this socket.
|
548
739
|
def inspect
|
549
740
|
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:ip, :port, :prototype].map{|attr| aa="@#{attr}";aa+'='+instance_variable_get(aa).inspect}.join(', ')}>"
|
550
741
|
end
|
551
742
|
end
|
552
743
|
|
744
|
+
# represents a javascript object in ruby.
|
553
745
|
class JsshObject
|
554
|
-
|
555
|
-
attr_reader :
|
746
|
+
# the reference to the javascript object this JsshObject represents
|
747
|
+
attr_reader :ref
|
748
|
+
# the JsshSocket this JsshObject is on
|
749
|
+
attr_reader :jssh_socket
|
750
|
+
# whether this represents the result of a function call (if it does, then JsshSocket#typeof won't be called on it)
|
751
|
+
attr_reader :function_result
|
752
|
+
# this tracks the origins of this object - what calls were made along the way to get it.
|
753
|
+
attr_reader :debug_name
|
754
|
+
# :stopdoc:
|
556
755
|
# def logger
|
557
756
|
# jssh_socket.logger
|
558
757
|
# end
|
559
758
|
|
759
|
+
# :startdoc:
|
760
|
+
|
560
761
|
public
|
561
762
|
# initializes a JsshObject with a string of javascript containing a reference to
|
562
763
|
# the object, and a JsshSocket that the object is defined on.
|
@@ -576,6 +777,32 @@ class JsshObject
|
|
576
777
|
def val
|
577
778
|
jssh_socket.value_json(ref, :error_on_undefined => !function_result)
|
578
779
|
end
|
780
|
+
|
781
|
+
# whether JsshObject shall try to dynamically define methods on initialization, using
|
782
|
+
# #define_methods! default is false.
|
783
|
+
def self.always_define_methods
|
784
|
+
unless class_variable_defined?('@@always_define_methods')
|
785
|
+
# if not defined, set the default.
|
786
|
+
@@always_define_methods=false
|
787
|
+
end
|
788
|
+
@@always_define_methods
|
789
|
+
end
|
790
|
+
# set whether JsshObject shall try to dynamically define methods in #val_or_object, using
|
791
|
+
# #define_methods!
|
792
|
+
#
|
793
|
+
# I find this useful to set to true in irb, for tab-completion of methods. it may cause
|
794
|
+
# jssh operations to be considerably slower, however.
|
795
|
+
#
|
796
|
+
# for always setting this in irb, I set this beforehand, overriding the default,
|
797
|
+
# by including in my .irbrc the following (which doesn't require jssh_socket.rb to be
|
798
|
+
# required):
|
799
|
+
#
|
800
|
+
# class JsshObject
|
801
|
+
# @@always_define_methods=true
|
802
|
+
# end
|
803
|
+
def self.always_define_methods=(val)
|
804
|
+
@@always_define_methods = val
|
805
|
+
end
|
579
806
|
|
580
807
|
# returns the value just as a string with no attempt to deal with type using json. via JsshSocket#value
|
581
808
|
#
|
@@ -597,15 +824,22 @@ class JsshObject
|
|
597
824
|
end
|
598
825
|
end
|
599
826
|
|
600
|
-
#
|
601
|
-
# note that
|
827
|
+
# calls the javascript instanceof operator on this object and the given interface (expected to
|
828
|
+
# be a JsshObject) note that the javascript instanceof operator is not to be confused with
|
829
|
+
# ruby's #instance_of? method - this takes a javascript interface; #instance_of? takes a ruby
|
830
|
+
# module.
|
602
831
|
#
|
603
832
|
# example:
|
604
|
-
#
|
605
|
-
#
|
833
|
+
# window.instanceof(window.jssh_socket.Components.interfaces.nsIDOMChromeWindow)
|
834
|
+
# => true
|
606
835
|
def instanceof(interface)
|
607
836
|
jssh_socket.instanceof(self.ref, interface.ref)
|
608
837
|
end
|
838
|
+
# returns an array of interfaces which this object is an instance of. this is achieved
|
839
|
+
# by looping over each value of Components.interfaces (see https://developer.mozilla.org/en/Components.interfaces )
|
840
|
+
# and calling the #instanceof operator with this and the interface.
|
841
|
+
#
|
842
|
+
# this may be rather slow.
|
609
843
|
def implemented_interfaces
|
610
844
|
jssh_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
|
611
845
|
list << interface if instanceof(interface)
|
@@ -618,6 +852,8 @@ class JsshObject
|
|
618
852
|
# This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
|
619
853
|
# Raises an error if this JsshObject points to something other than a javascript 'object'
|
620
854
|
# type ('function' or 'number' or whatever)
|
855
|
+
#
|
856
|
+
# this isn't used, doesn't seem useful, and may go away in the future.
|
621
857
|
def object_type
|
622
858
|
@object_type ||= begin
|
623
859
|
case type
|
@@ -630,59 +866,99 @@ class JsshObject
|
|
630
866
|
end
|
631
867
|
end
|
632
868
|
|
869
|
+
# checks the type of this object, and if it is a type that can be simply converted to a ruby
|
870
|
+
# object via json, returns the ruby value. that occurs if the type is one of:
|
871
|
+
#
|
872
|
+
# 'boolean','number','string','null'
|
873
|
+
#
|
874
|
+
# otherwise - if the type is something else (probably 'function' or 'object'; or maybe something else)
|
875
|
+
# then this JsshObject is returned.
|
876
|
+
#
|
877
|
+
# if the object this refers to is undefined in javascript, then behavor depends on the options
|
878
|
+
# hash. if :error_on_undefined is true, then nil is returned; otherwise JsshUndefinedValueError
|
879
|
+
# is raised.
|
880
|
+
#
|
881
|
+
# if this is a function result, this will store the result in a temporary location (thereby
|
882
|
+
# calling the function to acquire the result) before making the above decision.
|
883
|
+
#
|
884
|
+
# this method also calls #define_methods! on this if JsshObject.always_define_methods is true.
|
885
|
+
# this can be overridden in the options hash using the :define_methods key (true or false).
|
633
886
|
def val_or_object(options={})
|
634
|
-
options={:error_on_undefined=>true}.merge(options)
|
887
|
+
options={:error_on_undefined=>true, :define_methods => self.class.always_define_methods}.merge(options)
|
635
888
|
if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
|
636
|
-
store_rand_object_key(jssh_socket.temp_object).val_or_object(:error_on_undefined => false)
|
889
|
+
store_rand_object_key(jssh_socket.temp_object).val_or_object(options.merge(:error_on_undefined => false))
|
637
890
|
else
|
638
891
|
case self.type
|
639
892
|
when 'undefined'
|
640
893
|
if !options[:error_on_undefined]
|
641
894
|
nil
|
642
895
|
else
|
643
|
-
raise JsshUndefinedValueError, "undefined expression #{ref}"
|
896
|
+
raise JsshUndefinedValueError, "undefined expression represented by #{self.inspect} (javascript reference is #{@ref})"
|
644
897
|
end
|
645
898
|
when 'boolean','number','string','null'
|
646
899
|
val
|
647
900
|
else # 'function','object', or anything else
|
901
|
+
if options[:define_methods] && type=='object'
|
902
|
+
define_methods!
|
903
|
+
end
|
648
904
|
self
|
649
905
|
end
|
650
906
|
end
|
651
907
|
end
|
652
|
-
|
653
|
-
#
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
# undefined, use #attr).
|
658
|
-
def invoke(attribute, *args)
|
659
|
-
attr_obj=attr(attribute)
|
660
|
-
type=attr_obj.type
|
661
|
-
case type
|
662
|
-
when 'function'
|
663
|
-
attr_obj.call(*args)
|
908
|
+
# does the work of #method_missing to determine whether to call a function what to return based
|
909
|
+
# on the defined behavior of the given suffix. see #method_missing for more. information.
|
910
|
+
def assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
911
|
+
if suffix=='='
|
912
|
+
assign(*args)
|
664
913
|
else
|
665
|
-
if
|
666
|
-
|
914
|
+
obj = if type=='function'
|
915
|
+
pass(*args)
|
916
|
+
elsif !args.empty?
|
917
|
+
raise ArgumentError, "Cannot pass arguments to Javascript object #{inspect} (ref = #{ref})"
|
918
|
+
else
|
919
|
+
self
|
920
|
+
end
|
921
|
+
case suffix
|
922
|
+
when nil
|
923
|
+
obj.val_or_object
|
924
|
+
when '?'
|
925
|
+
obj.val_or_object(:error_on_undefined => false)
|
926
|
+
when '!'
|
927
|
+
obj.val
|
667
928
|
else
|
668
|
-
raise ArgumentError, "
|
929
|
+
raise ArgumentError, "suffix should be one of: nil, '?', '!', '='; got: #{suffix.inspect}"
|
669
930
|
end
|
670
931
|
end
|
671
932
|
end
|
672
933
|
|
934
|
+
# returns a JsshObject representing the given attribute. Checks the type, and if it is a
|
935
|
+
# function, calls the function with any arguments given (which are converted to javascript)
|
936
|
+
# and returns the return value of the function (or nil if the function returns undefined).
|
937
|
+
#
|
938
|
+
# If the attribute is undefined, raises an error (if you want an attribute even if it's
|
939
|
+
# undefined, use #invoke? or #attr).
|
940
|
+
def invoke(attribute, *args)
|
941
|
+
attr(attribute).assign_or_call_or_val_or_object_by_suffix(nil, *args)
|
942
|
+
end
|
943
|
+
# same as #invoke, but returns nil for undefined attributes rather than raising an
|
944
|
+
# error.
|
945
|
+
def invoke?(attribute, *args)
|
946
|
+
attr(attribute).assign_or_call_or_val_or_object_by_suffix('?', *args)
|
947
|
+
end
|
948
|
+
|
673
949
|
# returns a JsshObject referencing the given attribute of this object
|
674
|
-
def attr(attribute)
|
950
|
+
def attr(attribute, options={})
|
675
951
|
unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
|
676
952
|
raise JsshSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
|
677
953
|
end
|
678
954
|
JsshObject.new("#{ref}.#{attribute}", jssh_socket, :debug_name => "#{debug_name}.#{attribute}")
|
679
955
|
end
|
680
956
|
|
681
|
-
# assigns
|
957
|
+
# assigns the given ruby value (converted to javascript) to the reference
|
682
958
|
# for this object. returns self.
|
683
959
|
def assign(val)
|
684
|
-
@debug_name="(#{debug_name}=#{val.is_a?(JsshObject) ? val.debug_name : val
|
685
|
-
result=assign_expr
|
960
|
+
@debug_name="(#{debug_name}=#{val.is_a?(JsshObject) ? val.debug_name : JsshSocket.to_javascript(val)})"
|
961
|
+
result=assign_expr(JsshSocket.to_javascript(val))
|
686
962
|
# logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
|
687
963
|
result
|
688
964
|
end
|
@@ -697,49 +973,67 @@ class JsshObject
|
|
697
973
|
self
|
698
974
|
end
|
699
975
|
|
700
|
-
# returns a JsshObject for
|
701
|
-
#
|
976
|
+
# returns a JsshObject for the result of calling the function represented by this object, passing
|
977
|
+
# the given arguments, which are converted to javascript. if this is not a function, javascript will raise an error.
|
702
978
|
def pass(*args)
|
703
|
-
JsshObject.new("#{ref}(#{args.map{|arg| arg
|
979
|
+
JsshObject.new("#{ref}(#{args.map{|arg| JsshSocket.to_javascript(arg)}.join(', ')})", jssh_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JsshObject) ? arg.debug_name : JsshSocket.to_javascript(arg)}.join(', ')})")
|
704
980
|
end
|
705
981
|
|
706
982
|
# returns the value (via JsshSocket#value_json) or a JsshObject (see #val_or_object) of the return
|
707
983
|
# value of this function (assumes this object is a function) passing it the given arguments (which
|
708
|
-
# are converted
|
984
|
+
# are converted to javascript).
|
985
|
+
#
|
709
986
|
# simply, it just calls self.pass(*args).val_or_object
|
710
987
|
def call(*args)
|
711
988
|
pass(*args).val_or_object
|
712
989
|
end
|
713
990
|
|
991
|
+
# assuming the javascript object represented is a constructor, this returns a new
|
992
|
+
# instance passing the given arguments.
|
993
|
+
#
|
994
|
+
# date_class = jssh_socket.object('Date')
|
995
|
+
# => #<JsshObject:0x0118eee8 type=function, debug_name=Date>
|
996
|
+
# date = date_class.new
|
997
|
+
# => #<JsshObject:0x01188a84 type=object, debug_name=new Date()>
|
998
|
+
# date.getFullYear
|
999
|
+
# => 2010
|
1000
|
+
# date_class.new('october 4, 1978').getFullYear
|
1001
|
+
# => 1978
|
1002
|
+
def new(*args)
|
1003
|
+
JsshObject.new("new #{ref}", jssh_socket, :debug_name => "new #{debug_name}").call(*args)
|
1004
|
+
end
|
1005
|
+
|
714
1006
|
# sets the given javascript variable to this object, and returns a JsshObject referring
|
715
1007
|
# to the variable.
|
716
1008
|
#
|
717
|
-
#
|
718
|
-
#
|
719
|
-
#
|
720
|
-
#
|
1009
|
+
# >> foo=document.getElementById('guser').store('foo')
|
1010
|
+
# => #<JsshObject:0x2dff870 @ref="foo" ...>
|
1011
|
+
# >> foo.tagName
|
1012
|
+
# => "DIV"
|
1013
|
+
#
|
1014
|
+
# the second argument is only used internally and shouldn't be used.
|
721
1015
|
def store(js_variable, somewhere_meaningful=true)
|
722
1016
|
stored=JsshObject.new(js_variable, jssh_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
|
723
1017
|
stored.assign_expr(self.ref)
|
724
1018
|
stored
|
725
1019
|
end
|
726
1020
|
|
1021
|
+
private
|
1022
|
+
# takes a block which, when yielded a random key, should result in a random reference. this checks
|
1023
|
+
# that the reference is not already in use and stores this object in that reference, and returns
|
1024
|
+
# a JsshObject referring to the stored object.
|
727
1025
|
def store_rand_named(&name_proc)
|
728
1026
|
base=36
|
729
|
-
length=
|
1027
|
+
length=32
|
730
1028
|
begin
|
731
1029
|
name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
|
732
|
-
end while JsshObject.new(name,jssh_socket).type!='undefined'
|
1030
|
+
end #while JsshObject.new(name,jssh_socket).type!='undefined'
|
733
1031
|
# okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
|
734
1032
|
store(name, false)
|
735
1033
|
end
|
1034
|
+
public
|
736
1035
|
|
737
|
-
|
738
|
-
store_rand_named do |r|
|
739
|
-
prefix+"_"+r
|
740
|
-
end
|
741
|
-
end
|
742
|
-
|
1036
|
+
# stores this object in a random key of the given object and returns the stored object.
|
743
1037
|
def store_rand_object_key(object)
|
744
1038
|
raise ArgumentError("Object is not a JsshObject: got #{object.inspect}") unless object.is_a?(JsshObject)
|
745
1039
|
store_rand_named do |r|
|
@@ -747,64 +1041,80 @@ class JsshObject
|
|
747
1041
|
end
|
748
1042
|
end
|
749
1043
|
|
1044
|
+
# stores this object in a random key of the designated temporary object for this socket and returns the stored object.
|
750
1045
|
def store_rand_temp
|
751
1046
|
store_rand_object_key(jssh_socket.temp_object)
|
752
1047
|
end
|
753
1048
|
|
754
|
-
# returns a JsshObject referring to a subscript of this object, specified as a _javascript_ expression
|
755
|
-
# (doesn't use to_jssh)
|
756
|
-
# def sub_expr(key_expr)
|
757
|
-
# JsshObject.new("#{ref}[#{key_expr}]", jssh_socket, :debug_name => "#{debug_name}[#{}]")
|
758
|
-
# end
|
759
|
-
|
760
1049
|
# returns a JsshObject referring to a subscript of this object, specified as a ruby object converted to
|
761
|
-
# javascript
|
1050
|
+
# javascript.
|
1051
|
+
#
|
1052
|
+
# similar to [], but [] calls #val_or_object; this always returns a JsshObject.
|
762
1053
|
def sub(key)
|
763
|
-
JsshObject.new("#{ref}[#{key
|
1054
|
+
JsshObject.new("#{ref}[#{JsshSocket.to_javascript(key)}]", jssh_socket, :debug_name => "#{debug_name}[#{key.is_a?(JsshObject) ? key.debug_name : JsshSocket.to_javascript(key)}]")
|
764
1055
|
end
|
765
1056
|
|
766
1057
|
# returns a JsshObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
|
767
|
-
#
|
1058
|
+
#
|
1059
|
+
# subscript is specified as ruby (converted to javascript).
|
768
1060
|
def [](key)
|
769
1061
|
sub(key).val_or_object(:error_on_undefined => false)
|
770
1062
|
end
|
771
|
-
|
772
|
-
# (
|
1063
|
+
|
1064
|
+
# assigns the given ruby value (which is converted to javascript) to the given subscript
|
1065
|
+
# (the key is also converted to javascript).
|
773
1066
|
def []=(key, value)
|
774
1067
|
self.sub(key).assign(value)
|
775
1068
|
end
|
776
1069
|
|
777
|
-
# calls a binary operator with self and another operand
|
1070
|
+
# calls a binary operator (in javascript) with self and another operand.
|
1071
|
+
#
|
1072
|
+
# the operator should be string of javascript; the operand will be converted to javascript.
|
778
1073
|
def binary_operator(operator, operand)
|
779
|
-
JsshObject.new("(#{ref}#{operator}#{operand
|
1074
|
+
JsshObject.new("(#{ref}#{operator}#{JsshSocket.to_javascript(operand)})", jssh_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JsshObject) ? operand.debug_name : JsshSocket.to_javascript(operand)})").val_or_object
|
780
1075
|
end
|
1076
|
+
# addition, using the + operator in javascript
|
781
1077
|
def +(operand)
|
782
1078
|
binary_operator('+', operand)
|
783
1079
|
end
|
1080
|
+
# subtraction, using the - operator in javascript
|
784
1081
|
def -(operand)
|
785
1082
|
binary_operator('-', operand)
|
786
1083
|
end
|
1084
|
+
# division, using the / operator in javascript
|
787
1085
|
def /(operand)
|
788
1086
|
binary_operator('/', operand)
|
789
1087
|
end
|
1088
|
+
# multiplication, using the * operator in javascript
|
790
1089
|
def *(operand)
|
791
1090
|
binary_operator('*', operand)
|
792
1091
|
end
|
1092
|
+
# modulus, using the % operator in javascript
|
793
1093
|
def %(operand)
|
794
1094
|
binary_operator('%', operand)
|
795
1095
|
end
|
1096
|
+
# returns true if the javascript object represented by this is equal to the given operand.
|
796
1097
|
def ==(operand)
|
797
1098
|
operand.is_a?(JsshObject) && binary_operator('==', operand)
|
798
1099
|
end
|
1100
|
+
# javascript triple-equals (===) operator. very different from ruby's tripl-equals operator -
|
1101
|
+
# in javascript this means "really really equal"; in ruby it means "sort of equal-ish"
|
1102
|
+
def triple_equals(operand)
|
1103
|
+
operand.is_a?(JsshObject) && binary_operator('===', operand)
|
1104
|
+
end
|
1105
|
+
# inequality, using the > operator in javascript
|
799
1106
|
def >(operand)
|
800
1107
|
binary_operator('>', operand)
|
801
1108
|
end
|
1109
|
+
# inequality, using the < operator in javascript
|
802
1110
|
def <(operand)
|
803
1111
|
binary_operator('<', operand)
|
804
1112
|
end
|
1113
|
+
# inequality, using the >= operator in javascript
|
805
1114
|
def >=(operand)
|
806
1115
|
binary_operator('>=', operand)
|
807
1116
|
end
|
1117
|
+
# inequality, using the <= operator in javascript
|
808
1118
|
def <=(operand)
|
809
1119
|
binary_operator('<=', operand)
|
810
1120
|
end
|
@@ -812,113 +1122,92 @@ class JsshObject
|
|
812
1122
|
# method_missing handles unknown method calls in a way that makes it possible to write
|
813
1123
|
# javascript-like syntax in ruby, to some extent.
|
814
1124
|
#
|
1125
|
+
# method_missing checks the attribute of the represented javascript object with with the name of the given method. if that
|
1126
|
+
# attribute refers to a function, then that function is called with any given arguments
|
1127
|
+
# (like #invoke does). If that attribute is undefined, an error will be raised, unless a '?'
|
1128
|
+
# suffix is used (see below).
|
1129
|
+
#
|
815
1130
|
# method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
|
816
1131
|
# special characters, only alphanumeric/underscores, starting with alpha or underscore - with
|
817
1132
|
# the exception of three special behaviors:
|
818
1133
|
#
|
819
|
-
# If the method ends with an equals sign (=), it does assignment - it calls
|
820
|
-
# to do the assignment and returns the assigned
|
821
|
-
#
|
822
|
-
# If the method ends with a bang (!), then it will attempt to get the value (using json) of the
|
823
|
-
# reference, using JsonObject#val. For simple types (null, string, boolean, number), this is what
|
824
|
-
# happens by default anyway, but if you have an object or an array that you know you can json-ize,
|
825
|
-
# you can use ! to force that. See #invoke documentation for more information.
|
1134
|
+
# If the method ends with an equals sign (=), it does assignment - it calls #assign on the given
|
1135
|
+
# attribute, with the given (single) argument, to do the assignment and returns the assigned
|
1136
|
+
# value.
|
826
1137
|
#
|
827
|
-
# If the method ends with a
|
828
|
-
#
|
829
|
-
#
|
830
|
-
#
|
1138
|
+
# If the method ends with a bang (!), then it will attempt to get the value of the reference,
|
1139
|
+
# using JsshObject#val, which converts the javascript to json and then to ruby. For simple types
|
1140
|
+
# (null, string, boolean, number), this is what gets returned anyway. With other types (usually
|
1141
|
+
# the 'object' type), attempting to convert to json can raise errors or cause infinite
|
1142
|
+
# recursion, so is not attempted. but if you have an object or an array that you know you can
|
1143
|
+
# json-ize, you can use ! to force that.
|
831
1144
|
#
|
832
|
-
#
|
833
|
-
#
|
1145
|
+
# If the method ends with a question mark (?), then if the attribute is undefined, no error is
|
1146
|
+
# raised (as usually happens) - instead nil is just returned.
|
834
1147
|
#
|
835
|
-
#
|
836
|
-
#
|
837
|
-
#
|
838
|
-
# this lets you do things like:
|
839
|
-
# >> jssh_socket.object('getWindows()').length
|
840
|
-
# => 2
|
841
|
-
# >> jssh_socket.object('getWindows()')[1].getBrowser.contentDocument?
|
842
|
-
# => "[object XPCNativeWrapper [object HTMLDocument]]"
|
843
|
-
# >> document=jssh_socket.object('getWindows()')[1].getBrowser.contentDocument
|
844
|
-
# => #<JsshObject:0x34f01fc @ref="getWindows()[1].getBrowser().contentDocument" ...>
|
845
|
-
# >> document.title
|
846
|
-
# => "ruby - Google Search"
|
847
|
-
# >> document.forms[0].q.value
|
848
|
-
# => "ruby"
|
849
|
-
# >> document.forms[0].q.value='foobar'
|
850
|
-
# => "foobar"
|
851
|
-
# >> document.forms[0].q.value
|
852
|
-
# => "foobar"
|
1148
|
+
# otherwise, method_missing behaves like #invoke, and returns a JsshObject, a string, a boolean,
|
1149
|
+
# a number, or null.
|
853
1150
|
#
|
1151
|
+
# Since method_missing returns a JsshObject for javascript objects, this means that you can
|
1152
|
+
# string together method_missings and the result looks rather like javascript.
|
1153
|
+
#--
|
854
1154
|
# $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
|
855
1155
|
# methods to arrays and hashes - see http://www.prototypejs.org/
|
856
1156
|
# You can use these methods with method_missing just like any other:
|
857
1157
|
#
|
858
|
-
#
|
859
|
-
#
|
860
|
-
#
|
861
|
-
#
|
862
|
-
#
|
863
|
-
#
|
864
|
-
#
|
865
|
-
#
|
866
|
-
#
|
867
|
-
#
|
868
|
-
#
|
869
|
-
#
|
1158
|
+
# >> js_hash=jssh_socket.object('$H')
|
1159
|
+
# => #<JsshObject:0x2beb598 @ref="$H" ...>
|
1160
|
+
# >> js_arr=jssh_socket.object('$A')
|
1161
|
+
# => #<JsshObject:0x2be40e0 @ref="$A" ...>
|
1162
|
+
#
|
1163
|
+
# >> js_arr.call(document.body.childNodes).pluck! :tagName
|
1164
|
+
# => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
|
1165
|
+
# >> js_arr.call(document.body.childNodes).pluck! :id
|
1166
|
+
# => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
|
1167
|
+
# >> js_hash.call(document.getElementById('tbd')).keys!
|
1168
|
+
# => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", ...]
|
870
1169
|
def method_missing(method, *args)
|
871
1170
|
method=method.to_s
|
872
1171
|
if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
|
873
1172
|
method = $1
|
874
|
-
|
875
|
-
|
876
|
-
#Object.instance_method(:method_missing).bind(self).call(method, *args) # let Object#method_missing raise its usual error
|
877
|
-
return super
|
878
|
-
end
|
879
|
-
case special
|
880
|
-
when nil
|
881
|
-
invoke(method, *args)
|
882
|
-
when '!'
|
883
|
-
got=invoke(method, *args)
|
884
|
-
got.is_a?(JsshObject) ? got.val : got
|
885
|
-
when '?'
|
886
|
-
begin
|
887
|
-
got=invoke(method, *args)
|
888
|
-
got.is_a?(JsshObject) ? got.val_str : got
|
889
|
-
rescue JsshUndefinedValueError
|
890
|
-
nil
|
891
|
-
end
|
892
|
-
when '='
|
893
|
-
attr(method).assign(*args)
|
1173
|
+
suffix = $2
|
1174
|
+
attr(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
894
1175
|
else
|
895
|
-
|
1176
|
+
# don't deal with any special character crap
|
1177
|
+
super
|
896
1178
|
end
|
897
1179
|
end
|
898
|
-
|
1180
|
+
# calls define_method for each key of this object as a hash. useful for tab-completing attributes
|
1181
|
+
# in irb, mostly.
|
1182
|
+
def define_methods! # :nodoc:
|
899
1183
|
metaclass=(class << self; self; end)
|
900
|
-
|
1184
|
+
keys=jssh_socket.object("function(obj) { var keys=[]; for(var key in obj) { keys.push(key); } return keys; }").pass(self).val
|
1185
|
+
|
1186
|
+
keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
|
901
1187
|
metaclass.send(:define_method, key) do |*args|
|
902
1188
|
invoke(key, *args)
|
903
1189
|
end
|
904
1190
|
end
|
905
1191
|
end
|
906
|
-
|
907
|
-
|
1192
|
+
# returns true if this object responds to the given method (that is, it's a defined ruby method)
|
1193
|
+
# or if #method_missing will handle it
|
1194
|
+
def respond_to?(method, include_private = false)
|
908
1195
|
super || object_respond_to?(method)
|
909
1196
|
end
|
1197
|
+
# returns true if the javascript object this represents responds to the given method. this does not pay attention
|
1198
|
+
# to any defined ruby methods, just javascript.
|
910
1199
|
def object_respond_to?(method)
|
911
1200
|
method=method.to_s
|
912
1201
|
if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
|
913
1202
|
method = $1
|
914
|
-
|
1203
|
+
suffix = $2
|
915
1204
|
else # don't deal with any special character crap
|
916
1205
|
return false
|
917
1206
|
end
|
918
1207
|
|
919
1208
|
if self.type=='undefined'
|
920
1209
|
return false
|
921
|
-
elsif
|
1210
|
+
elsif suffix=='='
|
922
1211
|
if self.type=='object'
|
923
1212
|
return true # yeah, you can generally assign attributes to objects
|
924
1213
|
else
|
@@ -930,40 +1219,54 @@ class JsshObject
|
|
930
1219
|
end
|
931
1220
|
end
|
932
1221
|
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
# convert the object to json.
|
939
|
-
def to_jssh
|
940
|
-
ref
|
941
|
-
end
|
942
|
-
# this still needs to be defined because when ActiveSupport::JSON.encode is called by to_jssh
|
943
|
-
# on an array or hash containing a JsshObject, it calls to_json. which apparently just freezes.
|
944
|
-
# I guess that's because JsshSocket circularly references itself with its instance variables.
|
945
|
-
def to_json(options={})
|
946
|
-
ref
|
1222
|
+
# undefine Object#id, and... anything else I think of that needs undef'ing in the future
|
1223
|
+
[:id, :display].each do |method_name|
|
1224
|
+
if method_defined?(method_name)
|
1225
|
+
undef_method(method_name)
|
1226
|
+
end
|
947
1227
|
end
|
948
1228
|
|
1229
|
+
# returns this object passed through the $A function of the prototype javascript library.
|
949
1230
|
def to_js_array
|
950
1231
|
jssh_socket.object('$A').call(self)
|
951
1232
|
end
|
1233
|
+
# returns this object passed through the $H function of the prototype javascript library.
|
952
1234
|
def to_js_hash
|
953
1235
|
jssh_socket.object('$H').call(self)
|
954
1236
|
end
|
1237
|
+
# returns this object passed through a javascript function which copies each key onto a blank object and rescues any errors.
|
955
1238
|
def to_js_hash_safe
|
956
1239
|
jssh_socket.object('$_H').call(self)
|
957
1240
|
end
|
1241
|
+
# returns a JsshArray representing this object
|
958
1242
|
def to_array
|
959
1243
|
JsshArray.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
960
1244
|
end
|
1245
|
+
# returns a JsshHash representing this object
|
961
1246
|
def to_hash
|
962
1247
|
JsshHash.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
963
1248
|
end
|
1249
|
+
# returns a JsshDOMNode representing this object
|
964
1250
|
def to_dom
|
965
1251
|
JsshDOMNode.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
966
1252
|
end
|
1253
|
+
|
1254
|
+
# returns a ruby Hash. each key/value pair of this object
|
1255
|
+
# is represented in the returned hash.
|
1256
|
+
#
|
1257
|
+
# if an error is encountered trying to access the value for an attribute, then in the
|
1258
|
+
# returned hash, that attribute is set to the error that was encountered rather than
|
1259
|
+
# the actual value (since the value wasn't successfully retrieved).
|
1260
|
+
#
|
1261
|
+
# options may be specified. the only option currently supported is:
|
1262
|
+
# * :recurse => a number or nil. if it's a number, then this will recurse to that
|
1263
|
+
# depth. If it's nil, this won't recurse at all.
|
1264
|
+
#
|
1265
|
+
# below the specified recursion level, this will return this JsshObject rather than recursing
|
1266
|
+
# down into it.
|
1267
|
+
#
|
1268
|
+
# this function isn't expected to raise any errors, since encountered errors are set as
|
1269
|
+
# attribute values.
|
967
1270
|
def to_ruby_hash(options={})
|
968
1271
|
options={:recurse => 1}.merge(options)
|
969
1272
|
return self if !options[:recurse] || options[:recurse]==0
|
@@ -989,10 +1292,16 @@ class JsshObject
|
|
989
1292
|
end
|
990
1293
|
end
|
991
1294
|
|
1295
|
+
# returns an Array in which each element is the #val_or_Object of each element of this javascript array.
|
1296
|
+
def to_ruby_array
|
1297
|
+
self.to_array.to_a
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
# represents this javascript object in one line, displaying the type and debug name.
|
992
1301
|
def inspect
|
993
1302
|
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
|
994
1303
|
end
|
995
|
-
def pretty_print(pp)
|
1304
|
+
def pretty_print(pp) # :nodoc:
|
996
1305
|
pp.object_address_group(self) do
|
997
1306
|
pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
|
998
1307
|
pp.breakable ' '
|
@@ -1007,8 +1316,12 @@ class JsshObject
|
|
1007
1316
|
end
|
1008
1317
|
end
|
1009
1318
|
|
1319
|
+
# represents a node on the DOM. not substantially from JsshObject, but #inspect
|
1320
|
+
# is more informative, and #dump is defined for extensive debug info.
|
1321
|
+
#
|
1322
|
+
# This class is mostly useful for debug, not used anywhere in production at the moment.
|
1010
1323
|
class JsshDOMNode < JsshObject
|
1011
|
-
def inspect_stuff
|
1324
|
+
def inspect_stuff # :nodoc:
|
1012
1325
|
[:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map do |attrn|
|
1013
1326
|
attr=attr(attrn)
|
1014
1327
|
if ['undefined','null'].include?(attr.type)
|
@@ -1018,10 +1331,11 @@ class JsshDOMNode < JsshObject
|
|
1018
1331
|
end
|
1019
1332
|
end.compact
|
1020
1333
|
end
|
1334
|
+
# returns a string with a bunch of information about this dom node
|
1021
1335
|
def inspect
|
1022
1336
|
"\#<#{self.class.name} #{inspect_stuff.map{|(k,v)| "#{k}=#{v.inspect}"}.join(', ')}>"
|
1023
1337
|
end
|
1024
|
-
def pretty_print(pp)
|
1338
|
+
def pretty_print(pp) # :nodoc:
|
1025
1339
|
pp.object_address_group(self) do
|
1026
1340
|
pp.seplist(inspect_stuff, lambda { pp.text ',' }) do |attr_val|
|
1027
1341
|
pp.breakable ' '
|
@@ -1034,6 +1348,10 @@ class JsshDOMNode < JsshObject
|
|
1034
1348
|
end
|
1035
1349
|
end
|
1036
1350
|
end
|
1351
|
+
# returns a string (most useful when written to STDOUT or to a file) consisting of this dom node
|
1352
|
+
# and its child nodes, recursively. each node is one line and depth is indicated by spacing.
|
1353
|
+
#
|
1354
|
+
# call #dump(:recurse => n) to recurse down only n levels. default is to recurse all the way down the dom tree.
|
1037
1355
|
def dump(options={})
|
1038
1356
|
options={:recurse => nil, :level => 0}.merge(options)
|
1039
1357
|
next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
|
@@ -1049,7 +1367,11 @@ class JsshDOMNode < JsshObject
|
|
1049
1367
|
end
|
1050
1368
|
end
|
1051
1369
|
|
1370
|
+
# this class represents a javascript array - that is, a javascript object that has a 'length'
|
1371
|
+
# attribute which is a non-negative integer, and returns elements at each subscript from 0
|
1372
|
+
# to less than than that length.
|
1052
1373
|
class JsshArray < JsshObject
|
1374
|
+
# yields the element at each subscript of this javascript array, from 0 to self.length.
|
1053
1375
|
def each
|
1054
1376
|
length=self.length
|
1055
1377
|
raise JsshError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
|
@@ -1063,35 +1385,34 @@ class JsshArray < JsshObject
|
|
1063
1385
|
end
|
1064
1386
|
end
|
1065
1387
|
include Enumerable
|
1066
|
-
def to_json(options={}) # Enumerable clobbers this; redefine
|
1067
|
-
ref
|
1068
|
-
end
|
1069
1388
|
end
|
1070
1389
|
|
1390
|
+
# this class represents a hash, or 'object' type in javascript.
|
1071
1391
|
class JsshHash < JsshObject
|
1392
|
+
# returns an array of keys of this javascript object
|
1072
1393
|
def keys
|
1073
|
-
|
1074
|
-
{ var keys=[];
|
1075
|
-
for(var key in obj)
|
1076
|
-
{ keys.push(key);
|
1077
|
-
}
|
1078
|
-
return keys;
|
1079
|
-
}"
|
1080
|
-
@keys=jssh_socket.object(keyfunc).pass(self).val
|
1394
|
+
@keys=jssh_socket.call_function(:obj => self){ "var keys=[]; for(var key in obj) { keys.push(key); } return keys;" }.val
|
1081
1395
|
end
|
1082
|
-
|
1396
|
+
# returns whether the given key is a defined key of this javascript object
|
1397
|
+
def key?(key)
|
1398
|
+
jssh_socket.call_function(:obj => self, :key => key){ "return key in obj;" }
|
1399
|
+
end
|
1400
|
+
# yields each key and value
|
1401
|
+
def each(&block) # :yields: key, value
|
1083
1402
|
keys.each do |key|
|
1084
|
-
|
1403
|
+
if block.arity==1
|
1404
|
+
yield [key, self[key]]
|
1405
|
+
else
|
1406
|
+
yield key, self[key]
|
1407
|
+
end
|
1085
1408
|
end
|
1086
1409
|
end
|
1410
|
+
# yields each key and value for this object
|
1087
1411
|
def each_pair
|
1088
|
-
each do |
|
1089
|
-
yield
|
1412
|
+
each do |key,value|
|
1413
|
+
yield key,value
|
1090
1414
|
end
|
1091
1415
|
end
|
1092
1416
|
|
1093
1417
|
include Enumerable
|
1094
|
-
def to_json(options={}) # Enumerable clobbers this; redefine
|
1095
|
-
ref
|
1096
|
-
end
|
1097
1418
|
end
|