Re-added mixed-in helper methods for the JavascriptGenerator. Moved JavascriptGenerators methods to a module that is mixed in after the helpers are added. Also fixed that variables set in the enumeration methods like #collect are set correctly. Documentation added for the enumeration methods [Rick Olson].

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3814 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Rick Olson
2006-03-08 05:21:27 +00:00
parent 3c78d0b1eb
commit 043bee3338
4 changed files with 347 additions and 292 deletions

View File

@@ -1,5 +1,12 @@
*SVN*
* Re-added mixed-in helper methods for the JavascriptGenerator. Moved JavascriptGenerators methods to a module that is mixed in after the helpers are added. Also fixed that variables set in the enumeration methods like #collect are set correctly. Documentation added for the enumeration methods [Rick Olson]. Examples:
page.select('#items li').collect('items') do |element|
element.hide
end
# => var items = $$('#items li').collect(function(value, index) { return value.hide(); });
* Added plugin support for parameter parsers, which allows for better support for REST web services. By default, posts submitted with the application/xml content type is handled by creating a XmlSimple hash with the same name as the root element of the submitted xml. More handlers can easily be registered like this:
# Assign a new param parser to a new content type

View File

@@ -360,267 +360,287 @@ module ActionView
end
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 <script> 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:
#
# 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
#
# generates the following JavaScript:
#
# new Insertion.Bottom("list", "<li>Some item</li>");
# new Effect.Highlight("list");
# ["status-indicator", "cancel-link"].each(Element.hide);
#
# 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
#
# You can also use PrototypeHelper#update_page_tag instead of
# PrototypeHelper#update_page to wrap the generated JavaScript in a
# <script> tag.
class JavaScriptGenerator
# All the methods were moved to GeneratorMethods so that
# #include_helpers_from_context has nothing to overwrite.
class JavaScriptGenerator #:nodoc:
def initialize(context, &block) #:nodoc:
@context, @lines = context, []
# removed because those methods were overriding valid generator methods
# include_helpers_from_context
include_helpers_from_context
@context.instance_exec(self, &block)
end
def to_s #:nodoc:
@lines * $/
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();
def [](id)
JavaScriptElementProxy.new(self, id)
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();
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 preceeding 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'.
# insert_html :before, 'content', :partial => 'navigation'
#
# # Add a list item to the bottom of the <ul> with ID 'list'.
# insert_html :bottom, 'list', '<li>Last item</li>'
#
def insert_html(position, id, *options_for_render)
insertion = position.to_s.camelize
call "new Insertion.#{insertion}", id, render(*options_for_render)
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.
# replace_html 'person-45', :partial => 'person', :object => @person
#
def replace_html(id, *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.
# replace_html '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
# page.insert_html :bottom, :partial => 'person', :object => @person
#
# # Replace an existing person
# page.replace 'person_45', :partial => 'person', :object => @person
#
def replace(id, *options_for_render)
call 'Element.replace', id, render(*options_for_render)
end
# Removes the DOM elements with the given +ids+ from the page.
def remove(*ids)
record "#{javascript_object_for(ids)}.each(Element.remove)"
end
# Shows hidden DOM elements with the given +ids+.
def show(*ids)
call 'Element.show', *ids
end
# Hides the visible DOM elements with the given +ids+.
def hide(*ids)
call 'Element.hide', *ids
end
# Toggles the visibility of the DOM elements with the given +ids+.
def toggle(*ids)
call 'Element.toggle', *ids
end
# Displays an alert dialog with the given +message+.
def alert(message)
call 'alert', message
end
# Redirects the browser to the given +location+, in the same form as
# +url_for+.
def redirect_to(location)
assign 'window.location.href', @context.url_for(location)
end
# Calls the JavaScript +function+, optionally with the given
# +arguments+.
def call(function, *arguments)
record "#{function}(#{arguments_for_call(arguments)})"
end
# Assigns the JavaScript +variable+ the given +value+.
def assign(variable, value)
record "#{variable} = #{javascript_object_for(value)}"
end
# Writes raw JavaScript to the page.
def <<(javascript)
@lines << javascript
end
# Executes the content of the block after a delay of +seconds+. Example:
#
# 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
# Starts a script.aculo.us visual effect. See
# ActionView::Helpers::ScriptaculousHelper for more information.
def visual_effect(name, id = nil, options = {})
record @context.send(:visual_effect, name, id, options)
end
# Creates a script.aculo.us sortable element. Useful
# to recreate sortable elements after items get added
# or deleted.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def sortable(id, options = {})
record @context.send(:sortable_element_js, id, options)
end
# Creates a script.aculo.us draggable element.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def draggable(id, options = {})
record @context.send(:draggable_element_js, id, options)
end
# Creates a script.aculo.us drop receiving element.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def drop_receiving(id, options = {})
record @context.send(:drop_receiving_element_js, id, options)
end
private
private
def include_helpers_from_context
@context.extended_by.each do |mod|
extend mod unless mod.name =~ /^ActionView::Helpers/
end
extend GeneratorMethods
end
def page
self
end
def record(line)
returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do
self << line
# 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 <script> 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:
#
# 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
#
# generates the following JavaScript:
#
# new Insertion.Bottom("list", "<li>Some item</li>");
# new Effect.Highlight("list");
# ["status-indicator", "cancel-link"].each(Element.hide);
#
# 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
#
# You can also use PrototypeHelper#update_page_tag instead of
# PrototypeHelper#update_page to wrap the generated JavaScript in a
# <script> tag.
module GeneratorMethods
def to_s #:nodoc:
@lines * $/
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();
def [](id)
JavaScriptElementProxy.new(self, id)
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:
#
# page.select('#items li').each do |value|
# value.hide
# end
# # => $$('#items li').each(function(value) { value.hide(); });
#
# 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:
#
# page.select('#items li').collect('hidden') do |item|
# item.hide
# end
# # => var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
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 preceeding 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'.
# insert_html :before, 'content', :partial => 'navigation'
#
# # Add a list item to the bottom of the <ul> with ID 'list'.
# insert_html :bottom, 'list', '<li>Last item</li>'
#
def insert_html(position, id, *options_for_render)
insertion = position.to_s.camelize
call "new Insertion.#{insertion}", id, render(*options_for_render)
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.
# replace_html 'person-45', :partial => 'person', :object => @person
#
def replace_html(id, *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.
# replace_html '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
# page.insert_html :bottom, :partial => 'person', :object => @person
#
# # Replace an existing person
# page.replace 'person_45', :partial => 'person', :object => @person
#
def replace(id, *options_for_render)
call 'Element.replace', id, render(*options_for_render)
end
# Removes the DOM elements with the given +ids+ from the page.
def remove(*ids)
record "#{javascript_object_for(ids)}.each(Element.remove)"
end
# Shows hidden DOM elements with the given +ids+.
def show(*ids)
call 'Element.show', *ids
end
# Hides the visible DOM elements with the given +ids+.
def hide(*ids)
call 'Element.hide', *ids
end
# Toggles the visibility of the DOM elements with the given +ids+.
def toggle(*ids)
call 'Element.toggle', *ids
end
# Displays an alert dialog with the given +message+.
def alert(message)
call 'alert', message
end
# Redirects the browser to the given +location+, in the same form as
# +url_for+.
def redirect_to(location)
assign 'window.location.href', @context.url_for(location)
end
# Calls the JavaScript +function+, optionally with the given
# +arguments+.
def call(function, *arguments)
record "#{function}(#{arguments_for_call(arguments)})"
end
# Assigns the JavaScript +variable+ the given +value+.
def assign(variable, value)
record "#{variable} = #{javascript_object_for(value)}"
end
# Writes raw JavaScript to the page.
def <<(javascript)
@lines << javascript
end
# Executes the content of the block after a delay of +seconds+. Example:
#
# 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
# Starts a script.aculo.us visual effect. See
# ActionView::Helpers::ScriptaculousHelper for more information.
def visual_effect(name, id = nil, options = {})
record @context.send(:visual_effect, name, id, options)
end
# Creates a script.aculo.us sortable element. Useful
# to recreate sortable elements after items get added
# or deleted.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def sortable(id, options = {})
record @context.send(:sortable_element_js, id, options)
end
# Creates a script.aculo.us draggable element.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def draggable(id, options = {})
record @context.send(:draggable_element_js, id, options)
end
# Creates a script.aculo.us drop receiving element.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def drop_receiving(id, options = {})
record @context.send(:drop_receiving_element_js, id, options)
end
private
def page
self
end
def record(line)
returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do
self << line
end
end
def render(*options_for_render)
Hash === options_for_render.first ?
@context.render(*options_for_render) :
options_for_render.first.to_s
end
def javascript_object_for(object)
object.respond_to?(:to_json) ? object.to_json : object.inspect
end
def arguments_for_call(arguments)
arguments.map { |argument| javascript_object_for(argument) }.join ', '
end
end
def render(*options_for_render)
Hash === options_for_render.first ?
@context.render(*options_for_render) :
options_for_render.first.to_s
end
def javascript_object_for(object)
object.respond_to?(:to_json) ? object.to_json : object.inspect
end
def arguments_for_call(arguments)
arguments.map { |argument| javascript_object_for(argument) }.join ', '
end
end
@@ -769,17 +789,19 @@ module ActionView
class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :findAll, :select, :max, :min, :partition, :reject, :sortBy]
ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each]
attr_reader :generator
delegate :arguments_for_call, :to => :generator
def initialize(generator, pattern)
super(generator, @pattern = pattern)
end
def grep(variable, pattern, &block)
enumerable_method("grep(#{pattern.to_json}, function(value, index) {", variable, %w(value index), &block)
enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block
end
def inject(variable, memo, &block)
enumerable_method("inject(#{memo.to_json}, function(memo, value, index) {", variable, %w(memo value index), &block)
enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
end
def pluck(variable, property)
@@ -789,7 +811,7 @@ module ActionView
def zip(variable, *arguments, &block)
add_variable_assignment!(variable)
append_enumerable_function!("zip(#{arguments.collect { |a| a.to_json } * ', '}")
append_enumerable_function!("zip(#{arguments_for_call arguments}")
if block
function_chain[-1] += ", function(array) {"
yield ActiveSupport::JSON::Variable.new('array')
@@ -802,24 +824,36 @@ module ActionView
private
def method_missing(method, *arguments, &block)
ENUMERABLE_METHODS.include?(method) ? enumerate(method, ENUMERABLE_METHODS_WITH_RETURN.include?(method), &block) : super
if ENUMERABLE_METHODS.include?(method)
returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method)
variable = arguments.first if returnable
enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block)
else
super
end
end
def enumerate(enumerable, variable = nil, &block)
enumerable_method("#{enumerable}(function(value, index) {", variable, %w(value index), &block)
end
def enumerable_method(enumerable, variable, yield_params, &block)
add_variable_assignment!(variable) if variable
append_enumerable_function!(enumerable)
# Options
# * variable - name of the variable to set the result of the enumeration to
# * method_args - array of the javascript enumeration method args that occur before the function
# * yield_args - array of the javascript yield args
# * return - true if the enumeration should return the last statement
def enumerate(enumerable, options = {}, &block)
options[:method_args] ||= []
options[:yield_args] ||= []
yield_args = options[:yield_args] * ', '
method_args = arguments_for_call options[:method_args] # foo, bar, function
method_args << ', ' unless method_args.blank?
add_variable_assignment!(options[:variable]) if options[:variable]
append_enumerable_function!("#{enumerable}(#{method_args}function(#{yield_args}) {")
# only yield as many params as were passed in the block
yield *yield_params.collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1]
add_return_statement! if variable
yield *options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1]
add_return_statement! if options[:return]
@generator << '});'
end
def add_variable_assignment!(variable)
function_chain.push("#{variable} = #{function_chain.pop}")
function_chain.push("var #{variable} = #{function_chain.pop}")
end
def add_return_statement!

View File

@@ -9,6 +9,12 @@ module Fun
end
end
module NewRenderTestHelper
def rjs_helper_method_from_module
page.visual_effect :highlight
end
end
class NewRenderTestController < ActionController::Base
layout :determine_layout
@@ -178,11 +184,18 @@ class NewRenderTestController < ActionController::Base
render :action => "potential_conflicts"
end
helper NewRenderTestHelper
helper do
def rjs_helper_method(value)
page.visual_effect :highlight, value
end
end
def enum_rjs_test
render :update do |page|
page.select('.product').each do |value|
page.visual_effect :highlight
page.visual_effect :highlight, value
page.rjs_helper_method_from_module
page.rjs_helper_method(value)
page.sortable(value, :url => { :action => "order" })
page.draggable(value)
end

View File

@@ -148,8 +148,6 @@ class PrototypeHelperTest < Test::Unit::TestCase
end
end
ActionView::Helpers::JavaScriptCollectionProxy.send :public, :enumerate
class JavaScriptGeneratorTest < Test::Unit::TestCase
include BaseTest
@@ -284,6 +282,15 @@ Element.update("baz", "<p>This is a test</p>");
@generator.drop_receiving('blah', :url => { :action => "order" })
end
def test_collection_first_and_last
@generator.select('p.welcome b').first.hide()
@generator.select('p.welcome b').last.show()
assert_equal <<-EOS.strip, @generator.to_s
$$('p.welcome b').first().hide();
$$('p.welcome b').last().show();
EOS
end
def test_collection_proxy_with_each
@generator.select('p.welcome b').each do |value|
value.remove_class_name 'selected'
@@ -301,24 +308,18 @@ new Effect.Highlight(value,{});
EOS
end
def test_collection_proxy_on_enumerables_with_return_and_index
iterator = Proc.new { |value| @generator << '(value.className == "welcome")' }
iterator_with_index = Proc.new { |value, index| @generator.call 'alert', index ; @generator << '(value.className == "welcome")' }
ActionView::Helpers::JavaScriptCollectionProxy::ENUMERABLE_METHODS_WITH_RETURN.each do |enum|
@generator.select('p').enumerate(enum, 'a', &iterator)
@generator.select('p').enumerate(enum, 'b', &iterator_with_index)
assert_equal <<-EOS.strip, @generator.to_s
a = $$('p').#{enum}(function(value, index) {
return (value.className == "welcome");
def test_collection_proxy_on_collect
@generator.select('p').collect('a') { |para| para.show }
@generator.select('p').collect { |para| para.hide }
assert_equal <<-EOS.strip, @generator.to_s
var a = $$('p').collect(function(value, index) {
return value.show();
});
b = $$('p').#{enum}(function(value, index) {
alert(index);
return (value.className == "welcome");
$$('p').collect(function(value, index) {
return value.hide();
});
EOS
@generator = create_generator
end
EOS
@generator = create_generator
end
def test_collection_proxy_with_grep
@@ -331,10 +332,10 @@ return (value.className == "welcome");
end
assert_equal <<-EOS.strip, @generator.to_s
a = $$('p').grep(/^a/, function(value, index) {
var a = $$('p').grep(/^a/, function(value, index) {
return (value.className == "welcome");
});
b = $$('p').grep(/b$/, function(value, index) {
var b = $$('p').grep(/b$/, function(value, index) {
alert(value);
return (value.className == "welcome");
});
@@ -351,10 +352,10 @@ return (value.className == "welcome");
end
assert_equal <<-EOS.strip, @generator.to_s
a = $$('p').inject([], function(memo, value, index) {
var a = $$('p').inject([], function(memo, value, index) {
return (value.className == "welcome");
});
b = $$('p').inject(null, function(memo, value, index) {
var b = $$('p').inject(null, function(memo, value, index) {
alert(memo);
return (value.className == "welcome");
});
@@ -363,7 +364,7 @@ return (value.className == "welcome");
def test_collection_proxy_with_pluck
@generator.select('p').pluck('a', 'className')
assert_equal %(a = $$('p').pluck("className");), @generator.to_s
assert_equal %(var a = $$('p').pluck("className");), @generator.to_s
end
def test_collection_proxy_with_zip
@@ -373,8 +374,8 @@ return (value.className == "welcome");
end
assert_equal <<-EOS.strip, @generator.to_s
a = [1, 2, 3].zip([4, 5, 6], [7, 8, 9]);
b = [1, 2, 3].zip([4, 5, 6], [7, 8, 9], function(array) {
var a = [1, 2, 3].zip([4, 5, 6], [7, 8, 9]);
var b = [1, 2, 3].zip([4, 5, 6], [7, 8, 9], function(array) {
return array.reverse();
});
EOS