initialized kettu on gitorious
[kettu:kettu.git] / vendor / sammy / sammy.js
1 // name: sammy
2 // version: 0.5.1
3
4 (function($) {
5   
6   var Sammy,
7       PATH_REPLACER = "([^\/]+)",
8       PATH_NAME_MATCHER = /:([\w\d]+)/g,
9       QUERY_STRING_MATCHER = /\?([^#]*)$/,
10       _decode = decodeURIComponent,
11       _routeWrapper = function(verb) {
12         return function(path, callback) { return this.route.apply(this, [verb, path, callback]); };
13       },
14       loggers = [];
15   
16   
17   // <tt>Sammy</tt> (also aliased as $.sammy) is not only the namespace for a 
18   // number of prototypes, its also a top level method that allows for easy
19   // creation/management of <tt>Sammy.Application</tt> instances. There are a
20   // number of different forms for <tt>Sammy()</tt> but each returns an instance
21   // of <tt>Sammy.Application</tt>. When a new instance is created using
22   // <tt>Sammy</tt> it is added to an Object called <tt>Sammy.apps</tt>. This
23   // provides for an easy way to get at existing Sammy applications. Only one
24   // instance is allowed per <tt>element_selector</tt> so when calling
25   // <tt>Sammy('selector')</tt> multiple times, the first time will create 
26   // the application and the following times will extend the application
27   // already added to that selector.
28   //
29   // === Example
30   //
31   //      // returns the app at #main or a new app
32   //      Sammy('#main') 
33   //
34   //      // equivilent to "new Sammy.Application", except appends to apps
35   //      Sammy();
36   //      Sammy(function() { ... }); 
37   //
38   //      // extends the app at '#main' with function.
39   //      Sammy('#main', function() { ... });
40   //
41   Sammy = function() {
42     var args = $.makeArray(arguments), 
43         app, selector;
44     Sammy.apps = Sammy.apps || {};
45     if (args.length === 0 || args[0] && $.isFunction(args[0])) { // Sammy()
46       return Sammy.apply(Sammy, ['body'].concat(args));
47     } else if (typeof (selector = args.shift()) == 'string') { // Sammy('#main')
48       app = Sammy.apps[selector] || new Sammy.Application();
49       app.element_selector = selector;
50       if (args.length > 0) {
51         $.each(args, function(i, plugin) {
52           app.use(plugin);
53         });
54       }
55       // if the selector changes make sure the refrence in Sammy.apps changes
56       if (app.element_selector != selector) {
57         delete Sammy.apps[selector];
58       }
59       Sammy.apps[app.element_selector] = app;
60       return app;
61     }
62   };
63   
64   Sammy.VERSION = '0.5.1';
65     
66   // Add to the global logger pool. Takes a function that accepts an 
67   // unknown number of arguments and should print them or send them somewhere
68   // The first argument is always a timestamp.
69   Sammy.addLogger = function(logger) {
70     loggers.push(logger);
71   };
72   
73   // Sends a log message to each logger listed in the global
74   // loggers pool. Can take any number of arguments.
75   // Also prefixes the arguments with a timestamp.
76   Sammy.log = function()        {
77     var args = $.makeArray(arguments);
78     args.unshift("[" + Date() + "]");
79     $.each(loggers, function(i, logger) {
80       logger.apply(Sammy, args);
81     });
82         };
83         
84         if (typeof window.console != 'undefined') {
85           if ($.isFunction(console.log.apply)) {
86       Sammy.addLogger(function() {
87         window.console.log.apply(console, arguments);
88       });
89     } else {
90       Sammy.addLogger(function() {
91         window.console.log(arguments);
92       });
93     }
94   } else if (typeof console != 'undefined') {
95     Sammy.addLogger(function() {
96       console.log.apply(console, arguments);
97     });
98   }
99     
100   // Sammy.Object is the base for all other Sammy classes. It provides some useful 
101   // functionality, including cloning, iterating, etc.
102   Sammy.Object = function(obj) { // constructor
103     return $.extend(this, obj || {});
104   };
105         
106   $.extend(Sammy.Object.prototype, {    
107             
108     // Returns a copy of the object with Functions removed.
109     toHash: function() {
110       var json = {}; 
111       $.each(this, function(k,v) {
112         if (!$.isFunction(v)) {
113           json[k] = v;
114         }
115       });
116       return json;
117     },
118     
119     // Renders a simple HTML version of this Objects attributes.
120     // Does not render functions.
121     // For example. Given this Sammy.Object:
122     //    
123     //    var s = new Sammy.Object({first_name: 'Sammy', last_name: 'Davis Jr.'});
124     //    s.toHTML() //=> '<strong>first_name</strong> Sammy<br /><strong>last_name</strong> Davis Jr.<br />'
125     //
126     toHTML: function() {
127       var display = "";
128       $.each(this, function(k, v) {
129         if (!$.isFunction(v)) {
130           display += "<strong>" + k + "</strong> " + v + "<br />";
131         }
132       });
133       return display;
134     },
135     
136     // Generates a unique identifing string. Used for application namespaceing.
137     uuid: function() {
138       if (typeof this._uuid == 'undefined' || !this._uuid) {
139         this._uuid = (new Date()).getTime() + '-' + parseInt(Math.random() * 1000, 10);
140       }
141       return this._uuid;
142     },
143     
144     // Returns an array of keys for this object. If <tt>attributes_only</tt> 
145     // is true will not return keys that map to a <tt>function()</tt>
146     keys: function(attributes_only) {
147       var keys = [];
148       for (var property in this) {
149         if (!$.isFunction(this[property]) || !attributes_only) {
150           keys.push(property);
151         }
152       }
153       return keys;
154     },
155     
156     // Checks if the object has a value at <tt>key</tt> and that the value is not empty
157     has: function(key) {
158       return this[key] && $.trim(this[key].toString()) != '';
159     },
160         
161     // convenience method to join as many arguments as you want 
162     // by the first argument - useful for making paths
163     join: function() {
164       var args = $.makeArray(arguments);
165       var delimiter = args.shift();
166       return args.join(delimiter);
167     },
168     
169     // Shortcut to Sammy.log
170     log: function() {
171       Sammy.log.apply(Sammy, arguments);
172     },
173     
174     // Returns a string representation of this object. 
175     // if <tt>include_functions</tt> is true, it will also toString() the 
176     // methods of this object. By default only prints the attributes.
177     toString: function(include_functions) {
178       var s = [];
179       $.each(this, function(k, v) {
180                     if (!$.isFunction(v) || include_functions) {
181           s.push('"' + k + '": ' + v.toString());
182                     }
183       });
184       return "Sammy.Object: {" + s.join(',') + "}"; 
185     }
186   });
187   
188   // The HashLocationProxy is the default location proxy for all Sammy applications.
189   // A location proxy is a prototype that conforms to a simple interface. The purpose
190   // of a location proxy is to notify the Sammy.Application its bound to when the location
191   // or 'external state' changes. The HashLocationProxy considers the state to be
192   // changed when the 'hash' (window.location.hash / '#') changes. It does this in two
193   // different ways depending on what browser you are using. The newest browsers 
194   // (IE, Safari > 4, FF >= 3.6) support a 'onhashchange' DOM event, thats fired whenever
195   // the location.hash changes. In this situation the HashLocationProxy just binds
196   // to this event and delegates it to the application. In the case of older browsers
197   // a poller is set up to track changes to the hash. Unlike Sammy 0.3 or earlier,
198   // the HashLocationProxy allows the poller to be a global object, eliminating the
199   // need for multiple pollers even when thier are multiple apps on the page.
200   Sammy.HashLocationProxy = function(app, run_interval_every) {
201     this.app = app;
202     
203     // check for native hash support
204     if ('onhashchange' in window) {
205       Sammy.log('native hash change exists, using');
206       this.is_native = true;
207     } else {
208       Sammy.log('no native hash change, falling back to polling');
209       this.is_native = false;
210       this._startPolling(run_interval_every);
211     }
212   };
213   
214   Sammy.HashLocationProxy.prototype = {
215     // bind the proxy events to the current app.
216     bind: function() {
217       var app = this.app;
218       $(window).bind('hashchange.' + this.app.eventNamespace(), function() {
219         app.trigger('location-changed');
220       });
221     },
222     // unbind the proxy events from the current app
223     unbind: function() {
224       $(window).die('hashchange.' + this.app.eventNamespace());
225     },
226     // get the current location from the hash.
227     getLocation: function() {
228      // Bypass the `window.location.hash` attribute.  If a question mark
229       // appears in the hash IE6 will strip it and all of the following
230       // characters from `window.location.hash`.
231       var matches = window.location.toString().match(/^[^#]*(#.+)$/);
232       return matches ? matches[1] : '';
233     },
234     // set the current location to <tt>new_location</tt>
235     setLocation: function(new_location) {
236       return (window.location = new_location);
237     },
238     
239     _startPolling: function(every) {
240       // set up interval
241       var proxy = this;
242       if (!Sammy.HashLocationProxy._interval) {
243         if (!every) { every = 10; }
244         var hashCheck = function() {
245           current_location = proxy.getLocation();
246           // Sammy.log('getLocation', current_location);
247           if (!Sammy.HashLocationProxy._last_location || 
248             current_location != Sammy.HashLocationProxy._last_location) {
249             setTimeout(function() {
250               $(window).trigger('hashchange');
251             }, 1);
252           }
253           Sammy.HashLocationProxy._last_location = current_location;
254         };
255         hashCheck();
256         Sammy.HashLocationProxy._interval = setInterval(hashCheck, every);
257         $(window).bind('beforeunload', function() {
258           clearInterval(Sammy.HashLocationProxy._interval);
259         });
260       }
261     }
262   };
263   
264   // The DataLocationProxy is an optional location proxy prototype. As opposed to
265   // the <tt>HashLocationProxy</tt> it gets its location from a jQuery.data attribute
266   // tied to the application's element. You can set the name of the attribute by
267   // passing a string as the second argument to the constructor. The default attribute
268   // name is 'sammy-location'. To read more about location proxies, check out the 
269   // documentation for <tt>Sammy.HashLocationProxy</tt>
270   Sammy.DataLocationProxy = function(app, data_name) {
271     this.app = app;
272     this.data_name = data_name || 'sammy-location';
273   };
274   
275   Sammy.DataLocationProxy.prototype = {
276     bind: function() {
277       var proxy = this;
278       this.app.$element().bind('setData', function(e, key) {
279         if (key == proxy.data_name) {
280           proxy.app.trigger('location-changed');
281         }
282       });
283     },
284     
285     unbind: function() {
286       this.app.$element().die('setData');
287     },
288     
289     getLocation: function() {
290       return this.app.$element().data(this.data_name);
291     },
292     
293     setLocation: function(new_location) {
294       return this.app.$element().data(this.data_name, new_location);
295     }
296   };
297   
298   // Sammy.Application is the Base prototype for defining 'applications'.
299   // An 'application' is a collection of 'routes' and bound events that is
300   // attached to an element when <tt>run()</tt> is called.
301   // The only argument an 'app_function' is evaluated within the context of the application.
302   Sammy.Application = function(app_function) {
303     var app = this;
304     this.routes            = {};
305     this.listeners         = new Sammy.Object({});
306     this.arounds           = [];
307     this.befores           = [];
308     this.namespace         = this.uuid();
309     this.context_prototype = function() { Sammy.EventContext.apply(this, arguments); };
310     this.context_prototype.prototype = new Sammy.EventContext();
311
312     if ($.isFunction(app_function)) {
313       app_function.apply(this, [this]);
314     }
315     // set the location proxy if not defined to the default (HashLocationProxy)
316     if (!this.location_proxy) {
317       this.location_proxy = new Sammy.HashLocationProxy(app, this.run_interval_every);
318     }
319     if (this.debug) {
320       this.bindToAllEvents(function(e, data) {
321         app.log(app.toString(), e.cleaned_type, data || {});
322       });
323     }
324   };
325   
326   Sammy.Application.prototype = $.extend({}, Sammy.Object.prototype, {
327     
328     // the four route verbs
329     ROUTE_VERBS: ['get','post','put','delete'],
330     
331     // An array of the default events triggered by the 
332     // application during its lifecycle
333     APP_EVENTS: ['run','unload','lookup-route','run-route','route-found','event-context-before','event-context-after','changed','error','check-form-submission','redirect'],
334     
335     _last_route: null,
336     _running: false,
337         
338     // Defines what element the application is bound to. Provide a selector 
339     // (parseable by <tt>jQuery()</tt>) and this will be used by <tt>$element()</tt>
340     element_selector: 'body',
341     
342     // When set to true, logs all of the default events using <tt>log()</tt>
343     debug: false,
344     
345     // When set to true, and the error() handler is not overriden, will actually
346     // raise JS errors in routes (500) and when routes can't be found (404)
347     raise_errors: false,
348     
349     // The time in milliseconds that the URL is queried for changes
350     run_interval_every: 50, 
351     
352     // The location proxy for the current app. By default this is set to a new
353     // <tt>Sammy.HashLocationProxy</tt> on initialization. However, you can set
354     // the location_proxy inside you're app function to give youre app a custom
355     // location mechanism
356     location_proxy: null,
357     
358     // The default template engine to use when using <tt>partial()</tt> in an 
359     // <tt>EventContext</tt>. <tt>template_engine</tt> can either be a string that 
360     // corresponds to the name of a method/helper on EventContext or it can be a function
361     // that takes two arguments, the content of the unrendered partial and an optional
362     // JS object that contains interpolation data. Template engine is only called/refered
363     // to if the extension of the partial is null or unknown. See <tt>partial()</tt>
364     // for more information
365     template_engine: null,
366         
367     // //=> Sammy.Application: body
368     toString: function() {
369       return 'Sammy.Application:' + this.element_selector;
370     },
371     
372     // returns a jQuery object of the Applications bound element.
373     $element: function() {
374       return $(this.element_selector);
375     },
376     
377     // <tt>use()</tt> is the entry point for including Sammy plugins.
378     // The first argument to use should be a function() that is evaluated 
379     // in the context of the current application, just like the <tt>app_function</tt>
380     // argument to the <tt>Sammy.Application</tt> constructor.
381     //
382     // Any additional arguments are passed to the app function sequentially.
383     //
384     // For much more detail about plugins, check out: 
385     // http://code.quirkey.com/sammy/doc/plugins.html
386     // 
387     // === Example
388     //
389     //      var MyPlugin = function(app, prepend) {
390     //
391     //        this.helpers({
392     //          myhelper: function(text) {
393     //            alert(prepend + " " + text);
394     //          }
395     //        });
396     //  
397     //      };
398     //
399     //      var app = $.sammy(function() {
400     // 
401     //        this.use(MyPlugin, 'This is my plugin');
402     //  
403     //        this.get('#/', function() {
404     //          this.myhelper('and dont you forget it!'); 
405     //          //=> Alerts: This is my plugin and dont you forget it!
406     //        });
407     //
408     //      });
409     //
410     use: function() {
411       // flatten the arguments
412       var args = $.makeArray(arguments);
413       var plugin = args.shift();
414       try {
415         args.unshift(this);
416         plugin.apply(this, args);
417       } catch(e) {
418         if (typeof plugin == 'undefined') {
419           this.error("Plugin Error: called use() but plugin is not defined", e);
420         } else if (!$.isFunction(plugin)) {
421           this.error("Plugin Error: called use() but '" + plugin.toString() + "' is not a function", e);
422         } else {
423           this.error("Plugin Error", e);
424         }
425       }
426       return this;
427     },
428     
429     // <tt>route()</tt> is the main method for defining routes within an application.
430     // For great detail on routes, check out: http://code.quirkey.com/sammy/doc/routes.html
431     //
432     // This method also has aliases for each of the different verbs (eg. <tt>get()</tt>, <tt>post()</tt>, etc.)
433     //
434     // === Arguments
435     //
436     // +verb+::     A String in the set of ROUTE_VERBS or 'any'. 'any' will add routes for each
437     //              of the ROUTE_VERBS. If only two arguments are passed, 
438     //              the first argument is the path, the second is the callback and the verb
439     //              is assumed to be 'any'.
440     // +path+::     A Regexp or a String representing the path to match to invoke this verb.
441     // +callback+:: A Function that is called/evaluated whent the route is run see: <tt>runRoute()</tt>.
442     //              It is also possible to pass a string as the callback, which is looked up as the name
443     //              of a method on the application.
444     //
445     route: function(verb, path, callback) {
446       var app = this, param_names = [], add_route;
447       
448       // if the method signature is just (path, callback)
449       // assume the verb is 'any'
450       if (!callback && $.isFunction(path)) {
451         path = verb;
452         callback = path;
453         verb = 'any';
454       }
455       
456       verb = verb.toLowerCase(); // ensure verb is lower case
457       
458       // if path is a string turn it into a regex
459       if (path.constructor == String) {
460         
461         // Needs to be explicitly set because IE will maintain the index unless NULL is returned,
462         // which means that with two consecutive routes that contain params, the second set of params will not be found and end up in splat instead of params
463         // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp/lastIndex        
464         PATH_NAME_MATCHER.lastIndex = 0;
465         
466         // find the names
467         while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) {
468           param_names.push(path_match[1]);
469         }
470         // replace with the path replacement
471         path = new RegExp("^" + path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
472       }
473       // lookup callback
474       if (typeof callback == 'string') {
475         callback = app[callback];
476       }
477       
478       add_route = function(with_verb) {
479         var r = {verb: with_verb, path: path, callback: callback, param_names: param_names};
480         // add route to routes array
481         app.routes[with_verb] = app.routes[with_verb] || [];
482         // place routes in order of definition
483         app.routes[with_verb].push(r);
484       };
485       
486       if (verb === 'any') {
487         $.each(this.ROUTE_VERBS, function(i, v) { add_route(v); });
488       } else {
489         add_route(verb);
490       }
491       
492       // return the app
493       return this;
494     },
495     
496     // Alias for route('get', ...)
497     get: _routeWrapper('get'),
498     
499     // Alias for route('post', ...)
500     post: _routeWrapper('post'),
501
502     // Alias for route('put', ...)
503     put: _routeWrapper('put'),
504     
505     // Alias for route('delete', ...)
506     del: _routeWrapper('delete'),
507     
508     // Alias for route('any', ...)
509     any: _routeWrapper('any'),
510     
511     // <tt>mapRoutes</tt> takes an array of arrays, each array being passed to route()
512     // as arguments, this allows for mass definition of routes. Another benefit is
513     // this makes it possible/easier to load routes via remote JSON.
514     //
515     // === Example
516     //
517     //    var app = $.sammy(function() {
518     //      
519     //      this.mapRoutes([
520     //          ['get', '#/', function() { this.log('index'); }],
521     //          // strings in callbacks are looked up as methods on the app
522     //          ['post', '#/create', 'addUser'],
523     //          // No verb assumes 'any' as the verb
524     //          [/dowhatever/, function() { this.log(this.verb, this.path)}];
525     //        ]);
526     //    })
527     //
528     mapRoutes: function(route_array) {
529       var app = this;
530       $.each(route_array, function(i, route_args) {
531         app.route.apply(app, route_args);
532       });
533       return this;
534     },
535     
536     // A unique event namespace defined per application.
537     // All events bound with <tt>bind()</tt> are automatically bound within this space.
538     eventNamespace: function() {
539       return ['sammy-app', this.namespace].join('-');
540     },
541     
542     // Works just like <tt>jQuery.fn.bind()</tt> with a couple noteable differences.
543     //
544     // * It binds all events to the application element
545     // * All events are bound within the <tt>eventNamespace()</tt>
546     // * Events are not actually bound until the application is started with <tt>run()</tt>
547     // * callbacks are evaluated within the context of a Sammy.EventContext
548     //
549     // See http://code.quirkey.com/sammy/docs/events.html for more info.
550     //
551     bind: function(name, data, callback) {
552       var app = this;
553       // build the callback
554       // if the arity is 2, callback is the second argument
555       if (typeof callback == 'undefined') { callback = data; }
556       var listener_callback =  function() {
557         // pull off the context from the arguments to the callback
558         var e, context, data; 
559         e       = arguments[0];
560         data    = arguments[1];        
561         if (data && data.context) {
562           context = data.context;
563           delete data.context;
564         } else {
565           context = new app.context_prototype(app, 'bind', e.type, data);
566         }
567         e.cleaned_type = e.type.replace(app.eventNamespace(), '');
568         callback.apply(context, [e, data]);
569       };
570       
571       // it could be that the app element doesnt exist yet
572       // so attach to the listeners array and then run()
573       // will actually bind the event.
574       if (!this.listeners[name]) { this.listeners[name] = []; }
575       this.listeners[name].push(listener_callback);
576       if (this.isRunning()) {
577         // if the app is running
578         // *actually* bind the event to the app element
579         this._listen(name, listener_callback);
580       }
581       return this;
582     },
583     
584     // Triggers custom events defined with <tt>bind()</tt>
585     //
586     // === Arguments
587     // 
588     // +name+::     The name of the event. Automatically prefixed with the <tt>eventNamespace()</tt>
589     // +data+::     An optional Object that can be passed to the bound callback.
590     // +context+::  An optional context/Object in which to execute the bound callback. 
591     //              If no context is supplied a the context is a new <tt>Sammy.EventContext</tt>
592     //
593     trigger: function(name, data) {
594       this.$element().trigger([name, this.eventNamespace()].join('.'), [data]);
595       return this;
596     },
597     
598     // Reruns the current route
599     refresh: function() {
600       this.last_location = null;
601       this.trigger('location-changed');
602       return this;
603     },
604     
605     // Takes a single callback that is pushed on to a stack.
606     // Before any route is run, the callbacks are evaluated in order within 
607     // the current <tt>Sammy.EventContext</tt>
608     //
609     // If any of the callbacks explicitly return false, execution of any 
610     // further callbacks and the route itself is halted.
611     // 
612     // You can also provide a set of options that will define when to run this
613     // before based on the route it proceeds. 
614     //
615     // === Example
616     //
617     //      var app = $.sammy(function() {
618     //        
619     //        // will run at #/route but not at #/
620     //        this.before('#/route', function() {
621     //          //...
622     //        });
623     //        
624     //        // will run at #/ but not at #/route
625     //        this.before({except: {path: '#/route'}}, function() {
626     //          this.log('not before #/route');
627     //        });
628     //        
629     //        this.get('#/', function() {});
630     //        
631     //        this.get('#/route', function() {});
632     //        
633     //      });
634     //      
635     // See <tt>contextMatchesOptions()</tt> for a full list of supported options
636     //
637     before: function(options, callback) {
638       if ($.isFunction(options)) {
639         callback = options;
640         options = {};
641       }
642       this.befores.push([options, callback]);
643       return this;
644     },
645     
646     // A shortcut for binding a callback to be run after a route is executed.
647     // After callbacks have no guarunteed order.
648     after: function(callback) {
649       return this.bind('event-context-after', callback);
650     },
651     
652     
653     // Adds an around filter to the application. around filters are functions
654     // that take a single argument <tt>callback</tt> which is the entire route 
655     // execution path wrapped up in a closure. This means you can decide whether
656     // or not to proceed with execution by not invoking <tt>callback</tt> or, 
657     // more usefuly wrapping callback inside the result of an asynchronous execution.
658     //
659     // === Example
660     //
661     // The most common use case for around() is calling a _possibly_ async function
662     // and executing the route within the functions callback:
663     //
664     //      var app = $.sammy(function() {
665     //        
666     //        var current_user = false;
667     //        
668     //        function checkLoggedIn(callback) {
669     //          // /session returns a JSON representation of the logged in user 
670     //          // or an empty object
671     //          if (!current_user) {
672     //            $.getJSON('/session', function(json) {
673     //              if (json.login) {
674     //                // show the user as logged in
675     //                current_user = json;
676     //                // execute the route path
677     //                callback();
678     //              } else {
679     //                // show the user as not logged in
680     //                current_user = false;
681     //                // the context of aroundFilters is an EventContext
682     //                this.redirect('#/login');
683     //              }
684     //            });
685     //          } else {
686     //            // execute the route path
687     //            callback();
688     //          }
689     //        };
690     //        
691     //        this.around(checkLoggedIn);
692     //        
693     //      });
694     //
695     around: function(callback) {
696       this.arounds.push(callback);
697       return this;
698     },
699     
700     // Returns a boolean of weather the current application is running.
701     isRunning: function() {
702       return this._running;
703     },
704     
705     // Helpers extends the EventContext prototype specific to this app.
706     // This allows you to define app specific helper functions that can be used
707     // whenever you're inside of an event context (templates, routes, bind).
708     // 
709     // === Example
710     //
711     //    var app = $.sammy(function() {
712     //      
713     //      helpers({
714     //        upcase: function(text) {
715     //         return text.toString().toUpperCase();
716     //        }
717     //      });
718     //      
719     //      get('#/', function() { with(this) {
720     //        // inside of this context I can use the helpers
721     //        $('#main').html(upcase($('#main').text());
722     //      }});
723     //      
724     //    });
725     //
726     //    
727     // === Arguments
728     // 
729     // +extensions+:: An object collection of functions to extend the context.
730     //  
731     helpers: function(extensions) {
732       $.extend(this.context_prototype.prototype, extensions);
733       return this;
734     },
735     
736     // Helper extends the event context just like <tt>helpers()</tt> but does it
737     // a single method at a time. This is especially useful for dynamically named 
738     // helpers
739     // 
740     // === Example
741     //     
742     //     // Trivial example that adds 3 helper methods to the context dynamically
743     //     var app = $.sammy(function(app) {
744     //       
745     //       $.each([1,2,3], function(i, num) {
746     //         app.helper('helper' + num, function() {
747     //           this.log("I'm helper number " + num);
748     //         }); 
749     //       });
750     //       
751     //       this.get('#/', function() {
752     //         this.helper2(); //=> I'm helper number 2
753     //       });
754     //     });
755     //     
756     // === Arguments
757     // 
758     // +name+:: The name of the method
759     // +method+:: The function to be added to the prototype at <tt>name</tt>
760     //
761     helper: function(name, method) {
762       this.context_prototype.prototype[name] = method;
763       return this;
764     },
765     
766     // Actually starts the application's lifecycle. <tt>run()</tt> should be invoked
767     // within a document.ready block to ensure the DOM exists before binding events, etc.
768     //
769     // === Example
770     // 
771     //    var app = $.sammy(function() { ... }); // your application
772     //    $(function() { // document.ready
773     //        app.run();
774     //     });
775     //
776     // === Arguments
777     //
778     // +start_url+::  "value", Optionally, a String can be passed which the App will redirect to 
779     //                after the events/routes have been bound.
780     run: function(start_url) {
781       if (this.isRunning()) { return false; }
782       var app = this;
783       
784       // actually bind all the listeners
785       $.each(this.listeners.toHash(), function(name, callbacks) {
786         $.each(callbacks, function(i, listener_callback) {
787           app._listen(name, listener_callback);
788         });
789       });
790       
791       this.trigger('run', {start_url: start_url});
792       this._running = true;
793       // set last location
794       this.last_location = null;
795       if (this.getLocation() == '' && typeof start_url != 'undefined') {
796         this.setLocation(start_url);
797       } 
798       // check url
799       this._checkLocation();
800       this.location_proxy.bind();
801       this.bind('location-changed', function() {
802         app._checkLocation();
803       });
804       
805       // bind to submit to capture post/put/delete routes
806       this.bind('submit', function(e) {
807         var returned = app._checkFormSubmission($(e.target).closest('form'));
808         return (returned === false) ? e.preventDefault() : false;
809       });
810
811       // bind unload to body unload
812       $(window).bind('beforeunload', function() {
813         app.unload();
814       });
815       
816       // trigger html changed
817       return this.trigger('changed');
818     },
819     
820     // The opposite of <tt>run()</tt>, un-binds all event listeners and intervals
821     // <tt>run()</tt> Automaticaly binds a <tt>onunload</tt> event to run this when
822     // the document is closed.
823     unload: function() {
824       if (!this.isRunning()) { return false; }
825       var app = this;
826       this.trigger('unload');
827       // clear interval
828       this.location_proxy.unbind();
829       // unbind form submits
830       this.$element().unbind('submit').removeClass(app.eventNamespace());
831       // unbind all events
832       $.each(this.listeners.toHash() , function(name, listeners) {
833         $.each(listeners, function(i, listener_callback) {
834           app._unlisten(name, listener_callback);
835         });
836       });
837       this._running = false;
838       return this;
839     },
840     
841     // Will bind a single callback function to every event that is already 
842     // being listened to in the app. This includes all the <tt>APP_EVENTS</tt>
843     // as well as any custom events defined with <tt>bind()</tt>.
844     // 
845     // Used internally for debug logging.
846     bindToAllEvents: function(callback) {
847       var app = this;
848       // bind to the APP_EVENTS first
849       $.each(this.APP_EVENTS, function(i, e) {
850         app.bind(e, callback);
851       });
852       // next, bind to listener names (only if they dont exist in APP_EVENTS)
853       $.each(this.listeners.keys(true), function(i, name) {
854         if (app.APP_EVENTS.indexOf(name) == -1) {
855           app.bind(name, callback);
856         }
857       });
858       return this;
859     },
860
861     // Returns a copy of the given path with any query string after the hash
862     // removed.
863     routablePath: function(path) {
864       return path.replace(QUERY_STRING_MATCHER, '');
865     },
866     
867     // Given a verb and a String path, will return either a route object or false
868     // if a matching route can be found within the current defined set. 
869     lookupRoute: function(verb, path) {
870       var app = this, routed = false;
871       this.trigger('lookup-route', {verb: verb, path: path});
872       if (typeof this.routes[verb] != 'undefined') {
873         $.each(this.routes[verb], function(i, route) {
874           if (app.routablePath(path).match(route.path)) {
875             routed = route;
876             return false;
877           }
878         });
879       }
880       return routed;
881     },
882
883     // First, invokes <tt>lookupRoute()</tt> and if a route is found, parses the 
884     // possible URL params and then invokes the route's callback within a new
885     // <tt>Sammy.EventContext</tt>. If the route can not be found, it calls 
886     // <tt>notFound()</tt>. If <tt>raise_errors</tt> is set to <tt>true</tt> and 
887     // the <tt>error()</tt> has not been overriden, it will throw an actual JS
888     // error. 
889     //
890     // You probably will never have to call this directly.
891     //
892     // === Arguments
893     // 
894     // +verb+:: A String for the verb.
895     // +path+:: A String path to lookup.
896     // +params+:: An Object of Params pulled from the URI or passed directly.
897     //
898     // === Returns
899     //
900     // Either returns the value returned by the route callback or raises a 404 Not Found error.
901     //
902     runRoute: function(verb, path, params) {
903       var app = this, 
904           route = this.lookupRoute(verb, path),
905           context, 
906           wrapped_route, 
907           arounds, 
908           around, 
909           befores, 
910           before, 
911           callback_args, 
912           final_returned;
913
914       this.log('runRoute', [verb, path].join(' '));
915       this.trigger('run-route', {verb: verb, path: path, params: params});
916       if (typeof params == 'undefined') { params = {}; }
917
918       $.extend(params, this._parseQueryString(path));
919       
920       if (route) {
921         this.trigger('route-found', {route: route});
922         // pull out the params from the path
923         if ((path_params = route.path.exec(this.routablePath(path))) !== null) {
924           // first match is the full path
925           path_params.shift();
926           // for each of the matches
927           $.each(path_params, function(i, param) {
928             // if theres a matching param name
929             if (route.param_names[i]) {
930               // set the name to the match
931               params[route.param_names[i]] = _decode(param);
932             } else {
933               // initialize 'splat'
934               if (!params.splat) { params.splat = []; }
935               params.splat.push(_decode(param));
936             }
937           });
938         }
939         
940         // set event context
941         context  = new this.context_prototype(this, verb, path, params);
942         // ensure arrays
943         arounds = this.arounds.slice(0);  
944         befores = this.befores.slice(0);
945         // set the callback args to the context + contents of the splat
946         callback_args = [context].concat(params.splat);
947         // wrap the route up with the before filters
948         wrapped_route = function() {
949           var returned;
950           while (befores.length > 0) {
951             before = befores.shift();
952             // check the options
953             if (app.contextMatchesOptions(context, before[0])) {
954               returned = before[1].apply(context, [context]);
955               if (returned === false) { return false; }
956             }
957           }
958           app.last_route = route;
959           context.trigger('event-context-before', {context: context});
960           returned = route.callback.apply(context, callback_args);
961           context.trigger('event-context-after', {context: context});
962           return returned;
963         };
964         $.each(arounds.reverse(), function(i, around) {
965           var last_wrapped_route = wrapped_route;
966           wrapped_route = function() { return around.apply(context, [last_wrapped_route]); };
967         });
968         try {
969           final_returned = wrapped_route();
970         } catch(e) {
971           this.error(['500 Error', verb, path].join(' '), e);
972         }
973         return final_returned;
974       } else {
975         return this.notFound(verb, path);
976       }
977     },
978     
979     // Matches an object of options against an <tt>EventContext</tt> like object that
980     // contains <tt>path</tt> and <tt>verb</tt> attributes. Internally Sammy uses this
981     // for matching <tt>before()</tt> filters against specific options. You can set the 
982     // object to _only_ match certain paths or verbs, or match all paths or verbs _except_
983     // those that match the options.
984     //
985     // === Example
986     //   
987     //     var app = $.sammy(),
988     //         context = {verb: 'get', path: '#/mypath'};
989     //     
990     //     // match against a path string
991     //     app.contextMatchesOptions(context, '#/mypath'); //=> true
992     //     app.contextMatchesOptions(context, '#/otherpath'); //=> false
993     //     // equivilent to
994     //     app.contextMatchesOptions(context, {only: {path:'#/mypath'}}); //=> true
995     //     app.contextMatchesOptions(context, {only: {path:'#/otherpath'}}); //=> false
996     //     // match against a path regexp
997     //     app.contextMatchesOptions(context, /path/); //=> true
998     //     app.contextMatchesOptions(context, /^path/); //=> false
999     //     // match only a verb
1000     //     app.contextMatchesOptions(context, {only: {verb:'get'}}); //=> true
1001     //     app.contextMatchesOptions(context, {only: {verb:'post'}}); //=> false
1002     //     // match all except a verb
1003     //     app.contextMatchesOptions(context, {except: {verb:'post'}}); //=> true
1004     //     app.contextMatchesOptions(context, {except: {verb:'get'}}); //=> false
1005     //     // match all except a path
1006     //     app.contextMatchesOptions(context, {except: {path:'#/otherpath'}}); //=> true
1007     //     app.contextMatchesOptions(context, {except: {path:'#/mypath'}}); //=> false
1008     //   
1009     contextMatchesOptions: function(context, match_options, positive) {
1010       // empty options always match
1011       var options = match_options;
1012       if (typeof options === 'undefined' || options == {}) {
1013         return true;
1014       }
1015       if (typeof positive === 'undefined') {
1016         positive = true;
1017       }
1018       // normalize options
1019       if (typeof options === 'string' || $.isFunction(options.test)) {
1020         options = {path: options};
1021       }
1022       if (options.only) {
1023         return this.contextMatchesOptions(context, options.only, true);
1024       } else if (options.except) {
1025         return this.contextMatchesOptions(context, options.except, false);  
1026       }
1027       var path_matched = true, verb_matched = true;
1028       if (options.path) {
1029         // wierd regexp test
1030         if ($.isFunction(options.path.test)) {
1031           path_matched = options.path.test(context.path);
1032         } else {
1033           path_matched = (options.path.toString() === context.path);
1034         }
1035       }
1036       if (options.verb) {
1037         verb_matched = options.verb === context.verb;
1038       }
1039       return positive ? (verb_matched && path_matched) : !(verb_matched && path_matched);
1040     },
1041     
1042
1043     // Delegates to the <tt>location_proxy</tt> to get the current location.
1044     // See <tt>Sammy.HashLocationProxy</tt> for more info on location proxies.
1045     getLocation: function() {
1046       return this.location_proxy.getLocation();
1047     },
1048     
1049     // Delegates to the <tt>location_proxy</tt> to set the current location.
1050     // See <tt>Sammy.HashLocationProxy</tt> for more info on location proxies.
1051     //
1052     // === Arguments
1053     // 
1054     // +new_location+:: A new location string (e.g. '#/')
1055     //
1056     setLocation: function(new_location) {
1057       return this.location_proxy.setLocation(new_location);
1058     },
1059     
1060     // Swaps the content of <tt>$element()</tt> with <tt>content</tt>
1061     // You can override this method to provide an alternate swap behavior
1062     // for <tt>EventContext.partial()</tt>.
1063     // 
1064     // === Example
1065     //
1066     //    var app = $.sammy(function() {
1067     //      
1068     //      // implements a 'fade out'/'fade in'
1069     //      this.swap = function(content) {
1070     //        this.$element().hide('slow').html(content).show('slow');
1071     //      }
1072     //      
1073     //      get('#/', function() {
1074     //        this.partial('index.html.erb') // will fade out and in
1075     //      });
1076     //      
1077     //    });
1078     //
1079     swap: function(content) {
1080       return this.$element().html(content);
1081     },
1082     
1083     // This thows a '404 Not Found' error by invoking <tt>error()</tt>. 
1084     // Override this method or <tt>error()</tt> to provide custom
1085     // 404 behavior (i.e redirecting to / or showing a warning)
1086     notFound: function(verb, path) {
1087       var ret = this.error(['404 Not Found', verb, path].join(' '));
1088       return (verb === 'get') ? ret : true;
1089     },
1090     
1091     // The base error handler takes a string <tt>message</tt> and an <tt>Error</tt>
1092     // object. If <tt>raise_errors</tt> is set to <tt>true</tt> on the app level,
1093     // this will re-throw the error to the browser. Otherwise it will send the error
1094     // to <tt>log()</tt>. Override this method to provide custom error handling
1095     // e.g logging to a server side component or displaying some feedback to the
1096     // user.
1097     error: function(message, original_error) {
1098       if (!original_error) { original_error = new Error(); }
1099       original_error.message = [message, original_error.message].join(' ');
1100       this.trigger('error', {message: original_error.message, error: original_error});
1101       if (this.raise_errors) {
1102         throw(original_error);
1103       } else {
1104         this.log(original_error.message, original_error);
1105       }
1106     },
1107     
1108     _checkLocation: function() {
1109       var location, returned;
1110       // get current location
1111       location = this.getLocation();
1112       // compare to see if hash has changed
1113       if (location != this.last_location) {
1114         // lookup route for current hash
1115         returned = this.runRoute('get', location);
1116       }
1117       // reset last location
1118       this.last_location = location;
1119       return returned;
1120     },
1121     
1122     _checkFormSubmission: function(form) {
1123       var $form, path, verb, params, returned;
1124       this.trigger('check-form-submission', {form: form});
1125       $form = $(form);
1126       path  = $form.attr('action');
1127       verb  = $.trim($form.attr('method').toString().toLowerCase());
1128       if (!verb || verb == '') { verb = 'get'; }
1129       this.log('_checkFormSubmission', $form, path, verb);
1130       params = $.extend({}, this._parseFormParams($form), {'$form': $form});
1131       returned = this.runRoute(verb, path, params);
1132       return (typeof returned == 'undefined') ? false : returned;
1133     },
1134     
1135     _parseFormParams: function($form) {
1136       var params = {};
1137       $.each($form.serializeArray(), function(i, field) {
1138         if (params[field.name]) {
1139           if ($.isArray(params[field.name])) {
1140             params[field.name].push(field.value);
1141           } else {
1142             params[field.name] = [params[field.name], field.value];
1143           }
1144         } else {
1145           params[field.name] = field.value;
1146         }
1147       });
1148       return params;
1149     },
1150     
1151     _parseQueryString: function(path) {
1152       var query = {}, parts, pairs, pair, i;
1153
1154       parts = path.match(QUERY_STRING_MATCHER);
1155       if (parts) {
1156         pairs = parts[1].split('&');
1157         for (i = 0; i < pairs.length; i += 1) {
1158           pair = pairs[i].split('=');
1159           query[pair[0]] = _decode(pair[1]);
1160         }
1161       }
1162
1163       return query;
1164     },
1165     
1166     _listen: function(name, callback) {
1167       return this.$element().bind([name, this.eventNamespace()].join('.'), callback);
1168     },
1169     
1170     _unlisten: function(name, callback) {
1171       return this.$element().unbind([name, this.eventNamespace()].join('.'), callback);
1172     }
1173
1174   });
1175   
1176   // <tt>Sammy.EventContext</tt> objects are created every time a route is run or a 
1177   // bound event is triggered. The callbacks for these events are evaluated within a <tt>Sammy.EventContext</tt>
1178   // This within these callbacks the special methods of <tt>EventContext</tt> are available.
1179   // 
1180   // === Example
1181   //
1182   //  $.sammy(function() { with(this) {
1183   //    // The context here is this Sammy.Application
1184   //    get('#/:name', function() { with(this) {
1185   //      // The context here is a new Sammy.EventContext
1186   //      if (params['name'] == 'sammy') {
1187   //        partial('name.html.erb', {name: 'Sammy'});
1188   //      } else {
1189   //        redirect('#/somewhere-else')
1190   //      }
1191   //    }});
1192   //  }});
1193   //
1194   // Initialize a new EventContext
1195   //
1196   // === Arguments
1197   //
1198   // +app+::    The <tt>Sammy.Application</tt> this event is called within.
1199   // +verb+::   The verb invoked to run this context/route.
1200   // +path+::   The string path invoked to run this context/route.
1201   // +params+:: An Object of optional params to pass to the context. Is converted
1202   //            to a <tt>Sammy.Object</tt>.
1203   Sammy.EventContext = function(app, verb, path, params) {
1204     this.app    = app;
1205     this.verb   = verb;
1206     this.path   = path;
1207     this.params = new Sammy.Object(params);
1208   };
1209    
1210   Sammy.EventContext.prototype = $.extend({}, Sammy.Object.prototype, {
1211     
1212     // A shortcut to the app's <tt>$element()</tt>
1213     $element: function() {
1214       return this.app.$element();
1215     },
1216             
1217     // Used for rendering remote templates or documents within the current application/DOM.
1218     // By default Sammy and <tt>partial()</tt> know nothing about how your templates
1219     // should be interpeted/rendered. This is easy to change, though. <tt>partial()</tt> looks
1220     // for a method in <tt>EventContext</tt> that matches the extension of the file you're
1221     // fetching (e.g. 'myfile.template' will look for a template() method, 'myfile.haml' => haml(), etc.)
1222     // If no matching render method is found it just takes the file contents as is. 
1223     // 
1224     // If you're templates have different (or no) extensions, and you want to render them all
1225     // through the same engine, you can set the default/fallback template engine on the app level
1226     // by setting <tt>app.template_engine</tt> to the name of the engine or a <tt>function() {}</tt>
1227     //
1228     // === Caching
1229     //
1230     // If you use the <tt>Sammy.Cache</tt> plugin, remote requests will be automatically cached unless
1231     // you explicitly set <tt>cache_partials</tt> to <tt>false</tt>
1232     //
1233     // === Example
1234     //
1235     // There are a couple different ways to use <tt>partial()</tt>:
1236     // 
1237     //      partial('doc.html');
1238     //      //=> Replaces $element() with the contents of doc.html
1239     //
1240     //      use(Sammy.Template); 
1241     //      //=> includes the template() method
1242     //      partial('doc.template', {name: 'Sammy'}); 
1243     //      //=> Replaces $element() with the contents of doc.template run through <tt>template()</tt>
1244     //
1245     //      partial('doc.html', function(data) {
1246     //        // data is the contents of the template.
1247     //        $('.other-selector').html(data); 
1248     //      });
1249     //
1250     // === Iteration/Arrays
1251     //
1252     // If the data object passed to <tt>partial()</tt> is an Array, <tt>partial()</tt> 
1253     // will itterate over each element in data calling the callback with the 
1254     // results of interpolation and the index of the element in the array.
1255     // 
1256     //    use(Sammy.Template);
1257     //    // item.template => "<li>I'm an item named <%= name %></li>"
1258     //    partial('item.template', [{name: "Item 1"}, {name: "Item 2"}])
1259     //    //=> Replaces $element() with: 
1260     //    // <li>I'm an item named Item 1</li><li>I'm an item named Item 2</li>
1261     //    partial('item.template', [{name: "Item 1"}, {name: "Item 2"}], function(rendered, i) {
1262     //      rendered; //=> <li>I'm an item named Item 1</li> // for each element in the Array
1263     //      i; // the 0 based index of the itteration
1264     //    });
1265     // 
1266     partial: function(path, data, callback) {
1267       var file_data, 
1268           wrapped_callback,
1269           engine,
1270           data_array,
1271           cache_key = 'partial:' + path,
1272           context = this;
1273
1274       // engine setup
1275       if ((engine = path.match(/\.([^\.]+)$/))) { engine = engine[1]; }
1276       // set the engine to the default template engine if no match is found
1277       if ((!engine || !$.isFunction(context[engine])) && this.app.template_engine) {
1278         engine = this.app.template_engine;
1279       }
1280       if (engine && !$.isFunction(engine) && $.isFunction(context[engine])) { 
1281         engine = context[engine]; 
1282       }
1283       if (!callback && $.isFunction(data)) {
1284         // callback is in the data position
1285         callback = data;
1286         data = {};
1287       }
1288       data_array = ($.isArray(data) ? data : [data || {}]);
1289       wrapped_callback = function(response) {
1290         var new_content = response,
1291             all_content =  "";
1292         $.each(data_array, function(i, idata) {
1293           // extend the data object with the context
1294           $.extend(idata, context);        
1295           if ($.isFunction(engine)) {
1296             new_content = engine.apply(context, [response, idata]);
1297           } 
1298           // collect the content
1299           all_content += new_content;
1300           // if callback exists call it for each iteration
1301           if (callback) { 
1302             // return the result of the callback 
1303             // (you can bail the loop by returning false)
1304             return callback.apply(context, [new_content, i]); 
1305           }
1306         });
1307         if (!callback) { context.swap(all_content); }
1308         context.trigger('changed');
1309       };
1310       if (this.app.cache_partials && this.cache(cache_key)) {
1311         // try to load the template from the cache
1312         wrapped_callback.apply(context, [this.cache(cache_key)]);
1313       } else {
1314         // the template wasnt cached, we need to fetch it
1315         $.get(path, function(response) {
1316           if (context.app.cache_partials) { context.cache(cache_key, response); }
1317           wrapped_callback.apply(context, [response]);
1318         });
1319       }
1320     },
1321     
1322     // Changes the location of the current window. If <tt>to</tt> begins with 
1323     // '#' it only changes the document's hash. If passed more than 1 argument
1324     // redirect will join them together with forward slashes.
1325     //
1326     // === Example
1327     //
1328     //      redirect('#/other/route');
1329     //      // equivilent to
1330     //      redirect('#', 'other', 'route');
1331     //
1332     redirect: function() {
1333       var to, args = $.makeArray(arguments), 
1334           current_location = this.app.getLocation();
1335       if (args.length > 1) {
1336         args.unshift('/');
1337         to = this.join.apply(this, args);
1338       } else {
1339         to = args[0];
1340       }
1341       this.trigger('redirect', {to: to});
1342       this.app.last_location = this.path;
1343       this.app.setLocation(to);
1344       if (current_location == to) {
1345         this.app.trigger('location-changed');
1346       }
1347     },
1348     
1349     // Triggers events on <tt>app</tt> within the current context.
1350     trigger: function(name, data) {
1351       if (typeof data == 'undefined') { data = {}; }
1352       if (!data.context) { data.context = this; }
1353       return this.app.trigger(name, data);
1354     },
1355     
1356     // A shortcut to app's <tt>eventNamespace()</tt>
1357     eventNamespace: function() {
1358       return this.app.eventNamespace();
1359     },
1360     
1361     // A shortcut to app's <tt>swap()</tt>
1362     swap: function(contents) {
1363       return this.app.swap(contents);
1364     },
1365     
1366     // Raises a possible <tt>notFound()</tt> error for the current path.
1367     notFound: function() {
1368       return this.app.notFound(this.verb, this.path);
1369     },
1370     
1371     // //=> Sammy.EventContext: get #/ {}
1372     toString: function() {
1373       return "Sammy.EventContext: " + [this.verb, this.path, this.params].join(' ');
1374     }
1375         
1376   });
1377   
1378   // An alias to Sammy
1379   $.sammy = window.Sammy = Sammy;
1380   
1381 })(jQuery);