module Ruby2JS
Kinda like Object.assign, except it handles properties
Note: Object.defineProperties, Object.getOwnPropertyNames, etc. technically
were not part of ES5, but were implemented by IE prior to ES6, and are the only way to implement getters and setters.
Manage a list of methods to be included or excluded. This allows fine grained control over filters.
Note care is taken to run all the filters first before camelCasing. This ensures that Ruby methods like each_pair can be mapped to JavaScript before camelcasing.
Jquery functions are either invoked using jQuery() or, more commonly, $(). The former presents no problem, the latter is not legal Ruby.
Accordingly, the first accomodation this filter provides is to map $$ to $. This works find for $$.ajax and the like, but less so for direct calls as $$(this) is also a syntax error. $$.(this) and $$[this] will work but are a bit clumsy.
So as a second accomodation, the rarely used binary one’s complement unary operator (namely, ~) is usurped, and the AST is rewritten to provide the effect of this operator being of a higher precedence than method calls. Passing multiple parameters can be accomplished by using array index syntax (e.g., ~[‘a’, self])
As a part of this rewriting, calls to getters and setters are rewritten to match jQuery’s convention for getters and setters:
http://learn.jquery.com/using-jquery-core/working-with-selections/
Selected DOM properties (namely checked, disabled, readOnly, and required) can also use getter and setter syntax. Additionally, readOnly may be spelled ‘readonly’.
Of course, using jQuery’s style of getter and setter calls is supported, and indeed is convenient when using method chaining.
Additionally, the tilde AST rewriting can be avoided by using consecutive tildes (~~ is a common Ruby idiom for Math.floor, ~~~ will return the binary one’s complement.); and the getter and setter AST rewriting can be avoided by the use of parenthesis, e.g. (~this).text.
Finally, the fourth parameter of $.post defaults to :json, allowing Ruby block syntax to be used for the success function.
Some examples of before/after conversions:
~this.val $(this).val() ~"button.continue".html = "Next Step..." $("button.continue").html("Next Step...") ~"button".readonly = false $("button").prop("readOnly", false) $$.ajax( url: "/api/getWeather", data: {zipcode: 97201}, success: proc do |data| `"#weather-temp".html = "<strong>#{data}</strong> degrees" end ) $.ajax({ url: "/api/getWeather", data: {zipcode: 97201}, success: function(data) { $("#weather-temp").html("<strong>" + data + "</strong> degrees"); } })
Convert Wunderbar syntax to JSX
Experimental secure random support
TODO: This feature is deprecated.
convert a JSX expression into wunderbar statements
Once the syntax is converted to pure Ruby statements, it can then be converted into either React or Vue rendering instructions.
Instances of this class keep track of both classes and modules that we have seen before, as well as the methods and properties that are defined in each.
Use cases this enables:
* detection of "open" classes and modules, i.e., redefining a class or module that was previously declared in order to add or modify methods or properties. * knowing when to prefix method or property access with `this.` and when to add `.bind(this)` for methods and properties that were defined outside of this class.
Public Class Methods
# File lib/ruby2js/execjs.rb, line 7 def self.compile(source, options={}) ExecJS.compile(convert(source, options).to_s) end
TODO: this method has gotten long and unwieldy!
# File lib/ruby2js.rb, line 219 def self.convert(source, options={}) Filter.autoregister unless RUBY_ENGINE == 'opal' options = options.dup if Proc === source file,line = source.source_location source = IO.read(file) ast, comments = parse(source) comments = Parser::Source::Comment.associate(ast, comments) if ast ast = find_block( ast, line ) options[:file] ||= file elsif Parser::AST::Node === source ast, comments = source, {} source = ast.loc.expression.source_buffer.source else ast, comments = parse( source, options[:file] ) comments = ast ? Parser::Source::Comment.associate(ast, comments) : {} end # check if magic comment is present first_comment = comments.values.first&.map(&:text)&.first if first_comment if first_comment.include?(" ruby2js: preset") options[:preset] = true if first_comment.include?("filters: ") options[:filters] = first_comment.match(%r(filters:\s*?([^\s]+)\s?.*$))[1].split(",").map(&:to_sym) end if first_comment.include?("eslevel: ") options[:eslevel] = first_comment.match(%r(eslevel:\s*?([^\s]+)\s?.*$))[1].to_i end if first_comment.include?("disable_filters: ") options[:disable_filters] = first_comment.match(%r(disable_filters:\s*?([^\s]+)\s?.*$))[1].split(",").map(&:to_sym) end end disable_autoimports = first_comment.include?(" autoimports: false") disable_autoexports = first_comment.include?(" autoexports: false") end unless RUBY_ENGINE == 'opal' unless options.key?(:config_file) || !File.exist?("config/ruby2js.rb") options[:config_file] ||= "config/ruby2js.rb" end if options[:config_file] options = ConfigurationDSL.load_from_file(options[:config_file], options).to_h end end if options[:preset] options[:eslevel] ||= @@eslevel_preset_default options[:filters] = Filter::PRESET_FILTERS + Array(options[:filters]).uniq if options[:disable_filters] options[:filters] -= options[:disable_filters] end options[:comparison] ||= :identity options[:underscored_private] = true unless options[:underscored_private] == false end options[:eslevel] ||= @@eslevel_default options[:strict] = @@strict_default if options[:strict] == nil options[:module] ||= @@module_default || :esm namespace = Namespace.new filters = Filter.require_filters(options[:filters] || Filter::DEFAULTS) unless filters.empty? filter_options = options.merge({ filters: filters }) filters.dup.each do |filter| filters = filter.reorder(filters) if filter.respond_to? :reorder end filter = Filter::Processor filters.reverse.each do |mod| filter = Class.new(filter) {include mod} end filter = filter.new(comments) filter.disable_autoimports = disable_autoimports filter.disable_autoexports = disable_autoexports filter.options = filter_options filter.namespace = namespace ast = filter.process(ast) unless filter.prepend_list.empty? prepend = filter.prepend_list.sort_by {|ast| ast.type == :import ? 0 : 1} prepend.reject! {|ast| ast.type == :import} if filter.disable_autoimports ast = Parser::AST::Node.new(:begin, [*prepend, ast]) end end ruby2js = Ruby2JS::Converter.new(ast, comments) ruby2js.binding = options[:binding] ruby2js.ivars = options[:ivars] ruby2js.eslevel = options[:eslevel] ruby2js.strict = options[:strict] ruby2js.comparison = options[:comparison] || :equality ruby2js.or = options[:or] || :logical ruby2js.module_type = options[:module] || :esm ruby2js.underscored_private = (options[:eslevel] < 2022) || options[:underscored_private] ruby2js.namespace = namespace if ruby2js.binding and not ruby2js.ivars ruby2js.ivars = ruby2js.binding.eval \ 'Hash[instance_variables.map {|var| [var, instance_variable_get(var)]}]' elsif options[:scope] and not ruby2js.ivars scope = options.delete(:scope) ruby2js.ivars = Hash[scope.instance_variables.map {|var| [var, scope.instance_variable_get(var)]}] end ruby2js.width = options[:width] if options[:width] ruby2js.enable_vertical_whitespace if source.include? "\n" ruby2js.convert ruby2js.timestamp options[:file] ruby2js.file_name = options[:file] || ast&.loc&.expression&.source_buffer&.name || '' ruby2js end
# File lib/ruby2js.rb, line 30 def self.eslevel_default @@eslevel_default end
# File lib/ruby2js.rb, line 34 def self.eslevel_default=(level) @@eslevel_default = level end
# File lib/ruby2js/execjs.rb, line 11 def self.eval(source, options={}) ExecJS.eval(convert(source, options.merge(strict: false)).to_s) end
# File lib/ruby2js/execjs.rb, line 15 def self.exec(source, options={}) ExecJS.exec(convert(source, options).to_s) end
# File lib/ruby2js.rb, line 360 def self.find_block(ast, line) if ast.type == :block and ast.loc.expression.line == line return ast.children.last end ast.children.each do |child| if Parser::AST::Node === child block = find_block child, line return block if block end end nil end
# File lib/ruby2js/jsx.rb, line 10 def self.jsx2_rb(string) JsxParser.new(string.chars.each).parse.join("\n") end
# File lib/ruby2js.rb, line 46 def self.module_default @@module_default end
# File lib/ruby2js.rb, line 50 def self.module_default=(module_type) @@module_default = module_type end
# File lib/ruby2js.rb, line 344 def self.parse(source, file=nil, line=1) buffer = Parser::Source::Buffer.new(file, line) buffer.source = source.encode('UTF-8') parser = Parser::CurrentRuby.new parser.diagnostics.all_errors_are_fatal = true parser.diagnostics.consumer = lambda {|diagnostic| nil} parser.builder.emit_file_line_as_literals = false parser.parse_with_comments(buffer) rescue Parser::SyntaxError => e split = source[0..e.diagnostic.location.begin_pos].split("\n") line, col = split.length, split.last.length message = "line #{line}, column #{col}: #{e.diagnostic.message}" message += "\n in file #{file}" if file raise Ruby2JS::SyntaxError.new(message, e.diagnostic) end
# File lib/ruby2js.rb, line 38 def self.strict_default @@strict_default end
# File lib/ruby2js.rb, line 42 def self.strict_default=(level) @@strict_default = level end