opal_proxy 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +36 -0
- data/CHANGELOG.md +15 -1
- data/README.md +35 -5
- data/lib/js/proxy.rb +166 -97
- data/lib/opal_proxy/version.rb +1 -1
- data/lib/opal_proxy.rb +3 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16f6f216939bb7ad43a3e8046eba3a468376b385b7c517e7f114ba3ed4fb6f8e
|
|
4
|
+
data.tar.gz: 2fd8b4d5b95354facae44f4978a2b8ed566ff70d005269d2ab715ab1e60a69f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 378e55ea22b7d67177c22cf580e4adf00fc92d9d76d67c3ee531fdc8a462a2471b12625639d8c7a82d213a6960b96e4a0f86511b5a5df2c733edd0da3416dbad
|
|
7
|
+
data.tar.gz: f4520e248767de0a95bb026eba5adadf06e0b0473626436421ff27759a73a4c455862a640535ccef3b0a62cb4b9b54d9108fbadb00b2907a689e2eea19e8c6cf
|
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
SuggestExtensions: false
|
|
2
4
|
TargetRubyVersion: 3.1
|
|
3
5
|
|
|
6
|
+
Metrics/ClassLength:
|
|
7
|
+
Exclude:
|
|
8
|
+
- "test/**/*.rb"
|
|
9
|
+
Max: 150
|
|
10
|
+
|
|
11
|
+
Metrics/MethodLength:
|
|
12
|
+
Exclude:
|
|
13
|
+
- "lib/js/**/*.rb"
|
|
14
|
+
- "test/**/*.rb"
|
|
15
|
+
|
|
16
|
+
Lint/LiteralAsCondition:
|
|
17
|
+
Exclude:
|
|
18
|
+
- "lib/js/**/*.rb"
|
|
19
|
+
|
|
20
|
+
Lint/UnusedMethodArgument:
|
|
21
|
+
Exclude:
|
|
22
|
+
- "lib/js/**/*.rb"
|
|
23
|
+
|
|
24
|
+
Lint/UselessAssignment:
|
|
25
|
+
Exclude:
|
|
26
|
+
- "lib/js/**/*.rb"
|
|
27
|
+
|
|
28
|
+
Lint/Void:
|
|
29
|
+
Exclude:
|
|
30
|
+
- "lib/js/**/*.rb"
|
|
31
|
+
|
|
32
|
+
Naming/PredicateMethod:
|
|
33
|
+
Exclude:
|
|
34
|
+
- "lib/js/**/*.rb"
|
|
35
|
+
|
|
36
|
+
Style/CommandLiteral:
|
|
37
|
+
Exclude:
|
|
38
|
+
- "lib/js/**/*.rb"
|
|
39
|
+
|
|
4
40
|
Style/StringLiterals:
|
|
5
41
|
EnforcedStyle: double_quotes
|
|
6
42
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
- Preserve `JS::Promise` wrappers across `then` and `catch` chains.
|
|
6
|
+
- Wrap callback receivers and arguments, and unwrap callback return values.
|
|
7
|
+
- Support JavaScript iterables and array-like objects through `Enumerable`.
|
|
8
|
+
- Make `respond_to?` reflect native property availability.
|
|
9
|
+
- Improve exact, camelCase, and acronym-aware property lookup.
|
|
10
|
+
- Refresh native property introspection for dynamically added properties.
|
|
11
|
+
- Add deterministic browser coverage without external network dependencies.
|
|
12
|
+
|
|
13
|
+
## [0.1.1] - 2025-08-08
|
|
14
|
+
|
|
15
|
+
- Add `[]=` method to Proxy.
|
|
16
|
+
|
|
3
17
|
## [0.1.0] - 2025-07-24
|
|
4
18
|
|
|
5
|
-
- Initial release
|
|
19
|
+
- Initial release.
|
data/README.md
CHANGED
|
@@ -58,14 +58,44 @@ window.set_timeout(-> {
|
|
|
58
58
|
puts "1. Timeout test OK (1s delay)"
|
|
59
59
|
}, 1000)
|
|
60
60
|
window.fetch("https://jsonplaceholder.typicode.com/todos/1")
|
|
61
|
-
.then
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
61
|
+
.then { |response| response.json }
|
|
62
|
+
.then do |data|
|
|
63
|
+
puts "5. Fetched: #{data["title"]}"
|
|
64
|
+
document.get_element_by_id("output").inner_html += "<p>5. Fetched: #{data["title"]}</p>"
|
|
66
65
|
end
|
|
66
|
+
.catch { |error| warn error["message"] }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Promise callbacks automatically unwrap returned `JS::Proxy` and `JS::Promise` values,
|
|
70
|
+
so native Promise flattening works across chained `then`, `catch`, and `finally` calls.
|
|
71
|
+
|
|
72
|
+
## Interop behavior
|
|
73
|
+
|
|
74
|
+
Property lookup tries the original name first, then camelCase and common JavaScript
|
|
75
|
+
acronyms such as `HTML`, `URL`, and `URI`. For example, both `inner_html` and
|
|
76
|
+
`document_uri` map to their DOM equivalents.
|
|
77
|
+
|
|
78
|
+
JavaScript properties that collide with Ruby methods can always be accessed with `[]`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
proxy["count"]
|
|
82
|
+
proxy["class"]
|
|
67
83
|
```
|
|
68
84
|
|
|
85
|
+
When a Ruby block is passed to a JavaScript method, it is appended as the final callback
|
|
86
|
+
argument. The callback receives its JavaScript `this` value first, followed by the native
|
|
87
|
+
callback arguments, all wrapped when necessary:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
target.add_event_listener("click") do |receiver, event|
|
|
91
|
+
event.prevent_default
|
|
92
|
+
puts receiver
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`JS::Proxy` is enumerable for JavaScript iterable and array-like objects, including
|
|
97
|
+
arrays, `Set`, `Map`, `NodeList`, and objects with numeric indexes and a `length`.
|
|
98
|
+
|
|
69
99
|
## JQuery example
|
|
70
100
|
|
|
71
101
|
```ruby
|
data/lib/js/proxy.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
1
4
|
require "opal"
|
|
2
5
|
require "native"
|
|
3
6
|
|
|
4
7
|
module JS
|
|
8
|
+
# Shared conversion helpers for values crossing the Ruby/JavaScript boundary.
|
|
5
9
|
module Helpers
|
|
6
10
|
def wrap_result(result)
|
|
7
|
-
if `result
|
|
11
|
+
return nil if `result == null`
|
|
12
|
+
|
|
13
|
+
if `typeof result.then === "function" && !result.then.$$owner`
|
|
8
14
|
Promise.new(result)
|
|
9
|
-
elsif `
|
|
15
|
+
elsif `result instanceof Number || result instanceof String || result instanceof Boolean`
|
|
16
|
+
`result.valueOf()`
|
|
17
|
+
elsif `typeof result === "object"`
|
|
10
18
|
Proxy.new(result)
|
|
11
19
|
else
|
|
12
20
|
result
|
|
@@ -14,109 +22,151 @@ module JS
|
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def native_methods
|
|
17
|
-
|
|
18
|
-
let
|
|
19
|
-
const
|
|
25
|
+
%x{
|
|
26
|
+
let object = #{to_n};
|
|
27
|
+
const properties = new Set();
|
|
28
|
+
|
|
29
|
+
while (object !== null) {
|
|
30
|
+
for (const key of Reflect.ownKeys(object)) {
|
|
31
|
+
if (typeof key === "symbol") continue;
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const stringKey = key.toString()
|
|
24
|
-
const rubyName = #{to_rb_name(`stringKey`)}
|
|
33
|
+
const nativeName = key.toString();
|
|
34
|
+
const rubyName = #{to_rb_name(`nativeName`)};
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
properties.add(nativeName);
|
|
37
|
+
properties.add(rubyName);
|
|
28
38
|
}
|
|
29
|
-
|
|
39
|
+
object = Object.getPrototypeOf(object);
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
return Array.from(
|
|
42
|
+
return Array.from(properties);
|
|
33
43
|
}
|
|
34
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def unwrap_result(result)
|
|
49
|
+
Native.try_convert(result, result)
|
|
50
|
+
end
|
|
35
51
|
end
|
|
36
52
|
|
|
53
|
+
# Provides Ruby-style access to the properties and methods of a JavaScript object.
|
|
37
54
|
class Proxy
|
|
38
55
|
include Enumerable
|
|
39
56
|
include Helpers
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
IRREGULARS = %w(html url uri)
|
|
58
|
+
IRREGULARS = %w[html url uri].freeze
|
|
44
59
|
|
|
45
60
|
def initialize(native)
|
|
46
|
-
|
|
61
|
+
self.native = native
|
|
47
62
|
end
|
|
48
63
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
unless existing_property?(js_name) || js_name.end_with?("=")
|
|
53
|
-
raise NoMethodError, "undefined method `#{name}` for #{self}"
|
|
54
|
-
end
|
|
64
|
+
def native
|
|
65
|
+
Native(to_n)
|
|
66
|
+
end
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
native[prop] = args.first
|
|
59
|
-
else
|
|
60
|
-
val = native[js_name]
|
|
61
|
-
|
|
62
|
-
if `typeof val === 'function'`
|
|
63
|
-
js_args = args.dup
|
|
64
|
-
|
|
65
|
-
if block
|
|
66
|
-
js_callback = %x{
|
|
67
|
-
function() {
|
|
68
|
-
let args = Array.prototype.slice.call(arguments);
|
|
69
|
-
return #{block.call(self.class.new(`this`), *args)};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
js_args << js_callback
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
result = `val.apply(#{to_n}, #{js_args.to_n})`
|
|
76
|
-
wrap_result(result)
|
|
77
|
-
elsif `typeof val === 'object' && val !== null`
|
|
78
|
-
wrap_result(val)
|
|
79
|
-
else
|
|
80
|
-
val
|
|
81
|
-
end
|
|
82
|
-
end
|
|
68
|
+
def native=(value)
|
|
69
|
+
@native = Native.try_convert(value, value)
|
|
83
70
|
end
|
|
84
71
|
|
|
85
|
-
def
|
|
86
|
-
|
|
72
|
+
def method_missing(name, *args, &block)
|
|
73
|
+
setter = name.end_with?("=")
|
|
74
|
+
property = resolve_property_name(name, allow_missing: setter)
|
|
75
|
+
|
|
76
|
+
return super unless property
|
|
77
|
+
return write_property(property, args.first) if setter
|
|
78
|
+
|
|
79
|
+
read_property(property, args, block)
|
|
87
80
|
end
|
|
88
81
|
|
|
89
82
|
def respond_to_missing?(name, include_private = false)
|
|
90
|
-
|
|
83
|
+
setter = name.end_with?("=")
|
|
84
|
+
!!resolve_property_name(name, allow_missing: setter) || super
|
|
91
85
|
end
|
|
92
86
|
|
|
93
|
-
def each
|
|
94
|
-
return enum_for(:each) unless
|
|
87
|
+
def each(&block)
|
|
88
|
+
return enum_for(:each) unless block
|
|
95
89
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
if iterable?
|
|
91
|
+
each_iterable(&block)
|
|
92
|
+
elsif array_like?
|
|
93
|
+
each_array_like(&block)
|
|
94
|
+
else
|
|
95
|
+
raise TypeError, "#{self.class} does not wrap an iterable or array-like object"
|
|
99
96
|
end
|
|
97
|
+
|
|
98
|
+
self
|
|
100
99
|
end
|
|
101
100
|
|
|
102
101
|
def [](index)
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
wrap_result(`#{to_n}[#{index}]`)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def []=(key, value)
|
|
106
|
+
converted = unwrap_result(value)
|
|
107
|
+
`#{to_n}[#{key}] = #{converted}`
|
|
108
|
+
value
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
def to_n
|
|
108
|
-
native
|
|
112
|
+
@native
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_str
|
|
116
|
+
`String(#{to_n})`
|
|
109
117
|
end
|
|
110
118
|
|
|
111
119
|
def length
|
|
112
|
-
|
|
120
|
+
`#{to_n}.length`
|
|
113
121
|
end
|
|
114
122
|
|
|
115
123
|
private
|
|
116
124
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
def read_property(property, args, block)
|
|
126
|
+
value = `#{to_n}[#{property}]`
|
|
127
|
+
return wrap_result(value) unless `typeof value === "function"`
|
|
128
|
+
|
|
129
|
+
invoke_native_function(value, args, block)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def write_property(property, value)
|
|
133
|
+
converted = unwrap_result(value)
|
|
134
|
+
`#{to_n}[#{property}] = #{converted}`
|
|
135
|
+
value
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def invoke_native_function(callable, args, block)
|
|
139
|
+
arguments = args.dup
|
|
140
|
+
arguments << callback_for(block) if block
|
|
141
|
+
wrap_result(`callable.apply(#{to_n}, #{arguments.to_n})`)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def callback_for(block)
|
|
145
|
+
%x{
|
|
146
|
+
return function() {
|
|
147
|
+
const callbackArgs = Array.prototype.slice.call(arguments).map(function(argument) {
|
|
148
|
+
return #{wrap_result(`argument`)};
|
|
149
|
+
});
|
|
150
|
+
const receiver = #{wrap_result(`this`)};
|
|
151
|
+
return #{unwrap_result(block.call(`receiver`, *`callbackArgs`))};
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def resolve_property_name(name, allow_missing: false)
|
|
157
|
+
ruby_name = name.to_s.delete_suffix("=")
|
|
158
|
+
candidates = js_name_candidates(ruby_name)
|
|
159
|
+
candidates.find { |candidate| existing_property?(candidate) } ||
|
|
160
|
+
(candidates.last if allow_missing)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def js_name_candidates(name)
|
|
164
|
+
[name, camelize(name), camelize(name, acronyms: true)].uniq
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def camelize(name, acronyms: false)
|
|
168
|
+
name.split("_").map.with_index do |part, index|
|
|
169
|
+
if acronyms && IRREGULARS.include?(part.downcase)
|
|
120
170
|
part.upcase
|
|
121
171
|
else
|
|
122
172
|
index.zero? ? part : part.capitalize
|
|
@@ -127,51 +177,70 @@ module JS
|
|
|
127
177
|
def to_rb_name(name)
|
|
128
178
|
name
|
|
129
179
|
.to_s
|
|
130
|
-
.gsub(/([A-Z]+)
|
|
131
|
-
.
|
|
180
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
|
|
181
|
+
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
182
|
+
.tr("-", "_")
|
|
183
|
+
.downcase
|
|
132
184
|
end
|
|
133
185
|
|
|
134
186
|
def existing_property?(property)
|
|
135
187
|
`#{property} in #{to_n}`
|
|
136
188
|
end
|
|
137
|
-
end
|
|
138
189
|
|
|
139
|
-
|
|
140
|
-
|
|
190
|
+
def iterable?
|
|
191
|
+
`typeof Symbol !== "undefined" && typeof #{to_n}[Symbol.iterator] === "function"`
|
|
192
|
+
end
|
|
141
193
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return ruby_result.to_n();
|
|
150
|
-
} else {
|
|
151
|
-
return ruby_result;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
194
|
+
def array_like?
|
|
195
|
+
%x{
|
|
196
|
+
const length = #{to_n}.length;
|
|
197
|
+
return typeof length === "number" &&
|
|
198
|
+
Number.isFinite(length) &&
|
|
199
|
+
length >= 0 &&
|
|
200
|
+
Math.floor(length) === length;
|
|
154
201
|
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def each_iterable
|
|
205
|
+
iterator = `#{to_n}[Symbol.iterator]()`
|
|
206
|
+
|
|
207
|
+
loop do
|
|
208
|
+
step = `iterator.next()`
|
|
209
|
+
break if `step.done`
|
|
155
210
|
|
|
156
|
-
|
|
211
|
+
yield wrap_result(`step.value`)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def each_array_like
|
|
216
|
+
(0...length).each { |index| yield self[index] }
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Ruby wrapper for JavaScript promises with chain-preserving return values.
|
|
221
|
+
class Promise < Proxy
|
|
222
|
+
def then(&block)
|
|
223
|
+
result = block ? `#{to_n}.then(#{promise_callback(block)})` : `#{to_n}.then()`
|
|
224
|
+
Promise.new(result)
|
|
157
225
|
end
|
|
158
226
|
|
|
159
227
|
def catch(&block)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (ruby_result && typeof ruby_result.then === 'function') {
|
|
165
|
-
return ruby_result;
|
|
166
|
-
} else if (ruby_result && typeof ruby_result.to_n === 'function') {
|
|
167
|
-
return ruby_result.to_n();
|
|
168
|
-
} else {
|
|
169
|
-
return ruby_result;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
228
|
+
result = block ? `#{to_n}.catch(#{promise_callback(block)})` : `#{to_n}.catch()`
|
|
229
|
+
Promise.new(result)
|
|
230
|
+
end
|
|
173
231
|
|
|
174
|
-
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
def promise_callback(block)
|
|
235
|
+
%x{
|
|
236
|
+
const proxy = #{self};
|
|
237
|
+
|
|
238
|
+
return function(value) {
|
|
239
|
+
const wrappedValue = proxy.$wrap_result(value);
|
|
240
|
+
const rubyResult = block.$call(wrappedValue);
|
|
241
|
+
return proxy.$unwrap_result(rubyResult);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
175
244
|
end
|
|
176
245
|
end
|
|
177
246
|
end
|
data/lib/opal_proxy/version.rb
CHANGED
data/lib/opal_proxy.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
if RUBY_ENGINE ==
|
|
3
|
+
if RUBY_ENGINE == "opal"
|
|
4
4
|
require_relative "js/proxy"
|
|
5
5
|
else
|
|
6
6
|
require "opal"
|
|
7
7
|
require_relative "opal_proxy/version"
|
|
8
8
|
|
|
9
|
-
Opal.append_path File.expand_path(
|
|
9
|
+
Opal.append_path File.expand_path("lib", __dir__)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Namespace for Opal Proxy versioning and Ruby-side integration.
|
|
12
13
|
module OpalProxy
|
|
13
14
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opal_proxy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joseph Schito
|
|
@@ -24,8 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 1.8.2
|
|
26
26
|
description: Opal Proxy provides a dynamic interface to JavaScript objects in Opal,
|
|
27
|
-
|
|
28
|
-
Ruby syntax.
|
|
27
|
+
with idiomatic property access, method calls, and Promise handling.
|
|
29
28
|
email:
|
|
30
29
|
- joseph.schito@gmail.com
|
|
31
30
|
executables: []
|
|
@@ -48,6 +47,7 @@ metadata:
|
|
|
48
47
|
homepage_uri: https://github.com/josephschito/opal_proxy
|
|
49
48
|
source_code_uri: https://github.com/josephschito/opal_proxy
|
|
50
49
|
changelog_uri: https://github.com/josephschito/opal_proxy/blob/main/CHANGELOG.md
|
|
50
|
+
rubygems_mfa_required: 'true'
|
|
51
51
|
rdoc_options: []
|
|
52
52
|
require_paths:
|
|
53
53
|
- lib
|
|
@@ -62,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
62
62
|
- !ruby/object:Gem::Version
|
|
63
63
|
version: '0'
|
|
64
64
|
requirements: []
|
|
65
|
-
rubygems_version:
|
|
65
|
+
rubygems_version: 4.0.16
|
|
66
66
|
specification_version: 4
|
|
67
67
|
summary: Dynamic Ruby-style wrapper for JavaScript objects in Opal.
|
|
68
68
|
test_files: []
|