rjack-solr 3.6.2.0-java → 4.0.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (315) hide show
  1. data/History.rdoc +5 -0
  2. data/Manifest.txt +268 -33
  3. data/NOTICE.txt +1 -1
  4. data/README.rdoc +6 -6
  5. data/assembly.xml +2 -2
  6. data/bin/rjack-solr-fg +1 -1
  7. data/init/rjack-solr +2 -2
  8. data/lib/rjack-solr.rb +7 -2
  9. data/lib/rjack-solr/base.rb +2 -2
  10. data/lib/rjack-solr/commons-cli-1.2.jar +0 -0
  11. data/lib/rjack-solr/config.rb +1 -1
  12. data/lib/rjack-solr/server.rb +1 -1
  13. data/lib/rjack-solr/solr-core-4.0.0.jar +0 -0
  14. data/lib/rjack-solr/solr-solrj-4.0.0.jar +0 -0
  15. data/pom.xml +19 -6
  16. data/test/test_solr.rb +7 -5
  17. data/webapp/META-INF/LICENSE.txt +0 -940
  18. data/webapp/META-INF/MANIFEST.MF +5 -5
  19. data/webapp/META-INF/NOTICE.txt +160 -93
  20. data/webapp/WEB-INF/web.xml +57 -43
  21. data/webapp/admin.html +140 -0
  22. data/webapp/css/chosen.css +392 -0
  23. data/webapp/css/styles/analysis.css +293 -0
  24. data/webapp/css/styles/cloud.css +331 -0
  25. data/webapp/css/styles/common.css +593 -0
  26. data/webapp/css/styles/cores.css +203 -0
  27. data/webapp/css/styles/dashboard.css +127 -0
  28. data/webapp/css/styles/dataimport.css +222 -0
  29. data/webapp/css/styles/index.css +195 -0
  30. data/webapp/css/styles/java-properties.css +33 -0
  31. data/webapp/css/styles/logging.css +323 -0
  32. data/webapp/css/styles/menu.css +272 -0
  33. data/webapp/css/styles/plugins.css +175 -0
  34. data/webapp/css/styles/query.css +137 -0
  35. data/webapp/css/styles/replication.css +494 -0
  36. data/webapp/css/styles/schema-browser.css +476 -0
  37. data/webapp/css/styles/threads.css +153 -0
  38. data/webapp/img/ZeroClipboard.swf +0 -0
  39. data/webapp/img/chosen-sprite.png +0 -0
  40. data/webapp/img/div.gif +0 -0
  41. data/webapp/{admin → img}/favicon.ico +0 -0
  42. data/webapp/img/filetypes/7z.png +0 -0
  43. data/webapp/img/filetypes/README +27 -0
  44. data/webapp/img/filetypes/ai.png +0 -0
  45. data/webapp/img/filetypes/aiff.png +0 -0
  46. data/webapp/img/filetypes/asc.png +0 -0
  47. data/webapp/img/filetypes/audio.png +0 -0
  48. data/webapp/img/filetypes/bin.png +0 -0
  49. data/webapp/img/filetypes/bz2.png +0 -0
  50. data/webapp/img/filetypes/c.png +0 -0
  51. data/webapp/img/filetypes/cfc.png +0 -0
  52. data/webapp/img/filetypes/cfm.png +0 -0
  53. data/webapp/img/filetypes/chm.png +0 -0
  54. data/webapp/img/filetypes/class.png +0 -0
  55. data/webapp/img/filetypes/conf.png +0 -0
  56. data/webapp/img/filetypes/cpp.png +0 -0
  57. data/webapp/img/filetypes/cs.png +0 -0
  58. data/webapp/img/filetypes/css.png +0 -0
  59. data/webapp/img/filetypes/csv.png +0 -0
  60. data/webapp/img/filetypes/deb.png +0 -0
  61. data/webapp/img/filetypes/divx.png +0 -0
  62. data/webapp/img/filetypes/doc.png +0 -0
  63. data/webapp/img/filetypes/dot.png +0 -0
  64. data/webapp/img/filetypes/eml.png +0 -0
  65. data/webapp/img/filetypes/enc.png +0 -0
  66. data/webapp/img/filetypes/file.png +0 -0
  67. data/webapp/img/filetypes/gif.png +0 -0
  68. data/webapp/img/filetypes/gz.png +0 -0
  69. data/webapp/img/filetypes/hlp.png +0 -0
  70. data/webapp/img/filetypes/htm.png +0 -0
  71. data/webapp/img/filetypes/html.png +0 -0
  72. data/webapp/img/filetypes/image.png +0 -0
  73. data/webapp/img/filetypes/iso.png +0 -0
  74. data/webapp/img/filetypes/jar.png +0 -0
  75. data/webapp/img/filetypes/java.png +0 -0
  76. data/webapp/img/filetypes/jpeg.png +0 -0
  77. data/webapp/img/filetypes/jpg.png +0 -0
  78. data/webapp/img/filetypes/js.png +0 -0
  79. data/webapp/img/filetypes/lua.png +0 -0
  80. data/webapp/img/filetypes/m.png +0 -0
  81. data/webapp/img/filetypes/mm.png +0 -0
  82. data/webapp/img/filetypes/mov.png +0 -0
  83. data/webapp/img/filetypes/mp3.png +0 -0
  84. data/webapp/img/filetypes/mpg.png +0 -0
  85. data/webapp/img/filetypes/odc.png +0 -0
  86. data/webapp/img/filetypes/odf.png +0 -0
  87. data/webapp/img/filetypes/odg.png +0 -0
  88. data/webapp/img/filetypes/odi.png +0 -0
  89. data/webapp/img/filetypes/odp.png +0 -0
  90. data/webapp/img/filetypes/ods.png +0 -0
  91. data/webapp/img/filetypes/odt.png +0 -0
  92. data/webapp/img/filetypes/ogg.png +0 -0
  93. data/webapp/img/filetypes/pdf.png +0 -0
  94. data/webapp/img/filetypes/pgp.png +0 -0
  95. data/webapp/img/filetypes/php.png +0 -0
  96. data/webapp/img/filetypes/pl.png +0 -0
  97. data/webapp/img/filetypes/png.png +0 -0
  98. data/webapp/img/filetypes/ppt.png +0 -0
  99. data/webapp/img/filetypes/ps.png +0 -0
  100. data/webapp/img/filetypes/py.png +0 -0
  101. data/webapp/img/filetypes/ram.png +0 -0
  102. data/webapp/img/filetypes/rar.png +0 -0
  103. data/webapp/img/filetypes/rb.png +0 -0
  104. data/webapp/img/filetypes/rm.png +0 -0
  105. data/webapp/img/filetypes/rpm.png +0 -0
  106. data/webapp/img/filetypes/rtf.png +0 -0
  107. data/webapp/img/filetypes/sig.png +0 -0
  108. data/webapp/img/filetypes/sql.png +0 -0
  109. data/webapp/img/filetypes/swf.png +0 -0
  110. data/webapp/img/filetypes/sxc.png +0 -0
  111. data/webapp/img/filetypes/sxd.png +0 -0
  112. data/webapp/img/filetypes/sxi.png +0 -0
  113. data/webapp/img/filetypes/sxw.png +0 -0
  114. data/webapp/img/filetypes/tar.png +0 -0
  115. data/webapp/img/filetypes/tex.png +0 -0
  116. data/webapp/img/filetypes/tgz.png +0 -0
  117. data/webapp/img/filetypes/txt.png +0 -0
  118. data/webapp/img/filetypes/vcf.png +0 -0
  119. data/webapp/img/filetypes/video.png +0 -0
  120. data/webapp/img/filetypes/vsd.png +0 -0
  121. data/webapp/img/filetypes/wav.png +0 -0
  122. data/webapp/img/filetypes/wma.png +0 -0
  123. data/webapp/img/filetypes/wmv.png +0 -0
  124. data/webapp/img/filetypes/xls.png +0 -0
  125. data/webapp/img/filetypes/xml.png +0 -0
  126. data/webapp/img/filetypes/xpi.png +0 -0
  127. data/webapp/img/filetypes/xvid.png +0 -0
  128. data/webapp/img/filetypes/zip.png +0 -0
  129. data/webapp/img/ico/arrow-000-small.png +0 -0
  130. data/webapp/img/ico/arrow-circle.png +0 -0
  131. data/webapp/img/ico/arrow-switch.png +0 -0
  132. data/webapp/img/ico/asterisk.png +0 -0
  133. data/webapp/img/ico/battery.png +0 -0
  134. data/webapp/img/ico/block-small.png +0 -0
  135. data/webapp/img/ico/block.png +0 -0
  136. data/webapp/img/ico/book-open-text.png +0 -0
  137. data/webapp/img/ico/box.png +0 -0
  138. data/webapp/img/ico/bug.png +0 -0
  139. data/webapp/img/ico/chart.png +0 -0
  140. data/webapp/img/ico/chevron-small-expand.png +0 -0
  141. data/webapp/img/ico/chevron-small.png +0 -0
  142. data/webapp/img/ico/clipboard-list.png +0 -0
  143. data/webapp/img/ico/clipboard-paste-document-text.png +0 -0
  144. data/webapp/img/ico/clipboard-paste.png +0 -0
  145. data/webapp/img/ico/clock-select-remain.png +0 -0
  146. data/webapp/img/ico/clock-select.png +0 -0
  147. data/webapp/img/ico/construction.png +0 -0
  148. data/webapp/img/ico/cross-0.png +0 -0
  149. data/webapp/img/ico/cross-1.png +0 -0
  150. data/webapp/img/ico/cross.png +0 -0
  151. data/webapp/img/ico/dashboard.png +0 -0
  152. data/webapp/img/ico/database.png +0 -0
  153. data/webapp/img/ico/databases.png +0 -0
  154. data/webapp/img/ico/disk-black.png +0 -0
  155. data/webapp/img/ico/document-convert.png +0 -0
  156. data/webapp/img/ico/document-import.png +0 -0
  157. data/webapp/img/ico/document-list.png +0 -0
  158. data/webapp/img/ico/document-text.png +0 -0
  159. data/webapp/img/ico/download-cloud.png +0 -0
  160. data/webapp/img/ico/exclamation-button.png +0 -0
  161. data/webapp/img/ico/eye.png +0 -0
  162. data/webapp/img/ico/folder-export.png +0 -0
  163. data/webapp/img/ico/folder-tree.png +0 -0
  164. data/webapp/img/ico/folder.png +0 -0
  165. data/webapp/img/ico/funnel-small.png +0 -0
  166. data/webapp/img/ico/funnel.png +0 -0
  167. data/webapp/img/ico/gear.png +0 -0
  168. data/webapp/img/ico/globe-network.png +0 -0
  169. data/webapp/img/ico/globe.png +0 -0
  170. data/webapp/img/ico/hammer-screwdriver.png +0 -0
  171. data/webapp/img/ico/hand.png +0 -0
  172. data/webapp/img/ico/highlighter-text.png +0 -0
  173. data/webapp/img/ico/hourglass--exclamation.png +0 -0
  174. data/webapp/img/ico/hourglass.png +0 -0
  175. data/webapp/img/ico/inbox-document-text.png +0 -0
  176. data/webapp/img/ico/information-button.png +0 -0
  177. data/webapp/img/ico/information-small.png +0 -0
  178. data/webapp/img/ico/information-white.png +0 -0
  179. data/webapp/img/ico/information.png +0 -0
  180. data/webapp/img/ico/jar.png +0 -0
  181. data/webapp/img/ico/magnifier.png +0 -0
  182. data/webapp/img/ico/mail.png +0 -0
  183. data/webapp/img/ico/memory.png +0 -0
  184. data/webapp/img/ico/molecule.png +0 -0
  185. data/webapp/img/ico/network-cloud.png +0 -0
  186. data/webapp/img/ico/network-status-away.png +0 -0
  187. data/webapp/img/ico/network-status-busy.png +0 -0
  188. data/webapp/img/ico/network-status-offline.png +0 -0
  189. data/webapp/img/ico/network-status.png +0 -0
  190. data/webapp/img/ico/network.png +0 -0
  191. data/webapp/img/ico/node-design.png +0 -0
  192. data/webapp/img/ico/node-master.png +0 -0
  193. data/webapp/img/ico/node-select.png +0 -0
  194. data/webapp/img/ico/node-slave.png +0 -0
  195. data/webapp/img/ico/node.png +0 -0
  196. data/webapp/img/ico/pencil-small.png +0 -0
  197. data/webapp/img/ico/plus-button.png +0 -0
  198. data/webapp/img/ico/processor.png +0 -0
  199. data/webapp/img/ico/prohibition.png +0 -0
  200. data/webapp/img/ico/property.png +0 -0
  201. data/webapp/img/ico/question-small-white.png +0 -0
  202. data/webapp/img/ico/question-white.png +0 -0
  203. data/webapp/img/ico/question.png +0 -0
  204. data/webapp/img/ico/receipt-invoice.png +0 -0
  205. data/webapp/img/ico/receipt.png +0 -0
  206. data/webapp/img/ico/script-code.png +0 -0
  207. data/webapp/img/ico/server-cast.png +0 -0
  208. data/webapp/img/ico/server.png +0 -0
  209. data/webapp/img/ico/sitemap.png +0 -0
  210. data/webapp/img/ico/slash.png +0 -0
  211. data/webapp/img/ico/status-away.png +0 -0
  212. data/webapp/img/ico/status-busy.png +0 -0
  213. data/webapp/img/ico/status-offline.png +0 -0
  214. data/webapp/img/ico/status.png +0 -0
  215. data/webapp/img/ico/system-monitor--exclamation.png +0 -0
  216. data/webapp/img/ico/system-monitor.png +0 -0
  217. data/webapp/img/ico/table.png +0 -0
  218. data/webapp/img/ico/terminal.png +0 -0
  219. data/webapp/img/ico/tick-circle.png +0 -0
  220. data/webapp/img/ico/tick-red.png +0 -0
  221. data/webapp/img/ico/tick.png +0 -0
  222. data/webapp/img/ico/toggle-small-expand.png +0 -0
  223. data/webapp/img/ico/toggle-small.png +0 -0
  224. data/webapp/img/ico/toolbox.png +0 -0
  225. data/webapp/img/ico/ui-accordion.png +0 -0
  226. data/webapp/img/ico/ui-address-bar.png +0 -0
  227. data/webapp/img/ico/ui-check-box-uncheck.png +0 -0
  228. data/webapp/img/ico/ui-check-box.png +0 -0
  229. data/webapp/img/ico/ui-radio-button-uncheck.png +0 -0
  230. data/webapp/img/ico/ui-radio-button.png +0 -0
  231. data/webapp/img/ico/ui-text-field-select.png +0 -0
  232. data/webapp/img/ico/users.png +0 -0
  233. data/webapp/img/ico/wooden-box.png +0 -0
  234. data/webapp/img/ico/zone.png +0 -0
  235. data/webapp/img/loader-light.gif +0 -0
  236. data/webapp/img/loader.gif +0 -0
  237. data/webapp/img/lucene-ico.png +0 -0
  238. data/webapp/img/solr-ico.png +0 -0
  239. data/webapp/{admin/solr_small.png → img/solr.png} +0 -0
  240. data/webapp/img/tree.png +0 -0
  241. data/webapp/js/lib/ZeroClipboard.js +317 -0
  242. data/webapp/js/lib/chosen.js +953 -0
  243. data/webapp/{admin/get-properties.jsp → js/lib/console.js} +13 -10
  244. data/webapp/js/lib/d3.js +9342 -0
  245. data/webapp/js/lib/highlight.js +1 -0
  246. data/webapp/{admin/jquery-1.4.3.min.js → js/lib/jquery-1.7.2.min.js} +0 -0
  247. data/webapp/js/lib/jquery.blockUI.js +499 -0
  248. data/webapp/js/lib/jquery.cookie.js +47 -0
  249. data/webapp/js/lib/jquery.form.js +782 -0
  250. data/webapp/js/lib/jquery.jstree.js +3510 -0
  251. data/webapp/js/lib/jquery.sammy.js +1856 -0
  252. data/webapp/js/lib/jquery.sparkline.js +1271 -0
  253. data/webapp/js/lib/jquery.timeago.js +165 -0
  254. data/webapp/js/lib/linker.js +48 -0
  255. data/webapp/js/lib/order.js +189 -0
  256. data/webapp/js/main.js +57 -0
  257. data/webapp/js/require.js +11319 -0
  258. data/webapp/js/scripts/analysis.js +549 -0
  259. data/webapp/js/scripts/app.js +368 -0
  260. data/webapp/js/scripts/cloud.js +719 -0
  261. data/webapp/js/scripts/cores.js +540 -0
  262. data/webapp/js/scripts/dashboard.js +540 -0
  263. data/webapp/js/scripts/dataimport.js +567 -0
  264. data/webapp/js/scripts/file.js +56 -0
  265. data/webapp/js/scripts/index.js +344 -0
  266. data/webapp/js/scripts/java-properties.js +107 -0
  267. data/webapp/js/scripts/logging.js +519 -0
  268. data/webapp/js/scripts/ping.js +72 -0
  269. data/webapp/js/scripts/plugins.js +426 -0
  270. data/webapp/js/scripts/query.js +170 -0
  271. data/webapp/js/scripts/replication.js +557 -0
  272. data/webapp/js/scripts/schema-browser.js +1206 -0
  273. data/webapp/js/scripts/threads.js +159 -0
  274. data/webapp/tpl/analysis.html +83 -0
  275. data/webapp/tpl/cloud.html +54 -0
  276. data/webapp/tpl/cores.html +215 -0
  277. data/webapp/tpl/dashboard.html +164 -0
  278. data/webapp/tpl/dataimport.html +123 -0
  279. data/webapp/tpl/index.html +250 -0
  280. data/webapp/tpl/logging.html +23 -0
  281. data/webapp/tpl/plugins.html +39 -0
  282. data/webapp/tpl/query.html +318 -0
  283. data/webapp/tpl/replication.html +207 -0
  284. data/webapp/tpl/schema-browser.html +178 -0
  285. data/webapp/tpl/threads.html +56 -0
  286. metadata +307 -40
  287. data/lib/rjack-solr/solr-core-3.6.2.jar +0 -0
  288. data/lib/rjack-solr/solr-solrj-3.6.2.jar +0 -0
  289. data/webapp/admin/_info.jsp +0 -120
  290. data/webapp/admin/action.jsp +0 -94
  291. data/webapp/admin/analysis.jsp +0 -496
  292. data/webapp/admin/analysis.xsl +0 -179
  293. data/webapp/admin/dataimport.jsp +0 -53
  294. data/webapp/admin/debug.jsp +0 -103
  295. data/webapp/admin/distributiondump.jsp +0 -160
  296. data/webapp/admin/form.jsp +0 -152
  297. data/webapp/admin/get-file.jsp +0 -72
  298. data/webapp/admin/header.jsp +0 -44
  299. data/webapp/admin/index.jsp +0 -162
  300. data/webapp/admin/meta.xsl +0 -34
  301. data/webapp/admin/ping.jsp +0 -52
  302. data/webapp/admin/ping.xsl +0 -71
  303. data/webapp/admin/raw-schema.jsp +0 -38
  304. data/webapp/admin/registry.jsp +0 -107
  305. data/webapp/admin/registry.xsl +0 -321
  306. data/webapp/admin/replication/header.jsp +0 -89
  307. data/webapp/admin/replication/index.jsp +0 -378
  308. data/webapp/admin/schema.jsp +0 -690
  309. data/webapp/admin/solr-admin.css +0 -215
  310. data/webapp/admin/stats.jsp +0 -92
  311. data/webapp/admin/stats.xsl +0 -220
  312. data/webapp/admin/tabular.xsl +0 -141
  313. data/webapp/admin/threaddump.jsp +0 -110
  314. data/webapp/admin/threaddump.xsl +0 -103
  315. data/webapp/index.jsp +0 -49
@@ -0,0 +1,1856 @@
1
+ // name: sammy
2
+ // version: 0.6.2
3
+ /*
4
+ Licensed to the Apache Software Foundation (ASF) under one or more
5
+ contributor license agreements. See the NOTICE file distributed with
6
+ this work for additional information regarding copyright ownership.
7
+ The ASF licenses this file to You under the Apache License, Version 2.0
8
+ (the "License"); you may not use this file except in compliance with
9
+ the License. You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
20
+ (function($, window) {
21
+
22
+ var Sammy,
23
+ PATH_REPLACER = "([^\/]+)",
24
+ PATH_NAME_MATCHER = /:([\w\d]+)/g,
25
+ QUERY_STRING_MATCHER = /\?([^#]*)$/,
26
+ // mainly for making `arguments` an Array
27
+ _makeArray = function(nonarray) { return Array.prototype.slice.call(nonarray); },
28
+ // borrowed from jQuery
29
+ _isFunction = function( obj ) { return Object.prototype.toString.call(obj) === "[object Function]"; },
30
+ _isArray = function( obj ) { return Object.prototype.toString.call(obj) === "[object Array]"; },
31
+ _decode = decodeURIComponent,
32
+ _encode = encodeURIComponent,
33
+ _escapeHTML = function(s) {
34
+ return String(s).replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
35
+ },
36
+ _routeWrapper = function(verb) {
37
+ return function(path, callback) { return this.route.apply(this, [verb, path, callback]); };
38
+ },
39
+ _template_cache = {},
40
+ loggers = [];
41
+
42
+
43
+ // `Sammy` (also aliased as $.sammy) is not only the namespace for a
44
+ // number of prototypes, its also a top level method that allows for easy
45
+ // creation/management of `Sammy.Application` instances. There are a
46
+ // number of different forms for `Sammy()` but each returns an instance
47
+ // of `Sammy.Application`. When a new instance is created using
48
+ // `Sammy` it is added to an Object called `Sammy.apps`. This
49
+ // provides for an easy way to get at existing Sammy applications. Only one
50
+ // instance is allowed per `element_selector` so when calling
51
+ // `Sammy('selector')` multiple times, the first time will create
52
+ // the application and the following times will extend the application
53
+ // already added to that selector.
54
+ //
55
+ // ### Example
56
+ //
57
+ // // returns the app at #main or a new app
58
+ // Sammy('#main')
59
+ //
60
+ // // equivilent to "new Sammy.Application", except appends to apps
61
+ // Sammy();
62
+ // Sammy(function() { ... });
63
+ //
64
+ // // extends the app at '#main' with function.
65
+ // Sammy('#main', function() { ... });
66
+ //
67
+ Sammy = function() {
68
+ var args = _makeArray(arguments),
69
+ app, selector;
70
+ Sammy.apps = Sammy.apps || {};
71
+ if (args.length === 0 || args[0] && _isFunction(args[0])) { // Sammy()
72
+ return Sammy.apply(Sammy, ['body'].concat(args));
73
+ } else if (typeof (selector = args.shift()) == 'string') { // Sammy('#main')
74
+ app = Sammy.apps[selector] || new Sammy.Application();
75
+ app.element_selector = selector;
76
+ if (args.length > 0) {
77
+ $.each(args, function(i, plugin) {
78
+ app.use(plugin);
79
+ });
80
+ }
81
+ // if the selector changes make sure the refrence in Sammy.apps changes
82
+ if (app.element_selector != selector) {
83
+ delete Sammy.apps[selector];
84
+ }
85
+ Sammy.apps[app.element_selector] = app;
86
+ return app;
87
+ }
88
+ };
89
+
90
+ Sammy.VERSION = '0.6.2';
91
+
92
+ // Add to the global logger pool. Takes a function that accepts an
93
+ // unknown number of arguments and should print them or send them somewhere
94
+ // The first argument is always a timestamp.
95
+ Sammy.addLogger = function(logger) {
96
+ loggers.push(logger);
97
+ };
98
+
99
+ // Sends a log message to each logger listed in the global
100
+ // loggers pool. Can take any number of arguments.
101
+ // Also prefixes the arguments with a timestamp.
102
+ Sammy.log = function() {
103
+ var args = _makeArray(arguments);
104
+ args.unshift("[" + Date() + "]");
105
+ $.each(loggers, function(i, logger) {
106
+ logger.apply(Sammy, args);
107
+ });
108
+ };
109
+
110
+ if (typeof window.console != 'undefined') {
111
+ if (_isFunction(window.console.log.apply)) {
112
+ Sammy.addLogger(function() {
113
+ window.console.log.apply(window.console, arguments);
114
+ });
115
+ } else {
116
+ Sammy.addLogger(function() {
117
+ window.console.log(arguments);
118
+ });
119
+ }
120
+ } else if (typeof console != 'undefined') {
121
+ Sammy.addLogger(function() {
122
+ console.log.apply(console, arguments);
123
+ });
124
+ }
125
+
126
+ $.extend(Sammy, {
127
+ makeArray: _makeArray,
128
+ isFunction: _isFunction,
129
+ isArray: _isArray
130
+ })
131
+
132
+ // Sammy.Object is the base for all other Sammy classes. It provides some useful
133
+ // functionality, including cloning, iterating, etc.
134
+ Sammy.Object = function(obj) { // constructor
135
+ return $.extend(this, obj || {});
136
+ };
137
+
138
+ $.extend(Sammy.Object.prototype, {
139
+
140
+ // Escape HTML in string, use in templates to prevent script injection.
141
+ // Also aliased as `h()`
142
+ escapeHTML: _escapeHTML,
143
+ h: _escapeHTML,
144
+
145
+ // Returns a copy of the object with Functions removed.
146
+ toHash: function() {
147
+ var json = {};
148
+ $.each(this, function(k,v) {
149
+ if (!_isFunction(v)) {
150
+ json[k] = v;
151
+ }
152
+ });
153
+ return json;
154
+ },
155
+
156
+ // Renders a simple HTML version of this Objects attributes.
157
+ // Does not render functions.
158
+ // For example. Given this Sammy.Object:
159
+ //
160
+ // var s = new Sammy.Object({first_name: 'Sammy', last_name: 'Davis Jr.'});
161
+ // s.toHTML() //=> '<strong>first_name</strong> Sammy<br /><strong>last_name</strong> Davis Jr.<br />'
162
+ //
163
+ toHTML: function() {
164
+ var display = "";
165
+ $.each(this, function(k, v) {
166
+ if (!_isFunction(v)) {
167
+ display += "<strong>" + k + "</strong> " + v + "<br />";
168
+ }
169
+ });
170
+ return display;
171
+ },
172
+
173
+ // Returns an array of keys for this object. If `attributes_only`
174
+ // is true will not return keys that map to a `function()`
175
+ keys: function(attributes_only) {
176
+ var keys = [];
177
+ for (var property in this) {
178
+ if (!_isFunction(this[property]) || !attributes_only) {
179
+ keys.push(property);
180
+ }
181
+ }
182
+ return keys;
183
+ },
184
+
185
+ // Checks if the object has a value at `key` and that the value is not empty
186
+ has: function(key) {
187
+ return this[key] && $.trim(this[key].toString()) != '';
188
+ },
189
+
190
+ // convenience method to join as many arguments as you want
191
+ // by the first argument - useful for making paths
192
+ join: function() {
193
+ var args = _makeArray(arguments);
194
+ var delimiter = args.shift();
195
+ return args.join(delimiter);
196
+ },
197
+
198
+ // Shortcut to Sammy.log
199
+ log: function() {
200
+ Sammy.log.apply(Sammy, arguments);
201
+ },
202
+
203
+ // Returns a string representation of this object.
204
+ // if `include_functions` is true, it will also toString() the
205
+ // methods of this object. By default only prints the attributes.
206
+ toString: function(include_functions) {
207
+ var s = [];
208
+ $.each(this, function(k, v) {
209
+ if (!_isFunction(v) || include_functions) {
210
+ s.push('"' + k + '": ' + v.toString());
211
+ }
212
+ });
213
+ return "Sammy.Object: {" + s.join(',') + "}";
214
+ }
215
+ });
216
+
217
+ // The HashLocationProxy is the default location proxy for all Sammy applications.
218
+ // A location proxy is a prototype that conforms to a simple interface. The purpose
219
+ // of a location proxy is to notify the Sammy.Application its bound to when the location
220
+ // or 'external state' changes. The HashLocationProxy considers the state to be
221
+ // changed when the 'hash' (window.location.hash / '#') changes. It does this in two
222
+ // different ways depending on what browser you are using. The newest browsers
223
+ // (IE, Safari > 4, FF >= 3.6) support a 'onhashchange' DOM event, thats fired whenever
224
+ // the location.hash changes. In this situation the HashLocationProxy just binds
225
+ // to this event and delegates it to the application. In the case of older browsers
226
+ // a poller is set up to track changes to the hash. Unlike Sammy 0.3 or earlier,
227
+ // the HashLocationProxy allows the poller to be a global object, eliminating the
228
+ // need for multiple pollers even when thier are multiple apps on the page.
229
+ Sammy.HashLocationProxy = function(app, run_interval_every) {
230
+ this.app = app;
231
+ // set is native to false and start the poller immediately
232
+ this.is_native = false;
233
+ this._startPolling(run_interval_every);
234
+ };
235
+
236
+ Sammy.HashLocationProxy.prototype = {
237
+
238
+ // bind the proxy events to the current app.
239
+ bind: function() {
240
+ var proxy = this, app = this.app;
241
+ $(window).bind('hashchange.' + this.app.eventNamespace(), function(e, non_native) {
242
+ // if we receive a native hash change event, set the proxy accordingly
243
+ // and stop polling
244
+ if (proxy.is_native === false && !non_native) {
245
+ Sammy.log('native hash change exists, using');
246
+ proxy.is_native = true;
247
+ window.clearInterval(Sammy.HashLocationProxy._interval);
248
+ }
249
+ app.trigger('location-changed');
250
+ });
251
+ if (!Sammy.HashLocationProxy._bindings) {
252
+ Sammy.HashLocationProxy._bindings = 0;
253
+ }
254
+ Sammy.HashLocationProxy._bindings++;
255
+ },
256
+
257
+ // unbind the proxy events from the current app
258
+ unbind: function() {
259
+ $(window).unbind('hashchange.' + this.app.eventNamespace());
260
+ Sammy.HashLocationProxy._bindings--;
261
+ if (Sammy.HashLocationProxy._bindings <= 0) {
262
+ window.clearInterval(Sammy.HashLocationProxy._interval);
263
+ }
264
+ },
265
+
266
+ // get the current location from the hash.
267
+ getLocation: function() {
268
+ // Bypass the `window.location.hash` attribute. If a question mark
269
+ // appears in the hash IE6 will strip it and all of the following
270
+ // characters from `window.location.hash`.
271
+ var matches = window.location.toString().match(/^[^#]*(#.+)$/);
272
+ return matches ? matches[1] : '';
273
+ },
274
+
275
+ // set the current location to `new_location`
276
+ setLocation: function(new_location) {
277
+ return (window.location = new_location);
278
+ },
279
+
280
+ _startPolling: function(every) {
281
+ // set up interval
282
+ var proxy = this;
283
+ if (!Sammy.HashLocationProxy._interval) {
284
+ if (!every) { every = 10; }
285
+ var hashCheck = function() {
286
+ var current_location = proxy.getLocation();
287
+ if (!Sammy.HashLocationProxy._last_location ||
288
+ current_location != Sammy.HashLocationProxy._last_location) {
289
+ window.setTimeout(function() {
290
+ $(window).trigger('hashchange', [true]);
291
+ }, 13);
292
+ }
293
+ Sammy.HashLocationProxy._last_location = current_location;
294
+ };
295
+ hashCheck();
296
+ Sammy.HashLocationProxy._interval = window.setInterval(hashCheck, every);
297
+ }
298
+ }
299
+ };
300
+
301
+
302
+ // Sammy.Application is the Base prototype for defining 'applications'.
303
+ // An 'application' is a collection of 'routes' and bound events that is
304
+ // attached to an element when `run()` is called.
305
+ // The only argument an 'app_function' is evaluated within the context of the application.
306
+ Sammy.Application = function(app_function) {
307
+ var app = this;
308
+ this.routes = {};
309
+ this.listeners = new Sammy.Object({});
310
+ this.arounds = [];
311
+ this.befores = [];
312
+ // generate a unique namespace
313
+ this.namespace = (new Date()).getTime() + '-' + parseInt(Math.random() * 1000, 10);
314
+ this.context_prototype = function() { Sammy.EventContext.apply(this, arguments); };
315
+ this.context_prototype.prototype = new Sammy.EventContext();
316
+
317
+ if (_isFunction(app_function)) {
318
+ app_function.apply(this, [this]);
319
+ }
320
+ // set the location proxy if not defined to the default (HashLocationProxy)
321
+ if (!this._location_proxy) {
322
+ this.setLocationProxy(new Sammy.HashLocationProxy(this, this.run_interval_every));
323
+ }
324
+ if (this.debug) {
325
+ this.bindToAllEvents(function(e, data) {
326
+ app.log(app.toString(), e.cleaned_type, data || {});
327
+ });
328
+ }
329
+ };
330
+
331
+ Sammy.Application.prototype = $.extend({}, Sammy.Object.prototype, {
332
+
333
+ // the four route verbs
334
+ ROUTE_VERBS: ['get','post','put','delete'],
335
+
336
+ // An array of the default events triggered by the
337
+ // application during its lifecycle
338
+ APP_EVENTS: ['run',
339
+ 'unload',
340
+ 'lookup-route',
341
+ 'run-route',
342
+ 'route-found',
343
+ 'event-context-before',
344
+ 'event-context-after',
345
+ 'changed',
346
+ 'error',
347
+ 'check-form-submission',
348
+ 'redirect',
349
+ 'location-changed'],
350
+
351
+ _last_route: null,
352
+ _location_proxy: null,
353
+ _running: false,
354
+
355
+ // Defines what element the application is bound to. Provide a selector
356
+ // (parseable by `jQuery()`) and this will be used by `$element()`
357
+ element_selector: 'body',
358
+
359
+ // When set to true, logs all of the default events using `log()`
360
+ debug: false,
361
+
362
+ // When set to true, and the error() handler is not overriden, will actually
363
+ // raise JS errors in routes (500) and when routes can't be found (404)
364
+ raise_errors: false,
365
+
366
+ // The time in milliseconds that the URL is queried for changes
367
+ run_interval_every: 50,
368
+
369
+ // The default template engine to use when using `partial()` in an
370
+ // `EventContext`. `template_engine` can either be a string that
371
+ // corresponds to the name of a method/helper on EventContext or it can be a function
372
+ // that takes two arguments, the content of the unrendered partial and an optional
373
+ // JS object that contains interpolation data. Template engine is only called/refered
374
+ // to if the extension of the partial is null or unknown. See `partial()`
375
+ // for more information
376
+ template_engine: null,
377
+
378
+ // //=> Sammy.Application: body
379
+ toString: function() {
380
+ return 'Sammy.Application:' + this.element_selector;
381
+ },
382
+
383
+ // returns a jQuery object of the Applications bound element.
384
+ $element: function(selector) {
385
+ return selector ? $(this.element_selector).find(selector) : $(this.element_selector);
386
+ },
387
+
388
+ // `use()` is the entry point for including Sammy plugins.
389
+ // The first argument to use should be a function() that is evaluated
390
+ // in the context of the current application, just like the `app_function`
391
+ // argument to the `Sammy.Application` constructor.
392
+ //
393
+ // Any additional arguments are passed to the app function sequentially.
394
+ //
395
+ // For much more detail about plugins, check out:
396
+ // http://code.quirkey.com/sammy/doc/plugins.html
397
+ //
398
+ // ### Example
399
+ //
400
+ // var MyPlugin = function(app, prepend) {
401
+ //
402
+ // this.helpers({
403
+ // myhelper: function(text) {
404
+ // alert(prepend + " " + text);
405
+ // }
406
+ // });
407
+ //
408
+ // };
409
+ //
410
+ // var app = $.sammy(function() {
411
+ //
412
+ // this.use(MyPlugin, 'This is my plugin');
413
+ //
414
+ // this.get('#/', function() {
415
+ // this.myhelper('and dont you forget it!');
416
+ // //=> Alerts: This is my plugin and dont you forget it!
417
+ // });
418
+ //
419
+ // });
420
+ //
421
+ // If plugin is passed as a string it assumes your are trying to load
422
+ // Sammy."Plugin". This is the prefered way of loading core Sammy plugins
423
+ // as it allows for better error-messaging.
424
+ //
425
+ // ### Example
426
+ //
427
+ // $.sammy(function() {
428
+ // this.use('Mustache'); //=> Sammy.Mustache
429
+ // this.use('Storage'); //=> Sammy.Storage
430
+ // });
431
+ //
432
+ use: function() {
433
+ // flatten the arguments
434
+ var args = _makeArray(arguments),
435
+ plugin = args.shift(),
436
+ plugin_name = plugin || '';
437
+ try {
438
+ args.unshift(this);
439
+ if (typeof plugin == 'string') {
440
+ plugin_name = 'Sammy.' + plugin;
441
+ plugin = Sammy[plugin];
442
+ }
443
+ plugin.apply(this, args);
444
+ } catch(e) {
445
+ if (typeof plugin === 'undefined') {
446
+ this.error("Plugin Error: called use() but plugin (" + plugin_name.toString() + ") is not defined", e);
447
+ } else if (!_isFunction(plugin)) {
448
+ this.error("Plugin Error: called use() but '" + plugin_name.toString() + "' is not a function", e);
449
+ } else {
450
+ this.error("Plugin Error", e);
451
+ }
452
+ }
453
+ return this;
454
+ },
455
+
456
+ // Sets the location proxy for the current app. By default this is set to
457
+ // a new `Sammy.HashLocationProxy` on initialization. However, you can set
458
+ // the location_proxy inside you're app function to give your app a custom
459
+ // location mechanism. See `Sammy.HashLocationProxy` and `Sammy.DataLocationProxy`
460
+ // for examples.
461
+ //
462
+ // `setLocationProxy()` takes an initialized location proxy.
463
+ //
464
+ // ### Example
465
+ //
466
+ // // to bind to data instead of the default hash;
467
+ // var app = $.sammy(function() {
468
+ // this.setLocationProxy(new Sammy.DataLocationProxy(this));
469
+ // });
470
+ //
471
+ setLocationProxy: function(new_proxy) {
472
+ var original_proxy = this._location_proxy;
473
+ this._location_proxy = new_proxy;
474
+ if (this.isRunning()) {
475
+ if (original_proxy) {
476
+ // if there is already a location proxy, unbind it.
477
+ original_proxy.unbind();
478
+ }
479
+ this._location_proxy.bind();
480
+ }
481
+ },
482
+
483
+ // `route()` is the main method for defining routes within an application.
484
+ // For great detail on routes, check out: http://code.quirkey.com/sammy/doc/routes.html
485
+ //
486
+ // This method also has aliases for each of the different verbs (eg. `get()`, `post()`, etc.)
487
+ //
488
+ // ### Arguments
489
+ //
490
+ // * `verb` A String in the set of ROUTE_VERBS or 'any'. 'any' will add routes for each
491
+ // of the ROUTE_VERBS. If only two arguments are passed,
492
+ // the first argument is the path, the second is the callback and the verb
493
+ // is assumed to be 'any'.
494
+ // * `path` A Regexp or a String representing the path to match to invoke this verb.
495
+ // * `callback` A Function that is called/evaluated whent the route is run see: `runRoute()`.
496
+ // It is also possible to pass a string as the callback, which is looked up as the name
497
+ // of a method on the application.
498
+ //
499
+ route: function(verb, path, callback) {
500
+ var app = this, param_names = [], add_route, path_match;
501
+
502
+ // if the method signature is just (path, callback)
503
+ // assume the verb is 'any'
504
+ if (!callback && _isFunction(path)) {
505
+ path = verb;
506
+ callback = path;
507
+ verb = 'any';
508
+ }
509
+
510
+ verb = verb.toLowerCase(); // ensure verb is lower case
511
+
512
+ // if path is a string turn it into a regex
513
+ if (path.constructor == String) {
514
+
515
+ // Needs to be explicitly set because IE will maintain the index unless NULL is returned,
516
+ // 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
517
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp/lastIndex
518
+ PATH_NAME_MATCHER.lastIndex = 0;
519
+
520
+ // find the names
521
+ while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) {
522
+ param_names.push(path_match[1]);
523
+ }
524
+ // replace with the path replacement
525
+ path = new RegExp("^" + path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
526
+ }
527
+ // lookup callback
528
+ if (typeof callback == 'string') {
529
+ callback = app[callback];
530
+ }
531
+
532
+ add_route = function(with_verb) {
533
+ var r = {verb: with_verb, path: path, callback: callback, param_names: param_names};
534
+ // add route to routes array
535
+ app.routes[with_verb] = app.routes[with_verb] || [];
536
+ // place routes in order of definition
537
+ app.routes[with_verb].push(r);
538
+ };
539
+
540
+ if (verb === 'any') {
541
+ $.each(this.ROUTE_VERBS, function(i, v) { add_route(v); });
542
+ } else {
543
+ add_route(verb);
544
+ }
545
+
546
+ // return the app
547
+ return this;
548
+ },
549
+
550
+ // Alias for route('get', ...)
551
+ get: _routeWrapper('get'),
552
+
553
+ // Alias for route('post', ...)
554
+ post: _routeWrapper('post'),
555
+
556
+ // Alias for route('put', ...)
557
+ put: _routeWrapper('put'),
558
+
559
+ // Alias for route('delete', ...)
560
+ del: _routeWrapper('delete'),
561
+
562
+ // Alias for route('any', ...)
563
+ any: _routeWrapper('any'),
564
+
565
+ // `mapRoutes` takes an array of arrays, each array being passed to route()
566
+ // as arguments, this allows for mass definition of routes. Another benefit is
567
+ // this makes it possible/easier to load routes via remote JSON.
568
+ //
569
+ // ### Example
570
+ //
571
+ // var app = $.sammy(function() {
572
+ //
573
+ // this.mapRoutes([
574
+ // ['get', '#/', function() { this.log('index'); }],
575
+ // // strings in callbacks are looked up as methods on the app
576
+ // ['post', '#/create', 'addUser'],
577
+ // // No verb assumes 'any' as the verb
578
+ // [/dowhatever/, function() { this.log(this.verb, this.path)}];
579
+ // ]);
580
+ // })
581
+ //
582
+ mapRoutes: function(route_array) {
583
+ var app = this;
584
+ $.each(route_array, function(i, route_args) {
585
+ app.route.apply(app, route_args);
586
+ });
587
+ return this;
588
+ },
589
+
590
+ // A unique event namespace defined per application.
591
+ // All events bound with `bind()` are automatically bound within this space.
592
+ eventNamespace: function() {
593
+ return ['sammy-app', this.namespace].join('-');
594
+ },
595
+
596
+ // Works just like `jQuery.fn.bind()` with a couple noteable differences.
597
+ //
598
+ // * It binds all events to the application element
599
+ // * All events are bound within the `eventNamespace()`
600
+ // * Events are not actually bound until the application is started with `run()`
601
+ // * callbacks are evaluated within the context of a Sammy.EventContext
602
+ //
603
+ // See http://code.quirkey.com/sammy/docs/events.html for more info.
604
+ //
605
+ bind: function(name, data, callback) {
606
+ var app = this;
607
+ // build the callback
608
+ // if the arity is 2, callback is the second argument
609
+ if (typeof callback == 'undefined') { callback = data; }
610
+ var listener_callback = function() {
611
+ // pull off the context from the arguments to the callback
612
+ var e, context, data;
613
+ e = arguments[0];
614
+ data = arguments[1];
615
+ if (data && data.context) {
616
+ context = data.context;
617
+ delete data.context;
618
+ } else {
619
+ context = new app.context_prototype(app, 'bind', e.type, data, e.target);
620
+ }
621
+ e.cleaned_type = e.type.replace(app.eventNamespace(), '');
622
+ callback.apply(context, [e, data]);
623
+ };
624
+
625
+ // it could be that the app element doesnt exist yet
626
+ // so attach to the listeners array and then run()
627
+ // will actually bind the event.
628
+ if (!this.listeners[name]) { this.listeners[name] = []; }
629
+ this.listeners[name].push(listener_callback);
630
+ if (this.isRunning()) {
631
+ // if the app is running
632
+ // *actually* bind the event to the app element
633
+ this._listen(name, listener_callback);
634
+ }
635
+ return this;
636
+ },
637
+
638
+ // Triggers custom events defined with `bind()`
639
+ //
640
+ // ### Arguments
641
+ //
642
+ // * `name` The name of the event. Automatically prefixed with the `eventNamespace()`
643
+ // * `data` An optional Object that can be passed to the bound callback.
644
+ // * `context` An optional context/Object in which to execute the bound callback.
645
+ // If no context is supplied a the context is a new `Sammy.EventContext`
646
+ //
647
+ trigger: function(name, data) {
648
+ this.$element().trigger([name, this.eventNamespace()].join('.'), [data]);
649
+ return this;
650
+ },
651
+
652
+ // Reruns the current route
653
+ refresh: function() {
654
+ this.last_location = null;
655
+ this.trigger('location-changed');
656
+ return this;
657
+ },
658
+
659
+ // Takes a single callback that is pushed on to a stack.
660
+ // Before any route is run, the callbacks are evaluated in order within
661
+ // the current `Sammy.EventContext`
662
+ //
663
+ // If any of the callbacks explicitly return false, execution of any
664
+ // further callbacks and the route itself is halted.
665
+ //
666
+ // You can also provide a set of options that will define when to run this
667
+ // before based on the route it proceeds.
668
+ //
669
+ // ### Example
670
+ //
671
+ // var app = $.sammy(function() {
672
+ //
673
+ // // will run at #/route but not at #/
674
+ // this.before('#/route', function() {
675
+ // //...
676
+ // });
677
+ //
678
+ // // will run at #/ but not at #/route
679
+ // this.before({except: {path: '#/route'}}, function() {
680
+ // this.log('not before #/route');
681
+ // });
682
+ //
683
+ // this.get('#/', function() {});
684
+ //
685
+ // this.get('#/route', function() {});
686
+ //
687
+ // });
688
+ //
689
+ // See `contextMatchesOptions()` for a full list of supported options
690
+ //
691
+ before: function(options, callback) {
692
+ if (_isFunction(options)) {
693
+ callback = options;
694
+ options = {};
695
+ }
696
+ this.befores.push([options, callback]);
697
+ return this;
698
+ },
699
+
700
+ // A shortcut for binding a callback to be run after a route is executed.
701
+ // After callbacks have no guarunteed order.
702
+ after: function(callback) {
703
+ return this.bind('event-context-after', callback);
704
+ },
705
+
706
+
707
+ // Adds an around filter to the application. around filters are functions
708
+ // that take a single argument `callback` which is the entire route
709
+ // execution path wrapped up in a closure. This means you can decide whether
710
+ // or not to proceed with execution by not invoking `callback` or,
711
+ // more usefuly wrapping callback inside the result of an asynchronous execution.
712
+ //
713
+ // ### Example
714
+ //
715
+ // The most common use case for around() is calling a _possibly_ async function
716
+ // and executing the route within the functions callback:
717
+ //
718
+ // var app = $.sammy(function() {
719
+ //
720
+ // var current_user = false;
721
+ //
722
+ // function checkLoggedIn(callback) {
723
+ // // /session returns a JSON representation of the logged in user
724
+ // // or an empty object
725
+ // if (!current_user) {
726
+ // $.getJSON('/session', function(json) {
727
+ // if (json.login) {
728
+ // // show the user as logged in
729
+ // current_user = json;
730
+ // // execute the route path
731
+ // callback();
732
+ // } else {
733
+ // // show the user as not logged in
734
+ // current_user = false;
735
+ // // the context of aroundFilters is an EventContext
736
+ // this.redirect('#/login');
737
+ // }
738
+ // });
739
+ // } else {
740
+ // // execute the route path
741
+ // callback();
742
+ // }
743
+ // };
744
+ //
745
+ // this.around(checkLoggedIn);
746
+ //
747
+ // });
748
+ //
749
+ around: function(callback) {
750
+ this.arounds.push(callback);
751
+ return this;
752
+ },
753
+
754
+ // Returns `true` if the current application is running.
755
+ isRunning: function() {
756
+ return this._running;
757
+ },
758
+
759
+ // Helpers extends the EventContext prototype specific to this app.
760
+ // This allows you to define app specific helper functions that can be used
761
+ // whenever you're inside of an event context (templates, routes, bind).
762
+ //
763
+ // ### Example
764
+ //
765
+ // var app = $.sammy(function() {
766
+ //
767
+ // helpers({
768
+ // upcase: function(text) {
769
+ // return text.toString().toUpperCase();
770
+ // }
771
+ // });
772
+ //
773
+ // get('#/', function() { with(this) {
774
+ // // inside of this context I can use the helpers
775
+ // $('#main').html(upcase($('#main').text());
776
+ // }});
777
+ //
778
+ // });
779
+ //
780
+ //
781
+ // ### Arguments
782
+ //
783
+ // * `extensions` An object collection of functions to extend the context.
784
+ //
785
+ helpers: function(extensions) {
786
+ $.extend(this.context_prototype.prototype, extensions);
787
+ return this;
788
+ },
789
+
790
+ // Helper extends the event context just like `helpers()` but does it
791
+ // a single method at a time. This is especially useful for dynamically named
792
+ // helpers
793
+ //
794
+ // ### Example
795
+ //
796
+ // // Trivial example that adds 3 helper methods to the context dynamically
797
+ // var app = $.sammy(function(app) {
798
+ //
799
+ // $.each([1,2,3], function(i, num) {
800
+ // app.helper('helper' + num, function() {
801
+ // this.log("I'm helper number " + num);
802
+ // });
803
+ // });
804
+ //
805
+ // this.get('#/', function() {
806
+ // this.helper2(); //=> I'm helper number 2
807
+ // });
808
+ // });
809
+ //
810
+ // ### Arguments
811
+ //
812
+ // * `name` The name of the method
813
+ // * `method` The function to be added to the prototype at `name`
814
+ //
815
+ helper: function(name, method) {
816
+ this.context_prototype.prototype[name] = method;
817
+ return this;
818
+ },
819
+
820
+ // Actually starts the application's lifecycle. `run()` should be invoked
821
+ // within a document.ready block to ensure the DOM exists before binding events, etc.
822
+ //
823
+ // ### Example
824
+ //
825
+ // var app = $.sammy(function() { ... }); // your application
826
+ // $(function() { // document.ready
827
+ // app.run();
828
+ // });
829
+ //
830
+ // ### Arguments
831
+ //
832
+ // * `start_url` Optionally, a String can be passed which the App will redirect to
833
+ // after the events/routes have been bound.
834
+ run: function(start_url) {
835
+ if (this.isRunning()) { return false; }
836
+ var app = this;
837
+
838
+ // actually bind all the listeners
839
+ $.each(this.listeners.toHash(), function(name, callbacks) {
840
+ $.each(callbacks, function(i, listener_callback) {
841
+ app._listen(name, listener_callback);
842
+ });
843
+ });
844
+
845
+ this.trigger('run', {start_url: start_url});
846
+ this._running = true;
847
+ // set last location
848
+ this.last_location = null;
849
+ if (this.getLocation() == '' && typeof start_url != 'undefined') {
850
+ this.setLocation(start_url);
851
+ }
852
+ // check url
853
+ this._checkLocation();
854
+ this._location_proxy.bind();
855
+ this.bind('location-changed', function() {
856
+ app._checkLocation();
857
+ });
858
+
859
+ // bind to submit to capture post/put/delete routes
860
+ /*
861
+ this.bind('submit', function(e) {
862
+ var returned = app._checkFormSubmission($(e.target).closest('form'));
863
+ return (returned === false) ? e.preventDefault() : false;
864
+ });
865
+ */
866
+
867
+ // bind unload to body unload
868
+ $(window).bind('beforeunload', function() {
869
+ app.unload();
870
+ });
871
+
872
+ // trigger html changed
873
+ return this.trigger('changed');
874
+ },
875
+
876
+ // The opposite of `run()`, un-binds all event listeners and intervals
877
+ // `run()` Automaticaly binds a `onunload` event to run this when
878
+ // the document is closed.
879
+ unload: function() {
880
+ if (!this.isRunning()) { return false; }
881
+ var app = this;
882
+ this.trigger('unload');
883
+ // clear interval
884
+ this._location_proxy.unbind();
885
+ // unbind form submits
886
+ this.$element().unbind('submit').removeClass(app.eventNamespace());
887
+ // unbind all events
888
+ $.each(this.listeners.toHash() , function(name, listeners) {
889
+ $.each(listeners, function(i, listener_callback) {
890
+ app._unlisten(name, listener_callback);
891
+ });
892
+ });
893
+ this._running = false;
894
+ return this;
895
+ },
896
+
897
+ // Will bind a single callback function to every event that is already
898
+ // being listened to in the app. This includes all the `APP_EVENTS`
899
+ // as well as any custom events defined with `bind()`.
900
+ //
901
+ // Used internally for debug logging.
902
+ bindToAllEvents: function(callback) {
903
+ var app = this;
904
+ // bind to the APP_EVENTS first
905
+ $.each(this.APP_EVENTS, function(i, e) {
906
+ app.bind(e, callback);
907
+ });
908
+ // next, bind to listener names (only if they dont exist in APP_EVENTS)
909
+ $.each(this.listeners.keys(true), function(i, name) {
910
+ if (app.APP_EVENTS.indexOf(name) == -1) {
911
+ app.bind(name, callback);
912
+ }
913
+ });
914
+ return this;
915
+ },
916
+
917
+ // Returns a copy of the given path with any query string after the hash
918
+ // removed.
919
+ routablePath: function(path) {
920
+ return path.replace(QUERY_STRING_MATCHER, '');
921
+ },
922
+
923
+ // Given a verb and a String path, will return either a route object or false
924
+ // if a matching route can be found within the current defined set.
925
+ lookupRoute: function(verb, path) {
926
+ var app = this, routed = false;
927
+ this.trigger('lookup-route', {verb: verb, path: path});
928
+ if (typeof this.routes[verb] != 'undefined') {
929
+ $.each(this.routes[verb], function(i, route) {
930
+ if (app.routablePath(path).match(route.path)) {
931
+ routed = route;
932
+ return false;
933
+ }
934
+ });
935
+ }
936
+ return routed;
937
+ },
938
+
939
+ // First, invokes `lookupRoute()` and if a route is found, parses the
940
+ // possible URL params and then invokes the route's callback within a new
941
+ // `Sammy.EventContext`. If the route can not be found, it calls
942
+ // `notFound()`. If `raise_errors` is set to `true` and
943
+ // the `error()` has not been overriden, it will throw an actual JS
944
+ // error.
945
+ //
946
+ // You probably will never have to call this directly.
947
+ //
948
+ // ### Arguments
949
+ //
950
+ // * `verb` A String for the verb.
951
+ // * `path` A String path to lookup.
952
+ // * `params` An Object of Params pulled from the URI or passed directly.
953
+ //
954
+ // ### Returns
955
+ //
956
+ // Either returns the value returned by the route callback or raises a 404 Not Found error.
957
+ //
958
+ runRoute: function(verb, path, params, target) {
959
+ var app = this,
960
+ route = this.lookupRoute(verb, path),
961
+ context,
962
+ wrapped_route,
963
+ arounds,
964
+ around,
965
+ befores,
966
+ before,
967
+ callback_args,
968
+ path_params,
969
+ final_returned;
970
+
971
+ this.log('runRoute', [verb, path].join(' '));
972
+ this.trigger('run-route', {verb: verb, path: path, params: params});
973
+ if (typeof params == 'undefined') { params = {}; }
974
+
975
+ $.extend(params, this._parseQueryString(path));
976
+
977
+ if (route) {
978
+ this.trigger('route-found', {route: route});
979
+ // pull out the params from the path
980
+ if ((path_params = route.path.exec(this.routablePath(path))) !== null) {
981
+ // first match is the full path
982
+ path_params.shift();
983
+ // for each of the matches
984
+ $.each(path_params, function(i, param) {
985
+ // if theres a matching param name
986
+ if (route.param_names[i]) {
987
+ // set the name to the match
988
+ params[route.param_names[i]] = _decode(param);
989
+ } else {
990
+ // initialize 'splat'
991
+ if (!params.splat) { params.splat = []; }
992
+ params.splat.push(_decode(param));
993
+ }
994
+ });
995
+ }
996
+
997
+ // set event context
998
+ context = new this.context_prototype(this, verb, path, params, target);
999
+ // ensure arrays
1000
+ arounds = this.arounds.slice(0);
1001
+ befores = this.befores.slice(0);
1002
+ // set the callback args to the context + contents of the splat
1003
+ callback_args = [context].concat(params.splat);
1004
+ // wrap the route up with the before filters
1005
+ wrapped_route = function() {
1006
+ var returned;
1007
+ while (befores.length > 0) {
1008
+ before = befores.shift();
1009
+ // check the options
1010
+ if (app.contextMatchesOptions(context, before[0])) {
1011
+ returned = before[1].apply(context, [context]);
1012
+ if (returned === false) { return false; }
1013
+ }
1014
+ }
1015
+ app.last_route = route;
1016
+ context.trigger('event-context-before', {context: context});
1017
+ returned = route.callback.apply(context, callback_args);
1018
+ context.trigger('event-context-after', {context: context});
1019
+ return returned;
1020
+ };
1021
+ $.each(arounds.reverse(), function(i, around) {
1022
+ var last_wrapped_route = wrapped_route;
1023
+ wrapped_route = function() { return around.apply(context, [last_wrapped_route]); };
1024
+ });
1025
+ try {
1026
+ final_returned = wrapped_route();
1027
+ } catch(e) {
1028
+ this.error(['500 Error', verb, path].join(' '), e);
1029
+ }
1030
+ return final_returned;
1031
+ } else {
1032
+ return this.notFound(verb, path);
1033
+ }
1034
+ },
1035
+
1036
+ // Matches an object of options against an `EventContext` like object that
1037
+ // contains `path` and `verb` attributes. Internally Sammy uses this
1038
+ // for matching `before()` filters against specific options. You can set the
1039
+ // object to _only_ match certain paths or verbs, or match all paths or verbs _except_
1040
+ // those that match the options.
1041
+ //
1042
+ // ### Example
1043
+ //
1044
+ // var app = $.sammy(),
1045
+ // context = {verb: 'get', path: '#/mypath'};
1046
+ //
1047
+ // // match against a path string
1048
+ // app.contextMatchesOptions(context, '#/mypath'); //=> true
1049
+ // app.contextMatchesOptions(context, '#/otherpath'); //=> false
1050
+ // // equivilent to
1051
+ // app.contextMatchesOptions(context, {only: {path:'#/mypath'}}); //=> true
1052
+ // app.contextMatchesOptions(context, {only: {path:'#/otherpath'}}); //=> false
1053
+ // // match against a path regexp
1054
+ // app.contextMatchesOptions(context, /path/); //=> true
1055
+ // app.contextMatchesOptions(context, /^path/); //=> false
1056
+ // // match only a verb
1057
+ // app.contextMatchesOptions(context, {only: {verb:'get'}}); //=> true
1058
+ // app.contextMatchesOptions(context, {only: {verb:'post'}}); //=> false
1059
+ // // match all except a verb
1060
+ // app.contextMatchesOptions(context, {except: {verb:'post'}}); //=> true
1061
+ // app.contextMatchesOptions(context, {except: {verb:'get'}}); //=> false
1062
+ // // match all except a path
1063
+ // app.contextMatchesOptions(context, {except: {path:'#/otherpath'}}); //=> true
1064
+ // app.contextMatchesOptions(context, {except: {path:'#/mypath'}}); //=> false
1065
+ //
1066
+ contextMatchesOptions: function(context, match_options, positive) {
1067
+ // empty options always match
1068
+ var options = match_options;
1069
+ if (typeof options === 'undefined' || options == {}) {
1070
+ return true;
1071
+ }
1072
+ if (typeof positive === 'undefined') {
1073
+ positive = true;
1074
+ }
1075
+ // normalize options
1076
+ if (typeof options === 'string' || _isFunction(options.test)) {
1077
+ options = {path: options};
1078
+ }
1079
+ if (options.only) {
1080
+ return this.contextMatchesOptions(context, options.only, true);
1081
+ } else if (options.except) {
1082
+ return this.contextMatchesOptions(context, options.except, false);
1083
+ }
1084
+ var path_matched = true, verb_matched = true;
1085
+ if (options.path) {
1086
+ // wierd regexp test
1087
+ if (_isFunction(options.path.test)) {
1088
+ path_matched = options.path.test(context.path);
1089
+ } else {
1090
+ path_matched = (options.path.toString() === context.path);
1091
+ }
1092
+ }
1093
+ if (options.verb) {
1094
+ verb_matched = options.verb === context.verb;
1095
+ }
1096
+ return positive ? (verb_matched && path_matched) : !(verb_matched && path_matched);
1097
+ },
1098
+
1099
+
1100
+ // Delegates to the `location_proxy` to get the current location.
1101
+ // See `Sammy.HashLocationProxy` for more info on location proxies.
1102
+ getLocation: function() {
1103
+ return this._location_proxy.getLocation();
1104
+ },
1105
+
1106
+ // Delegates to the `location_proxy` to set the current location.
1107
+ // See `Sammy.HashLocationProxy` for more info on location proxies.
1108
+ //
1109
+ // ### Arguments
1110
+ //
1111
+ // * `new_location` A new location string (e.g. '#/')
1112
+ //
1113
+ setLocation: function(new_location) {
1114
+ return this._location_proxy.setLocation(new_location);
1115
+ },
1116
+
1117
+ // Swaps the content of `$element()` with `content`
1118
+ // You can override this method to provide an alternate swap behavior
1119
+ // for `EventContext.partial()`.
1120
+ //
1121
+ // ### Example
1122
+ //
1123
+ // var app = $.sammy(function() {
1124
+ //
1125
+ // // implements a 'fade out'/'fade in'
1126
+ // this.swap = function(content) {
1127
+ // this.$element().hide('slow').html(content).show('slow');
1128
+ // }
1129
+ //
1130
+ // get('#/', function() {
1131
+ // this.partial('index.html.erb') // will fade out and in
1132
+ // });
1133
+ //
1134
+ // });
1135
+ //
1136
+ swap: function(content) {
1137
+ return this.$element().html(content);
1138
+ },
1139
+
1140
+ // a simple global cache for templates. Uses the same semantics as
1141
+ // `Sammy.Cache` and `Sammy.Storage` so can easily be replaced with
1142
+ // a persistant storage that lasts beyond the current request.
1143
+ templateCache: function(key, value) {
1144
+ if (typeof value != 'undefined') {
1145
+ return _template_cache[key] = value;
1146
+ } else {
1147
+ return _template_cache[key];
1148
+ }
1149
+ },
1150
+
1151
+ // clear the templateCache
1152
+ clearTemplateCache: function() {
1153
+ return _template_cache = {};
1154
+ },
1155
+
1156
+ // This thows a '404 Not Found' error by invoking `error()`.
1157
+ // Override this method or `error()` to provide custom
1158
+ // 404 behavior (i.e redirecting to / or showing a warning)
1159
+ notFound: function(verb, path) {
1160
+ var ret = this.error(['404 Not Found', verb, path].join(' '));
1161
+ return (verb === 'get') ? ret : true;
1162
+ },
1163
+
1164
+ // The base error handler takes a string `message` and an `Error`
1165
+ // object. If `raise_errors` is set to `true` on the app level,
1166
+ // this will re-throw the error to the browser. Otherwise it will send the error
1167
+ // to `log()`. Override this method to provide custom error handling
1168
+ // e.g logging to a server side component or displaying some feedback to the
1169
+ // user.
1170
+ error: function(message, original_error) {
1171
+ if (!original_error) { original_error = new Error(); }
1172
+ original_error.message = [message, original_error.message].join(' ');
1173
+ this.trigger('error', {message: original_error.message, error: original_error});
1174
+ if (this.raise_errors) {
1175
+ throw(original_error);
1176
+ } else {
1177
+ this.log(original_error.message, original_error);
1178
+ }
1179
+ },
1180
+
1181
+ _checkLocation: function() {
1182
+ var location, returned;
1183
+ // get current location
1184
+ location = this.getLocation();
1185
+ // compare to see if hash has changed
1186
+ if (!this.last_location || this.last_location[0] != 'get' || this.last_location[1] != location) {
1187
+ // reset last location
1188
+ this.last_location = ['get', location];
1189
+ // lookup route for current hash
1190
+ returned = this.runRoute('get', location);
1191
+ }
1192
+ return returned;
1193
+ },
1194
+
1195
+ _getFormVerb: function(form) {
1196
+ var $form = $(form), verb, $_method;
1197
+ $_method = $form.find('input[name="_method"]');
1198
+ if ($_method.length > 0) { verb = $_method.val(); }
1199
+ if (!verb) { verb = $form[0].getAttribute('method'); }
1200
+ return $.trim(verb.toString().toLowerCase());
1201
+ },
1202
+
1203
+ _checkFormSubmission: function(form) {
1204
+ var $form, path, verb, params, returned;
1205
+ this.trigger('check-form-submission', {form: form});
1206
+ $form = $(form);
1207
+ path = $form.attr('action');
1208
+ verb = this._getFormVerb($form);
1209
+ if (!verb || verb == '') { verb = 'get'; }
1210
+ this.log('_checkFormSubmission', $form, path, verb);
1211
+ if (verb === 'get') {
1212
+ this.setLocation(path + '?' + this._serializeFormParams($form));
1213
+ returned = false;
1214
+ } else {
1215
+ params = $.extend({}, this._parseFormParams($form));
1216
+ returned = this.runRoute(verb, path, params, form.get(0));
1217
+ };
1218
+ return (typeof returned == 'undefined') ? false : returned;
1219
+ },
1220
+
1221
+ _serializeFormParams: function($form) {
1222
+ var queryString = "",
1223
+ fields = $form.serializeArray(),
1224
+ i;
1225
+ if (fields.length > 0) {
1226
+ queryString = this._encodeFormPair(fields[0].name, fields[0].value);
1227
+ for (i = 1; i < fields.length; i++) {
1228
+ queryString = queryString + "&" + this._encodeFormPair(fields[i].name, fields[i].value);
1229
+ }
1230
+ }
1231
+ return queryString;
1232
+ },
1233
+
1234
+ _encodeFormPair: function(name, value){
1235
+ return _encode(name) + "=" + _encode(value);
1236
+ },
1237
+
1238
+ _parseFormParams: function($form) {
1239
+ var params = {},
1240
+ form_fields = $form.serializeArray(),
1241
+ i;
1242
+ for (i = 0; i < form_fields.length; i++) {
1243
+ params = this._parseParamPair(params, form_fields[i].name, form_fields[i].value);
1244
+ }
1245
+ return params;
1246
+ },
1247
+
1248
+ _parseQueryString: function(path) {
1249
+ var params = {}, parts, pairs, pair, i;
1250
+
1251
+ parts = path.match(QUERY_STRING_MATCHER);
1252
+ if (parts) {
1253
+ pairs = parts[1].split('&');
1254
+ for (i = 0; i < pairs.length; i++) {
1255
+ pair = pairs[i].split('=');
1256
+ params = this._parseParamPair(params, _decode(pair[0]), _decode(pair[1]));
1257
+ }
1258
+ }
1259
+ return params;
1260
+ },
1261
+
1262
+ _parseParamPair: function(params, key, value) {
1263
+ if (params[key]) {
1264
+ if (_isArray(params[key])) {
1265
+ params[key].push(value);
1266
+ } else {
1267
+ params[key] = [params[key], value];
1268
+ }
1269
+ } else {
1270
+ params[key] = value;
1271
+ }
1272
+ return params;
1273
+ },
1274
+
1275
+ _listen: function(name, callback) {
1276
+ return this.$element().bind([name, this.eventNamespace()].join('.'), callback);
1277
+ },
1278
+
1279
+ _unlisten: function(name, callback) {
1280
+ return this.$element().unbind([name, this.eventNamespace()].join('.'), callback);
1281
+ }
1282
+
1283
+ });
1284
+
1285
+ // `Sammy.RenderContext` is an object that makes sequential template loading,
1286
+ // rendering and interpolation seamless even when dealing with asyncronous
1287
+ // operations.
1288
+ //
1289
+ // `RenderContext` objects are not usually created directly, rather they are
1290
+ // instatiated from an `Sammy.EventContext` by using `render()`, `load()` or
1291
+ // `partial()` which all return `RenderContext` objects.
1292
+ //
1293
+ // `RenderContext` methods always returns a modified `RenderContext`
1294
+ // for chaining (like jQuery itself).
1295
+ //
1296
+ // The core magic is in the `then()` method which puts the callback passed as
1297
+ // an argument into a queue to be executed once the previous callback is complete.
1298
+ // All the methods of `RenderContext` are wrapped in `then()` which allows you
1299
+ // to queue up methods by chaining, but maintaing a guarunteed execution order
1300
+ // even with remote calls to fetch templates.
1301
+ //
1302
+ Sammy.RenderContext = function(event_context) {
1303
+ this.event_context = event_context;
1304
+ this.callbacks = [];
1305
+ this.previous_content = null;
1306
+ this.content = null;
1307
+ this.next_engine = false;
1308
+ this.waiting = false;
1309
+ };
1310
+
1311
+ Sammy.RenderContext.prototype = $.extend({}, Sammy.Object.prototype, {
1312
+
1313
+ // The "core" of the `RenderContext` object, adds the `callback` to the
1314
+ // queue. If the context is `waiting` (meaning an async operation is happening)
1315
+ // then the callback will be executed in order, once the other operations are
1316
+ // complete. If there is no currently executing operation, the `callback`
1317
+ // is executed immediately.
1318
+ //
1319
+ // The value returned from the callback is stored in `content` for the
1320
+ // subsiquent operation. If you return `false`, the queue will pause, and
1321
+ // the next callback in the queue will not be executed until `next()` is
1322
+ // called. This allows for the guarunteed order of execution while working
1323
+ // with async operations.
1324
+ //
1325
+ // If then() is passed a string instead of a function, the string is looked
1326
+ // up as a helper method on the event context.
1327
+ //
1328
+ // ### Example
1329
+ //
1330
+ // this.get('#/', function() {
1331
+ // // initialize the RenderContext
1332
+ // // Even though `load()` executes async, the next `then()`
1333
+ // // wont execute until the load finishes
1334
+ // this.load('myfile.txt')
1335
+ // .then(function(content) {
1336
+ // // the first argument to then is the content of the
1337
+ // // prev operation
1338
+ // $('#main').html(content);
1339
+ // });
1340
+ // });
1341
+ //
1342
+ then: function(callback) {
1343
+ if (!_isFunction(callback)) {
1344
+ // if a string is passed to then, assume we want to call
1345
+ // a helper on the event context in its context
1346
+ if (typeof callback === 'string' && callback in this.event_context) {
1347
+ var helper = this.event_context[callback];
1348
+ callback = function(content) {
1349
+ return helper.apply(this.event_context, [content]);
1350
+ };
1351
+ } else {
1352
+ return this;
1353
+ }
1354
+ }
1355
+ var context = this;
1356
+ if (this.waiting) {
1357
+ this.callbacks.push(callback);
1358
+ } else {
1359
+ this.wait();
1360
+ window.setTimeout(function() {
1361
+ var returned = callback.apply(context, [context.content, context.previous_content]);
1362
+ if (returned !== false) {
1363
+ context.next(returned);
1364
+ }
1365
+ }, 13);
1366
+ }
1367
+ return this;
1368
+ },
1369
+
1370
+ // Pause the `RenderContext` queue. Combined with `next()` allows for async
1371
+ // operations.
1372
+ //
1373
+ // ### Example
1374
+ //
1375
+ // this.get('#/', function() {
1376
+ // this.load('mytext.json')
1377
+ // .then(function(content) {
1378
+ // var context = this,
1379
+ // data = JSON.parse(content);
1380
+ // // pause execution
1381
+ // context.wait();
1382
+ // // post to a url
1383
+ // $.post(data.url, {}, function(response) {
1384
+ // context.next(JSON.parse(response));
1385
+ // });
1386
+ // })
1387
+ // .then(function(data) {
1388
+ // // data is json from the previous post
1389
+ // $('#message').text(data.status);
1390
+ // });
1391
+ // });
1392
+ wait: function() {
1393
+ this.waiting = true;
1394
+ },
1395
+
1396
+ // Resume the queue, setting `content` to be used in the next operation.
1397
+ // See `wait()` for an example.
1398
+ next: function(content) {
1399
+ this.waiting = false;
1400
+ if (typeof content !== 'undefined') {
1401
+ this.previous_content = this.content;
1402
+ this.content = content;
1403
+ }
1404
+ if (this.callbacks.length > 0) {
1405
+ this.then(this.callbacks.shift());
1406
+ }
1407
+ },
1408
+
1409
+ // Load a template into the context.
1410
+ // The `location` can either be a string specifiying the remote path to the
1411
+ // file, a jQuery object, or a DOM element.
1412
+ //
1413
+ // No interpolation happens by default, the content is stored in
1414
+ // `content`.
1415
+ //
1416
+ // In the case of a path, unless the option `{cache: false}` is passed the
1417
+ // data is stored in the app's `templateCache()`.
1418
+ //
1419
+ // If a jQuery or DOM object is passed the `innerHTML` of the node is pulled in.
1420
+ // This is useful for nesting templates as part of the initial page load wrapped
1421
+ // in invisible elements or `<script>` tags. With template paths, the template
1422
+ // engine is looked up by the extension. For DOM/jQuery embedded templates,
1423
+ // this isnt possible, so there are a couple of options:
1424
+ //
1425
+ // * pass an `{engine:}` option.
1426
+ // * define the engine in the `data-engine` attribute of the passed node.
1427
+ // * just store the raw template data and use `interpolate()` manually
1428
+ //
1429
+ // If a `callback` is passed it is executed after the template load.
1430
+ load: function(location, options, callback) {
1431
+ var context = this;
1432
+ return this.then(function() {
1433
+ var should_cache, cached, is_json, location_array;
1434
+ if (_isFunction(options)) {
1435
+ callback = options;
1436
+ options = {};
1437
+ } else {
1438
+ options = $.extend({}, options);
1439
+ }
1440
+ if (callback) { this.then(callback); }
1441
+ if (typeof location === 'string') {
1442
+ // its a path
1443
+ is_json = (location.match(/\.json$/) || options.json);
1444
+ should_cache = ((is_json && options.cache === true) || options.cache !== false);
1445
+ context.next_engine = context.event_context.engineFor(location);
1446
+ delete options.cache;
1447
+ delete options.json;
1448
+ if (options.engine) {
1449
+ context.next_engine = options.engine;
1450
+ delete options.engine;
1451
+ }
1452
+ if (should_cache && (cached = this.event_context.app.templateCache(location))) {
1453
+ return cached;
1454
+ }
1455
+ this.wait();
1456
+ $.ajax($.extend({
1457
+ url: location,
1458
+ data: {},
1459
+ dataType: is_json ? 'json' : null,
1460
+ type: 'get',
1461
+ success: function(data) {
1462
+ if (should_cache) {
1463
+ context.event_context.app.templateCache(location, data);
1464
+ }
1465
+ context.next(data);
1466
+ }
1467
+ }, options));
1468
+ return false;
1469
+ } else {
1470
+ // its a dom/jQuery
1471
+ if (location.nodeType) {
1472
+ return location.innerHTML;
1473
+ }
1474
+ if (location.selector) {
1475
+ // its a jQuery
1476
+ context.next_engine = location.attr('data-engine');
1477
+ if (options.clone === false) {
1478
+ return location.remove()[0].innerHTML.toString();
1479
+ } else {
1480
+ return location[0].innerHTML.toString();
1481
+ }
1482
+ }
1483
+ }
1484
+ });
1485
+ },
1486
+
1487
+ // `load()` a template and then `interpolate()` it with data.
1488
+ //
1489
+ // ### Example
1490
+ //
1491
+ // this.get('#/', function() {
1492
+ // this.render('mytemplate.template', {name: 'test'});
1493
+ // });
1494
+ //
1495
+ render: function(location, data, callback) {
1496
+ if (_isFunction(location) && !data) {
1497
+ return this.then(location);
1498
+ } else {
1499
+ if (!data && this.content) { data = this.content; }
1500
+ return this.load(location)
1501
+ .interpolate(data, location)
1502
+ .then(callback);
1503
+ }
1504
+ },
1505
+
1506
+ // `render()` the the `location` with `data` and then `swap()` the
1507
+ // app's `$element` with the rendered content.
1508
+ partial: function(location, data) {
1509
+ return this.render(location, data).swap();
1510
+ },
1511
+
1512
+ // defers the call of function to occur in order of the render queue.
1513
+ // The function can accept any number of arguments as long as the last
1514
+ // argument is a callback function. This is useful for putting arbitrary
1515
+ // asynchronous functions into the queue. The content passed to the
1516
+ // callback is passed as `content` to the next item in the queue.
1517
+ //
1518
+ // === Example
1519
+ //
1520
+ // this.send($.getJSON, '/app.json')
1521
+ // .then(function(json) {
1522
+ // $('#message).text(json['message']);
1523
+ // });
1524
+ //
1525
+ //
1526
+ send: function() {
1527
+ var context = this,
1528
+ args = _makeArray(arguments),
1529
+ fun = args.shift();
1530
+
1531
+ if (_isArray(args[0])) { args = args[0]; }
1532
+
1533
+ return this.then(function(content) {
1534
+ args.push(function(response) { context.next(response); });
1535
+ context.wait();
1536
+ fun.apply(fun, args);
1537
+ return false;
1538
+ });
1539
+ },
1540
+
1541
+ // itterates over an array, applying the callback for each item item. the
1542
+ // callback takes the same style of arguments as `jQuery.each()` (index, item).
1543
+ // The return value of each callback is collected as a single string and stored
1544
+ // as `content` to be used in the next iteration of the `RenderContext`.
1545
+ collect: function(array, callback, now) {
1546
+ var context = this;
1547
+ var coll = function() {
1548
+ if (_isFunction(array)) {
1549
+ callback = array;
1550
+ array = this.content;
1551
+ }
1552
+ var contents = [], doms = false;
1553
+ $.each(array, function(i, item) {
1554
+ var returned = callback.apply(context, [i, item]);
1555
+ if (returned.jquery && returned.length == 1) {
1556
+ returned = returned[0];
1557
+ doms = true;
1558
+ }
1559
+ contents.push(returned);
1560
+ return returned;
1561
+ });
1562
+ return doms ? contents : contents.join('');
1563
+ };
1564
+ return now ? coll() : this.then(coll);
1565
+ },
1566
+
1567
+ // loads a template, and then interpolates it for each item in the `data`
1568
+ // array. If a callback is passed, it will call the callback with each
1569
+ // item in the array _after_ interpolation
1570
+ renderEach: function(location, name, data, callback) {
1571
+ if (_isArray(name)) {
1572
+ callback = data;
1573
+ data = name;
1574
+ name = null;
1575
+ }
1576
+ return this.load(location).then(function(content) {
1577
+ var rctx = this;
1578
+ if (!data) {
1579
+ data = _isArray(this.previous_content) ? this.previous_content : [];
1580
+ }
1581
+ if (callback) {
1582
+ $.each(data, function(i, value) {
1583
+ var idata = {}, engine = this.next_engine || location;
1584
+ name ? (idata[name] = value) : (idata = value);
1585
+ callback(value, rctx.event_context.interpolate(content, idata, engine));
1586
+ });
1587
+ } else {
1588
+ return this.collect(data, function(i, value) {
1589
+ var idata = {}, engine = this.next_engine || location;
1590
+ name ? (idata[name] = value) : (idata = value);
1591
+ return this.event_context.interpolate(content, idata, engine);
1592
+ }, true);
1593
+ }
1594
+ });
1595
+ },
1596
+
1597
+ // uses the previous loaded `content` and the `data` object to interpolate
1598
+ // a template. `engine` defines the templating/interpolation method/engine
1599
+ // that should be used. If `engine` is not passed, the `next_engine` is
1600
+ // used. If `retain` is `true`, the final interpolated data is appended to
1601
+ // the `previous_content` instead of just replacing it.
1602
+ interpolate: function(data, engine, retain) {
1603
+ var context = this;
1604
+ return this.then(function(content, prev) {
1605
+ if (!data && prev) { data = prev; }
1606
+ if (this.next_engine) {
1607
+ engine = this.next_engine;
1608
+ this.next_engine = false;
1609
+ }
1610
+ var rendered = context.event_context.interpolate(content, data, engine);
1611
+ return retain ? prev + rendered : rendered;
1612
+ });
1613
+ },
1614
+
1615
+ // executes `EventContext#swap()` with the `content`
1616
+ swap: function() {
1617
+ return this.then(function(content) {
1618
+ this.event_context.swap(content);
1619
+ }).trigger('changed', {});
1620
+ },
1621
+
1622
+ // Same usage as `jQuery.fn.appendTo()` but uses `then()` to ensure order
1623
+ appendTo: function(selector) {
1624
+ return this.then(function(content) {
1625
+ $(selector).append(content);
1626
+ }).trigger('changed', {});
1627
+ },
1628
+
1629
+ // Same usage as `jQuery.fn.prependTo()` but uses `then()` to ensure order
1630
+ prependTo: function(selector) {
1631
+ return this.then(function(content) {
1632
+ $(selector).prepend(content);
1633
+ }).trigger('changed', {});
1634
+ },
1635
+
1636
+ // Replaces the `$(selector)` using `html()` with the previously loaded
1637
+ // `content`
1638
+ replace: function(selector) {
1639
+ return this.then(function(content) {
1640
+ $(selector).html(content);
1641
+ }).trigger('changed', {});
1642
+ },
1643
+
1644
+ // trigger the event in the order of the event context. Same semantics
1645
+ // as `Sammy.EventContext#trigger()`. If data is ommitted, `content`
1646
+ // is sent as `{content: content}`
1647
+ trigger: function(name, data) {
1648
+ return this.then(function(content) {
1649
+ if (typeof data == 'undefined') { data = {content: content}; }
1650
+ this.event_context.trigger(name, data);
1651
+ });
1652
+ }
1653
+
1654
+ });
1655
+
1656
+ // `Sammy.EventContext` objects are created every time a route is run or a
1657
+ // bound event is triggered. The callbacks for these events are evaluated within a `Sammy.EventContext`
1658
+ // This within these callbacks the special methods of `EventContext` are available.
1659
+ //
1660
+ // ### Example
1661
+ //
1662
+ // $.sammy(function() {
1663
+ // // The context here is this Sammy.Application
1664
+ // this.get('#/:name', function() {
1665
+ // // The context here is a new Sammy.EventContext
1666
+ // if (this.params['name'] == 'sammy') {
1667
+ // this.partial('name.html.erb', {name: 'Sammy'});
1668
+ // } else {
1669
+ // this.redirect('#/somewhere-else')
1670
+ // }
1671
+ // });
1672
+ // });
1673
+ //
1674
+ // Initialize a new EventContext
1675
+ //
1676
+ // ### Arguments
1677
+ //
1678
+ // * `app` The `Sammy.Application` this event is called within.
1679
+ // * `verb` The verb invoked to run this context/route.
1680
+ // * `path` The string path invoked to run this context/route.
1681
+ // * `params` An Object of optional params to pass to the context. Is converted
1682
+ // to a `Sammy.Object`.
1683
+ // * `target` a DOM element that the event that holds this context originates
1684
+ // from. For post, put and del routes, this is the form element that triggered
1685
+ // the route.
1686
+ //
1687
+ Sammy.EventContext = function(app, verb, path, params, target) {
1688
+ this.app = app;
1689
+ this.verb = verb;
1690
+ this.path = path;
1691
+ this.params = new Sammy.Object(params);
1692
+ this.target = target;
1693
+ };
1694
+
1695
+ Sammy.EventContext.prototype = $.extend({}, Sammy.Object.prototype, {
1696
+
1697
+ // A shortcut to the app's `$element()`
1698
+ $element: function() {
1699
+ return this.app.$element(_makeArray(arguments).shift());
1700
+ },
1701
+
1702
+ // Look up a templating engine within the current app and context.
1703
+ // `engine` can be one of the following:
1704
+ //
1705
+ // * a function: should conform to `function(content, data) { return interploated; }`
1706
+ // * a template path: 'template.ejs', looks up the extension to match to
1707
+ // the `ejs()` helper
1708
+ // * a string referering to the helper: "mustache" => `mustache()`
1709
+ //
1710
+ // If no engine is found, use the app's default `template_engine`
1711
+ //
1712
+ engineFor: function(engine) {
1713
+ var context = this, engine_match;
1714
+ // if path is actually an engine function just return it
1715
+ if (_isFunction(engine)) { return engine; }
1716
+ // lookup engine name by path extension
1717
+ engine = (engine || context.app.template_engine).toString();
1718
+ if ((engine_match = engine.match(/\.([^\.]+)$/))) {
1719
+ engine = engine_match[1];
1720
+ }
1721
+ // set the engine to the default template engine if no match is found
1722
+ if (engine && _isFunction(context[engine])) {
1723
+ return context[engine];
1724
+ }
1725
+
1726
+ if (context.app.template_engine) {
1727
+ return this.engineFor(context.app.template_engine);
1728
+ }
1729
+ return function(content, data) { return content; };
1730
+ },
1731
+
1732
+ // using the template `engine` found with `engineFor()`, interpolate the
1733
+ // `data` into `content`
1734
+ interpolate: function(content, data, engine) {
1735
+ return this.engineFor(engine).apply(this, [content, data]);
1736
+ },
1737
+
1738
+ // Create and return a `Sammy.RenderContext` calling `render()` on it.
1739
+ // Loads the template and interpolate the data, however does not actual
1740
+ // place it in the DOM.
1741
+ //
1742
+ // ### Example
1743
+ //
1744
+ // // mytemplate.mustache <div class="name">{{name}}</div>
1745
+ // render('mytemplate.mustache', {name: 'quirkey'});
1746
+ // // sets the `content` to <div class="name">quirkey</div>
1747
+ // render('mytemplate.mustache', {name: 'quirkey'})
1748
+ // .appendTo('ul');
1749
+ // // appends the rendered content to $('ul')
1750
+ //
1751
+ render: function(location, data, callback) {
1752
+ return new Sammy.RenderContext(this).render(location, data, callback);
1753
+ },
1754
+
1755
+ // Create and return a `Sammy.RenderContext` calling `renderEach()` on it.
1756
+ // Loads the template and interpolates the data for each item,
1757
+ // however does not actual place it in the DOM.
1758
+ //
1759
+ // ### Example
1760
+ //
1761
+ // // mytemplate.mustache <div class="name">{{name}}</div>
1762
+ // renderEach('mytemplate.mustache', [{name: 'quirkey'}, {name: 'endor'}])
1763
+ // // sets the `content` to <div class="name">quirkey</div><div class="name">endor</div>
1764
+ // renderEach('mytemplate.mustache', [{name: 'quirkey'}, {name: 'endor'}]).appendTo('ul');
1765
+ // // appends the rendered content to $('ul')
1766
+ //
1767
+ renderEach: function(location, name, data, callback) {
1768
+ return new Sammy.RenderContext(this).renderEach(location, name, data, callback);
1769
+ },
1770
+
1771
+ // create a new `Sammy.RenderContext` calling `load()` with `location` and
1772
+ // `options`. Called without interpolation or placement, this allows for
1773
+ // preloading/caching the templates.
1774
+ load: function(location, options, callback) {
1775
+ return new Sammy.RenderContext(this).load(location, options, callback);
1776
+ },
1777
+
1778
+ // `render()` the the `location` with `data` and then `swap()` the
1779
+ // app's `$element` with the rendered content.
1780
+ partial: function(location, data) {
1781
+ return new Sammy.RenderContext(this).partial(location, data);
1782
+ },
1783
+
1784
+ // create a new `Sammy.RenderContext` calling `send()` with an arbitrary
1785
+ // function
1786
+ send: function() {
1787
+ var rctx = new Sammy.RenderContext(this);
1788
+ return rctx.send.apply(rctx, arguments);
1789
+ },
1790
+
1791
+ // Changes the location of the current window. If `to` begins with
1792
+ // '#' it only changes the document's hash. If passed more than 1 argument
1793
+ // redirect will join them together with forward slashes.
1794
+ //
1795
+ // ### Example
1796
+ //
1797
+ // redirect('#/other/route');
1798
+ // // equivilent to
1799
+ // redirect('#', 'other', 'route');
1800
+ //
1801
+ redirect: function() {
1802
+ var to, args = _makeArray(arguments),
1803
+ current_location = this.app.getLocation();
1804
+ if (args.length > 1) {
1805
+ args.unshift('/');
1806
+ to = this.join.apply(this, args);
1807
+ } else {
1808
+ to = args[0];
1809
+ }
1810
+ this.trigger('redirect', {to: to});
1811
+ this.app.last_location = [this.verb, this.path];
1812
+ this.app.setLocation(to);
1813
+ if (current_location == to) {
1814
+ this.app.trigger('location-changed');
1815
+ }
1816
+ },
1817
+
1818
+ // Triggers events on `app` within the current context.
1819
+ trigger: function(name, data) {
1820
+ if (typeof data == 'undefined') { data = {}; }
1821
+ if (!data.context) { data.context = this; }
1822
+ return this.app.trigger(name, data);
1823
+ },
1824
+
1825
+ // A shortcut to app's `eventNamespace()`
1826
+ eventNamespace: function() {
1827
+ return this.app.eventNamespace();
1828
+ },
1829
+
1830
+ // A shortcut to app's `swap()`
1831
+ swap: function(contents) {
1832
+ return this.app.swap(contents);
1833
+ },
1834
+
1835
+ // Raises a possible `notFound()` error for the current path.
1836
+ notFound: function() {
1837
+ return this.app.notFound(this.verb, this.path);
1838
+ },
1839
+
1840
+ // Default JSON parsing uses jQuery's `parseJSON()`. Include `Sammy.JSON`
1841
+ // plugin for the more conformant "crockford special".
1842
+ json: function(string) {
1843
+ return $.parseJSON(string);
1844
+ },
1845
+
1846
+ // //=> Sammy.EventContext: get #/ {}
1847
+ toString: function() {
1848
+ return "Sammy.EventContext: " + [this.verb, this.path, this.params].join(' ');
1849
+ }
1850
+
1851
+ });
1852
+
1853
+ // An alias to Sammy
1854
+ $.sammy = window.Sammy = Sammy;
1855
+
1856
+ })(jQuery, window);