changeset 15:238299b93f4c

Made all Javascript code use RequireJS.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 30 Dec 2012 23:15:32 -0800
parents 0a0a98b97e9c
children 8d6c2a5ed08d
files wikked/static/js/require.js wikked/static/js/wikked.js wikked/static/js/wikked/app.js wikked/static/js/wikked/client.js wikked/static/js/wikked/handlebars.js wikked/static/js/wikked/models.js wikked/static/js/wikked/util.js wikked/static/js/wikked/views.js wikked/templates/index.html
diffstat 9 files changed, 894 insertions(+), 704 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/require.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,35 @@
+/*
+ RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ Available via the MIT or new BSD license.
+ see: http://github.com/jrburke/requirejs for details
+*/
+var requirejs,require,define;
+(function(Y){function H(b){return"[object Function]"===L.call(b)}function I(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function M(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function r(b,c){return da.call(b,c)}function i(b,c){return r(b,c)&&b[c]}function E(b,c){for(var d in b)if(r(b,d)&&c(b[d],d))break}function Q(b,c,d,i){c&&E(c,function(c,h){if(d||!r(b,h))i&&"string"!==typeof c?(b[h]||(b[h]={}),Q(b[h],
+c,d,i)):b[h]=c});return b}function t(b,c){return function(){return c.apply(b,arguments)}}function Z(b){if(!b)return b;var c=Y;x(b.split("."),function(b){c=c[b]});return c}function J(b,c,d,i){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=i;d&&(c.originalError=d);return c}function ea(b){function c(a,g,v){var e,n,b,c,d,j,f,h=g&&g.split("/");e=h;var l=m.map,k=l&&l["*"];if(a&&"."===a.charAt(0))if(g){e=i(m.pkgs,g)?h=[g]:h.slice(0,h.length-1);g=a=e.concat(a.split("/"));
+for(e=0;g[e];e+=1)if(n=g[e],"."===n)g.splice(e,1),e-=1;else if(".."===n)if(1===e&&(".."===g[2]||".."===g[0]))break;else 0<e&&(g.splice(e-1,2),e-=2);e=i(m.pkgs,g=a[0]);a=a.join("/");e&&a===g+"/"+e.main&&(a=g)}else 0===a.indexOf("./")&&(a=a.substring(2));if(v&&(h||k)&&l){g=a.split("/");for(e=g.length;0<e;e-=1){b=g.slice(0,e).join("/");if(h)for(n=h.length;0<n;n-=1)if(v=i(l,h.slice(0,n).join("/")))if(v=i(v,b)){c=v;d=e;break}if(c)break;!j&&(k&&i(k,b))&&(j=i(k,b),f=e)}!c&&j&&(c=j,d=f);c&&(g.splice(0,d,
+c),a=g.join("/"))}return a}function d(a){z&&x(document.getElementsByTagName("script"),function(g){if(g.getAttribute("data-requiremodule")===a&&g.getAttribute("data-requirecontext")===j.contextName)return g.parentNode.removeChild(g),!0})}function y(a){var g=i(m.paths,a);if(g&&I(g)&&1<g.length)return d(a),g.shift(),j.require.undef(a),j.require([a]),!0}function f(a){var g,b=a?a.indexOf("!"):-1;-1<b&&(g=a.substring(0,b),a=a.substring(b+1,a.length));return[g,a]}function h(a,g,b,e){var n,u,d=null,h=g?g.name:
+null,l=a,m=!0,k="";a||(m=!1,a="_@r"+(L+=1));a=f(a);d=a[0];a=a[1];d&&(d=c(d,h,e),u=i(p,d));a&&(d?k=u&&u.normalize?u.normalize(a,function(a){return c(a,h,e)}):c(a,h,e):(k=c(a,h,e),a=f(k),d=a[0],k=a[1],b=!0,n=j.nameToUrl(k)));b=d&&!u&&!b?"_unnormalized"+(M+=1):"";return{prefix:d,name:k,parentMap:g,unnormalized:!!b,url:n,originalName:l,isDefine:m,id:(d?d+"!"+k:k)+b}}function q(a){var g=a.id,b=i(k,g);b||(b=k[g]=new j.Module(a));return b}function s(a,g,b){var e=a.id,n=i(k,e);if(r(p,e)&&(!n||n.defineEmitComplete))"defined"===
+g&&b(p[e]);else q(a).on(g,b)}function C(a,g){var b=a.requireModules,e=!1;if(g)g(a);else if(x(b,function(g){if(g=i(k,g))g.error=a,g.events.error&&(e=!0,g.emit("error",a))}),!e)l.onError(a)}function w(){R.length&&(fa.apply(F,[F.length-1,0].concat(R)),R=[])}function A(a,g,b){var e=a.map.id;a.error?a.emit("error",a.error):(g[e]=!0,x(a.depMaps,function(e,c){var d=e.id,h=i(k,d);h&&(!a.depMatched[c]&&!b[d])&&(i(g,d)?(a.defineDep(c,p[d]),a.check()):A(h,g,b))}),b[e]=!0)}function B(){var a,g,b,e,n=(b=1E3*m.waitSeconds)&&
+j.startTime+b<(new Date).getTime(),c=[],h=[],f=!1,l=!0;if(!T){T=!0;E(k,function(b){a=b.map;g=a.id;if(b.enabled&&(a.isDefine||h.push(b),!b.error))if(!b.inited&&n)y(g)?f=e=!0:(c.push(g),d(g));else if(!b.inited&&(b.fetched&&a.isDefine)&&(f=!0,!a.prefix))return l=!1});if(n&&c.length)return b=J("timeout","Load timeout for modules: "+c,null,c),b.contextName=j.contextName,C(b);l&&x(h,function(a){A(a,{},{})});if((!n||e)&&f)if((z||$)&&!U)U=setTimeout(function(){U=0;B()},50);T=!1}}function D(a){r(p,a[0])||
+q(h(a[0],null,!0)).init(a[1],a[2])}function G(a){var a=a.currentTarget||a.srcElement,b=j.onScriptLoad;a.detachEvent&&!V?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=j.onScriptError;(!a.detachEvent||V)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function K(){var a;for(w();F.length;){a=F.shift();if(null===a[0])return C(J("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));D(a)}}var T,W,j,N,U,m={waitSeconds:7,
+baseUrl:"./",paths:{},pkgs:{},shim:{},map:{},config:{}},k={},X={},F=[],p={},S={},L=1,M=1;N={require:function(a){return a.require?a.require:a.require=j.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=p[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m.config&&i(m.config,a.map.id)||{}},exports:p[a.map.id]}}};W=function(a){this.events=i(X,a.id)||{};this.map=a;this.shim=
+i(m.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};W.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=t(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=
+b)},fetch:function(){if(!this.fetched){this.fetched=!0;j.startTime=(new Date).getTime();var a=this.map;if(this.shim)j.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],t(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;S[a]||(S[a]=!0,j.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,n=this.factory;
+if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(H(n)){if(this.events.error)try{e=j.execCb(c,n,b,e)}catch(d){a=d}else e=j.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",C(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&
+!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(j,this.map,this.depMaps);delete k[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,f=j.makeRequire(a.parentMap,{enableBuildCallback:!0,
+skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(k,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error=
+a;a.requireModules=[b];E(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete k[a.map.id]});C(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(k){throw Error("fromText eval for "+d+" failed: "+k);}v&&(O=!0);this.depMaps.push(u);j.completeLoad(d);f([d],n)}),e.load(a.name,f,n,m)}));j.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a,
+b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=k[c];!r(N,c)&&(e&&!e.enabled)&&j.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(k,a.id);b&&!b.enabled&&j.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=
+this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};j={config:m,contextName:b,registry:k,defined:p,urlFetched:S,defQueue:F,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a,
+b){I(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=j.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)j.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments));
+return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function f(e,c,u){var i,m;d.enableBuildCallback&&(c&&H(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(H(c))return C(J("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](k[a.id]);if(l.get)return l.get(j,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?C(J("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();j.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap;
+m.init(e,c,u,{enabled:!0});B()});return f}d=d||{};Q(f,{isBrowser:z,toUrl:function(b){var d=b.lastIndexOf("."),g=null;-1!==d&&(g=b.substring(d,b.length),b=b.substring(0,d));return j.nameToUrl(c(b,a&&a.id,!0),g)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(k,b)}});a||(f.undef=function(b){w();var c=h(b,a,!0),d=i(k,b);delete p[b];delete S[c.url];delete X[b];d&&(d.events.defined&&(X[b]=d.events),delete k[b])});return f},enable:function(a){i(k,
+a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();F.length;){c=F.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(k,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:C(J("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}B()},nameToUrl:function(a,b){var c,d,h,f,j,k;if(l.jsExtRegExp.test(a))f=a+(b||"");else{c=m.paths;d=m.pkgs;f=a.split("/");for(j=f.length;0<j;j-=1)if(k=
+f.slice(0,j).join("/"),h=i(d,k),k=i(c,k)){I(k)&&(k=k[0]);f.splice(0,j,k);break}else if(h){c=a===h.name?h.location+"/"+h.main:h.location;f.splice(0,j,c);break}f=f.join("/");f+=b||(/\?/.test(f)?"":".js");f=("/"===f.charAt(0)||f.match(/^[\w\+\.\-]+:/)?"":m.baseUrl)+f}return m.urlArgs?f+((-1===f.indexOf("?")?"?":"&")+m.urlArgs):f},load:function(a,b){l.load(j,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ha.test((a.currentTarget||a.srcElement).readyState))P=
+null,a=G(a),j.completeLoad(a.id)},onScriptError:function(a){var b=G(a);if(!y(b.id))return C(J("scripterror","Script error",a,[b.id]))}};j.require=j.makeRequire();return j}var l,w,A,D,s,G,P,K,ba,ca,ia=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ja=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,aa=/\.js$/,ga=/^\.\//;w=Object.prototype;var L=w.toString,da=w.hasOwnProperty,fa=Array.prototype.splice,z=!!("undefined"!==typeof window&&navigator&&document),$=!z&&"undefined"!==typeof importScripts,ha=z&&
+"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,V="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),B={},q={},R=[],O=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(H(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!H(require)&&(q=require,require=void 0);l=requirejs=function(b,c,d,y){var f,h="_";!I(b)&&"string"!==typeof b&&(f=b,I(c)?(b=c,c=d,d=y):b=[]);f&&f.context&&(h=f.context);(y=i(B,h))||(y=B[h]=l.s.newContext(h));
+f&&y.configure(f);return y.require(b,c,d)};l.config=function(b){return l(b)};l.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=l);l.version="2.1.2";l.jsExtRegExp=/^\/|:|\?|\.js$/;l.isBrowser=z;w=l.s={contexts:B,newContext:ea};l({});x(["toUrl","undef","defined","specified"],function(b){l[b]=function(){var c=B._;return c.require[b].apply(c,arguments)}});if(z&&(A=w.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))A=
+w.head=D.parentNode;l.onError=function(b){throw b;};l.load=function(b,c,d){var i=b&&b.config||{},f;if(z)return f=i.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),f.type=i.scriptType||"text/javascript",f.charset="utf-8",f.async=!0,f.setAttribute("data-requirecontext",b.contextName),f.setAttribute("data-requiremodule",c),f.attachEvent&&!(f.attachEvent.toString&&0>f.attachEvent.toString().indexOf("[native code"))&&!V?(O=!0,f.attachEvent("onreadystatechange",
+b.onScriptLoad)):(f.addEventListener("load",b.onScriptLoad,!1),f.addEventListener("error",b.onScriptError,!1)),f.src=d,K=f,D?A.insertBefore(f,D):A.appendChild(f),K=null,f;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){A||(A=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(G=s.split("/"),ba=G.pop(),ca=G.length?G.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var i,
+f;"string"!==typeof b&&(d=c,c=b,b=null);I(c)||(d=c,c=[]);!c.length&&H(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),f=B[i.getAttribute("data-requirecontext")])}(f?f.defQueue:R).push([b,c,d])};define.amd=
+{jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this);
--- a/wikked/static/js/wikked.js	Sun Dec 30 20:08:11 2012 -0800
+++ b/wikked/static/js/wikked.js	Sun Dec 30 23:15:32 2012 -0800
@@ -1,713 +1,51 @@
 /**
- * Make Javascript suck less.
+ * RequireJS configuration.
+ *
+ * We need to alias/shim some of the libraries.
  */
-String.prototype.format = function() {
-    var args = arguments;
-    return this.replace(/\{(\d+)\}/g, function(match, number) { 
-        return typeof args[number] != 'undefined' ? args[number] : match;
-    });
-};
-
-this.Wikked = {};
-
-/**
- * Helper class to load template files
- * by name from the `tpl` directory.
- */
-var TemplateLoader = Wikked.TemplateLoader = {
-    loadedTemplates: {},
-    get: function(name, callback) {
-        if (name in this.loadedTemplates) {
-            callback(this.loadedTemplates[name]);
-        } else {
-            var $loader = this;
-            url = '/tpl/' + name + '.html' + '?' + (new Date()).getTime();
-            $.get(url, function(data) {
-                $loader.loadedTemplates[name] = data;
-                callback(data);
-            });
+require.config({
+    urlArgs: "bust=" + (new Date()).getTime(),
+    paths: {
+        jquery: 'jquery-1.8.3.min',
+        underscore: 'underscore-min',
+        backbone: 'backbone-min',
+        handlebars: 'handlebars-1.0.rc.1'
+    },
+    shim: {
+        'jquery': {
+            exports: '$'
+        },
+        'underscore': {
+            exports: '_'
+        },
+        'backbone': {
+            deps: ['underscore', 'jquery'],
+            exports: 'Backbone'
+        },
+        'handlebars': {
+            exports: 'Handlebars'
         }
     }
-};
-
-//-------------------------------------------------------------//
-
-/**
- * Handlebars helper: reverse iterator.
- */
-Handlebars.registerHelper('eachr', function(context, options) {
-    if (context === undefined) {
-        return '';
-    }
-    data = undefined;
-    if (options.data) {
-        data = Handlebars.createFrame(options.data);
-    }
-    var out = '';
-    for (var i=context.length - 1; i >= 0; i--) {
-        if (data !== undefined) {
-            data.index = (context.length - 1 - i);
-            data.rindex = i;
-        }
-        out += options.fn(context[i], { data: data });
-    }
-    return out;
-});
-
-/**
- * Would you believe Handlebars doesn't have an equality
- * operator?
- */
-Handlebars.registerHelper('ifeq', function(context, options) {
-	if (context == options.hash.to) {
-		return options.fn(this);
-    }
-	return options.inverse(this);
-});
-Handlebars.registerHelper('ifneq', function(context, options) {
-    if (context != options.hash.to) {
-        return options.fn(this);
-    }
-    return options.inverse(this);
 });
 
 //-------------------------------------------------------------//
 
 /**
- * Client-side Wikked.
- */
-var PageFormatter = Wikked.PageFormatter = {
-    formatLink: function(link) {
-        return link.toLowerCase().replace(/[^a-z0-9_\.\-\(\)\/]+/g, '-');
-    },
-    formatText: function(text) {
-        var $f = this;
-        text = text.replace(/^\[\[([a-z]+)\:\s*(.+)\]\]\s*$/m, function(m, a, b) {
-            var p = "<p><span class=\"preview-wiki-meta\">\n";
-            p += "<span class=\"meta-name\">" + a + "</span>";
-            p += "<span class=\"meta-value\">" + b + "</span>\n";
-            p += "</span></p>\n\n";
-            return p;
-        });
-        text = text.replace(/\[\[([^\|\]]+)\|([^\]]+)\]\]/g, function(m, a, b) {
-            var url = $f.formatLink(b);
-            return '[' + a + '](/#/read/' + url + ')';
-        });
-        text = text.replace(/\[\[([^\]]+\/)?([^\]]+)\]\]/g, function(m, a, b) {
-            var url = $f.formatLink(a + b);
-            return '[' + b + '](/#/read/' + url + ')';
-        });
-        return text;
-    }
-};
-
-//-------------------------------------------------------------//
-
-/**
- * Start the main app once the page is loaded.
+ * Entry point: run Backbone!
+ *
+ * We also import scripts like `handlebars` and `client` that
+ * are not used directly by anybody, but need to be evaluated.
  */
-$(function() {
-
-    /**
-     * Wiki page models.
-     */
-    var NavigationModel = Backbone.Model.extend({
-        idAttribute: 'path',
-        defaults: function() {
-            return {
-                path: "main-page",
-                action: "read",
-                user: false
-            };
-        },
-        initialize: function() {
-            this.on('change:path', function(model, path) {
-                model._onChangePath(path);
-            });
-            this._onChangePath(this.get('path'));
-            this.on('change:auth', function(model, auth) {
-                model._onChangeAuth(auth);
-            });
-            this._onChangeAuth(this.get('auth'));
-            return this;
-        },
-        _onChangePath: function(path) {
-            this.set('url_home', '/#/read/main-page');
-            this.set('url_read', '/#/read/' + path);
-            this.set('url_edit', '/#/edit/' + path);
-            this.set('url_hist', '/#/changes/' + path);
-            this.set('url_search', '/search');
-        },
-        _onChangeAuth: function(auth) {
-            if (auth) {
-                this.set('url_login', false);
-                this.set('url_logout', '/#/logout');
-                this.set('username', auth.username);
-            } else {
-                this.set('url_login', '/#/login');
-                this.set('url_logout', false);
-                this.set('username', false);
-            }
-        }
-    });
-
-    var FooterModel = Backbone.Model.extend({
-        defaults: function() {
-            return {
-                url_extras: [ { name: 'Home', url: '/' } ]
-            };
-        },
-        addExtraUrl: function(name, url, index) {
-            if (index === undefined) {
-                this.get('url_extras').push({ name: name, url: url });
-            } else {
-                this.get('url_extras').splice(index, 0, { name: name, url: url });
-            }
-        }
-    });
-
-    var LoginModel = Backbone.Model.extend({
-        doLogin: function(form) {
-            $.post('/api/user/login', $(form).serialize())
-                .success(function() {
-                    app.navigate('/', { trigger: true });
-                })
-                .error(function() {
-                    alert("Error while logging in...");
-                });
-        }
-    });
-
-    var PageModel = Backbone.Model.extend({
-        idAttribute: 'path',
-        defaults: function() {
-            return {
-                path: "main-page"
-            };
-        },
-        initialize: function() {
-            this.on('change:path', function(model, path) {
-                model._onChangePath(path);
-            });
-            this.on('change:text', function(model, text) {
-                model._onChangeText(text);
-            });
-            this._onChangePath(this.get('path'));
-            this._onChangeText('');
-            return this;
-        },
-        url: function() {
-            var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
-            if (this.isNew()) return base;
-            return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + this.id;
-        },
-        title: function() {
-            return this.getMeta('title');
-        },
-        getMeta: function(key) {
-            var meta = this.get('meta');
-            if (meta === undefined) {
-                return undefined;
-            }
-            return meta[key];
-        },
-        _onChangePath: function(path) {
-        },
-        _onChangeText: function(text) {
-            this.set('content', new Handlebars.SafeString(text));
-        }
-    });
-    
-    var PageStateModel = PageModel.extend({
-        urlRoot: '/api/state/'
-    });
-    
-    var MasterPageModel = PageModel.extend({
-        initialize: function() {
-            this.nav = new NavigationModel({ id: this.id });
-            this.footer = new FooterModel();
-            MasterPageModel.__super__.initialize.apply(this, arguments);
-            this.on('change:auth', function(model, auth) {
-                model._onChangeAuth(auth);
-            });
-            if (this.action !== undefined) {
-                this.nav.set('action', this.action);
-                this.footer.set('action', this.action);
-            }
-            return this;
-        },
-        _onChangePath: function(path) {
-            MasterPageModel.__super__._onChangePath.apply(this, arguments);
-            this.nav.set('path', path);
-        },
-        _onChangeAuth: function(auth) {
-            this.nav.set('auth', auth);
-        }
-    });
-
-    var PageReadModel = MasterPageModel.extend({
-        urlRoot: '/api/read/',
-        action: 'read',
-        _onChangePath: function(path) {
-            PageReadModel.__super__._onChangePath.apply(this, arguments);
-            this.footer.addExtraUrl('Pages Linking Here', '/#/inlinks/' + path, 1);
-            this.footer.addExtraUrl('JSON', '/api/read/' + path);
-        }
-    });
-
-    var PageSourceModel = MasterPageModel.extend({
-        urlRoot: '/api/raw/',
-        action: 'source'
-    });
-
-    var PageEditModel = MasterPageModel.extend({
-        urlRoot: '/api/edit/',
-        action: 'edit',
-        doEdit: function(form) {
-            var path = this.get('path');
-            $.post('/api/edit/' + path, $(form).serialize())
-                .success(function(data) {
-                    app.navigate('/read/' + path, { trigger: true });
-                })
-                .error(function() {
-                    alert('Error saving page...');
-                });
-        }
-    });
-
-    var PageHistoryModel = MasterPageModel.extend({
-        urlRoot: '/api/history/',
-        action: 'history',
-        _onChangePath: function(path) {
-            PageHistoryModel.__super__._onChangePath.apply(this, arguments);
-            this.set('url_rev', '/#/revision/' + path);
-            this.set('url_diffc', '/#/diff/c/' + path);
-            this.set('url_diffr', '/#/diff/r/' + path);
-        }
-    });
-
-    var PageRevisionModel = MasterPageModel.extend({
-        urlRoot: '/api/revision/',
-        idAttribute: 'path_and_rev',
-        action: 'revision',
-        defaults: function() {
-            return {
-                path: "main-page",
-                rev: "tip"
-            };
-        },
-        initialize: function() {
-            PageRevisionModel.__super__.initialize.apply(this, arguments);
-            this.on('change:path', function(model, path) {
-                model._onChangePathOrRev(path, model.get('rev'));
-            });
-            this.on('change:rev', function(model, rev) {
-                model._onChangePathOrRev(model.get('path'), rev);
-            });
-            this._onChangePathOrRev(this.get('path'), this.get('rev'));
-            return this;
-        },
-        _onChangePathOrRev: function(path, rev) {
-            this.set('path_and_rev', path + '/' + rev);
-            this.set('disp_rev', rev);
-            if (rev.match(/[a-f0-9]{40}/)) {
-                this.set('disp_rev', rev.substring(0, 8));
-            }
-        }
-    });
-
-    var PageDiffModel = MasterPageModel.extend({
-        urlRoot: '/api/diff/',
-        idAttribute: 'path_and_revs',
-        action: 'diff',
-        defaults: function() {
-            return {
-                path: "main-page",
-                rev1: "tip",
-                rev2: ""
-            };
-        },
-        initialize: function() {
-            PageDiffModel.__super__.initialize.apply(this, arguments);
-            this.on('change:path', function(model, path) {
-                model._onChangePathOrRevs(path, model.get('rev'));
-            });
-            this.on('change:rev1', function(model, rev1) {
-                model._onChangePathOrRevs(model.get('path'), rev1, model.get('rev2'));
-            });
-            this.on('change:rev2', function(model, rev2) {
-                model._onChangePathOrRevs(model.get('path'), model.get('rev1'), rev2);
-            });
-            this._onChangePathOrRevs(this.get('path'), this.get('rev1'), this.get('rev2'));
-            return this;
-        },
-        _onChangePathOrRevs: function(path, rev1, rev2) {
-            this.set('path_and_revs', path + '/' + rev1 + '/' + rev2);
-            if (!rev2) {
-                this.set('path_and_revs', path + '/' + rev1);
-            }
-            this.set('disp_rev1', rev1);
-            if (rev1 !== undefined && rev1.match(/[a-f0-9]{40}/)) {
-                this.set('disp_rev1', rev1.substring(0, 8));
-            }
-            this.set('disp_rev2', rev2);
-            if (rev2 !== undefined && rev2.match(/[a-f0-9]{40}/)) {
-                this.set('disp_rev2', rev2.substring(0, 8));
-            }
-        }
-    });
-
-    var IncomingLinksModel = MasterPageModel.extend({
-        urlRoot: '/api/inlinks/',
-        action: 'inlinks'
-    });
-
-    var WikiSearchModel = MasterPageModel.extend({
-        urlRoot: '/api/search/',
-        action: 'search',
-        title: function() {
-            return 'Search';
-        },
-        execute: function(query) {
-            var $model = this;
-            $.getJSON('/api/search', { q: query })
-                .success(function (data) {
-                    $model.set('hits', data.hits);
-                })
-                .error(function() {
-                    alert("Error searching...");
-                });
-        }
-    });
+require([
+        'wikked/app', 
+        'wikked/handlebars', 
+        'wikked/client', 
+        'backbone'
+        ],
+    function(app, hb, client, Backbone) {
 
-    /**
-     * Wiki page views.
-     */
-    var PageView = Backbone.View.extend({
-        tagName: 'div',
-        className: 'wrapper',
-        initialize: function() {
-            PageView.__super__.initialize.apply(this, arguments);
-            var $view = this;
-            this.model.on("change", function() { $view.render(); });
-            return this;
-        },
-        render: function(view) {
-            if (this.templateName !== undefined) {
-                this.renderTemplate(this.templateName, this.renderCallback);
-            }
-            this.renderTitle(this.titleFormat);
-            return this;
-        },
-        renderTemplate: function(tpl_name, callback) {
-            var $view = this;
-            TemplateLoader.get(tpl_name, function(src) {
-                var template = Handlebars.compile(src);
-                $view.$el.html(template($view.model.toJSON()));
-                if (callback !== undefined) {
-                    callback.call($view, $view, $view.model);
-                }
-            });
-        },
-        renderTitle: function(formatter) {
-            var title = this.model.title();
-            if (formatter !== undefined) {
-                title = formatter.call(this, title);
-            }
-            document.title = title;
-        }
-    });
-    _.extend(PageView, Backbone.Events);
-
-    var NavigationView = PageView.extend({
-        templateName: 'nav',
-        initialize: function() {
-            NavigationView.__super__.initialize.apply(this, arguments);
-            this.render();
-            return this;
-        },
-        render: function() {
-            this.renderTemplate('nav');
-        },
-        postRender: function() {
-            var $view = this;
-            this.$('#search').submit(function() {
-                app.navigate('/search/' + $(this.q).val(), { trigger: true });
-                return false;
-            });
-        }
-    });
-
-    var FooterView = PageView.extend({
-        templateName:  'footer',
-        initialize: function() {
-            FooterView.__super__.initialize.apply(this, arguments);
-            this.render();
-            return this;
-        },
-        render: function() {
-            this.renderTemplate('footer');
-        },
-        postRender: function() {
-        }
-    });
-
-    var LoginView = PageView.extend({
-        templateName: 'login',
-        initialize: function() {
-            LoginView.__super__.initialize.apply(this, arguments);
-            this.render();
-            return this;
-        },
-        render: function() {
-            this.renderTemplate('login', function(view, model) {
-                this.$('form#login').submit(function() {
-                    model.doLogin(this);
-                    return false;
-                });
-            });
-            document.title = 'Login';
-        }
-    });
-
-    var MasterPageView = PageView.extend({
-        initialize: function() {
-            MasterPageView.__super__.initialize.apply(this, arguments);
-            this.nav = new NavigationView({ model: this.model.nav });
-            this.footer = new FooterView({ model: this.model.footer });
-            this.render();
-            return this;
-        },
-        renderCallback: function(view, model) {
-            this.nav.$el.prependTo(this.$el);
-            this.nav.postRender();
-            this.footer.$el.appendTo(this.$el);
-            this.footer.postRender();
-        }
-    });
-
-    var PageReadView = MasterPageView.extend({
-        templateName: 'read-page',
-        initialize: function() {
-            PageReadView.__super__.initialize.apply(this, arguments);
-            // Also get the current state, and show a warning
-            // if the page is new or modified.
-            var stateModel = new PageStateModel({ path: this.model.get('path') });
-            stateModel.fetch({
-                success: function(model, response, options) {
-                    if (model.get('state') == 'new' || model.get('state') == 'modified') {
-                        TemplateLoader.get('state-warning', function(src) {
-                            var template = Handlebars.compile(src);
-                            var warning = $(template(model.toJSON()));
-                            warning.css('display', 'none');
-                            warning.prependTo($('#app'));
-                            warning.slideDown();
-                            $('.dismiss', warning).click(function() {
-                                warning.slideUp();
-                                return false;
-                            });
-                        });
-                    }
-                }
-            });
-            return this;
-        },
-        renderCallback: function(view, model) {
-            PageReadView.__super__.renderCallback.apply(this, arguments);
-            // Replace all wiki links with proper hyperlinks using the JS app's
-            // URL scheme.
-            this.$('a.wiki-link[data-wiki-url]').each(function(i) {
-                var jel = $(this);
-                if (jel.hasClass('missing'))
-                    jel.attr('href', '/#/edit/' + jel.attr('data-wiki-url'));
-                else
-                    jel.attr('href', '/#/read/' + jel.attr('data-wiki-url'));
-            });
-        }
-    });
-
-    var PageEditView = MasterPageView.extend({
-        templateName: 'edit-page',
-        renderCallback: function(view, model) {
-            PageEditView.__super__.renderCallback.apply(this, arguments);
-            this.$('#page-edit').submit(function() {
-                model.doEdit(this);
-                return false;
-            });
-        },
-        titleFormat: function(title) {
-            return 'Editing: ' + title;
-        }
-    });
+    var router = new app.Router();
+    Backbone.history.start();//{ pushState: true });
 
-    var PageHistoryView = MasterPageView.extend({
-        templateName: 'history-page',
-        renderCallback: function(view, model) {
-            PageHistoryView.__super__.renderCallback.apply(this, arguments);
-            this.$('#diff-page').submit(function() {
-                view._triggerDiff(this, model.get('path'));
-                return false;
-            });
-        },
-        titleFormat: function(title) {
-            return 'History: ' + title;
-        },
-        _triggerDiff: function(form, path) {
-            var rev1 = $('input[name=rev1]:checked', form).val();
-            var rev2 = $('input[name=rev2]:checked', form).val();
-            app.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2, { trigger: true });
-        }
-    });
-
-    var PageRevisionView = MasterPageView.extend({
-        templateName: 'revision-page',
-        titleFormat: function(title) {
-            return title + ' [' + this.model.get('rev') + ']';
-        }
-    });
-
-    var PageDiffView = MasterPageView.extend({
-        templateName: 'diff-page',
-        titleFormat: function(title) {
-            return title + ' [' + this.model.get('rev1') + '-' + this.model.get('rev2') + ']';
-        }
-    });
-
-    var IncomingLinksView = MasterPageView.extend({
-        templateName: 'inlinks-page',
-        titleFormat: function(title) {
-            return 'Incoming Links: ' + title;
-        }
-    });
-
-    var WikiSearchView = MasterPageView.extend({
-        templateName: 'search-results'
-    });
-
-    /**
-     * Main URL router.
-     */
-    var AppRouter = Backbone.Router.extend({
-        routes: {
-            'read/*path':           "readPage",
-            '':                     "readMainPage",
-            'edit/*path':           "editPage",
-            'changes/*path':        "showPageHistory",
-            'inlinks/*path':        "showIncomingLinks",
-            'revision/*path/:rev':  "readPageRevision",
-            'diff/c/*path/:rev':    "showDiffWithPrevious",
-            'diff/r/*path/:rev1/:rev2':"showDiff",
-            'search/:query':         "showSearchResults",
-            'login':                 "showLogin",
-            'logout':                "doLogout"
-        },
-        readPage: function(path) {
-            var view = new PageReadView({ 
-                el: $('#app'), 
-                model: new PageReadModel({ path: path })
-            });
-            view.model.fetch();
-            this.navigate('/read/' + path);
-        },
-        readMainPage: function() {
-            this.readPage('main-page');
-        },
-        editPage: function(path) {
-            var view = new PageEditView({ 
-                el: $('#app'),
-                model: new PageEditModel({ path: path })
-            });
-            view.model.fetch();
-            this.navigate('/edit/' + path);
-        },
-        showPageHistory: function(path) {
-            var view = new PageHistoryView({
-                el: $('#app'),
-                model: new PageHistoryModel({ path: path })
-            });
-            view.model.fetch();
-            this.navigate('/changes/' + path);
-        },
-        showIncomingLinks: function(path) {
-            var view = new IncomingLinksView({
-                el: $('#app'),
-                model: new IncomingLinksModel({ path: path })
-            });
-            view.model.fetch();
-            this.navigate('/inlinks/' + path);
-        },
-        readPageRevision: function(path, rev) {
-            var view = new PageRevisionView({
-                el: $('#app'),
-                rev: rev, 
-                model: new PageRevisionModel({ path: path, rev: rev })
-            });
-            view.model.fetch();
-            this.navigate('/revision/' + path + '/' + rev);
-        },
-        showDiffWithPrevious: function(path, rev) {
-            var view = new PageDiffView({
-                el: $('#app'),
-                rev1: rev,
-                model: new PageDiffModel({ path: path, rev1: rev })
-            });
-            view.model.fetch();
-            this.navigate('/diff/c/' + path + '/' + rev);
-        },
-        showDiff: function(path, rev1, rev2) {
-            var view = new PageDiffView({
-                el: $('#app'),
-                rev1: rev1,
-                rev2: rev2,
-                model: new PageDiffModel({ path: path, rev1: rev1, rev2: rev2 })
-            });
-            view.model.fetch();
-            this.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2);
-        },
-        showSearchResults: function(query) {
-            if (query === '') {
-                query = this.getQueryVariable('q');
-            }
-            var view = new WikiSearchView({
-                el: $('#app'),
-                model: new WikiSearchModel()
-            });
-            view.model.execute(query);
-            this.navigate('/search/' + query);
-        },
-        showLogin: function() {
-            var view = new LoginView({
-                el: $('#app'),
-                model: new LoginModel()
-            });
-            this.navigate('/login');
-        },
-        doLogout: function() {
-            $.post('/api/user/logout')
-                .success(function(data) {
-                    app.navigate('/', { trigger: true });
-                })
-                .error(function() {
-                    alert("Error logging out!");
-                });
-        },
-        getQueryVariable: function(variable) {
-            var query = window.location.search.substring(1);
-            var vars = query.split("&");
-            for (var i = 0; i < vars.length; i++) {
-                var pair = vars[i].split("=");
-                if (pair[0] == variable) {
-                    return unescape(pair[1]);
-                }
-            }
-            return false;
-        }
-    });
-
-    /**
-     * Launch!
-     */
-    var app = new AppRouter();
-    Backbone.history.start();//{ pushState: true });
 });
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/wikked/app.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,147 @@
+/**
+ * The main Wikked app/router.
+ */
+define([
+        'jquery',
+        'underscore',
+        'backbone',
+        './views',
+        './models'
+        ],
+    function($, _, Backbone, Views, Models) {
+
+    /**
+     * Main router.
+     */
+    var AppRouter = Backbone.Router.extend({
+        routes: {
+            'read/*path':           "readPage",
+            '':                     "readMainPage",
+            'edit/*path':           "editPage",
+            'changes/*path':        "showPageHistory",
+            'inlinks/*path':        "showIncomingLinks",
+            'revision/*path/:rev':  "readPageRevision",
+            'diff/c/*path/:rev':    "showDiffWithPrevious",
+            'diff/r/*path/:rev1/:rev2':"showDiff",
+            'search/:query':         "showSearchResults",
+            'login':                 "showLogin",
+            'logout':                "doLogout"
+        },
+        readPage: function(path) {
+            var view = new Views.PageReadView({ 
+                el: $('#app'), 
+                model: new Models.PageReadModel({ path: path })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/read/' + path);
+        },
+        readMainPage: function() {
+            this.readPage('main-page');
+        },
+        editPage: function(path) {
+            var view = new Views.PageEditView({ 
+                el: $('#app'),
+                model: new Models.PageEditModel({ path: path })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/edit/' + path);
+        },
+        showPageHistory: function(path) {
+            var view = new Views.PageHistoryView({
+                el: $('#app'),
+                model: new Models.PageHistoryModel({ path: path })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/changes/' + path);
+        },
+        showIncomingLinks: function(path) {
+            var view = new Views.IncomingLinksView({
+                el: $('#app'),
+                model: new Models.IncomingLinksModel({ path: path })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/inlinks/' + path);
+        },
+        readPageRevision: function(path, rev) {
+            var view = new Views.PageRevisionView({
+                el: $('#app'),
+                rev: rev, 
+                model: new Models.PageRevisionModel({ path: path, rev: rev })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/revision/' + path + '/' + rev);
+        },
+        showDiffWithPrevious: function(path, rev) {
+            var view = new Views.PageDiffView({
+                el: $('#app'),
+                rev1: rev,
+                model: new Models.PageDiffModel({ path: path, rev1: rev })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/diff/c/' + path + '/' + rev);
+        },
+        showDiff: function(path, rev1, rev2) {
+            var view = new Views.PageDiffView({
+                el: $('#app'),
+                rev1: rev1,
+                rev2: rev2,
+                model: new Models.PageDiffModel({ path: path, rev1: rev1, rev2: rev2 })
+            });
+            view.model.setApp(this);
+            view.model.fetch();
+            this.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2);
+        },
+        showSearchResults: function(query) {
+            if (query === '') {
+                query = this.getQueryVariable('q');
+            }
+            var view = new Views.WikiSearchView({
+                el: $('#app'),
+                model: new Models.WikiSearchModel()
+            });
+            view.model.setApp(this);
+            view.model.execute(query);
+            this.navigate('/search/' + query);
+        },
+        showLogin: function() {
+            var view = new Views.LoginView({
+                el: $('#app'),
+                model: new Models.LoginModel()
+            });
+            view.model.setApp(this);
+            this.navigate('/login');
+        },
+        doLogout: function() {
+            var $app = this;
+            $.post('/api/user/logout')
+                .success(function(data) {
+                    $app.navigate('/', { trigger: true });
+                })
+                .error(function() {
+                    alert("Error logging out!");
+                });
+        },
+        getQueryVariable: function(variable) {
+            var query = window.location.search.substring(1);
+            var vars = query.split("&");
+            for (var i = 0; i < vars.length; i++) {
+                var pair = vars[i].split("=");
+                if (pair[0] == variable) {
+                    return unescape(pair[1]);
+                }
+            }
+            return false;
+        }
+    });
+
+    return {
+        Router: AppRouter
+    };
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/wikked/client.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,39 @@
+/**
+ * Client-side Wikked.
+ */
+define(function() {
+
+    var PageFormatter = {
+        formatLink: function(link) {
+            return link.toLowerCase().replace(/[^a-z0-9_\.\-\(\)\/]+/g, '-');
+        },
+        formatText: function(text) {
+            var $f = this;
+            text = text.replace(/^\[\[([a-z]+)\:\s*(.+)\]\]\s*$/gm, function(m, a, b) {
+                var p = "<p><span class=\"preview-wiki-meta\">\n";
+                p += "<span class=\"meta-name\">" + a + "</span>";
+                p += "<span class=\"meta-value\">" + b + "</span>\n";
+                p += "</span></p>\n\n";
+                return p;
+            });
+            text = text.replace(/\[\[([^\|\]]+)\|([^\]]+)\]\]/g, function(m, a, b) {
+                var url = $f.formatLink(b);
+                return '[' + a + '](/#/read/' + url + ')';
+            });
+            text = text.replace(/\[\[([^\]]+\/)?([^\]]+)\]\]/g, function(m, a, b) {
+                var url = $f.formatLink(a + b);
+                return '[' + b + '](/#/read/' + url + ')';
+            });
+            return text;
+        }
+    };
+    //TODO: remove this and move the JS code from the template to the view.
+    if (!window.Wikked)
+        window.Wikked = {};
+    window.Wikked.PageFormatter = PageFormatter;
+
+    return {
+        PageFormatter: PageFormatter
+    };
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/wikked/handlebars.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,48 @@
+/**
+ * Handlebars helpers and extensions.
+ */
+define([
+        'handlebars'
+        ],
+    function(Handlebars) {
+
+    /**
+     * Handlebars helper: reverse iterator.
+     */
+    Handlebars.registerHelper('eachr', function(context, options) {
+        if (context === undefined) {
+            return '';
+        }
+        data = undefined;
+        if (options.data) {
+            data = Handlebars.createFrame(options.data);
+        }
+        var out = '';
+        for (var i=context.length - 1; i >= 0; i--) {
+            if (data !== undefined) {
+                data.index = (context.length - 1 - i);
+                data.rindex = i;
+            }
+            out += options.fn(context[i], { data: data });
+        }
+        return out;
+    });
+
+    /**
+     * Would you believe Handlebars doesn't have an equality
+     * operator?
+     */
+    Handlebars.registerHelper('ifeq', function(context, options) {
+        if (context == options.hash.to) {
+            return options.fn(this);
+        }
+        return options.inverse(this);
+    });
+    Handlebars.registerHelper('ifneq', function(context, options) {
+        if (context != options.hash.to) {
+            return options.fn(this);
+        }
+        return options.inverse(this);
+    });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/wikked/models.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,319 @@
+/**
+ * Wikked models.
+ */
+define([
+        'require',
+        'jquery',
+        'underscore',
+        'backbone',
+        'handlebars'
+        ],
+    function(require, $, _, Backbone, Handlebars) {
+
+    var NavigationModel = Backbone.Model.extend({
+        idAttribute: 'path',
+        defaults: function() {
+            return {
+                path: "main-page",
+                action: "read",
+                user: false
+            };
+        },
+        initialize: function() {
+            this.on('change:path', function(model, path) {
+                model._onChangePath(path);
+            });
+            this._onChangePath(this.get('path'));
+            this.on('change:auth', function(model, auth) {
+                model._onChangeAuth(auth);
+            });
+            this._onChangeAuth(this.get('auth'));
+            return this;
+        },
+        doSearch: function(form) {
+            this.app.navigate('/search/' + $(form.q).val(), { trigger: true });
+        },
+        _onChangePath: function(path) {
+            this.set('url_home', '/#/read/main-page');
+            this.set('url_read', '/#/read/' + path);
+            this.set('url_edit', '/#/edit/' + path);
+            this.set('url_hist', '/#/changes/' + path);
+            this.set('url_search', '/search');
+        },
+        _onChangeAuth: function(auth) {
+            if (auth) {
+                this.set('url_login', false);
+                this.set('url_logout', '/#/logout');
+                this.set('username', auth.username);
+            } else {
+                this.set('url_login', '/#/login');
+                this.set('url_logout', false);
+                this.set('username', false);
+            }
+        }
+    });
+
+    var FooterModel = Backbone.Model.extend({
+        defaults: function() {
+            return {
+                url_extras: [ { name: 'Home', url: '/' } ]
+            };
+        },
+        addExtraUrl: function(name, url, index) {
+            if (index === undefined) {
+                this.get('url_extras').push({ name: name, url: url });
+            } else {
+                this.get('url_extras').splice(index, 0, { name: name, url: url });
+            }
+        }
+    });
+
+    var LoginModel = Backbone.Model.extend({
+        setApp: function(app) {
+            this.app = app;
+        },
+        doLogin: function(form) {
+            var $model = this;
+            $.post('/api/user/login', $(form).serialize())
+                .success(function() {
+                    $model.app.navigate('/', { trigger: true });
+                })
+                .error(function() {
+                    alert("Error while logging in...");
+                });
+        }
+    });
+
+    var PageModel = Backbone.Model.extend({
+        idAttribute: 'path',
+        defaults: function() {
+            return {
+                path: "main-page"
+            };
+        },
+        initialize: function() {
+            this.on('change:path', function(model, path) {
+                model._onChangePath(path);
+            });
+            this.on('change:text', function(model, text) {
+                model._onChangeText(text);
+            });
+            this._onChangePath(this.get('path'));
+            this._onChangeText('');
+            return this;
+        },
+        url: function() {
+            var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
+            if (this.isNew()) return base;
+            return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + this.id;
+        },
+        title: function() {
+            return this.getMeta('title');
+        },
+        getMeta: function(key) {
+            var meta = this.get('meta');
+            if (meta === undefined) {
+                return undefined;
+            }
+            return meta[key];
+        },
+        setApp: function(app) {
+            this.app = app;
+            if (this._onAppSet !== undefined) {
+                this._onAppSet(app);
+            }
+        },
+        _onChangePath: function(path) {
+        },
+        _onChangeText: function(text) {
+            this.set('content', new Handlebars.SafeString(text));
+        }
+    });
+    
+    var PageStateModel = PageModel.extend({
+        urlRoot: '/api/state/'
+    });
+    
+    var MasterPageModel = PageModel.extend({
+        initialize: function() {
+            this.nav = new NavigationModel({ id: this.id });
+            this.footer = new FooterModel();
+            MasterPageModel.__super__.initialize.apply(this, arguments);
+            this.on('change:auth', function(model, auth) {
+                model._onChangeAuth(auth);
+            });
+            if (this.action !== undefined) {
+                this.nav.set('action', this.action);
+                this.footer.set('action', this.action);
+            }
+            return this;
+        },
+        _onAppSet: function(app) {
+            this.nav.app = app;
+            this.footer.app = app;
+        },
+        _onChangePath: function(path) {
+            MasterPageModel.__super__._onChangePath.apply(this, arguments);
+            this.nav.set('path', path);
+        },
+        _onChangeAuth: function(auth) {
+            this.nav.set('auth', auth);
+        }
+    });
+
+    var PageReadModel = MasterPageModel.extend({
+        urlRoot: '/api/read/',
+        action: 'read',
+        _onChangePath: function(path) {
+            PageReadModel.__super__._onChangePath.apply(this, arguments);
+            this.footer.addExtraUrl('Pages Linking Here', '/#/inlinks/' + path, 1);
+            this.footer.addExtraUrl('JSON', '/api/read/' + path);
+        }
+    });
+
+    var PageSourceModel = MasterPageModel.extend({
+        urlRoot: '/api/raw/',
+        action: 'source'
+    });
+
+    var PageEditModel = MasterPageModel.extend({
+        urlRoot: '/api/edit/',
+        action: 'edit',
+        doEdit: function(form) {
+            var $model = this;
+            var path = this.get('path');
+            $.post('/api/edit/' + path, $(form).serialize())
+                .success(function(data) {
+                    $model.app.navigate('/read/' + path, { trigger: true });
+                })
+                .error(function() {
+                    alert('Error saving page...');
+                });
+        }
+    });
+
+    var PageHistoryModel = MasterPageModel.extend({
+        urlRoot: '/api/history/',
+        action: 'history',
+        doDiff: function(form) {
+            var rev1 = $('input[name=rev1]:checked', form).val();
+            var rev2 = $('input[name=rev2]:checked', form).val();
+            this.app.navigate('/diff/r/' + this.get('path') + '/' + rev1 + '/' + rev2, { trigger: true });
+        },
+        _onChangePath: function(path) {
+            PageHistoryModel.__super__._onChangePath.apply(this, arguments);
+            this.set('url_rev', '/#/revision/' + path);
+            this.set('url_diffc', '/#/diff/c/' + path);
+            this.set('url_diffr', '/#/diff/r/' + path);
+        }
+    });
+
+    var PageRevisionModel = MasterPageModel.extend({
+        urlRoot: '/api/revision/',
+        idAttribute: 'path_and_rev',
+        action: 'revision',
+        defaults: function() {
+            return {
+                path: "main-page",
+                rev: "tip"
+            };
+        },
+        initialize: function() {
+            PageRevisionModel.__super__.initialize.apply(this, arguments);
+            this.on('change:path', function(model, path) {
+                model._onChangePathOrRev(path, model.get('rev'));
+            });
+            this.on('change:rev', function(model, rev) {
+                model._onChangePathOrRev(model.get('path'), rev);
+            });
+            this._onChangePathOrRev(this.get('path'), this.get('rev'));
+            return this;
+        },
+        _onChangePathOrRev: function(path, rev) {
+            this.set('path_and_rev', path + '/' + rev);
+            this.set('disp_rev', rev);
+            if (rev.match(/[a-f0-9]{40}/)) {
+                this.set('disp_rev', rev.substring(0, 8));
+            }
+        }
+    });
+
+    var PageDiffModel = MasterPageModel.extend({
+        urlRoot: '/api/diff/',
+        idAttribute: 'path_and_revs',
+        action: 'diff',
+        defaults: function() {
+            return {
+                path: "main-page",
+                rev1: "tip",
+                rev2: ""
+            };
+        },
+        initialize: function() {
+            PageDiffModel.__super__.initialize.apply(this, arguments);
+            this.on('change:path', function(model, path) {
+                model._onChangePathOrRevs(path, model.get('rev'));
+            });
+            this.on('change:rev1', function(model, rev1) {
+                model._onChangePathOrRevs(model.get('path'), rev1, model.get('rev2'));
+            });
+            this.on('change:rev2', function(model, rev2) {
+                model._onChangePathOrRevs(model.get('path'), model.get('rev1'), rev2);
+            });
+            this._onChangePathOrRevs(this.get('path'), this.get('rev1'), this.get('rev2'));
+            return this;
+        },
+        _onChangePathOrRevs: function(path, rev1, rev2) {
+            this.set('path_and_revs', path + '/' + rev1 + '/' + rev2);
+            if (!rev2) {
+                this.set('path_and_revs', path + '/' + rev1);
+            }
+            this.set('disp_rev1', rev1);
+            if (rev1 !== undefined && rev1.match(/[a-f0-9]{40}/)) {
+                this.set('disp_rev1', rev1.substring(0, 8));
+            }
+            this.set('disp_rev2', rev2);
+            if (rev2 !== undefined && rev2.match(/[a-f0-9]{40}/)) {
+                this.set('disp_rev2', rev2.substring(0, 8));
+            }
+        }
+    });
+
+    var IncomingLinksModel = MasterPageModel.extend({
+        urlRoot: '/api/inlinks/',
+        action: 'inlinks'
+    });
+
+    var WikiSearchModel = MasterPageModel.extend({
+        urlRoot: '/api/search/',
+        action: 'search',
+        title: function() {
+            return 'Search';
+        },
+        execute: function(query) {
+            var $model = this;
+            $.getJSON('/api/search', { q: query })
+                .success(function (data) {
+                    $model.set('hits', data.hits);
+                })
+                .error(function() {
+                    alert("Error searching...");
+                });
+        }
+    });
+
+    return {
+        NavigationModel: NavigationModel,
+        FooterModel: FooterModel,
+        PageReadModel: PageReadModel,
+        PageEditModel: PageEditModel,
+        PageHistoryModel: PageHistoryModel,
+        IncomingLinksModel: IncomingLinksModel,
+        PageRevisionModel: PageRevisionModel,
+        PageDiffModel: PageDiffModel,
+        WikiSearchModel: WikiSearchModel,
+        LoginModel: LoginModel,
+        PageStateModel: PageStateModel
+    };
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/wikked/util.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,43 @@
+/**
+ * Various utility classes/functions.
+ */
+define([
+        'jquery'
+        ],
+    function($) {
+
+    /**
+     * Make Javascript suck less.
+     */
+    String.prototype.format = function() {
+        var args = arguments;
+        return this.replace(/\{(\d+)\}/g, function(match, number) { 
+            return typeof args[number] != 'undefined' ? args[number] : match;
+        });
+    };
+
+    /**
+     * Helper class to load template files
+     * by name from the `tpl` directory.
+     */
+    var TemplateLoader = {
+        loadedTemplates: {},
+        get: function(name, callback) {
+            if (name in this.loadedTemplates) {
+                callback(this.loadedTemplates[name]);
+            } else {
+                var $loader = this;
+                url = '/tpl/' + name + '.html' + '?' + (new Date()).getTime();
+                $.get(url, function(data) {
+                    $loader.loadedTemplates[name] = data;
+                    callback(data);
+                });
+            }
+        }
+    };
+
+    return {
+        TemplateLoader: TemplateLoader
+    };
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/static/js/wikked/views.js	Sun Dec 30 23:15:32 2012 -0800
@@ -0,0 +1,225 @@
+/**
+ * Wikked views.
+ */
+define([
+        'jquery',
+        'underscore',
+        'backbone',
+        'handlebars',
+        './models',
+        './util'
+        ],
+    function($, _, Backbone, Handlebars, Models, Util) {
+
+    var PageView = Backbone.View.extend({
+        tagName: 'div',
+        className: 'wrapper',
+        initialize: function() {
+            PageView.__super__.initialize.apply(this, arguments);
+            var $view = this;
+            this.model.on("change", function() { $view.render(); });
+            return this;
+        },
+        render: function(view) {
+            if (this.templateName !== undefined) {
+                this.renderTemplate(this.templateName, this.renderCallback);
+            }
+            this.renderTitle(this.titleFormat);
+            return this;
+        },
+        renderTemplate: function(tpl_name, callback) {
+            var $view = this;
+            Util.TemplateLoader.get(tpl_name, function(src) {
+                var template = Handlebars.compile(src);
+                $view.$el.html(template($view.model.toJSON()));
+                if (callback !== undefined) {
+                    callback.call($view, $view, $view.model);
+                }
+            });
+        },
+        renderTitle: function(formatter) {
+            var title = this.model.title();
+            if (formatter !== undefined) {
+                title = formatter.call(this, title);
+            }
+            document.title = title;
+        }
+    });
+    _.extend(PageView, Backbone.Events);
+
+    var NavigationView = PageView.extend({
+        templateName: 'nav',
+        initialize: function() {
+            NavigationView.__super__.initialize.apply(this, arguments);
+            this.render();
+            return this;
+        },
+        render: function() {
+            this.renderTemplate('nav');
+        },
+        postRender: function() {
+            var model = this.model;
+            this.$('#search').submit(function(e) {
+                e.preventDefault();
+                model.doSearch(this);
+                return false;
+            });
+        }
+    });
+
+    var FooterView = PageView.extend({
+        templateName:  'footer',
+        initialize: function() {
+            FooterView.__super__.initialize.apply(this, arguments);
+            this.render();
+            return this;
+        },
+        render: function() {
+            this.renderTemplate('footer');
+        },
+        postRender: function() {
+        }
+    });
+
+    var LoginView = PageView.extend({
+        templateName: 'login',
+        initialize: function() {
+            LoginView.__super__.initialize.apply(this, arguments);
+            this.render();
+            return this;
+        },
+        render: function() {
+            this.renderTemplate('login', function(view, model) {
+                this.$('#login').submit(function(e) {
+                    e.preventDefault();
+                    model.doLogin(this);
+                    return false;
+                });
+            });
+            document.title = 'Login';
+        }
+    });
+
+    var MasterPageView = PageView.extend({
+        initialize: function() {
+            MasterPageView.__super__.initialize.apply(this, arguments);
+            this.nav = new NavigationView({ model: this.model.nav });
+            this.footer = new FooterView({ model: this.model.footer });
+            this.render();
+            return this;
+        },
+        renderCallback: function(view, model) {
+            this.nav.$el.prependTo(this.$el);
+            this.nav.postRender();
+            this.footer.$el.appendTo(this.$el);
+            this.footer.postRender();
+        }
+    });
+
+    var PageReadView = MasterPageView.extend({
+        templateName: 'read-page',
+        initialize: function() {
+            PageReadView.__super__.initialize.apply(this, arguments);
+            // Also get the current state, and show a warning
+            // if the page is new or modified.
+            var stateModel = new Models.PageStateModel({ path: this.model.get('path') });
+            stateModel.fetch({
+                success: function(model, response, options) {
+                    if (model.get('state') == 'new' || model.get('state') == 'modified') {
+                        Util.TemplateLoader.get('state-warning', function(src) {
+                            var template = Handlebars.compile(src);
+                            var warning = $(template(model.toJSON()));
+                            warning.css('display', 'none');
+                            warning.prependTo($('#app'));
+                            warning.slideDown();
+                            $('.dismiss', warning).click(function() {
+                                warning.slideUp();
+                                return false;
+                            });
+                        });
+                    }
+                }
+            });
+            return this;
+        },
+        renderCallback: function(view, model) {
+            PageReadView.__super__.renderCallback.apply(this, arguments);
+            // Replace all wiki links with proper hyperlinks using the JS app's
+            // URL scheme.
+            this.$('a.wiki-link[data-wiki-url]').each(function(i) {
+                var jel = $(this);
+                if (jel.hasClass('missing'))
+                    jel.attr('href', '/#/edit/' + jel.attr('data-wiki-url'));
+                else
+                    jel.attr('href', '/#/read/' + jel.attr('data-wiki-url'));
+            });
+        }
+    });
+
+    var PageEditView = MasterPageView.extend({
+        templateName: 'edit-page',
+        renderCallback: function(view, model) {
+            PageEditView.__super__.renderCallback.apply(this, arguments);
+            this.$('#page-edit').submit(function(e) {
+                e.preventDefault();
+                model.doEdit(this);
+                return false;
+            });
+        },
+        titleFormat: function(title) {
+            return 'Editing: ' + title;
+        }
+    });
+
+    var PageHistoryView = MasterPageView.extend({
+        templateName: 'history-page',
+        renderCallback: function(view, model) {
+            PageHistoryView.__super__.renderCallback.apply(this, arguments);
+            this.$('#diff-page').submit(function(e) {
+                e.preventDefault();
+                model.doDiff(this);
+                return false;
+            });
+        },
+        titleFormat: function(title) {
+            return 'History: ' + title;
+        }
+    });
+
+    var PageRevisionView = MasterPageView.extend({
+        templateName: 'revision-page',
+        titleFormat: function(title) {
+            return title + ' [' + this.model.get('rev') + ']';
+        }
+    });
+
+    var PageDiffView = MasterPageView.extend({
+        templateName: 'diff-page',
+        titleFormat: function(title) {
+            return title + ' [' + this.model.get('rev1') + '-' + this.model.get('rev2') + ']';
+        }
+    });
+
+    var IncomingLinksView = MasterPageView.extend({
+        templateName: 'inlinks-page',
+        titleFormat: function(title) {
+            return 'Incoming Links: ' + title;
+        }
+    });
+
+    var WikiSearchView = MasterPageView.extend({
+        templateName: 'search-results'
+    });
+
+    return {
+        PageReadView: PageReadView,
+        PageEditView: PageEditView,
+        PageHistoryView: PageHistoryView,
+        IncomingLinksView: IncomingLinksView,
+        PageRevisionView: PageRevisionView,
+        PageDiffView: PageDiffView,
+        WikiSearchView: WikiSearchView,
+        LoginView: LoginView
+    };
+});
+
--- a/wikked/templates/index.html	Sun Dec 30 20:08:11 2012 -0800
+++ b/wikked/templates/index.html	Sun Dec 30 23:15:32 2012 -0800
@@ -9,10 +9,6 @@
     <body>
         <div id="app" class="container">
         </div>
-        <script src="/js/jquery-1.8.3.min.js"></script>
-        <script src="/js/underscore-min.js"></script>
-        <script src="/js/backbone-min.js"></script>
-        <script src="/js/handlebars-1.0.rc.1.js"></script>
-        <script src="/js/wikked.js{{cache_bust}}"></script>
+        <script data-main="/js/wikked.js" src="/js/require.js{{cache_bust}}"></script>
     </body>
 </html>