Skip to content

Instantly share code, notes, and snippets.

@kuahyeow
Last active July 22, 2017 10:01
Show Gist options
  • Save kuahyeow/4de0b7594f8a48e7cba6dab0da017fde to your computer and use it in GitHub Desktop.
Save kuahyeow/4de0b7594f8a48e7cba6dab0da017fde to your computer and use it in GitHub Desktop.
De-RJS : transpile RJS to jQuery
require_relative 'rewrite_rjs'
require_relative 'jquery_hack_generator'
filenames = ARGV
filenames.each do |filename|
source = File.read filename
erbified_source = rewrite(source)
puts erbified_source if ENV['DEBUG']
generator = JqueryHackScriptGenerator.new(nil) {page| eval(erbified_source)}
File.open(filename, "w"){|f| f << generator.to_s}
end
class Erbify < Parser::Rewriter
def on_send(node)
receiver_node, method_name, *arg_nodes = *node
if receiver_node.to_a.last == :page
case method_name
when :replace_html
# page.replace_html(...)
rewrite_replace_html(receiver_node, method_name, *arg_nodes)
when :insert_html
# page.insert_html(...)
rewrite_insert_html(receiver_node, method_name, *arg_nodes)
else
# All others such as :
# page.alert
# page.hide
# page.redirect_to
# page.reload
# page.replace
# page.select
# page.show
# page.visual_effect
rewrite_all_args(receiver_node, method_name, *arg_nodes)
end
end
# page[:html_id]
rewrite_square_bracket(receiver_node) if receiver_node.to_a.first.to_a.last == :page && receiver_node.to_a[1] == :[]
# page[:html_id].replace_html or page[:html_id].replace
rewrite_square_replace(receiver_node, method_name, *arg_nodes) if receiver_node.to_a.first.to_a.last == :page && receiver_node.to_a[1] == :[] &&
[:replace, :replace_html].include?(method_name)
end
def rewrite_all_args(receiver_node, method_name, *arg_nodes)
arg_nodes.each {|arg_node| rewrite_to_erb_unless_static(arg_node) }
end
# id, *options_for_render
def rewrite_replace_html(receiver_node, method_name, *arg_nodes)
rewrite_to_erb_unless_static(arg_nodes.shift)
rewrite_options_for_render(arg_nodes)
end
# position, id, *options_for_render
def rewrite_insert_html(receiver_node, method_name, *arg_nodes)
rewrite_to_erb_unless_static(arg_nodes.shift)
rewrite_to_erb_unless_static(arg_nodes.shift)
rewrite_options_for_render(arg_nodes)
end
# e.g. page["sgfg"] or page["wat_#{@id}"]
def rewrite_square_bracket(node)
receiver_node, method_name, *arg_nodes = *node
rewrite_to_erb_unless_static(arg_nodes.shift)
end
# *options_for_render
def rewrite_square_replace(receiver_node, method_name, *arg_nodes)
rewrite_options_for_render(arg_nodes)
end
protected
def rewrite_to_erb_unless_static(id_arg)
return if [:str, :sym].include?(id_arg.type)
insert_before id_arg.loc.expression, "%q{<%= "
insert_after id_arg.loc.expression, " %>}"
end
def rewrite_options_for_render(arg_nodes)
insert_before arg_nodes.first.loc.expression, "%q{<%= escape_javascript(render("
insert_after arg_nodes.last.loc.expression, ")) %>}"
end
end
unless defined? JQUERY_VAR
JQUERY_VAR = 'jQuery'
end
class JqueryHackScriptGenerator #:nodoc:
class OutputBuffer < Array
def encoding
Encoding::UTF_8
end
end
def initialize(context, &block) #:nodoc:
@context, @lines = context, OutputBuffer.new
include_helpers_from_context
#@context.with_output_buffer(@lines) do
#@context.instance_exec(self, &block)
#end
self.instance_exec(&block)
end
private
def include_helpers_from_context
#extend @context.helpers if @context.respond_to?(:helpers) && @context.helpers
extend GeneratorMethods
end
# JavaScriptGenerator generates blocks of JavaScript code that allow you
# to change the content and presentation of multiple DOM elements. Use
# this in your Ajax response bodies, either in a <tt>\<script></tt> tag
# or as plain JavaScript sent with a Content-type of "text/javascript".
#
# Create new instances with PrototypeHelper#update_page or with
# ActionController::Base#render, then call +insert_html+, +replace_html+,
# +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in
# methods on the yielded generator in any order you like to modify the
# content and appearance of the current page.
#
# Example:
#
# # Generates:
# # new Element.insert("list", { bottom: "<li>Some item</li>" });
# # new Effect.Highlight("list");
# # ["status-indicator", "cancel-link"].each(Element.hide);
# update_page do |page|
# page.insert_html :bottom, 'list', "<li>#{@item.name}</li>"
# page.visual_effect :highlight, 'list'
# page.hide 'status-indicator', 'cancel-link'
# end
#
#
# Helper methods can be used in conjunction with JavaScriptGenerator.
# When a helper method is called inside an update block on the +page+
# object, that method will also have access to a +page+ object.
#
# Example:
#
# module ApplicationHelper
# def update_time
# page.replace_html 'time', Time.now.to_s(:db)
# page.visual_effect :highlight, 'time'
# end
# end
#
# # Controller action
# def poll
# render(:update) { |page| page.update_time }
# end
#
# Calls to JavaScriptGenerator not matching a helper method below
# generate a proxy to the JavaScript Class named by the method called.
#
# Examples:
#
# # Generates:
# # Foo.init();
# update_page do |page|
# page.foo.init
# end
#
# # Generates:
# # Event.observe('one', 'click', function () {
# # $('two').show();
# # });
# update_page do |page|
# page.event.observe('one', 'click') do |p|
# p[:two].show
# end
# end
#
# You can also use PrototypeHelper#update_page_tag instead of
# PrototypeHelper#update_page to wrap the generated JavaScript in a
# <tt>\<script></tt> tag.
module GeneratorMethods
def to_s #:nodoc:
(@lines * $/).tap do |javascript|
if ActionView::Base.debug_rjs
source = javascript.dup
javascript.replace "try {\n#{source}\n} catch (e) "
javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
end
end
end
def jquery_id(id) #:nodoc:
id.sub(/<%=.*%>/,'').to_s.count('#.*,>+~:[/ ') == 0 ? "##{id}" : id
end
def jquery_ids(ids) #:nodoc:
Array(ids).map{|id| jquery_id(id)}.join(',')
end
# Returns a element reference by finding it through +id+ in the DOM. This element can then be
# used for further method calls. Examples:
#
# page['blank_slate'] # => $('blank_slate');
# page['blank_slate'].show # => $('blank_slate').show();
# page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
#
# You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
# the correct id:
#
# page[@post] # => $('post_45')
# page[Post.new] # => $('new_post')
def [](id)
case id
when String, Symbol, NilClass
JavaScriptElementProxy.new(self, id)
else
JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
end
end
# Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
# expression as an argument to another JavaScriptGenerator method.
def literal(code)
::ActiveSupport::JSON::Variable.new(code.to_s)
end
# Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
# used for further method calls. Examples:
#
# page.select('p') # => $$('p');
# page.select('p.welcome b').first # => $$('p.welcome b').first();
# page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
#
# You can also use prototype enumerations with the collection. Observe:
#
# # Generates: $$('#items li').each(function(value) { value.hide(); });
# page.select('#items li').each do |value|
# value.hide
# end
#
# Though you can call the block param anything you want, they are always rendered in the
# javascript as 'value, index.' Other enumerations, like collect() return the last statement:
#
# # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
# page.select('#items li').collect('hidden') do |item|
# item.hide
# end
#
def select(pattern)
JavaScriptElementCollectionProxy.new(self, pattern)
end
# Inserts HTML at the specified +position+ relative to the DOM element
# identified by the given +id+.
#
# +position+ may be one of:
#
# <tt>:top</tt>:: HTML is inserted inside the element, before the
# element's existing content.
# <tt>:bottom</tt>:: HTML is inserted inside the element, after the
# element's existing content.
# <tt>:before</tt>:: HTML is inserted immediately preceding the element.
# <tt>:after</tt>:: HTML is inserted immediately following the element.
#
# +options_for_render+ may be either a string of HTML to insert, or a hash
# of options to be passed to ActionView::Base#render. For example:
#
# # Insert the rendered 'navigation' partial just before the DOM
# # element with ID 'content'.
# # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
# page.insert_html :before, 'content', :partial => 'navigation'
#
# # Add a list item to the bottom of the <ul> with ID 'list'.
# # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
# page.insert_html :bottom, 'list', '<li>Last item</li>'
#
def insert_html(position, id, *options_for_render)
insertion = position.to_s.downcase
insertion = 'append' if insertion == 'bottom'
insertion = 'prepend' if insertion == 'top'
call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").#{insertion}", render(*options_for_render)
# content = javascript_object_for(render(*options_for_render))
# record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
end
# Replaces the inner HTML of the DOM element with the given +id+.
#
# +options_for_render+ may be either a string of HTML to insert, or a hash
# of options to be passed to ActionView::Base#render. For example:
#
# # Replace the HTML of the DOM element having ID 'person-45' with the
# # 'person' partial for the appropriate object.
# # Generates: Element.update("person-45", "-- Contents of 'person' partial --");
# page.replace_html 'person-45', :partial => 'person', :object => @person
#
def replace_html(id, *options_for_render)
call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").html", render(*options_for_render)
# call 'Element.update', id, render(*options_for_render)
end
# Replaces the "outer HTML" (i.e., the entire element, not just its
# contents) of the DOM element with the given +id+.
#
# +options_for_render+ may be either a string of HTML to insert, or a hash
# of options to be passed to ActionView::Base#render. For example:
#
# # Replace the DOM element having ID 'person-45' with the
# # 'person' partial for the appropriate object.
# page.replace 'person-45', :partial => 'person', :object => @person
#
# This allows the same partial that is used for the +insert_html+ to
# be also used for the input to +replace+ without resorting to
# the use of wrapper elements.
#
# Examples:
#
# <div id="people">
# <%= render :partial => 'person', :collection => @people %>
# </div>
#
# # Insert a new person
# #
# # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
# page.insert_html :bottom, :partial => 'person', :object => @person
#
# # Replace an existing person
#
# # Generates: Element.replace("person_45", "-- Contents of partial --");
# page.replace 'person_45', :partial => 'person', :object => @person
#
def replace(id, *options_for_render)
call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").replaceWith", render(*options_for_render)
#call 'Element.replace', id, render(*options_for_render)
end
# Removes the DOM elements with the given +ids+ from the page.
#
# Example:
#
# # Remove a few people
# # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
# page.remove 'person_23', 'person_9', 'person_2'
#
def remove(*ids)
call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").remove"
#loop_on_multiple_args 'Element.remove', ids
end
# Shows hidden DOM elements with the given +ids+.
#
# Example:
#
# # Show a few people
# # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
# page.show 'person_6', 'person_13', 'person_223'
#
def show(*ids)
call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").show"
#loop_on_multiple_args 'Element.show', ids
end
# Hides the visible DOM elements with the given +ids+.
#
# Example:
#
# # Hide a few people
# # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
# page.hide 'person_29', 'person_9', 'person_0'
#
def hide(*ids)
call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").hide"
#loop_on_multiple_args 'Element.hide', ids
end
# Toggles the visibility of the DOM elements with the given +ids+.
# Example:
#
# # Show a few people
# # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle);
# page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements
# page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements
#
def toggle(*ids)
call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").toggle"
#loop_on_multiple_args 'Element.toggle', ids
end
# Displays an alert dialog with the given +message+.
#
# Example:
#
# # Generates: alert('This message is from Rails!')
# page.alert('This message is from Rails!')
def alert(message)
call 'alert', message
end
# Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
#
# Examples:
#
# # Generates: window.location.href = "/mycontroller";
# page.redirect_to(:action => 'index')
#
# # Generates: window.location.href = "/account/signup";
# page.redirect_to(:controller => 'account', :action => 'signup')
def redirect_to(location)
#url = location.is_a?(String) ? location : @context.url_for(location)
url = location
record "window.location.href = #{url.inspect}"
end
# Reloads the browser's current +location+ using JavaScript
#
# Examples:
#
# # Generates: window.location.reload();
# page.reload
def reload
record 'window.location.reload()'
end
# Calls the JavaScript +function+, optionally with the given +arguments+.
#
# If a block is given, the block will be passed to a new JavaScriptGenerator;
# the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
# and passed as the called function's final argument.
#
# Examples:
#
# # Generates: Element.replace(my_element, "My content to replace with.")
# page.call 'Element.replace', 'my_element', "My content to replace with."
#
# # Generates: alert('My message!')
# page.call 'alert', 'My message!'
#
# # Generates:
# # my_method(function() {
# # $("one").show();
# # $("two").hide();
# # });
# page.call(:my_method) do |p|
# p[:one].show
# p[:two].hide
# end
def call(function, *arguments, &block)
record "#{function}(#{arguments_for_call(arguments, block)})"
end
# Assigns the JavaScript +variable+ the given +value+.
#
# Examples:
#
# # Generates: my_string = "This is mine!";
# page.assign 'my_string', 'This is mine!'
#
# # Generates: record_count = 33;
# page.assign 'record_count', 33
#
# # Generates: tabulated_total = 47
# page.assign 'tabulated_total', @total_from_cart
#
def assign(variable, value)
record "#{variable} = #{javascript_object_for(value)}"
end
# Writes raw JavaScript to the page.
#
# Example:
#
# page << "alert('JavaScript with Prototype.');"
def <<(javascript)
@lines << javascript
end
# Executes the content of the block after a delay of +seconds+. Example:
#
# # Generates:
# # setTimeout(function() {
# # ;
# # new Effect.Fade("notice",{});
# # }, 20000);
# page.delay(20) do
# page.visual_effect :fade, 'notice'
# end
def delay(seconds = 1)
record "setTimeout(function() {\n\n"
yield
record "}, #{(seconds * 1000).to_i})"
end
def arguments_for_call(arguments, block = nil)
arguments << block_to_function(block) if block
arguments.map { |argument| javascript_object_for(argument) }.join ', '
end
private
def loop_on_multiple_args(method, ids)
record(ids.size>1 ?
"#{javascript_object_for(ids)}.each(#{method})" :
"#{method}(#{javascript_object_for(ids.first)})")
end
def page
self
end
def record(line)
line = "#{line.to_s.chomp.gsub(/\;\z/, '')};".html_safe
self << line
line
end
def render(*options)
with_formats(:html) do
case option = options.first
when Hash
@context.render(*options)
else
option.to_s
end
end
end
def with_formats(*args)
return yield unless @context
lookup = @context.lookup_context
begin
old_formats, lookup.formats = lookup.formats, args
yield
ensure
lookup.formats = old_formats
end
end
def javascript_object_for(object)
return %Q{"#{object}"} if object.is_a?(String)
::ActiveSupport::JSON.encode(object)
end
def block_to_function(block)
generator = self.class.new(@context, &block)
literal("function() { #{generator.to_s} }")
end
def method_missing(method, *arguments)
ActionView::Helpers::JavaScriptProxy.new(self, method.to_s.camelize)
end
end
end
class JavaScriptElementProxy < ActionView::Helpers::JavaScriptProxy #:nodoc:
def initialize(generator, id)
id = id.sub(/<%=.*%>/,'').to_s.count('#.*,>+~:[/ ') == 0 ? "##{id}" : id
@id = id
super(generator, %Q{#{::JQUERY_VAR}("#{id}")}.html_safe)
end
# Allows access of element attributes through +attribute+. Examples:
#
# page['foo']['style'] # => $('foo').style;
# page['foo']['style']['color'] # => $('blank_slate').style.color;
# page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
# page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
def [](attribute)
append_to_function_chain!(attribute)
self
end
def []=(variable, value)
assign(variable, value)
end
def replace_html(*options_for_render)
call 'html', @generator.send(:render, *options_for_render)
end
def replace(*options_for_render)
call 'replaceWith', @generator.send(:render, *options_for_render)
end
def reload(options_for_replace = {})
replace(options_for_replace.merge({ :partial => @id.to_s.sub(/^#/,'') }))
end
def value()
call 'val()'
end
def value=(value)
call 'val', value
end
end
@kuahyeow
Copy link
Author

jquery_hack_generator from https://github.com/amatsuda/jquery-rjs (MIT)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment