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.
@@ -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
- class Object
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
- attr_reader :ip, :port, :prototype
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] || @@default_jssh_ip
93
- @port=options[:jssh_port] || @@default_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
- temp_object.assign({})
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
- # >> @socket.send "3\n4\n5\n", 0
155
- # => 6
156
- # >> read_value
157
- # => "3\n> 4\n> 5"
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 => 65536}.merge(options)
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 was: #{cleared_error}"
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
- # this will continue trying for DEFAULT_SOCKET_TIMEOUT until
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(65536)
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
- public
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(:"#{attr}=", stuff[attr])
328
+ err.send("#{attr}=", stuff[attr])
307
329
  end
308
330
  end
309
331
  raise err
310
332
  end
311
- private :js_error
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=="SyntaxError: syntax error"
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
- # >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
417
- # => {"foo"=>["baz", "qux"]}
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.to_jssh
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.to_jssh}
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, "Cannot invoke JSON on a Jssh session that does not have the Prototype library"
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 ActiveSupport::JSON.decode
497
- # Raises ActiveSupport::JSON::ParseError if given a blank string, something that is not a string, or
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
- def object(ref)
516
- JsshObject.new(ref, self, :debug_name => ref)
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
- def object_in_temp(ref)
519
- object(ref).store_rand_temp
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 ||= object('JsshTemp')
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 ||= object('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
- @getwindows ||= object('getWindows()')
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
- attr_reader :ref, :jssh_socket
555
- attr_reader :type, :function_result, :debug_name
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
- # returns javascript instanceof operator on this and the given interface (expected to be a JsshObject)
601
- # note that this is javascript, not to be confused with ruby's #instance_of? method.
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
- # window.instanceof(window.jssh_socket.Components.interfaces.nsIDOMChromeWindow)
605
- # => true
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
- # returns a JsshObject representing the given attribute. Checks the type, and if
654
- # it is a function, references the _return value_ of the function (with the given
655
- # arguments, if any, which are in ruby, converted to_jssh). If the type of the
656
- # expression is undefined, raises an error (if you want an attribute even if it's
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 args.empty?
666
- attr_obj.val_or_object
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, "Cannot pass arguments to expression #{attr_obj.ref} of type #{type}"
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 (via JsshSocket#assign) the given ruby value (converted to_jssh) to the reference
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.to_jssh})"
685
- result=assign_expr val.to_jssh
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 this object - assumes that this object is a function - passing
701
- # this function the specified arguments, which are converted to_jssh
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.to_jssh}.join(', ')})", jssh_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JsshObject) ? arg.debug_name : arg.to_jssh}.join(', ')})")
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 to_jssh).
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
- # >> foo=document.getElementById('guser').store('foo')
718
- # => #<JsshObject:0x2dff870 @ref="foo" ...>
719
- # >> foo.tagName
720
- # => "DIV"
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=6
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
- def store_rand_prefix(prefix)
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 via to_jssh
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.to_jssh}]", jssh_socket, :debug_name => "#{debug_name}[#{key.is_a?(JsshObject) ? key.debug_name : key.to_jssh}]")
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
- # subscript is specified as ruby (converted to_jssh).
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
- # assigns the given ruby value (passed through json via JsshSocket#assign_json) to the given subscript
772
- # (key is converted to_jssh).
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.to_jssh})", jssh_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JsshObject) ? operand.debug_name : operand.to_jssh})").val_or_object
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 JsshSocket#assign_json
820
- # to do the assignment and returns the assigned value.
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 question mark (?), then it will attempt to get a string representing the
828
- # value, using JsonObject#val_str. This is safer than ! because the javascript conversion to json
829
- # can error. This also catches the JsshUndefinedValueError that can occur, and just returns nil
830
- # for undefined stuff.
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
- # otherwise, method_missing calls to #invoke, and returns a JsshObject, a string, a boolean, a number, or
833
- # null - see documentation for #invoke.
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
- # Since #invoke returns a JsshObject for javascript objects, this means that you can string together
836
- # method_missings and the result looks rather like javascript.
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
- # >> js_hash=jssh_socket.object('$H')
859
- # => #<JsshObject:0x2beb598 @ref="$H" ...>
860
- # >> js_arr=jssh_socket.object('$A')
861
- # => #<JsshObject:0x2be40e0 @ref="$A" ...>
862
- #
863
- # >> js_arr.pass(document.body.childNodes).pluck! :tagName
864
- # => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
865
- # >> js_arr.pass(document.body.childNodes).pluck! :id
866
- # => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
867
- # >> js_hash.pass(document.getElementById('tbd')).keys!
868
- # => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", "style", "innerHTML", "nextSibling", "tagName", "id", "nodeName", "nodeValue", "nodeType", "childNodes", "firstChild", "lastChild", "previousSibling", "attributes", "ownerDocument", "insertBefore", "replaceChild", "removeChild", "hasChildNodes", "cloneNode", "normalize", "isSupported", "namespaceURI", "prefix", "localName", "hasAttributes", "getAttribute", "setAttribute", "removeAttribute", "getAttributeNode", "setAttributeNode", "removeAttributeNode", "getAttributeNS", "setAttributeNS", "removeAttributeNS", "getAttributeNodeNS", "setAttributeNodeNS", "getElementsByTagNameNS", "hasAttribute", "hasAttributeNS", "ELEMENT_NODE", "ATTRIBUTE_NODE", "TEXT_NODE", "CDATA_SECTION_NODE", "ENTITY_REFERENCE_NODE", "ENTITY_NODE", "PROCESSING_INSTRUCTION_NODE", "COMMENT_NODE", "DOCUMENT_NODE", "DOCUMENT_TYPE_NODE", "DOCUMENT_FRAGMENT_NODE", "NOTATION_NODE", "lang", "dir", "align", "offsetTop", "offsetLeft", "offsetWidth", "offsetHeight", "offsetParent", "scrollTop", "scrollLeft", "scrollHeight", "scrollWidth", "clientTop", "clientLeft", "clientHeight", "clientWidth", "tabIndex", "contentEditable", "blur", "focus", "spellcheck", "removeEventListener", "dispatchEvent", "baseURI", "compareDocumentPosition", "textContent", "isSameNode", "lookupPrefix", "isDefaultNamespace", "lookupNamespaceURI", "isEqualNode", "getFeature", "setUserData", "getUserData", "DOCUMENT_POSITION_DISCONNECTED", "DOCUMENT_POSITION_PRECEDING", "DOCUMENT_POSITION_FOLLOWING", "DOCUMENT_POSITION_CONTAINS", "DOCUMENT_POSITION_CONTAINED_BY", "DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC", "getElementsByClassName", "getClientRects", "getBoundingClientRect"]
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
- special = $2
875
- else # don't deal with any special character crap
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
- Object.instance_method(:method_missing).bind(self).call(method, *args) # this shouldn't happen
1176
+ # don't deal with any special character crap
1177
+ super
896
1178
  end
897
1179
  end
898
- def define_methods!
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
- self.to_hash.keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
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
- def respond_to?(method)
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
- special = $2
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 special=='='
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
- def id(*args)
934
- invoke :id, *args
935
- end
936
-
937
- # gives a reference for this object. this is the only class for which to_jssh doesn't
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
- keyfunc="function(obj)
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
- def each
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
- yield [key, self[key]]
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 |(k,v)|
1089
- yield k,v
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