From 984317e4fc09f7d39524f27f41f071f81e493597 Mon Sep 17 00:00:00 2001 From: Adam Davies Date: Wed, 10 Jun 2009 15:02:25 +0930 Subject: [PATCH] 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. --- lib/javascript_routes.rb | 266 +++++++++++++++++++++------------------ 1 file changed, 144 insertions(+), 122 deletions(-) diff --git a/lib/javascript_routes.rb b/lib/javascript_routes.rb index 3cc41a0..3515e0d 100644 --- a/lib/javascript_routes.rb +++ b/lib/javascript_routes.rb @@ -3,146 +3,168 @@ module JavascriptRoutes JS = File.join(File.dirname(__FILE__), 'javascripts', 'routes.js') JS_PACKED = File.join(File.dirname(__FILE__), 'javascripts', 'routes-min.js') JS_AJAX = File.join(File.dirname(__FILE__), 'javascripts', 'routes-ajax.js') + FILENAME = File.join(RAILS_ROOT, 'public', 'javascripts', 'routes.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 = {}) options.symbolize_keys!.reverse_merge!(:pack => true) - routes = options[:routes] || ActionController::Routing::Routes.routes.select{|r| - r.conditions[:method].nil? || r.conditions[:method] == :get - } - named_routes = options[:named_routes] || ActionController::Routing::Routes.named_routes.routes.select{|n,r| - r.conditions[:method].nil? || r.conditions[:method] == :get - } + routes = options[:routes] || processable_routes + named_routes = options[:named_routes] || processable_named_routes + filename = options[:filename] || FILENAME 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] + generate_lite(named_routes, filename) - File.open(filename, 'w') do |file| - routes_object = ' var r = "{' - 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) + # Simulate Rails route generation logic -- not plain functions + # (includes lib/routes.js) else - - 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 + generate_full(named_routes, routes, filename, options) - #This is ugly, but it works. It builds a JS array - #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 + # Add ajax extras File.open filename_ajax, 'w' do |f| f.write(File.read(JS_AJAX)) - end - + end end - - rescue => e - + + rescue => e warn("\n\nCould not write routes.js: \"#{e.class}:#{e.message}\"\n\n") File.truncate(filename, 0) rescue nil - 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