jim 0.1.2 → 0.2.0

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