vapir-firefox 1.7.0.rc1
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/History.txt +74 -0
- data/LICENSE.txt +42 -0
- data/README.txt +0 -0
- data/lib/vapir/ff.rb +1 -0
- data/lib/vapir/firefox.rb +1 -0
- data/lib/vapir-firefox/container.rb +462 -0
- data/lib/vapir-firefox/element.rb +277 -0
- data/lib/vapir-firefox/elements/button.rb +21 -0
- data/lib/vapir-firefox/elements/file_field.rb +27 -0
- data/lib/vapir-firefox/elements/form.rb +8 -0
- data/lib/vapir-firefox/elements/frame.rb +23 -0
- data/lib/vapir-firefox/elements/hidden.rb +12 -0
- data/lib/vapir-firefox/elements/image.rb +38 -0
- data/lib/vapir-firefox/elements/input_element.rb +12 -0
- data/lib/vapir-firefox/elements/link.rb +30 -0
- data/lib/vapir-firefox/elements/non_control_elements.rb +89 -0
- data/lib/vapir-firefox/elements/option.rb +12 -0
- data/lib/vapir-firefox/elements/radio_check_common.rb +36 -0
- data/lib/vapir-firefox/elements/select_list.rb +12 -0
- data/lib/vapir-firefox/elements/table.rb +16 -0
- data/lib/vapir-firefox/elements/table_cell.rb +12 -0
- data/lib/vapir-firefox/elements/table_row.rb +12 -0
- data/lib/vapir-firefox/elements/text_field.rb +64 -0
- data/lib/vapir-firefox/elements.rb +17 -0
- data/lib/vapir-firefox/firefox.rb +687 -0
- data/lib/vapir-firefox/jssh_socket.rb +1066 -0
- data/lib/vapir-firefox/modal_dialog.rb +144 -0
- data/lib/vapir-firefox/page_container.rb +91 -0
- data/lib/vapir-firefox/prototype.functional.js +1219 -0
- data/lib/vapir-firefox/version.rb +5 -0
- data/lib/vapir-firefox/window.rb +38 -0
- data/lib/vapir-firefox.rb +15 -0
- metadata +120 -0
@@ -0,0 +1,1066 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'active_support'
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
#require 'logger'
|
6
|
+
|
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
|
+
class Object
|
40
|
+
# this is like #to_json, but without the conflicting names between ActiveSupport and JSON gem,
|
41
|
+
# and also for JsshObject (which is a reference; not real json; see the overload in that class)
|
42
|
+
def to_jssh
|
43
|
+
ActiveSupport::JSON.encode(self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class JsshError < StandardError
|
49
|
+
attr_accessor :source, :lineNumber, :stack, :fileName
|
50
|
+
end
|
51
|
+
# this exception covers all connection errors either on startup or during usage
|
52
|
+
class JsshConnectionError < JsshError;end
|
53
|
+
# This exception is thrown if we are unable to connect to JSSh.
|
54
|
+
class JsshUnableToStart < JsshConnectionError;end
|
55
|
+
class JsshJavascriptError < JsshError;end
|
56
|
+
class JsshSyntaxError < JsshJavascriptError;end
|
57
|
+
class JsshUndefinedValueError < JsshJavascriptError;end
|
58
|
+
|
59
|
+
class JsshSocket
|
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
|
+
# IP Address of the machine where the script is to be executed. Default to localhost.
|
77
|
+
@@default_jssh_ip = "127.0.0.1"
|
78
|
+
@@default_jssh_port = 9997
|
79
|
+
PrototypeFile=File.join(File.dirname(__FILE__), "prototype.functional.js")
|
80
|
+
|
81
|
+
DEFAULT_SOCKET_TIMEOUT=64
|
82
|
+
SHORT_SOCKET_TIMEOUT=(2**-2).to_f
|
83
|
+
|
84
|
+
attr_reader :ip, :port, :prototype
|
85
|
+
|
86
|
+
# Connects a new socket to jssh
|
87
|
+
# Takes options:
|
88
|
+
# * :jssh_ip => the ip to connect to, default 127.0.0.1
|
89
|
+
# * :jssh_port => the port to connect to, default 9997
|
90
|
+
# * :send_prototype => true|false, whether to load and send the Prototype library (the functional programming part of it anyway, and JSON bits)
|
91
|
+
def initialize(options={})
|
92
|
+
@ip=options[:jssh_ip] || @@default_jssh_ip
|
93
|
+
@port=options[:jssh_port] || @@default_jssh_port
|
94
|
+
@prototype=options.key?(:send_prototype) ? options[:send_prototype] : true
|
95
|
+
begin
|
96
|
+
@socket = TCPSocket::new(@ip, @port)
|
97
|
+
@socket.sync = true
|
98
|
+
@expecting_prompt=false # initially, the welcome message comes before the prompt, so this so this is false to start with
|
99
|
+
@expecting_extra_maybe=false
|
100
|
+
welcome="Welcome to the Mozilla JavaScript Shell!\n"
|
101
|
+
read=read_value
|
102
|
+
if !read
|
103
|
+
@expecting_extra_maybe=true
|
104
|
+
raise JsshUnableToStart, "Something went wrong initializing - no response"
|
105
|
+
elsif read != welcome
|
106
|
+
@expecting_extra_maybe=true
|
107
|
+
raise JsshUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}"
|
108
|
+
end
|
109
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
|
110
|
+
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}")
|
111
|
+
err.set_backtrace($!.backtrace)
|
112
|
+
raise err
|
113
|
+
end
|
114
|
+
if @prototype
|
115
|
+
ret=send_and_read(File.read(PrototypeFile))
|
116
|
+
if ret != "done!"
|
117
|
+
@expecting_extra_maybe=true
|
118
|
+
raise JsshError, "Something went wrong loading Prototype - message #{ret.inspect}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
ret=send_and_read("(function()
|
122
|
+
{ nativeJSON=Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON);
|
123
|
+
nativeJSON_encode_length=function(object)
|
124
|
+
{ var encoded=nativeJSON.encode(object);
|
125
|
+
return encoded.length.toString()+\"\\n\"+encoded;
|
126
|
+
}
|
127
|
+
return 'done!';
|
128
|
+
})()")
|
129
|
+
if ret != "done!"
|
130
|
+
@expecting_extra_maybe=true
|
131
|
+
raise JsshError, "Something went wrong initializing native JSON - message #{ret.inspect}"
|
132
|
+
end
|
133
|
+
temp_object.assign({})
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
# reads from the socket and returns what seems to be the value that should be returned, by stripping prompts
|
138
|
+
# from the beginning and end where appropriate.
|
139
|
+
#
|
140
|
+
# does not deal with prompts in between values, because attempting to parse those out is impossible, it being
|
141
|
+
# perfectly possible that a string the same as the prompt is part of actual data. (even stripping it from the
|
142
|
+
# ends of the string is not entirely certain; data could have it at the ends too, but that's the best that can
|
143
|
+
# be done.) so, read_value should be called after every line, or you can end up with stuff like:
|
144
|
+
# >> @socket.send "3\n4\n5\n", 0
|
145
|
+
# => 6
|
146
|
+
# >> read_value
|
147
|
+
# => "3\n> 4\n> 5"
|
148
|
+
#
|
149
|
+
# by default, read_value reads until the socket is done being ready. "done being ready" is defined as Kernel.select
|
150
|
+
# saying that the socket isn't ready after waiting for SHORT_SOCKET_TIMEOUT. usually this will be true after a
|
151
|
+
# single read, as most things only take one #recv call to get the whole value. this waiting for SHORT_SOCKET_TIMEOUT
|
152
|
+
# can add up to being slow if you're doing a lot of socket activity.
|
153
|
+
# to solve this, performance can be improved significantly using the :length_before_value option. with this, you have
|
154
|
+
# to write your javascript to return the length of the value to be sent, followed by a newline, followed by the actual
|
155
|
+
# value (which must be of the length it says it is, or this method will error).
|
156
|
+
# if this option is set, this doesn't do any SHORT_SOCKET_TIMEOUT waiting once it gets the full value, it returns
|
157
|
+
# immediately.
|
158
|
+
def read_value(options={})
|
159
|
+
options={:timeout => DEFAULT_SOCKET_TIMEOUT, :length_before_value => false, :read_size => 65536}.merge(options)
|
160
|
+
received_data = []
|
161
|
+
value_string = ""
|
162
|
+
size_to_read=options[:read_size]
|
163
|
+
timeout=options[:timeout]
|
164
|
+
already_read_length=false
|
165
|
+
expected_size=nil
|
166
|
+
# logger.add(-1) { "RECV_SOCKET is starting. timeout=#{timeout}" }
|
167
|
+
while size_to_read > 0 && Kernel.select([@socket] , nil , nil, timeout)
|
168
|
+
data = @socket.recv(size_to_read)
|
169
|
+
received_data << data
|
170
|
+
value_string << data
|
171
|
+
if @expecting_prompt && utf8_length_safe(value_string) > PROMPT.length
|
172
|
+
if value_string =~ /\A#{Regexp.escape(PROMPT)}/
|
173
|
+
value_string.sub!(/\A#{Regexp.escape(PROMPT)}/, '')
|
174
|
+
@expecting_prompt=false
|
175
|
+
else
|
176
|
+
value_string << clear_error
|
177
|
+
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}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
if !@expecting_prompt
|
181
|
+
if options[:length_before_value] && !already_read_length && value_string.length > 0
|
182
|
+
if value_string =~ /\A(\d+)\n/
|
183
|
+
expected_size=$1.to_i
|
184
|
+
already_read_length=true
|
185
|
+
value_string.sub!(/\A\d+\n/, '')
|
186
|
+
elsif value_string =~ /\A\d+\z/
|
187
|
+
# rather unlikely, but maybe we just received part of the number so far - ignore
|
188
|
+
else
|
189
|
+
@expecting_extra_maybe=true
|
190
|
+
raise JsshError, "Expected length! unexpected data with no preceding length received: #{value_string.inspect}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
if expected_size
|
194
|
+
size_to_read = expected_size - utf8_length_safe(value_string)
|
195
|
+
end
|
196
|
+
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.
|
197
|
+
timeout=SHORT_SOCKET_TIMEOUT
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Kernel.select seems to indicate that a dead socket is ready to read, and returns endless blank strings to recv. rather irritating.
|
202
|
+
if received_data.length >= 3 && received_data[-3..-1].all?{|rd| rd==''}
|
203
|
+
raise JsshConnectionError, "Socket seems to no longer be connected"
|
204
|
+
end
|
205
|
+
# logger.add(-1) { "RECV_SOCKET is continuing. timeout=#{timeout}; data=#{data.inspect}" }
|
206
|
+
end
|
207
|
+
# logger.debug { "RECV_SOCKET is done. received_data=#{received_data.inspect}; value_string=#{value_string.inspect}" }
|
208
|
+
if @expecting_extra_maybe
|
209
|
+
if Kernel.select([@socket] , nil , nil, SHORT_SOCKET_TIMEOUT)
|
210
|
+
cleared_error=clear_error
|
211
|
+
if cleared_error==PROMPT
|
212
|
+
# 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
|
213
|
+
value_string << cleared_error
|
214
|
+
else
|
215
|
+
raise JsshError, "We finished receiving but the socket was still ready to send! extra data received was: #{cleared_error}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
@expecting_extra_maybe=false
|
219
|
+
end
|
220
|
+
|
221
|
+
if expected_size
|
222
|
+
value_string_length=value_string.unpack("U*").length # JSSH returns a utf-8 string, so unpack each character to get the right length
|
223
|
+
|
224
|
+
if value_string_length == expected_size
|
225
|
+
@expecting_prompt=true
|
226
|
+
elsif value_string_length == expected_size + PROMPT.length && value_string =~ /#{Regexp.escape(PROMPT)}\z/
|
227
|
+
value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
|
228
|
+
@expecting_prompt=false
|
229
|
+
else
|
230
|
+
@expecting_extra_maybe=true if value_string_length < expected_size
|
231
|
+
raise JsshError, "Expected a value of size #{expected_size}; received data of size #{value_string_length}: #{value_string.inspect}"
|
232
|
+
end
|
233
|
+
else
|
234
|
+
if value_string =~ /#{Regexp.escape(PROMPT)}\z/ # what if the value happens to end with the same string as the prompt?
|
235
|
+
value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
|
236
|
+
@expecting_prompt=false
|
237
|
+
else
|
238
|
+
@expecting_prompt=true
|
239
|
+
end
|
240
|
+
end
|
241
|
+
return value_string
|
242
|
+
end
|
243
|
+
|
244
|
+
private
|
245
|
+
def utf8_length_safe(string)
|
246
|
+
string=string.dup
|
247
|
+
begin
|
248
|
+
string.unpack("U*").length
|
249
|
+
rescue ArgumentError # this happens when the socket receive gets split across a utf8 character. we drop the incomplete character from the end.
|
250
|
+
if $!.message =~ /malformed UTF-8 character \(expected \d+ bytes, given (\d+) bytes\)/
|
251
|
+
given=$1.to_i
|
252
|
+
string[0...(-given)].unpack("U*").length
|
253
|
+
else # otherwise, this is some other issue we weren't expecting; we will not rescue it.
|
254
|
+
raise
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
# this should be called when an error occurs and we want to clear the socket of any value remaining on it.
|
259
|
+
# this will continue trying for DEFAULT_SOCKET_TIMEOUT until
|
260
|
+
def clear_error
|
261
|
+
data=""
|
262
|
+
while Kernel.select([@socket], nil, nil, SHORT_SOCKET_TIMEOUT)
|
263
|
+
# clear any other crap left on the socket
|
264
|
+
data << @socket.recv(65536)
|
265
|
+
end
|
266
|
+
if data =~ /#{Regexp.escape(PROMPT)}\z/
|
267
|
+
@expecting_prompt=false
|
268
|
+
end
|
269
|
+
data
|
270
|
+
end
|
271
|
+
|
272
|
+
public
|
273
|
+
def send_and_read(js_expr, options={})
|
274
|
+
# logger.add(-1) { "SEND_AND_READ is starting. options=#{options.inspect}" }
|
275
|
+
@last_expression=js_expr
|
276
|
+
js_expr=js_expr+"\n" unless js_expr =~ /\n\z/
|
277
|
+
# logger.debug { "SEND_AND_READ sending #{js_expr.inspect}" }
|
278
|
+
@socket.send(js_expr, 0)
|
279
|
+
return read_value(options)
|
280
|
+
end
|
281
|
+
|
282
|
+
def js_error(errclassname, message, source, stuff={})
|
283
|
+
errclass=if errclassname
|
284
|
+
unless JsshError.const_defined?(errclassname)
|
285
|
+
JsshError.const_set(errclassname, Class.new(JsshJavascriptError))
|
286
|
+
end
|
287
|
+
JsshError.const_get(errclassname)
|
288
|
+
else
|
289
|
+
JsshJavascriptError
|
290
|
+
end
|
291
|
+
err=errclass.new("#{message}\nEvaluating:\n#{source}\n\nOther stuff:\n#{stuff.inspect}")
|
292
|
+
err.source=source
|
293
|
+
["lineNumber", "stack", "fileName"].each do |attr|
|
294
|
+
if stuff.key?(attr)
|
295
|
+
err.send(:"#{attr}=", stuff[attr])
|
296
|
+
end
|
297
|
+
end
|
298
|
+
raise err
|
299
|
+
end
|
300
|
+
private :js_error
|
301
|
+
|
302
|
+
# returns the value of the given javascript expression, as reported by JSSH.
|
303
|
+
# This will be a string, the given expression's toString.
|
304
|
+
def value(js)
|
305
|
+
# this is wrapped in a function so that ...
|
306
|
+
# dang, now I can't remember. I'm sure I had a good reason at the time.
|
307
|
+
send_and_read("(function(){return #{js}})()")
|
308
|
+
end
|
309
|
+
|
310
|
+
# assigns to the javascript reference on the left the javascript expression on the right.
|
311
|
+
# returns the value of the expression as reported by JSSH, which
|
312
|
+
# will be a string, the expression's toString. Uses #value; see its documentation.
|
313
|
+
def assign(js_left, js_right)
|
314
|
+
value("#{js_left}= #{js_right}")
|
315
|
+
end
|
316
|
+
|
317
|
+
# calls to the given function (javascript reference to a function) passing it the
|
318
|
+
# given arguments (javascript expressions). returns the return value of the function,
|
319
|
+
# a string, the toString of the javascript value. Uses #value; see its documentation.
|
320
|
+
def call(js_function, *js_args)
|
321
|
+
value("#{js_function}(#{js_args.join(', ')})")
|
322
|
+
end
|
323
|
+
|
324
|
+
# if the given javascript expression ends with an = symbol, #handle calls to #assign
|
325
|
+
# assuming it is given one argument; if the expression refers to a function, calls
|
326
|
+
# that function with the given arguments using #call; if the expression is some other
|
327
|
+
# value, returns that value (its javascript toString), calling #value, assuming
|
328
|
+
# given no arguments. Uses #value; see its documentation.
|
329
|
+
def handle(js_expr, *args)
|
330
|
+
if js_expr=~/=\z/ # doing assignment
|
331
|
+
js_left=$`
|
332
|
+
if args.size != 1
|
333
|
+
raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
|
334
|
+
end
|
335
|
+
assign(js_left, *args)
|
336
|
+
else
|
337
|
+
type=typeof(js_expr)
|
338
|
+
case type
|
339
|
+
when "function"
|
340
|
+
call(js_expr, *args)
|
341
|
+
when "undefined"
|
342
|
+
raise JsshUndefinedValueError, "undefined expression #{js_expr.inspect}"
|
343
|
+
else
|
344
|
+
if !args.empty?
|
345
|
+
raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
|
346
|
+
end
|
347
|
+
value(js_expr)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# returns the value of the given javascript expression. Assuming that it can
|
353
|
+
# be converted to JSON, will return the equivalent ruby data type to the javascript
|
354
|
+
# value. Will raise an error if the javascript errors.
|
355
|
+
def value_json(js, options={})
|
356
|
+
options={:error_on_undefined => true}.merge(options)
|
357
|
+
raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
|
358
|
+
ensure_prototype
|
359
|
+
ref_error=options[:error_on_undefined] ? "typeof(result)=='undefined' ? {errored: true, value: {'name': 'ReferenceError', 'message': 'undefined expression in: '+result_f.toString()}} : " : ""
|
360
|
+
wrapped_js=
|
361
|
+
"try
|
362
|
+
{ var result_f=(function(){return #{js}});
|
363
|
+
var result=result_f();
|
364
|
+
nativeJSON_encode_length(#{ref_error} {errored: false, value: result});
|
365
|
+
}catch(e)
|
366
|
+
{ nativeJSON_encode_length({errored: true, value: e});
|
367
|
+
}"
|
368
|
+
val=send_and_read(wrapped_js, options.merge(:length_before_value => true))
|
369
|
+
error_or_val_json(val, js)
|
370
|
+
end
|
371
|
+
def error_or_val_json(val, js)
|
372
|
+
if !val || val==''
|
373
|
+
@expecting_extra_maybe=true
|
374
|
+
raise JsshError, "received no value! may have timed out waiting for a value that was not coming."
|
375
|
+
end
|
376
|
+
if val=="SyntaxError: syntax error"
|
377
|
+
raise JsshSyntaxError, val
|
378
|
+
end
|
379
|
+
errord_and_val=parse_json(val)
|
380
|
+
unless errord_and_val.is_a?(Hash) && errord_and_val.keys.sort == ['errored', 'value'].sort
|
381
|
+
raise RuntimeError, "unexpected result: \n\t#{errord_and_val.inspect} \nencountered parsing value: \n\t#{val.inspect} \nreturned from expression: \n\t#{js.inspect}"
|
382
|
+
end
|
383
|
+
errord=errord_and_val['errored']
|
384
|
+
val= errord_and_val['value']
|
385
|
+
if errord
|
386
|
+
case val
|
387
|
+
when Hash
|
388
|
+
js_error(val['name'],val['message'],js,val)
|
389
|
+
when String
|
390
|
+
js_error(nil, val, js)
|
391
|
+
else
|
392
|
+
js_error(nil, val.inspect, js)
|
393
|
+
end
|
394
|
+
else
|
395
|
+
val
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# assigns to the javascript reference on the left the object on the right.
|
400
|
+
# Assuming the right object can be converted to JSON, the javascript value will
|
401
|
+
# be the equivalent javascript data type to the ruby object. Will return
|
402
|
+
# the assigned value, converted from its javascript value back to ruby. So, the return
|
403
|
+
# value won't be exactly equivalent if you use symbols for example.
|
404
|
+
#
|
405
|
+
# >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
|
406
|
+
# => {"foo"=>["baz", "qux"]}
|
407
|
+
#
|
408
|
+
# Uses #value_json; see its documentation.
|
409
|
+
def assign_json(js_left, rb_right)
|
410
|
+
ensure_prototype
|
411
|
+
js_right=rb_right.to_jssh
|
412
|
+
value_json("#{js_left}=#{js_right}")
|
413
|
+
end
|
414
|
+
|
415
|
+
# calls to the given function (javascript reference to a function) passing it the
|
416
|
+
# given arguments, each argument being converted from a ruby object to a javascript object
|
417
|
+
# via JSON. returns the return value of the function, of equivalent type to the javascript
|
418
|
+
# return value, converted from javascript to ruby via JSON.
|
419
|
+
# Uses #value_json; see its documentation.
|
420
|
+
def call_json(js_function, *rb_args)
|
421
|
+
ensure_prototype
|
422
|
+
js_args=rb_args.map{|arg| arg.to_jssh}
|
423
|
+
value_json("#{js_function}(#{js_args.join(', ')})")
|
424
|
+
end
|
425
|
+
|
426
|
+
# does the same thing as #handle, but with json, calling #assign_json, #value_json,
|
427
|
+
# or #call_json.
|
428
|
+
# if the given javascript expression ends with an = symbol, #handle_json calls to
|
429
|
+
# #assign_json assuming it is given one argument; if the expression refers to a function,
|
430
|
+
# calls that function with the given arguments using #call_json; if the expression is
|
431
|
+
# some other value, returns that value, converted to ruby via JSON, assuming given no
|
432
|
+
# arguments. Uses #value_json; see its documentation.
|
433
|
+
def handle_json(js_expr, *args)
|
434
|
+
ensure_prototype
|
435
|
+
if js_expr=~/=\z/ # doing assignment
|
436
|
+
js_left=$`
|
437
|
+
if args.size != 1
|
438
|
+
raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
|
439
|
+
end
|
440
|
+
assign_json(js_left, *args)
|
441
|
+
else
|
442
|
+
type=typeof(js_expr)
|
443
|
+
case type
|
444
|
+
when "function"
|
445
|
+
call_json(js_expr, *args)
|
446
|
+
when "undefined"
|
447
|
+
raise JsshUndefinedValueError, "undefined expression #{js_expr}"
|
448
|
+
else
|
449
|
+
if !args.empty?
|
450
|
+
raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
|
451
|
+
end
|
452
|
+
value_json(js_expr)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# raises error if the prototype library (needed for JSON stuff in javascript) has not been loaded
|
458
|
+
def ensure_prototype
|
459
|
+
unless prototype
|
460
|
+
raise JsshError, "Cannot invoke JSON on a Jssh session that does not have the Prototype library"
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# returns the type of the given expression using javascript typeof operator, with the exception that
|
465
|
+
# if the expression is null, returns 'null' - whereas typeof(null) in javascript returns 'object'
|
466
|
+
def typeof(expression)
|
467
|
+
ensure_prototype
|
468
|
+
js="try
|
469
|
+
{ nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
|
470
|
+
} catch(e)
|
471
|
+
{ if(e.name=='ReferenceError')
|
472
|
+
{ nativeJSON_encode_length({errored: false, value: 'undefined'});
|
473
|
+
}
|
474
|
+
else
|
475
|
+
{ nativeJSON_encode_length({errored: true, value: e});
|
476
|
+
}
|
477
|
+
}"
|
478
|
+
error_or_val_json(send_and_read(js, :length_before_value => true),js)
|
479
|
+
end
|
480
|
+
|
481
|
+
def instanceof(js_expression, js_interface)
|
482
|
+
value_json "(#{js_expression}) instanceof (#{js_interface})"
|
483
|
+
end
|
484
|
+
|
485
|
+
# parses the given JSON string using ActiveSupport::JSON.decode
|
486
|
+
# Raises ActiveSupport::JSON::ParseError if given a blank string, something that is not a string, or
|
487
|
+
# a string that contains invalid JSON
|
488
|
+
def parse_json(json)
|
489
|
+
err_class=JSON::ParserError
|
490
|
+
decoder=JSON.method(:parse)
|
491
|
+
# err_class=ActiveSupport::JSON::ParseError
|
492
|
+
# decoder=ActiveSupport::JSON.method(:decode)
|
493
|
+
raise err_class, "Not a string! got: #{json.inspect}" unless json.is_a?(String)
|
494
|
+
raise err_class, "Blank string!" if json==''
|
495
|
+
begin
|
496
|
+
return decoder.call(json)
|
497
|
+
rescue err_class
|
498
|
+
err=$!.class.new($!.message+"\nParsing: #{json.inspect}")
|
499
|
+
err.set_backtrace($!.backtrace)
|
500
|
+
raise err
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
def object(ref)
|
505
|
+
JsshObject.new(ref, self, :debug_name => ref)
|
506
|
+
end
|
507
|
+
|
508
|
+
def temp_object
|
509
|
+
@temp_object ||= object('JsshTemp')
|
510
|
+
end
|
511
|
+
def Components
|
512
|
+
@components ||= object('Components')
|
513
|
+
end
|
514
|
+
def getWindows
|
515
|
+
@getwindows ||= object('getWindows()')
|
516
|
+
end
|
517
|
+
|
518
|
+
# raises an informative error if the socket is down for some reason
|
519
|
+
def assert_socket
|
520
|
+
begin
|
521
|
+
actual, expected=if prototype
|
522
|
+
[value_json('["foo"]'), ["foo"]]
|
523
|
+
else
|
524
|
+
[value('"foo"'), "foo"]
|
525
|
+
end
|
526
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
|
527
|
+
raise(JsshConnectionError, "Encountered a socket error while checking the socket.\n#{$!.class}\n#{$!.message}", $!.backtrace)
|
528
|
+
end
|
529
|
+
unless expected==actual
|
530
|
+
raise JsshError, "The socket seems to have a problem: sent #{expected.inspect} but got back #{actual.inspect}"
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def inspect
|
535
|
+
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:ip, :port, :prototype].map{|attr| aa="@#{attr}";aa+'='+instance_variable_get(aa).inspect}.join(', ')}>"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
class JsshObject
|
540
|
+
attr_reader :ref, :jssh_socket
|
541
|
+
attr_reader :type, :function_result, :debug_name
|
542
|
+
# def logger
|
543
|
+
# jssh_socket.logger
|
544
|
+
# end
|
545
|
+
|
546
|
+
public
|
547
|
+
# initializes a JsshObject with a string of javascript containing a reference to
|
548
|
+
# the object, and a JsshSocket that the object is defined on.
|
549
|
+
def initialize(ref, jssh_socket, other={})
|
550
|
+
other={:debug_name => ref, :function_result => false}.merge(other)
|
551
|
+
raise ArgumentError, "Empty object reference!" if !ref || ref==''
|
552
|
+
raise ArgumentError, "Reference must be a string - got #{ref.inspect} (#{ref.class.name})" unless ref.is_a?(String)
|
553
|
+
raise ArgumentError, "Not given a JsshSocket, instead given #{jssh_socket.inspect} (#{jssh_socket.class.name})" unless jssh_socket.is_a?(JsshSocket)
|
554
|
+
@ref=ref
|
555
|
+
@jssh_socket=jssh_socket
|
556
|
+
@debug_name=other[:debug_name]
|
557
|
+
@function_result=other[:function_result]
|
558
|
+
# logger.info { "#{self.class} initialized: #{debug_name} (type #{type})" }
|
559
|
+
end
|
560
|
+
|
561
|
+
# returns the value, via JsshSocket#value_json
|
562
|
+
def val
|
563
|
+
jssh_socket.value_json(ref, :error_on_undefined => !function_result)
|
564
|
+
end
|
565
|
+
|
566
|
+
# returns the value just as a string with no attempt to deal with type using json. via JsshSocket#value
|
567
|
+
#
|
568
|
+
# note that this can be slow if it evaluates to a blank string. for example, if ref is just ""
|
569
|
+
# then JsshSocket#value will wait DEFAULT_SOCKET_TIMEOUT seconds for data that is not to come.
|
570
|
+
# this also happens with functions that return undefined. if ref="function(){do_some_stuff;}"
|
571
|
+
# (with no return), it will also wait DEFAULT_SOCKET_TIMEOUT.
|
572
|
+
def val_str
|
573
|
+
jssh_socket.value(ref)
|
574
|
+
end
|
575
|
+
|
576
|
+
# returns javascript typeof this object
|
577
|
+
def type
|
578
|
+
if function_result # don't get type for function results, causes function evaluations when you probably didn't want that.
|
579
|
+
nil
|
580
|
+
else
|
581
|
+
# logger.add(-1) { "retrieving type for #{debug_name}" }
|
582
|
+
@type||= jssh_socket.typeof(ref)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# returns javascript instanceof operator on this and the given interface (expected to be a JsshObject)
|
587
|
+
# note that this is javascript, not to be confused with ruby's #instance_of? method.
|
588
|
+
#
|
589
|
+
# example:
|
590
|
+
# window.instanceof(window.jssh_socket.Components.interfaces.nsIDOMChromeWindow)
|
591
|
+
# => true
|
592
|
+
def instanceof(interface)
|
593
|
+
jssh_socket.instanceof(self.ref, interface.ref)
|
594
|
+
end
|
595
|
+
def implemented_interfaces
|
596
|
+
jssh_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
|
597
|
+
list << interface if instanceof(interface)
|
598
|
+
list
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# returns the type of object that is reported by the javascript toString() method, which
|
603
|
+
# returns such as "[object Object]" or "[object XPCNativeWrapper [object HTMLDocument]]"
|
604
|
+
# This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
|
605
|
+
# Raises an error if this JsshObject points to something other than a javascript 'object'
|
606
|
+
# type ('function' or 'number' or whatever)
|
607
|
+
def object_type
|
608
|
+
@object_type ||= begin
|
609
|
+
case type
|
610
|
+
when 'object'
|
611
|
+
self.toString! =~ /\A\[object\s+(.*)\]\Z/
|
612
|
+
$1
|
613
|
+
else
|
614
|
+
raise JsshError, "Type is #{type}, not object"
|
615
|
+
end
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
def val_or_object(options={})
|
620
|
+
options={:error_on_undefined=>true}.merge(options)
|
621
|
+
if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
|
622
|
+
store_rand_object_key(jssh_socket.temp_object).val_or_object(:error_on_undefined => false)
|
623
|
+
else
|
624
|
+
case self.type
|
625
|
+
when 'undefined'
|
626
|
+
if function_result
|
627
|
+
nil
|
628
|
+
elsif !options[:error_on_undefined]
|
629
|
+
self
|
630
|
+
else
|
631
|
+
raise JsshUndefinedValueError, "undefined expression #{ref}"
|
632
|
+
end
|
633
|
+
when 'boolean','number','string','null'
|
634
|
+
val
|
635
|
+
when 'function','object'
|
636
|
+
self
|
637
|
+
else
|
638
|
+
# here we perhaps could (but won't for now) raise JsshError, "Unknown type: #{type}"
|
639
|
+
self
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
# returns a JsshObject representing the given attribute. Checks the type, and if
|
645
|
+
# it is a function, references the _return value_ of the function (with the given
|
646
|
+
# arguments, if any, which are in ruby, converted to_jssh). If the type of the
|
647
|
+
# expression is undefined, raises an error (if you want an attribute even if it's
|
648
|
+
# undefined, use #attr).
|
649
|
+
def invoke(attribute, *args)
|
650
|
+
attr_obj=attr(attribute)
|
651
|
+
type=attr_obj.type
|
652
|
+
case type
|
653
|
+
when 'function'
|
654
|
+
attr_obj.call(*args)
|
655
|
+
else
|
656
|
+
if args.empty?
|
657
|
+
attr_obj.val_or_object
|
658
|
+
else
|
659
|
+
raise ArgumentError, "Cannot pass arguments to expression #{attr_obj.ref} of type #{type}"
|
660
|
+
end
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
# returns a JsshObject referencing the given attribute of this object
|
665
|
+
def attr(attribute)
|
666
|
+
unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
|
667
|
+
raise JsshSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
|
668
|
+
end
|
669
|
+
JsshObject.new("#{ref}.#{attribute}", jssh_socket, :debug_name => "#{debug_name}.#{attribute}")
|
670
|
+
end
|
671
|
+
|
672
|
+
# assigns (via JsshSocket#assign) the given ruby value (converted to_jssh) to the reference
|
673
|
+
# for this object. returns self.
|
674
|
+
def assign(val)
|
675
|
+
@debug_name="(#{debug_name}=#{val.is_a?(JsshObject) ? val.debug_name : val.to_jssh})"
|
676
|
+
result=assign_expr val.to_jssh
|
677
|
+
# logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
|
678
|
+
result
|
679
|
+
end
|
680
|
+
# assigns the given javascript expression (string) to the reference for this object
|
681
|
+
def assign_expr(val)
|
682
|
+
jssh_socket.value_json("(function(val){#{ref}=val; return null;}(#{val}))")
|
683
|
+
@type=nil # uncache this
|
684
|
+
# 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.
|
685
|
+
# 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
|
686
|
+
# using a json function is better because it catches errors much more elegantly.
|
687
|
+
# so, wrap it in a function that returns nil.
|
688
|
+
self
|
689
|
+
end
|
690
|
+
|
691
|
+
# returns a JsshObject for this object - assumes that this object is a function - passing
|
692
|
+
# this function the specified arguments, which are converted to_jssh
|
693
|
+
def pass(*args)
|
694
|
+
JsshObject.new("#{ref}(#{args.map{|arg| arg.to_jssh}.join(', ')})", jssh_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JsshObject) ? arg.debug_name : arg.to_jssh}.join(', ')})")
|
695
|
+
end
|
696
|
+
|
697
|
+
# returns the value (via JsshSocket#value_json) or a JsshObject (see #val_or_object) of the return
|
698
|
+
# value of this function (assumes this object is a function) passing it the given arguments (which
|
699
|
+
# are converted to_jssh).
|
700
|
+
# simply, it just calls self.pass(*args).val_or_object
|
701
|
+
def call(*args)
|
702
|
+
pass(*args).val_or_object
|
703
|
+
end
|
704
|
+
|
705
|
+
# sets the given javascript variable to this object, and returns a JsshObject referring
|
706
|
+
# to the variable.
|
707
|
+
#
|
708
|
+
# >> foo=document.getElementById('guser').store('foo')
|
709
|
+
# => #<JsshObject:0x2dff870 @ref="foo" ...>
|
710
|
+
# >> foo.tagName
|
711
|
+
# => "DIV"
|
712
|
+
def store(js_variable, somewhere_meaningful=true)
|
713
|
+
stored=JsshObject.new(js_variable, jssh_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
|
714
|
+
stored.assign_expr(self.ref)
|
715
|
+
stored
|
716
|
+
end
|
717
|
+
|
718
|
+
def store_rand_named(&name_proc)
|
719
|
+
base=36
|
720
|
+
length=6
|
721
|
+
begin
|
722
|
+
name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
|
723
|
+
end while JsshObject.new(name,jssh_socket).type!='undefined'
|
724
|
+
# okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
|
725
|
+
store(name, false)
|
726
|
+
end
|
727
|
+
|
728
|
+
def store_rand_prefix(prefix)
|
729
|
+
store_rand_named do |r|
|
730
|
+
prefix+"_"+r
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
def store_rand_object_key(object)
|
735
|
+
raise ArgumentError("Object is not a JsshObject: got #{object.inspect}") unless object.is_a?(JsshObject)
|
736
|
+
store_rand_named do |r|
|
737
|
+
object.sub(r).ref
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
def store_rand_temp
|
742
|
+
store_rand_object_key(jssh_socket.temp_object)
|
743
|
+
end
|
744
|
+
|
745
|
+
# returns a JsshObject referring to a subscript of this object, specified as a _javascript_ expression
|
746
|
+
# (doesn't use to_jssh)
|
747
|
+
# def sub_expr(key_expr)
|
748
|
+
# JsshObject.new("#{ref}[#{key_expr}]", jssh_socket, :debug_name => "#{debug_name}[#{}]")
|
749
|
+
# end
|
750
|
+
|
751
|
+
# returns a JsshObject referring to a subscript of this object, specified as a ruby object converted to
|
752
|
+
# javascript via to_jssh
|
753
|
+
def sub(key)
|
754
|
+
JsshObject.new("#{ref}[#{key.to_jssh}]", jssh_socket, :debug_name => "#{debug_name}[#{key.is_a?(JsshObject) ? key.debug_name : key.to_jssh}]")
|
755
|
+
end
|
756
|
+
|
757
|
+
# returns a JsshObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
|
758
|
+
# subscript is specified as ruby (converted to_jssh).
|
759
|
+
def [](key)
|
760
|
+
sub(key).val_or_object(:error_on_undefined => false)
|
761
|
+
end
|
762
|
+
# assigns the given ruby value (passed through json via JsshSocket#assign_json) to the given subscript
|
763
|
+
# (key is converted to_jssh).
|
764
|
+
def []=(key, value)
|
765
|
+
self.sub(key).assign(value)
|
766
|
+
end
|
767
|
+
|
768
|
+
# calls a binary operator with self and another operand
|
769
|
+
def binary_operator(operator, operand)
|
770
|
+
JsshObject.new("(#{ref}#{operator}#{operand.to_jssh})", jssh_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JsshObject) ? operand.debug_name : operand.to_jssh})").val_or_object
|
771
|
+
end
|
772
|
+
def +(operand)
|
773
|
+
binary_operator('+', operand)
|
774
|
+
end
|
775
|
+
def -(operand)
|
776
|
+
binary_operator('-', operand)
|
777
|
+
end
|
778
|
+
def /(operand)
|
779
|
+
binary_operator('/', operand)
|
780
|
+
end
|
781
|
+
def *(operand)
|
782
|
+
binary_operator('*', operand)
|
783
|
+
end
|
784
|
+
def %(operand)
|
785
|
+
binary_operator('%', operand)
|
786
|
+
end
|
787
|
+
def ==(operand)
|
788
|
+
binary_operator('==', operand)
|
789
|
+
end
|
790
|
+
def >(operand)
|
791
|
+
binary_operator('>', operand)
|
792
|
+
end
|
793
|
+
def <(operand)
|
794
|
+
binary_operator('<', operand)
|
795
|
+
end
|
796
|
+
def >=(operand)
|
797
|
+
binary_operator('>=', operand)
|
798
|
+
end
|
799
|
+
def <=(operand)
|
800
|
+
binary_operator('<=', operand)
|
801
|
+
end
|
802
|
+
|
803
|
+
# method_missing handles unknown method calls in a way that makes it possible to write
|
804
|
+
# javascript-like syntax in ruby, to some extent.
|
805
|
+
#
|
806
|
+
# method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
|
807
|
+
# special characters, only alphanumeric/underscores, starting with alpha or underscore - with
|
808
|
+
# the exception of three special behaviors:
|
809
|
+
#
|
810
|
+
# If the method ends with an equals sign (=), it does assignment - it calls JsshSocket#assign_json
|
811
|
+
# to do the assignment and returns the assigned value.
|
812
|
+
#
|
813
|
+
# If the method ends with a bang (!), then it will attempt to get the value (using json) of the
|
814
|
+
# reference, using JsonObject#val. For simple types (null, string, boolean, number), this is what
|
815
|
+
# happens by default anyway, but if you have an object or an array that you know you can json-ize,
|
816
|
+
# you can use ! to force that. See #invoke documentation for more information.
|
817
|
+
#
|
818
|
+
# If the method ends with a question mark (?), then it will attempt to get a string representing the
|
819
|
+
# value, using JsonObject#val_str. This is safer than ! because the javascript conversion to json
|
820
|
+
# can error. This also catches the JsshUndefinedValueError that can occur, and just returns nil
|
821
|
+
# for undefined stuff.
|
822
|
+
#
|
823
|
+
# otherwise, method_missing calls to #invoke, and returns a JsshObject, a string, a boolean, a number, or
|
824
|
+
# null - see documentation for #invoke.
|
825
|
+
#
|
826
|
+
# Since #invoke returns a JsshObject for javascript objects, this means that you can string together
|
827
|
+
# method_missings and the result looks rather like javascript.
|
828
|
+
#
|
829
|
+
# this lets you do things like:
|
830
|
+
# >> jssh_socket.object('getWindows()').length
|
831
|
+
# => 2
|
832
|
+
# >> jssh_socket.object('getWindows()')[1].getBrowser.contentDocument?
|
833
|
+
# => "[object XPCNativeWrapper [object HTMLDocument]]"
|
834
|
+
# >> document=jssh_socket.object('getWindows()')[1].getBrowser.contentDocument
|
835
|
+
# => #<JsshObject:0x34f01fc @ref="getWindows()[1].getBrowser().contentDocument" ...>
|
836
|
+
# >> document.title
|
837
|
+
# => "ruby - Google Search"
|
838
|
+
# >> document.forms[0].q.value
|
839
|
+
# => "ruby"
|
840
|
+
# >> document.forms[0].q.value='foobar'
|
841
|
+
# => "foobar"
|
842
|
+
# >> document.forms[0].q.value
|
843
|
+
# => "foobar"
|
844
|
+
#
|
845
|
+
# $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
|
846
|
+
# methods to arrays and hashes - see http://www.prototypejs.org/
|
847
|
+
# You can use these methods with method_missing just like any other:
|
848
|
+
#
|
849
|
+
# >> js_hash=jssh_socket.object('$H')
|
850
|
+
# => #<JsshObject:0x2beb598 @ref="$H" ...>
|
851
|
+
# >> js_arr=jssh_socket.object('$A')
|
852
|
+
# => #<JsshObject:0x2be40e0 @ref="$A" ...>
|
853
|
+
#
|
854
|
+
# >> js_arr.pass(document.body.childNodes).pluck! :tagName
|
855
|
+
# => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
|
856
|
+
# >> js_arr.pass(document.body.childNodes).pluck! :id
|
857
|
+
# => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
|
858
|
+
# >> js_hash.pass(document.getElementById('tbd')).keys!
|
859
|
+
# => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", "style", "innerHTML", "nextSibling", "tagName", "id", "nodeName", "nodeValue", "nodeType", "childNodes", "firstChild", "lastChild", "previousSibling", "attributes", "ownerDocument", "insertBefore", "replaceChild", "removeChild", "hasChildNodes", "cloneNode", "normalize", "isSupported", "namespaceURI", "prefix", "localName", "hasAttributes", "getAttribute", "setAttribute", "removeAttribute", "getAttributeNode", "setAttributeNode", "removeAttributeNode", "getAttributeNS", "setAttributeNS", "removeAttributeNS", "getAttributeNodeNS", "setAttributeNodeNS", "getElementsByTagNameNS", "hasAttribute", "hasAttributeNS", "ELEMENT_NODE", "ATTRIBUTE_NODE", "TEXT_NODE", "CDATA_SECTION_NODE", "ENTITY_REFERENCE_NODE", "ENTITY_NODE", "PROCESSING_INSTRUCTION_NODE", "COMMENT_NODE", "DOCUMENT_NODE", "DOCUMENT_TYPE_NODE", "DOCUMENT_FRAGMENT_NODE", "NOTATION_NODE", "lang", "dir", "align", "offsetTop", "offsetLeft", "offsetWidth", "offsetHeight", "offsetParent", "scrollTop", "scrollLeft", "scrollHeight", "scrollWidth", "clientTop", "clientLeft", "clientHeight", "clientWidth", "tabIndex", "contentEditable", "blur", "focus", "spellcheck", "removeEventListener", "dispatchEvent", "baseURI", "compareDocumentPosition", "textContent", "isSameNode", "lookupPrefix", "isDefaultNamespace", "lookupNamespaceURI", "isEqualNode", "getFeature", "setUserData", "getUserData", "DOCUMENT_POSITION_DISCONNECTED", "DOCUMENT_POSITION_PRECEDING", "DOCUMENT_POSITION_FOLLOWING", "DOCUMENT_POSITION_CONTAINS", "DOCUMENT_POSITION_CONTAINED_BY", "DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC", "getElementsByClassName", "getClientRects", "getBoundingClientRect"]
|
860
|
+
#
|
861
|
+
def method_missing(method, *args)
|
862
|
+
method=method.to_s
|
863
|
+
if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
|
864
|
+
method = $1
|
865
|
+
special = $2
|
866
|
+
else # don't deal with any special character crap
|
867
|
+
#Object.instance_method(:method_missing).bind(self).call(method, *args) # let Object#method_missing raise its usual error
|
868
|
+
return super
|
869
|
+
end
|
870
|
+
case special
|
871
|
+
when nil
|
872
|
+
invoke(method, *args)
|
873
|
+
when '!'
|
874
|
+
got=invoke(method, *args)
|
875
|
+
got.is_a?(JsshObject) ? got.val : got
|
876
|
+
when '?'
|
877
|
+
begin
|
878
|
+
got=invoke(method, *args)
|
879
|
+
got.is_a?(JsshObject) ? got.val_str : got
|
880
|
+
rescue JsshUndefinedValueError
|
881
|
+
nil
|
882
|
+
end
|
883
|
+
when '='
|
884
|
+
attr(method).assign(*args)
|
885
|
+
else
|
886
|
+
Object.instance_method(:method_missing).bind(self).call(method, *args) # this shouldn't happen
|
887
|
+
end
|
888
|
+
end
|
889
|
+
def define_methods!
|
890
|
+
metaclass=(class << self; self; end)
|
891
|
+
self.to_hash.keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
|
892
|
+
metaclass.send(:define_method, key) do |*args|
|
893
|
+
invoke(key, *args)
|
894
|
+
end
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
def respond_to?(method)
|
899
|
+
super || object_respond_to?(method)
|
900
|
+
end
|
901
|
+
def object_respond_to?(method)
|
902
|
+
method=method.to_s
|
903
|
+
if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
|
904
|
+
method = $1
|
905
|
+
special = $2
|
906
|
+
else # don't deal with any special character crap
|
907
|
+
return false
|
908
|
+
end
|
909
|
+
|
910
|
+
if self.type=='undefined'
|
911
|
+
return false
|
912
|
+
elsif special=='='
|
913
|
+
if self.type=='object'
|
914
|
+
return true # yeah, you can generally assign attributes to objects
|
915
|
+
else
|
916
|
+
return false # no, you can't generally assign attributes to (boolean, number, string, null)
|
917
|
+
end
|
918
|
+
else
|
919
|
+
attr=attr(method)
|
920
|
+
return attr.type!='undefined'
|
921
|
+
end
|
922
|
+
end
|
923
|
+
|
924
|
+
def id(*args)
|
925
|
+
invoke :id, *args
|
926
|
+
end
|
927
|
+
|
928
|
+
# gives a reference for this object. this is the only class for which to_jssh doesn't
|
929
|
+
# convert the object to json.
|
930
|
+
def to_jssh
|
931
|
+
ref
|
932
|
+
end
|
933
|
+
# this still needs to be defined because when ActiveSupport::JSON.encode is called by to_jssh
|
934
|
+
# on an array or hash containing a JsshObject, it calls to_json. which apparently just freezes.
|
935
|
+
# I guess that's because JsshSocket circularly references itself with its instance variables.
|
936
|
+
def to_json(options={})
|
937
|
+
ref
|
938
|
+
end
|
939
|
+
|
940
|
+
def to_js_array
|
941
|
+
jssh_socket.object('$A').call(self)
|
942
|
+
end
|
943
|
+
def to_js_hash
|
944
|
+
jssh_socket.object('$H').call(self)
|
945
|
+
end
|
946
|
+
def to_js_hash_safe
|
947
|
+
jssh_socket.object('$_H').call(self)
|
948
|
+
end
|
949
|
+
def to_array
|
950
|
+
JsshArray.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
951
|
+
end
|
952
|
+
def to_hash
|
953
|
+
JsshHash.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
954
|
+
end
|
955
|
+
def to_dom
|
956
|
+
JsshDOMNode.new(self.ref, self.jssh_socket, :debug_name => debug_name)
|
957
|
+
end
|
958
|
+
def to_ruby_hash(options={})
|
959
|
+
options={:recurse => 1}.merge(options)
|
960
|
+
return self if !options[:recurse] || options[:recurse]==0
|
961
|
+
return self if self.type!='object'
|
962
|
+
next_options=options.merge(:recurse => options[:recurse]-1)
|
963
|
+
begin
|
964
|
+
keys=self.to_hash.keys
|
965
|
+
rescue JsshError
|
966
|
+
return self
|
967
|
+
end
|
968
|
+
keys.inject({}) do |hash, key|
|
969
|
+
val=begin
|
970
|
+
self[key]
|
971
|
+
rescue JsshError
|
972
|
+
$!
|
973
|
+
end
|
974
|
+
hash[key]=if val.is_a?(JsshObject)
|
975
|
+
val.to_ruby_hash(next_options)
|
976
|
+
else
|
977
|
+
val
|
978
|
+
end
|
979
|
+
hash
|
980
|
+
end
|
981
|
+
end
|
982
|
+
|
983
|
+
def inspect
|
984
|
+
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
|
985
|
+
end
|
986
|
+
def pretty_print(pp)
|
987
|
+
pp.object_address_group(self) do
|
988
|
+
pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
|
989
|
+
pp.breakable ' '
|
990
|
+
pp.group(0) do
|
991
|
+
pp.text attr.to_s
|
992
|
+
pp.text ': '
|
993
|
+
#pp.breakable
|
994
|
+
pp.text send(attr)
|
995
|
+
end
|
996
|
+
end
|
997
|
+
end
|
998
|
+
end
|
999
|
+
end
|
1000
|
+
|
1001
|
+
class JsshDOMNode < JsshObject
|
1002
|
+
def inspect
|
1003
|
+
# "\#<#{self.class.name} #{[:nodeName, :nodeType, :tagName, :textContent, :id, :name, :value, :type].map{|attrn| attr=attr(attrn);attrn.to_s+'='+(attr.type=='undefined' ? 'undefined' : attr.val_or_object(:error_on_undefined => false).inspect)}.join(', ')}>"
|
1004
|
+
"\#<#{self.class.name} #{[:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map{|attrn|attr=attr(attrn);(['undefined','null'].include?(attr.type) ? nil : attrn.to_s+'='+attr.val_or_object(:error_on_undefined => false).inspect)}.select{|a|a}.join(', ')}>"
|
1005
|
+
end
|
1006
|
+
def dump(options={})
|
1007
|
+
options={:recurse => nil, :level => 0}.merge(options)
|
1008
|
+
next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
|
1009
|
+
result=(" "*options[:level]*2)+self.inspect+"\n"
|
1010
|
+
if options[:recurse]==0
|
1011
|
+
result+=(" "*next_options[:level]*2)+"...\n"
|
1012
|
+
else
|
1013
|
+
self.childNodes.to_array.each do |child|
|
1014
|
+
result+=child.to_dom.dump(next_options)
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
result
|
1018
|
+
end
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
class JsshArray < JsshObject
|
1022
|
+
def each
|
1023
|
+
length=self.length
|
1024
|
+
raise JsshError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
|
1025
|
+
for i in 0...length
|
1026
|
+
element=self[i]
|
1027
|
+
if element.is_a?(JsshObject)
|
1028
|
+
# yield a more permanent reference than the array subscript
|
1029
|
+
element=element.store_rand_temp
|
1030
|
+
end
|
1031
|
+
yield element
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
include Enumerable
|
1035
|
+
def to_json(options={}) # Enumerable clobbers this; redefine
|
1036
|
+
ref
|
1037
|
+
end
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
class JsshHash < JsshObject
|
1041
|
+
def keys
|
1042
|
+
keyfunc="function(obj)
|
1043
|
+
{ var keys=[];
|
1044
|
+
for(var key in obj)
|
1045
|
+
{ keys.push(key);
|
1046
|
+
}
|
1047
|
+
return keys;
|
1048
|
+
}"
|
1049
|
+
@keys=jssh_socket.object(keyfunc).pass(self).val
|
1050
|
+
end
|
1051
|
+
def each
|
1052
|
+
keys.each do |key|
|
1053
|
+
yield [key, self[key]]
|
1054
|
+
end
|
1055
|
+
end
|
1056
|
+
def each_pair
|
1057
|
+
each do |(k,v)|
|
1058
|
+
yield k,v
|
1059
|
+
end
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
include Enumerable
|
1063
|
+
def to_json(options={}) # Enumerable clobbers this; redefine
|
1064
|
+
ref
|
1065
|
+
end
|
1066
|
+
end
|