opal-js_wrap-three 0.1.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.
@@ -0,0 +1,4 @@
1
+ require "js_wrap"
2
+ require "js_wrap/three/three" # JavaScript
3
+
4
+ Three = JSWrap::ClassView.wrap(`Three`)
@@ -0,0 +1,720 @@
1
+ # helpers: native, defineProperty
2
+
3
+ `var inspect_stack = []`
4
+
5
+ # JSWrap is basically a Native v2. It has the same aims and goals. It should
6
+ # be interoperable with Native, ie. both modules can coexist.
7
+
8
+ # TODO: DSL methods:
9
+ # - js_method
10
+ # - (
11
+ # how about something like: js_method :set_timeout => [:$block, :timeout]
12
+ # which would allow us also to call it using any of the following:
13
+ # `Window.set_timeout(timeout: 123) { p "Hello!" }`
14
+ # `Window.set_timeout(123) { p "Hello!" }`
15
+ # `Window.set_timeout(-> { p "Hello" }, 123)`
16
+ # )
17
+ # TODO: support for restricted values
18
+ # TODO: to_js
19
+
20
+ module JSWrap
21
+ module WrapperClassMethods
22
+ # By default, we will transfer calls and properties like
23
+ # "my_method" into "myMethod". Since, unlike with Native, we
24
+ # inherit from Object, not BasicObject, some call names may
25
+ # conflict, but we can use names like "class_" instead of
26
+ # "class". This setting allows us to disable this behavior.
27
+ attr_accessor :js_raw_naming
28
+
29
+ # The default behavior when dispatching to a function is to
30
+ # call it and return a result.
31
+ attr_accessor :js_function_wrap
32
+
33
+ # The default behavior for things based on ObjectWrapper is to
34
+ # automatically wrap even the regular Arrays, so that we may
35
+ # correctly wrap all members.
36
+ #
37
+ # The default behavior for things based on Wrapper is to never
38
+ # wrap the regular Arrays, only Array-like things. It is the
39
+ # responsibility of the programmer to provide a good interface.
40
+ #
41
+ # This property allows you to change the default behavior.
42
+ attr_accessor :js_array_wrap
43
+
44
+ def js_attr_reader(*vars)
45
+ vars.each do |var|
46
+ define_method(var) { js_property(var) }
47
+ end
48
+ vars.length > 1 ? vars : vars.first
49
+ end
50
+
51
+ def js_attr_writer(*vars)
52
+ vars.each do |var|
53
+ define_method(:"#{var}=") { |val| js_property_set(var, val) }
54
+ end
55
+ vars.length > 1 ? vars : vars.first
56
+ end
57
+
58
+ def js_attr_accessor(*vars)
59
+ Array(js_attr_reader(*vars)) + Array(js_attr_writer(*vars))
60
+ end
61
+
62
+ def js_method(*vars)
63
+ vars.each do |var|
64
+ define_method(var) do |*args,&block|
65
+ args << block if block_given?
66
+ js_property(var, args: args)
67
+ end
68
+ end
69
+ vars.length > 1 ? vars : vars.first
70
+ end
71
+
72
+ def js_export_class(classname = name)
73
+ `Opal.global[name] = self`
74
+ end
75
+
76
+ alias js_export_module js_export_class
77
+
78
+ # Example:
79
+ #
80
+ # module Blobulizer
81
+ # include JSWrap::Wrapper
82
+ # js_export_module
83
+ #
84
+ # js_export def self.extended_blobulize(thing)
85
+ # Blobulizer::Processor.new(thing).extended_blobulize
86
+ # end
87
+ # end
88
+ #
89
+ # JS:
90
+ # Blobulizer.extendedBlobulize({hello: "world"})
91
+ def js_export(*args)
92
+ args = Array(args.first) if args.length == 1
93
+
94
+ args.each do |i|
95
+ `self[#{js_property_name_rb2js(i)}] = #{JSWrap.unwrap_proc(`method(i).to_proc`, self)}`
96
+ end
97
+ end
98
+
99
+ def wrap(object, parent = nil)
100
+ obj = allocate
101
+ obj.initialize_wrapped(object, parent)
102
+ obj
103
+ end
104
+
105
+ def js_class(js_constructor)
106
+ js_constructor = JSWrap.unwrap(js_constructor, self)
107
+ @native = js_constructor
108
+ JSWrap.register_wrapping(self, js_constructor)
109
+ end
110
+
111
+ # Also includes RawClassWrapper, but we don't have that yet.
112
+ end
113
+
114
+ module Wrapper
115
+ def self.included(klass)
116
+ klass.extend(WrapperClassMethods)
117
+ super
118
+ end
119
+
120
+ attr_accessor :native
121
+
122
+ def initialize_wrapped(object, parent = nil)
123
+ %x{
124
+ try {
125
+ $defineProperty(object, '$$js_wrap', self)
126
+ }
127
+ catch(e) {}
128
+ }
129
+ @native_parent = parent if parent
130
+ @native = object
131
+ end
132
+
133
+ def initialize(*args, &block)
134
+ raise NotImplementedError, 'See [doc:JSWrap::Wrapper#initialize]' unless `#{self.class}.native`
135
+
136
+ args << block if block_given?
137
+ args = JSWrap.unwrap_ary(args, self)
138
+ obj = `new (self.$$class.native.bind.apply(self.$$class.native, [null].concat(args)))`
139
+ initialize_wrapped(obj)
140
+ end
141
+
142
+ # @private
143
+ def js_property_name_rb2js(name, raw: nil)
144
+ raw = self.js_function_wrap if raw.nil?
145
+ if raw || name.start_with?(/[A-Z]/)
146
+ name
147
+ else
148
+ name.gsub(/_(.?)/) { Regexp.last_match(1).upcase }
149
+ end
150
+ end
151
+
152
+ # @private
153
+ def js_property_name_js2rb(name, raw: nil)
154
+ raw = self.js_function_wrap if raw.nil?
155
+ if raw || name.start_with?(/[A-Z]/)
156
+ name
157
+ else
158
+ name.gsub(/([A-Z])/) { '_' + Regexp.last_match(1).downcase }
159
+ end
160
+ end
161
+
162
+ def js_properties(not_just_own = true, raw: nil)
163
+ %x{
164
+ var out = [];
165
+ for (var name in self.native) {
166
+ if (!not_just_own || self.native.hasOwnProperty(name)) {
167
+ #{`out` << js_property_name_js2rb(`name`, raw: raw)}
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+ end
173
+
174
+ def js_property?(name, raw: nil)
175
+ %x{
176
+ return #{js_property_name_rb2js(name, raw: raw)} in self.native
177
+ }
178
+ end
179
+
180
+ # Returns a wrapped property. If args are nil, functions are
181
+ # always returned verbatim.
182
+ def js_property(name, raw: nil, args: nil)
183
+ JSWrap(`self.native[#{
184
+ js_property_name_rb2js(name, raw: raw)
185
+ }]`,
186
+ self, args, name: name
187
+ )
188
+ end
189
+
190
+ def js_property_set(name, value, raw: nil)
191
+ `self.native[#{
192
+ js_property_name_rb2js(name, raw: raw)
193
+ }] = #{JSWrap.unwrap(value, self)}`
194
+ end
195
+
196
+ def js_raw_naming
197
+ @js_raw_naming.nil? ? self.class.js_raw_naming : @js_raw_naming
198
+ end
199
+
200
+ def js_function_wrap
201
+ @js_function_wrap.nil? ? self.class.js_function_wrap : @js_function_wrap
202
+ end
203
+
204
+ def js_array_wrap
205
+ @js_array_wrap.nil? ? self.class.js_array_wrap : @js_array_wrap
206
+ end
207
+
208
+ def to_js
209
+ @native
210
+ end
211
+
212
+ alias to_n to_js
213
+ end
214
+
215
+ module ObjectWrapper
216
+ include Enumerable
217
+ include Wrapper
218
+
219
+ alias [] js_property
220
+
221
+ def []=(name, kwargs = {}, value)
222
+ js_property_set(name, value, **kwargs)
223
+ end
224
+
225
+ alias keys js_properties
226
+ alias has? js_property
227
+ alias include? js_property
228
+
229
+ def values
230
+ to_h.values
231
+ end
232
+
233
+ def each(&block)
234
+ return enum_for(:each) { length } unless block_given?
235
+
236
+ %x{
237
+ for (var name in self.native) {
238
+ Opal.yieldX(block, [
239
+ #{js_property_name_js2rb `name`},
240
+ #{JSWrap(`self.native[name]`, self)}
241
+ ]);
242
+ }
243
+ }
244
+ self
245
+ end
246
+
247
+ def inspect
248
+ if `inspect_stack`.include?(__id__)
249
+ inside = '...'
250
+ else
251
+ inside = to_h.inspect
252
+ `inspect_stack` << __id__
253
+ pushed = true
254
+ end
255
+ klass = `Object.prototype.toString.apply(self.native)`
256
+ klass =~ /^\[object (.*?)\]$/
257
+ "#<#{self.class.name} #{Regexp.last_match(1) || klass} #{inside}>"
258
+ ensure
259
+ `inspect_stack.pop()` if pushed
260
+ end
261
+
262
+ def pretty_print(o)
263
+ klass = `Object.prototype.toString.apply(self.native)`
264
+ klass =~ /^\[object (.*?)\]$/
265
+ o.group(1, "#<#{self.class.name} #{Regexp.last_match(1) || klass}", '>') do
266
+ o.breakable
267
+ o.pp(to_h)
268
+ end
269
+ end
270
+
271
+ def method_missing(method, *args, &block)
272
+ if method.end_with? '='
273
+ raise ArgumentError, 'JS attr assignment needs 1 argument' if args.length != 1
274
+ js_property_set(method[0..-2], args.first)
275
+ elsif js_property?(method)
276
+ args << block if block_given?
277
+ js_property(method, args: args)
278
+ else
279
+ super
280
+ end
281
+ end
282
+
283
+ def respond_to_missing?(method, include_all = true)
284
+ if method.end_with?('=') || js_property?(method)
285
+ true
286
+ else
287
+ super
288
+ end
289
+ end
290
+
291
+ def raw(raw = true)
292
+ self.dup.tap do |i|
293
+ i.instance_variable_set(:@js_raw_naming, true)
294
+ end
295
+ end
296
+ end
297
+
298
+ module ArrayWrapper
299
+ include Enumerable
300
+ include ObjectWrapper
301
+
302
+ def each(&block)
303
+ return enum_for(:each) { length } unless block_given?
304
+
305
+ %x{
306
+ for (var i = 0; i < self.native.length; i++) {
307
+ Opal.yield1(block, #{JSWrap(`self.native[i]`, self)});
308
+ }
309
+ }
310
+ self
311
+ end
312
+
313
+ def [](key)
314
+ if key.is_a? Number
315
+ JSWrap.wrap(`self.native[key]`, self)
316
+ else
317
+ super
318
+ end
319
+ end
320
+
321
+ def []=(key, kwargs = {}, value)
322
+ if key.is_a? Number
323
+ `self.native[key] = #{JSWrap.unwrap(value, self)}`
324
+ else
325
+ super
326
+ end
327
+ end
328
+
329
+ def <<(value)
330
+ `Array.prototype.push.apply(self.native, [#{JSWrap.unwrap(value, self)}])`
331
+ self
332
+ end
333
+
334
+ alias append <<
335
+
336
+ def length
337
+ `self.native.length`
338
+ end
339
+
340
+ def inspect
341
+ if `inspect_stack`.include?(__id__)
342
+ inside = '[...]'
343
+ else
344
+ `inspect_stack` << __id__
345
+ pushed = true
346
+ inside = to_a.inspect
347
+ end
348
+ klass = `Object.prototype.toString.apply(self.native)`
349
+ klass =~ /^\[object (.*?)\]$/
350
+ "#<#{self.class.name} #{Regexp.last_match(1) || klass} #{inside}>"
351
+ ensure
352
+ `inspect_stack.pop()` if pushed
353
+ end
354
+
355
+ def pretty_print(o)
356
+ klass = `Object.prototype.toString.apply(self.native)`
357
+ klass =~ /^\[object (.*?)\]$/
358
+ o.group(1, "#<#{self.class.name} #{Regexp.last_match(1) || klass}", '>') do
359
+ o.breakable
360
+ o.pp(to_a)
361
+ end
362
+ end
363
+ end
364
+
365
+ module FunctionWrapper
366
+ include ObjectWrapper
367
+
368
+ def call(*args, &block)
369
+ JSWrap.call(@native_parent, @native, *args, &block)
370
+ end
371
+
372
+ def to_proc
373
+ proc do |*args, &block|
374
+ call(*args, &block)
375
+ end
376
+ end
377
+
378
+ def inspect
379
+ if `typeof self.native.toString !== 'function'`
380
+ super
381
+ else
382
+ fundesc = `self.native.toString()`.split('{').first.strip.delete("\n")
383
+ "#<#{self.class.name} #{fundesc}>"
384
+ end
385
+ end
386
+
387
+ def pretty_print(o)
388
+ if `typeof self.native.toString !== 'function'`
389
+ super
390
+ else
391
+ o.text(inspect)
392
+ end
393
+ end
394
+ end
395
+
396
+ module RawClassWrapper
397
+ include Wrapper
398
+
399
+ def superclass
400
+ JSWrap(`#{@native}.prototype.__proto__`, self)
401
+ end
402
+ end
403
+
404
+ module ClassWrapper
405
+ include FunctionWrapper
406
+ include RawClassWrapper
407
+
408
+ def const_missing(name)
409
+ if js_property?(name)
410
+ js_property(name)
411
+ else
412
+ super
413
+ end
414
+ end
415
+
416
+ def new(*args, &block)
417
+ args << block if block_given?
418
+ args = JSWrap.unwrap_ary(args, self)
419
+ JSWrap(`new (#{@native}.bind.apply(#{@native}, [null].concat(args)))`, self)
420
+ end
421
+ end
422
+
423
+ def self.wrapped?(object)
424
+ `object != null && '$$class' in object && Opal.is_a(object, Opal.JSWrap.Wrapper)`
425
+ end
426
+
427
+ def self.unwrap(object, parent = nil)
428
+ %x{
429
+ if (object === null || object === undefined || object === nil) {
430
+ return null;
431
+ }
432
+ else if (object.$$class !== undefined) {
433
+ // Opal < 1.4 bug: Opal.respond_to vs #respond_to_missing? does not call
434
+ // #respond_to?
435
+ if (object.$to_js !== undefined && !object.$to_js.$$stub) {
436
+ return object.$to_js(parent);
437
+ }
438
+ else if (object.$to_n !== undefined && !object.$to_n.$$stub) {
439
+ return object.$to_n();
440
+ }
441
+ }
442
+ return object;
443
+ }
444
+ end
445
+
446
+ def self.unwrap_ary(ary, parent = nil)
447
+ %x{
448
+ var i;
449
+ for (i = 0; i < ary.length; i++) {
450
+ ary[i] = #{unwrap(ary[`i`], parent)}
451
+ }
452
+ return ary;
453
+ }
454
+ end
455
+
456
+ def self.unwrap_proc(proc, parent = nil)
457
+ %x{
458
+ var f = (function() {
459
+ var i, ary = Array.prototype.slice.apply(arguments), ret;
460
+ for (i = 0; i < ary.length; i++) {
461
+ ary[i] = #{wrap(`ary[i]`, parent)}
462
+ }
463
+ ret = proc.apply(null, ary);
464
+ return #{unwrap(`ret`, parent)}
465
+ });
466
+ f.$$proc_unwrapped = proc;
467
+ return f;
468
+ }
469
+ end
470
+
471
+ def self.wrap(object, parent = nil, args = nil, name: nil)
472
+ %x{
473
+ var i, out, wrapping;
474
+ for (i = self.wrappings.length - 1; i >= 0; i--) {
475
+ wrapping = self.wrappings[i];
476
+ if (wrapping.block !== nil) {
477
+ out = #{`wrapping.block`.call(object, parent, args, name)}
478
+ if (out == null) return nil;
479
+ else if (out !== nil) return out;
480
+ }
481
+ else if (object instanceof wrapping.js_constructor) {
482
+ return #{`wrapping.opal_klass`.wrap(`object`)}
483
+ }
484
+ }
485
+ }
486
+ end
487
+
488
+ def self.call(parent, item, *args, &block)
489
+ args << block if block_given?
490
+ orig_parent = parent
491
+ parent = unwrap(parent, orig_parent)
492
+ args = unwrap_ary(args, orig_parent)
493
+ %x{
494
+ if (typeof item !== 'function') {
495
+ item = parent[item];
496
+ }
497
+ item = item.apply(parent, args);
498
+ return #{wrap(item, orig_parent)}
499
+ }
500
+ end
501
+
502
+ @wrappings = []
503
+ def self.register_wrapping(opal_klass = undefined, js_constructor = undefined, priority: 0, &block)
504
+ @wrappings << `{
505
+ priority: priority,
506
+ opal_klass: opal_klass,
507
+ js_constructor: js_constructor,
508
+ block: block
509
+ }`
510
+ @wrappings.sort_by!(&`function(i){
511
+ return -i.priority;
512
+ }`
513
+ )
514
+ end
515
+
516
+ module WrapperClassMethods
517
+ include RawClassWrapper
518
+ end
519
+
520
+ class ObjectView
521
+ extend WrapperClassMethods
522
+ include ObjectWrapper
523
+ self.js_array_wrap = true
524
+ undef Array, String
525
+ end
526
+ class ArrayView
527
+ extend WrapperClassMethods
528
+ include ArrayWrapper
529
+ self.js_array_wrap = true
530
+ end
531
+ # ClassView does not work as a class fully. This is only a limited contract.
532
+ # ie. ClassView.wrap(`BigInt`).new.class == ObjectView.
533
+ # The reason why we inherit from Class is that so we can access properties like A::B
534
+ class ClassView < Class
535
+ extend WrapperClassMethods
536
+ include ClassWrapper
537
+ self.js_array_wrap = true
538
+ end
539
+ class FunctionView
540
+ extend WrapperClassMethods
541
+ include FunctionWrapper
542
+ self.js_array_wrap = true
543
+ end
544
+ end
545
+
546
+ module Kernel
547
+ def JSWrap(*args, &block)
548
+ JSWrap.wrap(*args, &block)
549
+ end
550
+ end
551
+
552
+ JSWrap.register_wrapping(priority: -10) do |item, parent, args, name|
553
+ %x{
554
+ var type = typeof item;
555
+
556
+ var array_wrap = $truthy(parent) ? $truthy(parent.$js_array_wrap()) : true;
557
+ var function_wrap = $truthy(parent) ? $truthy(parent.$js_function_wrap()) : false;
558
+
559
+ if (type === 'undefined' || item === null || item === nil) {
560
+ // A special case not documented anywhere: null makes it dispatch a nil.
561
+ if (args !== nil && args.length !== 0) {
562
+ #{raise ArgumentError, 'given args while dispatching a null value'}
563
+ }
564
+ return null;
565
+ }
566
+ else if (type === 'symbol' || type === 'bigint') {
567
+ // As of Opal 1.4, we don't support those. Let's pretend they
568
+ // are just objects.
569
+ if (args !== nil && args.length !== 0) {
570
+ #{raise ArgumentError, "given args while dispatching a #{`type`}"}
571
+ }
572
+ return #{JSWrap::ObjectView.wrap(item, parent)}
573
+ }
574
+ else if (type === 'number' || type === 'string' || type === 'boolean') {
575
+ // Otherwise it's some primitive and it's wrapped
576
+ if (args !== nil && args.length !== 0) {
577
+ #{raise ArgumentError, "given args while dispatching a #{`type`}"}
578
+ }
579
+ return item;
580
+ }
581
+ else if ('$$js_wrap' in item) {
582
+ // Wrapping is dispatched already, we can trust it to be wrapped properly
583
+ if (args !== nil && args.length !== 0) {
584
+ #{raise ArgumentError, 'given args while dispatching an already dispatched value'}
585
+ }
586
+ return item.$$js_wrap;
587
+ }
588
+ else if (type === 'function') {
589
+ if (item.$$class === Opal.Class) {
590
+ // Opal Class
591
+ if (args !== nil && args.length !== 0) {
592
+ #{raise ArgumentError, 'given args while dispatching an Opal class'}
593
+ }
594
+ return item;
595
+ }
596
+ else if ("$$arity" in item) {
597
+ // Native Opal proc
598
+ return item;
599
+ }
600
+ else if ("$$proc_unwrapped" in item) {
601
+ // Unwrapped native Opal proc
602
+ return item.$$proc_unwrapped;
603
+ }
604
+ else if (name !== nil && 'prototype' in item && name.match(/^[A-Z]/)) {
605
+ // Class
606
+ // There is no reliable way to detect a JS class. So we check if its
607
+ // name starts with an uppercase letter.
608
+ if (args !== nil && args.length !== 0) {
609
+ #{raise ArgumentError, 'given args while dispatching a class'}
610
+ }
611
+ return #{JSWrap::ClassView.wrap(item, parent)}
612
+ }
613
+ else {
614
+ // Regular function
615
+ if (function_wrap || args === nil) {
616
+ return #{JSWrap::FunctionView.wrap(item, parent)}
617
+ }
618
+ else {
619
+ var ret = #{JSWrap.call(parent, item, *args)}
620
+ return ret === nil ? null : ret;
621
+ }
622
+ }
623
+ }
624
+ else if (type === 'object') {
625
+ if (item instanceof Array) {
626
+ // A regular array
627
+ if (args !== nil && args.length !== 0) {
628
+ #{raise ArgumentError, 'given args while dispatching an array'}
629
+ }
630
+ if (array_wrap) {
631
+ return #{JSWrap::ArrayView.wrap(item, parent)}
632
+ }
633
+ else {
634
+ return item;
635
+ }
636
+ }
637
+ else if ('$$class' in item) {
638
+ // Opal item
639
+ if (args !== nil && args.length !== 0) {
640
+ #{raise ArgumentError, 'given args while dispatching an Opal object'}
641
+ }
642
+ return item;
643
+ }
644
+ else {
645
+ // Pass.
646
+ return nil;
647
+ }
648
+ }
649
+ else {
650
+ #{raise ArgumentError, "unknown value type #{type}"}
651
+ }
652
+ }
653
+ end
654
+
655
+ # Now custom wrappers run... but if nothing is found... we run a
656
+ # final wrapper:
657
+
658
+ JSWrap.register_wrapping(priority: 10) do |item, parent, args|
659
+ %x{
660
+ if (args !== nil && args.length !== 0) {
661
+ #{raise ArgumentError, 'given args while dispatching an object'}
662
+ }
663
+ else if ('length' in item) {
664
+ // A pseudo-array, we always wrap those
665
+ return #{JSWrap::ArrayView.wrap(item, parent)}
666
+ }
667
+ else {
668
+ // Otherwise it is a regulat object
669
+ return #{JSWrap::ObjectView.wrap(item, parent)}
670
+ }
671
+ }
672
+ end
673
+
674
+ class Hash
675
+ # @return a JavaScript object with the same keys but calling #to_n on
676
+ # all values.
677
+ def to_js(parent=nil)
678
+ %x{
679
+ var result = {},
680
+ keys = self.$$keys,
681
+ smap = self.$$smap,
682
+ key, value;
683
+ for (var i = 0, length = keys.length; i < length; i++) {
684
+ key = keys[i];
685
+ if (key.$$is_string) {
686
+ value = smap[key];
687
+ } else {
688
+ key = key.key;
689
+ value = key.value;
690
+ }
691
+ key = #{parent.js_property_name_rb2js(`key`)}
692
+ result[key] = #{JSWrap.unwrap(`value`, parent)}
693
+ }
694
+ return result;
695
+ }
696
+ end
697
+ end
698
+
699
+ class Method
700
+ def to_js(parent=nil)
701
+ JSWrap.unwrap_proc(to_proc, parent)
702
+ end
703
+ end
704
+
705
+ class Proc
706
+ def to_js(parent=nil)
707
+ %x{
708
+ // Is this a native Opal Proc? Or is it JavaScript
709
+ // created?
710
+ if ('$$arity' in self) {
711
+ return #{JSWrap.unwrap_proc(self, parent)}
712
+ }
713
+ else {
714
+ return self;
715
+ }
716
+ }
717
+ end
718
+ end
719
+
720
+ JSGlobal = JSWrap(`Opal.global`)