vapir-firefox 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|