Commits

Ian Bicking committed ececa80

Moved to a jQuery/Sammy/PURE system instead of rendering server-side templates

  • Participants
  • Parent commits c705522

Comments (0)

Files changed (11)

File silverlog/__init__.py

 import os
 import re
+import json
 import tempita
 from webob.dec import wsgify
 from webob import exc
 class Application(object):
 
     map = Mapper()
-    map.connect('index', '/', method='index')
-    map.connect('view', '/view/{group}', method='view')
-    template_base = os.path.join(os.path.dirname(__file__), 'templates')
+    map.connect('list_logs', '/api/list-logs', method='list_logs')
+    map.connect('log_view', '/api/log/{id}', method='log_view')
 
     def __init__(self, dirs=None, template_base=None):
         if template_base:
         self.log_set = LogSet(dirs)
         self.log_set.read()
 
-    def render(self, template_name, req, title, **kw):
-        kw['req'] = req
-        kw['title'] = title
-        kw['app'] = self
-        kw['log_set'] = self.log_set
-        tmpl = tempita.HTMLTemplate.from_filename(
-            os.path.join(self.template_base, template_name + '.html'),
-            default_inherit=os.path.join(self.template_base, 'base.html'))
-        return tmpl.substitute(kw)
-
     @wsgify
     def __call__(self, req):
         results = self.map.routematch(environ=req.environ)
+        print results, req.path_info
         if not results:
             return exc.HTTPNotFound()
         match, route = results
         req.link = link
         return getattr(self, method)(req, **kwargs)
 
-    def index(self, req):
-        return Response(
-            self.render('index', req, 'Index of logs'))
+    def list_logs(self, req):
+        result = {}
+        items = result['logs'] = []
+        for path, logs in sorted(self.log_set.logs.iteritems()):
+            for log in logs.values():
+                items.append(dict(path=path, group=log.group,
+                                  id=log.id, description=log.description))
+        return json_response(result)
 
-    def view(self, req, group):
-        path = req.GET['path']
-        log = self.log_set.logs[group][path]
-        return Response(
-            self.render('view', req, 'Viewing %s' % log.description,
-                        log=log))
+    def log_view(self, req, id):
+        log = self.log_set.log_from_id(id)
+        result = dict(
+            path=log.path, group=log.group,
+            id=log.id, description=log.description,
+            content=log.content())
+        return json_response(result)
 
 NAMES = [
     (r'^SILVER_DIR/apps/(?P<app>[^/]+)/stderr.log$',
                 ## FIXME: should log something about ignoring the log
                 self.skipped_files.append(filename)
 
+    def log_from_id(self, id):
+        path = '/' + id.replace('_', '/')
+        for group, items in self.logs.iteritems():
+            if path in items:
+                return items[path]
+        raise LookupError("No log with the path %r" % path)
+
 def walk_files(dirs):
     for dir in dirs:
         for dirpath, dirnames, filenames in os.walk(dir):
         self.path = path
         self.group = group
         self.description = description
-        
+
     def __repr__(self):
         return '<%s %s: %r>' % (
             self.__class__.__name__,
         c = fp.read()
         fp.close()
         return c
+
+    @property
+    def id(self):
+        id = self.path.replace('/', '_').strip('_')
+        return id
+
+def json_response(data):
+    return Response(json.dumps(data),
+                    content_type='application/json')

File silverlog/static/index.html

+<html>
+<head>
+<link href="/style.css" type="text/css" rel="stylesheet" />
+<script
+src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+<script src="/sammy.js"></script>
+<script src="/script.js"></script>
+<script src="/pure.js"></script>
+<title>Silver Logs</title>
+</head>
+<body>
+
+<div id="header">
+  <h1><a href="#/">Silver Logs:</a>
+  <span id="title-slot"></span></h1>
+</div>
+
+<div id="body">
+
+<div id="main" style="display: none">
+  <ul id="log-list">
+    <li class="logs"><a class="description link@href"
+     href="">description</a></li>
+  </ul>
+</div>
+
+<div id="log-view" style="display: none">
+  <h2 class="description">description</h2>
+  <pre class="content"></pre>
+</div>
+
+</div><!-- /#body -->
+
+</body>
+</html>

File silverlog/static/pure.js

+/*!
+	PURE Unobtrusive Rendering Engine for HTML
+
+	Licensed under the MIT licenses.
+	More information at: http://www.opensource.org
+
+	Copyright (c) 2010 Michael Cvilic - BeeBole.com
+
+	Thanks to Rog Peppe for the functional JS jump
+	revision: 2.42
+*/
+
+var $p, pure = $p = function(){
+	var sel = arguments[0], 
+		ctxt = false;
+
+	if(typeof sel === 'string'){
+		ctxt = arguments[1] || false;
+	}
+	return $p.core(sel, ctxt);
+};
+
+$p.core = function(sel, ctxt, plugins){
+	//get an instance of the plugins
+	var plugins = getPlugins(),
+		templates = [];
+
+	//search for the template node(s)
+	switch(typeof sel){
+		case 'string':
+			templates = plugins.find(ctxt || document, sel);
+			if(templates.length === 0) {
+				error('The template "' + sel + '" was not found');
+			}
+		break;
+		case 'undefined':
+			error('The template root is undefined, check your selector');
+		break;
+		default:
+			templates = [sel];
+	}
+	
+	for(var i = 0, ii = templates.length; i < ii; i++){
+		plugins[i] = templates[i];
+	}
+	plugins.length = ii;
+
+	// set the signature string that will be replaced at render time
+	var Sig = '_s' + Math.floor( Math.random() * 1000000 ) + '_',
+		// another signature to prepend to attributes and avoid checks: style, height, on[events]...
+		attPfx = '_a' + Math.floor( Math.random() * 1000000 ) + '_',
+		// rx to parse selectors, e.g. "+tr.foo[class]"
+		selRx = /^(\+)?([^\@\+]+)?\@?([^\+]+)?(\+)?$/,
+		// set automatically attributes for some tags
+		autoAttr = {
+			IMG:'src',
+			INPUT:'value'
+		};
+	
+	return plugins;
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * *
+		core functions
+	 * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+	// error utility
+	function error(e){
+		if(typeof console !== 'undefined'){
+			console.log(e);
+			debugger;
+		}else{ alert(e); }
+		throw('pure error: ' + e);
+	}
+	
+	//return a new instance of plugins
+	function getPlugins(){
+		var plugins = $p.plugins,
+			f = function(){};
+		f.prototype = plugins;
+
+		// do not overwrite functions if external definition
+		f.prototype.compile    = plugins.compile || compile;
+		f.prototype.render     = plugins.render || render;
+		f.prototype.autoRender = plugins.autoRender || autoRender;
+		f.prototype.find       = plugins.find || find;
+		
+		// give the compiler and the error handling to the plugin context
+		f.prototype._compiler  = compiler;
+		f.prototype._error     = error;
+ 
+		return new f();
+	}
+	
+	// returns the outer HTML of a node
+	function outerHTML(node){
+		// if IE take the internal method otherwise build one
+		return node.outerHTML || (
+			function(n){
+        		var div = document.createElement('div'), h;
+	        	div.appendChild( n.cloneNode(true) );
+				h = div.innerHTML;
+				div = null;
+				return h;
+			})(node);
+	}
+
+	// check if the argument is an array
+	function isArray(o){
+		return Object.prototype.toString.call( o ) === "[object Array]";
+	}
+	
+	// returns the string generator function
+	function wrapquote(qfn, f){
+		return function(ctxt){
+			return qfn('' + f.call(ctxt.context, ctxt));
+		};
+	}
+
+	// default find using querySelector when available on the browser
+	function find(n, sel){
+		if(typeof n === 'string'){
+			sel = n;
+			n = false;
+		}
+		if(typeof document.querySelectorAll !== 'undefined'){
+			return (n||document).querySelectorAll( sel );
+		}else{
+			error('You can test PURE standalone with: iPhone, FF3.5+, Safari4+ and IE8+\n\nTo run PURE on your browser, you need a JS library/framework with a CSS selector engine');
+		}
+	}
+	
+	// create a function that concatenates constant string
+	// sections (given in parts) and the results of called
+	// functions to fill in the gaps between parts (fns).
+	// fns[n] fills in the gap between parts[n-1] and parts[n];
+	// fns[0] is unused.
+	// this is the inner template evaluation loop.
+	function concatenator(parts, fns){
+		return function(ctxt){
+			var strs = [ parts[ 0 ] ],
+				n = parts.length,
+				fnVal, pVal, attLine, pos;
+
+			for(var i = 1; i < n; i++){
+				fnVal = fns[i]( ctxt );
+				pVal = parts[i];
+				
+				// if the value is empty and attribute, remove it
+				if(fnVal === ''){
+					attLine = strs[ strs.length - 1 ];
+					if( ( pos = attLine.search( /[\w]+=\"?$/ ) ) > -1){
+						strs[ strs.length - 1 ] = attLine.substring( 0, pos );
+						pVal = pVal.substr( 1 );
+					}
+				}
+				
+				strs[ strs.length ] = fnVal;
+				strs[ strs.length ] = pVal;
+			}
+			return strs.join('');
+		};
+	}
+
+	// parse and check the loop directive
+	function parseloopspec(p){
+		var m = p.match( /^(\w+)\s*<-\s*(\S+)?$/ );
+		if(m === null){
+			error('bad loop spec: "' + p + '"');
+		}
+		if(m[1] === 'item'){
+			error('"item<-..." is a reserved word for the current running iteration.\n\nPlease choose another name for your loop.');
+		}
+		if( !m[2] || (m[2] && (/context/i).test(m[2]))){ //undefined or space(IE) 
+			m[2] = function(ctxt){return ctxt.context;};
+		}
+		return {name: m[1], sel: m[2]};
+	}
+
+	// parse a data selector and return a function that
+	// can traverse the data accordingly, given a context.
+	function dataselectfn(sel){
+		if(typeof(sel) === 'function'){
+			return sel;
+		}
+		//check for a valid js variable name with hyphen(for properties only) and $
+		var m = sel.match(/^[a-zA-Z$_][\w$]*(\.[\w$-]*[^\.])*$/);
+		if(m === null){
+			var found = false, s = sel, parts = [], pfns = [], i = 0, retStr;
+			// check if literal
+			if(/\'|\"/.test( s.charAt(0) )){
+				if(/\'|\"/.test( s.charAt(s.length-1) )){
+					retStr = s.substring(1, s.length-1);
+					return function(){ return retStr; };
+				}
+			}else{
+				// check if literal + #{var}
+				while((m = s.match(/#\{([^{}]+)\}/)) !== null){
+					found = true;
+					parts[i++] = s.slice(0, m.index);
+					pfns[i] = dataselectfn(m[1]);
+					s = s.slice(m.index + m[0].length, s.length);
+				}
+			}
+			if(!found){
+				error('bad data selector syntax: ' + sel);
+			}
+			parts[i] = s;
+			return concatenator(parts, pfns);
+		}
+		m = sel.split('.');
+		return function(ctxt){
+			var data = ctxt.context;
+			if(!data){
+				return '';
+			}
+			var	v = ctxt[m[0]],
+				i = 0;
+			if(v && v.item){
+				data = v.item;
+				i += 1;
+			}
+			var n = m.length;
+			for(; i < n; i++){
+				if(!data){break;}
+				data = data[m[i]];
+			}
+			return (!data && data !== 0) ? '':data;
+		};
+	}
+
+	// wrap in an object the target node/attr and their properties
+	function gettarget(dom, sel, isloop){
+		var osel, prepend, selector, attr, append, target = [];
+		if( typeof sel === 'string' ){
+			osel = sel;
+			var m = sel.match(selRx);
+			if( !m ){
+				error( 'bad selector syntax: ' + sel );
+			}
+			
+			prepend = m[1];
+			selector = m[2];
+			attr = m[3];
+			append = m[4];
+			
+			if(selector === '.' || ( !selector && attr ) ){
+				target[0] = dom;
+			}else{
+				target = plugins.find(dom, selector);
+			}
+			if(!target || target.length === 0){
+				return error('The node "' + sel + '" was not found in the template');
+			}
+		}else{
+			// autoRender node
+			prepend = sel.prepend;
+			attr = sel.attr;
+			append = sel.append;
+			target = [dom];
+		}
+		
+		if( prepend || append ){
+			if( prepend && append ){
+				error('append/prepend cannot take place at the same time');
+			}else if( isloop ){
+				error('no append/prepend/replace modifiers allowed for loop target');
+			}else if( append && isloop ){
+				error('cannot append with loop (sel: ' + osel + ')');
+			}
+		}
+		var setstr, getstr, quotefn, isStyle, isClass, attName, setfn;
+		if(attr){
+			isStyle = (/^style$/i).test(attr);
+			isClass = (/^class$/i).test(attr);
+			attName = isClass ? 'className' : attr;
+			setstr = function(node, s) {
+				node.setAttribute(attPfx + attr, s);
+				if (attName in node && !isStyle) {
+					node[attName] = '';
+				}
+				if (node.nodeType === 1) {
+					node.removeAttribute(attr);
+					isClass && node.removeAttribute(attName);
+				}
+			};
+			if (isStyle || isClass) {//IE no quotes special care
+				if(isStyle){
+					getstr = function(n){ return n.style.cssText; };
+				}else{
+					getstr = function(n){ return n.className;	};
+				}
+				quotefn = function(s){ return s.replace(/\"/g, '&quot;'); };
+			}else {
+				getstr = function(n){ return n.getAttribute(attr); };
+				quotefn = function(s){ return s.replace(/\"/g, '&quot;').replace(/\s/g, '&nbsp;'); };
+			}
+			if(prepend){
+				setfn = function(node, s){ setstr( node, s + getstr( node )); };
+			}else if(append){
+				setfn = function(node, s){ setstr( node, getstr( node ) + s); };
+			}else{
+				setfn = function(node, s){ setstr( node, s ); };
+			}
+		}else{
+			if (isloop) {
+				setfn = function(node, s) {
+					var pn = node.parentNode;
+					if (pn) {
+						//replace node with s
+						pn.insertBefore(document.createTextNode(s), node.nextSibling);
+						pn.removeChild(node);
+					}
+				};
+			} else {
+				if (prepend) {
+					setfn = function(node, s) { node.insertBefore(document.createTextNode(s), node.firstChild);	};
+				} else if (append) {
+					setfn = function(node, s) { node.appendChild(document.createTextNode(s));};
+				} else {
+					setfn = function(node, s) {
+						while (node.firstChild) { node.removeChild(node.firstChild); }
+						node.appendChild(document.createTextNode(s));
+					};
+				}
+			}
+			quotefn = function(s) { return s; };
+		}
+		return { attr: attr, nodes: target, set: setfn, sel: osel, quotefn: quotefn };
+	}
+
+	function setsig(target, n){
+		var sig = Sig + n + ':';
+		for(var i = 0; i < target.nodes.length; i++){
+			// could check for overlapping targets here.
+			target.set( target.nodes[i], sig );
+		}
+	}
+
+	// read de loop data, and pass it to the inner rendering function
+	function loopfn(name, dselect, inner, sorter, filter){
+		return function(ctxt){
+			var a = dselect(ctxt),
+				old = ctxt[name],
+				temp = { items : a },
+				filtered = 0,
+				length,
+				strs = [],
+				buildArg = function(idx, temp, ftr, len){
+					ctxt.pos = temp.pos = idx;
+					ctxt.item = temp.item = a[ idx ];
+					ctxt.items = a;
+					//if array, set a length property - filtered items
+					typeof len !== 'undefined' &&  (ctxt.length = len);
+					//if filter directive
+					if(typeof ftr === 'function' && !ftr(ctxt)){
+						filtered++;
+						return;
+					}
+					strs.push( inner.call(temp, ctxt ) );
+				};
+			ctxt[name] = temp;
+			if( isArray(a) ){
+				length = a.length || 0;
+				// if sort directive
+				if(typeof sorter === 'function'){
+					a.sort(sorter);
+				}
+				//loop on array
+				for(var i = 0, ii = length; i < ii; i++){
+					buildArg(i, temp, filter, length - filtered);
+				}
+			}else{
+				if(a && typeof sorter !== 'undefined'){
+					error('sort is only available on arrays, not objects');
+				}
+				//loop on collections
+				for(var prop in a){
+					a.hasOwnProperty( prop ) && buildArg(prop, temp, filter);
+				}
+			}
+
+			typeof old !== 'undefined' ? ctxt[name] = old : delete ctxt[name];
+			return strs.join('');
+		};
+	}
+	// generate the template for a loop node
+	function loopgen(dom, sel, loop, fns){
+		var already = false, ls, sorter, filter, prop;
+		for(prop in loop){
+			if(loop.hasOwnProperty(prop)){
+				if(prop === 'sort'){
+					sorter = loop.sort;
+					continue;
+				}else if(prop === 'filter'){
+					filter = loop.filter;
+					continue;
+				}
+				if(already){
+					error('cannot have more than one loop on a target');
+				}
+				ls = prop;
+				already = true;
+			}
+		}
+		if(!ls){
+			error('Error in the selector: ' + sel + '\nA directive action must be a string, a function or a loop(<-)');
+		}
+		var dsel = loop[ls];
+		// if it's a simple data selector then we default to contents, not replacement.
+		if(typeof(dsel) === 'string' || typeof(dsel) === 'function'){
+			loop = {};
+			loop[ls] = {root: dsel};
+			return loopgen(dom, sel, loop, fns);
+		}
+		var spec = parseloopspec(ls),
+			itersel = dataselectfn(spec.sel),
+			target = gettarget(dom, sel, true),
+			nodes = target.nodes;
+			
+		for(i = 0; i < nodes.length; i++){
+			var node = nodes[i],
+				inner = compiler(node, dsel);
+			fns[fns.length] = wrapquote(target.quotefn, loopfn(spec.name, itersel, inner, sorter, filter));
+			target.nodes = [node];		// N.B. side effect on target.
+			setsig(target, fns.length - 1);
+		}
+	}
+	
+	function getAutoNodes(n, data){
+		var ns = n.getElementsByTagName('*'),
+			an = [],
+			openLoops = {a:[],l:{}},
+			cspec,
+			isNodeValue,
+			i, ii, j, jj, ni, cs, cj;
+		//for each node found in the template
+		for(i = -1, ii = ns.length; i < ii; i++){
+			ni = i > -1 ?ns[i]:n;
+			if(ni.nodeType === 1 && ni.className !== ''){
+				//when a className is found
+				cs = ni.className.split(' ');
+				// for each className 
+				for(j = 0, jj=cs.length;j<jj;j++){
+					cj = cs[j];
+					// check if it is related to a context property
+					cspec = checkClass(cj, ni.tagName);
+					// if so, store the node, plus the type of data
+					if(cspec !== false){
+						isNodeValue = (/nodevalue/i).test(cspec.attr);
+						if(cspec.sel.indexOf('@') > -1 || isNodeValue){
+							ni.className = ni.className.replace('@'+cspec.attr, '');
+							if(isNodeValue){
+								cspec.attr = false;
+							} 
+						}
+						an.push({n:ni, cspec:cspec});
+					}
+				}
+			}
+		}
+		return an;
+		
+		function checkClass(c, tagName){
+			// read the class
+			var ca = c.match(selRx),
+				attr = ca[3] || autoAttr[tagName],
+				cspec = {prepend:!!ca[1], prop:ca[2], attr:attr, append:!!ca[4], sel:c},
+				i, ii, loopi, loopil, val;
+			// check in existing open loops
+			for(i = openLoops.a.length-1; i >= 0; i--){
+				loopi = openLoops.a[i];
+				loopil = loopi.l[0];
+				val = loopil && loopil[cspec.prop];
+				if(typeof val !== 'undefined'){
+					cspec.prop = loopi.p + '.' + cspec.prop;
+					if(openLoops.l[cspec.prop] === true){
+						val = val[0];
+					}
+					break;
+				}
+			}
+			// not found check first level of data
+			if(typeof val === 'undefined'){
+				val = isArray(data) ? data[0][cspec.prop] : data[cspec.prop];
+				// nothing found return
+				if(typeof val === 'undefined'){
+					return false;
+				}
+			}
+			// set the spec for autoNode
+			if(isArray(val)){
+				openLoops.a.push( {l:val, p:cspec.prop} );
+				openLoops.l[cspec.prop] = true;
+				cspec.t = 'loop';
+			}else{
+				cspec.t = 'str';
+			}
+			return cspec;
+		}
+	}
+
+	// returns a function that, given a context argument,
+	// will render the template defined by dom and directive.
+	function compiler(dom, directive, data, ans){
+		var fns = [];
+		// autoRendering nodes parsing -> auto-nodes
+		ans = ans || data && getAutoNodes(dom, data);
+		if(data){
+			var j, jj, cspec, n, target, nodes, itersel, node, inner;
+			// for each auto-nodes
+			while(ans.length > 0){
+				cspec = ans[0].cspec;
+				n = ans[0].n;
+				ans.splice(0, 1);
+				if(cspec.t === 'str'){
+					// if the target is a value
+					target = gettarget(n, cspec, false);
+					setsig(target, fns.length);
+					fns[fns.length] = wrapquote(target.quotefn, dataselectfn(cspec.prop));
+				}else{
+					// if the target is a loop
+					itersel = dataselectfn(cspec.sel);
+					target = gettarget(n, cspec, true);
+					nodes = target.nodes;
+					for(j = 0, jj = nodes.length; j < jj; j++){
+						node = nodes[j];
+						inner = compiler(node, false, data, ans);
+						fns[fns.length] = wrapquote(target.quotefn, loopfn(cspec.sel, itersel, inner));
+						target.nodes = [node];
+						setsig(target, fns.length - 1);
+					}
+				}
+			}
+		}
+		// read directives
+		var target, dsel;
+		for(var sel in directive){
+			if(directive.hasOwnProperty(sel)){
+				dsel = directive[sel];
+				if(typeof(dsel) === 'function' || typeof(dsel) === 'string'){
+					// set the value for the node/attr
+					target = gettarget(dom, sel, false);
+					setsig(target, fns.length);
+					fns[fns.length] = wrapquote(target.quotefn, dataselectfn(dsel));
+				}else{
+					// loop on node
+					loopgen(dom, sel, dsel, fns);
+				}
+			}
+		}
+        // convert node to a string 
+        var h = outerHTML(dom), pfns = [];
+		// IE adds an unremovable "selected, value" attribute
+		// hard replace while waiting for a better solution
+        h = h.replace(/<([^>]+)\s(value\=""|selected)\s?([^>]*)>/ig, "<$1 $3>");
+		
+        // remove attribute prefix
+        h = h.split(attPfx).join('');
+
+		// slice the html string at "Sig"
+		var parts = h.split( Sig ), p;
+		// for each slice add the return string of 
+		for(var i = 1; i < parts.length; i++){
+			p = parts[i];
+			// part is of the form "fn-number:..." as placed there by setsig.
+			pfns[i] = fns[ parseInt(p, 10) ];
+			parts[i] = p.substring( p.indexOf(':') + 1 );
+		}
+		return concatenator(parts, pfns);
+	}
+	// compile the template with directive
+	// if a context is passed, the autoRendering is triggered automatically
+	// return a function waiting the data as argument
+	function compile(directive, ctxt, template){
+		var rfn = compiler( ( template || this[0] ).cloneNode(true), directive, ctxt);
+		return function(context){
+			return rfn({context:context});
+		};
+	}
+	//compile with the directive as argument
+	// run the template function on the context argument
+	// return an HTML string 
+	// should replace the template and return this
+	function render(ctxt, directive){
+		var fn = typeof directive === 'function' ? directive : plugins.compile( directive, false, this[0] );
+		for(var i = 0, ii = this.length; i < ii; i++){
+			this[i] = replaceWith( this[i], fn( ctxt, false ));
+		}
+		context = null;
+		return this;
+	}
+
+	// compile the template with autoRender
+	// run the template function on the context argument
+	// return an HTML string 
+	function autoRender(ctxt, directive){
+		var fn = plugins.compile( directive, ctxt, this[0] );
+		for(var i = 0, ii = this.length; i < ii; i++){
+			this[i] = replaceWith( this[i], fn( ctxt, false));
+		}
+		context = null;
+		return this;
+	}
+	
+	function replaceWith(elm, html) {
+		var ne,
+			ep = elm.parentNode,
+			depth = 0;
+		switch (elm.tagName) {
+			case 'TBODY': case 'THEAD': case 'TFOOT':
+				html = '<TABLE>' + html + '</TABLE>';
+				depth = 1;
+			break;
+			case 'TR':
+				html = '<TABLE><TBODY>' + html + '</TBODY></TABLE>';
+				depth = 2;
+			break;
+			case 'TD': case 'TH':
+				html = '<TABLE><TBODY><TR>' + html + '</TR></TBODY></TABLE>';
+				depth = 3;
+			break;
+		}
+		tmp = document.createElement('SPAN');
+		tmp.style.display = 'none';
+		document.body.appendChild(tmp);
+		tmp.innerHTML = html;
+		ne = tmp.firstChild;
+		while (depth--) {
+			ne = ne.firstChild;
+		}
+		ep.insertBefore(ne, elm);
+		ep.removeChild(elm);
+		document.body.removeChild(tmp);
+		elm = ne;
+
+		ne = ep = null;
+		return elm;
+	}
+};
+
+$p.plugins = {};
+
+$p.libs = {
+	dojo:function(){
+		if(typeof document.querySelector === 'undefined'){
+			$p.plugins.find = function(n, sel){
+				return dojo.query(sel, n);
+			};
+		}
+	},
+	domassistant:function(){
+		if(typeof document.querySelector === 'undefined'){
+			$p.plugins.find = function(n, sel){
+				return $(n).cssSelect(sel);
+			};
+		}
+		DOMAssistant.attach({ 
+			publicMethods : [ 'compile', 'render', 'autoRender'],
+			compile:function(directive, ctxt){ return $p(this).compile(directive, ctxt); },
+			render:function(ctxt, directive){ return $( $p(this).render(ctxt, directive) )[0]; },
+			autoRender:function(ctxt, directive){ return $( $p(this).autoRender(ctxt, directive) )[0]; }
+		});
+	},
+	jquery:function(){
+		if(typeof document.querySelector === 'undefined'){
+			$p.plugins.find = function(n, sel){
+				return $(n).find(sel);
+			};
+		}
+		jQuery.fn.extend({
+			compile:function(directive, ctxt){ return $p(this[0]).compile(directive, ctxt); },
+			render:function(ctxt, directive){ return jQuery( $p( this[0] ).render( ctxt, directive ) ); },
+			autoRender:function(ctxt, directive){ return jQuery( $p( this[0] ).autoRender( ctxt, directive ) ); }
+		});
+	},
+	mootools:function(){
+		if(typeof document.querySelector === 'undefined'){
+			$p.plugins.find = function(n, sel){
+				return $(n).getElements(sel);
+			};
+		}
+		Element.implement({
+			compile:function(directive, ctxt){ return $p(this).compile(directive, ctxt); },
+			render:function(ctxt, directive){ return $p(this).render(ctxt, directive); },
+			autoRender:function(ctxt, directive){ return $p(this).autoRender(ctxt, directive); }
+		});
+	},
+	prototype:function(){
+		if(typeof document.querySelector === 'undefined'){
+			$p.plugins.find = function(n, sel){
+				n = n === document ? n.body : n;
+				return typeof n === 'string' ? $$(n) : $(n).select(sel);
+			};
+		}
+		Element.addMethods({
+			compile:function(element, directive, ctxt){ return $p(element).compile(directive, ctxt); }, 
+			render:function(element, ctxt, directive){ return $p(element).render(ctxt, directive); }, 
+			autoRender:function(element, ctxt, directive){ return $p(element).autoRender(ctxt, directive); }
+		});
+	},
+	sizzle:function(){
+		if(typeof document.querySelector === 'undefined'){
+			$p.plugins.find = function(n, sel){
+				return Sizzle(sel, n);
+			};
+		}
+	},
+	sly:function(){
+		if(typeof document.querySelector === 'undefined'){  
+			$p.plugins.find = function(n, sel){
+				return Sly(sel, n);
+			};
+		}
+	}
+};
+
+// get lib specifics if available
+(function(){
+	var libkey = 
+		typeof dojo         !== 'undefined' && 'dojo' || 
+		typeof DOMAssistant !== 'undefined' && 'domassistant' ||
+		typeof jQuery       !== 'undefined' && 'jquery' || 
+		typeof MooTools     !== 'undefined' && 'mootools' ||
+		typeof Prototype    !== 'undefined' && 'prototype' || 
+		typeof Sizzle       !== 'undefined' && 'sizzle' ||
+		typeof Sly          !== 'undefined' && 'sly';
+		
+	libkey && $p.libs[libkey]();
+})();

File silverlog/static/pure_packed.js

+/*!
+    PURE Unobtrusive Rendering Engine for HTML
+
+    Licensed under the MIT licenses.
+    More information at: http://www.opensource.org
+
+    Copyright (c) 2010 Michael Cvilic - BeeBole.com
+
+	Thanks to Rog Peppe for the functional JS jump
+    revision: 2.42
+*/
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('9 $p,3h=$p=6(){9 a=2B[0],2c=K;7(y a===\'15\'){2c=2B[1]||K}8 $p.30(a,2c)};$p.30=6(q,r,t){9 t=2T(),1e=[];2L(y q){14\'15\':1e=t.L(r||E,q);7(1e.x===0){G(\'2p 2r "\'+q+\'" 2H 2u 36\')}18;14\'A\':G(\'2p 2r 2M 2o A, 3r 2k V\');18;4H:1e=[q]}F(9 i=0,O=1e.x;i<O;i++){t[i]=1e[i]}t.x=O;9 u=\'4u\'+1H.3a(1H.3j()*3k)+\'22\',2h=\'44\'+1H.3a(1H.3j()*3k)+\'22\',2i=/^(\\+)?([^\\@\\+]+)?\\@?([^\\+]+)?(\\+)?$/,2R={3z:\'3y\',3x:\'2X\'};8 t;6 G(e){7(y 35!==\'A\'){35.4N(e);4E}C{48(e)}47(\'3h G: \'+e);}6 2T(){9 a=$p.Y,f=6(){};f.T=a;f.T.I=a.I||I;f.T.N=a.N||N;f.T.Q=a.Q||Q;f.T.L=a.L||L;f.T.4C=1x;f.T.4a=G;8 4n f()}6 2f(b){8 b.2f||(6(n){9 a=E.2C(\'3J\'),h;a.1I(n.2Z(13));h=a.2Y;a=16;8 h})(b)}6 1S(o){8 4D.T.4X.2x(o)==="[46 49]"}6 1w(b,f){8 6(a){8 b(\'\'+f.2x(a.1b,a))}}6 L(n,a){7(y n===\'15\'){a=n;n=K}7(y E.2N!==\'A\'){8(n||E).2N(a)}C{G(\'4r 4v 1a 2V 4M 2a: 4Y, 3m.5+, 3w+ 3D 3F+\\n\\3H 3I 2V 29 2k 3K, 3L 3Y a 3Z 40/42 2a a 43 V 45\')}}6 28(c,d){8 6(a){9 b=[c[0]],n=c.x,20,1E,1Y,1G;F(9 i=1;i<n;i++){20=d[i](a);1E=c[i];7(20===\'\'){1Y=b[b.x-1];7((1G=1Y.4q(/[\\w]+=\\"?$/))>-1){b[b.x-1]=1Y.27(0,1G);1E=1E.4y(1)}}b[b.x]=20;b[b.x]=1E}8 b.23(\'\')}}6 3i(p){9 m=p.1F(/^(\\w+)\\s*<-\\s*(\\S+)?$/);7(m===16){G(\'25 17 51: "\'+p+\'"\')}7(m[1]===\'1j\'){G(\'"1j<-..." 2o a 3n 3o F 1N 3p 3q 3s.\\n\\3t 3u 3v 2j F 2k 17.\')}7(!m[2]||(m[2]&&(/1b/i).1a(m[2]))){m[2]=6(a){8 a.1b}}8{2j:m[1],12:m[2]}}6 1g(c){7(y(c)===\'6\'){8 c}9 m=c.1F(/^[a-3A-Z$22][\\w$]*(\\.[\\w$-]*[^\\.])*$/);7(m===16){9 d=K,s=c,1P=[],1f=[],i=0,2m;7(/\\\'|\\"/.1a(s.2J(0))){7(/\\\'|\\"/.1a(s.2J(s.x-1))){2m=s.27(1,s.x-1);8 6(){8 2m}}}C{1T((m=s.1F(/#\\{([^{}]+)\\}/))!==16){d=13;1P[i++]=s.2F(0,m.2D);1f[i]=1g(m[1]);s=s.2F(m.2D+m[0].x,s.x)}}7(!d){G(\'25 3M V 2K: \'+c)}1P[i]=s;8 28(1P,1f)}m=c.1X(\'.\');8 6(a){9 b=a.1b;7(!b){8\'\'}9 v=a[m[0]],i=0;7(v&&v.1j){b=v.1j;i+=1}9 n=m.x;F(;i<n;i++){7(!b){18}b=b[m[i]]}8(!b&&b!==0)?\'\':b}}6 1D(c,d,e){9 f,R,V,B,P,J=[];7(y d===\'15\'){f=d;9 m=d.1F(2i);7(!m){G(\'25 V 2K: \'+d)}R=m[1];V=m[2];B=m[3];P=m[4];7(V===\'.\'||(!V&&B)){J[0]=c}C{J=t.L(c,V)}7(!J||J.x===0){8 G(\'2p 1C "\'+d+\'" 2H 2u 36 1o 1N 2r\')}}C{R=d.R;B=d.B;P=d.P;J=[c]}7(R||P){7(R&&P){G(\'P/R 24 4d 4e 4f 1N 4g 4h\')}C 7(e){G(\'4j P/R/1n 4o 4p F 17 J\')}C 7(P&&e){G(\'24 P 2a 17 (12: \'+f+\')\')}}9 g,1l,U,1B,1A,1z,X;7(B){1B=(/^2b$/i).1a(B);1A=(/^4F$/i).1a(B);1z=1A?\'1p\':B;g=6(a,s){a.4J(2h+B,s);7(1z 1o a&&!1B){a[1z]=\'\'}7(a.33===1){a.32(B);1A&&a.32(1z)}};7(1B||1A){7(1B){1l=6(n){8 n.2b.4Q}}C{1l=6(n){8 n.1p}}U=6(s){8 s.1n(/\\"/g,\'&31;\')}}C{1l=6(n){8 n.4V(B)};U=6(s){8 s.1n(/\\"/g,\'&31;\').1n(/\\s/g,\'&4W;\')}}7(R){X=6(a,s){g(a,s+1l(a))}}C 7(P){X=6(a,s){g(a,1l(a)+s)}}C{X=6(a,s){g(a,s)}}}C{7(e){X=6(a,s){9 b=a.2W;7(b){b.2g(E.1Z(s),a.3l);b.1W(a)}}}C{7(R){X=6(a,s){a.2g(E.1Z(s),a.1y)}}C 7(P){X=6(a,s){a.1I(E.1Z(s))}}C{X=6(a,s){1T(a.1y){a.1W(a.1y)}a.1I(E.1Z(s))}}}U=6(s){8 s}}8{B:B,M:J,2S:X,12:f,U:U}}6 1v(a,n){9 b=u+n+\':\';F(9 i=0;i<a.M.x;i++){a.2S(a.M[i],b)}}6 2n(h,j,k,l,m){8 6(f){9 a=j(f),2q=f[h],1V={2I:a},2t=0,x,21=[],2v=6(b,c,d,e){f.1G=c.1G=b;f.1j=c.1j=a[b];f.2I=a;y e!==\'A\'&&(f.x=e);7(y d===\'6\'&&!d(f)){2t++;8}21.2w(k.2x(c,f))};f[h]=1V;7(1S(a)){x=a.x||0;7(y l===\'6\'){a.1U(l)}F(9 i=0,O=x;i<O;i++){2v(i,1V,m,x-2t)}}C{7(a&&y l!==\'A\'){G(\'1U 2o 3B 3C 29 4b, 2u 3E\')}F(9 g 1o a){a.2z(g)&&2v(g,1V,m)}}y 2q!==\'A\'?f[h]=2q:3G f[h];8 21.23(\'\')}}6 2A(a,b,c,d){9 e=K,1c,2y,1u,H;F(H 1o c){7(c.2z(H)){7(H===\'1U\'){2y=c.1U;2E}C 7(H===\'1u\'){1u=c.1u;2E}7(e){G(\'24 3N 3O 3P 3Q 17 29 a J\')}1c=H;e=13}}7(!1c){G(\'3R 1o 1N V: \'+b+\'\\3S 3T 3U 3V 3W a 15, a 6 3X a 17(<-)\')}9 f=c[1c];7(y(f)===\'15\'||y(f)===\'6\'){c={};c[1c]={2M:f};8 2A(a,b,c,d)}9 g=3i(1c),1t=1g(g.12),J=1D(a,b,13),M=J.M;F(i=0;i<M.x;i++){9 h=M[i],1s=1x(h,f);d[d.x]=1w(J.U,2n(g.2j,1t,1s,2y,1u));J.M=[h];1v(J,d.x-1)}}6 2G(n,d){9 e=n.41(\'*\'),2s=[],1k={a:[],l:{}},z,1R,i,O,j,1h,11,1Q,2l;F(i=-1,O=e.x;i<O;i++){11=i>-1?e[i]:n;7(11.33===1&&11.1p!==\'\'){1Q=11.1p.1X(\' \');F(j=0,1h=1Q.x;j<1h;j++){2l=1Q[j];z=2O(2l,11.2P);7(z!==K){1R=(/4c/i).1a(z.B);7(z.12.2Q(\'@\')>-1||1R){11.1p=11.1p.1n(\'@\'+z.B,\'\');7(1R){z.B=K}}2s.2w({n:11,z:z})}}}}8 2s;6 2O(c,a){9 b=c.1F(2i),B=b[3]||2R[a],z={R:!!b[1],H:b[2],B:B,P:!!b[4],12:c},i,O,1O,1M,W;F(i=1k.a.x-1;i>=0;i--){1O=1k.a[i];1M=1O.l[0];W=1M&&1M[z.H];7(y W!==\'A\'){z.H=1O.p+\'.\'+z.H;7(1k.l[z.H]===13){W=W[0]}18}}7(y W===\'A\'){W=1S(d)?d[0][z.H]:d[z.H];7(y W===\'A\'){8 K}}7(1S(W)){1k.a.2w({l:W,p:z.H});1k.l[z.H]=13;z.t=\'17\'}C{z.t=\'2U\'}8 z}}6 1x(a,b,c,d){9 e=[];d=d||c&&2G(a,c);7(c){9 j,1h,z,n,f,M,1t,1C,1s;1T(d.x>0){z=d[0].z;n=d[0].n;d.4i(0,1);7(z.t===\'2U\'){f=1D(n,z,K);1v(f,e.x);e[e.x]=1w(f.U,1g(z.H))}C{1t=1g(z.12);f=1D(n,z,13);M=f.M;F(j=0,1h=M.x;j<1h;j++){1C=M[j];1s=1x(1C,K,c,d);e[e.x]=1w(f.U,2n(z.12,1t,1s));f.M=[1C];1v(f,e.x-1)}}}}9 f,1d;F(9 g 1o b){7(b.2z(g)){1d=b[g];7(y(1d)===\'6\'||y(1d)===\'15\'){f=1D(a,g,K);1v(f,e.x);e[e.x]=1w(f.U,1g(1d))}C{2A(a,g,1d,e)}}}9 h=2f(a),1f=[];h=h.1n(/<([^>]+)\\s(2X\\=""|4k)\\s?([^>]*)>/4l,"<$1 $3>");h=h.1X(2h).23(\'\');9 k=h.1X(u),p;F(9 i=1;i<k.x;i++){p=k[i];1f[i]=e[4m(p,10)];k[i]=p.27(p.2Q(\':\')+1)}8 28(k,1f)}6 I(b,c,d){9 e=1x((d||D[0]).2Z(13),b,c);8 6(a){8 e({1b:a})}}6 N(a,b){9 c=y b===\'6\'?b:t.I(b,K,D[0]);F(9 i=0,O=D.x;i<O;i++){D[i]=2e(D[i],c(a,K))}1b=16;8 D}6 Q(a,b){9 c=t.I(b,a,D[0]);F(9 i=0,O=D.x;i<O;i++){D[i]=2e(D[i],c(a,K))}1b=16;8 D}6 2e(a,b){9 c,1L=a.2W,1r=0;2L(a.2P){14\'1q\':14\'4s\':14\'4t\':b=\'<1m>\'+b+\'</1m>\';1r=1;18;14\'2d\':b=\'<1m><1q>\'+b+\'</1q></1m>\';1r=2;18;14\'4w\':14\'4x\':b=\'<1m><1q><2d>\'+b+\'</2d></1q></1m>\';1r=3;18}1i=E.2C(\'4z\');1i.2b.4A=\'4B\';E.26.1I(1i);1i.2Y=b;c=1i.1y;1T(1r--){c=c.1y}1L.2g(c,a);1L.1W(a);E.26.1W(1i);a=c;c=1L=16;8 a}};$p.Y={};$p.34={1K:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){8 1K.4G(a,n)}}},37:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){8 $(n).4I(a)}}38.4K({4L:[\'I\',\'N\',\'Q\'],I:6(a,b){8 $p(D).I(a,b)},N:6(a,b){8 $($p(D).N(a,b))[0]},Q:6(a,b){8 $($p(D).Q(a,b))[0]}})},39:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){8 $(n).L(a)}}1J.4O.4P({I:6(a,b){8 $p(D[0]).I(a,b)},N:6(a,b){8 1J($p(D[0]).N(a,b))},Q:6(a,b){8 1J($p(D[0]).Q(a,b))}})},3b:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){8 $(n).4R(a)}}3c.4S({I:6(a,b){8 $p(D).I(a,b)},N:6(a,b){8 $p(D).N(a,b)},Q:6(a,b){8 $p(D).Q(a,b)}})},T:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){n=n===E?n.26:n;8 y n===\'15\'?$$(n):$(n).4T(a)}}3c.4U({I:6(a,b,c){8 $p(a).I(b,c)},N:6(a,b,c){8 $p(a).N(b,c)},Q:6(a,b,c){8 $p(a).Q(b,c)}})},3d:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){8 3e(a,n)}}},3f:6(){7(y E.19===\'A\'){$p.Y.L=6(n,a){8 3g(a,n)}}}};(6(){9 a=y 1K!==\'A\'&&\'1K\'||y 38!==\'A\'&&\'37\'||y 1J!==\'A\'&&\'39\'||y 4Z!==\'A\'&&\'3b\'||y 50!==\'A\'&&\'T\'||y 3e!==\'A\'&&\'3d\'||y 3g!==\'A\'&&\'3f\';a&&$p.34[a]()})();',62,312,'||||||function|if|return|var||||||||||||||||||||||||length|typeof|cspec|undefined|attr|else|this|document|for|error|prop|compile|target|false|find|nodes|render|ii|append|autoRender|prepend||prototype|quotefn|selector|val|setfn|plugins|||ni|sel|true|case|string|null|loop|break|querySelector|test|context|ls|dsel|templates|pfns|dataselectfn|jj|tmp|item|openLoops|getstr|TABLE|replace|in|className|TBODY|depth|inner|itersel|filter|setsig|wrapquote|compiler|firstChild|attName|isClass|isStyle|node|gettarget|pVal|match|pos|Math|appendChild|jQuery|dojo|ep|loopil|the|loopi|parts|cs|isNodeValue|isArray|while|sort|temp|removeChild|split|attLine|createTextNode|fnVal|strs|_|join|cannot|bad|body|substring|concatenator|on|with|style|ctxt|TR|replaceWith|outerHTML|insertBefore|attPfx|selRx|name|your|cj|retStr|loopfn|is|The|old|template|an|filtered|not|buildArg|push|call|sorter|hasOwnProperty|loopgen|arguments|createElement|index|continue|slice|getAutoNodes|was|items|charAt|syntax|switch|root|querySelectorAll|checkClass|tagName|indexOf|autoAttr|set|getPlugins|str|PURE|parentNode|value|innerHTML|cloneNode|core|quot|removeAttribute|nodeType|libs|console|found|domassistant|DOMAssistant|jquery|floor|mootools|Element|sizzle|Sizzle|sly|Sly|pure|parseloopspec|random|1000000|nextSibling|FF3|reserved|word|current|running|check|iteration|nPlease|choose|another|Safari4|INPUT|src|IMG|zA|only|available|and|objects|IE8|delete|nTo|run|div|browser|you|data|have|more|than|one|Error|nA|directive|action|must|be|or|need|JS|library|getElementsByTagName|framework|CSS|_a|engine|object|throw|alert|Array|_error|arrays|nodevalue|take|place|at|same|time|splice|no|selected|ig|parseInt|new|modifiers|allowed|search|You|THEAD|TFOOT|_s|can|TD|TH|substr|SPAN|display|none|_compiler|Object|debugger|class|query|default|cssSelect|setAttribute|attach|publicMethods|standalone|log|fn|extend|cssText|getElements|implement|select|addMethods|getAttribute|nbsp|toString|iPhone|MooTools|Prototype|spec'.split('|'),0,{}))

File silverlog/static/sammy-0.5.3.min.js

+// -- Sammy -- /sammy.js
+// http://code.quirkey.com/sammy
+// Version: 0.5.3
+// Built: Fri Apr 02 13:39:33 -0400 2010
+(function(g){var h,d="([^/]+)",f=/:([\w\d]+)/g,c=/\?([^#]*)$/,a=decodeURIComponent,b=function(i){return function(j,k){return this.route.apply(this,[i,j,k])}},e=[];h=function(){var j=g.makeArray(arguments),k,i;h.apps=h.apps||{};if(j.length===0||j[0]&&g.isFunction(j[0])){return h.apply(h,["body"].concat(j))}else{if(typeof(i=j.shift())=="string"){k=h.apps[i]||new h.Application();k.element_selector=i;if(j.length>0){g.each(j,function(l,m){k.use(m)})}if(k.element_selector!=i){delete h.apps[i]}h.apps[k.element_selector]=k;return k}}};h.VERSION="0.5.3";h.addLogger=function(i){e.push(i)};h.log=function(){var i=g.makeArray(arguments);i.unshift("["+Date()+"]");g.each(e,function(k,j){j.apply(h,i)})};if(typeof window.console!="undefined"){if(g.isFunction(console.log.apply)){h.addLogger(function(){window.console.log.apply(console,arguments)})}else{h.addLogger(function(){window.console.log(arguments)})}}else{if(typeof console!="undefined"){h.addLogger(function(){console.log.apply(console,arguments)})}}h.Object=function(i){return g.extend(this,i||{})};g.extend(h.Object.prototype,{toHash:function(){var i={};g.each(this,function(l,j){if(!g.isFunction(j)){i[l]=j}});return i},toHTML:function(){var i="";g.each(this,function(l,j){if(!g.isFunction(j)){i+="<strong>"+l+"</strong> "+j+"<br />"}});return i},uuid:function(){if(typeof this._uuid=="undefined"||!this._uuid){this._uuid=(new Date()).getTime()+"-"+parseInt(Math.random()*1000,10)}return this._uuid},keys:function(i){var j=[];for(var k in this){if(!g.isFunction(this[k])||!i){j.push(k)}}return j},has:function(i){return this[i]&&g.trim(this[i].toString())!=""},join:function(){var j=g.makeArray(arguments);var i=j.shift();return j.join(i)},log:function(){h.log.apply(h,arguments)},toString:function(i){var j=[];g.each(this,function(m,l){if(!g.isFunction(l)||i){j.push('"'+m+'": '+l.toString())}});return"Sammy.Object: {"+j.join(",")+"}"}});h.HashLocationProxy=function(j,i){this.app=j;this.is_native=false;this._startPolling(i)};h.HashLocationProxy.prototype={bind:function(){var i=this,j=this.app;g(window).bind("hashchange."+this.app.eventNamespace(),function(l,k){if(i.is_native===false&&!k){h.log("native hash change exists, using");i.is_native=true;clearInterval(h.HashLocationProxy._interval)}j.trigger("location-changed")})},unbind:function(){g(window).unbind("hashchange."+this.app.eventNamespace())},getLocation:function(){var i=window.location.toString().match(/^[^#]*(#.+)$/);return i?i[1]:""},setLocation:function(i){return(window.location=i)},_startPolling:function(k){var j=this;if(!h.HashLocationProxy._interval){if(!k){k=10}var i=function(){current_location=j.getLocation();if(!h.HashLocationProxy._last_location||current_location!=h.HashLocationProxy._last_location){setTimeout(function(){g(window).trigger("hashchange",[true])},1)}h.HashLocationProxy._last_location=current_location};i();h.HashLocationProxy._interval=setInterval(i,k);g(window).bind("beforeunload",function(){clearInterval(h.HashLocationProxy._interval)})}}};h.DataLocationProxy=function(j,i){this.app=j;this.data_name=i||"sammy-location"};h.DataLocationProxy.prototype={bind:function(){var i=this;this.app.$element().bind("setData",function(l,j,k){if(j==i.data_name){i.app.$element().each(function(){g.data(this,i.data_name,k)});i.app.trigger("location-changed")}})},unbind:function(){this.app.$element().unbind("setData")},getLocation:function(){return this.app.$element().data(this.data_name)},setLocation:function(i){return this.app.$element().data(this.data_name,i)}};h.Application=function(i){var j=this;this.routes={};this.listeners=new h.Object({});this.arounds=[];this.befores=[];this.namespace=this.uuid();this.context_prototype=function(){h.EventContext.apply(this,arguments)};this.context_prototype.prototype=new h.EventContext();if(g.isFunction(i)){i.apply(this,[this])}if(!this.location_proxy){this.location_proxy=new h.HashLocationProxy(j,this.run_interval_every)}if(this.debug){this.bindToAllEvents(function(l,k){j.log(j.toString(),l.cleaned_type,k||{})})}};h.Application.prototype=g.extend({},h.Object.prototype,{ROUTE_VERBS:["get","post","put","delete"],APP_EVENTS:["run","unload","lookup-route","run-route","route-found","event-context-before","event-context-after","changed","error","check-form-submission","redirect"],_last_route:null,_running:false,element_selector:"body",debug:false,raise_errors:false,run_interval_every:50,location_proxy:null,template_engine:null,toString:function(){return"Sammy.Application:"+this.element_selector},$element:function(){return g(this.element_selector)},use:function(){var i=g.makeArray(arguments);var j=i.shift();try{i.unshift(this);j.apply(this,i)}catch(k){if(typeof j=="undefined"){this.error("Plugin Error: called use() but plugin is not defined",k)}else{if(!g.isFunction(j)){this.error("Plugin Error: called use() but '"+j.toString()+"' is not a function",k)}else{this.error("Plugin Error",k)}}}return this},route:function(l,j,n){var k=this,m=[],i;if(!n&&g.isFunction(j)){j=l;n=j;l="any"}l=l.toLowerCase();if(j.constructor==String){f.lastIndex=0;while((path_match=f.exec(j))!==null){m.push(path_match[1])}j=new RegExp("^"+j.replace(f,d)+"$")}if(typeof n=="string"){n=k[n]}i=function(o){var p={verb:o,path:j,callback:n,param_names:m};k.routes[o]=k.routes[o]||[];k.routes[o].push(p)};if(l==="any"){g.each(this.ROUTE_VERBS,function(p,o){i(o)})}else{i(l)}return this},get:b("get"),post:b("post"),put:b("put"),del:b("delete"),any:b("any"),mapRoutes:function(j){var i=this;g.each(j,function(k,l){i.route.apply(i,l)});return this},eventNamespace:function(){return["sammy-app",this.namespace].join("-")},bind:function(i,k,m){var l=this;if(typeof m=="undefined"){m=k}var j=function(){var p,n,o;p=arguments[0];o=arguments[1];if(o&&o.context){n=o.context;delete o.context}else{n=new l.context_prototype(l,"bind",p.type,o)}p.cleaned_type=p.type.replace(l.eventNamespace(),"");m.apply(n,[p,o])};if(!this.listeners[i]){this.listeners[i]=[]}this.listeners[i].push(j);if(this.isRunning()){this._listen(i,j)}return this},trigger:function(i,j){this.$element().trigger([i,this.eventNamespace()].join("."),[j]);return this},refresh:function(){this.last_location=null;this.trigger("location-changed");return this},before:function(i,j){if(g.isFunction(i)){j=i;i={}}this.befores.push([i,j]);return this},after:function(i){return this.bind("event-context-after",i)},around:function(i){this.arounds.push(i);return this},isRunning:function(){return this._running},helpers:function(i){g.extend(this.context_prototype.prototype,i);return this},helper:function(i,j){this.context_prototype.prototype[i]=j;return this},run:function(i){if(this.isRunning()){return false}var j=this;g.each(this.listeners.toHash(),function(k,l){g.each(l,function(n,m){j._listen(k,m)})});this.trigger("run",{start_url:i});this._running=true;this.last_location=null;if(this.getLocation()==""&&typeof i!="undefined"){this.setLocation(i)}this._checkLocation();this.location_proxy.bind();this.bind("location-changed",function(){j._checkLocation()});this.bind("submit",function(l){var k=j._checkFormSubmission(g(l.target).closest("form"));return(k===false)?l.preventDefault():false});g(window).bind("beforeunload",function(){j.unload()});return this.trigger("changed")},unload:function(){if(!this.isRunning()){return false}var i=this;this.trigger("unload");this.location_proxy.unbind();this.$element().unbind("submit").removeClass(i.eventNamespace());g.each(this.listeners.toHash(),function(j,k){g.each(k,function(m,l){i._unlisten(j,l)})});this._running=false;return this},bindToAllEvents:function(j){var i=this;g.each(this.APP_EVENTS,function(k,l){i.bind(l,j)});g.each(this.listeners.keys(true),function(l,k){if(i.APP_EVENTS.indexOf(k)==-1){i.bind(k,j)}});return this},routablePath:function(i){return i.replace(c,"")},lookupRoute:function(l,j){var k=this,i=false;this.trigger("lookup-route",{verb:l,path:j});if(typeof this.routes[l]!="undefined"){g.each(this.routes[l],function(n,m){if(k.routablePath(j).match(m.path)){i=m;return false}})}return i},runRoute:function(k,v,m){var l=this,t=this.lookupRoute(k,v),j,r,n,q,u,s,p,i;this.log("runRoute",[k,v].join(" "));this.trigger("run-route",{verb:k,path:v,params:m});if(typeof m=="undefined"){m={}}g.extend(m,this._parseQueryString(v));if(t){this.trigger("route-found",{route:t});if((path_params=t.path.exec(this.routablePath(v)))!==null){path_params.shift();g.each(path_params,function(w,x){if(t.param_names[w]){m[t.param_names[w]]=a(x)}else{if(!m.splat){m.splat=[]}m.splat.push(a(x))}})}j=new this.context_prototype(this,k,v,m);n=this.arounds.slice(0);u=this.befores.slice(0);p=[j].concat(m.splat);r=function(){var w;while(u.length>0){s=u.shift();if(l.contextMatchesOptions(j,s[0])){w=s[1].apply(j,[j]);if(w===false){return false}}}l.last_route=t;j.trigger("event-context-before",{context:j});w=t.callback.apply(j,p);j.trigger("event-context-after",{context:j});return w};g.each(n.reverse(),function(w,x){var y=r;r=function(){return x.apply(j,[y])}});try{i=r()}catch(o){this.error(["500 Error",k,v].join(" "),o)}return i}else{return this.notFound(k,v)}},contextMatchesOptions:function(l,n,j){var k=n;if(typeof k==="undefined"||k=={}){return true}if(typeof j==="undefined"){j=true}if(typeof k==="string"||g.isFunction(k.test)){k={path:k}}if(k.only){return this.contextMatchesOptions(l,k.only,true)}else{if(k.except){return this.contextMatchesOptions(l,k.except,false)}}var i=true,m=true;if(k.path){if(g.isFunction(k.path.test)){i=k.path.test(l.path)}else{i=(k.path.toString()===l.path)}}if(k.verb){m=k.verb===l.verb}return j?(m&&i):!(m&&i)},getLocation:function(){return this.location_proxy.getLocation()},setLocation:function(i){return this.location_proxy.setLocation(i)},swap:function(i){return this.$element().html(i)},notFound:function(k,j){var i=this.error(["404 Not Found",k,j].join(" "));return(k==="get")?i:true},error:function(j,i){if(!i){i=new Error()}i.message=[j,i.message].join(" ");this.trigger("error",{message:i.message,error:i});if(this.raise_errors){throw (i)}else{this.log(i.message,i)}},_checkLocation:function(){var i,j;i=this.getLocation();if(i!=this.last_location){j=this.runRoute("get",i)}this.last_location=i;return j},_checkFormSubmission:function(k){var i,l,n,m,j;this.trigger("check-form-submission",{form:k});i=g(k);l=i.attr("action");n=g.trim(i.attr("method").toString().toLowerCase());if(!n||n==""){n="get"}this.log("_checkFormSubmission",i,l,n);m=g.extend({},this._parseFormParams(i),{"$form":i});j=this.runRoute(n,l,m);return(typeof j=="undefined")?false:j},_parseFormParams:function(i){var j={};g.each(i.serializeArray(),function(k,l){if(j[l.name]){if(g.isArray(j[l.name])){j[l.name].push(l.value)}else{j[l.name]=[j[l.name],l.value]}}else{j[l.name]=l.value}});return j},_parseQueryString:function(n){var l={},m,k,o,j;m=n.match(c);if(m){k=m[1].split("&");for(j=0;j<k.length;j+=1){o=k[j].split("=");l[o[0]]=a(o[1])}}return l},_listen:function(i,j){return this.$element().bind([i,this.eventNamespace()].join("."),j)},_unlisten:function(i,j){return this.$element().unbind([i,this.eventNamespace()].join("."),j)}});h.EventContext=function(l,k,i,j){this.app=l;this.verb=k;this.path=i;this.params=new h.Object(j)};h.EventContext.prototype=g.extend({},h.Object.prototype,{$element:function(){return this.app.$element()},partial:function(q,m,p){var i,l,o,n,k="partial:"+q,j=this;if((o=q.match(/\.([^\.]+)$/))){o=o[1]}if((!o||!g.isFunction(j[o]))&&this.app.template_engine){o=this.app.template_engine}if(o&&!g.isFunction(o)&&g.isFunction(j[o])){o=j[o]}if(!p&&g.isFunction(m)){p=m;m={}}n=(g.isArray(m)?m:[m||{}]);l=function(r){var s=r,t="";g.each(n,function(u,v){if(g.isFunction(o)){s=o.apply(j,[r,v])}t+=s;if(p){return p.apply(j,[s,u])}});if(!p){j.swap(t)}j.trigger("changed")};if(this.app.cache_partials&&this.cache(k)){l.apply(j,[this.cache(k)])}else{g.get(q,function(r){if(j.app.cache_partials){j.cache(k,r)}l.apply(j,[r])})}},redirect:function(){var k,j=g.makeArray(arguments),i=this.app.getLocation();if(j.length>1){j.unshift("/");k=this.join.apply(this,j)}else{k=j[0]}this.trigger("redirect",{to:k});this.app.last_location=this.path;this.app.setLocation(k);if(i==k){this.app.trigger("location-changed")}},trigger:function(i,j){if(typeof j=="undefined"){j={}}if(!j.context){j.context=this}return this.app.trigger(i,j)},eventNamespace:function(){return this.app.eventNamespace()},swap:function(i){return this.app.swap(i)},notFound:function(){return this.app.notFound(this.verb,this.path)},toString:function(){return"Sammy.EventContext: "+[this.verb,this.path,this.params].join(" ")}});g.sammy=window.Sammy=h})(jQuery);

File silverlog/static/sammy.js

+// name: sammy
+// version: 0.5.2
+
+(function($) {
+  
+  var Sammy,
+      PATH_REPLACER = "([^\/]+)",
+      PATH_NAME_MATCHER = /:([\w\d]+)/g,
+      QUERY_STRING_MATCHER = /\?([^#]*)$/,
+      _decode = decodeURIComponent,
+      _routeWrapper = function(verb) {
+        return function(path, callback) { return this.route.apply(this, [verb, path, callback]); };
+      },
+      loggers = [];
+  
+  
+  // <tt>Sammy</tt> (also aliased as $.sammy) is not only the namespace for a 
+  // number of prototypes, its also a top level method that allows for easy
+  // creation/management of <tt>Sammy.Application</tt> instances. There are a
+  // number of different forms for <tt>Sammy()</tt> but each returns an instance
+  // of <tt>Sammy.Application</tt>. When a new instance is created using
+  // <tt>Sammy</tt> it is added to an Object called <tt>Sammy.apps</tt>. This
+  // provides for an easy way to get at existing Sammy applications. Only one
+  // instance is allowed per <tt>element_selector</tt> so when calling
+  // <tt>Sammy('selector')</tt> multiple times, the first time will create 
+  // the application and the following times will extend the application
+  // already added to that selector.
+  //
+  // === Example
+  //
+  //      // returns the app at #main or a new app
+  //      Sammy('#main') 
+  //
+  //      // equivilent to "new Sammy.Application", except appends to apps
+  //      Sammy();
+  //      Sammy(function() { ... }); 
+  //
+  //      // extends the app at '#main' with function.
+  //      Sammy('#main', function() { ... });
+  //
+  Sammy = function() {
+    var args = $.makeArray(arguments), 
+        app, selector;
+    Sammy.apps = Sammy.apps || {};
+    if (args.length === 0 || args[0] && $.isFunction(args[0])) { // Sammy()
+      return Sammy.apply(Sammy, ['body'].concat(args));
+    } else if (typeof (selector = args.shift()) == 'string') { // Sammy('#main')
+      app = Sammy.apps[selector] || new Sammy.Application();
+      app.element_selector = selector;
+      if (args.length > 0) {
+        $.each(args, function(i, plugin) {
+          app.use(plugin);
+        });
+      }
+      // if the selector changes make sure the refrence in Sammy.apps changes
+      if (app.element_selector != selector) {
+        delete Sammy.apps[selector];
+      }
+      Sammy.apps[app.element_selector] = app;
+      return app;
+    }
+  };
+  
+  Sammy.VERSION = '0.5.3';
+    
+  // Add to the global logger pool. Takes a function that accepts an 
+  // unknown number of arguments and should print them or send them somewhere
+  // The first argument is always a timestamp.
+  Sammy.addLogger = function(logger) {
+    loggers.push(logger);
+  };
+  
+  // Sends a log message to each logger listed in the global
+  // loggers pool. Can take any number of arguments.
+  // Also prefixes the arguments with a timestamp.
+  Sammy.log = function()	{
+    var args = $.makeArray(arguments);
+    args.unshift("[" + Date() + "]");
+    $.each(loggers, function(i, logger) {
+      logger.apply(Sammy, args);
+    });
+	};
+	
+	if (typeof window.console != 'undefined') {
+	  if ($.isFunction(console.log.apply)) {
+      Sammy.addLogger(function() {
+        window.console.log.apply(console, arguments);
+      });
+    } else {
+      Sammy.addLogger(function() {
+        window.console.log(arguments);
+      });
+    }
+  } else if (typeof console != 'undefined') {
+    Sammy.addLogger(function() {
+      console.log.apply(console, arguments);
+    });
+  }
+    
+  // Sammy.Object is the base for all other Sammy classes. It provides some useful 
+  // functionality, including cloning, iterating, etc.
+  Sammy.Object = function(obj) { // constructor
+    return $.extend(this, obj || {});
+  };
+        
+  $.extend(Sammy.Object.prototype, {    
+            
+    // Returns a copy of the object with Functions removed.
+    toHash: function() {
+      var json = {}; 
+      $.each(this, function(k,v) {
+        if (!$.isFunction(v)) {
+          json[k] = v;
+        }
+      });
+      return json;
+    },
+    
+    // Renders a simple HTML version of this Objects attributes.
+    // Does not render functions.
+    // For example. Given this Sammy.Object:
+    //    
+    //    var s = new Sammy.Object({first_name: 'Sammy', last_name: 'Davis Jr.'});
+    //    s.toHTML() //=> '<strong>first_name</strong> Sammy<br /><strong>last_name</strong> Davis Jr.<br />'
+    //
+    toHTML: function() {
+      var display = "";
+      $.each(this, function(k, v) {
+        if (!$.isFunction(v)) {
+          display += "<strong>" + k + "</strong> " + v + "<br />";
+        }
+      });
+      return display;
+    },
+    
+    // Generates a unique identifing string. Used for application namespaceing.
+    uuid: function() {
+      if (typeof this._uuid == 'undefined' || !this._uuid) {
+        this._uuid = (new Date()).getTime() + '-' + parseInt(Math.random() * 1000, 10);
+      }
+      return this._uuid;
+    },
+    
+    // Returns an array of keys for this object. If <tt>attributes_only</tt> 
+    // is true will not return keys that map to a <tt>function()</tt>
+    keys: function(attributes_only) {
+      var keys = [];
+      for (var property in this) {
+        if (!$.isFunction(this[property]) || !attributes_only) {
+          keys.push(property);
+        }
+      }
+      return keys;
+    },
+    
+    // Checks if the object has a value at <tt>key</tt> and that the value is not empty
+    has: function(key) {
+      return this[key] && $.trim(this[key].toString()) != '';
+    },
+        
+    // convenience method to join as many arguments as you want 
+    // by the first argument - useful for making paths
+    join: function() {
+      var args = $.makeArray(arguments);
+      var delimiter = args.shift();
+      return args.join(delimiter);
+    },
+    
+    // Shortcut to Sammy.log
+    log: function() {
+      Sammy.log.apply(Sammy, arguments);
+    },
+    
+    // Returns a string representation of this object. 
+    // if <tt>include_functions</tt> is true, it will also toString() the 
+    // methods of this object. By default only prints the attributes.
+    toString: function(include_functions) {
+      var s = [];
+      $.each(this, function(k, v) {
+		    if (!$.isFunction(v) || include_functions) {
+          s.push('"' + k + '": ' + v.toString());
+		    }
+      });
+      return "Sammy.Object: {" + s.join(',') + "}"; 
+    }
+  });
+  
+  // The HashLocationProxy is the default location proxy for all Sammy applications.
+  // A location proxy is a prototype that conforms to a simple interface. The purpose
+  // of a location proxy is to notify the Sammy.Application its bound to when the location
+  // or 'external state' changes. The HashLocationProxy considers the state to be
+  // changed when the 'hash' (window.location.hash / '#') changes. It does this in two
+  // different ways depending on what browser you are using. The newest browsers 
+  // (IE, Safari > 4, FF >= 3.6) support a 'onhashchange' DOM event, thats fired whenever
+  // the location.hash changes. In this situation the HashLocationProxy just binds
+  // to this event and delegates it to the application. In the case of older browsers
+  // a poller is set up to track changes to the hash. Unlike Sammy 0.3 or earlier,
+  // the HashLocationProxy allows the poller to be a global object, eliminating the
+  // need for multiple pollers even when thier are multiple apps on the page.
+  Sammy.HashLocationProxy = function(app, run_interval_every) {
+    this.app = app;
+    // set is native to false and start the poller immediately
+    this.is_native = false;
+    this._startPolling(run_interval_every);
+  };
+  
+  Sammy.HashLocationProxy.prototype = {
+    // bind the proxy events to the current app.
+    bind: function() {
+      var proxy = this, app = this.app;
+      $(window).bind('hashchange.' + this.app.eventNamespace(), function(e, non_native) {
+        // if we receive a native hash change event, set the proxy accordingly
+        // and stop polling
+        if (proxy.is_native === false && !non_native) {
+          Sammy.log('native hash change exists, using');
+          proxy.is_native = true;
+          clearInterval(Sammy.HashLocationProxy._interval);
+        }
+        app.trigger('location-changed');
+      });
+    },
+    // unbind the proxy events from the current app
+    unbind: function() {
+      $(window).unbind('hashchange.' + this.app.eventNamespace());
+    },
+    // get the current location from the hash.
+    getLocation: function() {
+     // Bypass the `window.location.hash` attribute.  If a question mark
+      // appears in the hash IE6 will strip it and all of the following
+      // characters from `window.location.hash`.
+      var matches = window.location.toString().match(/^[^#]*(#.+)$/);
+      return matches ? matches[1] : '';
+    },
+    // set the current location to <tt>new_location</tt>
+    setLocation: function(new_location) {
+      return (window.location = new_location);
+    },
+    
+    _startPolling: function(every) {
+      // set up interval
+      var proxy = this;
+      if (!Sammy.HashLocationProxy._interval) {
+        if (!every) { every = 10; }
+        var hashCheck = function() {
+          current_location = proxy.getLocation();
+          // Sammy.log('getLocation', current_location);
+          if (!Sammy.HashLocationProxy._last_location || 
+            current_location != Sammy.HashLocationProxy._last_location) {
+            setTimeout(function() {
+              $(window).trigger('hashchange', [true]);
+            }, 1);
+          }
+          Sammy.HashLocationProxy._last_location = current_location;
+        };
+        hashCheck();
+        Sammy.HashLocationProxy._interval = setInterval(hashCheck, every);
+        $(window).bind('beforeunload', function() {
+          clearInterval(Sammy.HashLocationProxy._interval);
+        });
+      }
+    }
+  };
+  
+  // The DataLocationProxy is an optional location proxy prototype. As opposed to
+  // the <tt>HashLocationProxy</tt> it gets its location from a jQuery.data attribute
+  // tied to the application's element. You can set the name of the attribute by
+  // passing a string as the second argument to the constructor. The default attribute
+  // name is 'sammy-location'. To read more about location proxies, check out the 
+  // documentation for <tt>Sammy.HashLocationProxy</tt>
+  Sammy.DataLocationProxy = function(app, data_name) {
+    this.app = app;
+    this.data_name = data_name || 'sammy-location';
+  };
+  
+  Sammy.DataLocationProxy.prototype = {
+    bind: function() {
+      var proxy = this;
+      this.app.$element().bind('setData', function(e, key, value) {
+        if (key == proxy.data_name) {
+          // jQuery unfortunately fires the event before it sets the value
+          // work around it, by setting the value ourselves
+          proxy.app.$element().each(function() { 
+            $.data(this, proxy.data_name, value);
+          });
+          proxy.app.trigger('location-changed');
+        }
+      });
+    },
+    
+    unbind: function() {
+      this.app.$element().unbind('setData');
+    },
+    
+    getLocation: function() {
+      return this.app.$element().data(this.data_name);
+    },
+    
+    setLocation: function(new_location) {
+      return this.app.$element().data(this.data_name, new_location);
+    }
+  };
+  
+  // Sammy.Application is the Base prototype for defining 'applications'.
+  // An 'application' is a collection of 'routes' and bound events that is
+  // attached to an element when <tt>run()</tt> is called.
+  // The only argument an 'app_function' is evaluated within the context of the application.
+  Sammy.Application = function(app_function) {
+    var app = this;
+    this.routes            = {};
+    this.listeners         = new Sammy.Object({});
+    this.arounds           = [];
+    this.befores           = [];
+    this.namespace         = this.uuid();
+    this.context_prototype = function() { Sammy.EventContext.apply(this, arguments); };
+    this.context_prototype.prototype = new Sammy.EventContext();
+
+    if ($.isFunction(app_function)) {
+      app_function.apply(this, [this]);
+    }
+    // set the location proxy if not defined to the default (HashLocationProxy)
+    if (!this.location_proxy) {
+      this.location_proxy = new Sammy.HashLocationProxy(app, this.run_interval_every);
+    }
+    if (this.debug) {
+      this.bindToAllEvents(function(e, data) {
+        app.log(app.toString(), e.cleaned_type, data || {});
+      });
+    }
+  };
+  
+  Sammy.Application.prototype = $.extend({}, Sammy.Object.prototype, {
+    
+    // the four route verbs
+    ROUTE_VERBS: ['get','post','put','delete'],
+    
+    // An array of the default events triggered by the 
+    // application during its lifecycle
+    APP_EVENTS: ['run','unload','lookup-route','run-route','route-found','event-context-before','event-context-after','changed','error','check-form-submission','redirect'],
+    
+    _last_route: null,
+    _running: false,
+        
+    // Defines what element the application is bound to. Provide a selector 
+    // (parseable by <tt>jQuery()</tt>) and this will be used by <tt>$element()</tt>
+    element_selector: 'body',
+    
+    // When set to true, logs all of the default events using <tt>log()</tt>
+    debug: false,
+    
+    // When set to true, and the error() handler is not overriden, will actually
+    // raise JS errors in routes (500) and when routes can't be found (404)
+    raise_errors: false,
+    
+    // The time in milliseconds that the URL is queried for changes
+    run_interval_every: 50, 
+    
+    // The location proxy for the current app. By default this is set to a new
+    // <tt>Sammy.HashLocationProxy</tt> on initialization. However, you can set
+    // the location_proxy inside you're app function to give youre app a custom
+    // location mechanism
+    location_proxy: null,
+    
+    // The default template engine to use when using <tt>partial()</tt> in an 
+    // <tt>EventContext</tt>. <tt>template_engine</tt> can either be a string that 
+    // corresponds to the name of a method/helper on EventContext or it can be a function
+    // that takes two arguments, the content of the unrendered partial and an optional
+    // JS object that contains interpolation data. Template engine is only called/refered
+    // to if the extension of the partial is null or unknown. See <tt>partial()</tt>
+    // for more information
+    template_engine: null,
+        
+    // //=> Sammy.Application: body
+    toString: function() {
+      return 'Sammy.Application:' + this.element_selector;
+    },
+    
+    // returns a jQuery object of the Applications bound element.
+    $element: function() {
+      return $(this.element_selector);
+    },
+    
+    // <tt>use()</tt> is the entry point for including Sammy plugins.
+    // The first argument to use should be a function() that is evaluated 
+    // in the context of the current application, just like the <tt>app_function</tt>
+    // argument to the <tt>Sammy.Application</tt> constructor.
+    //
+    // Any additional arguments are passed to the app function sequentially.
+    //
+    // For much more detail about plugins, check out: 
+    // http://code.quirkey.com/sammy/doc/plugins.html
+    // 
+    // === Example
+    //
+    //      var MyPlugin = function(app, prepend) {
+    //
+    //        this.helpers({
+    //          myhelper: function(text) {
+    //            alert(prepend + " " + text);
+    //          }
+    //        });
+    //  
+    //      };
+    //
+    //      var app = $.sammy(function() {
+    // 
+    //        this.use(MyPlugin, 'This is my plugin');
+    //  
+    //        this.get('#/', function() {
+    //          this.myhelper('and dont you forget it!'); 
+    //          //=> Alerts: This is my plugin and dont you forget it!
+    //        });
+    //
+    //      });
+    //
+    use: function() {
+      // flatten the arguments
+      var args = $.makeArray(arguments);
+      var plugin = args.shift();
+      try {
+        args.unshift(this);
+        plugin.apply(this, args);
+      } catch(e) {
+        if (typeof plugin == 'undefined') {
+          this.error("Plugin Error: called use() but plugin is not defined", e);
+        } else if (!$.isFunction(plugin)) {
+          this.error("Plugin Error: called use() but '" + plugin.toString() + "' is not a function", e);
+        } else {
+          this.error("Plugin Error", e);
+        }
+      }
+      return this;
+    },
+    
+    // <tt>route()</tt> is the main method for defining routes within an application.
+    // For great detail on routes, check out: http://code.quirkey.com/sammy/doc/routes.html
+    //
+    // This method also has aliases for each of the different verbs (eg. <tt>get()</tt>, <tt>post()</tt>, etc.)
+    //
+    // === Arguments
+    //
+    // +verb+::     A String in the set of ROUTE_VERBS or 'any'. 'any' will add routes for each
+    //              of the ROUTE_VERBS. If only two arguments are passed, 
+    //              the first argument is the path, the second is the callback and the verb
+    //              is assumed to be 'any'.
+    // +path+::     A Regexp or a String representing the path to match to invoke this verb.
+    // +callback+:: A Function that is called/evaluated whent the route is run see: <tt>runRoute()</tt>.
+    //              It is also possible to pass a string as the callback, which is looked up as the name
+    //              of a method on the application.
+    //
+    route: function(verb, path, callback) {
+      var app = this, param_names = [], add_route;
+      
+      // if the method signature is just (path, callback)
+      // assume the verb is 'any'
+      if (!callback && $.isFunction(path)) {
+        path = verb;
+        callback = path;
+        verb = 'any';
+      }
+      
+      verb = verb.toLowerCase(); // ensure verb is lower case
+      
+      // if path is a string turn it into a regex
+      if (path.constructor == String) {
+        
+        // Needs to be explicitly set because IE will maintain the index unless NULL is returned,
+        // 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
+        // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp/lastIndex        
+        PATH_NAME_MATCHER.lastIndex = 0;
+        
+        // find the names
+        while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) {
+          param_names.push(path_match[1]);
+        }
+        // replace with the path replacement
+        path = new RegExp("^" + path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
+      }
+      // lookup callback
+      if (typeof callback == 'string') {
+        callback = app[callback];
+      }
+      
+      add_route = function(with_verb) {
+        var r = {verb: with_verb, path: path, callback: callback, param_names: param_names};
+        // add route to routes array
+        app.routes[with_verb] = app.routes[with_verb] || [];
+        // place routes in order of definition
+        app.routes[with_verb].push(r);
+      };
+      
+      if (verb === 'any') {
+        $.each(this.ROUTE_VERBS, function(i, v) { add_route(v); });
+      } else {
+        add_route(verb);
+      }
+      
+      // return the app
+      return this;
+    },
+    
+    // Alias for route('get', ...)
+    get: _routeWrapper('get'),
+    
+    // Alias for route('post', ...)
+    post: _routeWrapper('post'),
+
+    // Alias for route('put', ...)
+    put: _routeWrapper('put'),
+    
+    // Alias for route('delete', ...)
+    del: _routeWrapper('delete'),
+    
+    // Alias for route('any', ...)
+    any: _routeWrapper('any'),
+    
+    // <tt>mapRoutes</tt> takes an array of arrays, each array being passed to route()
+    // as arguments, this allows for mass definition of routes. Another benefit is
+    // this makes it possible/easier to load routes via remote JSON.
+    //
+    // === Example
+    //
+    //    var app = $.sammy(function() {
+    //      
+    //      this.mapRoutes([
+    //          ['get', '#/', function() { this.log('index'); }],
+    //          // strings in callbacks are looked up as methods on the app
+    //          ['post', '#/create', 'addUser'],
+    //          // No verb assumes 'any' as the verb
+    //          [/dowhatever/, function() { this.log(this.verb, this.path)}];
+    //        ]);
+    //    })
+    //
+    mapRoutes: function(route_array) {
+      var app = this;
+      $.each(route_array, function(i, route_args) {
+        app.route.apply(app, route_args);
+      });
+      return this;
+    },
+    
+    // A unique event namespace defined per application.
+    // All events bound with <tt>bind()</tt> are automatically bound within this space.
+    eventNamespace: function() {
+      return ['sammy-app', this.namespace].join('-');
+    },
+    
+    // Works just like <tt>jQuery.fn.bind()</tt> with a couple noteable differences.
+    //
+    // * It binds all events to the application element
+    // * All events are bound within the <tt>eventNamespace()</tt>
+    // * Events are not actually bound until the application is started with <tt>run()</tt>
+    // * callbacks are evaluated within the context of a Sammy.EventContext
+    //
+    // See http://code.quirkey.com/sammy/docs/events.html for more info.
+    //
+    bind: function(name, data, callback) {
+      var app = this;
+      // build the callback
+      // if the arity is 2, callback is the second argument
+      if (typeof callback == 'undefined') { callback = data; }
+      var listener_callback =  function() {
+        // pull off the context from the arguments to the callback
+        var e, context, data; 
+        e       = arguments[0];
+        data    = arguments[1];        
+        if (data && data.context) {
+          context = data.context;
+          delete data.context;
+        } else {
+          context = new app.context_prototype(app, 'bind', e.type, data);
+        }
+        e.cleaned_type = e.type.replace(app.eventNamespace(), '');
+        callback.apply(context, [e, data]);
+      };
+      
+      // it could be that the app element doesnt exist yet
+      // so attach to the listeners array and then run()
+      // will actually bind the event.
+      if (!this.listeners[name]) { this.listeners[name] = []; }
+      this.listeners[name].push(listener_callback);
+      if (this.isRunning()) {
+        // if the app is running
+        // *actually* bind the event to the app element
+        this._listen(name, listener_callback);
+      }
+      return this;
+    },
+    
+    // Triggers custom events defined with <tt>bind()</tt>
+    //
+    // === Arguments
+    // 
+    // +name+::     The name of the event. Automatically prefixed with the <tt>eventNamespace()</tt>
+    // +data+::     An optional Object that can be passed to the bound callback.
+    // +context+::  An optional context/Object in which to execute the bound callback. 
+    //              If no context is supplied a the context is a new <tt>Sammy.EventContext</tt>
+    //
+    trigger: function(name, data) {
+      this.$element().trigger([name, this.eventNamespace()].join('.'), [data]);
+      return this;
+    },
+    
+    // Reruns the current route
+    refresh: function() {
+      this.last_location = null;
+      this.trigger('location-changed');
+      return this;
+    },
+    
+    // Takes a single callback that is pushed on to a stack.
+    // Before any route is run, the callbacks are evaluated in order within 
+    // the current <tt>Sammy.EventContext</tt>
+    //
+    // If any of the callbacks explicitly return false, execution of any 
+    // further callbacks and the route itself is halted.
+    // 
+    // You can also provide a set of options that will define when to run this
+    // before based on the route it proceeds. 
+    //
+    // === Example
+    //
+    //      var app = $.sammy(function() {
+    //        
+    //        // will run at #/route but not at #/
+    //        this.before('#/route', function() {
+    //          //...
+    //        });
+    //        
+    //        // will run at #/ but not at #/route
+    //        this.before({except: {path: '#/route'}}, function() {
+    //          this.log('not before #/route');
+    //        });
+    //        
+    //        this.get('#/', function() {});
+    //        
+    //        this.get('#/route', function() {});
+    //        
+    //      });
+    //      
+    // See <tt>contextMatchesOptions()</tt> for a full list of supported options
+    //
+    before: function(options, callback) {
+      if ($.isFunction(options)) {
+        callback = options;
+        options = {};
+      }
+      this.befores.push([options, callback]);
+      return this;
+    },
+    
+    // A shortcut for binding a callback to be run after a route is executed.
+    // After callbacks have no guarunteed order.
+    after: function(callback) {
+      return this.bind('event-context-after', callback);
+    },
+    
+    
+    // Adds an around filter to the application. around filters are functions
+    // that take a single argument <tt>callback</tt> which is the entire route 
+    // execution path wrapped up in a closure. This means you can decide whether
+    // or not to proceed with execution by not invoking <tt>callback</tt> or, 
+    // more usefuly wrapping callback inside the result of an asynchronous execution.
+    //
+    // === Example
+    //
+    // The most common use case for around() is calling a _possibly_ async function
+    // and executing the route within the functions callback:
+    //
+    //      var app = $.sammy(function() {
+    //        
+    //        var current_user = false;
+    //        
+    //        function checkLoggedIn(callback) {
+    //          // /session returns a JSON representation of the logged in user 
+    //          // or an empty object
+    //          if (!current_user) {
+    //            $.getJSON('/session', function(json) {
+    //              if (json.login) {
+    //                // show the user as logged in
+    //                current_user = json;
+    //                // execute the route path
+    //                callback();
+    //              } else {
+    //                // show the user as not logged in
+    //                current_user = false;
+    //                // the context of aroundFilters is an EventContext
+    //                this.redirect('#/login');
+    //              }
+    //            });
+    //          } else {
+    //            // execute the route path
+    //            callback();
+    //          }
+    //        };
+    //        
+    //        this.around(checkLoggedIn);
+    //        
+    //      });
+    //
+    around: function(callback) {
+      this.arounds.push(callback);
+      return this;
+    },
+    
+    // Returns a boolean of weather the current application is running.
+    isRunning: function() {
+      return this._running;
+    },
+    
+    // Helpers extends the EventContext prototype specific to this app.
+    // This allows you to define app specific helper functions that can be used
+    // whenever you're inside of an event context (templates, routes, bind).
+    // 
+    // === Example
+    //
+    //    var app = $.sammy(function() {
+    //      
+    //      helpers({
+    //        upcase: function(text) {
+    //         return text.toString().toUpperCase();
+    //        }
+    //      });
+    //      
+    //      get('#/', function() { with(this) {
+    //        // inside of this context I can use the helpers
+    //        $('#main').html(upcase($('#main').text());
+    //      }});
+    //      
+    //    });
+    //
+    //    
+    // === Arguments
+    // 
+    // +extensions+:: An object collection of functions to extend the context.
+    //  
+    helpers: function(extensions) {
+      $.extend(this.context_prototype.prototype, extensions);
+      return this;
+    },
+    
+    // Helper extends the event context just like <tt>helpers()</tt> but does it
+    // a single method at a time. This is especially useful for dynamically named 
+    // helpers
+    // 
+    // === Example
+    //     
+    //     // Trivial example that adds 3 helper methods to the context dynamically
+    //     var app = $.sammy(function(app) {
+    //       
+    //       $.each([1,2,3], function(i, num) {
+    //         app.helper('helper' + num, function() {
+    //           this.log("I'm helper number " + num);
+    //         }); 
+    //       });
+    //       
+    //       this.get('#/', function() {
+    //         this.helper2(); //=> I'm helper number 2
+    //       });
+    //     });
+    //     
+    // === Arguments
+    // 
+    // +name+:: The name of the method
+    // +method+:: The function to be added to the prototype at <tt>name</tt>
+    //
+    helper: function(name, method) {
+      this.context_prototype.prototype[name] = method;
+      return this;
+    },
+    
+    // Actually starts the application's lifecycle. <tt>run()</tt> should be invoked
+    // within a document.ready block to ensure the DOM exists before binding events, etc.
+    //
+    // === Example
+    // 
+    //    var app = $.sammy(function() { ... }); // your application
+    //    $(function() { // document.ready
+    //        app.run();
+    //     });
+    //
+    // === Arguments
+    //
+    // +start_url+::  "value", Optionally, a String can be passed which the App will redirect to 
+    //                after the events/routes have been bound.
+    run: function(start_url) {
+      if (this.isRunning()) { return false; }
+      var app = this;
+      
+      // actually bind all the listeners
+      $.each(this.listeners.toHash(), function(name, callbacks) {
+        $.each(callbacks, function(i, listener_callback) {
+          app._listen(name, listener_callback);
+        });
+      });
+      
+      this.trigger('run', {start_url: start_url});
+      this._running = true;
+      // set last location
+      this.last_location = null;
+      if (this.getLocation() == '' && typeof start_url != 'undefined') {
+        this.setLocation(start_url);
+      } 
+      // check url
+      this._checkLocation();
+      this.location_proxy.bind();
+      this.bind('location-changed', function() {
+        app._checkLocation();
+      });
+      
+      // bind to submit to capture post/put/delete routes
+      this.bind('submit', function(e) {
+        var returned = app._checkFormSubmission($(e.target).closest('form'));
+        return (returned === false) ? e.preventDefault() : false;
+      });
+
+      // bind unload to body unload
+      $(window).bind('beforeunload', function() {
+        app.unload();
+      });
+      
+      // trigger html changed
+      return this.trigger('changed');
+    },
+    
+    // The opposite of <tt>run()</tt>, un-binds all event listeners and intervals
+    // <tt>run()</tt> Automaticaly binds a <tt>onunload</tt> event to run this when
+    // the document is closed.
+    unload: function() {
+      if (!this.isRunning()) { return false; }
+      var app = this;
+      this.trigger('unload');
+      // clear interval
+      this.location_proxy.unbind();
+      // unbind form submits
+      this.$element().unbind('submit').removeClass(app.eventNamespace());
+      // unbind all events
+      $.each(this.listeners.toHash() , function(name, listeners) {
+        $.each(listeners, function(i, listener_callback) {
+          app._unlisten(name, listener_callback);
+        });
+      });
+      this._running = false;
+      return this;
+    },
+    
+    // Will bind a single callback function to every event that is already 
+    // being listened to in the app. This includes all the <tt>APP_EVENTS</tt>
+    // as well as any custom events defined with <tt>bind()</tt>.
+    // 
+    // Used internally for debug logging.
+    bindToAllEvents: function(callback) {
+      var app = this;
+      // bind to the APP_EVENTS first
+      $.each(this.APP_EVENTS, function(i, e) {
+        app.bind(e, callback);
+      });
+      // next, bind to listener names (only if they dont exist in APP_EVENTS)
+      $.each(this.listeners.keys(true), function(i, name) {
+        if (app.APP_EVENTS.indexOf(name) == -1) {
+          app.bind(name, callback);
+        }
+      });
+      return this;
+    },
+
+    // Returns a copy of the given path with any query string after the hash
+    // removed.
+    routablePath: function(path) {
+      return path.replace(QUERY_STRING_MATCHER, '');
+    },
+    
+    // Given a verb and a String path, will return either a route object or false
+    // if a matching route can be found within the current defined set. 
+    lookupRoute: function(verb, path) {
+      var app = this, routed = false;
+      this.trigger('lookup-route', {verb: verb, path: path});
+      if (typeof this.routes[verb] != 'undefined') {
+        $.each(this.routes[verb], function(i, route) {
+          if (app.routablePath(path).match(route.path)) {
+            routed = route;
+            return false;
+          }
+        });
+      }
+      return routed;
+    },
+
+    // First, invokes <tt>lookupRoute()</tt> and if a route is found, parses the 
+    // possible URL params and then invokes the route's callback within a new
+    // <tt>Sammy.EventContext</tt>. If the route can not be found, it calls 
+    // <tt>notFound()</tt>. If <tt>raise_errors</tt> is set to <tt>true</tt> and 
+    // the <tt>error()</tt> has not been overriden, it will throw an actual JS
+    // error. 
+    //
+    // You probably will never have to call this directly.
+    //
+    // === Arguments
+    // 
+    // +verb+:: A String for the verb.
+    // +path+:: A String path to lookup.
+    // +params+:: An Object of Params pulled from the URI or passed directly.
+    //
+    // === Returns
+    //
+    // Either returns the value returned by the route callback or raises a 404 Not Found error.
+    //
+    runRoute: function(verb, path, params) {
+      var app = this, 
+          route = this.lookupRoute(verb, path),
+          context, 
+          wrapped_route, 
+          arounds, 
+          around, 
+          befores, 
+          before, 
+          callback_args, 
+          final_returned;
+
+      this.log('runRoute', [verb, path].join(' '));
+      this.trigger('run-route', {verb: verb, path: path, params: params});
+      if (typeof params == 'undefined') { params = {}; }
+
+      $.extend(params, this._parseQueryString(path));
+      
+      if (route) {
+        this.trigger('route-found', {route: route});
+        // pull out the params from the path
+        if ((path_params = route.path.exec(this.routablePath(path))) !== null) {
+          // first match is the full path
+          path_params.shift();
+          // for each of the matches
+          $.each(path_params, function(i, param) {
+            // if theres a matching param name
+            if (route.param_names[i]) {
+              // set the name to the match
+              params[route.param_names[i]] = _decode(param);
+            } else {
+              // initialize 'splat'
+              if (!params.splat) { params.splat = []; }
+              params.splat.push(_decode(param));
+            }
+          });
+        }
+        
+        // set event context
+        context  = new this.context_prototype(this, verb, path, params);
+        // ensure arrays
+        arounds = this.arounds.slice(0);  
+        befores = this.befores.slice(0);
+        // set the callback args to the context + contents of the splat
+        callback_args = [context].concat(params.splat);
+        // wrap the route up with the before filters
+        wrapped_route = function() {
+          var returned;
+          while (befores.length > 0) {
+            before = befores.shift();
+            // check the options
+            if (app.contextMatchesOptions(context, before[0])) {
+              returned = before[1].apply(context, [context]);
+              if (returned === false) { return false; }
+            }
+          }
+          app.last_route = route;
+          context.trigger('event-context-before', {context: context});
+          returned = route.callback.apply(context, callback_args);
+          context.trigger('event-context-after', {context: context});
+          return returned;
+        };
+        $.each(arounds.reverse(), function(i, around) {
+          var last_wrapped_route = wrapped_route;
+          wrapped_route = function() { return around.apply(context, [last_wrapped_route]); };
+        });
+        try {
+          final_returned = wrapped_route();
+        } catch(e) {
+          this.error(['500 Error', verb, path].join(' '), e);
+        }
+        return final_returned;
+      } else {
+        return this.notFound(verb, path);
+      }
+    },
+    
+    // Matches an object of options against an <tt>EventContext</tt> like object that
+    // contains <tt>path</tt> and <tt>verb</tt> attributes. Internally Sammy uses this
+    // for matching <tt>before()</tt> filters against specific options. You can set the 
+    // object to _only_ match certain paths or verbs, or match all paths or verbs _except_
+    // those that match the options.
+    //
+    // === Example
+    //   
+    //     var app = $.sammy(),
+    //         context = {verb: 'get', path: '#/mypath'};
+    //     
+    //     // match against a path string
+    //     app.contextMatchesOptions(context, '#/mypath'); //=> true
+    //     app.contextMatchesOptions(context, '#/otherpath'); //=> false
+    //     // equivilent to
+    //     app.contextMatchesOptions(context, {only: {path:'#/mypath'}}); //=> true
+    //     app.contextMatchesOptions(context, {only: {path:'#/otherpath'}}); //=> false
+    //     // match against a path regexp
+    //     app.contextMatchesOptions(context, /path/); //=> true
+    //     app.contextMatchesOptions(context, /^path/); //=> false
+    //     // match only a verb
+    //     app.contextMatchesOptions(context, {only: {verb:'get'}}); //=> true
+    //     app.contextMatchesOptions(context, {only: {verb:'post'}}); //=> false
+    //     // match all except a verb
+    //     app.contextMatchesOptions(context, {except: {verb:'post'}}); //=> true
+    //     app.contextMatchesOptions(context, {except: {verb:'get'}}); //=> false
+    //     // match all except a path
+    //     app.contextMatchesOptions(context, {except: {path:'#/otherpath'}}); //=> true
+    //     app.contextMatchesOptions(context, {except: {path:'#/mypath'}}); //=> false
+    //   
+    contextMatchesOptions: function(context, match_options, positive) {
+      // empty options always match
+      var options = match_options;
+      if (typeof options === 'undefined' || options == {}) {
+        return true;
+      }
+      if (typeof positive === 'undefined') {
+        positive = true;
+      }
+      // normalize options
+      if (typeof options === 'string' || $.isFunction(options.test)) {
+        options = {path: options};
+      }
+      if (options.only) {
+        return this.contextMatchesOptions(context, options.only, true);
+      } else if (options.except) {
+        return this.contextMatchesOptions(context, options.except, false);  
+      }
+      var path_matched = true, verb_matched = true;
+      if (options.path) {
+        // wierd regexp test
+        if ($.isFunction(options.path.test)) {
+          path_matched = options.path.test(context.path);
+        } else {
+          path_matched = (options.path.toString() === context.path);
+        }
+      }
+      if (options.verb) {
+        verb_matched = options.verb === context.verb;
+      }
+      return positive ? (verb_matched && path_matched) : !(verb_matched && path_matched);
+    },
+    
+
+    // Delegates to the <tt>location_proxy</tt> to get the current location.
+    // See <tt>Sammy.HashLocationProxy</tt> for more info on location proxies.
+    getLocation: function() {
+      return this.location_proxy.getLocation();
+    },
+    
+    // Delegates to the <tt>location_proxy</tt> to set the current location.
+    // See <tt>Sammy.HashLocationProxy</tt> for more info on location proxies.
+    //
+    // === Arguments
+    // 
+    // +new_location+:: A new location string (e.g. '#/')
+    //
+    setLocation: function(new_location) {
+      return this.location_proxy.setLocation(new_location);
+    },
+    
+    // Swaps the content of <tt>$element()</tt> with <tt>content</tt>
+    // You can override this method to provide an alternate swap behavior
+    // for <tt>EventContext.partial()</tt>.
+    // 
+    // === Example
+    //
+    //    var app = $.sammy(function() {
+    //      
+    //      // implements a 'fade out'/'fade in'
+    //      this.swap = function(content) {
+    //        this.$element().hide('slow').html(content).show('slow');
+    //      }
+    //      
+    //      get('#/', function() {
+    //        this.partial('index.html.erb') // will fade out and in
+    //      });
+    //      
+    //    });
+    //
+    swap: function(content) {
+      return this.$element().html(content);
+    },
+    
+    // This thows a '404 Not Found' error by invoking <tt>error()</tt>. 
+    // Override this method or <tt>error()</tt> to provide custom
+    // 404 behavior (i.e redirecting to / or showing a warning)
+    notFound: function(verb, path) {
+      var ret = this.error(['404 Not Found', verb, path].join(' '));
+      return (verb === 'get') ? ret : true;
+    },
+    
+    // The base error handler takes a string <tt>message</tt> and an <tt>Error</tt>
+    // object. If <tt>raise_errors</tt> is set to <tt>true</tt> on the app level,
+    // this will re-throw the error to the browser. Otherwise it will send the error
+    // to <tt>log()</tt>. Override this method to provide custom error handling
+    // e.g logging to a server side component or displaying some feedback to the
+    // user.
+    error: function(message, original_error) {
+      if (!original_error) { original_error = new Error(); }
+      original_error.message = [message, original_error.message].join(' ');
+      this.trigger('error', {message: original_error.message, error: original_error});
+      if (this.raise_errors) {
+        throw(original_error);
+      } else {
+        this.log(original_error.message, original_error);
+      }
+    },
+    
+    _checkLocation: function() {
+      var location, returned;
+      // get current location
+      location = this.getLocation();
+      // compare to see if hash has changed
+      if (location != this.last_location) {
+        // lookup route for current hash
+        returned = this.runRoute('get', location);
+      }
+      // reset last location
+      this.last_location = location;
+      return returned;
+    },
+    
+    _checkFormSubmission: function(form) {
+      var $form, path, verb, params, returned;
+      this.trigger('check-form-submission', {form: form});
+      $form = $(form);
+      path  = $form.attr('action');
+      verb  = $.trim($form.attr('method').toString().toLowerCase());
+      if (!verb || verb == '') { verb = 'get'; }
+      this.log('_checkFormSubmission', $form, path, verb);
+      params = $.extend({}, this._parseFormParams($form), {'$form': $form});
+      returned = this.runRoute(verb, path, params);
+      return (typeof returned == 'undefined') ? false : returned;
+    },
+    
+    _parseFormParams: function($form) {
+      var params = {};
+      $.each($form.serializeArray(), function(i, field) {
+        if (params[field.name]) {
+          if ($.isArray(params[field.name])) {
+            params[field.name].push(field.value);
+          } else {
+            params[field.name] = [params[field.name], field.value];
+          }
+        } else {