Simplified logic & added comments to main routes.rb generator, and removed compression
Kept compression of route list, but removed compression of route names. This compression was working by finding words > 5 chars, and encoding them in #{}, then decoding upon load in browser. Since the difference wasn't really great, and caused extra work for browser, I took it out.
This commit is contained in:
parent
0e4f9ab13b
commit
984317e4fc
|
@ -3,146 +3,168 @@ module JavascriptRoutes
|
||||||
JS = File.join(File.dirname(__FILE__), 'javascripts', 'routes.js')
|
JS = File.join(File.dirname(__FILE__), 'javascripts', 'routes.js')
|
||||||
JS_PACKED = File.join(File.dirname(__FILE__), 'javascripts', 'routes-min.js')
|
JS_PACKED = File.join(File.dirname(__FILE__), 'javascripts', 'routes-min.js')
|
||||||
JS_AJAX = File.join(File.dirname(__FILE__), 'javascripts', 'routes-ajax.js')
|
JS_AJAX = File.join(File.dirname(__FILE__), 'javascripts', 'routes-ajax.js')
|
||||||
|
|
||||||
FILENAME = File.join(RAILS_ROOT, 'public', 'javascripts', 'routes.js')
|
FILENAME = File.join(RAILS_ROOT, 'public', 'javascripts', 'routes.js')
|
||||||
FILENAME_AJAX = File.join(RAILS_ROOT, 'public', 'javascripts', 'routes-ajax.js')
|
FILENAME_AJAX = File.join(RAILS_ROOT, 'public', 'javascripts', 'routes-ajax.js')
|
||||||
|
|
||||||
|
|
||||||
|
# Generate...
|
||||||
|
#
|
||||||
|
# Options are:
|
||||||
|
# :filename => name of routes javascript file (default routes.js)
|
||||||
|
# :filename_ajax => name of routes ajax-extras (default routes-ajax.js)
|
||||||
|
#
|
||||||
|
# :lite => only generate functions, not the unnamed generational routes (i.e. from controller/action)
|
||||||
|
#
|
||||||
|
# :pack => use the packed version
|
||||||
|
#
|
||||||
|
# :routes - which routes (leave out for all)
|
||||||
|
# :named_routes - which named routes (leave out for all)
|
||||||
|
#
|
||||||
|
#
|
||||||
def self.generate(options = {})
|
def self.generate(options = {})
|
||||||
options.symbolize_keys!.reverse_merge!(:pack => true)
|
options.symbolize_keys!.reverse_merge!(:pack => true)
|
||||||
|
|
||||||
routes = options[:routes] || ActionController::Routing::Routes.routes.select{|r|
|
routes = options[:routes] || processable_routes
|
||||||
r.conditions[:method].nil? || r.conditions[:method] == :get
|
named_routes = options[:named_routes] || processable_named_routes
|
||||||
}
|
|
||||||
named_routes = options[:named_routes] || ActionController::Routing::Routes.named_routes.routes.select{|n,r|
|
|
||||||
r.conditions[:method].nil? || r.conditions[:method] == :get
|
|
||||||
}
|
|
||||||
filename = options[:filename] || FILENAME
|
filename = options[:filename] || FILENAME
|
||||||
filename_ajax = options[:filename_ajax] || FILENAME_AJAX
|
filename_ajax = options[:filename_ajax] || FILENAME_AJAX
|
||||||
|
|
||||||
#Will only create simple functions for named routes
|
# Create one function per route (simple lite version...)
|
||||||
if options[:lite]
|
if options[:lite]
|
||||||
|
generate_lite(named_routes, filename)
|
||||||
|
|
||||||
File.open(filename, 'w') do |file|
|
# Simulate Rails route generation logic -- not plain functions
|
||||||
routes_object = ' var r = "{'
|
# (includes lib/routes.js)
|
||||||
named_routes.each_with_index do |a,i|
|
|
||||||
n,r = *a
|
|
||||||
routes_object << "#{n}: function(#{r.segments.select{|s| s.respond_to?(:key) }.map(&:key).join(', ')}){ "
|
|
||||||
routes_object << 'return '
|
|
||||||
|
|
||||||
r.segments.each_with_index do |s,j|
|
|
||||||
if s.respond_to?(:key)
|
|
||||||
routes_object << "'" unless i==0 || r.segments[j-1].respond_to?(:key)
|
|
||||||
routes_object << "+#{s.key}"
|
|
||||||
else
|
|
||||||
routes_object << '+' if j > 0 && r.segments[j-1].respond_to?(:key)
|
|
||||||
routes_object << "'" if j == 0 || r.segments[j-1].respond_to?(:key)
|
|
||||||
routes_object << s.to_s unless j != 0 && j == r.segments.size-1 && s.to_s == '/'
|
|
||||||
routes_object << "'" if j == r.segments.size-1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
routes_object << ';'
|
|
||||||
routes_object << " }"
|
|
||||||
routes_object << ',' if i < named_routes.size-1
|
|
||||||
end
|
|
||||||
routes_object << "}\";\n"
|
|
||||||
|
|
||||||
#Find words with 5 or more characters that appear more than once
|
|
||||||
words = routes_object.scan(/[a-z_]{5,}/).group_by{|s| s }.inject([]){|r,a| r << a.first if a.last.size > 1; r }
|
|
||||||
#Replace words with placeholders
|
|
||||||
words.each_with_index{|w,i| routes_object.gsub!(w, "$#{i}") }
|
|
||||||
|
|
||||||
file << "var Routes = (function(){\n"
|
|
||||||
|
|
||||||
#Export words to JS
|
|
||||||
file << " var s = [" + words.map{|w| "'#{w}'" }.join(',') + "];\n"
|
|
||||||
file << routes_object
|
|
||||||
#Put the words back in (using JS) and eval the result
|
|
||||||
file << " return eval('('+r.replace(/\\$(\\d+)/g, function(m,i){ return s[i]; })+')');\n"
|
|
||||||
|
|
||||||
file << "})();"
|
|
||||||
end
|
|
||||||
|
|
||||||
#Will create all routes with generation logic (from lib/routes.js)
|
|
||||||
else
|
else
|
||||||
|
generate_full(named_routes, routes, filename, options)
|
||||||
File.open(filename, 'w') do |file|
|
|
||||||
file << File.read(options[:pack] ? JS_PACKED : JS)
|
|
||||||
|
|
||||||
file << "\n\n(function(){\n\n"#Don't pollute the global namespace
|
|
||||||
|
|
||||||
#This is ugly, but it works. It builds a JS array
|
# Add ajax extras
|
||||||
#with an object for each route. Most of the ugliness
|
|
||||||
#is to reduce the amount of space it takes up.
|
|
||||||
routes_array = ''
|
|
||||||
routes_array << 'var r = "['
|
|
||||||
routes.each_with_index do |r,i|
|
|
||||||
routes_array << '{'
|
|
||||||
|
|
||||||
#Append a name if this is a named route
|
|
||||||
named_route = named_routes.find{|name,route| route.equal?(r) }
|
|
||||||
routes_array << "n:'#{named_route.first}'," if named_route
|
|
||||||
|
|
||||||
#Append segments as a string with @ between. This will
|
|
||||||
#be split() using JS.
|
|
||||||
routes_array << "s:'"
|
|
||||||
routes_array << r.segments.map do |s|
|
|
||||||
if s.is_a?(ActionController::Routing::PathSegment)
|
|
||||||
'*' + s.to_s[1..-1] + (s.is_optional ? 't' : 'f')
|
|
||||||
else
|
|
||||||
s.to_s + (s.is_optional ? 't' : 'f')
|
|
||||||
end
|
|
||||||
end.join('@')
|
|
||||||
routes_array << "'"
|
|
||||||
|
|
||||||
#Append params object
|
|
||||||
routes_array << ',r:{'
|
|
||||||
routes_array << r.requirements.map do |k,v|
|
|
||||||
"#{k}:'#{v}'"
|
|
||||||
end.join(',')
|
|
||||||
routes_array << '}'
|
|
||||||
|
|
||||||
routes_array << '}'
|
|
||||||
routes_array << ',' unless i == routes.size-1
|
|
||||||
end
|
|
||||||
routes_array << "]\";\n"
|
|
||||||
|
|
||||||
#Find words that occur more than once and are more than 5 characters in length
|
|
||||||
words = routes_array.scan(/[a-z_]{5,}/).group_by{|s| s }.inject([]){|r,a| r << a.first if a.last.size > 1; r }
|
|
||||||
#Replace words with placeholders
|
|
||||||
words.each_with_index{|w,i| routes_array.gsub!(w, "$#{i}") }
|
|
||||||
|
|
||||||
#Export words to JS
|
|
||||||
file << " var s = [" + words.map{|w| "'#{w}'" }.join(',') + "];\n"
|
|
||||||
file << ' '+routes_array
|
|
||||||
#Put the words back in (using JS) and eval the result
|
|
||||||
file << " r = eval('('+r.replace(/\\$(\\d+)/g, function(m,i){ return s[i]; })+')');\n\n"
|
|
||||||
|
|
||||||
#Add routes
|
|
||||||
file << " for (var i = 0; i < r.length; i++) {\n"
|
|
||||||
file << " var s=[];\n"
|
|
||||||
file << " var segs=r[i].s.split('@');\n"
|
|
||||||
file << " for (var j = 0; j < segs.length; j++) {\n"
|
|
||||||
file << " s.push(Route.S(segs[j].slice(0, -1), segs[j].slice(-1) == 't'));\n"
|
|
||||||
file << " }\n"
|
|
||||||
file << " Routes.push(new Route(s, r[i].r, r[i].n));\n"
|
|
||||||
file << " }\n\n"
|
|
||||||
file << " Routes.extractNamed();\n\n"
|
|
||||||
|
|
||||||
file << "})();"
|
|
||||||
end
|
|
||||||
|
|
||||||
#Add ajax extras
|
|
||||||
File.open filename_ajax, 'w' do |f|
|
File.open filename_ajax, 'w' do |f|
|
||||||
f.write(File.read(JS_AJAX))
|
f.write(File.read(JS_AJAX))
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue => e
|
rescue => e
|
||||||
|
|
||||||
warn("\n\nCould not write routes.js: \"#{e.class}:#{e.message}\"\n\n")
|
warn("\n\nCould not write routes.js: \"#{e.class}:#{e.message}\"\n\n")
|
||||||
File.truncate(filename, 0) rescue nil
|
File.truncate(filename, 0) rescue nil
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def generate_lite(named_routes, filename)
|
||||||
|
route_functions = named_routes.map do |name, route|
|
||||||
|
processable_segments = route.segments.select{|s| processable_segment(s)}
|
||||||
|
|
||||||
|
# Generate the tokens that make up the single statement in this fn
|
||||||
|
tokens = processable_segments.inject([]) {|tokens, segment|
|
||||||
|
is_var = segment.respond_to?(:key)
|
||||||
|
prev_is_var = tokens.last.is_a?(Symbol)
|
||||||
|
|
||||||
|
value = (is_var ? segment.key : segment.to_s)
|
||||||
|
|
||||||
|
# Is the previous token ammendable?
|
||||||
|
require_new_token = (tokens.empty? || is_var || prev_is_var)
|
||||||
|
(require_new_token ? tokens : tokens.last) << value
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert strings to have quotes, and concatenate...
|
||||||
|
statement = tokens.map{|t| t.is_a?(Symbol) ? t : "\"#{t}\""}.join("+")
|
||||||
|
|
||||||
|
fn_params = processable_segments.select{|s|s.respond_to?(:key)}.map(&:key)
|
||||||
|
|
||||||
|
"#{name}_path: function(#{fn_params.join(', ')}) {return #{statement};}"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.open(filename, 'w') do |file|
|
||||||
|
file << "var Routes = (function(){\n"
|
||||||
|
file << " return {\n"
|
||||||
|
file << " " + route_functions.join(",\n ") + "\n"
|
||||||
|
file << " }\n"
|
||||||
|
file << "})();"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Generates all routes (named or unnamed) as an array of JSON objects
|
||||||
|
#
|
||||||
|
# The routes are encoded to reduce size:
|
||||||
|
# - each is an object with keys: 'n' for name, 's' for segments & 'r' for requirements
|
||||||
|
#
|
||||||
|
# Segments are further encode:
|
||||||
|
# - path segments indicated by first char being set to '*'
|
||||||
|
# - otherwise, the segment is just to_s
|
||||||
|
# - 't' or 'f' is appended to indicate is_optional
|
||||||
|
def self.generate_full(named_routes, routes, filename, options)
|
||||||
|
|
||||||
|
# Builds a JS array with a hash for each route
|
||||||
|
routes_array = routes.map{|r|
|
||||||
|
|
||||||
|
# Encode segments
|
||||||
|
encoded_segments = r.segments.select{|s|processable_segment(s)}.map{|s|
|
||||||
|
if s.is_a?(ActionController::Routing::PathSegment)
|
||||||
|
'*' + s.to_s[1..-1] + (s.is_optional ? 't' : 'f')
|
||||||
|
else
|
||||||
|
s.to_s + (s.is_optional ? 't' : 'f')
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate route as a hash
|
||||||
|
route_hash = {
|
||||||
|
's' => encoded_segments.join('@'), # 's' -- for segments
|
||||||
|
'r' => r.requirements # 'r' -- for requirements (params for a valid generation)
|
||||||
|
}
|
||||||
|
|
||||||
|
named_route = named_routes.find{|name,route| route.equal?(r) }
|
||||||
|
if named_route
|
||||||
|
route_hash['n'] = named_route.first # 'n' -- for name of named route
|
||||||
|
end
|
||||||
|
|
||||||
|
route_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
File.open(filename, 'w') do |file|
|
||||||
|
# Add core JS (external file)
|
||||||
|
file << File.read(options[:pack] ? JS_PACKED : JS)
|
||||||
|
|
||||||
|
js = <<-JS
|
||||||
|
var url_root = #{ActionController::Base.relative_url_root.to_json};
|
||||||
|
var routes_array = #{routes_array.to_json};
|
||||||
|
for (var i = 0; i < routes_array.length; i++) {
|
||||||
|
var route = routes_array[i]; var segments = [];
|
||||||
|
var segment_strings = route.s.split('@');
|
||||||
|
for (var j = 0; j < segment_strings.length; j++) {
|
||||||
|
var segment_string = segment_strings[j];
|
||||||
|
segments.push(Route.S(segment_string.slice(0, -1), segment_string.slice(-1) == 't'));
|
||||||
|
}
|
||||||
|
Routes.push(new Route(segments, route.r, route.n));
|
||||||
|
}
|
||||||
|
Routes.extractNamed();
|
||||||
|
JS
|
||||||
|
|
||||||
|
# Wrap in a function (called straight away) to not pollute namespace
|
||||||
|
file << "\n(function(){\n#{js}\n})();"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def self.processable_segment(segment)
|
||||||
|
!segment.is_a?(ActionController::Routing::OptionalFormatSegment)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def self.processable_routes
|
||||||
|
ActionController::Routing::Routes.routes.select{|r|
|
||||||
|
r.conditions[:method].nil? || r.conditions[:method] == :get
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def self.processable_named_routes
|
||||||
|
ActionController::Routing::Routes.named_routes.routes.select{|n,r|
|
||||||
|
r.conditions[:method].nil? || r.conditions[:method] == :get
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue