vapir-firefox 1.8.1 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/vapir-firefox/browser.rb +226 -115
- data/lib/vapir-firefox/clear_tracks.rb +6 -6
- data/lib/vapir-firefox/config.rb +5 -0
- data/lib/vapir-firefox/container.rb +74 -65
- data/lib/vapir-firefox/element.rb +13 -13
- data/lib/vapir-firefox/firefox_socket/base.rb +790 -0
- data/lib/vapir-firefox/firefox_socket/jssh.rb +49 -0
- data/lib/vapir-firefox/firefox_socket/mozrepl.rb +58 -0
- data/lib/vapir-firefox/{prototype.functional.js → firefox_socket/prototype.functional.js} +0 -0
- data/lib/vapir-firefox/javascript_object.rb +736 -0
- data/lib/vapir-firefox/modal_dialog.rb +4 -4
- data/lib/vapir-firefox/page_container.rb +4 -4
- data/lib/vapir-firefox/version.rb +1 -1
- metadata +16 -13
- data/lib/vapir-firefox/jssh_socket.rb +0 -1418
@@ -31,8 +31,8 @@ module Vapir
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def exists?
|
34
|
-
#
|
35
|
-
@modal_window && @browser.
|
34
|
+
# firefox_socket may be nil if the window has closed
|
35
|
+
@modal_window && @browser.firefox_socket && @browser.class.window_objects.any?{|window_object| window_object==@modal_window }
|
36
36
|
end
|
37
37
|
|
38
38
|
def text
|
@@ -107,7 +107,7 @@ module Vapir
|
|
107
107
|
|
108
108
|
def initialize(containing_modal_dialog, options={})
|
109
109
|
options=handle_options(options, :timeout => ModalDialog::DEFAULT_TIMEOUT, :error => true)
|
110
|
-
@
|
110
|
+
@firefox_socket=containing_modal_dialog.browser.firefox_socket
|
111
111
|
@browser_object=containing_modal_dialog.modal_window.getBrowser
|
112
112
|
|
113
113
|
@containing_modal_dialog=containing_modal_dialog
|
@@ -139,6 +139,6 @@ module Vapir
|
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
|
-
attr_reader :
|
142
|
+
attr_reader :firefox_socket
|
143
143
|
end
|
144
144
|
end
|
@@ -36,24 +36,24 @@ module Vapir
|
|
36
36
|
# their methods from the top-level context, you get an exception:
|
37
37
|
#
|
38
38
|
# >> browser.element(:tag_name => 'embed').element_object.PercentLoaded()
|
39
|
-
#
|
39
|
+
# FirefoxSocketJavascriptError: NPMethod called on non-NPObject wrapped JSObject!
|
40
40
|
#
|
41
41
|
# but, this method executes script in the context of the content window, so the following works:
|
42
42
|
#
|
43
43
|
# >> browser.execute_script('element.PercentLoaded()', :element => browser.element(:tag_name => 'embed').element_object)
|
44
44
|
# => 100
|
45
45
|
def execute_script(javascript, other_variables={})
|
46
|
-
sandbox=
|
46
|
+
sandbox=firefox_socket.Components.utils.Sandbox(content_window_object)
|
47
47
|
sandbox.window=content_window_object.window
|
48
48
|
other_variables.each do |name, var|
|
49
49
|
sandbox[name]=var
|
50
50
|
end
|
51
|
-
return
|
51
|
+
return firefox_socket.Components.utils.evalInSandbox('with(window) { '+javascript+' }', sandbox)
|
52
52
|
end
|
53
53
|
|
54
54
|
# Returns the html of the document
|
55
55
|
def outer_html
|
56
|
-
|
56
|
+
firefox_socket.call_function(:document => document_object) do %Q(
|
57
57
|
var temp_el=document.createElement('div');
|
58
58
|
for(var i in document.childNodes)
|
59
59
|
{ try
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vapir-firefox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 51
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 1.
|
8
|
+
- 9
|
9
|
+
- 0
|
10
|
+
version: 1.9.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ethan
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-08-04 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -26,12 +26,12 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - "="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
hash:
|
29
|
+
hash: 51
|
30
30
|
segments:
|
31
31
|
- 1
|
32
|
-
-
|
33
|
-
-
|
34
|
-
version: 1.
|
32
|
+
- 9
|
33
|
+
- 0
|
34
|
+
version: 1.9.0
|
35
35
|
type: :runtime
|
36
36
|
version_requirements: *id001
|
37
37
|
- !ruby/object:Gem::Dependency
|
@@ -48,7 +48,7 @@ dependencies:
|
|
48
48
|
version: "0"
|
49
49
|
type: :runtime
|
50
50
|
version_requirements: *id002
|
51
|
-
description: " Vapir-Firefox is a library to programatically drive the Firefox\n browser
|
51
|
+
description: " Vapir-Firefox is a library to programatically drive the Firefox\n browser, exposing a simple-to-use and powerful API to make automated \n testing a simple and joyous affair. \n Forked from the Watir library. \n"
|
52
52
|
email: vapir@googlegroups.com
|
53
53
|
executables: []
|
54
54
|
|
@@ -89,8 +89,11 @@ files:
|
|
89
89
|
- lib/vapir-firefox/elements/text_field.rb
|
90
90
|
- lib/vapir-firefox/elements.rb
|
91
91
|
- lib/vapir-firefox/clear_tracks.rb
|
92
|
-
- lib/vapir-firefox/
|
93
|
-
- lib/vapir-firefox/
|
92
|
+
- lib/vapir-firefox/firefox_socket/base.rb
|
93
|
+
- lib/vapir-firefox/firefox_socket/jssh.rb
|
94
|
+
- lib/vapir-firefox/firefox_socket/mozrepl.rb
|
95
|
+
- lib/vapir-firefox/firefox_socket/prototype.functional.js
|
96
|
+
- lib/vapir-firefox/javascript_object.rb
|
94
97
|
has_rdoc: true
|
95
98
|
homepage: http://www.vapir.org/
|
96
99
|
licenses: []
|
@@ -124,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
127
|
- 0
|
125
128
|
version: "0"
|
126
129
|
requirements:
|
127
|
-
- Firefox browser with JSSH extension installed
|
130
|
+
- Firefox browser with MozRepl or JSSH extension installed
|
128
131
|
rubyforge_project:
|
129
132
|
rubygems_version: 1.3.7
|
130
133
|
signing_key:
|
@@ -1,1418 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'socket'
|
3
|
-
require 'timeout'
|
4
|
-
#require 'logger'
|
5
|
-
|
6
|
-
# :stopdoc:
|
7
|
-
#class LoggerWithCallstack < Logger
|
8
|
-
# class TimeElapsedFormatter < Formatter
|
9
|
-
# def initialize
|
10
|
-
# super
|
11
|
-
# @time_started=Time.now
|
12
|
-
# end
|
13
|
-
# def format_datetime(time)
|
14
|
-
# "%10.3f"%(time.to_f-@time_started.to_f)
|
15
|
-
# end
|
16
|
-
#
|
17
|
-
# end
|
18
|
-
# def add(severity, message = nil, progname = nil, &block)
|
19
|
-
# severity ||= UNKNOWN
|
20
|
-
# if @logdev.nil? or severity < @level
|
21
|
-
# return true
|
22
|
-
# end
|
23
|
-
# progname ||= @progname
|
24
|
-
# if message.nil?
|
25
|
-
# if block_given?
|
26
|
-
# message = yield
|
27
|
-
# else
|
28
|
-
# message = progname
|
29
|
-
# progname = @progname
|
30
|
-
# end
|
31
|
-
# end
|
32
|
-
# message=message.to_s+" FROM: "+caller.map{|c|"\t\t#{c}\n"}.join("")
|
33
|
-
# @logdev.write(
|
34
|
-
# format_message(format_severity(severity), Time.now, progname, message))
|
35
|
-
# true
|
36
|
-
# end
|
37
|
-
#end
|
38
|
-
|
39
|
-
# :startdoc:
|
40
|
-
|
41
|
-
# base exception class for all exceptions raised from Jssh sockets and objects.
|
42
|
-
class JsshError < StandardError;end
|
43
|
-
# this exception covers all connection errors either on startup or during usage. often it represents an Errno error such as Errno::ECONNRESET.
|
44
|
-
class JsshConnectionError < JsshError;end
|
45
|
-
# This exception is thrown if we are unable to connect to JSSh.
|
46
|
-
class JsshUnableToStart < JsshConnectionError;end
|
47
|
-
# Represents an error encountered on the javascript side, caught in a try/catch block.
|
48
|
-
class JsshJavascriptError < JsshError
|
49
|
-
attr_accessor :source, :js_err, :lineNumber, :stack, :fileName
|
50
|
-
end
|
51
|
-
# represents a syntax error in javascript.
|
52
|
-
class JsshSyntaxError < JsshJavascriptError;end
|
53
|
-
# raised when a javascript value is expected to be defined but is undefined
|
54
|
-
class JsshUndefinedValueError < JsshJavascriptError;end
|
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.
|
58
|
-
class JsshSocket
|
59
|
-
# :stopdoc:
|
60
|
-
# def self.logger
|
61
|
-
# @@logger||=begin
|
62
|
-
# logfile=File.open('c:/tmp/jssh_log.txt', File::WRONLY|File::TRUNC|File::CREAT)
|
63
|
-
# logfile.sync=true
|
64
|
-
# logger=Logger.new(logfile)
|
65
|
-
# logger.level = -1#Logger::DEBUG#Logger::INFO
|
66
|
-
# #logger.formatter=LoggerWithCallstack::TimeElapsedFormatter.new
|
67
|
-
# logger
|
68
|
-
# end
|
69
|
-
# end
|
70
|
-
# def logger
|
71
|
-
# self.class.logger
|
72
|
-
# end
|
73
|
-
|
74
|
-
PROMPT="\n> "
|
75
|
-
|
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
|
83
|
-
|
84
|
-
# maximum time JsshSocket waits for a value to be sent before giving up
|
85
|
-
DEFAULT_SOCKET_TIMEOUT=64
|
86
|
-
# maximum time JsshSocket will wait for additional reads on a socket that is actively sending
|
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
|
90
|
-
|
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
|
97
|
-
|
98
|
-
# Connects a new socket to jssh
|
99
|
-
#
|
100
|
-
# Takes options:
|
101
|
-
# * :jssh_ip => the ip to connect to, default 127.0.0.1
|
102
|
-
# * :jssh_port => the port to connect to, default 9997
|
103
|
-
# * :send_prototype => true|false, whether to load and send the Prototype library (the functional programming part of it anyway, and JSON bits)
|
104
|
-
def initialize(options={})
|
105
|
-
@ip=options[:jssh_ip] || DEFAULT_IP
|
106
|
-
@port=options[:jssh_port] || DEFAULT_PORT
|
107
|
-
@prototype=options.key?(:send_prototype) ? options[:send_prototype] : true
|
108
|
-
begin
|
109
|
-
@socket = TCPSocket::new(@ip, @port)
|
110
|
-
@socket.sync = true
|
111
|
-
@expecting_prompt=false # initially, the welcome message comes before the prompt, so this so this is false to start with
|
112
|
-
@expecting_extra_maybe=false
|
113
|
-
welcome="Welcome to the Mozilla JavaScript Shell!\n"
|
114
|
-
read=read_value
|
115
|
-
if !read
|
116
|
-
@expecting_extra_maybe=true
|
117
|
-
raise JsshUnableToStart, "Something went wrong initializing - no response"
|
118
|
-
elsif read != welcome
|
119
|
-
@expecting_extra_maybe=true
|
120
|
-
raise JsshUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}"
|
121
|
-
end
|
122
|
-
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
|
123
|
-
err=JsshUnableToStart.new("Could not connect to JSSH sever #{@ip}:#{@port}. Ensure that Firefox is running and has JSSH configured, or try restarting firefox.\nMessage from TCPSocket:\n#{$!.message}")
|
124
|
-
err.set_backtrace($!.backtrace)
|
125
|
-
raise err
|
126
|
-
end
|
127
|
-
if @prototype
|
128
|
-
ret=send_and_read(File.read(PrototypeFile))
|
129
|
-
if ret != "done!"
|
130
|
-
@expecting_extra_maybe=true
|
131
|
-
raise JsshError, "Something went wrong loading Prototype - message #{ret.inspect}"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
ret=send_and_read("(function()
|
135
|
-
{ nativeJSON=Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON);
|
136
|
-
nativeJSON_encode_length=function(object)
|
137
|
-
{ var encoded=nativeJSON.encode(object);
|
138
|
-
return encoded.length.toString()+\"\\n\"+encoded;
|
139
|
-
}
|
140
|
-
return 'done!';
|
141
|
-
})()")
|
142
|
-
if ret != "done!"
|
143
|
-
@expecting_extra_maybe=true
|
144
|
-
raise JsshError, "Something went wrong initializing native JSON - message #{ret.inspect}"
|
145
|
-
end
|
146
|
-
root.JsshTemp={}
|
147
|
-
end
|
148
|
-
|
149
|
-
private
|
150
|
-
# sets the error state if an exception is encountered while running the given block. the
|
151
|
-
# exception is not rescued.
|
152
|
-
def ensuring_extra_handled
|
153
|
-
begin
|
154
|
-
yield
|
155
|
-
rescue Exception
|
156
|
-
@expecting_extra_maybe = true
|
157
|
-
raise
|
158
|
-
end
|
159
|
-
end
|
160
|
-
# reads from the socket and returns what seems to be the value that should be returned, by stripping prompts
|
161
|
-
# from the beginning and end where appropriate.
|
162
|
-
#
|
163
|
-
# does not deal with prompts in between values, because attempting to parse those out is impossible, it being
|
164
|
-
# perfectly possible that a string the same as the prompt is part of actual data. (even stripping it from the
|
165
|
-
# ends of the string is not entirely certain; data could have it at the ends too, but that's the best that can
|
166
|
-
# be done.) so, read_value should be called after every line, or you can end up with stuff like:
|
167
|
-
#
|
168
|
-
# >> @socket.send "3\n4\n5\n", 0
|
169
|
-
# => 6
|
170
|
-
# >> read_value
|
171
|
-
# => "3\n> 4\n> 5"
|
172
|
-
#
|
173
|
-
# by default, read_value reads until the socket is done being ready. "done being ready" is defined as Kernel.select
|
174
|
-
# saying that the socket isn't ready after waiting for SHORT_SOCKET_TIMEOUT. usually this will be true after a
|
175
|
-
# single read, as most things only take one #recv call to get the whole value. this waiting for SHORT_SOCKET_TIMEOUT
|
176
|
-
# can add up to being slow if you're doing a lot of socket activity.
|
177
|
-
#
|
178
|
-
# to solve this, performance can be improved significantly using the :length_before_value option. with this, you have
|
179
|
-
# to write your javascript to return the length of the value to be sent, followed by a newline, followed by the actual
|
180
|
-
# value (which must be of the length it says it is, or this method will error).
|
181
|
-
#
|
182
|
-
# if this option is set, this doesn't do any SHORT_SOCKET_TIMEOUT waiting once it gets the full value, it returns
|
183
|
-
# immediately.
|
184
|
-
def read_value(options={})
|
185
|
-
options={:timeout => DEFAULT_SOCKET_TIMEOUT, :length_before_value => false, :read_size => READ_SIZE}.merge(options)
|
186
|
-
received_data = []
|
187
|
-
value_string = ""
|
188
|
-
size_to_read=options[:read_size]
|
189
|
-
timeout=options[:timeout]
|
190
|
-
already_read_length=false
|
191
|
-
expected_size=nil
|
192
|
-
# logger.add(-1) { "RECV_SOCKET is starting. timeout=#{timeout}" }
|
193
|
-
while size_to_read > 0 && ensuring_extra_handled { Kernel.select([@socket] , nil , nil, timeout) }
|
194
|
-
data = ensuring_extra_handled { @socket.recv(size_to_read) }
|
195
|
-
received_data << data
|
196
|
-
value_string << data
|
197
|
-
if @expecting_prompt && utf8_length_safe(value_string) > PROMPT.length
|
198
|
-
if value_string =~ /\A#{Regexp.escape(PROMPT)}/
|
199
|
-
value_string.sub!(/\A#{Regexp.escape(PROMPT)}/, '')
|
200
|
-
@expecting_prompt=false
|
201
|
-
else
|
202
|
-
value_string << clear_error
|
203
|
-
raise JsshError, "Expected a prompt! received unexpected data #{value_string.inspect}. maybe left on the socket by last evaluated expression? last expression was:\n\n#{@last_expression}"
|
204
|
-
end
|
205
|
-
end
|
206
|
-
if !@expecting_prompt
|
207
|
-
if options[:length_before_value] && !already_read_length && value_string.length > 0
|
208
|
-
if value_string =~ /\A(\d+)\n/
|
209
|
-
expected_size=$1.to_i
|
210
|
-
already_read_length=true
|
211
|
-
value_string.sub!(/\A\d+\n/, '')
|
212
|
-
elsif value_string =~ /\A\d+\z/
|
213
|
-
# rather unlikely, but maybe we just received part of the number so far - ignore
|
214
|
-
else
|
215
|
-
@expecting_extra_maybe=true
|
216
|
-
raise JsshError, "Expected length! unexpected data with no preceding length received: #{value_string.inspect}"
|
217
|
-
end
|
218
|
-
end
|
219
|
-
if expected_size
|
220
|
-
size_to_read = expected_size - utf8_length_safe(value_string)
|
221
|
-
end
|
222
|
-
unless value_string.empty? # switch to short timeout - unless we got a prompt (leaving value_string blank). switching to short timeout when all we got was a prompt would probably accidentally leave the value on the socket.
|
223
|
-
timeout=SHORT_SOCKET_TIMEOUT
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
# Kernel.select seems to indicate that a dead socket is ready to read, and returns endless blank strings to recv. rather irritating.
|
228
|
-
if received_data.length >= 3 && received_data[-3..-1].all?{|rd| rd==''}
|
229
|
-
raise JsshConnectionError, "Socket seems to no longer be connected"
|
230
|
-
end
|
231
|
-
# logger.add(-1) { "RECV_SOCKET is continuing. timeout=#{timeout}; data=#{data.inspect}" }
|
232
|
-
end
|
233
|
-
# logger.debug { "RECV_SOCKET is done. received_data=#{received_data.inspect}; value_string=#{value_string.inspect}" }
|
234
|
-
if @expecting_extra_maybe
|
235
|
-
if Kernel.select([@socket] , nil , nil, SHORT_SOCKET_TIMEOUT)
|
236
|
-
cleared_error=clear_error
|
237
|
-
if cleared_error==PROMPT
|
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
|
239
|
-
value_string << cleared_error
|
240
|
-
else
|
241
|
-
raise JsshError, "We finished receiving but the socket was still ready to send! extra data received were: #{cleared_error}"
|
242
|
-
end
|
243
|
-
end
|
244
|
-
@expecting_extra_maybe=false
|
245
|
-
end
|
246
|
-
|
247
|
-
if expected_size
|
248
|
-
value_string_length=value_string.unpack("U*").length # JSSH returns a utf-8 string, so unpack each character to get the right length
|
249
|
-
|
250
|
-
if value_string_length == expected_size
|
251
|
-
@expecting_prompt=true
|
252
|
-
elsif value_string_length == expected_size + PROMPT.length && value_string =~ /#{Regexp.escape(PROMPT)}\z/
|
253
|
-
value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
|
254
|
-
@expecting_prompt=false
|
255
|
-
else
|
256
|
-
@expecting_extra_maybe=true if value_string_length < expected_size
|
257
|
-
raise JsshError, "Expected a value of size #{expected_size}; received data of size #{value_string_length}: #{value_string.inspect}"
|
258
|
-
end
|
259
|
-
else
|
260
|
-
if value_string =~ /#{Regexp.escape(PROMPT)}\z/ # what if the value happens to end with the same string as the prompt?
|
261
|
-
value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
|
262
|
-
@expecting_prompt=false
|
263
|
-
else
|
264
|
-
@expecting_prompt=true
|
265
|
-
end
|
266
|
-
end
|
267
|
-
return value_string
|
268
|
-
end
|
269
|
-
|
270
|
-
private
|
271
|
-
# returns the number of complete utf-8 encoded characters in the string, without erroring on
|
272
|
-
# partial characters.
|
273
|
-
def utf8_length_safe(string)
|
274
|
-
string=string.dup
|
275
|
-
begin
|
276
|
-
string.unpack("U*").length
|
277
|
-
rescue ArgumentError # this happens when the socket receive gets split across a utf8 character. we drop the incomplete character from the end.
|
278
|
-
if $!.message =~ /malformed UTF-8 character \(expected \d+ bytes, given (\d+) bytes\)/
|
279
|
-
given=$1.to_i
|
280
|
-
string[0...(-given)].unpack("U*").length
|
281
|
-
else # otherwise, this is some other issue we weren't expecting; we will not rescue it.
|
282
|
-
raise
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
286
|
-
# this should be called when an error occurs and we want to clear the socket of any value remaining on it.
|
287
|
-
# tries for SHORT_SOCKET_TIMEOUT to see if a value will appear on the socket; if one does, returns it.
|
288
|
-
def clear_error
|
289
|
-
data=""
|
290
|
-
while Kernel.select([@socket], nil, nil, SHORT_SOCKET_TIMEOUT)
|
291
|
-
# clear any other crap left on the socket
|
292
|
-
data << @socket.recv(READ_SIZE)
|
293
|
-
end
|
294
|
-
if data =~ /#{Regexp.escape(PROMPT)}\z/
|
295
|
-
@expecting_prompt=false
|
296
|
-
end
|
297
|
-
data
|
298
|
-
end
|
299
|
-
|
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.
|
303
|
-
def send_and_read(js_expr, options={})
|
304
|
-
# logger.add(-1) { "SEND_AND_READ is starting. options=#{options.inspect}" }
|
305
|
-
@last_expression=js_expr
|
306
|
-
js_expr=js_expr+"\n" unless js_expr =~ /\n\z/
|
307
|
-
# logger.debug { "SEND_AND_READ sending #{js_expr.inspect}" }
|
308
|
-
@socket.send(js_expr, 0)
|
309
|
-
return read_value(options)
|
310
|
-
end
|
311
|
-
|
312
|
-
private
|
313
|
-
# creates a ruby exception from the given information and raises it.
|
314
|
-
def js_error(errclassname, message, source, stuff={})
|
315
|
-
errclass=if errclassname
|
316
|
-
unless JsshError.const_defined?(errclassname)
|
317
|
-
JsshError.const_set(errclassname, Class.new(JsshJavascriptError))
|
318
|
-
end
|
319
|
-
JsshError.const_get(errclassname)
|
320
|
-
else
|
321
|
-
JsshJavascriptError
|
322
|
-
end
|
323
|
-
err=errclass.new("#{message}\nEvaluating:\n#{source}\n\nOther stuff:\n#{stuff.inspect}")
|
324
|
-
err.source=source
|
325
|
-
err.js_err=stuff
|
326
|
-
["lineNumber", "stack", "fileName"].each do |attr|
|
327
|
-
if stuff.key?(attr)
|
328
|
-
err.send("#{attr}=", stuff[attr])
|
329
|
-
end
|
330
|
-
end
|
331
|
-
raise err
|
332
|
-
end
|
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
|
357
|
-
|
358
|
-
# returns the value of the given javascript expression, as reported by JSSH.
|
359
|
-
#
|
360
|
-
# This will be a string, the given expression's toString.
|
361
|
-
def value(js)
|
362
|
-
# this is wrapped in a function so that ...
|
363
|
-
# dang, now I can't remember. I'm sure I had a good reason at the time.
|
364
|
-
send_and_read("(function(){return #{js}})()")
|
365
|
-
end
|
366
|
-
|
367
|
-
# assigns to the javascript reference on the left the javascript expression on the right.
|
368
|
-
# returns the value of the expression as reported by JSSH, which
|
369
|
-
# will be a string, the expression's toString. Uses #value; see its documentation.
|
370
|
-
def assign(js_left, js_right)
|
371
|
-
value("#{js_left}= #{js_right}")
|
372
|
-
end
|
373
|
-
|
374
|
-
# calls to the given function (javascript reference to a function) passing it the
|
375
|
-
# given arguments (javascript expressions). returns the return value of the function,
|
376
|
-
# a string, the toString of the javascript value. Uses #value; see its documentation.
|
377
|
-
def call(js_function, *js_args)
|
378
|
-
value("#{js_function}(#{js_args.join(', ')})")
|
379
|
-
end
|
380
|
-
|
381
|
-
# if the given javascript expression ends with an = symbol, #handle calls to #assign
|
382
|
-
# assuming it is given one argument; if the expression refers to a function, calls
|
383
|
-
# that function with the given arguments using #call; if the expression is some other
|
384
|
-
# value, returns that value (its javascript toString), calling #value, assuming
|
385
|
-
# given no arguments. Uses #value; see its documentation.
|
386
|
-
def handle(js_expr, *args)
|
387
|
-
if js_expr=~/=\z/ # doing assignment
|
388
|
-
js_left=$`
|
389
|
-
if args.size != 1
|
390
|
-
raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
|
391
|
-
end
|
392
|
-
assign(js_left, *args)
|
393
|
-
else
|
394
|
-
type=typeof(js_expr)
|
395
|
-
case type
|
396
|
-
when "function"
|
397
|
-
call(js_expr, *args)
|
398
|
-
when "undefined"
|
399
|
-
raise JsshUndefinedValueError, "undefined expression #{js_expr.inspect}"
|
400
|
-
else
|
401
|
-
if !args.empty?
|
402
|
-
raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
|
403
|
-
end
|
404
|
-
value(js_expr)
|
405
|
-
end
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
# returns the value of the given javascript expression. Assuming that it can
|
410
|
-
# be converted to JSON, will return the equivalent ruby data type to the javascript
|
411
|
-
# value. Will raise an error if the javascript errors.
|
412
|
-
def value_json(js, options={})
|
413
|
-
options={:error_on_undefined => true}.merge(options)
|
414
|
-
raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
|
415
|
-
ensure_prototype
|
416
|
-
ref_error=options[:error_on_undefined] ? "typeof(result)=='undefined' ? {errored: true, value: {'name': 'ReferenceError', 'message': 'undefined expression in: '+result_f.toString()}} : " : ""
|
417
|
-
wrapped_js=
|
418
|
-
"try
|
419
|
-
{ var result_f=(function(){return #{js}});
|
420
|
-
var result=result_f();
|
421
|
-
nativeJSON_encode_length(#{ref_error} {errored: false, value: result});
|
422
|
-
}catch(e)
|
423
|
-
{ nativeJSON_encode_length({errored: true, value: Object.extend({}, e)});
|
424
|
-
}"
|
425
|
-
val=send_and_read(wrapped_js, options.merge(:length_before_value => true))
|
426
|
-
error_or_val_json(val, js)
|
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.
|
432
|
-
def error_or_val_json(val, js)
|
433
|
-
if !val || val==''
|
434
|
-
@expecting_extra_maybe=true
|
435
|
-
raise JsshError, "received no value! may have timed out waiting for a value that was not coming."
|
436
|
-
end
|
437
|
-
if val=~ /\ASyntaxError: /
|
438
|
-
raise JsshSyntaxError, val
|
439
|
-
end
|
440
|
-
errord_and_val=parse_json(val)
|
441
|
-
unless errord_and_val.is_a?(Hash) && errord_and_val.keys.sort == ['errored', 'value'].sort
|
442
|
-
raise RuntimeError, "unexpected result: \n\t#{errord_and_val.inspect} \nencountered parsing value: \n\t#{val.inspect} \nreturned from expression: \n\t#{js.inspect}"
|
443
|
-
end
|
444
|
-
errord=errord_and_val['errored']
|
445
|
-
val= errord_and_val['value']
|
446
|
-
if errord
|
447
|
-
case val
|
448
|
-
when Hash
|
449
|
-
js_error(val['name'],val['message'],js,val)
|
450
|
-
when String
|
451
|
-
js_error(nil, val, js)
|
452
|
-
else
|
453
|
-
js_error(nil, val.inspect, js)
|
454
|
-
end
|
455
|
-
else
|
456
|
-
val
|
457
|
-
end
|
458
|
-
end
|
459
|
-
public
|
460
|
-
|
461
|
-
# assigns to the javascript reference on the left the object on the right.
|
462
|
-
# Assuming the right object can be converted to JSON, the javascript value will
|
463
|
-
# be the equivalent javascript data type to the ruby object. Will return
|
464
|
-
# the assigned value, converted from its javascript value back to ruby. So, the return
|
465
|
-
# value won't be exactly equivalent if you use symbols for example.
|
466
|
-
#
|
467
|
-
# >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
|
468
|
-
# => {"foo"=>["baz", "qux"]}
|
469
|
-
#
|
470
|
-
# Uses #value_json; see its documentation.
|
471
|
-
def assign_json(js_left, rb_right)
|
472
|
-
ensure_prototype
|
473
|
-
js_right=JsshSocket.to_javascript(rb_right)
|
474
|
-
value_json("#{js_left}=#{js_right}")
|
475
|
-
end
|
476
|
-
|
477
|
-
# calls to the given function (javascript reference to a function) passing it the
|
478
|
-
# given arguments, each argument being converted from a ruby object to a javascript object
|
479
|
-
# via JSON. returns the return value of the function, of equivalent type to the javascript
|
480
|
-
# return value, converted from javascript to ruby via JSON.
|
481
|
-
# Uses #value_json; see its documentation.
|
482
|
-
def call_json(js_function, *rb_args)
|
483
|
-
ensure_prototype
|
484
|
-
js_args=rb_args.map{|arg| JsshSocket.to_javascript(arg) }
|
485
|
-
value_json("#{js_function}(#{js_args.join(', ')})")
|
486
|
-
end
|
487
|
-
|
488
|
-
# does the same thing as #handle, but with json, calling #assign_json, #value_json,
|
489
|
-
# or #call_json.
|
490
|
-
#
|
491
|
-
# if the given javascript expression ends with an = symbol, #handle_json calls to
|
492
|
-
# #assign_json assuming it is given one argument; if the expression refers to a function,
|
493
|
-
# calls that function with the given arguments using #call_json; if the expression is
|
494
|
-
# some other value, returns that value, converted to ruby via JSON, assuming given no
|
495
|
-
# arguments. Uses #value_json; see its documentation.
|
496
|
-
def handle_json(js_expr, *args)
|
497
|
-
ensure_prototype
|
498
|
-
if js_expr=~/=\z/ # doing assignment
|
499
|
-
js_left=$`
|
500
|
-
if args.size != 1
|
501
|
-
raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
|
502
|
-
end
|
503
|
-
assign_json(js_left, *args)
|
504
|
-
else
|
505
|
-
type=typeof(js_expr)
|
506
|
-
case type
|
507
|
-
when "function"
|
508
|
-
call_json(js_expr, *args)
|
509
|
-
when "undefined"
|
510
|
-
raise JsshUndefinedValueError, "undefined expression #{js_expr}"
|
511
|
-
else
|
512
|
-
if !args.empty?
|
513
|
-
raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
|
514
|
-
end
|
515
|
-
value_json(js_expr)
|
516
|
-
end
|
517
|
-
end
|
518
|
-
end
|
519
|
-
|
520
|
-
# raises error if the prototype library (needed for JSON stuff in javascript) has not been loaded
|
521
|
-
def ensure_prototype
|
522
|
-
unless prototype
|
523
|
-
raise JsshError, "This functionality requires the prototype library; cannot be called on a Jssh session that has not loaded the Prototype library"
|
524
|
-
end
|
525
|
-
end
|
526
|
-
|
527
|
-
# returns the type of the given expression using javascript typeof operator, with the exception that
|
528
|
-
# if the expression is null, returns 'null' - whereas typeof(null) in javascript returns 'object'
|
529
|
-
def typeof(expression)
|
530
|
-
ensure_prototype
|
531
|
-
js="try
|
532
|
-
{ nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
|
533
|
-
} catch(e)
|
534
|
-
{ if(e.name=='ReferenceError')
|
535
|
-
{ nativeJSON_encode_length({errored: false, value: 'undefined'});
|
536
|
-
}
|
537
|
-
else
|
538
|
-
{ nativeJSON_encode_length({errored: true, value: Object.extend({}, e)});
|
539
|
-
}
|
540
|
-
}"
|
541
|
-
error_or_val_json(send_and_read(js, :length_before_value => true),js)
|
542
|
-
end
|
543
|
-
|
544
|
-
# uses the javascript 'instanceof' operator, passing it the given
|
545
|
-
# expression and interface. this should return true or false.
|
546
|
-
def instanceof(js_expression, js_interface)
|
547
|
-
value_json "(#{js_expression}) instanceof (#{js_interface})"
|
548
|
-
end
|
549
|
-
|
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
|
552
|
-
# a string that contains invalid JSON
|
553
|
-
def parse_json(json)
|
554
|
-
err_class=JSON::ParserError
|
555
|
-
decoder=JSON.method(:parse)
|
556
|
-
# err_class=ActiveSupport::JSON::ParseError
|
557
|
-
# decoder=ActiveSupport::JSON.method(:decode)
|
558
|
-
raise err_class, "Not a string! got: #{json.inspect}" unless json.is_a?(String)
|
559
|
-
raise err_class, "Blank string!" if json==''
|
560
|
-
begin
|
561
|
-
return decoder.call(json)
|
562
|
-
rescue err_class
|
563
|
-
err=$!.class.new($!.message+"\nParsing: #{json.inspect}")
|
564
|
-
err.set_backtrace($!.backtrace)
|
565
|
-
raise err
|
566
|
-
end
|
567
|
-
end
|
568
|
-
|
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
|
578
|
-
end
|
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
|
639
|
-
end
|
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.
|
709
|
-
def temp_object
|
710
|
-
@temp_object ||= root.JsshTemp
|
711
|
-
end
|
712
|
-
# returns a JsshObject representing the Components top-level javascript object.
|
713
|
-
#
|
714
|
-
# https://developer.mozilla.org/en/Components_object
|
715
|
-
def Components
|
716
|
-
@components ||= root.Components
|
717
|
-
end
|
718
|
-
# returns a JsshObject representing the return value of JSSH's builtin getWindows() function.
|
719
|
-
def getWindows
|
720
|
-
root.getWindows
|
721
|
-
end
|
722
|
-
# raises an informative error if the socket is down for some reason
|
723
|
-
def assert_socket
|
724
|
-
begin
|
725
|
-
actual, expected=if prototype
|
726
|
-
[value_json('["foo"]'), ["foo"]]
|
727
|
-
else
|
728
|
-
[value('"foo"'), "foo"]
|
729
|
-
end
|
730
|
-
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
|
731
|
-
raise(JsshConnectionError, "Encountered a socket error while checking the socket.\n#{$!.class}\n#{$!.message}", $!.backtrace)
|
732
|
-
end
|
733
|
-
unless expected==actual
|
734
|
-
raise JsshError, "The socket seems to have a problem: sent #{expected.inspect} but got back #{actual.inspect}"
|
735
|
-
end
|
736
|
-
end
|
737
|
-
|
738
|
-
# returns a string of basic information about this socket.
|
739
|
-
def inspect
|
740
|
-
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:ip, :port, :prototype].map{|attr| aa="@#{attr}";aa+'='+instance_variable_get(aa).inspect}.join(', ')}>"
|
741
|
-
end
|
742
|
-
end
|
743
|
-
|
744
|
-
# represents a javascript object in ruby.
|
745
|
-
class JsshObject
|
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:
|
755
|
-
# def logger
|
756
|
-
# jssh_socket.logger
|
757
|
-
# end
|
758
|
-
|
759
|
-
# :startdoc:
|
760
|
-
|
761
|
-
public
|
762
|
-
# initializes a JsshObject with a string of javascript containing a reference to
|
763
|
-
# the object, and a JsshSocket that the object is defined on.
|
764
|
-
def initialize(ref, jssh_socket, other={})
|
765
|
-
other={:debug_name => ref, :function_result => false}.merge(other)
|
766
|
-
raise ArgumentError, "Empty object reference!" if !ref || ref==''
|
767
|
-
raise ArgumentError, "Reference must be a string - got #{ref.inspect} (#{ref.class.name})" unless ref.is_a?(String)
|
768
|
-
raise ArgumentError, "Not given a JsshSocket, instead given #{jssh_socket.inspect} (#{jssh_socket.class.name})" unless jssh_socket.is_a?(JsshSocket)
|
769
|
-
@ref=ref
|
770
|
-
@jssh_socket=jssh_socket
|
771
|
-
@debug_name=other[:debug_name]
|
772
|
-
@function_result=other[:function_result]
|
773
|
-
# logger.info { "#{self.class} initialized: #{debug_name} (type #{type})" }
|
774
|
-
end
|
775
|
-
|
776
|
-
# returns the value, via JsshSocket#value_json
|
777
|
-
def val
|
778
|
-
jssh_socket.value_json(ref, :error_on_undefined => !function_result)
|
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
|
806
|
-
|
807
|
-
# returns the value just as a string with no attempt to deal with type using json. via JsshSocket#value
|
808
|
-
#
|
809
|
-
# note that this can be slow if it evaluates to a blank string. for example, if ref is just ""
|
810
|
-
# then JsshSocket#value will wait DEFAULT_SOCKET_TIMEOUT seconds for data that is not to come.
|
811
|
-
# this also happens with functions that return undefined. if ref="function(){do_some_stuff;}"
|
812
|
-
# (with no return), it will also wait DEFAULT_SOCKET_TIMEOUT.
|
813
|
-
def val_str
|
814
|
-
jssh_socket.value(ref)
|
815
|
-
end
|
816
|
-
|
817
|
-
# returns javascript typeof this object
|
818
|
-
def type
|
819
|
-
if function_result # don't get type for function results, causes function evaluations when you probably didn't want that.
|
820
|
-
nil
|
821
|
-
else
|
822
|
-
# logger.add(-1) { "retrieving type for #{debug_name}" }
|
823
|
-
@type||= jssh_socket.typeof(ref)
|
824
|
-
end
|
825
|
-
end
|
826
|
-
|
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.
|
831
|
-
#
|
832
|
-
# example:
|
833
|
-
# window.instanceof(window.jssh_socket.Components.interfaces.nsIDOMChromeWindow)
|
834
|
-
# => true
|
835
|
-
def instanceof(interface)
|
836
|
-
jssh_socket.instanceof(self.ref, interface.ref)
|
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.
|
843
|
-
def implemented_interfaces
|
844
|
-
jssh_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
|
845
|
-
list << interface if instanceof(interface)
|
846
|
-
list
|
847
|
-
end
|
848
|
-
end
|
849
|
-
|
850
|
-
# returns the type of object that is reported by the javascript toString() method, which
|
851
|
-
# returns such as "[object Object]" or "[object XPCNativeWrapper [object HTMLDocument]]"
|
852
|
-
# This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
|
853
|
-
# Raises an error if this JsshObject points to something other than a javascript 'object'
|
854
|
-
# type ('function' or 'number' or whatever)
|
855
|
-
#
|
856
|
-
# this isn't used, doesn't seem useful, and may go away in the future.
|
857
|
-
def object_type
|
858
|
-
@object_type ||= begin
|
859
|
-
case type
|
860
|
-
when 'object'
|
861
|
-
self.toString! =~ /\A\[object\s+(.*)\]\Z/
|
862
|
-
$1
|
863
|
-
else
|
864
|
-
raise JsshError, "Type is #{type}, not object"
|
865
|
-
end
|
866
|
-
end
|
867
|
-
end
|
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).
|
886
|
-
def val_or_object(options={})
|
887
|
-
options={:error_on_undefined=>true, :define_methods => self.class.always_define_methods}.merge(options)
|
888
|
-
if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
|
889
|
-
store_rand_object_key(jssh_socket.temp_object).val_or_object(options.merge(:error_on_undefined => false))
|
890
|
-
else
|
891
|
-
case self.type
|
892
|
-
when 'undefined'
|
893
|
-
if !options[:error_on_undefined]
|
894
|
-
nil
|
895
|
-
else
|
896
|
-
raise JsshUndefinedValueError, "undefined expression represented by #{self.inspect} (javascript reference is #{@ref})"
|
897
|
-
end
|
898
|
-
when 'boolean','number','string','null'
|
899
|
-
val
|
900
|
-
else # 'function','object', or anything else
|
901
|
-
if options[:define_methods] && type=='object'
|
902
|
-
define_methods!
|
903
|
-
end
|
904
|
-
self
|
905
|
-
end
|
906
|
-
end
|
907
|
-
end
|
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)
|
913
|
-
else
|
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
|
928
|
-
else
|
929
|
-
raise ArgumentError, "suffix should be one of: nil, '?', '!', '='; got: #{suffix.inspect}"
|
930
|
-
end
|
931
|
-
end
|
932
|
-
end
|
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
|
-
|
949
|
-
# returns a JsshObject referencing the given attribute of this object
|
950
|
-
def attr(attribute, options={})
|
951
|
-
unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
|
952
|
-
raise JsshSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
|
953
|
-
end
|
954
|
-
JsshObject.new("#{ref}.#{attribute}", jssh_socket, :debug_name => "#{debug_name}.#{attribute}")
|
955
|
-
end
|
956
|
-
|
957
|
-
# assigns the given ruby value (converted to javascript) to the reference
|
958
|
-
# for this object. returns self.
|
959
|
-
def assign(val)
|
960
|
-
@debug_name="(#{debug_name}=#{val.is_a?(JsshObject) ? val.debug_name : JsshSocket.to_javascript(val)})"
|
961
|
-
result=assign_expr(JsshSocket.to_javascript(val))
|
962
|
-
# logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
|
963
|
-
result
|
964
|
-
end
|
965
|
-
# assigns the given javascript expression (string) to the reference for this object
|
966
|
-
def assign_expr(val)
|
967
|
-
jssh_socket.value_json("(function(val){#{ref}=val; return null;}(#{val}))")
|
968
|
-
@type=nil # uncache this
|
969
|
-
# don't want to use JsshSocket#assign_json because converting the result of the assignment (that is, the expression assigned) to json is error-prone and we don't really care about the result.
|
970
|
-
# don't want to use JsshSocket#assign because the result can be blank and cause send_and_read to wait for data that's not coming - also
|
971
|
-
# using a json function is better because it catches errors much more elegantly.
|
972
|
-
# so, wrap it in a function that returns nil.
|
973
|
-
self
|
974
|
-
end
|
975
|
-
|
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.
|
978
|
-
def pass(*args)
|
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(', ')})")
|
980
|
-
end
|
981
|
-
|
982
|
-
# returns the value (via JsshSocket#value_json) or a JsshObject (see #val_or_object) of the return
|
983
|
-
# value of this function (assumes this object is a function) passing it the given arguments (which
|
984
|
-
# are converted to javascript).
|
985
|
-
#
|
986
|
-
# simply, it just calls self.pass(*args).val_or_object
|
987
|
-
def call(*args)
|
988
|
-
pass(*args).val_or_object
|
989
|
-
end
|
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
|
-
|
1006
|
-
# sets the given javascript variable to this object, and returns a JsshObject referring
|
1007
|
-
# to the variable.
|
1008
|
-
#
|
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.
|
1015
|
-
def store(js_variable, somewhere_meaningful=true)
|
1016
|
-
stored=JsshObject.new(js_variable, jssh_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
|
1017
|
-
stored.assign_expr(self.ref)
|
1018
|
-
stored
|
1019
|
-
end
|
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.
|
1025
|
-
def store_rand_named(&name_proc)
|
1026
|
-
base=36
|
1027
|
-
length=32
|
1028
|
-
begin
|
1029
|
-
name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
|
1030
|
-
end #while JsshObject.new(name,jssh_socket).type!='undefined'
|
1031
|
-
# okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
|
1032
|
-
store(name, false)
|
1033
|
-
end
|
1034
|
-
public
|
1035
|
-
|
1036
|
-
# stores this object in a random key of the given object and returns the stored object.
|
1037
|
-
def store_rand_object_key(object)
|
1038
|
-
raise ArgumentError("Object is not a JsshObject: got #{object.inspect}") unless object.is_a?(JsshObject)
|
1039
|
-
store_rand_named do |r|
|
1040
|
-
object.sub(r).ref
|
1041
|
-
end
|
1042
|
-
end
|
1043
|
-
|
1044
|
-
# stores this object in a random key of the designated temporary object for this socket and returns the stored object.
|
1045
|
-
def store_rand_temp
|
1046
|
-
store_rand_object_key(jssh_socket.temp_object)
|
1047
|
-
end
|
1048
|
-
|
1049
|
-
# returns a JsshObject referring to a subscript of this object, specified as a ruby object converted to
|
1050
|
-
# javascript.
|
1051
|
-
#
|
1052
|
-
# similar to [], but [] calls #val_or_object; this always returns a JsshObject.
|
1053
|
-
def sub(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)}]")
|
1055
|
-
end
|
1056
|
-
|
1057
|
-
# returns a JsshObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
|
1058
|
-
#
|
1059
|
-
# subscript is specified as ruby (converted to javascript).
|
1060
|
-
def [](key)
|
1061
|
-
sub(key).val_or_object(:error_on_undefined => false)
|
1062
|
-
end
|
1063
|
-
|
1064
|
-
# assigns the given ruby value (which is converted to javascript) to the given subscript
|
1065
|
-
# (the key is also converted to javascript).
|
1066
|
-
def []=(key, value)
|
1067
|
-
self.sub(key).assign(value)
|
1068
|
-
end
|
1069
|
-
|
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.
|
1073
|
-
def binary_operator(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
|
1075
|
-
end
|
1076
|
-
# addition, using the + operator in javascript
|
1077
|
-
def +(operand)
|
1078
|
-
binary_operator('+', operand)
|
1079
|
-
end
|
1080
|
-
# subtraction, using the - operator in javascript
|
1081
|
-
def -(operand)
|
1082
|
-
binary_operator('-', operand)
|
1083
|
-
end
|
1084
|
-
# division, using the / operator in javascript
|
1085
|
-
def /(operand)
|
1086
|
-
binary_operator('/', operand)
|
1087
|
-
end
|
1088
|
-
# multiplication, using the * operator in javascript
|
1089
|
-
def *(operand)
|
1090
|
-
binary_operator('*', operand)
|
1091
|
-
end
|
1092
|
-
# modulus, using the % operator in javascript
|
1093
|
-
def %(operand)
|
1094
|
-
binary_operator('%', operand)
|
1095
|
-
end
|
1096
|
-
# returns true if the javascript object represented by this is equal to the given operand.
|
1097
|
-
def ==(operand)
|
1098
|
-
operand.is_a?(JsshObject) && binary_operator('==', operand)
|
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
|
1106
|
-
def >(operand)
|
1107
|
-
binary_operator('>', operand)
|
1108
|
-
end
|
1109
|
-
# inequality, using the < operator in javascript
|
1110
|
-
def <(operand)
|
1111
|
-
binary_operator('<', operand)
|
1112
|
-
end
|
1113
|
-
# inequality, using the >= operator in javascript
|
1114
|
-
def >=(operand)
|
1115
|
-
binary_operator('>=', operand)
|
1116
|
-
end
|
1117
|
-
# inequality, using the <= operator in javascript
|
1118
|
-
def <=(operand)
|
1119
|
-
binary_operator('<=', operand)
|
1120
|
-
end
|
1121
|
-
|
1122
|
-
# method_missing handles unknown method calls in a way that makes it possible to write
|
1123
|
-
# javascript-like syntax in ruby, to some extent.
|
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
|
-
#
|
1130
|
-
# method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
|
1131
|
-
# special characters, only alphanumeric/underscores, starting with alpha or underscore - with
|
1132
|
-
# the exception of three special behaviors:
|
1133
|
-
#
|
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.
|
1137
|
-
#
|
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.
|
1144
|
-
#
|
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.
|
1147
|
-
#
|
1148
|
-
# otherwise, method_missing behaves like #invoke, and returns a JsshObject, a string, a boolean,
|
1149
|
-
# a number, or null.
|
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
|
-
#--
|
1154
|
-
# $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
|
1155
|
-
# methods to arrays and hashes - see http://www.prototypejs.org/
|
1156
|
-
# You can use these methods with method_missing just like any other:
|
1157
|
-
#
|
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", ...]
|
1169
|
-
def method_missing(method, *args)
|
1170
|
-
method=method.to_s
|
1171
|
-
if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
|
1172
|
-
method = $1
|
1173
|
-
suffix = $2
|
1174
|
-
attr(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
1175
|
-
else
|
1176
|
-
# don't deal with any special character crap
|
1177
|
-
super
|
1178
|
-
end
|
1179
|
-
end
|
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:
|
1183
|
-
metaclass=(class << self; self; end)
|
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|
|
1187
|
-
metaclass.send(:define_method, key) do |*args|
|
1188
|
-
invoke(key, *args)
|
1189
|
-
end
|
1190
|
-
end
|
1191
|
-
end
|
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)
|
1195
|
-
super || object_respond_to?(method)
|
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.
|
1199
|
-
def object_respond_to?(method)
|
1200
|
-
method=method.to_s
|
1201
|
-
if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
|
1202
|
-
method = $1
|
1203
|
-
suffix = $2
|
1204
|
-
else # don't deal with any special character crap
|
1205
|
-
return false
|
1206
|
-
end
|
1207
|
-
|
1208
|
-
if self.type=='undefined'
|
1209
|
-
return false
|
1210
|
-
elsif suffix=='='
|
1211
|
-
if self.type=='object'
|
1212
|
-
return true # yeah, you can generally assign attributes to objects
|
1213
|
-
else
|
1214
|
-
return false # no, you can't generally assign attributes to (boolean, number, string, null)
|
1215
|
-
end
|
1216
|
-
else
|
1217
|
-
attr=attr(method)
|
1218
|
-
return attr.type!='undefined'
|
1219
|
-
end
|
1220
|
-
end
|
1221
|
-
|
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
|
1227
|
-
end
|
1228
|
-
|
1229
|
-
# returns this object passed through the $A function of the prototype javascript library.
|
1230
|
-
def to_js_array
|
1231
|
-
jssh_socket.object('$A').call(self)
|
1232
|
-
end
|
1233
|
-
# returns this object passed through the $H function of the prototype javascript library.
|
1234
|
-
def to_js_hash
|
1235
|
-
jssh_socket.object('$H').call(self)
|
1236
|
-
end
|
1237
|
-
# returns this object passed through a javascript function which copies each key onto a blank object and rescues any errors.
|
1238
|
-
def to_js_hash_safe
|
1239
|
-
jssh_socket.object('$_H').call(self)
|
1240
|
-
end
|
1241
|
-
# returns a JsshArray representing this object
|
1242
|
-
def to_array
|
1243
|
-
JsshArray.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
1244
|
-
end
|
1245
|
-
# returns a JsshHash representing this object
|
1246
|
-
def to_hash
|
1247
|
-
JsshHash.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
1248
|
-
end
|
1249
|
-
# returns a JsshDOMNode representing this object
|
1250
|
-
def to_dom
|
1251
|
-
JsshDOMNode.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
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.
|
1270
|
-
def to_ruby_hash(options={})
|
1271
|
-
options={:recurse => 1}.merge(options)
|
1272
|
-
return self if !options[:recurse] || options[:recurse]==0
|
1273
|
-
return self if self.type!='object'
|
1274
|
-
next_options=options.merge(:recurse => options[:recurse]-1)
|
1275
|
-
begin
|
1276
|
-
keys=self.to_hash.keys
|
1277
|
-
rescue JsshError
|
1278
|
-
return self
|
1279
|
-
end
|
1280
|
-
keys.inject({}) do |hash, key|
|
1281
|
-
val=begin
|
1282
|
-
self[key]
|
1283
|
-
rescue JsshError
|
1284
|
-
$!
|
1285
|
-
end
|
1286
|
-
hash[key]=if val.is_a?(JsshObject)
|
1287
|
-
val.to_ruby_hash(next_options)
|
1288
|
-
else
|
1289
|
-
val
|
1290
|
-
end
|
1291
|
-
hash
|
1292
|
-
end
|
1293
|
-
end
|
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.
|
1301
|
-
def inspect
|
1302
|
-
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
|
1303
|
-
end
|
1304
|
-
def pretty_print(pp) # :nodoc:
|
1305
|
-
pp.object_address_group(self) do
|
1306
|
-
pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
|
1307
|
-
pp.breakable ' '
|
1308
|
-
pp.group(0) do
|
1309
|
-
pp.text attr.to_s
|
1310
|
-
pp.text ': '
|
1311
|
-
#pp.breakable
|
1312
|
-
pp.text send(attr)
|
1313
|
-
end
|
1314
|
-
end
|
1315
|
-
end
|
1316
|
-
end
|
1317
|
-
end
|
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.
|
1323
|
-
class JsshDOMNode < JsshObject
|
1324
|
-
def inspect_stuff # :nodoc:
|
1325
|
-
[:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map do |attrn|
|
1326
|
-
attr=attr(attrn)
|
1327
|
-
if ['undefined','null'].include?(attr.type)
|
1328
|
-
nil
|
1329
|
-
else
|
1330
|
-
[attrn, attr.val_or_object(:error_on_undefined => false)]
|
1331
|
-
end
|
1332
|
-
end.compact
|
1333
|
-
end
|
1334
|
-
# returns a string with a bunch of information about this dom node
|
1335
|
-
def inspect
|
1336
|
-
"\#<#{self.class.name} #{inspect_stuff.map{|(k,v)| "#{k}=#{v.inspect}"}.join(', ')}>"
|
1337
|
-
end
|
1338
|
-
def pretty_print(pp) # :nodoc:
|
1339
|
-
pp.object_address_group(self) do
|
1340
|
-
pp.seplist(inspect_stuff, lambda { pp.text ',' }) do |attr_val|
|
1341
|
-
pp.breakable ' '
|
1342
|
-
pp.group(0) do
|
1343
|
-
pp.text attr_val.first.to_s
|
1344
|
-
pp.text ': '
|
1345
|
-
#pp.breakable
|
1346
|
-
pp.text attr_val.last.inspect
|
1347
|
-
end
|
1348
|
-
end
|
1349
|
-
end
|
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.
|
1355
|
-
def dump(options={})
|
1356
|
-
options={:recurse => nil, :level => 0}.merge(options)
|
1357
|
-
next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
|
1358
|
-
result=(" "*options[:level]*2)+self.inspect+"\n"
|
1359
|
-
if options[:recurse]==0
|
1360
|
-
result+=(" "*next_options[:level]*2)+"...\n"
|
1361
|
-
else
|
1362
|
-
self.childNodes.to_array.each do |child|
|
1363
|
-
result+=child.to_dom.dump(next_options)
|
1364
|
-
end
|
1365
|
-
end
|
1366
|
-
result
|
1367
|
-
end
|
1368
|
-
end
|
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.
|
1373
|
-
class JsshArray < JsshObject
|
1374
|
-
# yields the element at each subscript of this javascript array, from 0 to self.length.
|
1375
|
-
def each
|
1376
|
-
length=self.length
|
1377
|
-
raise JsshError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
|
1378
|
-
for i in 0...length
|
1379
|
-
element=self[i]
|
1380
|
-
if element.is_a?(JsshObject)
|
1381
|
-
# yield a more permanent reference than the array subscript
|
1382
|
-
element=element.store_rand_temp
|
1383
|
-
end
|
1384
|
-
yield element
|
1385
|
-
end
|
1386
|
-
end
|
1387
|
-
include Enumerable
|
1388
|
-
end
|
1389
|
-
|
1390
|
-
# this class represents a hash, or 'object' type in javascript.
|
1391
|
-
class JsshHash < JsshObject
|
1392
|
-
# returns an array of keys of this javascript object
|
1393
|
-
def keys
|
1394
|
-
@keys=jssh_socket.call_function(:obj => self){ "var keys=[]; for(var key in obj) { keys.push(key); } return keys;" }.val
|
1395
|
-
end
|
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
|
1402
|
-
keys.each do |key|
|
1403
|
-
if block.arity==1
|
1404
|
-
yield [key, self[key]]
|
1405
|
-
else
|
1406
|
-
yield key, self[key]
|
1407
|
-
end
|
1408
|
-
end
|
1409
|
-
end
|
1410
|
-
# yields each key and value for this object
|
1411
|
-
def each_pair
|
1412
|
-
each do |key,value|
|
1413
|
-
yield key,value
|
1414
|
-
end
|
1415
|
-
end
|
1416
|
-
|
1417
|
-
include Enumerable
|
1418
|
-
end
|