From 570d822a4a4f0d08462bdea8ce1c3d058cacf96c Mon Sep 17 00:00:00 2001 From: Andi Date: Fri, 2 Feb 2024 22:59:39 +0900 Subject: [PATCH 01/18] Support dot syntax for attributes and convert values --- packages/gems/js/lib/js.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index cbf1074485..890781103e 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -168,12 +168,23 @@ def to_a # * If the JavaScript method name ends with a question mark (?) def method_missing(sym, *args, &block) sym_str = sym.to_s + sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") if sym_str.end_with?("?") # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True elsif self[sym].typeof == "function" self.call(sym, *args, &block) + elsif self[sym].typeof != "undefined" + if sym_str.end_with?("=") + self[sym] = args[0] + else + if self[sym].typeof === "number" + self[sym].to_f # todo, this conversion maybe belongs elsewhere + else + self[sym] + end + end else super end @@ -185,8 +196,8 @@ def method_missing(sym, *args, &block) def respond_to_missing?(sym, include_private) return true if super sym_str = sym.to_s - sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") - self[sym].typeof == "function" + sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") + self[sym].typeof != "undefined" end # Await a JavaScript Promise like `await` in JavaScript. From f7fd52cc3e387b7b77ffd5228e057f933cbe6ea7 Mon Sep 17 00:00:00 2001 From: Andi Date: Fri, 2 Feb 2024 23:26:46 +0900 Subject: [PATCH 02/18] Fix cases like JS.global.URLSearchParams --- packages/gems/js/lib/js.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 890781103e..88eceba56d 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -173,8 +173,12 @@ def method_missing(sym, *args, &block) # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True - elsif self[sym].typeof == "function" - self.call(sym, *args, &block) + elsif self[sym].typeof == "function" # Todo: What do we do when we want to copy functions around? + begin + self.call(sym, *args, &block) + rescue + self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] + end elsif self[sym].typeof != "undefined" if sym_str.end_with?("=") self[sym] = args[0] From 7ee7b03fc2a4ce78cd33ae19580d2737a7dee84c Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 4 Feb 2024 16:48:28 +0900 Subject: [PATCH 03/18] Bugfix target is null and cast more types (experimental) --- packages/gems/js/lib/js.rb | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 88eceba56d..8c3e121cfa 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -151,6 +151,10 @@ def to_a Array.new(as_array[:length].to_i) { as_array[_1] } end + def nil? + return self === JS::Null + end + # Provide a shorthand form for JS::Object#call # # This method basically calls the JavaScript method with the same @@ -179,16 +183,26 @@ def method_missing(sym, *args, &block) rescue self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] end - elsif self[sym].typeof != "undefined" - if sym_str.end_with?("=") - self[sym] = args[0] + elsif sym_str.end_with?("=") + if args[0].respond_to?("to_js") + self[sym] = args[0].to_js else + self[sym] = args[0] + end + elsif self[sym].typeof != "undefined" if self[sym].typeof === "number" self[sym].to_f # todo, this conversion maybe belongs elsewhere + elsif self[sym].typeof === "string" + self[sym].to_s + elsif self[sym].typeof === "boolean" + self[sym] === JS::True + elsif self[sym].typeof === "symbol" # TODO: check if this works with assingment + self[sym].to_sym + elsif self[sym].typeof === "bigint" + self[sym].to_i else self[sym] end - end else super end @@ -201,7 +215,7 @@ def respond_to_missing?(sym, include_private) return true if super sym_str = sym.to_s sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") - self[sym].typeof != "undefined" + self[sym] != JS::Undefined and self[sym] != JS::Null end # Await a JavaScript Promise like `await` in JavaScript. From 223af00efeb7da179d31396041b9a753d368647b Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 4 Feb 2024 17:08:57 +0900 Subject: [PATCH 04/18] Add correct path relative to first script to eval --- packages/gems/js/lib/js/require_remote.rb | 2 +- packages/gems/js/lib/js/require_remote/url_resolver.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/gems/js/lib/js/require_remote.rb b/packages/gems/js/lib/js/require_remote.rb index 5c9946cf45..6c76ad7b6e 100644 --- a/packages/gems/js/lib/js/require_remote.rb +++ b/packages/gems/js/lib/js/require_remote.rb @@ -70,7 +70,7 @@ def load(relative_feature) code = response.text().await.to_s - evaluate(code, location.filename, final_url) + evaluate(code, location.path, final_url) end private diff --git a/packages/gems/js/lib/js/require_remote/url_resolver.rb b/packages/gems/js/lib/js/require_remote/url_resolver.rb index 7bbaae095c..a5b3e62aa0 100644 --- a/packages/gems/js/lib/js/require_remote/url_resolver.rb +++ b/packages/gems/js/lib/js/require_remote/url_resolver.rb @@ -1,6 +1,6 @@ module JS class RequireRemote - ScriptLocation = Data.define(:url, :filename) + ScriptLocation = Data.define(:url, :filename, :path) # When require_relative is called within a running Ruby script, # the URL is resolved from a relative file path based on the URL of the running Ruby script. @@ -15,7 +15,8 @@ def initialize(base_url) def get_location(relative_feature) filename = filename_from(relative_feature) url = resolve(filename) - ScriptLocation.new(url, filename) + path = JS.global[:URL].new(url, @url_stack.first).pathname.to_s # Get path relative to first call. Supports different urls. + ScriptLocation.new(url, filename, path) end def push(url) From 1f7bdb04fe440dd60358430927663d89ae27b01e Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 4 Feb 2024 22:08:44 +0900 Subject: [PATCH 05/18] support when booleans in js are called with ? for instance element.checked? --- packages/gems/js/lib/js.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 8c3e121cfa..8c7a29e33d 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -176,7 +176,11 @@ def method_missing(sym, *args, &block) if sym_str.end_with?("?") # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. - self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True + if self[sym].typeof == "function" + self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True + else + self[sym] == JS::True + end elsif self[sym].typeof == "function" # Todo: What do we do when we want to copy functions around? begin self.call(sym, *args, &block) @@ -191,7 +195,7 @@ def method_missing(sym, *args, &block) end elsif self[sym].typeof != "undefined" if self[sym].typeof === "number" - self[sym].to_f # todo, this conversion maybe belongs elsewhere + self[sym].to_f # todo, this conversion maybe belongs elsewhere. http status 200 -> 200.0 elsif self[sym].typeof === "string" self[sym].to_s elsif self[sym].typeof === "boolean" From a633fa992a116cbb67e82ad7114b2f9db09ae83c Mon Sep 17 00:00:00 2001 From: Andi Date: Tue, 6 Feb 2024 01:11:13 +0900 Subject: [PATCH 06/18] add require pathname --- lib/ruby_wasm/packager/file_system.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ruby_wasm/packager/file_system.rb b/lib/ruby_wasm/packager/file_system.rb index 955354ef7f..3a3e6e90d6 100644 --- a/lib/ruby_wasm/packager/file_system.rb +++ b/lib/ruby_wasm/packager/file_system.rb @@ -1,3 +1,4 @@ +require 'pathname' # Package Ruby code into a mountable directory. class RubyWasm::Packager::FileSystem def initialize(dest_dir, packager) From 3c26b541a7439373ff39a87981933bbd97aaf492 Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 7 Feb 2024 17:27:50 +0900 Subject: [PATCH 07/18] some additions before refactoring --- packages/gems/js/lib/js.rb | 47 ++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 8c7a29e33d..6305fc1e5e 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -151,10 +151,25 @@ def to_a Array.new(as_array[:length].to_i) { as_array[_1] } end - def nil? - return self === JS::Null + # todo to rub + def to_rb + + # TODO convert js to numbers integers to + # and change this from the method_missing + # make sure to run it in to _a + + # JS::True + # Date to Ruby Date end + # def nil? + # return self === JS::Null + # end + + # def undefined? + # return self === JS::Undefined + # end + # Provide a shorthand form for JS::Object#call # # This method basically calls the JavaScript method with the same @@ -176,12 +191,12 @@ def method_missing(sym, *args, &block) if sym_str.end_with?("?") # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. - if self[sym].typeof == "function" + if self[sym]&.typeof == "function" self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True else self[sym] == JS::True end - elsif self[sym].typeof == "function" # Todo: What do we do when we want to copy functions around? + elsif self[sym]&.typeof == "function" # Todo: What do we do when we want to copy functions around? begin self.call(sym, *args, &block) rescue @@ -193,7 +208,7 @@ def method_missing(sym, *args, &block) else self[sym] = args[0] end - elsif self[sym].typeof != "undefined" + elsif self[sym]&.typeof != "undefined" if self[sym].typeof === "number" self[sym].to_f # todo, this conversion maybe belongs elsewhere. http status 200 -> 200.0 elsif self[sym].typeof === "string" @@ -252,6 +267,28 @@ def await promise = JS.global[:Promise].resolve(self) JS.promise_scheduler.await(promise) end + + # List the methods the object has with the ones in JS + def methods(regular=true) + # Get all properties of the document object, including inherited ones + if self.js_class != JS::Null and self.js_class != JS::Undefined + props = JS.global[:Object].getOwnPropertyNames(self.js_class.prototype) + else + props = [] + end + # Filter the properties to get only methods (functions) + js_methods = props.to_a.select { |prop| self[prop.to_sym].typeof === 'function' } + js_methods + super + end + + #public_methods, private_methods, protected_methods, method, public_method + + def js_class + #return JS::Null if self.nil? or self === JS::Undefined # not sure if it can be undefined + return self[:constructor] + rescue + return nil + end end # A wrapper class for JavaScript Error to allow the Error to be thrown in Ruby. From e0948c0debabf1dfe42b740a8b98a75b2512a9aa Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 7 Feb 2024 18:20:49 +0900 Subject: [PATCH 08/18] Adding enumerables so we can use map and each, etc with JS! --- packages/gems/js/lib/js.rb | 40 +++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 6305fc1e5e..698d9b0e71 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -125,7 +125,10 @@ def self.__async(future, &block) end end +# We treat all JavaScript objects and values as JS::Objects class JS::Object + include Enumerable # Make it possible to enumerate JS:Objects. Example: JS.global.document.querySelectorAll("div").each do + # Create a JavaScript object with the new method # # The below examples show typical usage in Ruby @@ -153,15 +156,30 @@ def to_a # todo to rub def to_rb + return nil if self[sym] + #case self[sym] + # TODO convert js to numbers integers to # and change this from the method_missing # make sure to run it in to _a - + # JS::True # Date to Ruby Date end + def each(&block) + if block_given? + if JS.global[:Array].isArray(self) + self.to_a.each(&block) + else + __props.each(&block) + end + else + to_enum(:each) + end + end + # def nil? # return self === JS::Null # end @@ -271,11 +289,9 @@ def await # List the methods the object has with the ones in JS def methods(regular=true) # Get all properties of the document object, including inherited ones - if self.js_class != JS::Null and self.js_class != JS::Undefined - props = JS.global[:Object].getOwnPropertyNames(self.js_class.prototype) - else - props = [] - end + + props = __props + # Filter the properties to get only methods (functions) js_methods = props.to_a.select { |prop| self[prop.to_sym].typeof === 'function' } js_methods + super @@ -288,7 +304,17 @@ def js_class return self[:constructor] rescue return nil - end + end + + private + + def __props + if self.js_class != JS::Null and self.js_class != JS::Undefined + props = JS.global[:Object].getOwnPropertyNames(self.js_class.prototype) + else + props = [] + end + end end # A wrapper class for JavaScript Error to allow the Error to be thrown in Ruby. From 2f398b4ef719bc1e09343154dd649fd0ef85b0bf Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 7 Feb 2024 19:56:46 +0900 Subject: [PATCH 09/18] move type conversions into method to_rb to_a now converts datatypes inside array to ruby --- packages/gems/js/lib/js.rb | 78 +++++++++++++++++++++------- packages/gems/js/lib/js/date_time.rb | 7 +++ 2 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 packages/gems/js/lib/js/date_time.rb diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 698d9b0e71..2a7962de40 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -2,6 +2,7 @@ require_relative "js/hash.rb" require_relative "js/array.rb" require_relative "js/nil_class.rb" +require_relative "js/date_time.rb" # The JS module provides a way to interact with JavaScript from Ruby. # @@ -70,6 +71,15 @@ module JS True = JS.eval("return true;") False = JS.eval("return false;") + + # JS.try_convert_to_rb(obj) -> Ruby Object or JS::Object + # + # Try to convert the given object to a Ruby Datatype using to_rb + # method. Returns the parameter as JS::Object if the object cannot be converted. + def try_convert_to_rb(obj) + return obj.to_rb + end + class PromiseScheduler def initialize(loop) @loop = loop @@ -151,12 +161,45 @@ def new(*args) # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ... def to_a as_array = JS.global[:Array].from(self) - Array.new(as_array[:length].to_i) { as_array[_1] } + Array.new(as_array[:length].to_i) { + item = as_array[_1] + item.to_rb if item.respond_to?(:to_rb) + } end - # todo to rub + # Try to convert JS Objects into Ruby Objects + # Todo: make a list of Types that need to remain JS::Objects (array methods?) def to_rb - return nil if self[sym] + return nil if self == JS::Null + case self.typeof + when "number" + # TODO: HTTP Codes end up as 200.0, check if it could be integer? + # In JS all numbers are floating point. + # Is there float.to_js? Create tests for number conversion. Check Ruby JSON parser. + self.to_f + when "string" + self.to_s + when "boolean" + self == JS::True + when "symbol" # TODO: check if this works with assingment + self.to_sym + when "bigint" + self.to_i + when "object" + #if self.call(:instanceof, JS.global[:Date]) and not JS.global.isNaN(self) + # self.toISOString() + #elsif JS.global[:Array].isArray(self) + if self.isJSArray + self.to_a + else + self + end + else + self + end + #return self.static_to_rb(self) + + #case self[sym] @@ -168,9 +211,18 @@ def to_rb # Date to Ruby Date end + def isJSArray() + JS.global[:Array].isArray(self) == JS::True + end + + def self.static_to_rb(object) + return nil if self[sym] == JS::Null + + end + def each(&block) if block_given? - if JS.global[:Array].isArray(self) + if self.isJSArray self.to_a.each(&block) else __props.each(&block) @@ -221,25 +273,13 @@ def method_missing(sym, *args, &block) self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] end elsif sym_str.end_with?("=") - if args[0].respond_to?("to_js") + if args[0].respond_to?(:to_js) self[sym] = args[0].to_js else self[sym] = args[0] end - elsif self[sym]&.typeof != "undefined" - if self[sym].typeof === "number" - self[sym].to_f # todo, this conversion maybe belongs elsewhere. http status 200 -> 200.0 - elsif self[sym].typeof === "string" - self[sym].to_s - elsif self[sym].typeof === "boolean" - self[sym] === JS::True - elsif self[sym].typeof === "symbol" # TODO: check if this works with assingment - self[sym].to_sym - elsif self[sym].typeof === "bigint" - self[sym].to_i - else - self[sym] - end + elsif self[sym]&.typeof != "undefined" and self[sym].respond_to?(:to_rb) + self[sym].to_rb else super end diff --git a/packages/gems/js/lib/js/date_time.rb b/packages/gems/js/lib/js/date_time.rb new file mode 100644 index 0000000000..d6aa4596a9 --- /dev/null +++ b/packages/gems/js/lib/js/date_time.rb @@ -0,0 +1,7 @@ +require 'date' + +class DateTime + def to_js + JS.global[:Date].new(self.iso8601) + end +end \ No newline at end of file From 62a58b1d0e3cf36817a5e7466dbf6463a36b8be6 Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 7 Feb 2024 21:27:35 +0900 Subject: [PATCH 10/18] new feature: users dont need to == JS::True anymore. This will catch a lot of mistakes. mine included --- packages/gems/js/lib/js.rb | 81 ++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 2a7962de40..f99e44cfb8 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -159,11 +159,15 @@ def new(*args) # # JS.eval("return [1, 2, 3]").to_a.map(&:to_i) # => [1, 2, 3] # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ... - def to_a + def to_a(convertTypes: true) as_array = JS.global[:Array].from(self) Array.new(as_array[:length].to_i) { item = as_array[_1] - item.to_rb if item.respond_to?(:to_rb) + if convertTypes and item.respond_to?(:to_rb) + return item.to_rb + else + return item + end } end @@ -211,6 +215,18 @@ def to_rb # Date to Ruby Date end + # Support self == true instead of self == JS:True + alias_method :orig_eq, :== + def ==(other) + if other.equal? true + return orig_eq(JS::True) + elsif other.equal? false + return orig_eq(JS::False) + end + + orig_eq(other) + end + def isJSArray() JS.global[:Array].isArray(self) == JS::True end @@ -225,7 +241,7 @@ def each(&block) if self.isJSArray self.to_a.each(&block) else - __props.each(&block) + Array.new(__props).each(&block) end else to_enum(:each) @@ -261,14 +277,19 @@ def method_missing(sym, *args, &block) if sym_str.end_with?("?") # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. - if self[sym]&.typeof == "function" + if self[sym]&.typeof?(:function) self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True else self[sym] == JS::True end - elsif self[sym]&.typeof == "function" # Todo: What do we do when we want to copy functions around? + elsif self[sym]&.typeof?(:function) # Todo: What do we do when we want to copy functions around? begin - self.call(sym, *args, &block) + result = self.call(sym, *args, &block) + if result.typeof?(:boolean) # fixes if searchParams.has("locations") + result == JS::True + else + result + end rescue self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] end @@ -278,7 +299,7 @@ def method_missing(sym, *args, &block) else self[sym] = args[0] end - elsif self[sym]&.typeof != "undefined" and self[sym].respond_to?(:to_rb) + elsif self[sym]&.typeof?(:undefined) == false and self[sym].respond_to?(:to_rb) self[sym].to_rb else super @@ -333,27 +354,45 @@ def methods(regular=true) props = __props # Filter the properties to get only methods (functions) - js_methods = props.to_a.select { |prop| self[prop.to_sym].typeof === 'function' } + #js_methods = props.to_a.select { |prop| self[prop.to_sym].typeof === 'function' }.map { _1.to_sym } + js_methods = props.sort.uniq.filter do |e| + self.JS[e].typeof?(:function) + end js_methods + super end #public_methods, private_methods, protected_methods, method, public_method - def js_class - #return JS::Null if self.nil? or self === JS::Undefined # not sure if it can be undefined - return self[:constructor] - rescue - return nil - end + # def js_class + # #return JS::Null if self.nil? or self === JS::Undefined # not sure if it can be undefined + # return self[:constructor] + # rescue + # return nil + # end - private + # #private + + # def __props + # if self.js_class != JS::Null and self.js_class != JS::Undefined + # prototype = JS.global[:Object].getPrototypeOf(self) + # props = JS.global[:Object].getOwnPropertyNames(self) + # else + # props = [] + # end + # end + + def typeof?(type) + self.typeof === type.to_s + end def __props - if self.js_class != JS::Null and self.js_class != JS::Undefined - props = JS.global[:Object].getOwnPropertyNames(self.js_class.prototype) - else - props = [] - end + props = [] + current_obj = self + + begin + props.concat(JS.global[:Object].getOwnPropertyNames(current_obj).to_a) + current_obj = JS.global[:Object].getPrototypeOf(current_obj) + end while current_obj != nil end end @@ -366,7 +405,7 @@ def initialize(exception) def message stack = @exception[:stack] - if stack.typeof == "string" + if stack.typeof?(:string) # Error.stack contains the error message also stack.to_s else From 44e2797b50d54001aeacc8ba6d94b5440487d8d9 Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 7 Feb 2024 21:37:49 +0900 Subject: [PATCH 11/18] handle comparing to null as well --- packages/gems/js/lib/js.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index f99e44cfb8..cbe75f48f5 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -160,6 +160,7 @@ def new(*args) # JS.eval("return [1, 2, 3]").to_a.map(&:to_i) # => [1, 2, 3] # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ... def to_a(convertTypes: true) + return self if self.is_a? Array as_array = JS.global[:Array].from(self) Array.new(as_array[:length].to_i) { item = as_array[_1] @@ -222,6 +223,8 @@ def ==(other) return orig_eq(JS::True) elsif other.equal? false return orig_eq(JS::False) + elsif other.equal? nil + return orig_eq(JS::Null) || orig_eq(JS::Undefined) end orig_eq(other) @@ -248,13 +251,13 @@ def each(&block) end end - # def nil? - # return self === JS::Null - # end + def nil? + return self == JS::Null + end - # def undefined? - # return self === JS::Undefined - # end + def undefined? + return self == JS::Undefined + end # Provide a shorthand form for JS::Object#call # @@ -390,7 +393,9 @@ def __props current_obj = self begin - props.concat(JS.global[:Object].getOwnPropertyNames(current_obj).to_a) + current_props = JS.global[:Object].getOwnPropertyNames(current_obj).to_a + p current_props.inspect + props.concat(current_props) current_obj = JS.global[:Object].getPrototypeOf(current_obj) end while current_obj != nil end From fa03b2f381709b52a84498d282ab2b7e74823598 Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 7 Feb 2024 21:51:54 +0900 Subject: [PATCH 12/18] JS methods autocompletion! --- packages/gems/js/lib/js.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index cbe75f48f5..45782e0d21 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -160,14 +160,13 @@ def new(*args) # JS.eval("return [1, 2, 3]").to_a.map(&:to_i) # => [1, 2, 3] # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ... def to_a(convertTypes: true) - return self if self.is_a? Array as_array = JS.global[:Array].from(self) - Array.new(as_array[:length].to_i) { + Array.new(as_array[:length].to_i) { item = as_array[_1] if convertTypes and item.respond_to?(:to_rb) - return item.to_rb + item.to_rb else - return item + item end } end @@ -358,8 +357,8 @@ def methods(regular=true) # Filter the properties to get only methods (functions) #js_methods = props.to_a.select { |prop| self[prop.to_sym].typeof === 'function' }.map { _1.to_sym } - js_methods = props.sort.uniq.filter do |e| - self.JS[e].typeof?(:function) + js_methods = props.sort.uniq.filter do |prop| + self[prop].typeof?(:function) end js_methods + super end @@ -394,10 +393,10 @@ def __props begin current_props = JS.global[:Object].getOwnPropertyNames(current_obj).to_a - p current_props.inspect props.concat(current_props) current_obj = JS.global[:Object].getPrototypeOf(current_obj) end while current_obj != nil + return props.map(&:to_sym) end end From e1bf009bf6be9553cf2866074b02d1716a188036 Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 8 Feb 2024 17:35:03 +0900 Subject: [PATCH 13/18] also show attributes in the code completion --- packages/gems/js/lib/js.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 45782e0d21..4c3f549df4 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -356,9 +356,9 @@ def methods(regular=true) props = __props # Filter the properties to get only methods (functions) - #js_methods = props.to_a.select { |prop| self[prop.to_sym].typeof === 'function' }.map { _1.to_sym } js_methods = props.sort.uniq.filter do |prop| - self[prop].typeof?(:function) +# self[prop].typeof?(:function) + true end js_methods + super end From b7c3672780d3886521f974def51bac67d7b735ca Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 8 Feb 2024 21:25:26 +0900 Subject: [PATCH 14/18] Fix JS error not being propagated and add returns to method_missing. Fix respond_to_missing --- packages/gems/js/lib/js.rb | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 4c3f549df4..f2b392d0c9 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -127,9 +127,9 @@ def self.__call_async_method(recv, method_name, future, *args) def self.__async(future, &block) Fiber .new do - future.resolve block.call + future.call(:resolve, block.call) rescue => e - future.reject JS::Object.wrap(e) + future.call(:reject, JS::Object.wrap(e)) end .transfer end @@ -280,31 +280,31 @@ def method_missing(sym, *args, &block) # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. if self[sym]&.typeof?(:function) - self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True + return self.call(sym, *args, &block) == JS::True else - self[sym] == JS::True + return self[sym] == JS::True end elsif self[sym]&.typeof?(:function) # Todo: What do we do when we want to copy functions around? begin result = self.call(sym, *args, &block) if result.typeof?(:boolean) # fixes if searchParams.has("locations") - result == JS::True + return result == JS::True else - result + return result end rescue - self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] + return self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] end elsif sym_str.end_with?("=") if args[0].respond_to?(:to_js) - self[sym] = args[0].to_js + return self[sym] = args[0].to_js else - self[sym] = args[0] + return self[sym] = args[0] end elsif self[sym]&.typeof?(:undefined) == false and self[sym].respond_to?(:to_rb) - self[sym].to_rb + return self[sym].to_rb else - super + return super end end @@ -313,9 +313,10 @@ def method_missing(sym, *args, &block) # See JS::Object#method_missing for details. def respond_to_missing?(sym, include_private) return true if super + return false if self.typeof === "undefined" # Avoid target is undefined error sym_str = sym.to_s sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") - self[sym] != JS::Undefined and self[sym] != JS::Null + self[sym].typeof != "undefined" end # Await a JavaScript Promise like `await` in JavaScript. From 770ecb01fedad11b6b94a48e86c2fad5b67c9752 Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 8 Feb 2024 21:41:22 +0900 Subject: [PATCH 15/18] refactor method_missing --- packages/gems/js/lib/js.rb | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index f2b392d0c9..fe38fab2b5 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -281,10 +281,20 @@ def method_missing(sym, *args, &block) # and the return value is converted to a Ruby boolean value automatically. if self[sym]&.typeof?(:function) return self.call(sym, *args, &block) == JS::True - else - return self[sym] == JS::True end - elsif self[sym]&.typeof?(:function) # Todo: What do we do when we want to copy functions around? + + return self[sym] == JS::True + end + + if sym_str.end_with?("=") + if args[0].respond_to?(:to_js) + return self[sym] = args[0].to_js + end + + return self[sym] = args[0] + end + + if self[sym]&.typeof?(:function) # Todo: What do we do when we want to copy functions around? begin result = self.call(sym, *args, &block) if result.typeof?(:boolean) # fixes if searchParams.has("locations") @@ -295,17 +305,13 @@ def method_missing(sym, *args, &block) rescue return self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] end - elsif sym_str.end_with?("=") - if args[0].respond_to?(:to_js) - return self[sym] = args[0].to_js - else - return self[sym] = args[0] - end - elsif self[sym]&.typeof?(:undefined) == false and self[sym].respond_to?(:to_rb) + end + + if self[sym]&.typeof?(:undefined) == false and self[sym].respond_to?(:to_rb) return self[sym].to_rb - else - return super end + + return super end # Check if a JavaScript method exists From f0a38c4a0ddea1e7875a847018e7be1edaef143e Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 8 Feb 2024 23:05:28 +0900 Subject: [PATCH 16/18] handle methods missing on JS::Null --- packages/gems/js/lib/js.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index fe38fab2b5..4a1da02fcd 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -274,6 +274,7 @@ def undefined? # * If the method name is already defined as a Ruby method under JS::Object # * If the JavaScript method name ends with a question mark (?) def method_missing(sym, *args, &block) + return super if self === JS::Null sym_str = sym.to_s sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") if sym_str.end_with?("?") @@ -319,6 +320,7 @@ def method_missing(sym, *args, &block) # See JS::Object#method_missing for details. def respond_to_missing?(sym, include_private) return true if super + return false if self === JS::Null return false if self.typeof === "undefined" # Avoid target is undefined error sym_str = sym.to_s sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") @@ -403,7 +405,7 @@ def __props props.concat(current_props) current_obj = JS.global[:Object].getPrototypeOf(current_obj) end while current_obj != nil - return props.map(&:to_sym) + return props.compact.map(&:to_sym) end end From 79c7691a4f2593ed9940e1bfdb275c3d7846db9d Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 8 Feb 2024 23:36:00 +0900 Subject: [PATCH 17/18] Also convert the method results to ruby objects ... Otherwise users will run into hard to debug bugs (js string include? etc) --- packages/gems/js/lib/js.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 4a1da02fcd..7e95c3ee91 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -301,6 +301,7 @@ def method_missing(sym, *args, &block) if result.typeof?(:boolean) # fixes if searchParams.has("locations") return result == JS::True else + return result.to_rb if result.respond_to?(:to_rb) return result end rescue From 59bd4b8e5e3c5826aedb2697bd7b0cf653e9c16a Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 29 Feb 2024 18:09:05 +0900 Subject: [PATCH 18/18] run formatter --- packages/gems/js/lib/js.rb | 67 +++++++++++++++----------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 7e95c3ee91..65ba4484ba 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -71,9 +71,8 @@ module JS True = JS.eval("return true;") False = JS.eval("return false;") - # JS.try_convert_to_rb(obj) -> Ruby Object or JS::Object - # + # # Try to convert the given object to a Ruby Datatype using to_rb # method. Returns the parameter as JS::Object if the object cannot be converted. def try_convert_to_rb(obj) @@ -161,14 +160,10 @@ def new(*args) # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ... def to_a(convertTypes: true) as_array = JS.global[:Array].from(self) - Array.new(as_array[:length].to_i) { + Array.new(as_array[:length].to_i) do item = as_array[_1] - if convertTypes and item.respond_to?(:to_rb) - item.to_rb - else - item - end - } + convertTypes and item.respond_to?(:to_rb) ? item.to_rb : item + end end # Try to convert JS Objects into Ruby Objects @@ -193,21 +188,15 @@ def to_rb #if self.call(:instanceof, JS.global[:Date]) and not JS.global.isNaN(self) # self.toISOString() #elsif JS.global[:Array].isArray(self) - if self.isJSArray - self.to_a - else - self - end + self.isJSArray ? self.to_a : self else self end #return self.static_to_rb(self) - #case self[sym] - - # TODO convert js to numbers integers to + # TODO convert js to numbers integers to # and change this from the method_missing # make sure to run it in to _a @@ -225,7 +214,7 @@ def ==(other) elsif other.equal? nil return orig_eq(JS::Null) || orig_eq(JS::Undefined) end - + orig_eq(other) end @@ -235,16 +224,11 @@ def isJSArray() def self.static_to_rb(object) return nil if self[sym] == JS::Null - end def each(&block) if block_given? - if self.isJSArray - self.to_a.each(&block) - else - Array.new(__props).each(&block) - end + self.isJSArray ? self.to_a.each(&block) : Array.new(__props).each(&block) else to_enum(:each) end @@ -276,25 +260,24 @@ def undefined? def method_missing(sym, *args, &block) return super if self === JS::Null sym_str = sym.to_s - sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") + sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or + sym_str.end_with?("=") if sym_str.end_with?("?") # When a JS method is called with a ? suffix, it is treated as a predicate method, # and the return value is converted to a Ruby boolean value automatically. if self[sym]&.typeof?(:function) return self.call(sym, *args, &block) == JS::True end - + return self[sym] == JS::True end - if sym_str.end_with?("=") - if args[0].respond_to?(:to_js) - return self[sym] = args[0].to_js - end - + if sym_str.end_with?("=") + return self[sym] = args[0].to_js if args[0].respond_to?(:to_js) + return self[sym] = args[0] end - + if self[sym]&.typeof?(:function) # Todo: What do we do when we want to copy functions around? begin result = self.call(sym, *args, &block) @@ -304,7 +287,7 @@ def method_missing(sym, *args, &block) return result.to_rb if result.respond_to?(:to_rb) return result end - rescue + rescue StandardError return self[sym] # TODO: this is necessary in cases like JS.global[:URLSearchParams] end end @@ -312,7 +295,7 @@ def method_missing(sym, *args, &block) if self[sym]&.typeof?(:undefined) == false and self[sym].respond_to?(:to_rb) return self[sym].to_rb end - + return super end @@ -324,7 +307,8 @@ def respond_to_missing?(sym, include_private) return false if self === JS::Null return false if self.typeof === "undefined" # Avoid target is undefined error sym_str = sym.to_s - sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or sym_str.end_with?("=") + sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") or + sym_str.end_with?("=") self[sym].typeof != "undefined" end @@ -360,16 +344,17 @@ def await end # List the methods the object has with the ones in JS - def methods(regular=true) - # Get all properties of the document object, including inherited ones - + def methods(regular = true) + # Get all properties of the document object, including inherited ones + props = __props # Filter the properties to get only methods (functions) - js_methods = props.sort.uniq.filter do |prop| -# self[prop].typeof?(:function) + js_methods = + props.sort.uniq.filter do |prop| + # self[prop].typeof?(:function) true - end + end js_methods + super end