commit 2ea0ee5b85a16cf0b5e792a6c705a2c8c13ec884 Author: Tore Darell Date: Thu Apr 17 11:47:13 2008 +0200 Initial commit diff --git a/README b/README new file mode 100644 index 0000000..3681c9a --- /dev/null +++ b/README @@ -0,0 +1,4 @@ +JavascriptRoutes +================ + +Description goes here \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9b01da1 --- /dev/null +++ b/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the javascript_routes plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the javascript_routes plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'JavascriptRoutes' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/bin/jsmin.rb b/bin/jsmin.rb new file mode 100755 index 0000000..38ba292 --- /dev/null +++ b/bin/jsmin.rb @@ -0,0 +1,208 @@ +#!/usr/bin/ruby +# jsmin.rb 2007-05-22 +# Author: Uladzislau Latynski +# This work is a translation from C to Ruby of jsmin.c published by +# Douglas Crockford. Permission is hereby granted to use the Ruby +# version under the same conditions as the jsmin.c on which it is +# based. +# +# /* jsmin.c +# 2003-04-21 +# +# Copyright (c) 2002 Douglas Crockford (www.crockford.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# The Software shall be used for Good, not Evil. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +class JSMin + + EOF = -1 + + def initialize(input, output) + @input, @output = input, output + @theA, @theB = '', '' + end + + # isAlphanum -- return true if the character is a letter, digit, underscore, + # dollar sign, or non-ASCII character + def isAlphanum(c) + return false if !c || c == EOF + return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || + c == '\\' || c[0] > 126) + end + + # get -- return the next character from stdin. Watch out for lookahead. If + # the character is a control character, translate it to a space or linefeed. + def get() + c = @input.getc + return EOF if(!c) + c = c.chr + return c if (c >= " " || c == "\n" || c.unpack("c") == EOF) + return "\n" if (c == "\r") + return " " + end + + # Get the next character without getting it. + def peek() + lookaheadChar = @input.getc + @input.ungetc(lookaheadChar) + return lookaheadChar.chr + end + + # mynext -- get the next character, excluding comments. + # peek() is used to see if a '/' is followed by a '/' or '*'. + def mynext() + c = get + if (c == "/") + if(peek == "/") + while(true) + c = get + if (c <= "\n") + return c + end + end + end + if(peek == "*") + get + while(true) + case get + when "*" + if (peek == "/") + get + return " " + end + when EOF + raise "Unterminated comment" + end + end + end + end + return c + end + + + # action -- do something! What you do is determined by the argument: 1 + # Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B. + # (Delete A). 3 Get the next B. (Delete B). action treats a string as a + # single character. Wow! action recognizes a regular expression if it is + # preceded by ( or , or =. + def action(a) + if(a==1) + @output.write @theA + end + if(a==1 || a==2) + @theA = @theB + if (@theA == "\'" || @theA == "\"") + while (true) + @output.write @theA + @theA = get + break if (@theA == @theB) + raise "Unterminated string literal" if (@theA <= "\n") + if (@theA == "\\") + @output.write @theA + @theA = get + end + end + end + end + if(a==1 || a==2 || a==3) + @theB = mynext + if (@theB == "/" && (@theA == "(" || @theA == "," || @theA == "=" || + @theA == ":" || @theA == "[" || @theA == "!" || + @theA == "&" || @theA == "|" || @theA == "?" || + @theA == "{" || @theA == "}" || @theA == ";" || + @theA == "\n")) + @output.write @theA + @output.write @theB + while (true) + @theA = get + if (@theA == "/") + break + elsif (@theA == "\\") + @output.write @theA + @theA = get + elsif (@theA <= "\n") + raise "Unterminated RegExp Literal" + end + @output.write @theA + end + @theB = mynext + end + end + end + + # jsmin -- Copy the input to the output, deleting the characters which are + # insignificant to JavaScript. Comments will be removed. Tabs will be + # replaced with spaces. Carriage returns will be replaced with linefeeds. + # Most spaces and linefeeds will be removed. + def jsmin + @theA = "\n" + action(3) + while (@theA != EOF) + case @theA + when " " + if (isAlphanum(@theB)) + action(1) + else + action(2) + end + when "\n" + case (@theB) + when "{","[","(","+","-" + action(1) + when " " + action(3) + else + if (isAlphanum(@theB)) + action(1) + else + action(2) + end + end + else + case (@theB) + when " " + if (isAlphanum(@theA)) + action(1) + else + action(3) + end + when "\n" + case (@theA) + when "}","]",")","+","-","\"","\\", "'", '"' + action(1) + else + if (isAlphanum(@theA)) + action(1) + else + action(3) + end + end + else + action(1) + end + end + end + end + +end + +JSMin.new($stdin, $stdout).jsmin if __FILE__ == $0 \ No newline at end of file diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..06eaa5f --- /dev/null +++ b/init.rb @@ -0,0 +1,4 @@ +require 'javascript_routes' + +ActionController::Routing::Routes.load! +JavascriptRoutes.generate(:lite => ENV['ROUTES_JS_LITE']) \ No newline at end of file diff --git a/install.rb b/install.rb new file mode 100644 index 0000000..f7732d3 --- /dev/null +++ b/install.rb @@ -0,0 +1 @@ +# Install hook code here diff --git a/lib/javascript_routes.rb b/lib/javascript_routes.rb new file mode 100644 index 0000000..3cc41a0 --- /dev/null +++ b/lib/javascript_routes.rb @@ -0,0 +1,148 @@ +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') + + 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 + } + filename = options[:filename] || FILENAME + filename_ajax = options[:filename_ajax] || FILENAME_AJAX + + #Will only create simple functions for named routes + if options[:lite] + + 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) + 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 + + #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 + File.open filename_ajax, 'w' do |f| + f.write(File.read(JS_AJAX)) + end + + end + + rescue => e + + warn("\n\nCould not write routes.js: \"#{e.class}:#{e.message}\"\n\n") + File.truncate(filename, 0) rescue nil + + end + +end diff --git a/lib/javascripts/routes-ajax.js b/lib/javascripts/routes-ajax.js new file mode 100644 index 0000000..3f97acf --- /dev/null +++ b/lib/javascripts/routes-ajax.js @@ -0,0 +1,41 @@ +//Michael Schuerig + +Route.Object = function (url) { + this.url = url; +}; + +Route.Object.prototype = { + toString: function() { return this.url; } +}; + + +//Replace Route.prototype.generate +(function(oldGenerate){ + + Route.prototype.generate = function(){ + var path = oldGenerate.apply(this, arguments); + return path && new Route.Object(path); + }; + +})(Route.prototype.generate); + + +if (window.Prototype) { + Object.extend(Route.Object.prototype, { + get: function(options) { this.method = 'get'; return this.request(options) }, + post: function(options) { this.method = 'post'; return this.request(options) }, + put: function(options) { this.method = 'put'; return this.request(options) }, + 'delete': function(options) { this.method = 'delete'; return this.request(options) }, + request: function(options) { + var result = this; + options = options || {}; + var async = $H(options).any(function(p) { return /^on[A-Z1-5]/.test(p[0]); }); + options = Object.extend({ asynchronous: async, method: this.method }, options); + if (!async) { + options.onComplete = function(r) { result = r.responseText; }; + } + new Ajax.Request(this.url, options); + return result; + } + }); +} diff --git a/lib/javascripts/routes-min.js b/lib/javascripts/routes-min.js new file mode 100644 index 0000000..14684ed --- /dev/null +++ b/lib/javascripts/routes-min.js @@ -0,0 +1,18 @@ + +(function(){var iterate=function(o,fn){if(typeof o.length==='number'){for(var i=0;i0){path=path+'?'+leftOvers.join('&');} +if(options.escape){path=encodeURI(path);} +return path;},toString:function(){return this.segments.join('');}};Route.Segment=function(value,type,optional){this.value=value;this.type=type||'static';this.optional=(typeof optional==='boolean'?optional:true);};Route.Segment.prototype={isDynamic:function(){return this.type==='dynamic'||this.type==='path';},toString:function(){if(this.type==='dynamic'){return':'+this.value;}else if(this.type==='path'){return'*'+this.value;}else{return this.value;}},equal:function(other){return other.constructor===this.constructor&&other.value===this.value&&other.type===this.type&&other.optional===this.optional;}};Route.createSegment=function(s,optional){if(s.match(/^[\/;?.]$/)){return new Route.Segment(s,'divider',optional);}else if(s.indexOf(':')===0){return new Route.Segment(s.slice(1),'dynamic',optional);}else if(s.indexOf('*')===0){return new Route.Segment(s.slice(1),'path',optional);}else{return new Route.Segment(s,'static',optional);}};Route.S=Route.createSegment;var Routes=[];Routes.named=[];Routes.defaultParams={action:'index'};Routes.defaultOptions={escape:true,port:window.location.port,protocol:window.location.protocol+'//',host:window.location.hostname};Routes.extractNamed=function(){var route;for(var i=0;icount){params[route.segments[i].value]=arguments[count];} +count++;}}} +return route.generate(params,options);};fn.toParams=function(params){return merge(route.params,params||{});};return fn;})(route);}}};Routes.generate=function(params,options){params=params||{};var path;for(var i=0;i count) { params[route.segments[i].value] = arguments[count]; } + count++; + } + } + } + + return route.generate(params, options); + }; + + //Routes.name.toParams() => {...} + //Like hash_for_x in Rails, kind of + fn.toParams = function(params){ + return merge(route.params, params || {}); + }; + + return fn; + })(route); //Pass the route to keep it in scope + + } + } + }; + + Routes.generate = function(params, options){ + params = params || {}; + var path; + for (var i = 0; i < this.length; i++) { + path = this[i].generate(params, options); + if (path) { + return path; + } + } + return false; + }; + + Routes.named.toString = Routes.toString = function(){ + return this.join(', '); + }; + + + window['Route'] = Route; + window['Routes'] = Routes; + +})(); diff --git a/tasks/javascript_routes_tasks.rake b/tasks/javascript_routes_tasks.rake new file mode 100644 index 0000000..9de6581 --- /dev/null +++ b/tasks/javascript_routes_tasks.rake @@ -0,0 +1,31 @@ +require File.join(RAILS_ROOT, 'config', 'environment') +require File.join(File.dirname(__FILE__), '..', 'lib', 'javascript_routes') +require File.join(File.dirname(__FILE__), '..', 'bin', 'jsmin') + +namespace :routes do + namespace :js do + + desc 'Generate routes.js based on routes defined in routes.rb' + task :generate do + ActionController::Routing::Routes.load! + JavascriptRoutes.generate(:lite => ENV['lite'], :pack => ENV['pack'] != 'false') + puts "Generated #{JavascriptRoutes::FILENAME}" + puts "Generated #{JavascriptRoutes::FILENAME_AJAX}" + end + + desc 'Minify the routes.js base file' + task :minify do + infile = JavascriptRoutes::JS + outfile = JavascriptRoutes::JS_PACKED + + File.open(infile, 'r') do |input| + File.open(outfile, 'w') do |output| + JSMin.new(input, output).jsmin + end + end + + puts "#{File.size(infile)} #{infile}" + puts "#{File.size(outfile)} #{outfile}" + end + end +end diff --git a/test/javascript_routes_test.rb b/test/javascript_routes_test.rb new file mode 100644 index 0000000..d3a812b --- /dev/null +++ b/test/javascript_routes_test.rb @@ -0,0 +1,8 @@ +require 'test/unit' + +class JavascriptRoutesTest < Test::Unit::TestCase + # Replace this with your real tests. + def test_this_plugin + flunk + end +end diff --git a/uninstall.rb b/uninstall.rb new file mode 100644 index 0000000..9738333 --- /dev/null +++ b/uninstall.rb @@ -0,0 +1 @@ +# Uninstall hook code here