changeset 341:37f426e067c4

Big refactor to get rid of this whole single page app crap. Getting back to basics!
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 16 Sep 2015 23:04:28 -0700
parents ef1206634939
children 0979ace46676
files Gruntfile.js wikked/api/__init__.py wikked/api/admin.py wikked/api/edit.py wikked/api/history.py wikked/api/read.py wikked/api/special.py wikked/assets/css/wikked/nav.less wikked/assets/js/backbone-min.js wikked/assets/js/handlebars-1.0.rc.1.js wikked/assets/js/jsonjs/README wikked/assets/js/jsonjs/cycle.js wikked/assets/js/jsonjs/json.js wikked/assets/js/jsonjs/json2.js wikked/assets/js/jsonjs/json_parse.js wikked/assets/js/jsonjs/json_parse_state.js wikked/assets/js/less-1.3.1.min.js wikked/assets/js/moment.min.js wikked/assets/js/pagedown/Markdown.Converter.js wikked/assets/js/pagedown/Markdown.Editor.js wikked/assets/js/pagedown/Markdown.Sanitizer.js wikked/assets/js/text.js wikked/assets/js/wikked.app.js wikked/assets/js/wikked.edit.js wikked/assets/js/wikked.js wikked/assets/js/wikked/app.js wikked/assets/js/wikked/client.js wikked/assets/js/wikked/edit.js wikked/assets/js/wikked/handlebars.js wikked/assets/js/wikked/models.js wikked/assets/js/wikked/util.js wikked/assets/js/wikked/view-manager.js wikked/assets/js/wikked/views.js wikked/assets/json/special-pagelists.json wikked/assets/json/special-sections.json wikked/assets/tpl/404.html wikked/assets/tpl/category.html wikked/assets/tpl/diff-page.html wikked/assets/tpl/edit-page.html wikked/assets/tpl/error-not-found.html wikked/assets/tpl/error-unauthorized-edit.html wikked/assets/tpl/error-unauthorized.html wikked/assets/tpl/footer.html wikked/assets/tpl/history-page.html wikked/assets/tpl/inlinks-page.html wikked/assets/tpl/login.html wikked/assets/tpl/meta-page.html wikked/assets/tpl/nav.html wikked/assets/tpl/read-page.html wikked/assets/tpl/revision-page.html wikked/assets/tpl/search-results.html wikked/assets/tpl/special-changes.html wikked/assets/tpl/special-nav.html wikked/assets/tpl/special-pagelist.html wikked/assets/tpl/special-pages.html wikked/assets/tpl/state-warning.html wikked/commands/web.py wikked/formatter.py wikked/resolver.py wikked/scm/mercurial.py wikked/templates/404.html wikked/templates/circular_include_error.html wikked/templates/diff-page.html wikked/templates/edit-page.html wikked/templates/error-not-found.html wikked/templates/error-unauthorized-edit.html wikked/templates/error-unauthorized.html wikked/templates/footer.html wikked/templates/history-page.html wikked/templates/index-dev.html wikked/templates/index.html wikked/templates/inlinks-page.html wikked/templates/login.html wikked/templates/meta_page.html wikked/templates/nav.html wikked/templates/read-page.html wikked/templates/revision-page.html wikked/templates/search-results.html wikked/templates/special-broken-redirects.html wikked/templates/special-changes.html wikked/templates/special-dead-ends.html wikked/templates/special-double-redirects.html wikked/templates/special-nav.html wikked/templates/special-orphans.html wikked/templates/special-pagelist.html wikked/templates/special-pages.html wikked/templates/state-warning.html wikked/utils.py wikked/views/__init__.py wikked/views/admin.py wikked/views/edit.py wikked/views/error.py wikked/views/history.py wikked/views/read.py wikked/views/special.py wikked/web.py wikked/webimpl/__init__.py wikked/webimpl/admin.py wikked/webimpl/edit.py wikked/webimpl/history.py wikked/webimpl/read.py wikked/webimpl/special.py
diffstat 101 files changed, 2575 insertions(+), 11065 deletions(-) [+]
line wrap: on
line diff
--- a/Gruntfile.js	Sun Aug 30 21:45:42 2015 -0700
+++ b/Gruntfile.js	Wed Sep 16 23:04:28 2015 -0700
@@ -1,3 +1,7 @@
+/*global module:false*/
+'use strict';
+
+
 module.exports = function(grunt) {
 
   // Project configuration.
@@ -26,19 +30,37 @@
       development: {
         options: {
           optimize: "none",
-          baseUrl: "wikked/assets",
+          baseUrl: "wikked/assets/js",
           mainConfigFile: "wikked/assets/js/wikked.js",
-          name: "js/wikked",
-          out: "wikked/static/js/wikked.min.js"
+          dir: "wikked/static/js",
+          modules: [
+          {
+              name: "wikked.app",
+              include: ["require.js"]
+          },
+          {
+              name: "wikked.edit",
+              exclude: ["wikked.app"]
+          }
+          ]
         }
       },
       production: {
         options: {
           optimize: "uglify",
-          baseUrl: "wikked/assets",
+          baseUrl: "wikked/assets/js",
           mainConfigFile: "wikked/assets/js/wikked.js",
-          name: "js/wikked",
-          out: "wikked/static/js/wikked.min.js"
+          dir: "wikked/static/js",
+          modules: [
+          {
+              name: "wikked.app",
+              include: ["require.js"]
+          },
+          {
+              name: "wikked.edit",
+              exclude: ["wikked.app"]
+          }
+          ]
         }
       }
     },
@@ -56,9 +78,6 @@
       development: {
         files: [
           //{expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['img/**']},
-          {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['js/**']},
-          {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['tpl/**']},
-          {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['json/**']},
           {expand: true, cwd: 'wikked/assets/font-awesome', dest: 'wikked/static/', src: ['fonts/**']}
         ]
       },
@@ -67,12 +86,6 @@
           {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['js/wikked.js', 'js/wikked/**']}
         ]
       },
-      dev_templates: {
-        files: [
-          {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['tpl/**']},
-          {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['json/**']}
-        ]
-      },
       production: {
         files: [
           {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['js/require.js']},
@@ -89,10 +102,6 @@
         files: ['wikked/assets/js/wikked.js', 'wikked/assets/js/wikked/**'],
         tasks: ['jshint:all', 'copy:dev_scripts']
       },
-      templates: {
-        files: ['wikked/assets/tpl/**/*.html', 'wikked/assets/json/**/*.json'],
-        tasks: ['copy:dev_templates']
-      },
       styles: {
         files: ['wikked/assets/css/**/*.less'],
         tasks: ['less:development']
@@ -116,6 +125,6 @@
   grunt.registerTask('default', ['jshint', 'less:production', 'requirejs:production', 'imagemin:all', 'copy:production']);
 
   // Other tasks.
-  grunt.registerTask('dev', ['less:development', 'copy:production', 'copy:development']);
+  grunt.registerTask('dev', ['less:development', 'requirejs:development', 'copy:production', 'copy:development']);
 };
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/api/admin.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,69 @@
+from flask import jsonify, abort, request
+from flask.ext.login import logout_user, current_user
+from wikked.web import app, get_wiki, login_manager
+from wikked.webimpl.admin import do_login_user
+
+
+@app.route('/api/admin/reindex', methods=['POST'])
+def api_admin_reindex():
+    if not current_user.is_authenticated() or not current_user.is_admin():
+        return login_manager.unauthorized()
+    wiki = get_wiki()
+    wiki.index.reset(wiki.getPages())
+    result = {'ok': 1}
+    return jsonify(result)
+
+
+@app.route('/api/user/login', methods=['POST'])
+def api_user_login():
+    if do_login_user():
+        username = request.form.get('username')
+        result = {'username': username, 'logged_in': 1}
+        return jsonify(result)
+    abort(401)
+
+
+@app.route('/api/user/is_logged_in')
+def api_user_is_logged_in():
+    if current_user.is_authenticated():
+        result = {'logged_in': True}
+        return jsonify(result)
+    abort(401)
+
+
+@app.route('/api/user/logout', methods=['POST'])
+def api_user_logout():
+    logout_user()
+    result = {'ok': 1}
+    return jsonify(result)
+
+
+@app.route('/api/user/info')
+def api_current_user_info():
+    user = current_user
+    if user.is_authenticated():
+        result = {
+                'user': {
+                    'username': user.username,
+                    'groups': user.groups
+                    }
+                }
+        return jsonify(result)
+    return jsonify({'user': False})
+
+
+@app.route('/api/user/info/<name>')
+def api_user_info(name):
+    wiki = get_wiki()
+    user = wiki.auth.getUser(name)
+    if user is not None:
+        result = {
+                'user': {
+                    'username': user.username,
+                    'groups': user.groups
+                    }
+                }
+        return jsonify(result)
+    abort(404)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/api/edit.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,85 @@
+from flask import abort, request, jsonify
+from flask.ext.login import current_user
+from wikked.web import app, get_wiki
+from wikked.webimpl import url_from_viewarg, split_url_from_viewarg
+from wikked.webimpl.edit import (
+        get_edit_page, do_edit_page, preview_edited_page)
+
+
+@app.route('/api/edit/<path:url>', methods=['GET', 'POST'])
+def api_edit_page(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    endpoint, path = split_url_from_viewarg(url)
+
+    if request.method == 'GET':
+        url = path
+        custom_data = None
+        if endpoint is not None:
+            url = '%s:%s' % (endpoint, path)
+            custom_data = {
+                    'meta_query': endpoint,
+                    'meta_value': path.lstrip('/')
+                    }
+
+        data = get_edit_page(
+                wiki, user, url,
+                custom_data=custom_data)
+        return jsonify(data)
+
+    if request.method == 'POST':
+        url = path
+        if endpoint is not None:
+            url = '%s:%s' % (endpoint, path)
+
+        author = user or request.form.get('author')
+        if not author:
+            abort(400)
+
+        message = request.form.get('message')
+        if not message:
+            abort(400)
+
+        do_edit_page(wiki, user, url, author, message)
+        return jsonify({'edited': True})
+
+
+@app.route('/api/preview', methods=['POST'])
+def api_preview():
+    url = request.form.get('url')
+    url = url_from_viewarg(url)
+    text = request.form.get('text')
+    wiki = get_wiki()
+    preview = preview_edited_page(wiki, url, text)
+    result = {'text': preview}
+    return jsonify(result)
+
+
+@app.route('/api/rename/<path:url>', methods=['POST'])
+def api_rename_page(url):
+    pass
+
+
+@app.route('/api/delete/<path:url>', methods=['POST'])
+def api_delete_page(url):
+    pass
+
+
+@app.route('/api/validate/newpage', methods=['GET', 'POST'])
+def api_validate_newpage():
+    path = request.form.get('title')
+    if path is None:
+        abort(400)
+
+    path = url_from_viewarg(path)
+    try:
+        # Check that there's no page with that name already, and that
+        # the name can be correctly mapped to a filename.
+        wiki = get_wiki()
+        if wiki.pageExists(path):
+            raise Exception("Page '%s' already exists" % path)
+        wiki.fs.getPhysicalPagePath(path, make_new=True)
+    except Exception:
+        return '"This page name is invalid or unavailable"'
+    return '"true"'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/api/history.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,86 @@
+from flask import request, abort, jsonify
+from flask.ext.login import current_user
+from wikked.web import app, get_wiki
+from wikked.webimpl import url_from_viewarg
+from wikked.webimpl.history import (
+        get_site_history, get_page_history,
+        read_page_rev, diff_page_revs)
+
+
+@app.route('/api/site-history')
+def api_site_history():
+    wiki = get_wiki()
+    user = current_user.get_id()
+    after_rev = request.args.get('rev')
+    result = get_site_history(wiki, user, after_rev=after_rev)
+    return jsonify(result)
+
+
+@app.route('/api/history/')
+def api_main_page_history():
+    wiki = get_wiki()
+    return api_page_history(wiki.main_page_url.lstrip('/'))
+
+
+@app.route('/api/history/<path:url>')
+def api_page_history(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    result = get_page_history(wiki, user, url)
+    return jsonify(result)
+
+
+@app.route('/api/revision/<path:url>')
+def api_read_page_rev(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    rev = request.args.get('rev')
+    if rev is None:
+        abort(400)
+    result = read_page_rev(wiki, user, url, rev=rev)
+    return jsonify(result)
+
+
+@app.route('/api/diff/<path:url>')
+def api_diff_page(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    rev1 = request.args.get('rev1')
+    rev2 = request.args.get('rev2')
+    raw = request.args.get('raw')
+    if rev1 is None:
+        abort(400)
+    result = diff_page_revs(wiki, user, url,
+                            rev1=rev1, rev2=rev2, raw=raw)
+    return jsonify(result)
+
+
+@app.route('/api/revert/<path:url>', methods=['POST'])
+def api_revert_page(url):
+    # TODO: only users with write access can revert.
+    if 'rev' not in request.form:
+        abort(400)
+    rev = request.form['rev']
+
+    author = request.remote_addr
+    if 'author' in request.form and len(request.form['author']) > 0:
+        author = request.form['author']
+
+    message = 'Reverted %s to revision %s' % (url, rev)
+    if 'message' in request.form and len(request.form['message']) > 0:
+        message = request.form['message']
+
+    url = url_from_viewarg(url)
+    page_fields = {
+            'rev': rev,
+            'author': author,
+            'message': message
+            }
+    wiki = get_wiki()
+    wiki.revertPage(url, page_fields)
+    result = {'reverted': 1}
+    return jsonify(result)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/api/read.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,135 @@
+from flask import request, jsonify, make_response, abort
+from flask.ext.login import current_user
+from wikked.scm.base import STATE_NAMES
+from wikked.utils import PageNotFoundError
+from wikked.web import app, get_wiki
+from wikked.webimpl import (
+        CHECK_FOR_READ,
+        url_from_viewarg,
+        get_page_or_raise, get_page_or_none,
+        get_page_meta, is_page_readable,
+        RedirectNotFound, CircularRedirectError)
+from wikked.webimpl.read import (
+        read_page, get_incoming_links, get_outgoing_links)
+from wikked.webimpl.special import list_pages
+
+
+@app.route('/api/list')
+def api_list_all_pages():
+    return api_list_pages(None)
+
+
+@app.route('/api/list/<path:url>')
+def api_list_pages(url):
+    wiki = get_wiki()
+    url = url_from_viewarg(url)
+    result = list_pages(wiki, current_user.get_id(), url=url)
+    return jsonify(result)
+
+
+@app.route('/api/read/')
+def api_read_main_page():
+    wiki = get_wiki()
+    return api_read_page(wiki.main_page_url.lstrip('/'))
+
+
+@app.route('/api/read/<path:url>')
+def api_read_page(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    try:
+        result = read_page(wiki, user, url)
+        return jsonify(result)
+    except RedirectNotFound as e:
+        app.logger.exception(e)
+        abort(404)
+    except CircularRedirectError as e:
+        app.logger.exception(e)
+        abort(409)
+
+
+@app.route('/api/raw/')
+def api_read_main_page_raw():
+    wiki = get_wiki()
+    return api_read_page_raw(wiki.main_page_url.lstrip('/'))
+
+
+@app.route('/api/raw/<path:url>')
+def api_read_page_raw(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    try:
+        page = get_page_or_raise(
+                wiki, url,
+                check_perms=(user, CHECK_FOR_READ),
+                fields=['raw_text', 'meta'])
+    except PageNotFoundError as e:
+        app.logger.exception(e)
+        abort(404)
+    resp = make_response(page.raw_text)
+    resp.mimetype = 'text/plain'
+    return resp
+
+
+@app.route('/api/query')
+def api_query():
+    wiki = get_wiki()
+    query = dict(request.args)
+    pages = wiki.getPages(meta_query=query)
+    result = {
+            'query': query,
+            'pages': [get_page_meta(p) for p in pages]
+            }
+    return jsonify(result)
+
+
+@app.route('/api/state/')
+def api_get_main_page_state():
+    wiki = get_wiki()
+    return api_get_state(wiki.main_page_url.lstrip('/'))
+
+
+@app.route('/api/state/<path:url>')
+def api_get_state(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    page = get_page_or_raise(
+            wiki, url,
+            check_perms=(user, CHECK_FOR_READ),
+            fields=['url', 'title', 'path', 'meta'])
+    state = page.getState()
+    return jsonify({
+        'meta': get_page_meta(page, True),
+        'state': STATE_NAMES[state]
+        })
+
+
+@app.route('/api/outlinks/')
+def api_get_main_page_outgoing_links():
+    wiki = get_wiki()
+    return api_get_outgoing_links(wiki.main_page_url.lstrip('/'))
+
+
+@app.route('/api/outlinks/<path:url>')
+def api_get_outgoing_links(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    result = get_outgoing_links(wiki, user, url)
+    return jsonify(result)
+
+
+@app.route('/api/inlinks/')
+def api_get_main_page_incoming_links():
+    wiki = get_wiki()
+    return api_get_incoming_links(wiki.main_page_url.lstrip('/'))
+
+
+@app.route('/api/inlinks/<path:url>')
+def api_get_incoming_links(url):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    result = get_incoming_links(wiki, user, url)
+    return jsonify(result)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/api/special.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,51 @@
+from flask import jsonify, request, abort
+from flask.ext.login import current_user
+from wikked.web import app, get_wiki
+from wikked.webimpl.special import (
+        get_orphans, get_broken_redirects, get_double_redirects,
+        get_dead_ends, get_search_results, get_search_preview_results)
+
+
+def call_api(api_func, *args, **kwargs):
+    wiki = get_wiki()
+    user = current_user.get_id()
+    result = api_func(wiki, user, *args, **kwargs)
+    return jsonify(result)
+
+
+@app.route('/api/orphans')
+def api_special_orphans():
+    return call_api(get_orphans)
+
+
+@app.route('/api/broken-redirects')
+def api_special_broken_redirects():
+    return call_api(get_broken_redirects)
+
+
+@app.route('/api/double-redirects')
+def api_special_double_redirects():
+    return call_api(get_double_redirects)
+
+
+@app.route('/api/dead-ends')
+def api_special_dead_ends():
+    return call_api(get_dead_ends)
+
+
+@app.route('/api/search')
+def api_search():
+    query = request.args.get('q')
+    if query is None or query == '':
+        abort(400)
+    return call_api(get_search_results, query=query)
+
+
+@app.route('/api/searchpreview')
+def api_search_preview():
+    query = request.args.get('q')
+    if query is None or query == '':
+        abort(400)
+    return call_api(get_search_preview_results, query=query)
+
+
--- a/wikked/assets/css/wikked/nav.less	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/assets/css/wikked/nav.less	Wed Sep 16 23:04:28 2015 -0700
@@ -24,6 +24,13 @@
     text-decoration: none;
 }
 
+#wiki-menu-shortcut {
+    &:hover {
+        color: @color-gray-light;
+        text-shadow: #EEE 0 0 1em;
+    }
+}
+
 #wiki-menu-pin {
     height: @wiki-shortcut-height;
     padding: 0 1.5em 0 @wiki-shortcut-width;
@@ -33,12 +40,16 @@
 
     &:hover {
         color: @color-gray-light;
+        text-shadow: #EEE 0 0 1em;
     }
 
     span {
         line-height: @wiki-shortcut-height;
     }
 }
+#wiki-menu-pin.wiki-menu-pin-active {
+    color: @color-gray-light;
+}
 
 #wiki-menu {
     position: fixed !important;
--- a/wikked/assets/js/backbone-min.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-// Backbone.js 0.9.2
-
-// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
-// Backbone may be freely distributed under the MIT license.
-// For all details and documentation:
-// http://backbonejs.org
-(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
-{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
-z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
-{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
-b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
-b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
-a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
-h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
-return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
-{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
-!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
-this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
-l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
-a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
-shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
-this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
-e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
-{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
-arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
-C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
-this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
-""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
-!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
-this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
-stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
-function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
-this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
-f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
-for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
-!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
-e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
-b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
--- a/wikked/assets/js/handlebars-1.0.rc.1.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1920 +0,0 @@
-// lib/handlebars/base.js
-
-/*jshint eqnull:true*/
-this.Handlebars = {};
-
-(function(Handlebars) {
-
-Handlebars.VERSION = "1.0.rc.1";
-
-Handlebars.helpers  = {};
-Handlebars.partials = {};
-
-Handlebars.registerHelper = function(name, fn, inverse) {
-  if(inverse) { fn.not = inverse; }
-  this.helpers[name] = fn;
-};
-
-Handlebars.registerPartial = function(name, str) {
-  this.partials[name] = str;
-};
-
-Handlebars.registerHelper('helperMissing', function(arg) {
-  if(arguments.length === 2) {
-    return undefined;
-  } else {
-    throw new Error("Could not find property '" + arg + "'");
-  }
-});
-
-var toString = Object.prototype.toString, functionType = "[object Function]";
-
-Handlebars.registerHelper('blockHelperMissing', function(context, options) {
-  var inverse = options.inverse || function() {}, fn = options.fn;
-
-
-  var ret = "";
-  var type = toString.call(context);
-
-  if(type === functionType) { context = context.call(this); }
-
-  if(context === true) {
-    return fn(this);
-  } else if(context === false || context == null) {
-    return inverse(this);
-  } else if(type === "[object Array]") {
-    if(context.length > 0) {
-      return Handlebars.helpers.each(context, options);
-    } else {
-      return inverse(this);
-    }
-  } else {
-    return fn(context);
-  }
-});
-
-Handlebars.K = function() {};
-
-Handlebars.createFrame = Object.create || function(object) {
-  Handlebars.K.prototype = object;
-  var obj = new Handlebars.K();
-  Handlebars.K.prototype = null;
-  return obj;
-};
-
-Handlebars.registerHelper('each', function(context, options) {
-  var fn = options.fn, inverse = options.inverse;
-  var ret = "", data;
-
-  if (options.data) {
-    data = Handlebars.createFrame(options.data);
-  }
-
-  if(context && context.length > 0) {
-    for(var i=0, j=context.length; i<j; i++) {
-      if (data) { data.index = i; }
-      ret = ret + fn(context[i], { data: data });
-    }
-  } else {
-    ret = inverse(this);
-  }
-  return ret;
-});
-
-Handlebars.registerHelper('if', function(context, options) {
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
-
-  if(!context || Handlebars.Utils.isEmpty(context)) {
-    return options.inverse(this);
-  } else {
-    return options.fn(this);
-  }
-});
-
-Handlebars.registerHelper('unless', function(context, options) {
-  var fn = options.fn, inverse = options.inverse;
-  options.fn = inverse;
-  options.inverse = fn;
-
-  return Handlebars.helpers['if'].call(this, context, options);
-});
-
-Handlebars.registerHelper('with', function(context, options) {
-  return options.fn(context);
-});
-
-Handlebars.registerHelper('log', function(context) {
-  Handlebars.log(context);
-});
-
-}(this.Handlebars));
-;
-// lib/handlebars/compiler/parser.js
-/* Jison generated parser */
-var handlebars = (function(){
-var parser = {trace: function trace() { },
-yy: {},
-symbols_: {"error":2,"root":3,"program":4,"EOF":5,"statements":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"params":25,"hash":26,"DATA":27,"param":28,"STRING":29,"INTEGER":30,"BOOLEAN":31,"hashSegments":32,"hashSegment":33,"ID":34,"EQUALS":35,"pathSegments":36,"SEP":37,"$accept":0,"$end":1},
-terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",27:"DATA",29:"STRING",30:"INTEGER",31:"BOOLEAN",34:"ID",35:"EQUALS",37:"SEP"},
-productions_: [0,[3,2],[4,3],[4,1],[4,0],[6,1],[6,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[7,2],[17,3],[17,2],[17,2],[17,1],[17,1],[25,2],[25,1],[28,1],[28,1],[28,1],[28,1],[28,1],[26,1],[32,2],[32,1],[33,3],[33,3],[33,3],[33,3],[33,3],[21,1],[36,3],[36,1]],
-performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
-
-var $0 = $$.length - 1;
-switch (yystate) {
-case 1: return $$[$0-1]; 
-break;
-case 2: this.$ = new yy.ProgramNode($$[$0-2], $$[$0]); 
-break;
-case 3: this.$ = new yy.ProgramNode($$[$0]); 
-break;
-case 4: this.$ = new yy.ProgramNode([]); 
-break;
-case 5: this.$ = [$$[$0]]; 
-break;
-case 6: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 
-break;
-case 7: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]); 
-break;
-case 8: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]); 
-break;
-case 9: this.$ = $$[$0]; 
-break;
-case 10: this.$ = $$[$0]; 
-break;
-case 11: this.$ = new yy.ContentNode($$[$0]); 
-break;
-case 12: this.$ = new yy.CommentNode($$[$0]); 
-break;
-case 13: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); 
-break;
-case 14: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); 
-break;
-case 15: this.$ = $$[$0-1]; 
-break;
-case 16: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); 
-break;
-case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true); 
-break;
-case 18: this.$ = new yy.PartialNode($$[$0-1]); 
-break;
-case 19: this.$ = new yy.PartialNode($$[$0-2], $$[$0-1]); 
-break;
-case 20: 
-break;
-case 21: this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]]; 
-break;
-case 22: this.$ = [[$$[$0-1]].concat($$[$0]), null]; 
-break;
-case 23: this.$ = [[$$[$0-1]], $$[$0]]; 
-break;
-case 24: this.$ = [[$$[$0]], null]; 
-break;
-case 25: this.$ = [[new yy.DataNode($$[$0])], null]; 
-break;
-case 26: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 
-break;
-case 27: this.$ = [$$[$0]]; 
-break;
-case 28: this.$ = $$[$0]; 
-break;
-case 29: this.$ = new yy.StringNode($$[$0]); 
-break;
-case 30: this.$ = new yy.IntegerNode($$[$0]); 
-break;
-case 31: this.$ = new yy.BooleanNode($$[$0]); 
-break;
-case 32: this.$ = new yy.DataNode($$[$0]); 
-break;
-case 33: this.$ = new yy.HashNode($$[$0]); 
-break;
-case 34: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 
-break;
-case 35: this.$ = [$$[$0]]; 
-break;
-case 36: this.$ = [$$[$0-2], $$[$0]]; 
-break;
-case 37: this.$ = [$$[$0-2], new yy.StringNode($$[$0])]; 
-break;
-case 38: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])]; 
-break;
-case 39: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])]; 
-break;
-case 40: this.$ = [$$[$0-2], new yy.DataNode($$[$0])]; 
-break;
-case 41: this.$ = new yy.IdNode($$[$0]); 
-break;
-case 42: $$[$0-2].push($$[$0]); this.$ = $$[$0-2]; 
-break;
-case 43: this.$ = [$$[$0]]; 
-break;
-}
-},
-table: [{3:1,4:2,5:[2,4],6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{1:[3]},{5:[1,16]},{5:[2,3],7:17,8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,19],20:[2,3],22:[1,13],23:[1,14],24:[1,15]},{5:[2,5],14:[2,5],15:[2,5],16:[2,5],19:[2,5],20:[2,5],22:[2,5],23:[2,5],24:[2,5]},{4:20,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{4:21,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{17:22,21:23,27:[1,24],34:[1,26],36:25},{17:27,21:23,27:[1,24],34:[1,26],36:25},{17:28,21:23,27:[1,24],34:[1,26],36:25},{17:29,21:23,27:[1,24],34:[1,26],36:25},{21:30,34:[1,26],36:25},{1:[2,1]},{6:31,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{5:[2,6],14:[2,6],15:[2,6],16:[2,6],19:[2,6],20:[2,6],22:[2,6],23:[2,6],24:[2,6]},{17:22,18:[1,32],21:23,27:[1,24],34:[1,26],36:25},{10:33,20:[1,34]},{10:35,20:[1,34]},{18:[1,36]},{18:[2,24],21:41,25:37,26:38,27:[1,45],28:39,29:[1,42],30:[1,43],31:[1,44],32:40,33:46,34:[1,47],36:25},{18:[2,25]},{18:[2,41],27:[2,41],29:[2,41],30:[2,41],31:[2,41],34:[2,41],37:[1,48]},{18:[2,43],27:[2,43],29:[2,43],30:[2,43],31:[2,43],34:[2,43],37:[2,43]},{18:[1,49]},{18:[1,50]},{18:[1,51]},{18:[1,52],21:53,34:[1,26],36:25},{5:[2,2],8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,2],22:[1,13],23:[1,14],24:[1,15]},{14:[2,20],15:[2,20],16:[2,20],19:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,7],14:[2,7],15:[2,7],16:[2,7],19:[2,7],20:[2,7],22:[2,7],23:[2,7],24:[2,7]},{21:54,34:[1,26],36:25},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{18:[2,22],21:41,26:55,27:[1,45],28:56,29:[1,42],30:[1,43],31:[1,44],32:40,33:46,34:[1,47],36:25},{18:[2,23]},{18:[2,27],27:[2,27],29:[2,27],30:[2,27],31:[2,27],34:[2,27]},{18:[2,33],33:57,34:[1,58]},{18:[2,28],27:[2,28],29:[2,28],30:[2,28],31:[2,28],34:[2,28]},{18:[2,29],27:[2,29],29:[2,29],30:[2,29],31:[2,29],34:[2,29]},{18:[2,30],27:[2,30],29:[2,30],30:[2,30],31:[2,30],34:[2,30]},{18:[2,31],27:[2,31],29:[2,31],30:[2,31],31:[2,31],34:[2,31]},{18:[2,32],27:[2,32],29:[2,32],30:[2,32],31:[2,32],34:[2,32]},{18:[2,35],34:[2,35]},{18:[2,43],27:[2,43],29:[2,43],30:[2,43],31:[2,43],34:[2,43],35:[1,59],37:[2,43]},{34:[1,60]},{14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,17],14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]},{18:[1,61]},{18:[1,62]},{18:[2,21]},{18:[2,26],27:[2,26],29:[2,26],30:[2,26],31:[2,26],34:[2,26]},{18:[2,34],34:[2,34]},{35:[1,59]},{21:63,27:[1,67],29:[1,64],30:[1,65],31:[1,66],34:[1,26],36:25},{18:[2,42],27:[2,42],29:[2,42],30:[2,42],31:[2,42],34:[2,42],37:[2,42]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{18:[2,36],34:[2,36]},{18:[2,37],34:[2,37]},{18:[2,38],34:[2,38]},{18:[2,39],34:[2,39]},{18:[2,40],34:[2,40]}],
-defaultActions: {16:[2,1],24:[2,25],38:[2,23],55:[2,21]},
-parseError: function parseError(str, hash) {
-    throw new Error(str);
-},
-parse: function parse(input) {
-    var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
-    this.lexer.setInput(input);
-    this.lexer.yy = this.yy;
-    this.yy.lexer = this.lexer;
-    this.yy.parser = this;
-    if (typeof this.lexer.yylloc == "undefined")
-        this.lexer.yylloc = {};
-    var yyloc = this.lexer.yylloc;
-    lstack.push(yyloc);
-    var ranges = this.lexer.options && this.lexer.options.ranges;
-    if (typeof this.yy.parseError === "function")
-        this.parseError = this.yy.parseError;
-    function popStack(n) {
-        stack.length = stack.length - 2 * n;
-        vstack.length = vstack.length - n;
-        lstack.length = lstack.length - n;
-    }
-    function lex() {
-        var token;
-        token = self.lexer.lex() || 1;
-        if (typeof token !== "number") {
-            token = self.symbols_[token] || token;
-        }
-        return token;
-    }
-    var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
-    while (true) {
-        state = stack[stack.length - 1];
-        if (this.defaultActions[state]) {
-            action = this.defaultActions[state];
-        } else {
-            if (symbol === null || typeof symbol == "undefined") {
-                symbol = lex();
-            }
-            action = table[state] && table[state][symbol];
-        }
-        if (typeof action === "undefined" || !action.length || !action[0]) {
-            var errStr = "";
-            if (!recovering) {
-                expected = [];
-                for (p in table[state])
-                    if (this.terminals_[p] && p > 2) {
-                        expected.push("'" + this.terminals_[p] + "'");
-                    }
-                if (this.lexer.showPosition) {
-                    errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'";
-                } else {
-                    errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'");
-                }
-                this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
-            }
-        }
-        if (action[0] instanceof Array && action.length > 1) {
-            throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol);
-        }
-        switch (action[0]) {
-        case 1:
-            stack.push(symbol);
-            vstack.push(this.lexer.yytext);
-            lstack.push(this.lexer.yylloc);
-            stack.push(action[1]);
-            symbol = null;
-            if (!preErrorSymbol) {
-                yyleng = this.lexer.yyleng;
-                yytext = this.lexer.yytext;
-                yylineno = this.lexer.yylineno;
-                yyloc = this.lexer.yylloc;
-                if (recovering > 0)
-                    recovering--;
-            } else {
-                symbol = preErrorSymbol;
-                preErrorSymbol = null;
-            }
-            break;
-        case 2:
-            len = this.productions_[action[1]][1];
-            yyval.$ = vstack[vstack.length - len];
-            yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column};
-            if (ranges) {
-                yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]];
-            }
-            r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
-            if (typeof r !== "undefined") {
-                return r;
-            }
-            if (len) {
-                stack = stack.slice(0, -1 * len * 2);
-                vstack = vstack.slice(0, -1 * len);
-                lstack = lstack.slice(0, -1 * len);
-            }
-            stack.push(this.productions_[action[1]][0]);
-            vstack.push(yyval.$);
-            lstack.push(yyval._$);
-            newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
-            stack.push(newState);
-            break;
-        case 3:
-            return true;
-        }
-    }
-    return true;
-}
-};
-/* Jison generated lexer */
-var lexer = (function(){
-var lexer = ({EOF:1,
-parseError:function parseError(str, hash) {
-        if (this.yy.parser) {
-            this.yy.parser.parseError(str, hash);
-        } else {
-            throw new Error(str);
-        }
-    },
-setInput:function (input) {
-        this._input = input;
-        this._more = this._less = this.done = false;
-        this.yylineno = this.yyleng = 0;
-        this.yytext = this.matched = this.match = '';
-        this.conditionStack = ['INITIAL'];
-        this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
-        if (this.options.ranges) this.yylloc.range = [0,0];
-        this.offset = 0;
-        return this;
-    },
-input:function () {
-        var ch = this._input[0];
-        this.yytext += ch;
-        this.yyleng++;
-        this.offset++;
-        this.match += ch;
-        this.matched += ch;
-        var lines = ch.match(/(?:\r\n?|\n).*/g);
-        if (lines) {
-            this.yylineno++;
-            this.yylloc.last_line++;
-        } else {
-            this.yylloc.last_column++;
-        }
-        if (this.options.ranges) this.yylloc.range[1]++;
-
-        this._input = this._input.slice(1);
-        return ch;
-    },
-unput:function (ch) {
-        var len = ch.length;
-        var lines = ch.split(/(?:\r\n?|\n)/g);
-
-        this._input = ch + this._input;
-        this.yytext = this.yytext.substr(0, this.yytext.length-len-1);
-        //this.yyleng -= len;
-        this.offset -= len;
-        var oldLines = this.match.split(/(?:\r\n?|\n)/g);
-        this.match = this.match.substr(0, this.match.length-1);
-        this.matched = this.matched.substr(0, this.matched.length-1);
-
-        if (lines.length-1) this.yylineno -= lines.length-1;
-        var r = this.yylloc.range;
-
-        this.yylloc = {first_line: this.yylloc.first_line,
-          last_line: this.yylineno+1,
-          first_column: this.yylloc.first_column,
-          last_column: lines ?
-              (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length:
-              this.yylloc.first_column - len
-          };
-
-        if (this.options.ranges) {
-            this.yylloc.range = [r[0], r[0] + this.yyleng - len];
-        }
-        return this;
-    },
-more:function () {
-        this._more = true;
-        return this;
-    },
-less:function (n) {
-        this.unput(this.match.slice(n));
-    },
-pastInput:function () {
-        var past = this.matched.substr(0, this.matched.length - this.match.length);
-        return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
-    },
-upcomingInput:function () {
-        var next = this.match;
-        if (next.length < 20) {
-            next += this._input.substr(0, 20-next.length);
-        }
-        return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
-    },
-showPosition:function () {
-        var pre = this.pastInput();
-        var c = new Array(pre.length + 1).join("-");
-        return pre + this.upcomingInput() + "\n" + c+"^";
-    },
-next:function () {
-        if (this.done) {
-            return this.EOF;
-        }
-        if (!this._input) this.done = true;
-
-        var token,
-            match,
-            tempMatch,
-            index,
-            col,
-            lines;
-        if (!this._more) {
-            this.yytext = '';
-            this.match = '';
-        }
-        var rules = this._currentRules();
-        for (var i=0;i < rules.length; i++) {
-            tempMatch = this._input.match(this.rules[rules[i]]);
-            if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
-                match = tempMatch;
-                index = i;
-                if (!this.options.flex) break;
-            }
-        }
-        if (match) {
-            lines = match[0].match(/(?:\r\n?|\n).*/g);
-            if (lines) this.yylineno += lines.length;
-            this.yylloc = {first_line: this.yylloc.last_line,
-                           last_line: this.yylineno+1,
-                           first_column: this.yylloc.last_column,
-                           last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length};
-            this.yytext += match[0];
-            this.match += match[0];
-            this.matches = match;
-            this.yyleng = this.yytext.length;
-            if (this.options.ranges) {
-                this.yylloc.range = [this.offset, this.offset += this.yyleng];
-            }
-            this._more = false;
-            this._input = this._input.slice(match[0].length);
-            this.matched += match[0];
-            token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
-            if (this.done && this._input) this.done = false;
-            if (token) return token;
-            else return;
-        }
-        if (this._input === "") {
-            return this.EOF;
-        } else {
-            return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
-                    {text: "", token: null, line: this.yylineno});
-        }
-    },
-lex:function lex() {
-        var r = this.next();
-        if (typeof r !== 'undefined') {
-            return r;
-        } else {
-            return this.lex();
-        }
-    },
-begin:function begin(condition) {
-        this.conditionStack.push(condition);
-    },
-popState:function popState() {
-        return this.conditionStack.pop();
-    },
-_currentRules:function _currentRules() {
-        return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
-    },
-topState:function () {
-        return this.conditionStack[this.conditionStack.length-2];
-    },
-pushState:function begin(condition) {
-        this.begin(condition);
-    }});
-lexer.options = {};
-lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
-
-var YYSTATE=YY_START
-switch($avoiding_name_collisions) {
-case 0:
-                                   if(yy_.yytext.slice(-1) !== "\\") this.begin("mu");
-                                   if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu");
-                                   if(yy_.yytext) return 14;
-                                 
-break;
-case 1: return 14; 
-break;
-case 2:
-                                   if(yy_.yytext.slice(-1) !== "\\") this.popState();
-                                   if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1);
-                                   return 14;
-                                 
-break;
-case 3: return 24; 
-break;
-case 4: return 16; 
-break;
-case 5: return 20; 
-break;
-case 6: return 19; 
-break;
-case 7: return 19; 
-break;
-case 8: return 23; 
-break;
-case 9: return 23; 
-break;
-case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 
-break;
-case 11: return 22; 
-break;
-case 12: return 35; 
-break;
-case 13: return 34; 
-break;
-case 14: return 34; 
-break;
-case 15: return 37; 
-break;
-case 16: /*ignore whitespace*/ 
-break;
-case 17: this.popState(); return 18; 
-break;
-case 18: this.popState(); return 18; 
-break;
-case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29; 
-break;
-case 20: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29; 
-break;
-case 21: yy_.yytext = yy_.yytext.substr(1); return 27; 
-break;
-case 22: return 31; 
-break;
-case 23: return 31; 
-break;
-case 24: return 30; 
-break;
-case 25: return 34; 
-break;
-case 26: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 34; 
-break;
-case 27: return 'INVALID'; 
-break;
-case 28: return 5; 
-break;
-}
-};
-lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/];
-lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,28],"inclusive":true}};
-return lexer;})()
-parser.lexer = lexer;
-function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
-return new Parser;
-})();
-if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
-exports.parser = handlebars;
-exports.Parser = handlebars.Parser;
-exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); }
-exports.main = function commonjsMain(args) {
-    if (!args[1])
-        throw new Error('Usage: '+args[0]+' FILE');
-    var source, cwd;
-    if (typeof process !== 'undefined') {
-        source = require('fs').readFileSync(require('path').resolve(args[1]), "utf8");
-    } else {
-        source = require("file").path(require("file").cwd()).join(args[1]).read({charset: "utf-8"});
-    }
-    return exports.parser.parse(source);
-}
-if (typeof module !== 'undefined' && require.main === module) {
-  exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args);
-}
-};
-;
-// lib/handlebars/compiler/base.js
-Handlebars.Parser = handlebars;
-
-Handlebars.parse = function(string) {
-  Handlebars.Parser.yy = Handlebars.AST;
-  return Handlebars.Parser.parse(string);
-};
-
-Handlebars.print = function(ast) {
-  return new Handlebars.PrintVisitor().accept(ast);
-};
-
-Handlebars.logger = {
-  DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
-
-  // override in the host environment
-  log: function(level, str) {}
-};
-
-Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); };
-;
-// lib/handlebars/compiler/ast.js
-(function() {
-
-  Handlebars.AST = {};
-
-  Handlebars.AST.ProgramNode = function(statements, inverse) {
-    this.type = "program";
-    this.statements = statements;
-    if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
-  };
-
-  Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
-    this.type = "mustache";
-    this.escaped = !unescaped;
-    this.hash = hash;
-
-    var id = this.id = rawParams[0];
-    var params = this.params = rawParams.slice(1);
-
-    // a mustache is an eligible helper if:
-    // * its id is simple (a single part, not `this` or `..`)
-    var eligibleHelper = this.eligibleHelper = id.isSimple;
-
-    // a mustache is definitely a helper if:
-    // * it is an eligible helper, and
-    // * it has at least one parameter or hash segment
-    this.isHelper = eligibleHelper && (params.length || hash);
-
-    // if a mustache is an eligible helper but not a definite
-    // helper, it is ambiguous, and will be resolved in a later
-    // pass or at runtime.
-  };
-
-  Handlebars.AST.PartialNode = function(id, context) {
-    this.type    = "partial";
-
-    // TODO: disallow complex IDs
-
-    this.id      = id;
-    this.context = context;
-  };
-
-  var verifyMatch = function(open, close) {
-    if(open.original !== close.original) {
-      throw new Handlebars.Exception(open.original + " doesn't match " + close.original);
-    }
-  };
-
-  Handlebars.AST.BlockNode = function(mustache, program, inverse, close) {
-    verifyMatch(mustache.id, close);
-    this.type = "block";
-    this.mustache = mustache;
-    this.program  = program;
-    this.inverse  = inverse;
-
-    if (this.inverse && !this.program) {
-      this.isInverse = true;
-    }
-  };
-
-  Handlebars.AST.ContentNode = function(string) {
-    this.type = "content";
-    this.string = string;
-  };
-
-  Handlebars.AST.HashNode = function(pairs) {
-    this.type = "hash";
-    this.pairs = pairs;
-  };
-
-  Handlebars.AST.IdNode = function(parts) {
-    this.type = "ID";
-    this.original = parts.join(".");
-
-    var dig = [], depth = 0;
-
-    for(var i=0,l=parts.length; i<l; i++) {
-      var part = parts[i];
-
-      if(part === "..") { depth++; }
-      else if(part === "." || part === "this") { this.isScoped = true; }
-      else { dig.push(part); }
-    }
-
-    this.parts    = dig;
-    this.string   = dig.join('.');
-    this.depth    = depth;
-
-    // an ID is simple if it only has one part, and that part is not
-    // `..` or `this`.
-    this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
-  };
-
-  Handlebars.AST.DataNode = function(id) {
-    this.type = "DATA";
-    this.id = id;
-  };
-
-  Handlebars.AST.StringNode = function(string) {
-    this.type = "STRING";
-    this.string = string;
-  };
-
-  Handlebars.AST.IntegerNode = function(integer) {
-    this.type = "INTEGER";
-    this.integer = integer;
-  };
-
-  Handlebars.AST.BooleanNode = function(bool) {
-    this.type = "BOOLEAN";
-    this.bool = bool;
-  };
-
-  Handlebars.AST.CommentNode = function(comment) {
-    this.type = "comment";
-    this.comment = comment;
-  };
-
-})();;
-// lib/handlebars/utils.js
-Handlebars.Exception = function(message) {
-  var tmp = Error.prototype.constructor.apply(this, arguments);
-
-  for (var p in tmp) {
-    if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; }
-  }
-
-  this.message = tmp.message;
-};
-Handlebars.Exception.prototype = new Error();
-
-// Build out our basic SafeString type
-Handlebars.SafeString = function(string) {
-  this.string = string;
-};
-Handlebars.SafeString.prototype.toString = function() {
-  return this.string.toString();
-};
-
-(function() {
-  var escape = {
-    "&": "&amp;",
-    "<": "&lt;",
-    ">": "&gt;",
-    '"': "&quot;",
-    "'": "&#x27;",
-    "`": "&#x60;"
-  };
-
-  var badChars = /[&<>"'`]/g;
-  var possible = /[&<>"'`]/;
-
-  var escapeChar = function(chr) {
-    return escape[chr] || "&amp;";
-  };
-
-  Handlebars.Utils = {
-    escapeExpression: function(string) {
-      // don't escape SafeStrings, since they're already safe
-      if (string instanceof Handlebars.SafeString) {
-        return string.toString();
-      } else if (string == null || string === false) {
-        return "";
-      }
-
-      if(!possible.test(string)) { return string; }
-      return string.replace(badChars, escapeChar);
-    },
-
-    isEmpty: function(value) {
-      if (typeof value === "undefined") {
-        return true;
-      } else if (value === null) {
-        return true;
-      } else if (value === false) {
-        return true;
-      } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) {
-        return true;
-      } else {
-        return false;
-      }
-    }
-  };
-})();;
-// lib/handlebars/compiler/compiler.js
-
-/*jshint eqnull:true*/
-Handlebars.Compiler = function() {};
-Handlebars.JavaScriptCompiler = function() {};
-
-(function(Compiler, JavaScriptCompiler) {
-  // the foundHelper register will disambiguate helper lookup from finding a
-  // function in a context. This is necessary for mustache compatibility, which
-  // requires that context functions in blocks are evaluated by blockHelperMissing,
-  // and then proceed as if the resulting value was provided to blockHelperMissing.
-
-  Compiler.prototype = {
-    compiler: Compiler,
-
-    disassemble: function() {
-      var opcodes = this.opcodes, opcode, out = [], params, param;
-
-      for (var i=0, l=opcodes.length; i<l; i++) {
-        opcode = opcodes[i];
-
-        if (opcode.opcode === 'DECLARE') {
-          out.push("DECLARE " + opcode.name + "=" + opcode.value);
-        } else {
-          params = [];
-          for (var j=0; j<opcode.args.length; j++) {
-            param = opcode.args[j];
-            if (typeof param === "string") {
-              param = "\"" + param.replace("\n", "\\n") + "\"";
-            }
-            params.push(param);
-          }
-          out.push(opcode.opcode + " " + params.join(" "));
-        }
-      }
-
-      return out.join("\n");
-    },
-
-    guid: 0,
-
-    compile: function(program, options) {
-      this.children = [];
-      this.depths = {list: []};
-      this.options = options;
-
-      // These changes will propagate to the other compiler components
-      var knownHelpers = this.options.knownHelpers;
-      this.options.knownHelpers = {
-        'helperMissing': true,
-        'blockHelperMissing': true,
-        'each': true,
-        'if': true,
-        'unless': true,
-        'with': true,
-        'log': true
-      };
-      if (knownHelpers) {
-        for (var name in knownHelpers) {
-          this.options.knownHelpers[name] = knownHelpers[name];
-        }
-      }
-
-      return this.program(program);
-    },
-
-    accept: function(node) {
-      return this[node.type](node);
-    },
-
-    program: function(program) {
-      var statements = program.statements, statement;
-      this.opcodes = [];
-
-      for(var i=0, l=statements.length; i<l; i++) {
-        statement = statements[i];
-        this[statement.type](statement);
-      }
-      this.isSimple = l === 1;
-
-      this.depths.list = this.depths.list.sort(function(a, b) {
-        return a - b;
-      });
-
-      return this;
-    },
-
-    compileProgram: function(program) {
-      var result = new this.compiler().compile(program, this.options);
-      var guid = this.guid++, depth;
-
-      this.usePartial = this.usePartial || result.usePartial;
-
-      this.children[guid] = result;
-
-      for(var i=0, l=result.depths.list.length; i<l; i++) {
-        depth = result.depths.list[i];
-
-        if(depth < 2) { continue; }
-        else { this.addDepth(depth - 1); }
-      }
-
-      return guid;
-    },
-
-    block: function(block) {
-      var mustache = block.mustache,
-          program = block.program,
-          inverse = block.inverse;
-
-      if (program) {
-        program = this.compileProgram(program);
-      }
-
-      if (inverse) {
-        inverse = this.compileProgram(inverse);
-      }
-
-      var type = this.classifyMustache(mustache);
-
-      if (type === "helper") {
-        this.helperMustache(mustache, program, inverse);
-      } else if (type === "simple") {
-        this.simpleMustache(mustache);
-
-        // now that the simple mustache is resolved, we need to
-        // evaluate it by executing `blockHelperMissing`
-        this.opcode('pushProgram', program);
-        this.opcode('pushProgram', inverse);
-        this.opcode('pushLiteral', '{}');
-        this.opcode('blockValue');
-      } else {
-        this.ambiguousMustache(mustache, program, inverse);
-
-        // now that the simple mustache is resolved, we need to
-        // evaluate it by executing `blockHelperMissing`
-        this.opcode('pushProgram', program);
-        this.opcode('pushProgram', inverse);
-        this.opcode('pushLiteral', '{}');
-        this.opcode('ambiguousBlockValue');
-      }
-
-      this.opcode('append');
-    },
-
-    hash: function(hash) {
-      var pairs = hash.pairs, pair, val;
-
-      this.opcode('push', '{}');
-
-      for(var i=0, l=pairs.length; i<l; i++) {
-        pair = pairs[i];
-        val  = pair[1];
-
-        this.accept(val);
-        this.opcode('assignToHash', pair[0]);
-      }
-    },
-
-    partial: function(partial) {
-      var id = partial.id;
-      this.usePartial = true;
-
-      if(partial.context) {
-        this.ID(partial.context);
-      } else {
-        this.opcode('push', 'depth0');
-      }
-
-      this.opcode('invokePartial', id.original);
-      this.opcode('append');
-    },
-
-    content: function(content) {
-      this.opcode('appendContent', content.string);
-    },
-
-    mustache: function(mustache) {
-      var options = this.options;
-      var type = this.classifyMustache(mustache);
-
-      if (type === "simple") {
-        this.simpleMustache(mustache);
-      } else if (type === "helper") {
-        this.helperMustache(mustache);
-      } else {
-        this.ambiguousMustache(mustache);
-      }
-
-      if(mustache.escaped && !options.noEscape) {
-        this.opcode('appendEscaped');
-      } else {
-        this.opcode('append');
-      }
-    },
-
-    ambiguousMustache: function(mustache, program, inverse) {
-      var id = mustache.id, name = id.parts[0];
-
-      this.opcode('getContext', id.depth);
-
-      this.opcode('pushProgram', program);
-      this.opcode('pushProgram', inverse);
-
-      this.opcode('invokeAmbiguous', name);
-    },
-
-    simpleMustache: function(mustache, program, inverse) {
-      var id = mustache.id;
-
-      if (id.type === 'DATA') {
-        this.DATA(id);
-      } else if (id.parts.length) {
-        this.ID(id);
-      } else {
-        // Simplified ID for `this`
-        this.addDepth(id.depth);
-        this.opcode('getContext', id.depth);
-        this.opcode('pushContext');
-      }
-
-      this.opcode('resolvePossibleLambda');
-    },
-
-    helperMustache: function(mustache, program, inverse) {
-      var params = this.setupFullMustacheParams(mustache, program, inverse),
-          name = mustache.id.parts[0];
-
-      if (this.options.knownHelpers[name]) {
-        this.opcode('invokeKnownHelper', params.length, name);
-      } else if (this.knownHelpersOnly) {
-        throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name);
-      } else {
-        this.opcode('invokeHelper', params.length, name);
-      }
-    },
-
-    ID: function(id) {
-      this.addDepth(id.depth);
-      this.opcode('getContext', id.depth);
-
-      var name = id.parts[0];
-      if (!name) {
-        this.opcode('pushContext');
-      } else {
-        this.opcode('lookupOnContext', id.parts[0]);
-      }
-
-      for(var i=1, l=id.parts.length; i<l; i++) {
-        this.opcode('lookup', id.parts[i]);
-      }
-    },
-
-    DATA: function(data) {
-      this.options.data = true;
-      this.opcode('lookupData', data.id);
-    },
-
-    STRING: function(string) {
-      this.opcode('pushString', string.string);
-    },
-
-    INTEGER: function(integer) {
-      this.opcode('pushLiteral', integer.integer);
-    },
-
-    BOOLEAN: function(bool) {
-      this.opcode('pushLiteral', bool.bool);
-    },
-
-    comment: function() {},
-
-    // HELPERS
-    opcode: function(name) {
-      this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
-    },
-
-    declare: function(name, value) {
-      this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
-    },
-
-    addDepth: function(depth) {
-      if(isNaN(depth)) { throw new Error("EWOT"); }
-      if(depth === 0) { return; }
-
-      if(!this.depths[depth]) {
-        this.depths[depth] = true;
-        this.depths.list.push(depth);
-      }
-    },
-
-    classifyMustache: function(mustache) {
-      var isHelper   = mustache.isHelper;
-      var isEligible = mustache.eligibleHelper;
-      var options    = this.options;
-
-      // if ambiguous, we can possibly resolve the ambiguity now
-      if (isEligible && !isHelper) {
-        var name = mustache.id.parts[0];
-
-        if (options.knownHelpers[name]) {
-          isHelper = true;
-        } else if (options.knownHelpersOnly) {
-          isEligible = false;
-        }
-      }
-
-      if (isHelper) { return "helper"; }
-      else if (isEligible) { return "ambiguous"; }
-      else { return "simple"; }
-    },
-
-    pushParams: function(params) {
-      var i = params.length, param;
-
-      while(i--) {
-        param = params[i];
-
-        if(this.options.stringParams) {
-          if(param.depth) {
-            this.addDepth(param.depth);
-          }
-
-          this.opcode('getContext', param.depth || 0);
-          this.opcode('pushStringParam', param.string);
-        } else {
-          this[param.type](param);
-        }
-      }
-    },
-
-    setupMustacheParams: function(mustache) {
-      var params = mustache.params;
-      this.pushParams(params);
-
-      if(mustache.hash) {
-        this.hash(mustache.hash);
-      } else {
-        this.opcode('pushLiteral', '{}');
-      }
-
-      return params;
-    },
-
-    // this will replace setupMustacheParams when we're done
-    setupFullMustacheParams: function(mustache, program, inverse) {
-      var params = mustache.params;
-      this.pushParams(params);
-
-      this.opcode('pushProgram', program);
-      this.opcode('pushProgram', inverse);
-
-      if(mustache.hash) {
-        this.hash(mustache.hash);
-      } else {
-        this.opcode('pushLiteral', '{}');
-      }
-
-      return params;
-    }
-  };
-
-  var Literal = function(value) {
-    this.value = value;
-  };
-
-  JavaScriptCompiler.prototype = {
-    // PUBLIC API: You can override these methods in a subclass to provide
-    // alternative compiled forms for name lookup and buffering semantics
-    nameLookup: function(parent, name, type) {
-      if (/^[0-9]+$/.test(name)) {
-        return parent + "[" + name + "]";
-      } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
-        return parent + "." + name;
-      }
-      else {
-        return parent + "['" + name + "']";
-      }
-    },
-
-    appendToBuffer: function(string) {
-      if (this.environment.isSimple) {
-        return "return " + string + ";";
-      } else {
-        return "buffer += " + string + ";";
-      }
-    },
-
-    initializeBuffer: function() {
-      return this.quotedString("");
-    },
-
-    namespace: "Handlebars",
-    // END PUBLIC API
-
-    compile: function(environment, options, context, asObject) {
-      this.environment = environment;
-      this.options = options || {};
-
-      Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n");
-
-      this.name = this.environment.name;
-      this.isChild = !!context;
-      this.context = context || {
-        programs: [],
-        aliases: { }
-      };
-
-      this.preamble();
-
-      this.stackSlot = 0;
-      this.stackVars = [];
-      this.registers = { list: [] };
-      this.compileStack = [];
-
-      this.compileChildren(environment, options);
-
-      var opcodes = environment.opcodes, opcode;
-
-      this.i = 0;
-
-      for(l=opcodes.length; this.i<l; this.i++) {
-        opcode = opcodes[this.i];
-
-        if(opcode.opcode === 'DECLARE') {
-          this[opcode.name] = opcode.value;
-        } else {
-          this[opcode.opcode].apply(this, opcode.args);
-        }
-      }
-
-      return this.createFunctionContext(asObject);
-    },
-
-    nextOpcode: function() {
-      var opcodes = this.environment.opcodes, opcode = opcodes[this.i + 1];
-      return opcodes[this.i + 1];
-    },
-
-    eat: function(opcode) {
-      this.i = this.i + 1;
-    },
-
-    preamble: function() {
-      var out = [];
-
-      if (!this.isChild) {
-        var namespace = this.namespace;
-        var copies = "helpers = helpers || " + namespace + ".helpers;";
-        if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; }
-        if (this.options.data) { copies = copies + " data = data || {};"; }
-        out.push(copies);
-      } else {
-        out.push('');
-      }
-
-      if (!this.environment.isSimple) {
-        out.push(", buffer = " + this.initializeBuffer());
-      } else {
-        out.push("");
-      }
-
-      // track the last context pushed into place to allow skipping the
-      // getContext opcode when it would be a noop
-      this.lastContext = 0;
-      this.source = out;
-    },
-
-    createFunctionContext: function(asObject) {
-      var locals = this.stackVars.concat(this.registers.list);
-
-      if(locals.length > 0) {
-        this.source[1] = this.source[1] + ", " + locals.join(", ");
-      }
-
-      // Generate minimizer alias mappings
-      if (!this.isChild) {
-        var aliases = [];
-        for (var alias in this.context.aliases) {
-          this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
-        }
-      }
-
-      if (this.source[1]) {
-        this.source[1] = "var " + this.source[1].substring(2) + ";";
-      }
-
-      // Merge children
-      if (!this.isChild) {
-        this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
-      }
-
-      if (!this.environment.isSimple) {
-        this.source.push("return buffer;");
-      }
-
-      var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
-
-      for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
-        params.push("depth" + this.environment.depths.list[i]);
-      }
-
-      if (asObject) {
-        params.push(this.source.join("\n  "));
-
-        return Function.apply(this, params);
-      } else {
-        var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + this.source.join("\n  ") + '}';
-        Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
-        return functionSource;
-      }
-    },
-
-    // [blockValue]
-    //
-    // On stack, before: hash, inverse, program, value
-    // On stack, after: return value of blockHelperMissing
-    //
-    // The purpose of this opcode is to take a block of the form
-    // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
-    // replace it on the stack with the result of properly
-    // invoking blockHelperMissing.
-    blockValue: function() {
-      this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
-
-      var params = ["depth0"];
-      this.setupParams(0, params);
-
-      this.replaceStack(function(current) {
-        params.splice(1, 0, current);
-        return current + " = blockHelperMissing.call(" + params.join(", ") + ")";
-      });
-    },
-
-    // [ambiguousBlockValue]
-    //
-    // On stack, before: hash, inverse, program, value
-    // Compiler value, before: lastHelper=value of last found helper, if any
-    // On stack, after, if no lastHelper: same as [blockValue]
-    // On stack, after, if lastHelper: value
-    ambiguousBlockValue: function() {
-      this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
-
-      var params = ["depth0"];
-      this.setupParams(0, params);
-
-      var current = this.topStack();
-      params.splice(1, 0, current);
-
-      this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
-    },
-
-    // [appendContent]
-    //
-    // On stack, before: ...
-    // On stack, after: ...
-    //
-    // Appends the string value of `content` to the current buffer
-    appendContent: function(content) {
-      this.source.push(this.appendToBuffer(this.quotedString(content)));
-    },
-
-    // [append]
-    //
-    // On stack, before: value, ...
-    // On stack, after: ...
-    //
-    // Coerces `value` to a String and appends it to the current buffer.
-    //
-    // If `value` is truthy, or 0, it is coerced into a string and appended
-    // Otherwise, the empty string is appended
-    append: function() {
-      var local = this.popStack();
-      this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
-      if (this.environment.isSimple) {
-        this.source.push("else { " + this.appendToBuffer("''") + " }");
-      }
-    },
-
-    // [appendEscaped]
-    //
-    // On stack, before: value, ...
-    // On stack, after: ...
-    //
-    // Escape `value` and append it to the buffer
-    appendEscaped: function() {
-      var opcode = this.nextOpcode(), extra = "";
-      this.context.aliases.escapeExpression = 'this.escapeExpression';
-
-      if(opcode && opcode.opcode === 'appendContent') {
-        extra = " + " + this.quotedString(opcode.args[0]);
-        this.eat(opcode);
-      }
-
-      this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")" + extra));
-    },
-
-    // [getContext]
-    //
-    // On stack, before: ...
-    // On stack, after: ...
-    // Compiler value, after: lastContext=depth
-    //
-    // Set the value of the `lastContext` compiler value to the depth
-    getContext: function(depth) {
-      if(this.lastContext !== depth) {
-        this.lastContext = depth;
-      }
-    },
-
-    // [lookupOnContext]
-    //
-    // On stack, before: ...
-    // On stack, after: currentContext[name], ...
-    //
-    // Looks up the value of `name` on the current context and pushes
-    // it onto the stack.
-    lookupOnContext: function(name) {
-      this.pushStack(this.nameLookup('depth' + this.lastContext, name, 'context'));
-    },
-
-    // [pushContext]
-    //
-    // On stack, before: ...
-    // On stack, after: currentContext, ...
-    //
-    // Pushes the value of the current context onto the stack.
-    pushContext: function() {
-      this.pushStackLiteral('depth' + this.lastContext);
-    },
-
-    // [resolvePossibleLambda]
-    //
-    // On stack, before: value, ...
-    // On stack, after: resolved value, ...
-    //
-    // If the `value` is a lambda, replace it on the stack by
-    // the return value of the lambda
-    resolvePossibleLambda: function() {
-      this.context.aliases.functionType = '"function"';
-
-      this.replaceStack(function(current) {
-        return "typeof " + current + " === functionType ? " + current + "() : " + current;
-      });
-    },
-
-    // [lookup]
-    //
-    // On stack, before: value, ...
-    // On stack, after: value[name], ...
-    //
-    // Replace the value on the stack with the result of looking
-    // up `name` on `value`
-    lookup: function(name) {
-      this.replaceStack(function(current) {
-        return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
-      });
-    },
-
-    // [lookupData]
-    //
-    // On stack, before: ...
-    // On stack, after: data[id], ...
-    //
-    // Push the result of looking up `id` on the current data
-    lookupData: function(id) {
-      this.pushStack(this.nameLookup('data', id, 'data'));
-    },
-
-    // [pushStringParam]
-    //
-    // On stack, before: ...
-    // On stack, after: string, currentContext, ...
-    //
-    // This opcode is designed for use in string mode, which
-    // provides the string value of a parameter along with its
-    // depth rather than resolving it immediately.
-    pushStringParam: function(string) {
-      this.pushStackLiteral('depth' + this.lastContext);
-      this.pushString(string);
-    },
-
-    // [pushString]
-    //
-    // On stack, before: ...
-    // On stack, after: quotedString(string), ...
-    //
-    // Push a quoted version of `string` onto the stack
-    pushString: function(string) {
-      this.pushStackLiteral(this.quotedString(string));
-    },
-
-    // [push]
-    //
-    // On stack, before: ...
-    // On stack, after: expr, ...
-    //
-    // Push an expression onto the stack
-    push: function(expr) {
-      this.pushStack(expr);
-    },
-
-    // [pushLiteral]
-    //
-    // On stack, before: ...
-    // On stack, after: value, ...
-    //
-    // Pushes a value onto the stack. This operation prevents
-    // the compiler from creating a temporary variable to hold
-    // it.
-    pushLiteral: function(value) {
-      this.pushStackLiteral(value);
-    },
-
-    // [pushProgram]
-    //
-    // On stack, before: ...
-    // On stack, after: program(guid), ...
-    //
-    // Push a program expression onto the stack. This takes
-    // a compile-time guid and converts it into a runtime-accessible
-    // expression.
-    pushProgram: function(guid) {
-      if (guid != null) {
-        this.pushStackLiteral(this.programExpression(guid));
-      } else {
-        this.pushStackLiteral(null);
-      }
-    },
-
-    // [invokeHelper]
-    //
-    // On stack, before: hash, inverse, program, params..., ...
-    // On stack, after: result of helper invocation
-    //
-    // Pops off the helper's parameters, invokes the helper,
-    // and pushes the helper's return value onto the stack.
-    //
-    // If the helper is not found, `helperMissing` is called.
-    invokeHelper: function(paramSize, name) {
-      this.context.aliases.helperMissing = 'helpers.helperMissing';
-
-      var helper = this.lastHelper = this.setupHelper(paramSize, name);
-      this.register('foundHelper', helper.name);
-
-      this.pushStack("foundHelper ? foundHelper.call(" +
-        helper.callParams + ") " + ": helperMissing.call(" +
-        helper.helperMissingParams + ")");
-    },
-
-    // [invokeKnownHelper]
-    //
-    // On stack, before: hash, inverse, program, params..., ...
-    // On stack, after: result of helper invocation
-    //
-    // This operation is used when the helper is known to exist,
-    // so a `helperMissing` fallback is not required.
-    invokeKnownHelper: function(paramSize, name) {
-      var helper = this.setupHelper(paramSize, name);
-      this.pushStack(helper.name + ".call(" + helper.callParams + ")");
-    },
-
-    // [invokeAmbiguous]
-    //
-    // On stack, before: hash, inverse, program, params..., ...
-    // On stack, after: result of disambiguation
-    //
-    // This operation is used when an expression like `{{foo}}`
-    // is provided, but we don't know at compile-time whether it
-    // is a helper or a path.
-    //
-    // This operation emits more code than the other options,
-    // and can be avoided by passing the `knownHelpers` and
-    // `knownHelpersOnly` flags at compile-time.
-    invokeAmbiguous: function(name) {
-      this.context.aliases.functionType = '"function"';
-
-      this.pushStackLiteral('{}');
-      var helper = this.setupHelper(0, name);
-
-      var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
-      this.register('foundHelper', helperName);
-
-      var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
-      var nextStack = this.nextStack();
-
-      this.source.push('if (foundHelper) { ' + nextStack + ' = foundHelper.call(' + helper.callParams + '); }');
-      this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '() : ' + nextStack + '; }');
-    },
-
-    // [invokePartial]
-    //
-    // On stack, before: context, ...
-    // On stack after: result of partial invocation
-    //
-    // This operation pops off a context, invokes a partial with that context,
-    // and pushes the result of the invocation back.
-    invokePartial: function(name) {
-      var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
-
-      if (this.options.data) {
-        params.push("data");
-      }
-
-      this.context.aliases.self = "this";
-      this.pushStack("self.invokePartial(" + params.join(", ") + ");");
-    },
-
-    // [assignToHash]
-    //
-    // On stack, before: value, hash, ...
-    // On stack, after: hash, ...
-    //
-    // Pops a value and hash off the stack, assigns `hash[key] = value`
-    // and pushes the hash back onto the stack.
-    assignToHash: function(key) {
-      var value = this.popStack();
-      var hash = this.topStack();
-
-      this.source.push(hash + "['" + key + "'] = " + value + ";");
-    },
-
-    // HELPERS
-
-    compiler: JavaScriptCompiler,
-
-    compileChildren: function(environment, options) {
-      var children = environment.children, child, compiler;
-
-      for(var i=0, l=children.length; i<l; i++) {
-        child = children[i];
-        compiler = new this.compiler();
-
-        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
-        var index = this.context.programs.length;
-        child.index = index;
-        child.name = 'program' + index;
-        this.context.programs[index] = compiler.compile(child, options, this.context);
-      }
-    },
-
-    programExpression: function(guid) {
-      this.context.aliases.self = "this";
-
-      if(guid == null) {
-        return "self.noop";
-      }
-
-      var child = this.environment.children[guid],
-          depths = child.depths.list, depth;
-
-      var programParams = [child.index, child.name, "data"];
-
-      for(var i=0, l = depths.length; i<l; i++) {
-        depth = depths[i];
-
-        if(depth === 1) { programParams.push("depth0"); }
-        else { programParams.push("depth" + (depth - 1)); }
-      }
-
-      if(depths.length === 0) {
-        return "self.program(" + programParams.join(", ") + ")";
-      } else {
-        programParams.shift();
-        return "self.programWithDepth(" + programParams.join(", ") + ")";
-      }
-    },
-
-    register: function(name, val) {
-      this.useRegister(name);
-      this.source.push(name + " = " + val + ";");
-    },
-
-    useRegister: function(name) {
-      if(!this.registers[name]) {
-        this.registers[name] = true;
-        this.registers.list.push(name);
-      }
-    },
-
-    pushStackLiteral: function(item) {
-      this.compileStack.push(new Literal(item));
-      return item;
-    },
-
-    pushStack: function(item) {
-      this.source.push(this.incrStack() + " = " + item + ";");
-      this.compileStack.push("stack" + this.stackSlot);
-      return "stack" + this.stackSlot;
-    },
-
-    replaceStack: function(callback) {
-      var item = callback.call(this, this.topStack());
-
-      this.source.push(this.topStack() + " = " + item + ";");
-      return "stack" + this.stackSlot;
-    },
-
-    nextStack: function(skipCompileStack) {
-      var name = this.incrStack();
-      this.compileStack.push("stack" + this.stackSlot);
-      return name;
-    },
-
-    incrStack: function() {
-      this.stackSlot++;
-      if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
-      return "stack" + this.stackSlot;
-    },
-
-    popStack: function() {
-      var item = this.compileStack.pop();
-
-      if (item instanceof Literal) {
-        return item.value;
-      } else {
-        this.stackSlot--;
-        return item;
-      }
-    },
-
-    topStack: function() {
-      var item = this.compileStack[this.compileStack.length - 1];
-
-      if (item instanceof Literal) {
-        return item.value;
-      } else {
-        return item;
-      }
-    },
-
-    quotedString: function(str) {
-      return '"' + str
-        .replace(/\\/g, '\\\\')
-        .replace(/"/g, '\\"')
-        .replace(/\n/g, '\\n')
-        .replace(/\r/g, '\\r') + '"';
-    },
-
-    setupHelper: function(paramSize, name) {
-      var params = [];
-      this.setupParams(paramSize, params);
-      var foundHelper = this.nameLookup('helpers', name, 'helper');
-
-      return {
-        params: params,
-        name: foundHelper,
-        callParams: ["depth0"].concat(params).join(", "),
-        helperMissingParams: ["depth0", this.quotedString(name)].concat(params).join(", ")
-      };
-    },
-
-    // the params and contexts arguments are passed in arrays
-    // to fill in
-    setupParams: function(paramSize, params) {
-      var options = [], contexts = [], param, inverse, program;
-
-      options.push("hash:" + this.popStack());
-
-      inverse = this.popStack();
-      program = this.popStack();
-
-      // Avoid setting fn and inverse if neither are set. This allows
-      // helpers to do a check for `if (options.fn)`
-      if (program || inverse) {
-        if (!program) {
-          this.context.aliases.self = "this";
-          program = "self.noop";
-        }
-
-        if (!inverse) {
-         this.context.aliases.self = "this";
-          inverse = "self.noop";
-        }
-
-        options.push("inverse:" + inverse);
-        options.push("fn:" + program);
-      }
-
-      for(var i=0; i<paramSize; i++) {
-        param = this.popStack();
-        params.push(param);
-
-        if(this.options.stringParams) {
-          contexts.push(this.popStack());
-        }
-      }
-
-      if (this.options.stringParams) {
-        options.push("contexts:[" + contexts.join(",") + "]");
-      }
-
-      if(this.options.data) {
-        options.push("data:data");
-      }
-
-      params.push("{" + options.join(",") + "}");
-      return params.join(", ");
-    }
-  };
-
-  var reservedWords = (
-    "break else new var" +
-    " case finally return void" +
-    " catch for switch while" +
-    " continue function this with" +
-    " default if throw" +
-    " delete in try" +
-    " do instanceof typeof" +
-    " abstract enum int short" +
-    " boolean export interface static" +
-    " byte extends long super" +
-    " char final native synchronized" +
-    " class float package throws" +
-    " const goto private transient" +
-    " debugger implements protected volatile" +
-    " double import public let yield"
-  ).split(" ");
-
-  var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
-
-  for(var i=0, l=reservedWords.length; i<l; i++) {
-    compilerWords[reservedWords[i]] = true;
-  }
-
-  JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
-    if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
-      return true;
-    }
-    return false;
-  };
-
-})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
-
-Handlebars.precompile = function(string, options) {
-  options = options || {};
-
-  var ast = Handlebars.parse(string);
-  var environment = new Handlebars.Compiler().compile(ast, options);
-  return new Handlebars.JavaScriptCompiler().compile(environment, options);
-};
-
-Handlebars.compile = function(string, options) {
-  options = options || {};
-
-  var compiled;
-  function compile() {
-    var ast = Handlebars.parse(string);
-    var environment = new Handlebars.Compiler().compile(ast, options);
-    var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
-    return Handlebars.template(templateSpec);
-  }
-
-  // Template is only compiled on first use and cached after that point.
-  return function(context, options) {
-    if (!compiled) {
-      compiled = compile();
-    }
-    return compiled.call(this, context, options);
-  };
-};
-;
-// lib/handlebars/runtime.js
-Handlebars.VM = {
-  template: function(templateSpec) {
-    // Just add water
-    var container = {
-      escapeExpression: Handlebars.Utils.escapeExpression,
-      invokePartial: Handlebars.VM.invokePartial,
-      programs: [],
-      program: function(i, fn, data) {
-        var programWrapper = this.programs[i];
-        if(data) {
-          return Handlebars.VM.program(fn, data);
-        } else if(programWrapper) {
-          return programWrapper;
-        } else {
-          programWrapper = this.programs[i] = Handlebars.VM.program(fn);
-          return programWrapper;
-        }
-      },
-      programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop
-    };
-
-    return function(context, options) {
-      options = options || {};
-      return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
-    };
-  },
-
-  programWithDepth: function(fn, data, $depth) {
-    var args = Array.prototype.slice.call(arguments, 2);
-
-    return function(context, options) {
-      options = options || {};
-
-      return fn.apply(this, [context, options.data || data].concat(args));
-    };
-  },
-  program: function(fn, data) {
-    return function(context, options) {
-      options = options || {};
-
-      return fn(context, options.data || data);
-    };
-  },
-  noop: function() { return ""; },
-  invokePartial: function(partial, name, context, helpers, partials, data) {
-    var options = { helpers: helpers, partials: partials, data: data };
-
-    if(partial === undefined) {
-      throw new Handlebars.Exception("The partial " + name + " could not be found");
-    } else if(partial instanceof Function) {
-      return partial(context, options);
-    } else if (!Handlebars.compile) {
-      throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
-    } else {
-      partials[name] = Handlebars.compile(partial, {data: data !== undefined});
-      return partials[name](context, options);
-    }
-  }
-};
-
-Handlebars.template = Handlebars.VM.template;
-;
--- a/wikked/assets/js/jsonjs/README	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-JSON in JavaScript
-
-
-Douglas Crockford
-douglas@crockford.com
-
-2010-11-18
-
-
-JSON is a light-weight, language independent, data interchange format.
-See http://www.JSON.org/
-
-The files in this collection implement JSON encoders/decoders in JavaScript.
-
-JSON became a built-in feature of JavaScript when the ECMAScript Programming
-Language Standard - Fifth Edition was adopted by the ECMA General Assembly
-in December 2009. Most of the files in this collection are for applications
-that are expected to run in obsolete web browsers. For most purposes, json2.js
-is the best choice.
-
-
-json2.js: This file creates a JSON property in the global object, if there
-isn't already one, setting its value to an object containing a stringify
-method and a parse method. The parse method uses the eval method to do the
-parsing, guarding it with several regular expressions to defend against
-accidental code execution hazards. On current browsers, this file does nothing,
-prefering the built-in JSON object.
-
-json.js: This file does everything that json2.js does. It also adds a
-toJSONString method and a parseJSON method to Object.prototype. Use of this
-file is not recommended.
-
-json_parse.js: This file contains an alternative JSON parse function that
-uses recursive descent instead of eval.
-
-json_parse_state.js: This files contains an alternative JSON parse function that
-uses a state machine instead of eval.
-
-cycle.js: This file contains two functions, JSON.decycle and JSON.retrocycle,
-which make it possible to encode cyclical structures and dags in JSON, and to
-then recover them. JSONPath is used to represent the links.
-http://GOESSNER.net/articles/JsonPath/
--- a/wikked/assets/js/jsonjs/cycle.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-/*
-    cycle.js
-    2012-08-19
-
-    Public Domain.
-
-    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
-
-    This code should be minified before deployment.
-    See http://javascript.crockford.com/jsmin.html
-
-    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-    NOT CONTROL.
-*/
-
-/*jslint evil: true, regexp: true */
-
-/*members $ref, apply, call, decycle, hasOwnProperty, length, prototype, push,
-    retrocycle, stringify, test, toString
-*/
-
-if (typeof JSON.decycle !== 'function') {
-    JSON.decycle = function decycle(object) {
-        'use strict';
-
-// Make a deep copy of an object or array, assuring that there is at most
-// one instance of each object or array in the resulting structure. The
-// duplicate references (which might be forming cycles) are replaced with
-// an object of the form
-//      {$ref: PATH}
-// where the PATH is a JSONPath string that locates the first occurance.
-// So,
-//      var a = [];
-//      a[0] = a;
-//      return JSON.stringify(JSON.decycle(a));
-// produces the string '[{"$ref":"$"}]'.
-
-// JSONPath is used to locate the unique object. $ indicates the top level of
-// the object or array. [NUMBER] or [STRING] indicates a child member or
-// property.
-
-        var objects = [],   // Keep a reference to each unique object or array
-            paths = [];     // Keep the path to each unique object or array
-
-        return (function derez(value, path) {
-
-// The derez recurses through the object, producing the deep copy.
-
-            var i,          // The loop counter
-                name,       // Property name
-                nu;         // The new object or array
-
-            switch (typeof value) {
-            case 'object':
-
-// typeof null === 'object', so get out if this value is not really an object.
-// Also get out if it is a weird builtin object.
-
-                if (value === null ||
-                        value instanceof Boolean ||
-                        value instanceof Date    ||
-                        value instanceof Number  ||
-                        value instanceof RegExp  ||
-                        value instanceof String) {
-                    return value;
-                }
-
-// If the value is an object or array, look to see if we have already
-// encountered it. If so, return a $ref/path object. This is a hard way,
-// linear search that will get slower as the number of unique objects grows.
-
-                for (i = 0; i < objects.length; i += 1) {
-                    if (objects[i] === value) {
-                        return {$ref: paths[i]};
-                    }
-                }
-
-// Otherwise, accumulate the unique value and its path.
-
-                objects.push(value);
-                paths.push(path);
-
-// If it is an array, replicate the array.
-
-                if (Object.prototype.toString.apply(value) === '[object Array]') {
-                    nu = [];
-                    for (i = 0; i < value.length; i += 1) {
-                        nu[i] = derez(value[i], path + '[' + i + ']');
-                    }
-                } else {
-
-// If it is an object, replicate the object.
-
-                    nu = {};
-                    for (name in value) {
-                        if (Object.prototype.hasOwnProperty.call(value, name)) {
-                            nu[name] = derez(value[name],
-                                path + '[' + JSON.stringify(name) + ']');
-                        }
-                    }
-                }
-                return nu;
-            case 'number':
-            case 'string':
-            case 'boolean':
-                return value;
-            }
-        }(object, '$'));
-    };
-}
-
-
-if (typeof JSON.retrocycle !== 'function') {
-    JSON.retrocycle = function retrocycle($) {
-        'use strict';
-
-// Restore an object that was reduced by decycle. Members whose values are
-// objects of the form
-//      {$ref: PATH}
-// are replaced with references to the value found by the PATH. This will
-// restore cycles. The object will be mutated.
-
-// The eval function is used to locate the values described by a PATH. The
-// root object is kept in a $ variable. A regular expression is used to
-// assure that the PATH is extremely well formed. The regexp contains nested
-// * quantifiers. That has been known to have extremely bad performance
-// problems on some browsers for very long strings. A PATH is expected to be
-// reasonably short. A PATH is allowed to belong to a very restricted subset of
-// Goessner's JSONPath.
-
-// So,
-//      var s = '[{"$ref":"$"}]';
-//      return JSON.retrocycle(JSON.parse(s));
-// produces an array containing a single element which is the array itself.
-
-        var px =
-            /^\$(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/;
-
-        (function rez(value) {
-
-// The rez function walks recursively through the object looking for $ref
-// properties. When it finds one that has a value that is a path, then it
-// replaces the $ref object with a reference to the value that is found by
-// the path.
-
-            var i, item, name, path;
-
-            if (value && typeof value === 'object') {
-                if (Object.prototype.toString.apply(value) === '[object Array]') {
-                    for (i = 0; i < value.length; i += 1) {
-                        item = value[i];
-                        if (item && typeof item === 'object') {
-                            path = item.$ref;
-                            if (typeof path === 'string' && px.test(path)) {
-                                value[i] = eval(path);
-                            } else {
-                                rez(item);
-                            }
-                        }
-                    }
-                } else {
-                    for (name in value) {
-                        if (typeof value[name] === 'object') {
-                            item = value[name];
-                            if (item) {
-                                path = item.$ref;
-                                if (typeof path === 'string' && px.test(path)) {
-                                    value[name] = eval(path);
-                                } else {
-                                    rez(item);
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }($));
-        return $;
-    };
-}
--- a/wikked/assets/js/jsonjs/json.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,529 +0,0 @@
-/*
-    json.js
-    2012-10-08
-
-    Public Domain
-
-    No warranty expressed or implied. Use at your own risk.
-
-    This file has been superceded by http://www.JSON.org/json2.js
-
-    See http://www.JSON.org/js.html
-
-    This code should be minified before deployment.
-    See http://javascript.crockford.com/jsmin.html
-
-    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-    NOT CONTROL.
-
-    This file adds these methods to JavaScript:
-
-        object.toJSONString(whitelist)
-            This method produce a JSON text from a JavaScript value.
-            It must not contain any cyclical references. Illegal values
-            will be excluded.
-
-            The default conversion for dates is to an ISO string. You can
-            add a toJSONString method to any date object to get a different
-            representation.
-
-            The object and array methods can take an optional whitelist
-            argument. A whitelist is an array of strings. If it is provided,
-            keys in objects not found in the whitelist are excluded.
-
-        string.parseJSON(filter)
-            This method parses a JSON text to produce an object or
-            array. It can throw a SyntaxError exception.
-
-            The optional filter parameter is a function which can filter and
-            transform the results. It receives each of the keys and values, and
-            its return value is used instead of the original value. If it
-            returns what it received, then structure is not modified. If it
-            returns undefined then the member is deleted.
-
-            Example:
-
-            // Parse the text. If a key contains the string 'date' then
-            // convert the value to a date.
-
-            myData = text.parseJSON(function (key, value) {
-                return key.indexOf('date') >= 0 ? new Date(value) : value;
-            });
-
-    This file will break programs with improper for..in loops. See
-    http://yuiblog.com/blog/2006/09/26/for-in-intrigue/
-
-    This file creates a global JSON object containing two methods: stringify
-    and parse.
-
-        JSON.stringify(value, replacer, space)
-            value       any JavaScript value, usually an object or array.
-
-            replacer    an optional parameter that determines how object
-                        values are stringified for objects. It can be a
-                        function or an array of strings.
-
-            space       an optional parameter that specifies the indentation
-                        of nested structures. If it is omitted, the text will
-                        be packed without extra whitespace. If it is a number,
-                        it will specify the number of spaces to indent at each
-                        level. If it is a string (such as '\t' or '&nbsp;'),
-                        it contains the characters used to indent at each level.
-
-            This method produces a JSON text from a JavaScript value.
-
-            When an object value is found, if the object contains a toJSON
-            method, its toJSON method will be called and the result will be
-            stringified. A toJSON method does not serialize: it returns the
-            value represented by the name/value pair that should be serialized,
-            or undefined if nothing should be serialized. The toJSON method
-            will be passed the key associated with the value, and this will be
-            bound to the object holding the key.
-
-            For example, this would serialize Dates as ISO strings.
-
-                Date.prototype.toJSON = function (key) {
-                    function f(n) {
-                        // Format integers to have at least two digits.
-                        return n < 10 ? '0' + n : n;
-                    }
-
-                    return this.getUTCFullYear()   + '-' +
-                         f(this.getUTCMonth() + 1) + '-' +
-                         f(this.getUTCDate())      + 'T' +
-                         f(this.getUTCHours())     + ':' +
-                         f(this.getUTCMinutes())   + ':' +
-                         f(this.getUTCSeconds())   + 'Z';
-                };
-
-            You can provide an optional replacer method. It will be passed the
-            key and value of each member, with this bound to the containing
-            object. The value that is returned from your method will be
-            serialized. If your method returns undefined, then the member will
-            be excluded from the serialization.
-
-            If the replacer parameter is an array of strings, then it will be
-            used to select the members to be serialized. It filters the results
-            such that only members with keys listed in the replacer array are
-            stringified.
-
-            Values that do not have JSON representations, such as undefined or
-            functions, will not be serialized. Such values in objects will be
-            dropped; in arrays they will be replaced with null. You can use
-            a replacer function to replace those with JSON values.
-            JSON.stringify(undefined) returns undefined.
-
-            The optional space parameter produces a stringification of the
-            value that is filled with line breaks and indentation to make it
-            easier to read.
-
-            If the space parameter is a non-empty string, then that string will
-            be used for indentation. If the space parameter is a number, then
-            the indentation will be that many spaces.
-
-            Example:
-
-            text = JSON.stringify(['e', {pluribus: 'unum'}]);
-            // text is '["e",{"pluribus":"unum"}]'
-
-
-            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
-            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
-
-            text = JSON.stringify([new Date()], function (key, value) {
-                return this[key] instanceof Date ?
-                    'Date(' + this[key] + ')' : value;
-            });
-            // text is '["Date(---current time---)"]'
-
-
-        JSON.parse(text, reviver)
-            This method parses a JSON text to produce an object or array.
-            It can throw a SyntaxError exception.
-
-            The optional reviver parameter is a function that can filter and
-            transform the results. It receives each of the keys and values,
-            and its return value is used instead of the original value.
-            If it returns what it received, then the structure is not modified.
-            If it returns undefined then the member is deleted.
-
-            Example:
-
-            // Parse the text. Values that look like ISO date strings will
-            // be converted to Date objects.
-
-            myData = JSON.parse(text, function (key, value) {
-                var a;
-                if (typeof value === 'string') {
-                    a =
-/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
-                    if (a) {
-                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
-                            +a[5], +a[6]));
-                    }
-                }
-                return value;
-            });
-
-            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
-                var d;
-                if (typeof value === 'string' &&
-                        value.slice(0, 5) === 'Date(' &&
-                        value.slice(-1) === ')') {
-                    d = new Date(value.slice(5, -1));
-                    if (d) {
-                        return d;
-                    }
-                }
-                return value;
-            });
-
-
-    This is a reference implementation. You are free to copy, modify, or
-    redistribute.
-*/
-
-/*jslint evil: true, regexp: true, unparam: true */
-
-/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
-    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
-    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
-    lastIndex, length, parse, parseJSON, prototype, push, replace, slice,
-    stringify, test, toJSON, toJSONString, toString, valueOf
-*/
-
-
-// Create a JSON object only if one does not already exist. We create the
-// methods in a closure to avoid creating global variables.
-
-if (typeof JSON !== 'object') {
-    JSON = {};
-}
-
-(function () {
-    'use strict';
-
-    function f(n) {
-        // Format integers to have at least two digits.
-        return n < 10 ? '0' + n : n;
-    }
-
-    if (typeof Date.prototype.toJSON !== 'function') {
-
-        Date.prototype.toJSON = function (key) {
-
-            return isFinite(this.valueOf()) ?
-                this.getUTCFullYear()     + '-' +
-                f(this.getUTCMonth() + 1) + '-' +
-                f(this.getUTCDate())      + 'T' +
-                f(this.getUTCHours())     + ':' +
-                f(this.getUTCMinutes())   + ':' +
-                f(this.getUTCSeconds())   + 'Z' : null;
-        };
-
-        String.prototype.toJSON      =
-            Number.prototype.toJSON  =
-            Boolean.prototype.toJSON = function (key) {
-                return this.valueOf();
-            };
-    }
-
-    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
-        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
-        gap,
-        indent,
-        meta = {    // table of character substitutions
-            '\b': '\\b',
-            '\t': '\\t',
-            '\n': '\\n',
-            '\f': '\\f',
-            '\r': '\\r',
-            '"' : '\\"',
-            '\\': '\\\\'
-        },
-        rep;
-
-
-    function quote(string) {
-
-// If the string contains no control characters, no quote characters, and no
-// backslash characters, then we can safely slap some quotes around it.
-// Otherwise we must also replace the offending characters with safe escape
-// sequences.
-
-        escapable.lastIndex = 0;
-        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
-            var c = meta[a];
-            return typeof c === 'string' ? c :
-                '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
-        }) + '"' : '"' + string + '"';
-    }
-
-
-    function str(key, holder) {
-
-// Produce a string from holder[key].
-
-        var i,          // The loop counter.
-            k,          // The member key.
-            v,          // The member value.
-            length,
-            mind = gap,
-            partial,
-            value = holder[key];
-
-// If the value has a toJSON method, call it to obtain a replacement value.
-
-        if (value && typeof value === 'object' &&
-                typeof value.toJSON === 'function') {
-            value = value.toJSON(key);
-        }
-
-// If we were called with a replacer function, then call the replacer to
-// obtain a replacement value.
-
-        if (typeof rep === 'function') {
-            value = rep.call(holder, key, value);
-        }
-
-// What happens next depends on the value's type.
-
-        switch (typeof value) {
-        case 'string':
-            return quote(value);
-
-        case 'number':
-
-// JSON numbers must be finite. Encode non-finite numbers as null.
-
-            return isFinite(value) ? String(value) : 'null';
-
-        case 'boolean':
-        case 'null':
-
-// If the value is a boolean or null, convert it to a string. Note:
-// typeof null does not produce 'null'. The case is included here in
-// the remote chance that this gets fixed someday.
-
-            return String(value);
-
-// If the type is 'object', we might be dealing with an object or an array or
-// null.
-
-        case 'object':
-
-// Due to a specification blunder in ECMAScript, typeof null is 'object',
-// so watch out for that case.
-
-            if (!value) {
-                return 'null';
-            }
-
-// Make an array to hold the partial results of stringifying this object value.
-
-            gap += indent;
-            partial = [];
-
-// Is the value an array?
-
-            if (Object.prototype.toString.apply(value) === '[object Array]') {
-
-// The value is an array. Stringify every element. Use null as a placeholder
-// for non-JSON values.
-
-                length = value.length;
-                for (i = 0; i < length; i += 1) {
-                    partial[i] = str(i, value) || 'null';
-                }
-
-// Join all of the elements together, separated with commas, and wrap them in
-// brackets.
-
-                v = partial.length === 0 ? '[]' : gap ?
-                    '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
-                    '[' + partial.join(',') + ']';
-                gap = mind;
-                return v;
-            }
-
-// If the replacer is an array, use it to select the members to be stringified.
-
-            if (rep && typeof rep === 'object') {
-                length = rep.length;
-                for (i = 0; i < length; i += 1) {
-                    k = rep[i];
-                    if (typeof k === 'string') {
-                        v = str(k, value);
-                        if (v) {
-                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
-                        }
-                    }
-                }
-            } else {
-
-// Otherwise, iterate through all of the keys in the object.
-
-                for (k in value) {
-                    if (Object.prototype.hasOwnProperty.call(value, k)) {
-                        v = str(k, value);
-                        if (v) {
-                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
-                        }
-                    }
-                }
-            }
-
-// Join all of the member texts together, separated with commas,
-// and wrap them in braces.
-
-            v = partial.length === 0 ? '{}' : gap ?
-                '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
-                '{' + partial.join(',') + '}';
-            gap = mind;
-            return v;
-        }
-    }
-
-// If the JSON object does not yet have a stringify method, give it one.
-
-    if (typeof JSON.stringify !== 'function') {
-        JSON.stringify = function (value, replacer, space) {
-
-// The stringify method takes a value and an optional replacer, and an optional
-// space parameter, and returns a JSON text. The replacer can be a function
-// that can replace values, or an array of strings that will select the keys.
-// A default replacer method can be provided. Use of the space parameter can
-// produce text that is more easily readable.
-
-            var i;
-            gap = '';
-            indent = '';
-
-// If the space parameter is a number, make an indent string containing that
-// many spaces.
-
-            if (typeof space === 'number') {
-                for (i = 0; i < space; i += 1) {
-                    indent += ' ';
-                }
-
-// If the space parameter is a string, it will be used as the indent string.
-
-            } else if (typeof space === 'string') {
-                indent = space;
-            }
-
-// If there is a replacer, it must be a function or an array.
-// Otherwise, throw an error.
-
-            rep = replacer;
-            if (replacer && typeof replacer !== 'function' &&
-                    (typeof replacer !== 'object' ||
-                    typeof replacer.length !== 'number')) {
-                throw new Error('JSON.stringify');
-            }
-
-// Make a fake root object containing our value under the key of ''.
-// Return the result of stringifying the value.
-
-            return str('', {'': value});
-        };
-    }
-
-
-// If the JSON object does not yet have a parse method, give it one.
-
-    if (typeof JSON.parse !== 'function') {
-        JSON.parse = function (text, reviver) {
-
-// The parse method takes a text and an optional reviver function, and returns
-// a JavaScript value if the text is a valid JSON text.
-
-            var j;
-
-            function walk(holder, key) {
-
-// The walk method is used to recursively walk the resulting structure so
-// that modifications can be made.
-
-                var k, v, value = holder[key];
-                if (value && typeof value === 'object') {
-                    for (k in value) {
-                        if (Object.prototype.hasOwnProperty.call(value, k)) {
-                            v = walk(value, k);
-                            if (v !== undefined) {
-                                value[k] = v;
-                            } else {
-                                delete value[k];
-                            }
-                        }
-                    }
-                }
-                return reviver.call(holder, key, value);
-            }
-
-
-// Parsing happens in four stages. In the first stage, we replace certain
-// Unicode characters with escape sequences. JavaScript handles many characters
-// incorrectly, either silently deleting them, or treating them as line endings.
-
-            text = String(text);
-            cx.lastIndex = 0;
-            if (cx.test(text)) {
-                text = text.replace(cx, function (a) {
-                    return '\\u' +
-                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
-                });
-            }
-
-// In the second stage, we run the text against regular expressions that look
-// for non-JSON patterns. We are especially concerned with '()' and 'new'
-// because they can cause invocation, and '=' because it can cause mutation.
-// But just to be safe, we want to reject all unexpected forms.
-
-// We split the second stage into 4 regexp operations in order to work around
-// crippling inefficiencies in IE's and Safari's regexp engines. First we
-// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
-// replace all simple value tokens with ']' characters. Third, we delete all
-// open brackets that follow a colon or comma or that begin the text. Finally,
-// we look to see that the remaining characters are only whitespace or ']' or
-// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
-
-            if (/^[\],:{}\s]*$/
-                    .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
-                        .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
-                        .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
-
-// In the third stage we use the eval function to compile the text into a
-// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
-// in JavaScript: it can begin a block or an object literal. We wrap the text
-// in parens to eliminate the ambiguity.
-
-                j = eval('(' + text + ')');
-
-// In the optional fourth stage, we recursively walk the new structure, passing
-// each name/value pair to a reviver function for possible transformation.
-
-                return typeof reviver === 'function' ?
-                    walk({'': j}, '') : j;
-            }
-
-// If the text is not JSON parseable, then a SyntaxError is thrown.
-
-            throw new SyntaxError('JSON.parse');
-        };
-    }
-
-// Augment the basic prototypes if they have not already been augmented.
-// These forms are obsolete. It is recommended that JSON.stringify and
-// JSON.parse be used instead.
-
-    if (!Object.prototype.toJSONString) {
-        Object.prototype.toJSONString = function (filter) {
-            return JSON.stringify(this, filter);
-        };
-        Object.prototype.parseJSON = function (filter) {
-            return JSON.parse(this, filter);
-        };
-    }
-}());
--- a/wikked/assets/js/jsonjs/json2.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,486 +0,0 @@
-/*
-    json2.js
-    2012-10-08
-
-    Public Domain.
-
-    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
-
-    See http://www.JSON.org/js.html
-
-
-    This code should be minified before deployment.
-    See http://javascript.crockford.com/jsmin.html
-
-    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-    NOT CONTROL.
-
-
-    This file creates a global JSON object containing two methods: stringify
-    and parse.
-
-        JSON.stringify(value, replacer, space)
-            value       any JavaScript value, usually an object or array.
-
-            replacer    an optional parameter that determines how object
-                        values are stringified for objects. It can be a
-                        function or an array of strings.
-
-            space       an optional parameter that specifies the indentation
-                        of nested structures. If it is omitted, the text will
-                        be packed without extra whitespace. If it is a number,
-                        it will specify the number of spaces to indent at each
-                        level. If it is a string (such as '\t' or '&nbsp;'),
-                        it contains the characters used to indent at each level.
-
-            This method produces a JSON text from a JavaScript value.
-
-            When an object value is found, if the object contains a toJSON
-            method, its toJSON method will be called and the result will be
-            stringified. A toJSON method does not serialize: it returns the
-            value represented by the name/value pair that should be serialized,
-            or undefined if nothing should be serialized. The toJSON method
-            will be passed the key associated with the value, and this will be
-            bound to the value
-
-            For example, this would serialize Dates as ISO strings.
-
-                Date.prototype.toJSON = function (key) {
-                    function f(n) {
-                        // Format integers to have at least two digits.
-                        return n < 10 ? '0' + n : n;
-                    }
-
-                    return this.getUTCFullYear()   + '-' +
-                         f(this.getUTCMonth() + 1) + '-' +
-                         f(this.getUTCDate())      + 'T' +
-                         f(this.getUTCHours())     + ':' +
-                         f(this.getUTCMinutes())   + ':' +
-                         f(this.getUTCSeconds())   + 'Z';
-                };
-
-            You can provide an optional replacer method. It will be passed the
-            key and value of each member, with this bound to the containing
-            object. The value that is returned from your method will be
-            serialized. If your method returns undefined, then the member will
-            be excluded from the serialization.
-
-            If the replacer parameter is an array of strings, then it will be
-            used to select the members to be serialized. It filters the results
-            such that only members with keys listed in the replacer array are
-            stringified.
-
-            Values that do not have JSON representations, such as undefined or
-            functions, will not be serialized. Such values in objects will be
-            dropped; in arrays they will be replaced with null. You can use
-            a replacer function to replace those with JSON values.
-            JSON.stringify(undefined) returns undefined.
-
-            The optional space parameter produces a stringification of the
-            value that is filled with line breaks and indentation to make it
-            easier to read.
-
-            If the space parameter is a non-empty string, then that string will
-            be used for indentation. If the space parameter is a number, then
-            the indentation will be that many spaces.
-
-            Example:
-
-            text = JSON.stringify(['e', {pluribus: 'unum'}]);
-            // text is '["e",{"pluribus":"unum"}]'
-
-
-            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
-            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
-
-            text = JSON.stringify([new Date()], function (key, value) {
-                return this[key] instanceof Date ?
-                    'Date(' + this[key] + ')' : value;
-            });
-            // text is '["Date(---current time---)"]'
-
-
-        JSON.parse(text, reviver)
-            This method parses a JSON text to produce an object or array.
-            It can throw a SyntaxError exception.
-
-            The optional reviver parameter is a function that can filter and
-            transform the results. It receives each of the keys and values,
-            and its return value is used instead of the original value.
-            If it returns what it received, then the structure is not modified.
-            If it returns undefined then the member is deleted.
-
-            Example:
-
-            // Parse the text. Values that look like ISO date strings will
-            // be converted to Date objects.
-
-            myData = JSON.parse(text, function (key, value) {
-                var a;
-                if (typeof value === 'string') {
-                    a =
-/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
-                    if (a) {
-                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
-                            +a[5], +a[6]));
-                    }
-                }
-                return value;
-            });
-
-            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
-                var d;
-                if (typeof value === 'string' &&
-                        value.slice(0, 5) === 'Date(' &&
-                        value.slice(-1) === ')') {
-                    d = new Date(value.slice(5, -1));
-                    if (d) {
-                        return d;
-                    }
-                }
-                return value;
-            });
-
-
-    This is a reference implementation. You are free to copy, modify, or
-    redistribute.
-*/
-
-/*jslint evil: true, regexp: true */
-
-/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
-    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
-    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
-    lastIndex, length, parse, prototype, push, replace, slice, stringify,
-    test, toJSON, toString, valueOf
-*/
-
-
-// Create a JSON object only if one does not already exist. We create the
-// methods in a closure to avoid creating global variables.
-
-if (typeof JSON !== 'object') {
-    JSON = {};
-}
-
-(function () {
-    'use strict';
-
-    function f(n) {
-        // Format integers to have at least two digits.
-        return n < 10 ? '0' + n : n;
-    }
-
-    if (typeof Date.prototype.toJSON !== 'function') {
-
-        Date.prototype.toJSON = function (key) {
-
-            return isFinite(this.valueOf())
-                ? this.getUTCFullYear()     + '-' +
-                    f(this.getUTCMonth() + 1) + '-' +
-                    f(this.getUTCDate())      + 'T' +
-                    f(this.getUTCHours())     + ':' +
-                    f(this.getUTCMinutes())   + ':' +
-                    f(this.getUTCSeconds())   + 'Z'
-                : null;
-        };
-
-        String.prototype.toJSON      =
-            Number.prototype.toJSON  =
-            Boolean.prototype.toJSON = function (key) {
-                return this.valueOf();
-            };
-    }
-
-    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
-        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
-        gap,
-        indent,
-        meta = {    // table of character substitutions
-            '\b': '\\b',
-            '\t': '\\t',
-            '\n': '\\n',
-            '\f': '\\f',
-            '\r': '\\r',
-            '"' : '\\"',
-            '\\': '\\\\'
-        },
-        rep;
-
-
-    function quote(string) {
-
-// If the string contains no control characters, no quote characters, and no
-// backslash characters, then we can safely slap some quotes around it.
-// Otherwise we must also replace the offending characters with safe escape
-// sequences.
-
-        escapable.lastIndex = 0;
-        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
-            var c = meta[a];
-            return typeof c === 'string'
-                ? c
-                : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
-        }) + '"' : '"' + string + '"';
-    }
-
-
-    function str(key, holder) {
-
-// Produce a string from holder[key].
-
-        var i,          // The loop counter.
-            k,          // The member key.
-            v,          // The member value.
-            length,
-            mind = gap,
-            partial,
-            value = holder[key];
-
-// If the value has a toJSON method, call it to obtain a replacement value.
-
-        if (value && typeof value === 'object' &&
-                typeof value.toJSON === 'function') {
-            value = value.toJSON(key);
-        }
-
-// If we were called with a replacer function, then call the replacer to
-// obtain a replacement value.
-
-        if (typeof rep === 'function') {
-            value = rep.call(holder, key, value);
-        }
-
-// What happens next depends on the value's type.
-
-        switch (typeof value) {
-        case 'string':
-            return quote(value);
-
-        case 'number':
-
-// JSON numbers must be finite. Encode non-finite numbers as null.
-
-            return isFinite(value) ? String(value) : 'null';
-
-        case 'boolean':
-        case 'null':
-
-// If the value is a boolean or null, convert it to a string. Note:
-// typeof null does not produce 'null'. The case is included here in
-// the remote chance that this gets fixed someday.
-
-            return String(value);
-
-// If the type is 'object', we might be dealing with an object or an array or
-// null.
-
-        case 'object':
-
-// Due to a specification blunder in ECMAScript, typeof null is 'object',
-// so watch out for that case.
-
-            if (!value) {
-                return 'null';
-            }
-
-// Make an array to hold the partial results of stringifying this object value.
-
-            gap += indent;
-            partial = [];
-
-// Is the value an array?
-
-            if (Object.prototype.toString.apply(value) === '[object Array]') {
-
-// The value is an array. Stringify every element. Use null as a placeholder
-// for non-JSON values.
-
-                length = value.length;
-                for (i = 0; i < length; i += 1) {
-                    partial[i] = str(i, value) || 'null';
-                }
-
-// Join all of the elements together, separated with commas, and wrap them in
-// brackets.
-
-                v = partial.length === 0
-                    ? '[]'
-                    : gap
-                    ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
-                    : '[' + partial.join(',') + ']';
-                gap = mind;
-                return v;
-            }
-
-// If the replacer is an array, use it to select the members to be stringified.
-
-            if (rep && typeof rep === 'object') {
-                length = rep.length;
-                for (i = 0; i < length; i += 1) {
-                    if (typeof rep[i] === 'string') {
-                        k = rep[i];
-                        v = str(k, value);
-                        if (v) {
-                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
-                        }
-                    }
-                }
-            } else {
-
-// Otherwise, iterate through all of the keys in the object.
-
-                for (k in value) {
-                    if (Object.prototype.hasOwnProperty.call(value, k)) {
-                        v = str(k, value);
-                        if (v) {
-                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
-                        }
-                    }
-                }
-            }
-
-// Join all of the member texts together, separated with commas,
-// and wrap them in braces.
-
-            v = partial.length === 0
-                ? '{}'
-                : gap
-                ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
-                : '{' + partial.join(',') + '}';
-            gap = mind;
-            return v;
-        }
-    }
-
-// If the JSON object does not yet have a stringify method, give it one.
-
-    if (typeof JSON.stringify !== 'function') {
-        JSON.stringify = function (value, replacer, space) {
-
-// The stringify method takes a value and an optional replacer, and an optional
-// space parameter, and returns a JSON text. The replacer can be a function
-// that can replace values, or an array of strings that will select the keys.
-// A default replacer method can be provided. Use of the space parameter can
-// produce text that is more easily readable.
-
-            var i;
-            gap = '';
-            indent = '';
-
-// If the space parameter is a number, make an indent string containing that
-// many spaces.
-
-            if (typeof space === 'number') {
-                for (i = 0; i < space; i += 1) {
-                    indent += ' ';
-                }
-
-// If the space parameter is a string, it will be used as the indent string.
-
-            } else if (typeof space === 'string') {
-                indent = space;
-            }
-
-// If there is a replacer, it must be a function or an array.
-// Otherwise, throw an error.
-
-            rep = replacer;
-            if (replacer && typeof replacer !== 'function' &&
-                    (typeof replacer !== 'object' ||
-                    typeof replacer.length !== 'number')) {
-                throw new Error('JSON.stringify');
-            }
-
-// Make a fake root object containing our value under the key of ''.
-// Return the result of stringifying the value.
-
-            return str('', {'': value});
-        };
-    }
-
-
-// If the JSON object does not yet have a parse method, give it one.
-
-    if (typeof JSON.parse !== 'function') {
-        JSON.parse = function (text, reviver) {
-
-// The parse method takes a text and an optional reviver function, and returns
-// a JavaScript value if the text is a valid JSON text.
-
-            var j;
-
-            function walk(holder, key) {
-
-// The walk method is used to recursively walk the resulting structure so
-// that modifications can be made.
-
-                var k, v, value = holder[key];
-                if (value && typeof value === 'object') {
-                    for (k in value) {
-                        if (Object.prototype.hasOwnProperty.call(value, k)) {
-                            v = walk(value, k);
-                            if (v !== undefined) {
-                                value[k] = v;
-                            } else {
-                                delete value[k];
-                            }
-                        }
-                    }
-                }
-                return reviver.call(holder, key, value);
-            }
-
-
-// Parsing happens in four stages. In the first stage, we replace certain
-// Unicode characters with escape sequences. JavaScript handles many characters
-// incorrectly, either silently deleting them, or treating them as line endings.
-
-            text = String(text);
-            cx.lastIndex = 0;
-            if (cx.test(text)) {
-                text = text.replace(cx, function (a) {
-                    return '\\u' +
-                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
-                });
-            }
-
-// In the second stage, we run the text against regular expressions that look
-// for non-JSON patterns. We are especially concerned with '()' and 'new'
-// because they can cause invocation, and '=' because it can cause mutation.
-// But just to be safe, we want to reject all unexpected forms.
-
-// We split the second stage into 4 regexp operations in order to work around
-// crippling inefficiencies in IE's and Safari's regexp engines. First we
-// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
-// replace all simple value tokens with ']' characters. Third, we delete all
-// open brackets that follow a colon or comma or that begin the text. Finally,
-// we look to see that the remaining characters are only whitespace or ']' or
-// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
-
-            if (/^[\],:{}\s]*$/
-                    .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
-                        .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
-                        .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
-
-// In the third stage we use the eval function to compile the text into a
-// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
-// in JavaScript: it can begin a block or an object literal. We wrap the text
-// in parens to eliminate the ambiguity.
-
-                j = eval('(' + text + ')');
-
-// In the optional fourth stage, we recursively walk the new structure, passing
-// each name/value pair to a reviver function for possible transformation.
-
-                return typeof reviver === 'function'
-                    ? walk({'': j}, '')
-                    : j;
-            }
-
-// If the text is not JSON parseable, then a SyntaxError is thrown.
-
-            throw new SyntaxError('JSON.parse');
-        };
-    }
-}());
--- a/wikked/assets/js/jsonjs/json_parse.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,349 +0,0 @@
-/*
-    json_parse.js
-    2012-06-20
-
-    Public Domain.
-
-    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
-
-    This file creates a json_parse function.
-
-        json_parse(text, reviver)
-            This method parses a JSON text to produce an object or array.
-            It can throw a SyntaxError exception.
-
-            The optional reviver parameter is a function that can filter and
-            transform the results. It receives each of the keys and values,
-            and its return value is used instead of the original value.
-            If it returns what it received, then the structure is not modified.
-            If it returns undefined then the member is deleted.
-
-            Example:
-
-            // Parse the text. Values that look like ISO date strings will
-            // be converted to Date objects.
-
-            myData = json_parse(text, function (key, value) {
-                var a;
-                if (typeof value === 'string') {
-                    a =
-/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
-                    if (a) {
-                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
-                            +a[5], +a[6]));
-                    }
-                }
-                return value;
-            });
-
-    This is a reference implementation. You are free to copy, modify, or
-    redistribute.
-
-    This code should be minified before deployment.
-    See http://javascript.crockford.com/jsmin.html
-
-    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-    NOT CONTROL.
-*/
-
-/*members "", "\"", "\/", "\\", at, b, call, charAt, f, fromCharCode,
-    hasOwnProperty, message, n, name, prototype, push, r, t, text
-*/
-
-var json_parse = (function () {
-    "use strict";
-
-// This is a function that can parse a JSON text, producing a JavaScript
-// data structure. It is a simple, recursive descent parser. It does not use
-// eval or regular expressions, so it can be used as a model for implementing
-// a JSON parser in other languages.
-
-// We are defining the function inside of another function to avoid creating
-// global variables.
-
-    var at,     // The index of the current character
-        ch,     // The current character
-        escapee = {
-            '"':  '"',
-            '\\': '\\',
-            '/':  '/',
-            b:    '\b',
-            f:    '\f',
-            n:    '\n',
-            r:    '\r',
-            t:    '\t'
-        },
-        text,
-
-        error = function (m) {
-
-// Call error when something is wrong.
-
-            throw {
-                name:    'SyntaxError',
-                message: m,
-                at:      at,
-                text:    text
-            };
-        },
-
-        next = function (c) {
-
-// If a c parameter is provided, verify that it matches the current character.
-
-            if (c && c !== ch) {
-                error("Expected '" + c + "' instead of '" + ch + "'");
-            }
-
-// Get the next character. When there are no more characters,
-// return the empty string.
-
-            ch = text.charAt(at);
-            at += 1;
-            return ch;
-        },
-
-        number = function () {
-
-// Parse a number value.
-
-            var number,
-                string = '';
-
-            if (ch === '-') {
-                string = '-';
-                next('-');
-            }
-            while (ch >= '0' && ch <= '9') {
-                string += ch;
-                next();
-            }
-            if (ch === '.') {
-                string += '.';
-                while (next() && ch >= '0' && ch <= '9') {
-                    string += ch;
-                }
-            }
-            if (ch === 'e' || ch === 'E') {
-                string += ch;
-                next();
-                if (ch === '-' || ch === '+') {
-                    string += ch;
-                    next();
-                }
-                while (ch >= '0' && ch <= '9') {
-                    string += ch;
-                    next();
-                }
-            }
-            number = +string;
-            if (!isFinite(number)) {
-                error("Bad number");
-            } else {
-                return number;
-            }
-        },
-
-        string = function () {
-
-// Parse a string value.
-
-            var hex,
-                i,
-                string = '',
-                uffff;
-
-// When parsing for string values, we must look for " and \ characters.
-
-            if (ch === '"') {
-                while (next()) {
-                    if (ch === '"') {
-                        next();
-                        return string;
-                    }
-                    if (ch === '\\') {
-                        next();
-                        if (ch === 'u') {
-                            uffff = 0;
-                            for (i = 0; i < 4; i += 1) {
-                                hex = parseInt(next(), 16);
-                                if (!isFinite(hex)) {
-                                    break;
-                                }
-                                uffff = uffff * 16 + hex;
-                            }
-                            string += String.fromCharCode(uffff);
-                        } else if (typeof escapee[ch] === 'string') {
-                            string += escapee[ch];
-                        } else {
-                            break;
-                        }
-                    } else {
-                        string += ch;
-                    }
-                }
-            }
-            error("Bad string");
-        },
-
-        white = function () {
-
-// Skip whitespace.
-
-            while (ch && ch <= ' ') {
-                next();
-            }
-        },
-
-        word = function () {
-
-// true, false, or null.
-
-            switch (ch) {
-            case 't':
-                next('t');
-                next('r');
-                next('u');
-                next('e');
-                return true;
-            case 'f':
-                next('f');
-                next('a');
-                next('l');
-                next('s');
-                next('e');
-                return false;
-            case 'n':
-                next('n');
-                next('u');
-                next('l');
-                next('l');
-                return null;
-            }
-            error("Unexpected '" + ch + "'");
-        },
-
-        value,  // Place holder for the value function.
-
-        array = function () {
-
-// Parse an array value.
-
-            var array = [];
-
-            if (ch === '[') {
-                next('[');
-                white();
-                if (ch === ']') {
-                    next(']');
-                    return array;   // empty array
-                }
-                while (ch) {
-                    array.push(value());
-                    white();
-                    if (ch === ']') {
-                        next(']');
-                        return array;
-                    }
-                    next(',');
-                    white();
-                }
-            }
-            error("Bad array");
-        },
-
-        object = function () {
-
-// Parse an object value.
-
-            var key,
-                object = {};
-
-            if (ch === '{') {
-                next('{');
-                white();
-                if (ch === '}') {
-                    next('}');
-                    return object;   // empty object
-                }
-                while (ch) {
-                    key = string();
-                    white();
-                    next(':');
-                    if (Object.hasOwnProperty.call(object, key)) {
-                        error('Duplicate key "' + key + '"');
-                    }
-                    object[key] = value();
-                    white();
-                    if (ch === '}') {
-                        next('}');
-                        return object;
-                    }
-                    next(',');
-                    white();
-                }
-            }
-            error("Bad object");
-        };
-
-    value = function () {
-
-// Parse a JSON value. It could be an object, an array, a string, a number,
-// or a word.
-
-        white();
-        switch (ch) {
-        case '{':
-            return object();
-        case '[':
-            return array();
-        case '"':
-            return string();
-        case '-':
-            return number();
-        default:
-            return ch >= '0' && ch <= '9' ? number() : word();
-        }
-    };
-
-// Return the json_parse function. It will have access to all of the above
-// functions and variables.
-
-    return function (source, reviver) {
-        var result;
-
-        text = source;
-        at = 0;
-        ch = ' ';
-        result = value();
-        white();
-        if (ch) {
-            error("Syntax error");
-        }
-
-// If there is a reviver function, we recursively walk the new structure,
-// passing each name/value pair to the reviver function for possible
-// transformation, starting with a temporary root object that holds the result
-// in an empty key. If there is not a reviver function, we simply return the
-// result.
-
-        return typeof reviver === 'function'
-            ? (function walk(holder, key) {
-                var k, v, value = holder[key];
-                if (value && typeof value === 'object') {
-                    for (k in value) {
-                        if (Object.prototype.hasOwnProperty.call(value, k)) {
-                            v = walk(value, k);
-                            if (v !== undefined) {
-                                value[k] = v;
-                            } else {
-                                delete value[k];
-                            }
-                        }
-                    }
-                }
-                return reviver.call(holder, key, value);
-            }({'': result}, ''))
-            : result;
-    };
-}());
--- a/wikked/assets/js/jsonjs/json_parse_state.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,397 +0,0 @@
-/*
-    json_parse_state.js
-    2012-06-01
-
-    Public Domain.
-
-    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
-
-    This file creates a json_parse function.
-
-        json_parse(text, reviver)
-            This method parses a JSON text to produce an object or array.
-            It can throw a SyntaxError exception.
-
-            The optional reviver parameter is a function that can filter and
-            transform the results. It receives each of the keys and values,
-            and its return value is used instead of the original value.
-            If it returns what it received, then the structure is not modified.
-            If it returns undefined then the member is deleted.
-
-            Example:
-
-            // Parse the text. Values that look like ISO date strings will
-            // be converted to Date objects.
-
-            myData = json_parse(text, function (key, value) {
-                var a;
-                if (typeof value === 'string') {
-                    a =
-/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
-                    if (a) {
-                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
-                            +a[5], +a[6]));
-                    }
-                }
-                return value;
-            });
-
-    This is a reference implementation. You are free to copy, modify, or
-    redistribute.
-
-    This code should be minified before deployment.
-    See http://javascript.crockford.com/jsmin.html
-
-    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-    NOT CONTROL.
-*/
-
-/*jslint regexp: false*/
-
-/*members "", "\"", ",", "\/", ":", "[", "\\", "]", acomma, avalue, b,
-    call, colon, container, exec, f, false, firstavalue, firstokey,
-    fromCharCode, go, hasOwnProperty, key, length, n, null, ocomma, okey,
-    ovalue, pop, prototype, push, r, replace, slice, state, t, test, true,
-    value, "{", "}"
-*/
-
-var json_parse = (function () {
-    "use strict";
-
-// This function creates a JSON parse function that uses a state machine rather
-// than the dangerous eval function to parse a JSON text.
-
-    var state,      // The state of the parser, one of
-                    // 'go'         The starting state
-                    // 'ok'         The final, accepting state
-                    // 'firstokey'  Ready for the first key of the object or
-                    //              the closing of an empty object
-                    // 'okey'       Ready for the next key of the object
-                    // 'colon'      Ready for the colon
-                    // 'ovalue'     Ready for the value half of a key/value pair
-                    // 'ocomma'     Ready for a comma or closing }
-                    // 'firstavalue' Ready for the first value of an array or
-                    //              an empty array
-                    // 'avalue'     Ready for the next value of an array
-                    // 'acomma'     Ready for a comma or closing ]
-        stack,      // The stack, for controlling nesting.
-        container,  // The current container object or array
-        key,        // The current key
-        value,      // The current value
-        escapes = { // Escapement translation table
-            '\\': '\\',
-            '"': '"',
-            '/': '/',
-            't': '\t',
-            'n': '\n',
-            'r': '\r',
-            'f': '\f',
-            'b': '\b'
-        },
-        string = {   // The actions for string tokens
-            go: function () {
-                state = 'ok';
-            },
-            firstokey: function () {
-                key = value;
-                state = 'colon';
-            },
-            okey: function () {
-                key = value;
-                state = 'colon';
-            },
-            ovalue: function () {
-                state = 'ocomma';
-            },
-            firstavalue: function () {
-                state = 'acomma';
-            },
-            avalue: function () {
-                state = 'acomma';
-            }
-        },
-        number = {   // The actions for number tokens
-            go: function () {
-                state = 'ok';
-            },
-            ovalue: function () {
-                state = 'ocomma';
-            },
-            firstavalue: function () {
-                state = 'acomma';
-            },
-            avalue: function () {
-                state = 'acomma';
-            }
-        },
-        action = {
-
-// The action table describes the behavior of the machine. It contains an
-// object for each token. Each object contains a method that is called when
-// a token is matched in a state. An object will lack a method for illegal
-// states.
-
-            '{': {
-                go: function () {
-                    stack.push({state: 'ok'});
-                    container = {};
-                    state = 'firstokey';
-                },
-                ovalue: function () {
-                    stack.push({container: container, state: 'ocomma', key: key});
-                    container = {};
-                    state = 'firstokey';
-                },
-                firstavalue: function () {
-                    stack.push({container: container, state: 'acomma'});
-                    container = {};
-                    state = 'firstokey';
-                },
-                avalue: function () {
-                    stack.push({container: container, state: 'acomma'});
-                    container = {};
-                    state = 'firstokey';
-                }
-            },
-            '}': {
-                firstokey: function () {
-                    var pop = stack.pop();
-                    value = container;
-                    container = pop.container;
-                    key = pop.key;
-                    state = pop.state;
-                },
-                ocomma: function () {
-                    var pop = stack.pop();
-                    container[key] = value;
-                    value = container;
-                    container = pop.container;
-                    key = pop.key;
-                    state = pop.state;
-                }
-            },
-            '[': {
-                go: function () {
-                    stack.push({state: 'ok'});
-                    container = [];
-                    state = 'firstavalue';
-                },
-                ovalue: function () {
-                    stack.push({container: container, state: 'ocomma', key: key});
-                    container = [];
-                    state = 'firstavalue';
-                },
-                firstavalue: function () {
-                    stack.push({container: container, state: 'acomma'});
-                    container = [];
-                    state = 'firstavalue';
-                },
-                avalue: function () {
-                    stack.push({container: container, state: 'acomma'});
-                    container = [];
-                    state = 'firstavalue';
-                }
-            },
-            ']': {
-                firstavalue: function () {
-                    var pop = stack.pop();
-                    value = container;
-                    container = pop.container;
-                    key = pop.key;
-                    state = pop.state;
-                },
-                acomma: function () {
-                    var pop = stack.pop();
-                    container.push(value);
-                    value = container;
-                    container = pop.container;
-                    key = pop.key;
-                    state = pop.state;
-                }
-            },
-            ':': {
-                colon: function () {
-                    if (Object.hasOwnProperty.call(container, key)) {
-                        throw new SyntaxError('Duplicate key "' + key + '"');
-                    }
-                    state = 'ovalue';
-                }
-            },
-            ',': {
-                ocomma: function () {
-                    container[key] = value;
-                    state = 'okey';
-                },
-                acomma: function () {
-                    container.push(value);
-                    state = 'avalue';
-                }
-            },
-            'true': {
-                go: function () {
-                    value = true;
-                    state = 'ok';
-                },
-                ovalue: function () {
-                    value = true;
-                    state = 'ocomma';
-                },
-                firstavalue: function () {
-                    value = true;
-                    state = 'acomma';
-                },
-                avalue: function () {
-                    value = true;
-                    state = 'acomma';
-                }
-            },
-            'false': {
-                go: function () {
-                    value = false;
-                    state = 'ok';
-                },
-                ovalue: function () {
-                    value = false;
-                    state = 'ocomma';
-                },
-                firstavalue: function () {
-                    value = false;
-                    state = 'acomma';
-                },
-                avalue: function () {
-                    value = false;
-                    state = 'acomma';
-                }
-            },
-            'null': {
-                go: function () {
-                    value = null;
-                    state = 'ok';
-                },
-                ovalue: function () {
-                    value = null;
-                    state = 'ocomma';
-                },
-                firstavalue: function () {
-                    value = null;
-                    state = 'acomma';
-                },
-                avalue: function () {
-                    value = null;
-                    state = 'acomma';
-                }
-            }
-        };
-
-    function debackslashify(text) {
-
-// Remove and replace any backslash escapement.
-
-        return text.replace(/\\(?:u(.{4})|([^u]))/g, function (a, b, c) {
-            return b ? String.fromCharCode(parseInt(b, 16)) : escapes[c];
-        });
-    }
-
-    return function (source, reviver) {
-
-// A regular expression is used to extract tokens from the JSON text.
-// The extraction process is cautious.
-
-        var r,          // The result of the exec method.
-            tx = /^[\x20\t\n\r]*(?:([,:\[\]{}]|true|false|null)|(-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)|"((?:[^\r\n\t\\\"]|\\(?:["\\\/trnfb]|u[0-9a-fA-F]{4}))*)")/;
-
-// Set the starting state.
-
-        state = 'go';
-
-// The stack records the container, key, and state for each object or array
-// that contains another object or array while processing nested structures.
-
-        stack = [];
-
-// If any error occurs, we will catch it and ultimately throw a syntax error.
-
-        try {
-
-// For each token...
-
-            for (;;) {
-                r = tx.exec(source);
-                if (!r) {
-                    break;
-                }
-
-// r is the result array from matching the tokenizing regular expression.
-//  r[0] contains everything that matched, including any initial whitespace.
-//  r[1] contains any punctuation that was matched, or true, false, or null.
-//  r[2] contains a matched number, still in string form.
-//  r[3] contains a matched string, without quotes but with escapement.
-
-                if (r[1]) {
-
-// Token: Execute the action for this state and token.
-
-                    action[r[1]][state]();
-
-                } else if (r[2]) {
-
-// Number token: Convert the number string into a number value and execute
-// the action for this state and number.
-
-                    value = +r[2];
-                    number[state]();
-                } else {
-
-// String token: Replace the escapement sequences and execute the action for
-// this state and string.
-
-                    value = debackslashify(r[3]);
-                    string[state]();
-                }
-
-// Remove the token from the string. The loop will continue as long as there
-// are tokens. This is a slow process, but it allows the use of ^ matching,
-// which assures that no illegal tokens slip through.
-
-                source = source.slice(r[0].length);
-            }
-
-// If we find a state/token combination that is illegal, then the action will
-// cause an error. We handle the error by simply changing the state.
-
-        } catch (e) {
-            state = e;
-        }
-
-// The parsing is finished. If we are not in the final 'ok' state, or if the
-// remaining source contains anything except whitespace, then we did not have
-//a well-formed JSON text.
-
-        if (state !== 'ok' || /[^\x20\t\n\r]/.test(source)) {
-            throw state instanceof SyntaxError ? state : new SyntaxError('JSON');
-        }
-
-// If there is a reviver function, we recursively walk the new structure,
-// passing each name/value pair to the reviver function for possible
-// transformation, starting with a temporary root object that holds the current
-// value in an empty key. If there is not a reviver function, we simply return
-// that value.
-
-        return typeof reviver === 'function' ? (function walk(holder, key) {
-            var k, v, value = holder[key];
-            if (value && typeof value === 'object') {
-                for (k in value) {
-                    if (Object.prototype.hasOwnProperty.call(value, k)) {
-                        v = walk(value, k);
-                        if (v !== undefined) {
-                            value[k] = v;
-                        } else {
-                            delete value[k];
-                        }
-                    }
-                }
-            }
-            return reviver.call(holder, key, value);
-        }({'': value}, '')) : value;
-    };
-}());
--- a/wikked/assets/js/less-1.3.1.min.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-//
-// LESS - Leaner CSS v1.3.1
-// http://lesscss.org
-// 
-// Copyright (c) 2009-2011, Alexis Sellier
-// Licensed under the Apache 2.0 License.
-//
-(function(e,t){function n(t){return e.less[t.split("/")[1]]}function h(){var e=document.getElementsByTagName("style");for(var t=0;t<e.length;t++)e[t].type.match(l)&&(new r.Parser({filename:document.location.href.replace(/#.*$/,""),dumpLineNumbers:r.dumpLineNumbers})).parse(e[t].innerHTML||"",function(n,r){var i=r.toCSS(),s=e[t];s.type="text/css",s.styleSheet?s.styleSheet.cssText=i:s.innerHTML=i})}function p(e,t){for(var n=0;n<r.sheets.length;n++)d(r.sheets[n],e,t,r.sheets.length-(n+1))}function d(t,n,i,s){var o=t.contents||{},a=e.location.href.replace(/[#?].*$/,""),f=t.href.replace(/\?.*$/,""),l=u&&u.getItem(f),c=u&&u.getItem(f+":timestamp"),h={css:l,timestamp:c};/^[a-z-]+:/.test(f)||(f.charAt(0)=="/"?f=e.location.protocol+"//"+e.location.host+f:f=a.slice(0,a.lastIndexOf("/")+1)+f),g(t.href,t.type,function(e,u){if(!i&&h&&u&&(new Date(u)).valueOf()===(new Date(h.timestamp)).valueOf())m(h.css,t),n(null,null,e,t,{local:!0,remaining:s});else try{o[f]=e,(new r.Parser({optimization:r.optimization,paths:[f.replace(/[\w\.-]+$/,"")],mime:t.type,filename:f,contents:o,dumpLineNumbers:r.dumpLineNumbers})).parse(e,function(r,i){if(r)return E(r,f);try{n(r,i,e,t,{local:!1,lastModified:u,remaining:s}),b(document.getElementById("less-error-message:"+v(f)))}catch(r){E(r,f)}})}catch(a){E(a,f)}},function(e,t){throw new Error("Couldn't load "+t+" ("+e+")")})}function v(e){return e.replace(/^[a-z]+:\/\/?[^\/]+/,"").replace(/^\//,"").replace(/\?.*$/,"").replace(/\.[^\.\/]+$/,"").replace(/[^\.\w-]+/g,"-").replace(/\./g,":")}function m(e,t,n){var r,i=t.href?t.href.replace(/\?.*$/,""):"",s="less:"+(t.title||v(i));if((r=document.getElementById(s))===null){r=document.createElement("style"),r.type="text/css",t.media&&(r.media=t.media),r.id=s;var o=t&&t.nextSibling||null;document.getElementsByTagName("head")[0].insertBefore(r,o)}if(r.styleSheet)try{r.styleSheet.cssText=e}catch(a){throw new Error("Couldn't reassign styleSheet.cssText.")}else(function(e){r.childNodes.length>0?r.firstChild.nodeValue!==e.nodeValue&&r.replaceChild(e,r.firstChild):r.appendChild(e)})(document.createTextNode(e));if(n&&u){w("saving "+i+" to cache.");try{u.setItem(i,e),u.setItem(i+":timestamp",n)}catch(a){w("failed to save")}}}function g(e,t,n,i){function a(t,n,r){t.status>=200&&t.status<300?n(t.responseText,t.getResponseHeader("Last-Modified")):typeof r=="function"&&r(t.status,e)}var o=y(),u=s?r.fileAsync:r.async;typeof o.overrideMimeType=="function"&&o.overrideMimeType("text/css"),o.open("GET",e,u),o.setRequestHeader("Accept",t||"text/x-less, text/css; q=0.9, */*; q=0.5"),o.send(null),s&&!r.fileAsync?o.status===0||o.status>=200&&o.status<300?n(o.responseText):i(o.status,e):u?o.onreadystatechange=function(){o.readyState==4&&a(o,n,i)}:a(o,n,i)}function y(){if(e.XMLHttpRequest)return new XMLHttpRequest;try{return new ActiveXObject("MSXML2.XMLHTTP.3.0")}catch(t){return w("browser doesn't support AJAX."),null}}function b(e){return e&&e.parentNode.removeChild(e)}function w(e){r.env=="development"&&typeof console!="undefined"&&console.log("less: "+e)}function E(e,t){var n="less-error-message:"+v(t),i='<li><label>{line}</label><pre class="{class}">{content}</pre></li>',s=document.createElement("div"),o,u,a=[],f=e.filename||t,l=f.match(/([^\/]+)$/)[1];s.id=n,s.className="less-error-message",u="<h3>"+(e.message||"There is an error in your .less file")+"</h3>"+'<p>in <a href="'+f+'">'+l+"</a> ";var c=function(e,t,n){e.extract[t]&&a.push(i.replace(/\{line\}/,parseInt(e.line)+(t-1)).replace(/\{class\}/,n).replace(/\{content\}/,e.extract[t]))};e.stack?u+="<br/>"+e.stack.split("\n").slice(1).join("<br/>"):e.extract&&(c(e,0,""),c(e,1,"line"),c(e,2,""),u+="on line "+e.line+", column "+(e.column+1)+":</p>"+"<ul>"+a.join("")+"</ul>"),s.innerHTML=u,m([".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #dd6666;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.line {","color: #ff0000;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),s.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),r.env=="development"&&(o=setInterval(function(){document.body&&(document.getElementById(n)?document.body.replaceChild(s,document.getElementById(n)):document.body.insertBefore(s,document.body.firstChild),clearInterval(o))},10))}Array.isArray||(Array.isArray=function(e){return Object.prototype.toString.call(e)==="[object Array]"||e instanceof Array}),Array.prototype.forEach||(Array.prototype.forEach=function(e,t){var n=this.length>>>0;for(var r=0;r<n;r++)r in this&&e.call(t,this[r],r,this)}),Array.prototype.map||(Array.prototype.map=function(e){var t=this.length>>>0,n=new Array(t),r=arguments[1];for(var i=0;i<t;i++)i in this&&(n[i]=e.call(r,this[i],i,this));return n}),Array.prototype.filter||(Array.prototype.filter=function(e){var t=[],n=arguments[1];for(var r=0;r<this.length;r++)e.call(n,this[r])&&t.push(this[r]);return t}),Array.prototype.reduce||(Array.prototype.reduce=function(e){var t=this.length>>>0,n=0;if(t===0&&arguments.length===1)throw new TypeError;if(arguments.length>=2)var r=arguments[1];else do{if(n in this){r=this[n++];break}if(++n>=t)throw new TypeError}while(!0);for(;n<t;n++)n in this&&(r=e.call(null,r,this[n],n,this));return r}),Array.prototype.indexOf||(Array.prototype.indexOf=function(e){var t=this.length,n=arguments[1]||0;if(!t)return-1;if(n>=t)return-1;n<0&&(n+=t);for(;n<t;n++){if(!Object.prototype.hasOwnProperty.call(this,n))continue;if(e===this[n])return n}return-1}),Object.keys||(Object.keys=function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t}),String.prototype.trim||(String.prototype.trim=function(){return String(this).replace(/^\s\s*/,"").replace(/\s\s*$/,"")});var r,i;typeof environment=="object"&&{}.toString.call(environment)==="[object Environment]"?(typeof e=="undefined"?r={}:r=e.less={},i=r.tree={},r.mode="rhino"):typeof e=="undefined"?(r=exports,i=n("./tree"),r.mode="node"):(typeof e.less=="undefined"&&(e.less={}),r=e.less,i=e.less.tree={},r.mode="browser"),r.Parser=function(t){function g(){a=c[u],f=o,h=o}function y(){c[u]=a,o=f,h=o}function b(){o>h&&(c[u]=c[u].slice(o-h),h=o)}function w(e){var t=e.charCodeAt(0);return t===32||t===10||t===9}function E(e){var t,n,r,i,a;if(e instanceof Function)return e.call(p.parsers);if(typeof e=="string")t=s.charAt(o)===e?e:null,r=1,b();else{b();if(!(t=e.exec(c[u])))return null;r=t[0].length}if(t)return S(r),typeof t=="string"?t:t.length===1?t[0]:t}function S(e){var t=o,n=u,r=o+c[u].length,i=o+=e;while(o<r){if(!w(s.charAt(o)))break;o++}return c[u]=c[u].slice(e+(o-i)),h=o,c[u].length===0&&u<c.length-1&&u++,t!==o||n!==u}function x(e,t){var n=E(e);if(!!n)return n;T(t||(typeof e=="string"?"expected '"+e+"' got '"+s.charAt(o)+"'":"unexpected token"))}function T(e,t){throw{index:o,type:t||"Syntax",message:e}}function N(e){return typeof e=="string"?s.charAt(o)===e:e.test(c[u])?!0:!1}function C(e,t){return e.filename&&t.filename&&e.filename!==t.filename?p.imports.contents[e.filename]:s}function k(e,t){for(var n=e,r=-1;n>=0&&t.charAt(n)!=="\n";n--)r++;return{line:typeof e=="number"?(t.slice(0,e).match(/\n/g)||"").length:null,column:r}}function L(e){return r.mode==="browser"||r.mode==="rhino"?e.filename:n("path").resolve(e.filename)}function A(e,t,n){return{lineNumber:k(e,t).line+1,fileName:L(n)}}function O(e,t){var n=C(e,t),r=k(e.index,n),i=r.line,s=r.column,o=n.split("\n");this.type=e.type||"Syntax",this.message=e.message,this.filename=e.filename||t.filename,this.index=e.index,this.line=typeof i=="number"?i+1:null,this.callLine=e.call&&k(e.call,n).line+1,this.callExtract=o[k(e.call,n).line],this.stack=e.stack,this.column=s,this.extract=[o[i-1],o[i],o[i+1]]}var s,o,u,a,f,l,c,h,p,d=this,t=t||{};t.contents||(t.contents={});var v=function(){},m=this.imports={paths:t&&t.paths||[],queue:[],files:{},contents:t.contents,mime:t&&t.mime,error:null,push:function(e,n){var i=this;this.queue.push(e),r.Parser.importer(e,this.paths,function(t,r){i.queue.splice(i.queue.indexOf(e),1);var s=e in i.files;i.files[e]=r,t&&!i.error&&(i.error=t),n(t,r,s),i.queue.length===0&&v(t)},t)}};return this.env=t=t||{},this.optimization="optimization"in this.env?this.env.optimization:1,this.env.filename=this.env.filename||null,p={imports:m,parse:function(e,a){var f,d,m,g,y,b,w=[],S,x=null;o=u=h=l=0,s=e.replace(/\r\n/g,"\n"),s=s.replace(/^\uFEFF/,""),c=function(e){var n=0,r=/(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g,i=/\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,o=/"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g,u=0,a,f=e[0],l;for(var c=0,h,p;c<s.length;c++){r.lastIndex=c,(a=r.exec(s))&&a.index===c&&(c+=a[0].length,f.push(a[0])),h=s.charAt(c),i.lastIndex=o.lastIndex=c,(a=o.exec(s))&&a.index===c&&(c+=a[0].length,f.push(a[0]),h=s.charAt(c)),!l&&h==="/"&&(p=s.charAt(c+1),(p==="/"||p==="*")&&(a=i.exec(s))&&a.index===c&&(c+=a[0].length,f.push(a[0]),h=s.charAt(c)));switch(h){case"{":if(!l){u++,f.push(h);break};case"}":if(!l){u--,f.push(h),e[++n]=f=[];break};case"(":if(!l){l=!0,f.push(h);break};case")":if(l){l=!1,f.push(h);break};default:f.push(h)}}return u>0&&(x=new O({index:c,type:"Parse",message:"missing closing `}`",filename:t.filename},t)),e.map(function(e){return e.join("")})}([[]]);if(x)return a(x);try{f=new i.Ruleset([],E(this.parsers.primary)),f.root=!0}catch(T){return a(new O(T,t))}f.toCSS=function(e){var s,o,u;return function(s,o){var u=[],a;s=s||{},typeof o=="object"&&!Array.isArray(o)&&(o=Object.keys(o).map(function(e){var t=o[e];return t instanceof i.Value||(t instanceof i.Expression||(t=new i.Expression([t])),t=new i.Value([t])),new i.Rule("@"+e,t,!1,0)}),u=[new i.Ruleset(null,o)]);try{var f=e.call(this,{frames:u}).toCSS([],{compress:s.compress||!1,dumpLineNumbers:t.dumpLineNumbers})}catch(l){throw new O(l,t)}if(a=p.imports.error)throw a instanceof O?a:new O(a,t);return s.yuicompress&&r.mode==="node"?n("./cssmin").compressor.cssmin(f):s.compress?f.replace(/(\s)+/g,"$1"):f}}(f.eval);if(o<s.length-1){o=l,b=s.split("\n"),y=(s.slice(0,o).match(/\n/g)||"").length+1;for(var N=o,C=-1;N>=0&&s.charAt(N)!=="\n";N--)C++;x={type:"Parse",message:"Syntax Error on line "+y,index:o,filename:t.filename,line:y,column:C,extract:[b[y-2],b[y-1],b[y]]}}this.imports.queue.length>0?v=function(e){e?a(e):a(null,f)}:a(x,f)},parsers:{primary:function(){var e,t=[];while((e=E(this.mixin.definition)||E(this.rule)||E(this.ruleset)||E(this.mixin.call)||E(this.comment)||E(this.directive))||E(/^[\s\n]+/))e&&t.push(e);return t},comment:function(){var e;if(s.charAt(o)!=="/")return;if(s.charAt(o+1)==="/")return new i.Comment(E(/^\/\/.*/),!0);if(e=E(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/))return new i.Comment(e)},entities:{quoted:function(){var e,t=o,n;s.charAt(t)==="~"&&(t++,n=!0);if(s.charAt(t)!=='"'&&s.charAt(t)!=="'")return;n&&E("~");if(e=E(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/))return new i.Quoted(e[0],e[1]||e[2],n)},keyword:function(){var e;if(e=E(/^[_A-Za-z-][_A-Za-z0-9-]*/))return i.colors.hasOwnProperty(e)?new i.Color(i.colors[e].slice(1)):new i.Keyword(e)},call:function(){var e,n,r,s,a=o;if(!(e=/^([\w-]+|%|progid:[\w\.]+)\(/.exec(c[u])))return;e=e[1],n=e.toLowerCase();if(n==="url")return null;o+=e.length;if(n==="alpha"){s=E(this.alpha);if(typeof s!="undefined")return s}E("("),r=E(this.entities.arguments);if(!E(")"))return;if(e)return new i.Call(e,r,a,t.filename)},arguments:function(){var e=[],t;while(t=E(this.entities.assignment)||E(this.expression)){e.push(t);if(!E(","))break}return e},literal:function(){return E(this.entities.ratio)||E(this.entities.dimension)||E(this.entities.color)||E(this.entities.quoted)},assignment:function(){var e,t;if((e=E(/^\w+(?=\s?=)/i))&&E("=")&&(t=E(this.entity)))return new i.Assignment(e,t)},url:function(){var e;if(s.charAt(o)!=="u"||!E(/^url\(/))return;return e=E(this.entities.quoted)||E(this.entities.variable)||E(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/)||"",x(")"),new i.URL(e.value!=null||e instanceof i.Variable?e:new i.Anonymous(e),m.paths)},variable:function(){var e,n=o;if(s.charAt(o)==="@"&&(e=E(/^@@?[\w-]+/)))return new i.Variable(e,n,t.filename)},variableCurly:function(){var e,n,r=o;if(s.charAt(o)==="@"&&(n=E(/^@\{([\w-]+)\}/)))return new i.Variable("@"+n[1],r,t.filename)},color:function(){var e;if(s.charAt(o)==="#"&&(e=E(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/)))return new i.Color(e[1])},dimension:function(){var e,t=s.charCodeAt(o);if(t>57||t<45||t===47)return;if(e=E(/^(-?\d*\.?\d+)(px|%|em|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn|dpi|dpcm|dppx|rem|vw|vh|vmin|vm|ch)?/))return new i.Dimension(e[1],e[2])},ratio:function(){var e,t=s.charCodeAt(o);if(t>57||t<48)return;if(e=E(/^(\d+\/\d+)/))return new i.Ratio(e[1])},javascript:function(){var e,t=o,n;s.charAt(t)==="~"&&(t++,n=!0);if(s.charAt(t)!=="`")return;n&&E("~");if(e=E(/^`([^`]*)`/))return new i.JavaScript(e[1],o,n)}},variable:function(){var e;if(s.charAt(o)==="@"&&(e=E(/^(@[\w-]+)\s*:/)))return e[1]},shorthand:function(){var e,t;if(!N(/^[@\w.%-]+\/[@\w.-]+/))return;g();if((e=E(this.entity))&&E("/")&&(t=E(this.entity)))return new i.Shorthand(e,t);y()},mixin:{call:function(){var e=[],n,r,u=[],a,f=o,l=s.charAt(o),c,h,p=!1;if(l!=="."&&l!=="#")return;g();while(n=E(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/))e.push(new i.Element(r,n,o)),r=E(">");if(E("(")){while(a=E(this.expression)){h=a,c=null;if(a.value.length==1){var d=a.value[0];if(d instanceof i.Variable&&E(":")){if(!(h=E(this.expression)))throw new Error("Expected value");c=d.name}}u.push({name:c,value:h});if(!E(","))break}if(!E(")"))throw new Error("Expected )")}E(this.important)&&(p=!0);if(e.length>0&&(E(";")||N("}")))return new i.mixin.Call(e,u,f,t.filename,p);y()},definition:function(){var e,t=[],n,r,u,a,f,c=!1;if(s.charAt(o)!=="."&&s.charAt(o)!=="#"||N(/^[^{]*(;|})/))return;g();if(n=E(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)){e=n[1];do{if(s.charAt(o)==="."&&E(/^\.{3}/)){c=!0;break}if(!(u=E(this.entities.variable)||E(this.entities.literal)||E(this.entities.keyword)))break;if(u instanceof i.Variable)if(E(":"))a=x(this.expression,"expected expression"),t.push({name:u.name,value:a});else{if(E(/^\.{3}/)){t.push({name:u.name,variadic:!0}),c=!0;break}t.push({name:u.name})}else t.push({value:u})}while(E(","));E(")")||(l=o,y()),E(/^when/)&&(f=x(this.conditions,"expected condition")),r=E(this.block);if(r)return new i.mixin.Definition(e,t,r,f,c);y()}}},entity:function(){return E(this.entities.literal)||E(this.entities.variable)||E(this.entities.url)||E(this.entities.call)||E(this.entities.keyword)||E(this.entities.javascript)||E(this.comment)},end:function(){return E(";")||N("}")},alpha:function(){var e;if(!E(/^\(opacity=/i))return;if(e=E(/^\d+/)||E(this.entities.variable))return x(")"),new i.Alpha(e)},element:function(){var e,t,n,r;n=E(this.combinator),e=E(/^(?:\d+\.\d+|\d+)%/)||E(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)||E("*")||E("&")||E(this.attribute)||E(/^\([^)@]+\)/)||E(/^[\.#](?=@)/)||E(this.entities.variableCurly),e||E("(")&&(r=E(this.entities.variableCurly)||E(this.entities.variable))&&E(")")&&(e=new i.Paren(r));if(e)return new i.Element(n,e,o)},combinator:function(){var e,t=s.charAt(o);if(t===">"||t==="+"||t==="~"){o++;while(s.charAt(o).match(/\s/))o++;return new i.Combinator(t)}return s.charAt(o-1).match(/\s/)?new i.Combinator(" "):new i.Combinator(null)},selector:function(){var e,t,n=[],r,u;if(E("("))return e=E(this.entity),x(")"),new i.Selector([new i.Element("",e,o)]);while(t=E(this.element)){r=s.charAt(o),n.push(t);if(r==="{"||r==="}"||r===";"||r===",")break}if(n.length>0)return new i.Selector(n)},tag:function(){return E(/^[A-Za-z][A-Za-z-]*[0-9]?/)||E("*")},attribute:function(){var e="",t,n,r;if(!E("["))return;if(t=E(/^(?:[_A-Za-z0-9-]|\\.)+/)||E(this.entities.quoted))(r=E(/^[|~*$^]?=/))&&(n=E(this.entities.quoted)||E(/^[\w-]+/))?e=[t,r,n.toCSS?n.toCSS():n].join(""):e=t;if(!E("]"))return;if(e)return"["+e+"]"},block:function(){var e;if(E("{")&&(e=E(this.primary))&&E("}"))return e},ruleset:function(){var e=[],n,r,u,a;g(),t.dumpLineNumbers&&(a=A(o,s,t));while(n=E(this.selector)){e.push(n),E(this.comment);if(!E(","))break;E(this.comment)}if(e.length>0&&(r=E(this.block))){var f=new i.Ruleset(e,r,t.strictImports);return t.dumpLineNumbers&&(f.debugInfo=a),f}l=o,y()},rule:function(){var e,t,n=s.charAt(o),r,a;g();if(n==="."||n==="#"||n==="&")return;if(e=E(this.variable)||E(this.property)){e.charAt(0)!="@"&&(a=/^([^@+\/'"*`(;{}-]*);/.exec(c[u]))?(o+=a[0].length-1,t=new i.Anonymous(a[1])):e==="font"?t=E(this.font):t=E(this.value),r=E(this.important);if(t&&E(this.end))return new i.Rule(e,t,r,f);l=o,y()}},"import":function(){var e,t,n=o;g();var r=E(/^@import(?:-(once))?\s+/);if(r&&(e=E(this.entities.quoted)||E(this.entities.url))){t=E(this.mediaFeatures);if(E(";"))return new i.Import(e,m,t,r[1]==="once",n)}y()},mediaFeature:function(){var e,t,n=[];do if(e=E(this.entities.keyword))n.push(e);else if(E("(")){t=E(this.property),e=E(this.entity);if(!E(")"))return null;if(t&&e)n.push(new i.Paren(new i.Rule(t,e,null,o,!0)));else{if(!e)return null;n.push(new i.Paren(e))}}while(e);if(n.length>0)return new i.Expression(n)},mediaFeatures:function(){var e,t=[];do if(e=E(this.mediaFeature)){t.push(e);if(!E(","))break}else if(e=E(this.entities.variable)){t.push(e);if(!E(","))break}while(e);return t.length>0?t:null},media:function(){var e,n,r,u;t.dumpLineNumbers&&(u=A(o,s,t));if(E(/^@media/)){e=E(this.mediaFeatures);if(n=E(this.block))return r=new i.Media(n,e),t.dumpLineNumbers&&(r.debugInfo=u),r}},directive:function(){var e,t,n,r,u,a,f,l,c;if(s.charAt(o)!=="@")return;if(t=E(this["import"])||E(this.media))return t;g(),e=E(/^@[a-z-]+/),f=e,e.charAt(1)=="-"&&e.indexOf("-",2)>0&&(f="@"+e.slice(e.indexOf("-",2)+1));switch(f){case"@font-face":l=!0;break;case"@viewport":case"@top-left":case"@top-left-corner":case"@top-center":case"@top-right":case"@top-right-corner":case"@bottom-left":case"@bottom-left-corner":case"@bottom-center":case"@bottom-right":case"@bottom-right-corner":case"@left-top":case"@left-middle":case"@left-bottom":case"@right-top":case"@right-middle":case"@right-bottom":l=!0;break;case"@page":case"@document":case"@supports":case"@keyframes":l=!0,c=!0}c&&(e+=" "+(E(/^[^{]+/)||"").trim());if(l){if(n=E(this.block))return new i.Directive(e,n)}else if((t=E(this.entity))&&E(";"))return new i.Directive(e,t);y()},font:function(){var e=[],t=[],n,r,s,o;while(o=E(this.shorthand)||E(this.entity))t.push(o);e.push(new i.Expression(t));if(E(","))while(o=E(this.expression)){e.push(o);if(!E(","))break}return new i.Value(e)},value:function(){var e,t=[],n;while(e=E(this.expression)){t.push(e);if(!E(","))break}if(t.length>0)return new i.Value(t)},important:function(){if(s.charAt(o)==="!")return E(/^! *important/)},sub:function(){var e;if(E("(")&&(e=E(this.expression))&&E(")"))return e},multiplication:function(){var e,t,n,r;if(e=E(this.operand)){while(!N(/^\/\*/)&&(n=E("/")||E("*"))&&(t=E(this.operand)))r=new i.Operation(n,[r||e,t]);return r||e}},addition:function(){var e,t,n,r;if(e=E(this.multiplication)){while((n=E(/^[-+]\s+/)||!w(s.charAt(o-1))&&(E("+")||E("-")))&&(t=E(this.multiplication)))r=new i.Operation(n,[r||e,t]);return r||e}},conditions:function(){var e,t,n=o,r;if(e=E(this.condition)){while(E(",")&&(t=E(this.condition)))r=new i.Condition("or",r||e,t,n);return r||e}},condition:function(){var e,t,n,r,s=o,u=!1;E(/^not/)&&(u=!0),x("(");if(e=E(this.addition)||E(this.entities.keyword)||E(this.entities.quoted))return(r=E(/^(?:>=|=<|[<=>])/))?(t=E(this.addition)||E(this.entities.keyword)||E(this.entities.quoted))?n=new i.Condition(r,e,t,s,u):T("expected expression"):n=new i.Condition("=",e,new i.Keyword("true"),s,u),x(")"),E(/^and/)?new i.Condition("and",n,E(this.condition)):n},operand:function(){var e,t=s.charAt(o+1);s.charAt(o)==="-"&&(t==="@"||t==="(")&&(e=E("-"));var n=E(this.sub)||E(this.entities.dimension)||E(this.entities.color)||E(this.entities.variable)||E(this.entities.call);return e?new i.Operation("*",[new i.Dimension(-1),n]):n},expression:function(){var e,t,n=[],r;while(e=E(this.addition)||E(this.entity))n.push(e);if(n.length>0)return new i.Expression(n)},property:function(){var e;if(e=E(/^(\*?-?[_a-z0-9-]+)\s*:/))return e[1]}}}};if(r.mode==="browser"||r.mode==="rhino")r.Parser.importer=function(e,t,n,r){!/^([a-z-]+:)?\//.test(e)&&t.length>0&&(e=t[0]+e),d({href:e,title:e,type:r.mime,contents:r.contents},function(i){i&&typeof r.errback=="function"?r.errback.call(null,e,t,n,r):n.apply(null,arguments)},!0)};(function(e){function t(t){return e.functions.hsla(t.h,t.s,t.l,t.a)}function n(t){if(t instanceof e.Dimension)return parseFloat(t.unit=="%"?t.value/100:t.value);if(typeof t=="number")return t;throw{error:"RuntimeError",message:"color functions take numbers as parameters"}}function r(e){return Math.min(1,Math.max(0,e))}e.functions={rgb:function(e,t,n){return this.rgba(e,t,n,1)},rgba:function(t,r,i,s){var o=[t,r,i].map(function(e){return n(e)}),s=n(s);return new e.Color(o,s)},hsl:function(e,t,n){return this.hsla(e,t,n,1)},hsla:function(e,t,r,i){function u(e){return e=e<0?e+1:e>1?e-1:e,e*6<1?o+(s-o)*e*6:e*2<1?s:e*3<2?o+(s-o)*(2/3-e)*6:o}e=n(e)%360/360,t=n(t),r=n(r),i=n(i);var s=r<=.5?r*(t+1):r+t-r*t,o=r*2-s;return this.rgba(u(e+1/3)*255,u(e)*255,u(e-1/3)*255,i)},hue:function(t){return new e.Dimension(Math.round(t.toHSL().h))},saturation:function(t){return new e.Dimension(Math.round(t.toHSL().s*100),"%")},lightness:function(t){return new e.Dimension(Math.round(t.toHSL().l*100),"%")},red:function(t){return new e.Dimension(t.rgb[0])},green:function(t){return new e.Dimension(t.rgb[1])},blue:function(t){return new e.Dimension(t.rgb[2])},alpha:function(t){return new e.Dimension(t.toHSL().a)},luma:function(t){return new e.Dimension(Math.round((.2126*(t.rgb[0]/255)+.7152*(t.rgb[1]/255)+.0722*(t.rgb[2]/255))*t.alpha*100),"%")},saturate:function(e,n){var i=e.toHSL();return i.s+=n.value/100,i.s=r(i.s),t(i)},desaturate:function(e,n){var i=e.toHSL();return i.s-=n.value/100,i.s=r(i.s),t(i)},lighten:function(e,n){var i=e.toHSL();return i.l+=n.value/100,i.l=r(i.l),t(i)},darken:function(e,n){var i=e.toHSL();return i.l-=n.value/100,i.l=r(i.l),t(i)},fadein:function(e,n){var i=e.toHSL();return i.a+=n.value/100,i.a=r(i.a),t(i)},fadeout:function(e,n){var i=e.toHSL();return i.a-=n.value/100,i.a=r(i.a),t(i)},fade:function(e,n){var i=e.toHSL();return i.a=n.value/100,i.a=r(i.a),t(i)},spin:function(e,n){var r=e.toHSL(),i=(r.h+n.value)%360;return r.h=i<0?360+i:i,t(r)},mix:function(t,n,r){r||(r=new e.Dimension(50));var i=r.value/100,s=i*2-1,o=t.toHSL().a-n.toHSL().a,u=((s*o==-1?s:(s+o)/(1+s*o))+1)/2,a=1-u,f=[t.rgb[0]*u+n.rgb[0]*a,t.rgb[1]*u+n.rgb[1]*a,t.rgb[2]*u+n.rgb[2]*a],l=t.alpha*i+n.alpha*(1-i);return new e.Color(f,l)},greyscale:function(t){return this.desaturate(t,new e.Dimension(100))},contrast:function(e,t,n,r){return typeof n=="undefined"&&(n=this.rgba(255,255,255,1)),typeof t=="undefined"&&(t=this.rgba(0,0,0,1)),typeof r=="undefined"?r=.43:r=r.value,(.2126*(e.rgb[0]/255)+.7152*(e.rgb[1]/255)+.0722*(e.rgb[2]/255))*e.alpha<r?n:t},e:function(t){return new e.Anonymous(t instanceof e.JavaScript?t.evaluated:t)},escape:function(t){return new e.Anonymous(encodeURI(t.value).replace(/=/g,"%3D").replace(/:/g,"%3A").replace(/#/g,"%23").replace(/;/g,"%3B").replace(/\(/g,"%28").replace(/\)/g,"%29"))},"%":function(t){var n=Array.prototype.slice.call(arguments,1),r=t.value;for(var i=0;i<n.length;i++)r=r.replace(/%[sda]/i,function(e){var t=e.match(/s/i)?n[i].value:n[i].toCSS();return e.match(/[A-Z]$/)?encodeURIComponent(t):t});return r=r.replace(/%%/g,"%"),new e.Quoted('"'+r+'"',r)},round:function(t,r){var i=typeof r=="undefined"?0:r.value;if(t instanceof e.Dimension)return new e.Dimension(n(t).toFixed(i),t.unit);if(typeof t=="number")return t.toFixed(i);throw{type:"Argument",message:"argument must be a number"}},ceil:function(e){return this._math("ceil",e)},floor:function(e){return this._math("floor",e)},_math:function(t,r){if(r instanceof e.Dimension)return new e.Dimension(Math[t](n(r)),r.unit);if(typeof r=="number")return Math[t](r);throw{type:"Argument",message:"argument must be a number"}},argb:function(t){return new e.Anonymous(t.toARGB())},percentage:function(t){return new e.Dimension(t.value*100,"%")},color:function(t){if(t instanceof e.Quoted)return new e.Color(t.value.slice(1));throw{type:"Argument",message:"argument must be a string"}},iscolor:function(t){return this._isa(t,e.Color)},isnumber:function(t){return this._isa(t,e.Dimension)},isstring:function(t){return this._isa(t,e.Quoted)},iskeyword:function(t){return this._isa(t,e.Keyword)},isurl:function(t){return this._isa(t,e.URL)},ispixel:function(t){return t instanceof e.Dimension&&t.unit==="px"?e.True:e.False},ispercentage:function(t){return t instanceof e.Dimension&&t.unit==="%"?e.True:e.False},isem:function(t){return t instanceof e.Dimension&&t.unit==="em"?e.True:e.False},_isa:function(t,n){return t instanceof n?e.True:e.False},multiply:function(e,t){var n=e.rgb[0]*t.rgb[0]/255,r=e.rgb[1]*t.rgb[1]/255,i=e.rgb[2]*t.rgb[2]/255;return this.rgb(n,r,i)},screen:function(e,t){var n=255-(255-e.rgb[0])*(255-t.rgb[0])/255,r=255-(255-e.rgb[1])*(255-t.rgb[1])/255,i=255-(255-e.rgb[2])*(255-t.rgb[2])/255;return this.rgb(n,r,i)},overlay:function(e,t){var n=e.rgb[0]<128?2*e.rgb[0]*t.rgb[0]/255:255-2*(255-e.rgb[0])*(255-t.rgb[0])/255,r=e.rgb[1]<128?2*e.rgb[1]*t.rgb[1]/255:255-2*(255-e.rgb[1])*(255-t.rgb[1])/255,i=e.rgb[2]<128?2*e.rgb[2]*t.rgb[2]/255:255-2*(255-e.rgb[2])*(255-t.rgb[2])/255;return this.rgb(n,r,i)},softlight:function(e,t){var n=t.rgb[0]*e.rgb[0]/255,r=n+e.rgb[0]*(255-(255-e.rgb[0])*(255-t.rgb[0])/255-n)/255;n=t.rgb[1]*e.rgb[1]/255;var i=n+e.rgb[1]*(255-(255-e.rgb[1])*(255-t.rgb[1])/255-n)/255;n=t.rgb[2]*e.rgb[2]/255;var s=n+e.rgb[2]*(255-(255-e.rgb[2])*(255-t.rgb[2])/255-n)/255;return this.rgb(r,i,s)},hardlight:function(e,t){var n=t.rgb[0]<128?2*t.rgb[0]*e.rgb[0]/255:255-2*(255-t.rgb[0])*(255-e.rgb[0])/255,r=t.rgb[1]<128?2*t.rgb[1]*e.rgb[1]/255:255-2*(255-t.rgb[1])*(255-e.rgb[1])/255,i=t.rgb[2]<128?2*t.rgb[2]*e.rgb[2]/255:255-2*(255-t.rgb[2])*(255-e.rgb[2])/255;return this.rgb(n,r,i)},difference:function(e,t){var n=Math.abs(e.rgb[0]-t.rgb[0]),r=Math.abs(e.rgb[1]-t.rgb[1]),i=Math.abs(e.rgb[2]-t.rgb[2]);return this.rgb(n,r,i)},exclusion:function(e,t){var n=e.rgb[0]+t.rgb[0]*(255-e.rgb[0]-e.rgb[0])/255,r=e.rgb[1]+t.rgb[1]*(255-e.rgb[1]-e.rgb[1])/255,i=e.rgb[2]+t.rgb[2]*(255-e.rgb[2]-e.rgb[2])/255;return this.rgb(n,r,i)},average:function(e,t){var n=(e.rgb[0]+t.rgb[0])/2,r=(e.rgb[1]+t.rgb[1])/2,i=(e.rgb[2]+t.rgb[2])/2;return this.rgb(n,r,i)},negation:function(e,t){var n=255-Math.abs(255-t.rgb[0]-e.rgb[0]),r=255-Math.abs(255-t.rgb[1]-e.rgb[1]),i=255-Math.abs(255-t.rgb[2]-e.rgb[2]);return this.rgb(n,r,i)},tint:function(e,t){return this.mix(this.rgb(255,255,255),e,t)},shade:function(e,t){return this.mix(this.rgb(0,0,0),e,t)}}})(n("./tree")),function(e){e.colors={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgrey:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",grey:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"}}(n("./tree")),function(e){e.Alpha=function(e){this.value=e},e.Alpha.prototype={toCSS:function(){return"alpha(opacity="+(this.value.toCSS?this.value.toCSS():this.value)+")"},eval:function(e){return this.value.eval&&(this.value=this.value.eval(e)),this}}}(n("../tree")),function(e){e.Anonymous=function(e){this.value=e.value||e},e.Anonymous.prototype={toCSS:function(){return this.value},eval:function(){return this},compare:function(e){if(!e.toCSS)return-1;var t=this.toCSS(),n=e.toCSS();return t===n?0:t<n?-1:1}}}(n("../tree")),function(e){e.Assignment=function(e,t){this.key=e,this.value=t},e.Assignment.prototype={toCSS:function(){return this.key+"="+(this.value.toCSS?this.value.toCSS():this.value)},eval:function(t){return this.value.eval?new e.Assignment(this.key,this.value.eval(t)):this}}}(n("../tree")),function(e){e.Call=function(e,t,n,r){this.name=e,this.args=t,this.index=n,this.filename=r},e.Call.prototype={eval:function(t){var n=this.args.map(function(e){return e.eval(t)});if(!(this.name in e.functions))return new e.Anonymous(this.name+"("+n.map(function(e){return e.toCSS(t)}).join(", ")+")");try{return e.functions[this.name].apply(e.functions,n)}catch(r){throw{type:r.type||"Runtime",message:"error evaluating function `"+this.name+"`"+(r.message?": "+r.message:""),index:this.index,filename:this.filename}}},toCSS:function(e){return this.eval(e).toCSS()}}}(n("../tree")),function(e){e.Color=function(e,t){Array.isArray(e)?this.rgb=e:e.length==6?this.rgb=e.match(/.{2}/g).map(function(e){return parseInt(e,16)}):this.rgb=e.split("").map(function(e){return parseInt(e+e,16)}),this.alpha=typeof t=="number"?t:1},e.Color.prototype={eval:function(){return this},toCSS:function(){return this.alpha<1?"rgba("+this.rgb.map(function(e){return Math.round(e)}).concat(this.alpha).join(", ")+")":"#"+this.rgb.map(function(e){return e=Math.round(e),e=(e>255?255:e<0?0:e).toString(16),e.length===1?"0"+e:e}).join("")},operate:function(t,n){var r=[];n instanceof e.Color||(n=n.toColor());for(var i=0;i<3;i++)r[i]=e.operate(t,this.rgb[i],n.rgb[i]);return new e.Color(r,this.alpha+n.alpha)},toHSL:function(){var e=this.rgb[0]/255,t=this.rgb[1]/255,n=this.rgb[2]/255,r=this.alpha,i=Math.max(e,t,n),s=Math.min(e,t,
-n),o,u,a=(i+s)/2,f=i-s;if(i===s)o=u=0;else{u=a>.5?f/(2-i-s):f/(i+s);switch(i){case e:o=(t-n)/f+(t<n?6:0);break;case t:o=(n-e)/f+2;break;case n:o=(e-t)/f+4}o/=6}return{h:o*360,s:u,l:a,a:r}},toARGB:function(){var e=[Math.round(this.alpha*255)].concat(this.rgb);return"#"+e.map(function(e){return e=Math.round(e),e=(e>255?255:e<0?0:e).toString(16),e.length===1?"0"+e:e}).join("")},compare:function(e){return e.rgb?e.rgb[0]===this.rgb[0]&&e.rgb[1]===this.rgb[1]&&e.rgb[2]===this.rgb[2]&&e.alpha===this.alpha?0:-1:-1}}}(n("../tree")),function(e){e.Comment=function(e,t){this.value=e,this.silent=!!t},e.Comment.prototype={toCSS:function(e){return e.compress?"":this.value},eval:function(){return this}}}(n("../tree")),function(e){e.Condition=function(e,t,n,r,i){this.op=e.trim(),this.lvalue=t,this.rvalue=n,this.index=r,this.negate=i},e.Condition.prototype.eval=function(e){var t=this.lvalue.eval(e),n=this.rvalue.eval(e),r=this.index,i,i=function(e){switch(e){case"and":return t&&n;case"or":return t||n;default:if(t.compare)i=t.compare(n);else{if(!n.compare)throw{type:"Type",message:"Unable to perform comparison",index:r};i=n.compare(t)}switch(i){case-1:return e==="<"||e==="=<";case 0:return e==="="||e===">="||e==="=<";case 1:return e===">"||e===">="}}}(this.op);return this.negate?!i:i}}(n("../tree")),function(e){e.Dimension=function(e,t){this.value=parseFloat(e),this.unit=t||null},e.Dimension.prototype={eval:function(){return this},toColor:function(){return new e.Color([this.value,this.value,this.value])},toCSS:function(){var e=this.value+this.unit;return e},operate:function(t,n){return new e.Dimension(e.operate(t,this.value,n.value),this.unit||n.unit)},compare:function(t){return t instanceof e.Dimension?t.value>this.value?-1:t.value<this.value?1:0:-1}}}(n("../tree")),function(e){e.Directive=function(t,n){this.name=t,Array.isArray(n)?(this.ruleset=new e.Ruleset([],n),this.ruleset.allowImports=!0):this.value=n},e.Directive.prototype={toCSS:function(e,t){return this.ruleset?(this.ruleset.root=!0,this.name+(t.compress?"{":" {\n  ")+this.ruleset.toCSS(e,t).trim().replace(/\n/g,"\n  ")+(t.compress?"}":"\n}\n")):this.name+" "+this.value.toCSS()+";\n"},eval:function(t){var n=this;return this.ruleset&&(t.frames.unshift(this),n=new e.Directive(this.name),n.ruleset=this.ruleset.eval(t),t.frames.shift()),n},variable:function(t){return e.Ruleset.prototype.variable.call(this.ruleset,t)},find:function(){return e.Ruleset.prototype.find.apply(this.ruleset,arguments)},rulesets:function(){return e.Ruleset.prototype.rulesets.apply(this.ruleset)}}}(n("../tree")),function(e){e.Element=function(t,n,r){this.combinator=t instanceof e.Combinator?t:new e.Combinator(t),typeof n=="string"?this.value=n.trim():n?this.value=n:this.value="",this.index=r},e.Element.prototype.eval=function(t){return new e.Element(this.combinator,this.value.eval?this.value.eval(t):this.value,this.index)},e.Element.prototype.toCSS=function(e){var t=this.value.toCSS?this.value.toCSS(e):this.value;return t==""&&this.combinator.value.charAt(0)=="&"?"":this.combinator.toCSS(e||{})+t},e.Combinator=function(e){e===" "?this.value=" ":this.value=e?e.trim():""},e.Combinator.prototype.toCSS=function(e){return{"":""," ":" ",":":" :","+":e.compress?"+":" + ","~":e.compress?"~":" ~ ",">":e.compress?">":" > "}[this.value]}}(n("../tree")),function(e){e.Expression=function(e){this.value=e},e.Expression.prototype={eval:function(t){return this.value.length>1?new e.Expression(this.value.map(function(e){return e.eval(t)})):this.value.length===1?this.value[0].eval(t):this},toCSS:function(e){return this.value.map(function(t){return t.toCSS?t.toCSS(e):""}).join(" ")}}}(n("../tree")),function(e){e.Import=function(t,n,r,i,s){var o=this;this.once=i,this.index=s,this._path=t,this.features=r&&new e.Value(r),t instanceof e.Quoted?this.path=/\.(le?|c)ss(\?.*)?$/.test(t.value)?t.value:t.value+".less":this.path=t.value.value||t.value,this.css=/css(\?.*)?$/.test(this.path),this.css||n.push(this.path,function(t,n,r){t&&(t.index=s),r&&o.once&&(o.skip=r),o.root=n||new e.Ruleset([],[])})},e.Import.prototype={toCSS:function(e){var t=this.features?" "+this.features.toCSS(e):"";return this.css?"@import "+this._path.toCSS()+t+";\n":""},eval:function(t){var n,r=this.features&&this.features.eval(t);if(this.skip)return[];if(this.css)return this;n=new e.Ruleset([],this.root.rules.slice(0));for(var i=0;i<n.rules.length;i++)n.rules[i]instanceof e.Import&&Array.prototype.splice.apply(n.rules,[i,1].concat(n.rules[i].eval(t)));return this.features?new e.Media(n.rules,this.features.value):n.rules}}}(n("../tree")),function(e){e.JavaScript=function(e,t,n){this.escaped=n,this.expression=e,this.index=t},e.JavaScript.prototype={eval:function(t){var n,r=this,i={},s=this.expression.replace(/@\{([\w-]+)\}/g,function(n,i){return e.jsify((new e.Variable("@"+i,r.index)).eval(t))});try{s=new Function("return ("+s+")")}catch(o){throw{message:"JavaScript evaluation error: `"+s+"`",index:this.index}}for(var u in t.frames[0].variables())i[u.slice(1)]={value:t.frames[0].variables()[u].value,toJS:function(){return this.value.eval(t).toCSS()}};try{n=s.call(i)}catch(o){throw{message:"JavaScript evaluation error: '"+o.name+": "+o.message+"'",index:this.index}}return typeof n=="string"?new e.Quoted('"'+n+'"',n,this.escaped,this.index):Array.isArray(n)?new e.Anonymous(n.join(", ")):new e.Anonymous(n)}}}(n("../tree")),function(e){e.Keyword=function(e){this.value=e},e.Keyword.prototype={eval:function(){return this},toCSS:function(){return this.value},compare:function(t){return t instanceof e.Keyword?t.value===this.value?0:1:-1}},e.True=new e.Keyword("true"),e.False=new e.Keyword("false")}(n("../tree")),function(e){e.Media=function(t,n){var r=this.emptySelectors();this.features=new e.Value(n),this.ruleset=new e.Ruleset(r,t),this.ruleset.allowImports=!0},e.Media.prototype={toCSS:function(e,t){var n=this.features.toCSS(t);return this.ruleset.root=e.length===0||e[0].multiMedia,"@media "+n+(t.compress?"{":" {\n  ")+this.ruleset.toCSS(e,t).trim().replace(/\n/g,"\n  ")+(t.compress?"}":"\n}\n")},eval:function(t){t.mediaBlocks||(t.mediaBlocks=[],t.mediaPath=[]);var n=t.mediaBlocks.length;t.mediaPath.push(this),t.mediaBlocks.push(this);var r=new e.Media([],[]);return this.debugInfo&&(this.ruleset.debugInfo=this.debugInfo,r.debugInfo=this.debugInfo),r.features=this.features.eval(t),t.frames.unshift(this.ruleset),r.ruleset=this.ruleset.eval(t),t.frames.shift(),t.mediaBlocks[n]=r,t.mediaPath.pop(),t.mediaPath.length===0?r.evalTop(t):r.evalNested(t)},variable:function(t){return e.Ruleset.prototype.variable.call(this.ruleset,t)},find:function(){return e.Ruleset.prototype.find.apply(this.ruleset,arguments)},rulesets:function(){return e.Ruleset.prototype.rulesets.apply(this.ruleset)},emptySelectors:function(){var t=new e.Element("","&",0);return[new e.Selector([t])]},evalTop:function(t){var n=this;if(t.mediaBlocks.length>1){var r=this.emptySelectors();n=new e.Ruleset(r,t.mediaBlocks),n.multiMedia=!0}return delete t.mediaBlocks,delete t.mediaPath,n},evalNested:function(t){var n,r,i=t.mediaPath.concat([this]);for(n=0;n<i.length;n++)r=i[n].features instanceof e.Value?i[n].features.value:i[n].features,i[n]=Array.isArray(r)?r:[r];return this.features=new e.Value(this.permute(i).map(function(t){t=t.map(function(t){return t.toCSS?t:new e.Anonymous(t)});for(n=t.length-1;n>0;n--)t.splice(n,0,new e.Anonymous("and"));return new e.Expression(t)})),new e.Ruleset([],[])},permute:function(e){if(e.length===0)return[];if(e.length===1)return e[0];var t=[],n=this.permute(e.slice(1));for(var r=0;r<n.length;r++)for(var i=0;i<e[0].length;i++)t.push([e[0][i]].concat(n[r]));return t},bubbleSelectors:function(t){this.ruleset=new e.Ruleset(t.slice(0),[this.ruleset])}}}(n("../tree")),function(e){e.mixin={},e.mixin.Call=function(t,n,r,i,s){this.selector=new e.Selector(t),this.arguments=n,this.index=r,this.filename=i,this.important=s},e.mixin.Call.prototype={eval:function(e){var t,n,r=[],i=!1;for(var s=0;s<e.frames.length;s++)if((t=e.frames[s].find(this.selector)).length>0){n=this.arguments&&this.arguments.map(function(t){return{name:t.name,value:t.value.eval(e)}});for(var o=0;o<t.length;o++)if(t[o].match(n,e))try{Array.prototype.push.apply(r,t[o].eval(e,this.arguments,this.important).rules),i=!0}catch(u){throw{message:u.message,index:this.index,filename:this.filename,stack:u.stack}}if(i)return r;throw{type:"Runtime",message:"No matching definition was found for `"+this.selector.toCSS().trim()+"("+this.arguments.map(function(e){return e.toCSS()}).join(", ")+")`",index:this.index,filename:this.filename}}throw{type:"Name",message:this.selector.toCSS().trim()+" is undefined",index:this.index,filename:this.filename}}},e.mixin.Definition=function(t,n,r,i,s){this.name=t,this.selectors=[new e.Selector([new e.Element(null,t)])],this.params=n,this.condition=i,this.variadic=s,this.arity=n.length,this.rules=r,this._lookups={},this.required=n.reduce(function(e,t){return!t.name||t.name&&!t.value?e+1:e},0),this.parent=e.Ruleset.prototype,this.frames=[]},e.mixin.Definition.prototype={toCSS:function(){return""},variable:function(e){return this.parent.variable.call(this,e)},variables:function(){return this.parent.variables.call(this)},find:function(){return this.parent.find.apply(this,arguments)},rulesets:function(){return this.parent.rulesets.apply(this)},evalParams:function(t,n){var r=new e.Ruleset(null,[]),i,s;for(var o=0,u,a;o<this.params.length;o++){s=n&&n[o];if(s&&s.name){r.rules.unshift(new e.Rule(s.name,s.value.eval(t))),n.splice(o,1),o--;continue}if(a=this.params[o].name)if(this.params[o].variadic&&n){i=[];for(var f=o;f<n.length;f++)i.push(n[f].value.eval(t));r.rules.unshift(new e.Rule(a,(new e.Expression(i)).eval(t)))}else{if(!(u=s&&s.value||this.params[o].value))throw{type:"Runtime",message:"wrong number of arguments for "+this.name+" ("+n.length+" for "+this.arity+")"};r.rules.unshift(new e.Rule(a,u.eval(t)))}}return r},eval:function(t,n,r){var i=this.evalParams(t,n),s,o=[],u,a;for(var f=0;f<Math.max(this.params.length,n&&n.length);f++)o.push(n[f]&&n[f].value||this.params[f].value);return i.rules.unshift(new e.Rule("@arguments",(new e.Expression(o)).eval(t))),u=r?this.rules.map(function(t){return new e.Rule(t.name,t.value,"!important",t.index)}):this.rules.slice(0),(new e.Ruleset(null,u)).eval({frames:[this,i].concat(this.frames,t.frames)})},match:function(e,t){var n=e&&e.length||0,r,i;if(!this.variadic){if(n<this.required)return!1;if(n>this.params.length)return!1;if(this.required>0&&n>this.params.length)return!1}if(this.condition&&!this.condition.eval({frames:[this.evalParams(t,e)].concat(t.frames)}))return!1;r=Math.min(n,this.arity);for(var s=0;s<r;s++)if(!this.params[s].name&&e[s].value.eval(t).toCSS()!=this.params[s].value.eval(t).toCSS())return!1;return!0}}}(n("../tree")),function(e){e.Operation=function(e,t){this.op=e.trim(),this.operands=t},e.Operation.prototype.eval=function(t){var n=this.operands[0].eval(t),r=this.operands[1].eval(t),i;if(n instanceof e.Dimension&&r instanceof e.Color){if(this.op!=="*"&&this.op!=="+")throw{name:"OperationError",message:"Can't substract or divide a color from a number"};i=r,r=n,n=i}return n.operate(this.op,r)},e.operate=function(e,t,n){switch(e){case"+":return t+n;case"-":return t-n;case"*":return t*n;case"/":return t/n}}}(n("../tree")),function(e){e.Paren=function(e){this.value=e},e.Paren.prototype={toCSS:function(e){return"("+this.value.toCSS(e)+")"},eval:function(t){return new e.Paren(this.value.eval(t))}}}(n("../tree")),function(e){e.Quoted=function(e,t,n,r){this.escaped=n,this.value=t||"",this.quote=e.charAt(0),this.index=r},e.Quoted.prototype={toCSS:function(){return this.escaped?this.value:this.quote+this.value+this.quote},eval:function(t){var n=this,r=this.value.replace(/`([^`]+)`/g,function(r,i){return(new e.JavaScript(i,n.index,!0)).eval(t).value}).replace(/@\{([\w-]+)\}/g,function(r,i){var s=(new e.Variable("@"+i,n.index)).eval(t);return"value"in s?s.value:s.toCSS()});return new e.Quoted(this.quote+r+this.quote,r,this.escaped,this.index)},compare:function(e){if(!e.toCSS)return-1;var t=this.toCSS(),n=e.toCSS();return t===n?0:t<n?-1:1}}}(n("../tree")),function(e){e.Ratio=function(e){this.value=e},e.Ratio.prototype={toCSS:function(e){return this.value},eval:function(){return this}}}(n("../tree")),function(e){e.Rule=function(t,n,r,i,s){this.name=t,this.value=n instanceof e.Value?n:new e.Value([n]),this.important=r?" "+r.trim():"",this.index=i,this.inline=s||!1,t.charAt(0)==="@"?this.variable=!0:this.variable=!1},e.Rule.prototype.toCSS=function(e){return this.variable?"":this.name+(e.compress?":":": ")+this.value.toCSS(e)+this.important+(this.inline?"":";")},e.Rule.prototype.eval=function(t){return new e.Rule(this.name,this.value.eval(t),this.important,this.index,this.inline)},e.Shorthand=function(e,t){this.a=e,this.b=t},e.Shorthand.prototype={toCSS:function(e){return this.a.toCSS(e)+"/"+this.b.toCSS(e)},eval:function(){return this}}}(n("../tree")),function(e){e.Ruleset=function(e,t,n){this.selectors=e,this.rules=t,this._lookups={},this.strictImports=n},e.Ruleset.prototype={eval:function(t){var n=this.selectors&&this.selectors.map(function(e){return e.eval(t)}),r=new e.Ruleset(n,this.rules.slice(0),this.strictImports),i=[];r.root=this.root,r.allowImports=this.allowImports,this.debugInfo&&(r.debugInfo=this.debugInfo),t.frames.unshift(r);if(r.root||r.allowImports||!r.strictImports){for(var s=0;s<r.rules.length;s++)r.rules[s]instanceof e.Import?i=i.concat(r.rules[s].eval(t)):i.push(r.rules[s]);r.rules=i,i=[]}for(var s=0;s<r.rules.length;s++)r.rules[s]instanceof e.mixin.Definition&&(r.rules[s].frames=t.frames.slice(0));var o=t.mediaBlocks&&t.mediaBlocks.length||0;for(var s=0;s<r.rules.length;s++)r.rules[s]instanceof e.mixin.Call?i=i.concat(r.rules[s].eval(t)):i.push(r.rules[s]);r.rules=i;for(var s=0,u;s<r.rules.length;s++)u=r.rules[s],u instanceof e.mixin.Definition||(r.rules[s]=u.eval?u.eval(t):u);t.frames.shift();if(t.mediaBlocks)for(var s=o;s<t.mediaBlocks.length;s++)t.mediaBlocks[s].bubbleSelectors(n);return r},match:function(e){return!e||e.length===0},variables:function(){return this._variables?this._variables:this._variables=this.rules.reduce(function(t,n){return n instanceof e.Rule&&n.variable===!0&&(t[n.name]=n),t},{})},variable:function(e){return this.variables()[e]},rulesets:function(){return this._rulesets?this._rulesets:this._rulesets=this.rules.filter(function(t){return t instanceof e.Ruleset||t instanceof e.mixin.Definition})},find:function(t,n){n=n||this;var r=[],i,s,o=t.toCSS();return o in this._lookups?this._lookups[o]:(this.rulesets().forEach(function(i){if(i!==n)for(var o=0;o<i.selectors.length;o++)if(s=t.match(i.selectors[o])){t.elements.length>i.selectors[o].elements.length?Array.prototype.push.apply(r,i.find(new e.Selector(t.elements.slice(1)),n)):r.push(i);break}}),this._lookups[o]=r)},toCSS:function(t,n){var r=[],i=[],s=[],o=[],u=[],a,f,l;this.root||this.joinSelectors(u,t,this.selectors);for(var c=0;c<this.rules.length;c++)l=this.rules[c],l.rules||l instanceof e.Directive||l instanceof e.Media?o.push(l.toCSS(u,n)):l instanceof e.Comment?l.silent||(this.root?o.push(l.toCSS(n)):i.push(l.toCSS(n))):l.toCSS&&!l.variable?i.push(l.toCSS(n)):l.value&&!l.variable&&i.push(l.value.toString());o=o.join("");if(this.root)r.push(i.join(n.compress?"":"\n"));else if(i.length>0){f=e.debugInfo(n,this),a=u.map(function(e){return e.map(function(e){return e.toCSS(n)}).join("").trim()}).join(n.compress?",":",\n");for(var c=i.length-1;c>=0;c--)s.indexOf(i[c])===-1&&s.unshift(i[c]);i=s,r.push(f+a+(n.compress?"{":" {\n  ")+i.join(n.compress?"":"\n  ")+(n.compress?"}":"\n}\n"))}return r.push(o),r.join("")+(n.compress?"\n":"")},joinSelectors:function(e,t,n){for(var r=0;r<n.length;r++)this.joinSelector(e,t,n[r])},joinSelector:function(t,n,r){var i,s,o,u,a,f,l,c,h,p,d,v,m,g,y;for(i=0;i<r.elements.length;i++)f=r.elements[i],f.value==="&"&&(u=!0);if(!u){if(n.length>0)for(i=0;i<n.length;i++)t.push(n[i].concat(r));else t.push([r]);return}g=[],a=[[]];for(i=0;i<r.elements.length;i++){f=r.elements[i];if(f.value!=="&")g.push(f);else{y=[],g.length>0&&this.mergeElementsOnToSelectors(g,a);for(s=0;s<a.length;s++){l=a[s];if(n.length==0)l.length>0&&(l[0].elements=l[0].elements.slice(0),l[0].elements.push(new e.Element(f.combinator,"",0))),y.push(l);else for(o=0;o<n.length;o++)c=n[o],h=[],p=[],v=!0,l.length>0?(h=l.slice(0),m=h.pop(),d=new e.Selector(m.elements.slice(0)),v=!1):d=new e.Selector([]),c.length>1&&(p=p.concat(c.slice(1))),c.length>0&&(v=!1,d.elements.push(new e.Element(f.combinator,c[0].elements[0].value,0)),d.elements=d.elements.concat(c[0].elements.slice(1))),v||h.push(d),h=h.concat(p),y.push(h)}a=y,g=[]}}g.length>0&&this.mergeElementsOnToSelectors(g,a);for(i=0;i<a.length;i++)t.push(a[i])},mergeElementsOnToSelectors:function(t,n){var r,i;if(n.length==0){n.push([new e.Selector(t)]);return}for(r=0;r<n.length;r++)i=n[r],i.length>0?i[i.length-1]=new e.Selector(i[i.length-1].elements.concat(t)):i.push(new e.Selector(t))}}}(n("../tree")),function(e){e.Selector=function(e){this.elements=e},e.Selector.prototype.match=function(e){var t=this.elements.length,n=e.elements.length,r=Math.min(t,n);if(t<n)return!1;for(var i=0;i<r;i++)if(this.elements[i].value!==e.elements[i].value)return!1;return!0},e.Selector.prototype.eval=function(t){return new e.Selector(this.elements.map(function(e){return e.eval(t)}))},e.Selector.prototype.toCSS=function(e){return this._css?this._css:(this.elements[0].combinator.value===""?this._css=" ":this._css="",this._css+=this.elements.map(function(t){return typeof t=="string"?" "+t.trim():t.toCSS(e)}).join(""),this._css)}}(n("../tree")),function(t){t.URL=function(e,t){this.value=e,this.paths=t},t.URL.prototype={toCSS:function(){return"url("+this.value.toCSS()+")"},eval:function(n){var r=this.value.eval(n);return typeof e!="undefined"&&typeof r.value=="string"&&!/^(?:[a-z-]+:|\/)/.test(r.value)&&this.paths.length>0&&(r.value=this.paths[0]+(r.value.charAt(0)==="/"?r.value.slice(1):r.value)),new t.URL(r,this.paths)}}}(n("../tree")),function(e){e.Value=function(e){this.value=e,this.is="value"},e.Value.prototype={eval:function(t){return this.value.length===1?this.value[0].eval(t):new e.Value(this.value.map(function(e){return e.eval(t)}))},toCSS:function(e){return this.value.map(function(t){return t.toCSS(e)}).join(e.compress?",":", ")}}}(n("../tree")),function(e){e.Variable=function(e,t,n){this.name=e,this.index=t,this.file=n},e.Variable.prototype={eval:function(t){var n,r,i=this.name;i.indexOf("@@")==0&&(i="@"+(new e.Variable(i.slice(1))).eval(t).value);if(n=e.find(t.frames,function(e){if(r=e.variable(i))return r.value.eval(t)}))return n;throw{type:"Name",message:"variable "+i+" is undefined",filename:this.file,index:this.index}}}}(n("../tree")),function(e){e.debugInfo=function(t,n){var r="";if(t.dumpLineNumbers&&!t.compress)switch(t.dumpLineNumbers){case"comments":r=e.debugInfo.asComment(n);break;case"mediaquery":r=e.debugInfo.asMediaQuery(n);break;case"all":r=e.debugInfo.asComment(n)+e.debugInfo.asMediaQuery(n)}return r},e.debugInfo.asComment=function(e){return"/* line "+e.debugInfo.lineNumber+", "+e.debugInfo.fileName+" */\n"},e.debugInfo.asMediaQuery=function(e){return'@media -sass-debug-info{filename{font-family:"'+e.debugInfo.fileName+'";}line{font-family:"'+e.debugInfo.lineNumber+'";}}\n'},e.find=function(e,t){for(var n=0,r;n<e.length;n++)if(r=t.call(e,e[n]))return r;return null},e.jsify=function(e){return Array.isArray(e.value)&&e.value.length>1?"["+e.value.map(function(e){return e.toCSS(!1)}).join(", ")+"]":e.toCSS(!1)}}(n("./tree"));var s=/^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);r.env=r.env||(location.hostname=="127.0.0.1"||location.hostname=="0.0.0.0"||location.hostname=="localhost"||location.port.length>0||s?"development":"production"),r.async=r.async||!1,r.fileAsync=r.fileAsync||!1,r.poll=r.poll||(s?1e3:1500),r.watch=function(){return this.watchMode=!0},r.unwatch=function(){return this.watchMode=!1};if(r.env==="development"){r.optimization=0,/!watch/.test(location.hash)&&r.watch();var o=/!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);o&&(r.dumpLineNumbers=o[1]),r.watchTimer=setInterval(function(){r.watchMode&&p(function(e,t,n,r,i){t&&m(t.toCSS(),r,i.lastModified)})},r.poll)}else r.optimization=3;var u;try{u=typeof e.localStorage=="undefined"?null:e.localStorage}catch(a){u=null}var f=document.getElementsByTagName("link"),l=/^text\/(x-)?less$/;r.sheets=[];for(var c=0;c<f.length;c++)(f[c].rel==="stylesheet/less"||f[c].rel.match(/stylesheet/)&&f[c].type.match(l))&&r.sheets.push(f[c]);r.refresh=function(e){var t,n;t=n=new Date,p(function(e,r,i,s,o){o.local?w("loading "+s.href+" from cache."):(w("parsed "+s.href+" successfully."),m(r.toCSS(),s,o.lastModified)),w("css for "+s.href+" generated in "+(new Date-n)+"ms"),o.remaining===0&&w("css generated in "+(new Date-t)+"ms"),n=new Date},e),h()},r.refreshStyles=h,r.refresh(r.env==="development"),typeof define=="function"&&define.amd&&define("less",[],function(){return r})})(window);
\ No newline at end of file
--- a/wikked/assets/js/moment.min.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-// moment.js
-// version : 2.0.0
-// author : Tim Wood
-// license : MIT
-// momentjs.com
-(function(e){function O(e,t){return function(n){return j(e.call(this,n),t)}}function M(e){return function(t){return this.lang().ordinal(e.call(this,t))}}function _(){}function D(e){H(this,e)}function P(e){var t=this._data={},n=e.years||e.year||e.y||0,r=e.months||e.month||e.M||0,i=e.weeks||e.week||e.w||0,s=e.days||e.day||e.d||0,o=e.hours||e.hour||e.h||0,u=e.minutes||e.minute||e.m||0,a=e.seconds||e.second||e.s||0,f=e.milliseconds||e.millisecond||e.ms||0;this._milliseconds=f+a*1e3+u*6e4+o*36e5,this._days=s+i*7,this._months=r+n*12,t.milliseconds=f%1e3,a+=B(f/1e3),t.seconds=a%60,u+=B(a/60),t.minutes=u%60,o+=B(u/60),t.hours=o%24,s+=B(o/24),s+=i*7,t.days=s%30,r+=B(s/30),t.months=r%12,n+=B(r/12),t.years=n}function H(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function B(e){return e<0?Math.ceil(e):Math.floor(e)}function j(e,t){var n=e+"";while(n.length<t)n="0"+n;return n}function F(e,t,n){var r=t._milliseconds,i=t._days,s=t._months,o;r&&e._d.setTime(+e+r*n),i&&e.date(e.date()+i*n),s&&(o=e.date(),e.date(1).month(e.month()+s*n).date(Math.min(o,e.daysInMonth())))}function I(e){return Object.prototype.toString.call(e)==="[object Array]"}function q(e,t){var n=Math.min(e.length,t.length),r=Math.abs(e.length-t.length),i=0,s;for(s=0;s<n;s++)~~e[s]!==~~t[s]&&i++;return i+r}function R(e,t){return t.abbr=e,s[e]||(s[e]=new _),s[e].set(t),s[e]}function U(e){return e?(!s[e]&&o&&require("./lang/"+e),s[e]):t.fn._lang}function z(e){return e.match(/\[.*\]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"")}function W(e){var t=e.match(a),n,r;for(n=0,r=t.length;n<r;n++)A[t[n]]?t[n]=A[t[n]]:t[n]=z(t[n]);return function(i){var s="";for(n=0;n<r;n++)s+=typeof t[n].call=="function"?t[n].call(i,e):t[n];return s}}function X(e,t){function r(t){return e.lang().longDateFormat(t)||t}var n=5;while(n--&&f.test(t))t=t.replace(f,r);return C[t]||(C[t]=W(t)),C[t](e)}function V(e){switch(e){case"DDDD":return p;case"YYYY":return d;case"YYYYY":return v;case"S":case"SS":case"SSS":case"DDD":return h;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return m;case"X":return b;case"Z":case"ZZ":return g;case"T":return y;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return c;default:return new RegExp(e.replace("\\",""))}}function $(e,t,n){var r,i,s=n._a;switch(e){case"M":case"MM":s[1]=t==null?0:~~t-1;break;case"MMM":case"MMMM":r=U(n._l).monthsParse(t),r!=null?s[1]=r:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":t!=null&&(s[2]=~~t);break;case"YY":s[0]=~~t+(~~t>68?1900:2e3);break;case"YYYY":case"YYYYY":s[0]=~~t;break;case"a":case"A":n._isPm=(t+"").toLowerCase()==="pm";break;case"H":case"HH":case"h":case"hh":s[3]=~~t;break;case"m":case"mm":s[4]=~~t;break;case"s":case"ss":s[5]=~~t;break;case"S":case"SS":case"SSS":s[6]=~~(("0."+t)*1e3);break;case"X":n._d=new Date(parseFloat(t)*1e3);break;case"Z":case"ZZ":n._useUTC=!0,r=(t+"").match(x),r&&r[1]&&(n._tzh=~~r[1]),r&&r[2]&&(n._tzm=~~r[2]),r&&r[0]==="+"&&(n._tzh=-n._tzh,n._tzm=-n._tzm)}t==null&&(n._isValid=!1)}function J(e){var t,n,r=[];if(e._d)return;for(t=0;t<7;t++)e._a[t]=r[t]=e._a[t]==null?t===2?1:0:e._a[t];r[3]+=e._tzh||0,r[4]+=e._tzm||0,n=new Date(0),e._useUTC?(n.setUTCFullYear(r[0],r[1],r[2]),n.setUTCHours(r[3],r[4],r[5],r[6])):(n.setFullYear(r[0],r[1],r[2]),n.setHours(r[3],r[4],r[5],r[6])),e._d=n}function K(e){var t=e._f.match(a),n=e._i,r,i;e._a=[];for(r=0;r<t.length;r++)i=(V(t[r]).exec(n)||[])[0],i&&(n=n.slice(n.indexOf(i)+i.length)),A[t[r]]&&$(t[r],i,e);e._isPm&&e._a[3]<12&&(e._a[3]+=12),e._isPm===!1&&e._a[3]===12&&(e._a[3]=0),J(e)}function Q(e){var t,n,r,i=99,s,o,u;while(e._f.length){t=H({},e),t._f=e._f.pop(),K(t),n=new D(t);if(n.isValid()){r=n;break}u=q(t._a,n.toArray()),u<i&&(i=u,r=n)}H(e,r)}function G(e){var t,n=e._i;if(w.exec(n)){e._f="YYYY-MM-DDT";for(t=0;t<4;t++)if(S[t][1].exec(n)){e._f+=S[t][0];break}g.exec(n)&&(e._f+=" Z"),K(e)}else e._d=new Date(n)}function Y(t){var n=t._i,r=u.exec(n);n===e?t._d=new Date:r?t._d=new Date(+r[1]):typeof n=="string"?G(t):I(n)?(t._a=n.slice(0),J(t)):t._d=n instanceof Date?new Date(+n):new Date(n)}function Z(e,t,n,r,i){return i.relativeTime(t||1,!!n,e,r)}function et(e,t,n){var i=r(Math.abs(e)/1e3),s=r(i/60),o=r(s/60),u=r(o/24),a=r(u/365),f=i<45&&["s",i]||s===1&&["m"]||s<45&&["mm",s]||o===1&&["h"]||o<22&&["hh",o]||u===1&&["d"]||u<=25&&["dd",u]||u<=45&&["M"]||u<345&&["MM",r(u/30)]||a===1&&["y"]||["yy",a];return f[2]=t,f[3]=e>0,f[4]=n,Z.apply({},f)}function tt(e,n,r){var i=r-n,s=r-e.day();return s>i&&(s-=7),s<i-7&&(s+=7),Math.ceil(t(e).add("d",s).dayOfYear()/7)}function nt(e){var n=e._i,r=e._f;return n===null||n===""?null:(typeof n=="string"&&(e._i=n=U().preparse(n)),t.isMoment(n)?(e=H({},n),e._d=new Date(+n._d)):r?I(r)?Q(e):K(e):Y(e),new D(e))}function rt(e,n){t.fn[e]=t.fn[e+"s"]=function(e){var t=this._isUTC?"UTC":"";return e!=null?(this._d["set"+t+n](e),this):this._d["get"+t+n]()}}function it(e){t.duration.fn[e]=function(){return this._data[e]}}function st(e,n){t.duration.fn["as"+e]=function(){return+this/n}}var t,n="2.0.0",r=Math.round,i,s={},o=typeof module!="undefined"&&module.exports,u=/^\/?Date\((\-?\d+)/i,a=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,f=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,l=/([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,c=/\d\d?/,h=/\d{1,3}/,p=/\d{3}/,d=/\d{1,4}/,v=/[+\-]?\d{1,6}/,m=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,g=/Z|[\+\-]\d\d:?\d\d/i,y=/T/i,b=/[\+\-]?\d+(\.\d{1,3})?/,w=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,E="YYYY-MM-DDTHH:mm:ssZ",S=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],x=/([\+\-]|\d\d)/gi,T="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),N={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},C={},k="DDD w W M D d".split(" "),L="M D H h m s w W".split(" "),A={M:function(){return this.month()+1},MMM:function(e){return this.lang().monthsShort(this,e)},MMMM:function(e){return this.lang().months(this,e)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(e){return this.lang().weekdaysMin(this,e)},ddd:function(e){return this.lang().weekdaysShort(this,e)},dddd:function(e){return this.lang().weekdays(this,e)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return j(this.year()%100,2)},YYYY:function(){return j(this.year(),4)},YYYYY:function(){return j(this.year(),5)},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return j(~~(this.milliseconds()/10),2)},SSS:function(){return j(this.milliseconds(),3)},Z:function(){var e=-this.zone(),t="+";return e<0&&(e=-e,t="-"),t+j(~~(e/60),2)+":"+j(~~e%60,2)},ZZ:function(){var e=-this.zone(),t="+";return e<0&&(e=-e,t="-"),t+j(~~(10*e/6),4)},X:function(){return this.unix()}};while(k.length)i=k.pop(),A[i+"o"]=M(A[i]);while(L.length)i=L.pop(),A[i+i]=O(A[i],2);A.DDDD=O(A.DDD,3),_.prototype={set:function(e){var t,n;for(n in e)t=e[n],typeof t=="function"?this[n]=t:this["_"+n]=t},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(e){return this._months[e.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(e){return this._monthsShort[e.month()]},monthsParse:function(e){var n,r,i,s;this._monthsParse||(this._monthsParse=[]);for(n=0;n<12;n++){this._monthsParse[n]||(r=t([2e3,n]),i="^"+this.months(r,"")+"|^"+this.monthsShort(r,""),this._monthsParse[n]=new RegExp(i.replace(".",""),"i"));if(this._monthsParse[n].test(e))return n}},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(e){return this._weekdays[e.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(e){return this._weekdaysShort[e.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(e){return this._weekdaysMin[e.day()]},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(e){var t=this._longDateFormat[e];return!t&&this._longDateFormat[e.toUpperCase()]&&(t=this._longDateFormat[e.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(e){return e.slice(1)}),this._longDateFormat[e]=t),t},meridiem:function(e,t,n){return e>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[last] dddd [at] LT",sameElse:"L"},calendar:function(e,t){var n=this._calendar[e];return typeof n=="function"?n.apply(t):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(e,t,n,r){var i=this._relativeTime[n];return typeof i=="function"?i(e,t,n,r):i.replace(/%d/i,e)},pastFuture:function(e,t){var n=this._relativeTime[e>0?"future":"past"];return typeof n=="function"?n(t):n.replace(/%s/i,t)},ordinal:function(e){return this._ordinal.replace("%d",e)},_ordinal:"%d",preparse:function(e){return e},postformat:function(e){return e},week:function(e){return tt(e,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},t=function(e,t,n){return nt({_i:e,_f:t,_l:n,_isUTC:!1})},t.utc=function(e,t,n){return nt({_useUTC:!0,_isUTC:!0,_l:n,_i:e,_f:t})},t.unix=function(e){return t(e*1e3)},t.duration=function(e,n){var r=t.isDuration(e),i=typeof e=="number",s=r?e._data:i?{}:e,o;return i&&(n?s[n]=e:s.milliseconds=e),o=new P(s),r&&e.hasOwnProperty("_lang")&&(o._lang=e._lang),o},t.version=n,t.defaultFormat=E,t.lang=function(e,n){var r;if(!e)return t.fn._lang._abbr;n?R(e,n):s[e]||U(e),t.duration.fn._lang=t.fn._lang=U(e)},t.langData=function(e){return e&&e._lang&&e._lang._abbr&&(e=e._lang._abbr),U(e)},t.isMoment=function(e){return e instanceof D},t.isDuration=function(e){return e instanceof P},t.fn=D.prototype={clone:function(){return t(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return t.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var e=this;return[e.year(),e.month(),e.date(),e.hours(),e.minutes(),e.seconds(),e.milliseconds()]},isValid:function(){return this._isValid==null&&(this._a?this._isValid=!q(this._a,(this._isUTC?t.utc(this._a):t(this._a)).toArray()):this._isValid=!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(e){var n=X(this,e||t.defaultFormat);return this.lang().postformat(n)},add:function(e,n){var r;return typeof e=="string"?r=t.duration(+n,e):r=t.duration(e,n),F(this,r,1),this},subtract:function(e,n){var r;return typeof e=="string"?r=t.duration(+n,e):r=t.duration(e,n),F(this,r,-1),this},diff:function(e,n,r){var i=this._isUTC?t(e).utc():t(e).local(),s=(this.zone()-i.zone())*6e4,o,u;return n&&(n=n.replace(/s$/,"")),n==="year"||n==="month"?(o=(this.daysInMonth()+i.daysInMonth())*432e5,u=(this.year()-i.year())*12+(this.month()-i.month()),u+=(this-t(this).startOf("month")-(i-t(i).startOf("month")))/o,n==="year"&&(u/=12)):(o=this-i-s,u=n==="second"?o/1e3:n==="minute"?o/6e4:n==="hour"?o/36e5:n==="day"?o/864e5:n==="week"?o/6048e5:o),r?u:B(u)},from:function(e,n){return t.duration(this.diff(e)).lang(this.lang()._abbr).humanize(!n)},fromNow:function(e){return this.from(t(),e)},calendar:function(){var e=this.diff(t().startOf("day"),"days",!0),n=e<-6?"sameElse":e<-1?"lastWeek":e<0?"lastDay":e<1?"sameDay":e<2?"nextDay":e<7?"nextWeek":"sameElse";return this.format(this.lang().calendar(n,this))},isLeapYear:function(){var e=this.year();return e%4===0&&e%100!==0||e%400===0},isDST:function(){return this.zone()<t([this.year()]).zone()||this.zone()<t([this.year(),5]).zone()},day:function(e){var t=this._isUTC?this._d.getUTCDay():this._d.getDay();return e==null?t:this.add({d:e-t})},startOf:function(e){e=e.replace(/s$/,"");switch(e){case"year":this.month(0);case"month":this.date(1);case"week":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return e==="week"&&this.day(0),this},endOf:function(e){return this.startOf(e).add(e.replace(/s?$/,"s"),1).subtract("ms",1)},isAfter:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)>+t(e).startOf(n)},isBefore:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)<+t(e).startOf(n)},isSame:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)===+t(e).startOf(n)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return t.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(e){var n=r((t(this).startOf("day")-t(this).startOf("year"))/864e5)+1;return e==null?n:this.add("d",e-n)},isoWeek:function(e){var t=tt(this,1,4);return e==null?t:this.add("d",(e-t)*7)},week:function(e){var t=this.lang().week(this);return e==null?t:this.add("d",(e-t)*7)},lang:function(t){return t===e?this._lang:(this._lang=U(t),this)}};for(i=0;i<T.length;i++)rt(T[i].toLowerCase().replace(/s$/,""),T[i]);rt("year","FullYear"),t.fn.days=t.fn.day,t.fn.weeks=t.fn.week,t.fn.isoWeeks=t.fn.isoWeek,t.duration.fn=P.prototype={weeks:function(){return B(this.days()/7)},valueOf:function(){return this._milliseconds+this._days*864e5+this._months*2592e6},humanize:function(e){var t=+this,n=et(t,!e,this.lang());return e&&(n=this.lang().pastFuture(t,n)),this.lang().postformat(n)},lang:t.fn.lang};for(i in N)N.hasOwnProperty(i)&&(st(i,N[i]),it(i.toLowerCase()));st("Weeks",6048e5),t.lang("en",{ordinal:function(e){var t=e%10,n=~~(e%100/10)===1?"th":t===1?"st":t===2?"nd":t===3?"rd":"th";return e+n}}),o&&(module.exports=t),typeof ender=="undefined"&&(this.moment=t),typeof define=="function"&&define.amd&&define("moment",[],function(){return t})}).call(this);
\ No newline at end of file
--- a/wikked/assets/js/pagedown/Markdown.Converter.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1332 +0,0 @@
-var Markdown;
-
-if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module
-    Markdown = exports;
-else
-    Markdown = {};
-    
-// The following text is included for historical reasons, but should
-// be taken with a pinch of salt; it's not all true anymore.
-
-//
-// Wherever possible, Showdown is a straight, line-by-line port
-// of the Perl version of Markdown.
-//
-// This is not a normal parser design; it's basically just a
-// series of string substitutions.  It's hard to read and
-// maintain this way,  but keeping Showdown close to the original
-// design makes it easier to port new features.
-//
-// More importantly, Showdown behaves like markdown.pl in most
-// edge cases.  So web applications can do client-side preview
-// in Javascript, and then build identical HTML on the server.
-//
-// This port needs the new RegExp functionality of ECMA 262,
-// 3rd Edition (i.e. Javascript 1.5).  Most modern web browsers
-// should do fine.  Even with the new regular expression features,
-// We do a lot of work to emulate Perl's regex functionality.
-// The tricky changes in this file mostly have the "attacklab:"
-// label.  Major or self-explanatory changes don't.
-//
-// Smart diff tools like Araxis Merge will be able to match up
-// this file with markdown.pl in a useful way.  A little tweaking
-// helps: in a copy of markdown.pl, replace "#" with "//" and
-// replace "$text" with "text".  Be sure to ignore whitespace
-// and line endings.
-//
-
-
-//
-// Usage:
-//
-//   var text = "Markdown *rocks*.";
-//
-//   var converter = new Markdown.Converter();
-//   var html = converter.makeHtml(text);
-//
-//   alert(html);
-//
-// Note: move the sample code to the bottom of this
-// file before uncommenting it.
-//
-
-(function () {
-
-    function identity(x) { return x; }
-    function returnFalse(x) { return false; }
-
-    function HookCollection() { }
-
-    HookCollection.prototype = {
-
-        chain: function (hookname, func) {
-            var original = this[hookname];
-            if (!original)
-                throw new Error("unknown hook " + hookname);
-
-            if (original === identity)
-                this[hookname] = func;
-            else
-                this[hookname] = function (x) { return func(original(x)); }
-        },
-        set: function (hookname, func) {
-            if (!this[hookname])
-                throw new Error("unknown hook " + hookname);
-            this[hookname] = func;
-        },
-        addNoop: function (hookname) {
-            this[hookname] = identity;
-        },
-        addFalse: function (hookname) {
-            this[hookname] = returnFalse;
-        }
-    };
-
-    Markdown.HookCollection = HookCollection;
-
-    // g_urls and g_titles allow arbitrary user-entered strings as keys. This
-    // caused an exception (and hence stopped the rendering) when the user entered
-    // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this
-    // (since no builtin property starts with "s_"). See
-    // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug
-    // (granted, switching from Array() to Object() alone would have left only __proto__
-    // to be a problem)
-    function SaveHash() { }
-    SaveHash.prototype = {
-        set: function (key, value) {
-            this["s_" + key] = value;
-        },
-        get: function (key) {
-            return this["s_" + key];
-        }
-    };
-
-    Markdown.Converter = function () {
-        var pluginHooks = this.hooks = new HookCollection();
-        pluginHooks.addNoop("plainLinkText");  // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link
-        pluginHooks.addNoop("preConversion");  // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked
-        pluginHooks.addNoop("postConversion"); // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml
-
-        //
-        // Private state of the converter instance:
-        //
-
-        // Global hashes, used by various utility routines
-        var g_urls;
-        var g_titles;
-        var g_html_blocks;
-
-        // Used to track when we're inside an ordered or unordered list
-        // (see _ProcessListItems() for details):
-        var g_list_level;
-
-        this.makeHtml = function (text) {
-
-            //
-            // Main function. The order in which other subs are called here is
-            // essential. Link and image substitutions need to happen before
-            // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the <a>
-            // and <img> tags get encoded.
-            //
-
-            // This will only happen if makeHtml on the same converter instance is called from a plugin hook.
-            // Don't do that.
-            if (g_urls)
-                throw new Error("Recursive call to converter.makeHtml");
-        
-            // Create the private state objects.
-            g_urls = new SaveHash();
-            g_titles = new SaveHash();
-            g_html_blocks = [];
-            g_list_level = 0;
-
-            text = pluginHooks.preConversion(text);
-
-            // attacklab: Replace ~ with ~T
-            // This lets us use tilde as an escape char to avoid md5 hashes
-            // The choice of character is arbitray; anything that isn't
-            // magic in Markdown will work.
-            text = text.replace(/~/g, "~T");
-
-            // attacklab: Replace $ with ~D
-            // RegExp interprets $ as a special character
-            // when it's in a replacement string
-            text = text.replace(/\$/g, "~D");
-
-            // Standardize line endings
-            text = text.replace(/\r\n/g, "\n"); // DOS to Unix
-            text = text.replace(/\r/g, "\n"); // Mac to Unix
-
-            // Make sure text begins and ends with a couple of newlines:
-            text = "\n\n" + text + "\n\n";
-
-            // Convert all tabs to spaces.
-            text = _Detab(text);
-
-            // Strip any lines consisting only of spaces and tabs.
-            // This makes subsequent regexen easier to write, because we can
-            // match consecutive blank lines with /\n+/ instead of something
-            // contorted like /[ \t]*\n+/ .
-            text = text.replace(/^[ \t]+$/mg, "");
-
-            // Turn block-level HTML blocks into hash entries
-            text = _HashHTMLBlocks(text);
-
-            // Strip link definitions, store in hashes.
-            text = _StripLinkDefinitions(text);
-
-            text = _RunBlockGamut(text);
-
-            text = _UnescapeSpecialChars(text);
-
-            // attacklab: Restore dollar signs
-            text = text.replace(/~D/g, "$$");
-
-            // attacklab: Restore tildes
-            text = text.replace(/~T/g, "~");
-
-            text = pluginHooks.postConversion(text);
-
-            g_html_blocks = g_titles = g_urls = null;
-
-            return text;
-        };
-
-        function _StripLinkDefinitions(text) {
-            //
-            // Strips link definitions from text, stores the URLs and titles in
-            // hash references.
-            //
-
-            // Link defs are in the form: ^[id]: url "optional title"
-
-            /*
-            text = text.replace(/
-                ^[ ]{0,3}\[(.+)\]:  // id = $1  attacklab: g_tab_width - 1
-                [ \t]*
-                \n?                 // maybe *one* newline
-                [ \t]*
-                <?(\S+?)>?          // url = $2
-                (?=\s|$)            // lookahead for whitespace instead of the lookbehind removed below
-                [ \t]*
-                \n?                 // maybe one newline
-                [ \t]*
-                (                   // (potential) title = $3
-                    (\n*)           // any lines skipped = $4 attacklab: lookbehind removed
-                    [ \t]+
-                    ["(]
-                    (.+?)           // title = $5
-                    [")]
-                    [ \t]*
-                )?                  // title is optional
-                (?:\n+|$)
-            /gm, function(){...});
-            */
-
-            text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm,
-                function (wholeMatch, m1, m2, m3, m4, m5) {
-                    m1 = m1.toLowerCase();
-                    g_urls.set(m1, _EncodeAmpsAndAngles(m2));  // Link IDs are case-insensitive
-                    if (m4) {
-                        // Oops, found blank lines, so it's not a title.
-                        // Put back the parenthetical statement we stole.
-                        return m3;
-                    } else if (m5) {
-                        g_titles.set(m1, m5.replace(/"/g, "&quot;"));
-                    }
-
-                    // Completely remove the definition from the text
-                    return "";
-                }
-            );
-
-            return text;
-        }
-
-        function _HashHTMLBlocks(text) {
-
-            // Hashify HTML blocks:
-            // We only want to do this for block-level HTML tags, such as headers,
-            // lists, and tables. That's because we still want to wrap <p>s around
-            // "paragraphs" that are wrapped in non-block-level tags, such as anchors,
-            // phrase emphasis, and spans. The list of tags we're looking for is
-            // hard-coded:
-            var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del"
-            var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math"
-
-            // First, look for nested blocks, e.g.:
-            //   <div>
-            //     <div>
-            //     tags for inner block must be indented.
-            //     </div>
-            //   </div>
-            //
-            // The outermost tags must start at the left margin for this to match, and
-            // the inner nested divs must be indented.
-            // We need to do this before the next, more liberal match, because the next
-            // match will start at the first `<div>` and stop at the first `</div>`.
-
-            // attacklab: This regex can be expensive when it fails.
-
-            /*
-            text = text.replace(/
-                (                       // save in $1
-                    ^                   // start of line  (with /m)
-                    <($block_tags_a)    // start tag = $2
-                    \b                  // word break
-                                        // attacklab: hack around khtml/pcre bug...
-                    [^\r]*?\n           // any number of lines, minimally matching
-                    </\2>               // the matching end tag
-                    [ \t]*              // trailing spaces/tabs
-                    (?=\n+)             // followed by a newline
-                )                       // attacklab: there are sentinel newlines at end of document
-            /gm,function(){...}};
-            */
-            text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement);
-
-            //
-            // Now match more liberally, simply from `\n<tag>` to `</tag>\n`
-            //
-
-            /*
-            text = text.replace(/
-                (                       // save in $1
-                    ^                   // start of line  (with /m)
-                    <($block_tags_b)    // start tag = $2
-                    \b                  // word break
-                                        // attacklab: hack around khtml/pcre bug...
-                    [^\r]*?             // any number of lines, minimally matching
-                    .*</\2>             // the matching end tag
-                    [ \t]*              // trailing spaces/tabs
-                    (?=\n+)             // followed by a newline
-                )                       // attacklab: there are sentinel newlines at end of document
-            /gm,function(){...}};
-            */
-            text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement);
-
-            // Special case just for <hr />. It was easier to make a special case than
-            // to make the other regex more complicated.  
-
-            /*
-            text = text.replace(/
-                \n                  // Starting after a blank line
-                [ ]{0,3}
-                (                   // save in $1
-                    (<(hr)          // start tag = $2
-                        \b          // word break
-                        ([^<>])*?
-                    \/?>)           // the matching end tag
-                    [ \t]*
-                    (?=\n{2,})      // followed by a blank line
-                )
-            /g,hashElement);
-            */
-            text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement);
-
-            // Special case for standalone HTML comments:
-
-            /*
-            text = text.replace(/
-                \n\n                                            // Starting after a blank line
-                [ ]{0,3}                                        // attacklab: g_tab_width - 1
-                (                                               // save in $1
-                    <!
-                    (--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)   // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256
-                    >
-                    [ \t]*
-                    (?=\n{2,})                                  // followed by a blank line
-                )
-            /g,hashElement);
-            */
-            text = text.replace(/\n\n[ ]{0,3}(<!(--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement);
-
-            // PHP and ASP-style processor instructions (<?...?> and <%...%>)
-
-            /*
-            text = text.replace(/
-                (?:
-                    \n\n            // Starting after a blank line
-                )
-                (                   // save in $1
-                    [ ]{0,3}        // attacklab: g_tab_width - 1
-                    (?:
-                        <([?%])     // $2
-                        [^\r]*?
-                        \2>
-                    )
-                    [ \t]*
-                    (?=\n{2,})      // followed by a blank line
-                )
-            /g,hashElement);
-            */
-            text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement);
-
-            return text;
-        }
-
-        function hashElement(wholeMatch, m1) {
-            var blockText = m1;
-
-            // Undo double lines
-            blockText = blockText.replace(/^\n+/, "");
-
-            // strip trailing blank lines
-            blockText = blockText.replace(/\n+$/g, "");
-
-            // Replace the element text with a marker ("~KxK" where x is its key)
-            blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n";
-
-            return blockText;
-        }
-
-        function _RunBlockGamut(text, doNotUnhash) {
-            //
-            // These are all the transformations that form block-level
-            // tags like paragraphs, headers, and list items.
-            //
-            text = _DoHeaders(text);
-
-            // Do Horizontal Rules:
-            var replacement = "<hr />\n";
-            text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement);
-            text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement);
-            text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement);
-
-            text = _DoLists(text);
-            text = _DoCodeBlocks(text);
-            text = _DoBlockQuotes(text);
-
-            // We already ran _HashHTMLBlocks() before, in Markdown(), but that
-            // was to escape raw HTML in the original Markdown source. This time,
-            // we're escaping the markup we've just created, so that we don't wrap
-            // <p> tags around block-level tags.
-            text = _HashHTMLBlocks(text);
-            text = _FormParagraphs(text, doNotUnhash);
-
-            return text;
-        }
-
-        function _RunSpanGamut(text) {
-            //
-            // These are all the transformations that occur *within* block-level
-            // tags like paragraphs, headers, and list items.
-            //
-
-            text = _DoCodeSpans(text);
-            text = _EscapeSpecialCharsWithinTagAttributes(text);
-            text = _EncodeBackslashEscapes(text);
-
-            // Process anchor and image tags. Images must come first,
-            // because ![foo][f] looks like an anchor.
-            text = _DoImages(text);
-            text = _DoAnchors(text);
-
-            // Make links out of things like `<http://example.com/>`
-            // Must come after _DoAnchors(), because you can use < and >
-            // delimiters in inline links like [this](<url>).
-            text = _DoAutoLinks(text);
-            
-            text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now
-            
-            text = _EncodeAmpsAndAngles(text);
-            text = _DoItalicsAndBold(text);
-
-            // Do hard breaks:
-            text = text.replace(/  +\n/g, " <br>\n");
-
-            return text;
-        }
-
-        function _EscapeSpecialCharsWithinTagAttributes(text) {
-            //
-            // Within tags -- meaning between < and > -- encode [\ ` * _] so they
-            // don't conflict with their use in Markdown for code, italics and strong.
-            //
-
-            // Build a regex to find HTML tags and comments.  See Friedl's 
-            // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
-
-            // SE: changed the comment part of the regex
-
-            var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi;
-
-            text = text.replace(regex, function (wholeMatch) {
-                var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`");
-                tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987
-                return tag;
-            });
-
-            return text;
-        }
-
-        function _DoAnchors(text) {
-            //
-            // Turn Markdown link shortcuts into XHTML <a> tags.
-            //
-            //
-            // First, handle reference-style links: [link text] [id]
-            //
-
-            /*
-            text = text.replace(/
-                (                           // wrap whole match in $1
-                    \[
-                    (
-                        (?:
-                            \[[^\]]*\]      // allow brackets nested one level
-                            |
-                            [^\[]           // or anything else
-                        )*
-                    )
-                    \]
-
-                    [ ]?                    // one optional space
-                    (?:\n[ ]*)?             // one optional newline followed by spaces
-
-                    \[
-                    (.*?)                   // id = $3
-                    \]
-                )
-                ()()()()                    // pad remaining backreferences
-            /g, writeAnchorTag);
-            */
-            text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);
-
-            //
-            // Next, inline-style links: [link text](url "optional title")
-            //
-
-            /*
-            text = text.replace(/
-                (                           // wrap whole match in $1
-                    \[
-                    (
-                        (?:
-                            \[[^\]]*\]      // allow brackets nested one level
-                            |
-                            [^\[\]]         // or anything else
-                        )*
-                    )
-                    \]
-                    \(                      // literal paren
-                    [ \t]*
-                    ()                      // no id, so leave $3 empty
-                    <?(                     // href = $4
-                        (?:
-                            \([^)]*\)       // allow one level of (correctly nested) parens (think MSDN)
-                            |
-                            [^()\s]
-                        )*?
-                    )>?                
-                    [ \t]*
-                    (                       // $5
-                        (['"])              // quote char = $6
-                        (.*?)               // Title = $7
-                        \6                  // matching quote
-                        [ \t]*              // ignore any spaces/tabs between closing quote and )
-                    )?                      // title is optional
-                    \)
-                )
-            /g, writeAnchorTag);
-            */
-
-            text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?((?:\([^)]*\)|[^()\s])*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag);
-
-            //
-            // Last, handle reference-style shortcuts: [link text]
-            // These must come last in case you've also got [link test][1]
-            // or [link test](/foo)
-            //
-
-            /*
-            text = text.replace(/
-                (                   // wrap whole match in $1
-                    \[
-                    ([^\[\]]+)      // link text = $2; can't contain '[' or ']'
-                    \]
-                )
-                ()()()()()          // pad rest of backreferences
-            /g, writeAnchorTag);
-            */
-            text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
-
-            return text;
-        }
-
-        function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
-            if (m7 == undefined) m7 = "";
-            var whole_match = m1;
-            var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs
-            var link_id = m3.toLowerCase();
-            var url = m4;
-            var title = m7;
-
-            if (url == "") {
-                if (link_id == "") {
-                    // lower-case and turn embedded newlines into spaces
-                    link_id = link_text.toLowerCase().replace(/ ?\n/g, " ");
-                }
-                url = "#" + link_id;
-
-                if (g_urls.get(link_id) != undefined) {
-                    url = g_urls.get(link_id);
-                    if (g_titles.get(link_id) != undefined) {
-                        title = g_titles.get(link_id);
-                    }
-                }
-                else {
-                    if (whole_match.search(/\(\s*\)$/m) > -1) {
-                        // Special case for explicit empty url
-                        url = "";
-                    } else {
-                        return whole_match;
-                    }
-                }
-            }
-            url = encodeProblemUrlChars(url);
-            url = escapeCharacters(url, "*_");
-            var result = "<a href=\"" + url + "\"";
-
-            if (title != "") {
-                title = attributeEncode(title);
-                title = escapeCharacters(title, "*_");
-                result += " title=\"" + title + "\"";
-            }
-
-            result += ">" + link_text + "</a>";
-
-            return result;
-        }
-
-        function _DoImages(text) {
-            //
-            // Turn Markdown image shortcuts into <img> tags.
-            //
-
-            //
-            // First, handle reference-style labeled images: ![alt text][id]
-            //
-
-            /*
-            text = text.replace(/
-                (                   // wrap whole match in $1
-                    !\[
-                    (.*?)           // alt text = $2
-                    \]
-
-                    [ ]?            // one optional space
-                    (?:\n[ ]*)?     // one optional newline followed by spaces
-
-                    \[
-                    (.*?)           // id = $3
-                    \]
-                )
-                ()()()()            // pad rest of backreferences
-            /g, writeImageTag);
-            */
-            text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag);
-
-            //
-            // Next, handle inline images:  ![alt text](url "optional title")
-            // Don't forget: encode * and _
-
-            /*
-            text = text.replace(/
-                (                   // wrap whole match in $1
-                    !\[
-                    (.*?)           // alt text = $2
-                    \]
-                    \s?             // One optional whitespace character
-                    \(              // literal paren
-                    [ \t]*
-                    ()              // no id, so leave $3 empty
-                    <?(\S+?)>?      // src url = $4
-                    [ \t]*
-                    (               // $5
-                        (['"])      // quote char = $6
-                        (.*?)       // title = $7
-                        \6          // matching quote
-                        [ \t]*
-                    )?              // title is optional
-                    \)
-                )
-            /g, writeImageTag);
-            */
-            text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()<?(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag);
-
-            return text;
-        }
-        
-        function attributeEncode(text) {
-            // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title)
-            // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it)
-            return text.replace(/>/g, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
-        }
-
-        function writeImageTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
-            var whole_match = m1;
-            var alt_text = m2;
-            var link_id = m3.toLowerCase();
-            var url = m4;
-            var title = m7;
-
-            if (!title) title = "";
-
-            if (url == "") {
-                if (link_id == "") {
-                    // lower-case and turn embedded newlines into spaces
-                    link_id = alt_text.toLowerCase().replace(/ ?\n/g, " ");
-                }
-                url = "#" + link_id;
-
-                if (g_urls.get(link_id) != undefined) {
-                    url = g_urls.get(link_id);
-                    if (g_titles.get(link_id) != undefined) {
-                        title = g_titles.get(link_id);
-                    }
-                }
-                else {
-                    return whole_match;
-                }
-            }
-            
-            alt_text = escapeCharacters(attributeEncode(alt_text), "*_[]()");
-            url = escapeCharacters(url, "*_");
-            var result = "<img src=\"" + url + "\" alt=\"" + alt_text + "\"";
-
-            // attacklab: Markdown.pl adds empty title attributes to images.
-            // Replicate this bug.
-
-            //if (title != "") {
-            title = attributeEncode(title);
-            title = escapeCharacters(title, "*_");
-            result += " title=\"" + title + "\"";
-            //}
-
-            result += " />";
-
-            return result;
-        }
-
-        function _DoHeaders(text) {
-
-            // Setext-style headers:
-            //  Header 1
-            //  ========
-            //  
-            //  Header 2
-            //  --------
-            //
-            text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
-                function (wholeMatch, m1) { return "<h1>" + _RunSpanGamut(m1) + "</h1>\n\n"; }
-            );
-
-            text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
-                function (matchFound, m1) { return "<h2>" + _RunSpanGamut(m1) + "</h2>\n\n"; }
-            );
-
-            // atx-style headers:
-            //  # Header 1
-            //  ## Header 2
-            //  ## Header 2 with closing hashes ##
-            //  ...
-            //  ###### Header 6
-            //
-
-            /*
-            text = text.replace(/
-                ^(\#{1,6})      // $1 = string of #'s
-                [ \t]*
-                (.+?)           // $2 = Header text
-                [ \t]*
-                \#*             // optional closing #'s (not counted)
-                \n+
-            /gm, function() {...});
-            */
-
-            text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
-                function (wholeMatch, m1, m2) {
-                    var h_level = m1.length;
-                    return "<h" + h_level + ">" + _RunSpanGamut(m2) + "</h" + h_level + ">\n\n";
-                }
-            );
-
-            return text;
-        }
-
-        function _DoLists(text) {
-            //
-            // Form HTML ordered (numbered) and unordered (bulleted) lists.
-            //
-
-            // attacklab: add sentinel to hack around khtml/safari bug:
-            // http://bugs.webkit.org/show_bug.cgi?id=11231
-            text += "~0";
-
-            // Re-usable pattern to match any entirel ul or ol list:
-
-            /*
-            var whole_list = /
-                (                                   // $1 = whole list
-                    (                               // $2
-                        [ ]{0,3}                    // attacklab: g_tab_width - 1
-                        ([*+-]|\d+[.])              // $3 = first list item marker
-                        [ \t]+
-                    )
-                    [^\r]+?
-                    (                               // $4
-                        ~0                          // sentinel for workaround; should be $
-                        |
-                        \n{2,}
-                        (?=\S)
-                        (?!                         // Negative lookahead for another list item marker
-                            [ \t]*
-                            (?:[*+-]|\d+[.])[ \t]+
-                        )
-                    )
-                )
-            /g
-            */
-            var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
-
-            if (g_list_level) {
-                text = text.replace(whole_list, function (wholeMatch, m1, m2) {
-                    var list = m1;
-                    var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol";
-
-                    var result = _ProcessListItems(list, list_type);
-
-                    // Trim any trailing whitespace, to put the closing `</$list_type>`
-                    // up on the preceding line, to get it past the current stupid
-                    // HTML block parser. This is a hack to work around the terrible
-                    // hack that is the HTML block parser.
-                    result = result.replace(/\s+$/, "");
-                    result = "<" + list_type + ">" + result + "</" + list_type + ">\n";
-                    return result;
-                });
-            } else {
-                whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;
-                text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) {
-                    var runup = m1;
-                    var list = m2;
-
-                    var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol";
-                    var result = _ProcessListItems(list, list_type);
-                    result = runup + "<" + list_type + ">\n" + result + "</" + list_type + ">\n";
-                    return result;
-                });
-            }
-
-            // attacklab: strip sentinel
-            text = text.replace(/~0/, "");
-
-            return text;
-        }
-
-        var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" };
-
-        function _ProcessListItems(list_str, list_type) {
-            //
-            //  Process the contents of a single ordered or unordered list, splitting it
-            //  into individual list items.
-            //
-            //  list_type is either "ul" or "ol".
-
-            // The $g_list_level global keeps track of when we're inside a list.
-            // Each time we enter a list, we increment it; when we leave a list,
-            // we decrement. If it's zero, we're not in a list anymore.
-            //
-            // We do this because when we're not inside a list, we want to treat
-            // something like this:
-            //
-            //    I recommend upgrading to version
-            //    8. Oops, now this line is treated
-            //    as a sub-list.
-            //
-            // As a single paragraph, despite the fact that the second line starts
-            // with a digit-period-space sequence.
-            //
-            // Whereas when we're inside a list (or sub-list), that line will be
-            // treated as the start of a sub-list. What a kludge, huh? This is
-            // an aspect of Markdown's syntax that's hard to parse perfectly
-            // without resorting to mind-reading. Perhaps the solution is to
-            // change the syntax rules such that sub-lists must start with a
-            // starting cardinal number; e.g. "1." or "a.".
-
-            g_list_level++;
-
-            // trim trailing blank lines:
-            list_str = list_str.replace(/\n{2,}$/, "\n");
-
-            // attacklab: add sentinel to emulate \z
-            list_str += "~0";
-
-            // In the original attacklab showdown, list_type was not given to this function, and anything
-            // that matched /[*+-]|\d+[.]/ would just create the next <li>, causing this mismatch:
-            //
-            //  Markdown          rendered by WMD        rendered by MarkdownSharp
-            //  ------------------------------------------------------------------
-            //  1. first          1. first               1. first
-            //  2. second         2. second              2. second
-            //  - third           3. third                   * third
-            //
-            // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx,
-            // with {MARKER} being one of \d+[.] or [*+-], depending on list_type:
-        
-            /*
-            list_str = list_str.replace(/
-                (^[ \t]*)                       // leading whitespace = $1
-                ({MARKER}) [ \t]+               // list marker = $2
-                ([^\r]+?                        // list item text   = $3
-                    (\n+)
-                )
-                (?=
-                    (~0 | \2 ({MARKER}) [ \t]+)
-                )
-            /gm, function(){...});
-            */
-
-            var marker = _listItemMarkers[list_type];
-            var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm");
-            var last_item_had_a_double_newline = false;
-            list_str = list_str.replace(re,
-                function (wholeMatch, m1, m2, m3) {
-                    var item = m3;
-                    var leading_space = m1;
-                    var ends_with_double_newline = /\n\n$/.test(item);
-                    var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1;
-
-                    if (contains_double_newline || last_item_had_a_double_newline) {
-                        item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true);
-                    }
-                    else {
-                        // Recursion for sub-lists:
-                        item = _DoLists(_Outdent(item));
-                        item = item.replace(/\n$/, ""); // chomp(item)
-                        item = _RunSpanGamut(item);
-                    }
-                    last_item_had_a_double_newline = ends_with_double_newline;
-                    return "<li>" + item + "</li>\n";
-                }
-            );
-
-            // attacklab: strip sentinel
-            list_str = list_str.replace(/~0/g, "");
-
-            g_list_level--;
-            return list_str;
-        }
-
-        function _DoCodeBlocks(text) {
-            //
-            //  Process Markdown `<pre><code>` blocks.
-            //  
-
-            /*
-            text = text.replace(/
-                (?:\n\n|^)
-                (                               // $1 = the code block -- one or more lines, starting with a space/tab
-                    (?:
-                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
-                        .*\n+
-                    )+
-                )
-                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
-            /g ,function(){...});
-            */
-
-            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
-            text += "~0";
-
-            text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
-                function (wholeMatch, m1, m2) {
-                    var codeblock = m1;
-                    var nextChar = m2;
-
-                    codeblock = _EncodeCode(_Outdent(codeblock));
-                    codeblock = _Detab(codeblock);
-                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
-                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
-
-                    codeblock = "<pre><code>" + codeblock + "\n</code></pre>";
-
-                    return "\n\n" + codeblock + "\n\n" + nextChar;
-                }
-            );
-
-            // attacklab: strip sentinel
-            text = text.replace(/~0/, "");
-
-            return text;
-        }
-
-        function hashBlock(text) {
-            text = text.replace(/(^\n+|\n+$)/g, "");
-            return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n";
-        }
-
-        function _DoCodeSpans(text) {
-            //
-            // * Backtick quotes are used for <code></code> spans.
-            // 
-            // * You can use multiple backticks as the delimiters if you want to
-            //   include literal backticks in the code span. So, this input:
-            //     
-            //      Just type ``foo `bar` baz`` at the prompt.
-            //     
-            //   Will translate to:
-            //     
-            //      <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
-            //     
-            //   There's no arbitrary limit to the number of backticks you
-            //   can use as delimters. If you need three consecutive backticks
-            //   in your code, use four for delimiters, etc.
-            //
-            // * You can use spaces to get literal backticks at the edges:
-            //     
-            //      ... type `` `bar` `` ...
-            //     
-            //   Turns to:
-            //     
-            //      ... type <code>`bar`</code> ...
-            //
-
-            /*
-            text = text.replace(/
-                (^|[^\\])       // Character before opening ` can't be a backslash
-                (`+)            // $2 = Opening run of `
-                (               // $3 = The code block
-                    [^\r]*?
-                    [^`]        // attacklab: work around lack of lookbehind
-                )
-                \2              // Matching closer
-                (?!`)
-            /gm, function(){...});
-            */
-
-            text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
-                function (wholeMatch, m1, m2, m3, m4) {
-                    var c = m3;
-                    c = c.replace(/^([ \t]*)/g, ""); // leading whitespace
-                    c = c.replace(/[ \t]*$/g, ""); // trailing whitespace
-                    c = _EncodeCode(c);
-                    c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs.
-                    return m1 + "<code>" + c + "</code>";
-                }
-            );
-
-            return text;
-        }
-
-        function _EncodeCode(text) {
-            //
-            // Encode/escape certain characters inside Markdown code runs.
-            // The point is that in code, these characters are literals,
-            // and lose their special Markdown meanings.
-            //
-            // Encode all ampersands; HTML entities are not
-            // entities within a Markdown code span.
-            text = text.replace(/&/g, "&amp;");
-
-            // Do the angle bracket song and dance:
-            text = text.replace(/</g, "&lt;");
-            text = text.replace(/>/g, "&gt;");
-
-            // Now, escape characters that are magic in Markdown:
-            text = escapeCharacters(text, "\*_{}[]\\", false);
-
-            // jj the line above breaks this:
-            //---
-
-            //* Item
-
-            //   1. Subitem
-
-            //            special char: *
-            //---
-
-            return text;
-        }
-
-        function _DoItalicsAndBold(text) {
-
-            // <strong> must go first:
-            text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g,
-            "$1<strong>$3</strong>$4");
-
-            text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g,
-            "$1<em>$3</em>$4");
-
-            return text;
-        }
-
-        function _DoBlockQuotes(text) {
-
-            /*
-            text = text.replace(/
-                (                           // Wrap whole match in $1
-                    (
-                        ^[ \t]*>[ \t]?      // '>' at the start of a line
-                        .+\n                // rest of the first line
-                        (.+\n)*             // subsequent consecutive lines
-                        \n*                 // blanks
-                    )+
-                )
-            /gm, function(){...});
-            */
-
-            text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
-                function (wholeMatch, m1) {
-                    var bq = m1;
-
-                    // attacklab: hack around Konqueror 3.5.4 bug:
-                    // "----------bug".replace(/^-/g,"") == "bug"
-
-                    bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting
-
-                    // attacklab: clean up hack
-                    bq = bq.replace(/~0/g, "");
-
-                    bq = bq.replace(/^[ \t]+$/gm, "");     // trim whitespace-only lines
-                    bq = _RunBlockGamut(bq);             // recurse
-
-                    bq = bq.replace(/(^|\n)/g, "$1  ");
-                    // These leading spaces screw with <pre> content, so we need to fix that:
-                    bq = bq.replace(
-                            /(\s*<pre>[^\r]+?<\/pre>)/gm,
-                        function (wholeMatch, m1) {
-                            var pre = m1;
-                            // attacklab: hack around Konqueror 3.5.4 bug:
-                            pre = pre.replace(/^  /mg, "~0");
-                            pre = pre.replace(/~0/g, "");
-                            return pre;
-                        });
-
-                    return hashBlock("<blockquote>\n" + bq + "\n</blockquote>");
-                }
-            );
-            return text;
-        }
-
-        function _FormParagraphs(text, doNotUnhash) {
-            //
-            //  Params:
-            //    $text - string to process with html <p> tags
-            //
-
-            // Strip leading and trailing lines:
-            text = text.replace(/^\n+/g, "");
-            text = text.replace(/\n+$/g, "");
-
-            var grafs = text.split(/\n{2,}/g);
-            var grafsOut = [];
-            
-            var markerRe = /~K(\d+)K/;
-
-            //
-            // Wrap <p> tags.
-            //
-            var end = grafs.length;
-            for (var i = 0; i < end; i++) {
-                var str = grafs[i];
-
-                // if this is an HTML marker, copy it
-                if (markerRe.test(str)) {
-                    grafsOut.push(str);
-                }
-                else if (/\S/.test(str)) {
-                    str = _RunSpanGamut(str);
-                    str = str.replace(/^([ \t]*)/g, "<p>");
-                    str += "</p>"
-                    grafsOut.push(str);
-                }
-
-            }
-            //
-            // Unhashify HTML blocks
-            //
-            if (!doNotUnhash) {
-                end = grafsOut.length;
-                for (var i = 0; i < end; i++) {
-                    var foundAny = true;
-                    while (foundAny) { // we may need several runs, since the data may be nested
-                        foundAny = false;
-                        grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) {
-                            foundAny = true;
-                            return g_html_blocks[id];
-                        });
-                    }
-                }
-            }
-            return grafsOut.join("\n\n");
-        }
-
-        function _EncodeAmpsAndAngles(text) {
-            // Smart processing for ampersands and angle brackets that need to be encoded.
-
-            // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
-            //   http://bumppo.net/projects/amputator/
-            text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&amp;");
-
-            // Encode naked <'s
-            text = text.replace(/<(?![a-z\/?\$!])/gi, "&lt;");
-
-            return text;
-        }
-
-        function _EncodeBackslashEscapes(text) {
-            //
-            //   Parameter:  String.
-            //   Returns:    The string, with after processing the following backslash
-            //               escape sequences.
-            //
-
-            // attacklab: The polite way to do this is with the new
-            // escapeCharacters() function:
-            //
-            //     text = escapeCharacters(text,"\\",true);
-            //     text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);
-            //
-            // ...but we're sidestepping its use of the (slow) RegExp constructor
-            // as an optimization for Firefox.  This function gets called a LOT.
-
-            text = text.replace(/\\(\\)/g, escapeCharacters_callback);
-            text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback);
-            return text;
-        }
-
-        function _DoAutoLinks(text) {
-
-            // note that at this point, all other URL in the text are already hyperlinked as <a href=""></a>
-            // *except* for the <http://www.foo.com> case
-
-            // automatically add < and > around unadorned raw hyperlinks
-            // must be preceded by space/BOF and followed by non-word/EOF character    
-            text = text.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi, "$1<$2$3>$4");
-
-            //  autolink anything like <http://example.com>
-            
-            var replacer = function (wholematch, m1) { return "<a href=\"" + m1 + "\">" + pluginHooks.plainLinkText(m1) + "</a>"; }
-            text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer);
-
-            // Email addresses: <address@domain.foo>
-            /*
-            text = text.replace(/
-                <
-                (?:mailto:)?
-                (
-                    [-.\w]+
-                    \@
-                    [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
-                )
-                >
-            /gi, _DoAutoLinks_callback());
-            */
-
-            /* disabling email autolinking, since we don't do that on the server, either
-            text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
-                function(wholeMatch,m1) {
-                    return _EncodeEmailAddress( _UnescapeSpecialChars(m1) );
-                }
-            );
-            */
-            return text;
-        }
-
-        function _UnescapeSpecialChars(text) {
-            //
-            // Swap back in all the special characters we've hidden.
-            //
-            text = text.replace(/~E(\d+)E/g,
-                function (wholeMatch, m1) {
-                    var charCodeToReplace = parseInt(m1);
-                    return String.fromCharCode(charCodeToReplace);
-                }
-            );
-            return text;
-        }
-
-        function _Outdent(text) {
-            //
-            // Remove one level of line-leading tabs or spaces
-            //
-
-            // attacklab: hack around Konqueror 3.5.4 bug:
-            // "----------bug".replace(/^-/g,"") == "bug"
-
-            text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width
-
-            // attacklab: clean up hack
-            text = text.replace(/~0/g, "")
-
-            return text;
-        }
-
-        function _Detab(text) {
-            if (!/\t/.test(text))
-                return text;
-
-            var spaces = ["    ", "   ", "  ", " "],
-            skew = 0,
-            v;
-
-            return text.replace(/[\n\t]/g, function (match, offset) {
-                if (match === "\n") {
-                    skew = offset + 1;
-                    return match;
-                }
-                v = (offset - skew) % 4;
-                skew = offset + 1;
-                return spaces[v];
-            });
-        }
-
-        //
-        //  attacklab: Utility functions
-        //
-
-        var _problemUrlChars = /(?:["'*()[\]:]|~D)/g;
-
-        // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems 
-        function encodeProblemUrlChars(url) {
-            if (!url)
-                return "";
-
-            var len = url.length;
-
-            return url.replace(_problemUrlChars, function (match, offset) {
-                if (match == "~D") // escape for dollar
-                    return "%24";
-                if (match == ":") {
-                    if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1)))
-                        return ":"
-                }
-                return "%" + match.charCodeAt(0).toString(16);
-            });
-        }
-
-
-        function escapeCharacters(text, charsToEscape, afterBackslash) {
-            // First we have to escape the escape characters so that
-            // we can build a character class out of them
-            var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])";
-
-            if (afterBackslash) {
-                regexString = "\\\\" + regexString;
-            }
-
-            var regex = new RegExp(regexString, "g");
-            text = text.replace(regex, escapeCharacters_callback);
-
-            return text;
-        }
-
-
-        function escapeCharacters_callback(wholeMatch, m1) {
-            var charCodeToEscape = m1.charCodeAt(0);
-            return "~E" + charCodeToEscape + "E";
-        }
-
-    }; // end of the Markdown.Converter constructor
-
-})();
--- a/wikked/assets/js/pagedown/Markdown.Editor.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2211 +0,0 @@
-// needs Markdown.Converter.js at the moment
-
-(function () {
-
-    var util = {},
-        position = {},
-        ui = {},
-        doc = window.document,
-        re = window.RegExp,
-        nav = window.navigator,
-        SETTINGS = { lineLength: 72 },
-
-    // Used to work around some browser bugs where we can't use feature testing.
-        uaSniffed = {
-            isIE: /msie/.test(nav.userAgent.toLowerCase()),
-            isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()),
-            isOpera: /opera/.test(nav.userAgent.toLowerCase())
-        };
-
-    var defaultsStrings = {
-        bold: "Strong <strong> Ctrl+B",
-        boldexample: "strong text",
-
-        italic: "Emphasis <em> Ctrl+I",
-        italicexample: "emphasized text",
-
-        link: "Hyperlink <a> Ctrl+L",
-        linkdescription: "enter link description here",
-        linkdialog: "<p><b>Insert Hyperlink</b></p><p>http://example.com/ \"optional title\"</p>",
-
-        quote: "Blockquote <blockquote> Ctrl+Q",
-        quoteexample: "Blockquote",
-
-        code: "Code Sample <pre><code> Ctrl+K",
-        codeexample: "enter code here",
-
-        image: "Image <img> Ctrl+G",
-        imagedescription: "enter image description here",
-        imagedialog: "<p><b>Insert Image</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br>Need <a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>free image hosting?</a></p>",
-
-        olist: "Numbered List <ol> Ctrl+O",
-        ulist: "Bulleted List <ul> Ctrl+U",
-        litem: "List item",
-
-        heading: "Heading <h1>/<h2> Ctrl+H",
-        headingexample: "Heading",
-
-        hr: "Horizontal Rule <hr> Ctrl+R",
-
-        undo: "Undo - Ctrl+Z",
-        redo: "Redo - Ctrl+Y",
-        redomac: "Redo - Ctrl+Shift+Z",
-
-        help: "Markdown Editing Help"
-    };
-
-
-    // -------------------------------------------------------------------
-    //  YOUR CHANGES GO HERE
-    //
-    // I've tried to localize the things you are likely to change to
-    // this area.
-    // -------------------------------------------------------------------
-
-    // The default text that appears in the dialog input box when entering
-    // links.
-    var imageDefaultText = "http://";
-    var linkDefaultText = "http://";
-
-    // -------------------------------------------------------------------
-    //  END OF YOUR CHANGES
-    // -------------------------------------------------------------------
-
-    // options, if given, can have the following properties:
-    //   options.helpButton = { handler: yourEventHandler }
-    //   options.strings = { italicexample: "slanted text" }
-    // `yourEventHandler` is the click handler for the help button.
-    // If `options.helpButton` isn't given, not help button is created.
-    // `options.strings` can have any or all of the same properties as
-    // `defaultStrings` above, so you can just override some string displayed
-    // to the user on a case-by-case basis, or translate all strings to
-    // a different language.
-    //
-    // For backwards compatibility reasons, the `options` argument can also
-    // be just the `helpButton` object, and `strings.help` can also be set via
-    // `helpButton.title`. This should be considered legacy.
-    //
-    // The constructed editor object has the methods:
-    // - getConverter() returns the markdown converter object that was passed to the constructor
-    // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
-    // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
-    Markdown.Editor = function (markdownConverter, idPostfix, options) {
-        
-        options = options || {};
-
-        if (typeof options.handler === "function") { //backwards compatible behavior
-            options = { helpButton: options };
-        }
-        options.strings = options.strings || {};
-        if (options.helpButton) {
-            options.strings.help = options.strings.help || options.helpButton.title;
-        }
-        var getString = function (identifier) { return options.strings[identifier] || defaultsStrings[identifier]; }
-
-        idPostfix = idPostfix || "";
-
-        var hooks = this.hooks = new Markdown.HookCollection();
-        hooks.addNoop("onPreviewRefresh");       // called with no arguments after the preview has been refreshed
-        hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
-        hooks.addFalse("insertImageDialog");     /* called with one parameter: a callback to be called with the URL of the image. If the application creates
-                                                  * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
-                                                  * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
-                                                  */
-
-        this.getConverter = function () { return markdownConverter; }
-
-        var that = this,
-            panels;
-
-        this.run = function () {
-            if (panels)
-                return; // already initialized
-
-            panels = new PanelCollection(idPostfix);
-            var commandManager = new CommandManager(hooks, getString);
-            var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });
-            var undoManager, uiManager;
-
-            if (!/\?noundo/.test(doc.location.href)) {
-                undoManager = new UndoManager(function () {
-                    previewManager.refresh();
-                    if (uiManager) // not available on the first call
-                        uiManager.setUndoRedoButtonStates();
-                }, panels);
-                this.textOperation = function (f) {
-                    undoManager.setCommandMode();
-                    f();
-                    that.refreshPreview();
-                }
-            }
-
-            uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString);
-            uiManager.setUndoRedoButtonStates();
-
-            var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
-
-            forceRefresh();
-        };
-
-    }
-
-    // before: contains all the text in the input box BEFORE the selection.
-    // after: contains all the text in the input box AFTER the selection.
-    function Chunks() { }
-
-    // startRegex: a regular expression to find the start tag
-    // endRegex: a regular expresssion to find the end tag
-    Chunks.prototype.findTags = function (startRegex, endRegex) {
-
-        var chunkObj = this;
-        var regex;
-
-        if (startRegex) {
-
-            regex = util.extendRegExp(startRegex, "", "$");
-
-            this.before = this.before.replace(regex,
-                function (match) {
-                    chunkObj.startTag = chunkObj.startTag + match;
-                    return "";
-                });
-
-            regex = util.extendRegExp(startRegex, "^", "");
-
-            this.selection = this.selection.replace(regex,
-                function (match) {
-                    chunkObj.startTag = chunkObj.startTag + match;
-                    return "";
-                });
-        }
-
-        if (endRegex) {
-
-            regex = util.extendRegExp(endRegex, "", "$");
-
-            this.selection = this.selection.replace(regex,
-                function (match) {
-                    chunkObj.endTag = match + chunkObj.endTag;
-                    return "";
-                });
-
-            regex = util.extendRegExp(endRegex, "^", "");
-
-            this.after = this.after.replace(regex,
-                function (match) {
-                    chunkObj.endTag = match + chunkObj.endTag;
-                    return "";
-                });
-        }
-    };
-
-    // If remove is false, the whitespace is transferred
-    // to the before/after regions.
-    //
-    // If remove is true, the whitespace disappears.
-    Chunks.prototype.trimWhitespace = function (remove) {
-        var beforeReplacer, afterReplacer, that = this;
-        if (remove) {
-            beforeReplacer = afterReplacer = "";
-        } else {
-            beforeReplacer = function (s) { that.before += s; return ""; }
-            afterReplacer = function (s) { that.after = s + that.after; return ""; }
-        }
-
-        this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
-    };
-
-
-    Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
-
-        if (nLinesBefore === undefined) {
-            nLinesBefore = 1;
-        }
-
-        if (nLinesAfter === undefined) {
-            nLinesAfter = 1;
-        }
-
-        nLinesBefore++;
-        nLinesAfter++;
-
-        var regexText;
-        var replacementText;
-
-        // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
-        if (navigator.userAgent.match(/Chrome/)) {
-            "X".match(/()./);
-        }
-
-        this.selection = this.selection.replace(/(^\n*)/, "");
-
-        this.startTag = this.startTag + re.$1;
-
-        this.selection = this.selection.replace(/(\n*$)/, "");
-        this.endTag = this.endTag + re.$1;
-        this.startTag = this.startTag.replace(/(^\n*)/, "");
-        this.before = this.before + re.$1;
-        this.endTag = this.endTag.replace(/(\n*$)/, "");
-        this.after = this.after + re.$1;
-
-        if (this.before) {
-
-            regexText = replacementText = "";
-
-            while (nLinesBefore--) {
-                regexText += "\\n?";
-                replacementText += "\n";
-            }
-
-            if (findExtraNewlines) {
-                regexText = "\\n*";
-            }
-            this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
-        }
-
-        if (this.after) {
-
-            regexText = replacementText = "";
-
-            while (nLinesAfter--) {
-                regexText += "\\n?";
-                replacementText += "\n";
-            }
-            if (findExtraNewlines) {
-                regexText = "\\n*";
-            }
-
-            this.after = this.after.replace(new re(regexText, ""), replacementText);
-        }
-    };
-
-    // end of Chunks
-
-    // A collection of the important regions on the page.
-    // Cached so we don't have to keep traversing the DOM.
-    // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
-    // this issue:
-    // Internet explorer has problems with CSS sprite buttons that use HTML
-    // lists.  When you click on the background image "button", IE will
-    // select the non-existent link text and discard the selection in the
-    // textarea.  The solution to this is to cache the textarea selection
-    // on the button's mousedown event and set a flag.  In the part of the
-    // code where we need to grab the selection, we check for the flag
-    // and, if it's set, use the cached area instead of querying the
-    // textarea.
-    //
-    // This ONLY affects Internet Explorer (tested on versions 6, 7
-    // and 8) and ONLY on button clicks.  Keyboard shortcuts work
-    // normally since the focus never leaves the textarea.
-    function PanelCollection(postfix) {
-        this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
-        this.preview = doc.getElementById("wmd-preview" + postfix);
-        this.input = doc.getElementById("wmd-input" + postfix);
-    };
-
-    // Returns true if the DOM element is visible, false if it's hidden.
-    // Checks if display is anything other than none.
-    util.isVisible = function (elem) {
-
-        if (window.getComputedStyle) {
-            // Most browsers
-            return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
-        }
-        else if (elem.currentStyle) {
-            // IE
-            return elem.currentStyle["display"] !== "none";
-        }
-    };
-
-
-    // Adds a listener callback to a DOM element which is fired on a specified
-    // event.
-    util.addEvent = function (elem, event, listener) {
-        if (elem.attachEvent) {
-            // IE only.  The "on" is mandatory.
-            elem.attachEvent("on" + event, listener);
-        }
-        else {
-            // Other browsers.
-            elem.addEventListener(event, listener, false);
-        }
-    };
-
-
-    // Removes a listener callback from a DOM element which is fired on a specified
-    // event.
-    util.removeEvent = function (elem, event, listener) {
-        if (elem.detachEvent) {
-            // IE only.  The "on" is mandatory.
-            elem.detachEvent("on" + event, listener);
-        }
-        else {
-            // Other browsers.
-            elem.removeEventListener(event, listener, false);
-        }
-    };
-
-    // Converts \r\n and \r to \n.
-    util.fixEolChars = function (text) {
-        text = text.replace(/\r\n/g, "\n");
-        text = text.replace(/\r/g, "\n");
-        return text;
-    };
-
-    // Extends a regular expression.  Returns a new RegExp
-    // using pre + regex + post as the expression.
-    // Used in a few functions where we have a base
-    // expression and we want to pre- or append some
-    // conditions to it (e.g. adding "$" to the end).
-    // The flags are unchanged.
-    //
-    // regex is a RegExp, pre and post are strings.
-    util.extendRegExp = function (regex, pre, post) {
-
-        if (pre === null || pre === undefined) {
-            pre = "";
-        }
-        if (post === null || post === undefined) {
-            post = "";
-        }
-
-        var pattern = regex.toString();
-        var flags;
-
-        // Replace the flags with empty space and store them.
-        pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) {
-            flags = flagsPart;
-            return "";
-        });
-
-        // Remove the slash delimiters on the regular expression.
-        pattern = pattern.replace(/(^\/|\/$)/g, "");
-        pattern = pre + pattern + post;
-
-        return new re(pattern, flags);
-    }
-
-    // UNFINISHED
-    // The assignment in the while loop makes jslint cranky.
-    // I'll change it to a better loop later.
-    position.getTop = function (elem, isInner) {
-        var result = elem.offsetTop;
-        if (!isInner) {
-            while (elem = elem.offsetParent) {
-                result += elem.offsetTop;
-            }
-        }
-        return result;
-    };
-
-    position.getHeight = function (elem) {
-        return elem.offsetHeight || elem.scrollHeight;
-    };
-
-    position.getWidth = function (elem) {
-        return elem.offsetWidth || elem.scrollWidth;
-    };
-
-    position.getPageSize = function () {
-
-        var scrollWidth, scrollHeight;
-        var innerWidth, innerHeight;
-
-        // It's not very clear which blocks work with which browsers.
-        if (self.innerHeight && self.scrollMaxY) {
-            scrollWidth = doc.body.scrollWidth;
-            scrollHeight = self.innerHeight + self.scrollMaxY;
-        }
-        else if (doc.body.scrollHeight > doc.body.offsetHeight) {
-            scrollWidth = doc.body.scrollWidth;
-            scrollHeight = doc.body.scrollHeight;
-        }
-        else {
-            scrollWidth = doc.body.offsetWidth;
-            scrollHeight = doc.body.offsetHeight;
-        }
-
-        if (self.innerHeight) {
-            // Non-IE browser
-            innerWidth = self.innerWidth;
-            innerHeight = self.innerHeight;
-        }
-        else if (doc.documentElement && doc.documentElement.clientHeight) {
-            // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
-            innerWidth = doc.documentElement.clientWidth;
-            innerHeight = doc.documentElement.clientHeight;
-        }
-        else if (doc.body) {
-            // Other versions of IE
-            innerWidth = doc.body.clientWidth;
-            innerHeight = doc.body.clientHeight;
-        }
-
-        var maxWidth = Math.max(scrollWidth, innerWidth);
-        var maxHeight = Math.max(scrollHeight, innerHeight);
-        return [maxWidth, maxHeight, innerWidth, innerHeight];
-    };
-
-    // Handles pushing and popping TextareaStates for undo/redo commands.
-    // I should rename the stack variables to list.
-    function UndoManager(callback, panels) {
-
-        var undoObj = this;
-        var undoStack = []; // A stack of undo states
-        var stackPtr = 0; // The index of the current state
-        var mode = "none";
-        var lastState; // The last state
-        var timer; // The setTimeout handle for cancelling the timer
-        var inputStateObj;
-
-        // Set the mode for later logic steps.
-        var setMode = function (newMode, noSave) {
-            if (mode != newMode) {
-                mode = newMode;
-                if (!noSave) {
-                    saveState();
-                }
-            }
-
-            if (!uaSniffed.isIE || mode != "moving") {
-                timer = setTimeout(refreshState, 1);
-            }
-            else {
-                inputStateObj = null;
-            }
-        };
-
-        var refreshState = function (isInitialState) {
-            inputStateObj = new TextareaState(panels, isInitialState);
-            timer = undefined;
-        };
-
-        this.setCommandMode = function () {
-            mode = "command";
-            saveState();
-            timer = setTimeout(refreshState, 0);
-        };
-
-        this.canUndo = function () {
-            return stackPtr > 1;
-        };
-
-        this.canRedo = function () {
-            if (undoStack[stackPtr + 1]) {
-                return true;
-            }
-            return false;
-        };
-
-        // Removes the last state and restores it.
-        this.undo = function () {
-
-            if (undoObj.canUndo()) {
-                if (lastState) {
-                    // What about setting state -1 to null or checking for undefined?
-                    lastState.restore();
-                    lastState = null;
-                }
-                else {
-                    undoStack[stackPtr] = new TextareaState(panels);
-                    undoStack[--stackPtr].restore();
-
-                    if (callback) {
-                        callback();
-                    }
-                }
-            }
-
-            mode = "none";
-            panels.input.focus();
-            refreshState();
-        };
-
-        // Redo an action.
-        this.redo = function () {
-
-            if (undoObj.canRedo()) {
-
-                undoStack[++stackPtr].restore();
-
-                if (callback) {
-                    callback();
-                }
-            }
-
-            mode = "none";
-            panels.input.focus();
-            refreshState();
-        };
-
-        // Push the input area state to the stack.
-        var saveState = function () {
-            var currState = inputStateObj || new TextareaState(panels);
-
-            if (!currState) {
-                return false;
-            }
-            if (mode == "moving") {
-                if (!lastState) {
-                    lastState = currState;
-                }
-                return;
-            }
-            if (lastState) {
-                if (undoStack[stackPtr - 1].text != lastState.text) {
-                    undoStack[stackPtr++] = lastState;
-                }
-                lastState = null;
-            }
-            undoStack[stackPtr++] = currState;
-            undoStack[stackPtr + 1] = null;
-            if (callback) {
-                callback();
-            }
-        };
-
-        var handleCtrlYZ = function (event) {
-
-            var handled = false;
-
-            if (event.ctrlKey || event.metaKey) {
-
-                // IE and Opera do not support charCode.
-                var keyCode = event.charCode || event.keyCode;
-                var keyCodeChar = String.fromCharCode(keyCode);
-
-                switch (keyCodeChar.toLowerCase()) {
-
-                    case "y":
-                        undoObj.redo();
-                        handled = true;
-                        break;
-
-                    case "z":
-                        if (!event.shiftKey) {
-                            undoObj.undo();
-                        }
-                        else {
-                            undoObj.redo();
-                        }
-                        handled = true;
-                        break;
-                }
-            }
-
-            if (handled) {
-                if (event.preventDefault) {
-                    event.preventDefault();
-                }
-                if (window.event) {
-                    window.event.returnValue = false;
-                }
-                return;
-            }
-        };
-
-        // Set the mode depending on what is going on in the input area.
-        var handleModeChange = function (event) {
-
-            if (!event.ctrlKey && !event.metaKey) {
-
-                var keyCode = event.keyCode;
-
-                if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
-                    // 33 - 40: page up/dn and arrow keys
-                    // 63232 - 63235: page up/dn and arrow keys on safari
-                    setMode("moving");
-                }
-                else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
-                    // 8: backspace
-                    // 46: delete
-                    // 127: delete
-                    setMode("deleting");
-                }
-                else if (keyCode == 13) {
-                    // 13: Enter
-                    setMode("newlines");
-                }
-                else if (keyCode == 27) {
-                    // 27: escape
-                    setMode("escape");
-                }
-                else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
-                    // 16-20 are shift, etc.
-                    // 91: left window key
-                    // I think this might be a little messed up since there are
-                    // a lot of nonprinting keys above 20.
-                    setMode("typing");
-                }
-            }
-        };
-
-        var setEventHandlers = function () {
-            util.addEvent(panels.input, "keypress", function (event) {
-                // keyCode 89: y
-                // keyCode 90: z
-                if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
-                    event.preventDefault();
-                }
-            });
-
-            var handlePaste = function () {
-                if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {
-                    if (timer == undefined) {
-                        mode = "paste";
-                        saveState();
-                        refreshState();
-                    }
-                }
-            };
-
-            util.addEvent(panels.input, "keydown", handleCtrlYZ);
-            util.addEvent(panels.input, "keydown", handleModeChange);
-            util.addEvent(panels.input, "mousedown", function () {
-                setMode("moving");
-            });
-
-            panels.input.onpaste = handlePaste;
-            panels.input.ondrop = handlePaste;
-        };
-
-        var init = function () {
-            setEventHandlers();
-            refreshState(true);
-            saveState();
-        };
-
-        init();
-    }
-
-    // end of UndoManager
-
-    // The input textarea state/contents.
-    // This is used to implement undo/redo by the undo manager.
-    function TextareaState(panels, isInitialState) {
-
-        // Aliases
-        var stateObj = this;
-        var inputArea = panels.input;
-        this.init = function () {
-            if (!util.isVisible(inputArea)) {
-                return;
-            }
-            if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box
-                return;
-            }
-
-            this.setInputAreaSelectionStartEnd();
-            this.scrollTop = inputArea.scrollTop;
-            if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
-                this.text = inputArea.value;
-            }
-
-        }
-
-        // Sets the selected text in the input box after we've performed an
-        // operation.
-        this.setInputAreaSelection = function () {
-
-            if (!util.isVisible(inputArea)) {
-                return;
-            }
-
-            if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {
-
-                inputArea.focus();
-                inputArea.selectionStart = stateObj.start;
-                inputArea.selectionEnd = stateObj.end;
-                inputArea.scrollTop = stateObj.scrollTop;
-            }
-            else if (doc.selection) {
-
-                if (doc.activeElement && doc.activeElement !== inputArea) {
-                    return;
-                }
-
-                inputArea.focus();
-                var range = inputArea.createTextRange();
-                range.moveStart("character", -inputArea.value.length);
-                range.moveEnd("character", -inputArea.value.length);
-                range.moveEnd("character", stateObj.end);
-                range.moveStart("character", stateObj.start);
-                range.select();
-            }
-        };
-
-        this.setInputAreaSelectionStartEnd = function () {
-
-            if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) {
-
-                stateObj.start = inputArea.selectionStart;
-                stateObj.end = inputArea.selectionEnd;
-            }
-            else if (doc.selection) {
-
-                stateObj.text = util.fixEolChars(inputArea.value);
-
-                // IE loses the selection in the textarea when buttons are
-                // clicked.  On IE we cache the selection. Here, if something is cached,
-                // we take it.
-                var range = panels.ieCachedRange || doc.selection.createRange();
-
-                var fixedRange = util.fixEolChars(range.text);
-                var marker = "\x07";
-                var markedRange = marker + fixedRange + marker;
-                range.text = markedRange;
-                var inputText = util.fixEolChars(inputArea.value);
-
-                range.moveStart("character", -markedRange.length);
-                range.text = fixedRange;
-
-                stateObj.start = inputText.indexOf(marker);
-                stateObj.end = inputText.lastIndexOf(marker) - marker.length;
-
-                var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
-
-                if (len) {
-                    range.moveStart("character", -fixedRange.length);
-                    while (len--) {
-                        fixedRange += "\n";
-                        stateObj.end += 1;
-                    }
-                    range.text = fixedRange;
-                }
-
-                if (panels.ieCachedRange)
-                    stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange
-
-                panels.ieCachedRange = null;
-
-                this.setInputAreaSelection();
-            }
-        };
-
-        // Restore this state into the input area.
-        this.restore = function () {
-
-            if (stateObj.text != undefined && stateObj.text != inputArea.value) {
-                inputArea.value = stateObj.text;
-            }
-            this.setInputAreaSelection();
-            inputArea.scrollTop = stateObj.scrollTop;
-        };
-
-        // Gets a collection of HTML chunks from the inptut textarea.
-        this.getChunks = function () {
-
-            var chunk = new Chunks();
-            chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
-            chunk.startTag = "";
-            chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
-            chunk.endTag = "";
-            chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
-            chunk.scrollTop = stateObj.scrollTop;
-
-            return chunk;
-        };
-
-        // Sets the TextareaState properties given a chunk of markdown.
-        this.setChunks = function (chunk) {
-
-            chunk.before = chunk.before + chunk.startTag;
-            chunk.after = chunk.endTag + chunk.after;
-
-            this.start = chunk.before.length;
-            this.end = chunk.before.length + chunk.selection.length;
-            this.text = chunk.before + chunk.selection + chunk.after;
-            this.scrollTop = chunk.scrollTop;
-        };
-        this.init();
-    };
-
-    function PreviewManager(converter, panels, previewRefreshCallback) {
-
-        var managerObj = this;
-        var timeout;
-        var elapsedTime;
-        var oldInputText;
-        var maxDelay = 3000;
-        var startType = "delayed"; // The other legal value is "manual"
-
-        // Adds event listeners to elements
-        var setupEvents = function (inputElem, listener) {
-
-            util.addEvent(inputElem, "input", listener);
-            inputElem.onpaste = listener;
-            inputElem.ondrop = listener;
-
-            util.addEvent(inputElem, "keypress", listener);
-            util.addEvent(inputElem, "keydown", listener);
-        };
-
-        var getDocScrollTop = function () {
-
-            var result = 0;
-
-            if (window.innerHeight) {
-                result = window.pageYOffset;
-            }
-            else
-                if (doc.documentElement && doc.documentElement.scrollTop) {
-                    result = doc.documentElement.scrollTop;
-                }
-                else
-                    if (doc.body) {
-                        result = doc.body.scrollTop;
-                    }
-
-            return result;
-        };
-
-        var makePreviewHtml = function () {
-
-            // If there is no registered preview panel
-            // there is nothing to do.
-            if (!panels.preview)
-                return;
-
-            var text = panels.input.value;
-            if (text && text == oldInputText) {
-                return; // Input text hasn't changed.
-            }
-            else {
-                oldInputText = text;
-            }
-
-            var prevTime = new Date().getTime();
-
-            text = converter.makeHtml(text);
-
-            // Calculate the processing time of the HTML creation.
-            // It's used as the delay time in the event listener.
-            var currTime = new Date().getTime();
-            elapsedTime = currTime - prevTime;
-
-            pushPreviewHtml(text);
-        };
-
-        // setTimeout is already used.  Used as an event listener.
-        var applyTimeout = function () {
-
-            if (timeout) {
-                clearTimeout(timeout);
-                timeout = undefined;
-            }
-
-            if (startType !== "manual") {
-
-                var delay = 0;
-
-                if (startType === "delayed") {
-                    delay = elapsedTime;
-                }
-
-                if (delay > maxDelay) {
-                    delay = maxDelay;
-                }
-                timeout = setTimeout(makePreviewHtml, delay);
-            }
-        };
-
-        var getScaleFactor = function (panel) {
-            if (panel.scrollHeight <= panel.clientHeight) {
-                return 1;
-            }
-            return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
-        };
-
-        var setPanelScrollTops = function () {
-            if (panels.preview) {
-                panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
-            }
-        };
-
-        this.refresh = function (requiresRefresh) {
-
-            if (requiresRefresh) {
-                oldInputText = "";
-                makePreviewHtml();
-            }
-            else {
-                applyTimeout();
-            }
-        };
-
-        this.processingTime = function () {
-            return elapsedTime;
-        };
-
-        var isFirstTimeFilled = true;
-
-        // IE doesn't let you use innerHTML if the element is contained somewhere in a table
-        // (which is the case for inline editing) -- in that case, detach the element, set the
-        // value, and reattach. Yes, that *is* ridiculous.
-        var ieSafePreviewSet = function (text) {
-            var preview = panels.preview;
-            var parent = preview.parentNode;
-            var sibling = preview.nextSibling;
-            parent.removeChild(preview);
-            preview.innerHTML = text;
-            if (!sibling)
-                parent.appendChild(preview);
-            else
-                parent.insertBefore(preview, sibling);
-        }
-
-        var nonSuckyBrowserPreviewSet = function (text) {
-            panels.preview.innerHTML = text;
-        }
-
-        var previewSetter;
-
-        var previewSet = function (text) {
-            if (previewSetter)
-                return previewSetter(text);
-
-            try {
-                nonSuckyBrowserPreviewSet(text);
-                previewSetter = nonSuckyBrowserPreviewSet;
-            } catch (e) {
-                previewSetter = ieSafePreviewSet;
-                previewSetter(text);
-            }
-        };
-
-        var pushPreviewHtml = function (text) {
-
-            var emptyTop = position.getTop(panels.input) - getDocScrollTop();
-
-            if (panels.preview) {
-                previewSet(text);
-                previewRefreshCallback();
-            }
-
-            setPanelScrollTops();
-
-            if (isFirstTimeFilled) {
-                isFirstTimeFilled = false;
-                return;
-            }
-
-            var fullTop = position.getTop(panels.input) - getDocScrollTop();
-
-            if (uaSniffed.isIE) {
-                setTimeout(function () {
-                    window.scrollBy(0, fullTop - emptyTop);
-                }, 0);
-            }
-            else {
-                window.scrollBy(0, fullTop - emptyTop);
-            }
-        };
-
-        var init = function () {
-
-            setupEvents(panels.input, applyTimeout);
-            makePreviewHtml();
-
-            if (panels.preview) {
-                panels.preview.scrollTop = 0;
-            }
-        };
-
-        init();
-    };
-
-    // Creates the background behind the hyperlink text entry box.
-    // And download dialog
-    // Most of this has been moved to CSS but the div creation and
-    // browser-specific hacks remain here.
-    ui.createBackground = function () {
-
-        var background = doc.createElement("div"),
-            style = background.style;
-        
-        background.className = "wmd-prompt-background";
-        
-        style.position = "absolute";
-        style.top = "0";
-
-        style.zIndex = "1000";
-
-        if (uaSniffed.isIE) {
-            style.filter = "alpha(opacity=50)";
-        }
-        else {
-            style.opacity = "0.5";
-        }
-
-        var pageSize = position.getPageSize();
-        style.height = pageSize[1] + "px";
-
-        if (uaSniffed.isIE) {
-            style.left = doc.documentElement.scrollLeft;
-            style.width = doc.documentElement.clientWidth;
-        }
-        else {
-            style.left = "0";
-            style.width = "100%";
-        }
-
-        doc.body.appendChild(background);
-        return background;
-    };
-
-    // This simulates a modal dialog box and asks for the URL when you
-    // click the hyperlink or image buttons.
-    //
-    // text: The html for the input box.
-    // defaultInputText: The default value that appears in the input box.
-    // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
-    //      It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
-    //      was chosen).
-    ui.prompt = function (text, defaultInputText, callback) {
-
-        // These variables need to be declared at this level since they are used
-        // in multiple functions.
-        var dialog;         // The dialog box.
-        var input;         // The text box where you enter the hyperlink.
-
-
-        if (defaultInputText === undefined) {
-            defaultInputText = "";
-        }
-
-        // Used as a keydown event handler. Esc dismisses the prompt.
-        // Key code 27 is ESC.
-        var checkEscape = function (key) {
-            var code = (key.charCode || key.keyCode);
-            if (code === 27) {
-                close(true);
-            }
-        };
-
-        // Dismisses the hyperlink input box.
-        // isCancel is true if we don't care about the input text.
-        // isCancel is false if we are going to keep the text.
-        var close = function (isCancel) {
-            util.removeEvent(doc.body, "keydown", checkEscape);
-            var text = input.value;
-
-            if (isCancel) {
-                text = null;
-            }
-            else {
-                // Fixes common pasting errors.
-                text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
-                if (!/^(?:https?|ftp):\/\//.test(text))
-                    text = 'http://' + text;
-            }
-
-            dialog.parentNode.removeChild(dialog);
-
-            callback(text);
-            return false;
-        };
-
-
-
-        // Create the text input box form/window.
-        var createDialog = function () {
-
-            // The main dialog box.
-            dialog = doc.createElement("div");
-            dialog.className = "wmd-prompt-dialog";
-            dialog.style.padding = "10px;";
-            dialog.style.position = "fixed";
-            dialog.style.width = "400px";
-            dialog.style.zIndex = "1001";
-
-            // The dialog text.
-            var question = doc.createElement("div");
-            question.innerHTML = text;
-            question.style.padding = "5px";
-            dialog.appendChild(question);
-
-            // The web form container for the text box and buttons.
-            var form = doc.createElement("form"),
-                style = form.style;
-            form.onsubmit = function () { return close(false); };
-            style.padding = "0";
-            style.margin = "0";
-            style.cssFloat = "left";
-            style.width = "100%";
-            style.textAlign = "center";
-            style.position = "relative";
-            dialog.appendChild(form);
-
-            // The input text box
-            input = doc.createElement("input");
-            input.type = "text";
-            input.value = defaultInputText;
-            style = input.style;
-            style.display = "block";
-            style.width = "80%";
-            style.marginLeft = style.marginRight = "auto";
-            form.appendChild(input);
-
-            // The ok button
-            var okButton = doc.createElement("input");
-            okButton.type = "button";
-            okButton.onclick = function () { return close(false); };
-            okButton.value = "OK";
-            style = okButton.style;
-            style.margin = "10px";
-            style.display = "inline";
-            style.width = "7em";
-
-
-            // The cancel button
-            var cancelButton = doc.createElement("input");
-            cancelButton.type = "button";
-            cancelButton.onclick = function () { return close(true); };
-            cancelButton.value = "Cancel";
-            style = cancelButton.style;
-            style.margin = "10px";
-            style.display = "inline";
-            style.width = "7em";
-
-            form.appendChild(okButton);
-            form.appendChild(cancelButton);
-
-            util.addEvent(doc.body, "keydown", checkEscape);
-            dialog.style.top = "50%";
-            dialog.style.left = "50%";
-            dialog.style.display = "block";
-            if (uaSniffed.isIE_5or6) {
-                dialog.style.position = "absolute";
-                dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
-                dialog.style.left = "50%";
-            }
-            doc.body.appendChild(dialog);
-
-            // This has to be done AFTER adding the dialog to the form if you
-            // want it to be centered.
-            dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
-            dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
-
-        };
-
-        // Why is this in a zero-length timeout?
-        // Is it working around a browser bug?
-        setTimeout(function () {
-
-            createDialog();
-
-            var defTextLen = defaultInputText.length;
-            if (input.selectionStart !== undefined) {
-                input.selectionStart = 0;
-                input.selectionEnd = defTextLen;
-            }
-            else if (input.createTextRange) {
-                var range = input.createTextRange();
-                range.collapse(false);
-                range.moveStart("character", -defTextLen);
-                range.moveEnd("character", defTextLen);
-                range.select();
-            }
-
-            input.focus();
-        }, 0);
-    };
-
-    function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) {
-
-        var inputBox = panels.input,
-            buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
-
-        makeSpritedButtonRow();
-
-        var keyEvent = "keydown";
-        if (uaSniffed.isOpera) {
-            keyEvent = "keypress";
-        }
-
-        util.addEvent(inputBox, keyEvent, function (key) {
-
-            // Check to see if we have a button key and, if so execute the callback.
-            if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
-
-                var keyCode = key.charCode || key.keyCode;
-                var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
-
-                switch (keyCodeStr) {
-                    case "b":
-                        doClick(buttons.bold);
-                        break;
-                    case "i":
-                        doClick(buttons.italic);
-                        break;
-                    case "l":
-                        doClick(buttons.link);
-                        break;
-                    case "q":
-                        doClick(buttons.quote);
-                        break;
-                    case "k":
-                        doClick(buttons.code);
-                        break;
-                    case "g":
-                        doClick(buttons.image);
-                        break;
-                    case "o":
-                        doClick(buttons.olist);
-                        break;
-                    case "u":
-                        doClick(buttons.ulist);
-                        break;
-                    case "h":
-                        doClick(buttons.heading);
-                        break;
-                    case "r":
-                        doClick(buttons.hr);
-                        break;
-                    case "y":
-                        doClick(buttons.redo);
-                        break;
-                    case "z":
-                        if (key.shiftKey) {
-                            doClick(buttons.redo);
-                        }
-                        else {
-                            doClick(buttons.undo);
-                        }
-                        break;
-                    default:
-                        return;
-                }
-
-
-                if (key.preventDefault) {
-                    key.preventDefault();
-                }
-
-                if (window.event) {
-                    window.event.returnValue = false;
-                }
-            }
-        });
-
-        // Auto-indent on shift-enter
-        util.addEvent(inputBox, "keyup", function (key) {
-            if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
-                var keyCode = key.charCode || key.keyCode;
-                // Character 13 is Enter
-                if (keyCode === 13) {
-                    var fakeButton = {};
-                    fakeButton.textOp = bindCommand("doAutoindent");
-                    doClick(fakeButton);
-                }
-            }
-        });
-
-        // special handler because IE clears the context of the textbox on ESC
-        if (uaSniffed.isIE) {
-            util.addEvent(inputBox, "keydown", function (key) {
-                var code = key.keyCode;
-                if (code === 27) {
-                    return false;
-                }
-            });
-        }
-
-
-        // Perform the button's action.
-        function doClick(button) {
-
-            inputBox.focus();
-
-            if (button.textOp) {
-
-                if (undoManager) {
-                    undoManager.setCommandMode();
-                }
-
-                var state = new TextareaState(panels);
-
-                if (!state) {
-                    return;
-                }
-
-                var chunks = state.getChunks();
-
-                // Some commands launch a "modal" prompt dialog.  Javascript
-                // can't really make a modal dialog box and the WMD code
-                // will continue to execute while the dialog is displayed.
-                // This prevents the dialog pattern I'm used to and means
-                // I can't do something like this:
-                //
-                // var link = CreateLinkDialog();
-                // makeMarkdownLink(link);
-                //
-                // Instead of this straightforward method of handling a
-                // dialog I have to pass any code which would execute
-                // after the dialog is dismissed (e.g. link creation)
-                // in a function parameter.
-                //
-                // Yes this is awkward and I think it sucks, but there's
-                // no real workaround.  Only the image and link code
-                // create dialogs and require the function pointers.
-                var fixupInputArea = function () {
-
-                    inputBox.focus();
-
-                    if (chunks) {
-                        state.setChunks(chunks);
-                    }
-
-                    state.restore();
-                    previewManager.refresh();
-                };
-
-                var noCleanup = button.textOp(chunks, fixupInputArea);
-
-                if (!noCleanup) {
-                    fixupInputArea();
-                }
-
-            }
-
-            if (button.execute) {
-                button.execute(undoManager);
-            }
-        };
-
-        function setupButton(button, isEnabled) {
-
-            var normalYShift = "0px";
-            var disabledYShift = "-20px";
-            var highlightYShift = "-40px";
-            var image = button.getElementsByTagName("span")[0];
-            if (isEnabled) {
-                image.style.backgroundPosition = button.XShift + " " + normalYShift;
-                button.onmouseover = function () {
-                    image.style.backgroundPosition = this.XShift + " " + highlightYShift;
-                };
-
-                button.onmouseout = function () {
-                    image.style.backgroundPosition = this.XShift + " " + normalYShift;
-                };
-
-                // IE tries to select the background image "button" text (it's
-                // implemented in a list item) so we have to cache the selection
-                // on mousedown.
-                if (uaSniffed.isIE) {
-                    button.onmousedown = function () {
-                        if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection
-                            return;
-                        }
-                        panels.ieCachedRange = document.selection.createRange();
-                        panels.ieCachedScrollTop = panels.input.scrollTop;
-                    };
-                }
-
-                if (!button.isHelp) {
-                    button.onclick = function () {
-                        if (this.onmouseout) {
-                            this.onmouseout();
-                        }
-                        doClick(this);
-                        return false;
-                    }
-                }
-            }
-            else {
-                image.style.backgroundPosition = button.XShift + " " + disabledYShift;
-                button.onmouseover = button.onmouseout = button.onclick = function () { };
-            }
-        }
-
-        function bindCommand(method) {
-            if (typeof method === "string")
-                method = commandManager[method];
-            return function () { method.apply(commandManager, arguments); }
-        }
-
-        function makeSpritedButtonRow() {
-
-            var buttonBar = panels.buttonBar;
-
-            var normalYShift = "0px";
-            var disabledYShift = "-20px";
-            var highlightYShift = "-40px";
-
-            var buttonRow = document.createElement("ul");
-            buttonRow.id = "wmd-button-row" + postfix;
-            buttonRow.className = 'wmd-button-row';
-            buttonRow = buttonBar.appendChild(buttonRow);
-            var xPosition = 0;
-            var makeButton = function (id, title, XShift, textOp) {
-                var button = document.createElement("li");
-                button.className = "wmd-button";
-                button.style.left = xPosition + "px";
-                xPosition += 25;
-                var buttonImage = document.createElement("span");
-                button.id = id + postfix;
-                button.appendChild(buttonImage);
-                button.title = title;
-                button.XShift = XShift;
-                if (textOp)
-                    button.textOp = textOp;
-                setupButton(button, true);
-                buttonRow.appendChild(button);
-                return button;
-            };
-            var makeSpacer = function (num) {
-                var spacer = document.createElement("li");
-                spacer.className = "wmd-spacer wmd-spacer" + num;
-                spacer.id = "wmd-spacer" + num + postfix;
-                buttonRow.appendChild(spacer);
-                xPosition += 25;
-            }
-
-            buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold"));
-            buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic"));
-            makeSpacer(1);
-            buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function (chunk, postProcessing) {
-                return this.doLinkOrImage(chunk, postProcessing, false);
-            }));
-            buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote"));
-            buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode"));
-            buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function (chunk, postProcessing) {
-                return this.doLinkOrImage(chunk, postProcessing, true);
-            }));
-            makeSpacer(2);
-            buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function (chunk, postProcessing) {
-                this.doList(chunk, postProcessing, true);
-            }));
-            buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function (chunk, postProcessing) {
-                this.doList(chunk, postProcessing, false);
-            }));
-            buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading"));
-            buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule"));
-            makeSpacer(3);
-            buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null);
-            buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
-
-            var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
-                getString("redo") :
-                getString("redomac"); // mac and other non-Windows platforms
-
-            buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);
-            buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
-
-            if (helpOptions) {
-                var helpButton = document.createElement("li");
-                var helpButtonImage = document.createElement("span");
-                helpButton.appendChild(helpButtonImage);
-                helpButton.className = "wmd-button wmd-help-button";
-                helpButton.id = "wmd-help-button" + postfix;
-                helpButton.XShift = "-240px";
-                helpButton.isHelp = true;
-                helpButton.style.right = "0px";
-                helpButton.title = getString("help");
-                helpButton.onclick = helpOptions.handler;
-
-                setupButton(helpButton, true);
-                buttonRow.appendChild(helpButton);
-                buttons.help = helpButton;
-            }
-
-            setUndoRedoButtonStates();
-        }
-
-        function setUndoRedoButtonStates() {
-            if (undoManager) {
-                setupButton(buttons.undo, undoManager.canUndo());
-                setupButton(buttons.redo, undoManager.canRedo());
-            }
-        };
-
-        this.setUndoRedoButtonStates = setUndoRedoButtonStates;
-
-    }
-
-    function CommandManager(pluginHooks, getString) {
-        this.hooks = pluginHooks;
-        this.getString = getString;
-    }
-
-    var commandProto = CommandManager.prototype;
-
-    // The markdown symbols - 4 spaces = code, > = blockquote, etc.
-    commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
-
-    // Remove markdown symbols from the chunk selection.
-    commandProto.unwrap = function (chunk) {
-        var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
-        chunk.selection = chunk.selection.replace(txt, "$1 $2");
-    };
-
-    commandProto.wrap = function (chunk, len) {
-        this.unwrap(chunk);
-        var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"),
-            that = this;
-
-        chunk.selection = chunk.selection.replace(regex, function (line, marked) {
-            if (new re("^" + that.prefixes, "").test(line)) {
-                return line;
-            }
-            return marked + "\n";
-        });
-
-        chunk.selection = chunk.selection.replace(/\s+$/, "");
-    };
-
-    commandProto.doBold = function (chunk, postProcessing) {
-        return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample"));
-    };
-
-    commandProto.doItalic = function (chunk, postProcessing) {
-        return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample"));
-    };
-
-    // chunk: The selected region that will be enclosed with */**
-    // nStars: 1 for italics, 2 for bold
-    // insertText: If you just click the button without highlighting text, this gets inserted
-    commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
-
-        // Get rid of whitespace and fixup newlines.
-        chunk.trimWhitespace();
-        chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
-
-        // Look for stars before and after.  Is the chunk already marked up?
-        // note that these regex matches cannot fail
-        var starsBefore = /(\**$)/.exec(chunk.before)[0];
-        var starsAfter = /(^\**)/.exec(chunk.after)[0];
-
-        var prevStars = Math.min(starsBefore.length, starsAfter.length);
-
-        // Remove stars if we have to since the button acts as a toggle.
-        if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
-            chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
-            chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
-        }
-        else if (!chunk.selection && starsAfter) {
-            // It's not really clear why this code is necessary.  It just moves
-            // some arbitrary stuff around.
-            chunk.after = chunk.after.replace(/^([*_]*)/, "");
-            chunk.before = chunk.before.replace(/(\s?)$/, "");
-            var whitespace = re.$1;
-            chunk.before = chunk.before + starsAfter + whitespace;
-        }
-        else {
-
-            // In most cases, if you don't have any selected text and click the button
-            // you'll get a selected, marked up region with the default text inserted.
-            if (!chunk.selection && !starsAfter) {
-                chunk.selection = insertText;
-            }
-
-            // Add the true markup.
-            var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
-            chunk.before = chunk.before + markup;
-            chunk.after = markup + chunk.after;
-        }
-
-        return;
-    };
-
-    commandProto.stripLinkDefs = function (text, defsToAdd) {
-
-        text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
-            function (totalMatch, id, link, newlines, title) {
-                defsToAdd[id] = totalMatch.replace(/\s*$/, "");
-                if (newlines) {
-                    // Strip the title and return that separately.
-                    defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
-                    return newlines + title;
-                }
-                return "";
-            });
-
-        return text;
-    };
-
-    commandProto.addLinkDef = function (chunk, linkDef) {
-
-        var refNumber = 0; // The current reference number
-        var defsToAdd = {}; //
-        // Start with a clean slate by removing all previous link definitions.
-        chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
-        chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
-        chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
-
-        var defs = "";
-        var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
-
-        var addDefNumber = function (def) {
-            refNumber++;
-            def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");
-            defs += "\n" + def;
-        };
-
-        // note that
-        // a) the recursive call to getLink cannot go infinite, because by definition
-        //    of regex, inner is always a proper substring of wholeMatch, and
-        // b) more than one level of nesting is neither supported by the regex
-        //    nor making a lot of sense (the only use case for nesting is a linked image)
-        var getLink = function (wholeMatch, before, inner, afterInner, id, end) {
-            inner = inner.replace(regex, getLink);
-            if (defsToAdd[id]) {
-                addDefNumber(defsToAdd[id]);
-                return before + inner + afterInner + refNumber + end;
-            }
-            return wholeMatch;
-        };
-
-        chunk.before = chunk.before.replace(regex, getLink);
-
-        if (linkDef) {
-            addDefNumber(linkDef);
-        }
-        else {
-            chunk.selection = chunk.selection.replace(regex, getLink);
-        }
-
-        var refOut = refNumber;
-
-        chunk.after = chunk.after.replace(regex, getLink);
-
-        if (chunk.after) {
-            chunk.after = chunk.after.replace(/\n*$/, "");
-        }
-        if (!chunk.after) {
-            chunk.selection = chunk.selection.replace(/\n*$/, "");
-        }
-
-        chunk.after += "\n\n" + defs;
-
-        return refOut;
-    };
-
-    // takes the line as entered into the add link/as image dialog and makes
-    // sure the URL and the optinal title are "nice".
-    function properlyEncoded(linkdef) {
-        return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
-            link = link.replace(/\?.*$/, function (querypart) {
-                return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical
-            });
-            link = decodeURIComponent(link); // unencode first, to prevent double encoding
-            link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
-            link = link.replace(/\?.*$/, function (querypart) {
-                return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded
-            });
-            if (title) {
-                title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
-                title = title.replace(/"/g, "quot;").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
-            }
-            return title ? link + ' "' + title + '"' : link;
-        });
-    }
-
-    commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
-
-        chunk.trimWhitespace();
-        chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
-        var background;
-
-        if (chunk.endTag.length > 1 && chunk.startTag.length > 0) {
-
-            chunk.startTag = chunk.startTag.replace(/!?\[/, "");
-            chunk.endTag = "";
-            this.addLinkDef(chunk, null);
-
-        }
-        else {
-            
-            // We're moving start and end tag back into the selection, since (as we're in the else block) we're not
-            // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
-            // link text. linkEnteredCallback takes care of escaping any brackets.
-            chunk.selection = chunk.startTag + chunk.selection + chunk.endTag;
-            chunk.startTag = chunk.endTag = "";
-
-            if (/\n\n/.test(chunk.selection)) {
-                this.addLinkDef(chunk, null);
-                return;
-            }
-            var that = this;
-            // The function to be executed when you enter a link and press OK or Cancel.
-            // Marks up the link and adds the ref.
-            var linkEnteredCallback = function (link) {
-
-                background.parentNode.removeChild(background);
-
-                if (link !== null) {
-                    // (                          $1
-                    //     [^\\]                  anything that's not a backslash
-                    //     (?:\\\\)*              an even number (this includes zero) of backslashes
-                    // )
-                    // (?=                        followed by
-                    //     [[\]]                  an opening or closing bracket
-                    // )
-                    //
-                    // In other words, a non-escaped bracket. These have to be escaped now to make sure they
-                    // don't count as the end of the link or similar.
-                    // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets),
-                    // the bracket in one match may be the "not a backslash" character in the next match, so it
-                    // should not be consumed by the first match.
-                    // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the
-                    // start of the string, so this also works if the selection begins with a bracket. We cannot solve
-                    // this by anchoring with ^, because in the case that the selection starts with two brackets, this
-                    // would mean a zero-width match at the start. Since zero-width matches advance the string position,
-                    // the first bracket could then not act as the "not a backslash" for the second.
-                    chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
-                    
-                    var linkDef = " [999]: " + properlyEncoded(link);
-
-                    var num = that.addLinkDef(chunk, linkDef);
-                    chunk.startTag = isImage ? "![" : "[";
-                    chunk.endTag = "][" + num + "]";
-
-                    if (!chunk.selection) {
-                        if (isImage) {
-                            chunk.selection = that.getString("imagedescription");
-                        }
-                        else {
-                            chunk.selection = that.getString("linkdescription");
-                        }
-                    }
-                }
-                postProcessing();
-            };
-
-            background = ui.createBackground();
-
-            if (isImage) {
-                if (!this.hooks.insertImageDialog(linkEnteredCallback))
-                    ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback);
-            }
-            else {
-                ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback);
-            }
-            return true;
-        }
-    };
-
-    // When making a list, hitting shift-enter will put your cursor on the next line
-    // at the current indent level.
-    commandProto.doAutoindent = function (chunk, postProcessing) {
-
-        var commandMgr = this,
-            fakeSelection = false;
-
-        chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
-        chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
-        chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
-        
-        // There's no selection, end the cursor wasn't at the end of the line:
-        // The user wants to split the current list item / code line / blockquote line
-        // (for the latter it doesn't really matter) in two. Temporarily select the
-        // (rest of the) line to achieve this.
-        if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
-            chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
-                chunk.selection = wholeMatch;
-                return "";
-            });
-            fakeSelection = true;
-        }
-
-        if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
-            if (commandMgr.doList) {
-                commandMgr.doList(chunk);
-            }
-        }
-        if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
-            if (commandMgr.doBlockquote) {
-                commandMgr.doBlockquote(chunk);
-            }
-        }
-        if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
-            if (commandMgr.doCode) {
-                commandMgr.doCode(chunk);
-            }
-        }
-        
-        if (fakeSelection) {
-            chunk.after = chunk.selection + chunk.after;
-            chunk.selection = "";
-        }
-    };
-
-    commandProto.doBlockquote = function (chunk, postProcessing) {
-
-        chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
-            function (totalMatch, newlinesBefore, text, newlinesAfter) {
-                chunk.before += newlinesBefore;
-                chunk.after = newlinesAfter + chunk.after;
-                return text;
-            });
-
-        chunk.before = chunk.before.replace(/(>[ \t]*)$/,
-            function (totalMatch, blankLine) {
-                chunk.selection = blankLine + chunk.selection;
-                return "";
-            });
-
-        chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
-        chunk.selection = chunk.selection || this.getString("quoteexample");
-
-        // The original code uses a regular expression to find out how much of the
-        // text *directly before* the selection already was a blockquote:
-
-        /*
-        if (chunk.before) {
-        chunk.before = chunk.before.replace(/\n?$/, "\n");
-        }
-        chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
-        function (totalMatch) {
-        chunk.startTag = totalMatch;
-        return "";
-        });
-        */
-
-        // This comes down to:
-        // Go backwards as many lines a possible, such that each line
-        //  a) starts with ">", or
-        //  b) is almost empty, except for whitespace, or
-        //  c) is preceeded by an unbroken chain of non-empty lines
-        //     leading up to a line that starts with ">" and at least one more character
-        // and in addition
-        //  d) at least one line fulfills a)
-        //
-        // Since this is essentially a backwards-moving regex, it's susceptible to
-        // catstrophic backtracking and can cause the browser to hang;
-        // see e.g. http://meta.stackoverflow.com/questions/9807.
-        //
-        // Hence we replaced this by a simple state machine that just goes through the
-        // lines and checks for a), b), and c).
-
-        var match = "",
-            leftOver = "",
-            line;
-        if (chunk.before) {
-            var lines = chunk.before.replace(/\n$/, "").split("\n");
-            var inChain = false;
-            for (var i = 0; i < lines.length; i++) {
-                var good = false;
-                line = lines[i];
-                inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
-                if (/^>/.test(line)) {                // a)
-                    good = true;
-                    if (!inChain && line.length > 1)  // c) any line that starts with ">" and has at least one more character starts the chain
-                        inChain = true;
-                } else if (/^[ \t]*$/.test(line)) {   // b)
-                    good = true;
-                } else {
-                    good = inChain;                   // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain
-                }
-                if (good) {
-                    match += line + "\n";
-                } else {
-                    leftOver += match + line;
-                    match = "\n";
-                }
-            }
-            if (!/(^|\n)>/.test(match)) {             // d)
-                leftOver += match;
-                match = "";
-            }
-        }
-
-        chunk.startTag = match;
-        chunk.before = leftOver;
-
-        // end of change
-
-        if (chunk.after) {
-            chunk.after = chunk.after.replace(/^\n?/, "\n");
-        }
-
-        chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
-            function (totalMatch) {
-                chunk.endTag = totalMatch;
-                return "";
-            }
-        );
-
-        var replaceBlanksInTags = function (useBracket) {
-
-            var replacement = useBracket ? "> " : "";
-
-            if (chunk.startTag) {
-                chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
-                    function (totalMatch, markdown) {
-                        return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
-                    });
-            }
-            if (chunk.endTag) {
-                chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
-                    function (totalMatch, markdown) {
-                        return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
-                    });
-            }
-        };
-
-        if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
-            this.wrap(chunk, SETTINGS.lineLength - 2);
-            chunk.selection = chunk.selection.replace(/^/gm, "> ");
-            replaceBlanksInTags(true);
-            chunk.skipLines();
-        } else {
-            chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
-            this.unwrap(chunk);
-            replaceBlanksInTags(false);
-
-            if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
-                chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
-            }
-
-            if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
-                chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
-            }
-        }
-
-        chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
-
-        if (!/\n/.test(chunk.selection)) {
-            chunk.selection = chunk.selection.replace(/^(> *)/,
-            function (wholeMatch, blanks) {
-                chunk.startTag += blanks;
-                return "";
-            });
-        }
-    };
-
-    commandProto.doCode = function (chunk, postProcessing) {
-
-        var hasTextBefore = /\S[ ]*$/.test(chunk.before);
-        var hasTextAfter = /^[ ]*\S/.test(chunk.after);
-
-        // Use 'four space' markdown if the selection is on its own
-        // line or is multiline.
-        if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
-
-            chunk.before = chunk.before.replace(/[ ]{4}$/,
-                function (totalMatch) {
-                    chunk.selection = totalMatch + chunk.selection;
-                    return "";
-                });
-
-            var nLinesBack = 1;
-            var nLinesForward = 1;
-
-            if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
-                nLinesBack = 0;
-            }
-            if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
-                nLinesForward = 0;
-            }
-
-            chunk.skipLines(nLinesBack, nLinesForward);
-
-            if (!chunk.selection) {
-                chunk.startTag = "    ";
-                chunk.selection = this.getString("codeexample");
-            }
-            else {
-                if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
-                    if (/\n/.test(chunk.selection))
-                        chunk.selection = chunk.selection.replace(/^/gm, "    ");
-                    else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
-                        chunk.before += "    ";
-                }
-                else {
-                    chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
-                }
-            }
-        }
-        else {
-            // Use backticks (`) to delimit the code block.
-
-            chunk.trimWhitespace();
-            chunk.findTags(/`/, /`/);
-
-            if (!chunk.startTag && !chunk.endTag) {
-                chunk.startTag = chunk.endTag = "`";
-                if (!chunk.selection) {
-                    chunk.selection = this.getString("codeexample");
-                }
-            }
-            else if (chunk.endTag && !chunk.startTag) {
-                chunk.before += chunk.endTag;
-                chunk.endTag = "";
-            }
-            else {
-                chunk.startTag = chunk.endTag = "";
-            }
-        }
-    };
-
-    commandProto.doList = function (chunk, postProcessing, isNumberedList) {
-
-        // These are identical except at the very beginning and end.
-        // Should probably use the regex extension function to make this clearer.
-        var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
-        var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
-
-        // The default bullet is a dash but others are possible.
-        // This has nothing to do with the particular HTML bullet,
-        // it's just a markdown bullet.
-        var bullet = "-";
-
-        // The number in a numbered list.
-        var num = 1;
-
-        // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
-        var getItemPrefix = function () {
-            var prefix;
-            if (isNumberedList) {
-                prefix = " " + num + ". ";
-                num++;
-            }
-            else {
-                prefix = " " + bullet + " ";
-            }
-            return prefix;
-        };
-
-        // Fixes the prefixes of the other list items.
-        var getPrefixedItem = function (itemText) {
-
-            // The numbering flag is unset when called by autoindent.
-            if (isNumberedList === undefined) {
-                isNumberedList = /^\s*\d/.test(itemText);
-            }
-
-            // Renumber/bullet the list element.
-            itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
-                function (_) {
-                    return getItemPrefix();
-                });
-
-            return itemText;
-        };
-
-        chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
-
-        if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
-            chunk.before += chunk.startTag;
-            chunk.startTag = "";
-        }
-
-        if (chunk.startTag) {
-
-            var hasDigits = /\d+[.]/.test(chunk.startTag);
-            chunk.startTag = "";
-            chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
-            this.unwrap(chunk);
-            chunk.skipLines();
-
-            if (hasDigits) {
-                // Have to renumber the bullet points if this is a numbered list.
-                chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
-            }
-            if (isNumberedList == hasDigits) {
-                return;
-            }
-        }
-
-        var nLinesUp = 1;
-
-        chunk.before = chunk.before.replace(previousItemsRegex,
-            function (itemText) {
-                if (/^\s*([*+-])/.test(itemText)) {
-                    bullet = re.$1;
-                }
-                nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
-                return getPrefixedItem(itemText);
-            });
-
-        if (!chunk.selection) {
-            chunk.selection = this.getString("litem");
-        }
-
-        var prefix = getItemPrefix();
-
-        var nLinesDown = 1;
-
-        chunk.after = chunk.after.replace(nextItemsRegex,
-            function (itemText) {
-                nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
-                return getPrefixedItem(itemText);
-            });
-
-        chunk.trimWhitespace(true);
-        chunk.skipLines(nLinesUp, nLinesDown, true);
-        chunk.startTag = prefix;
-        var spaces = prefix.replace(/./g, " ");
-        this.wrap(chunk, SETTINGS.lineLength - spaces.length);
-        chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
-
-    };
-
-    commandProto.doHeading = function (chunk, postProcessing) {
-
-        // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
-        chunk.selection = chunk.selection.replace(/\s+/g, " ");
-        chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
-
-        // If we clicked the button with no selected text, we just
-        // make a level 2 hash header around some default text.
-        if (!chunk.selection) {
-            chunk.startTag = "## ";
-            chunk.selection = this.getString("headingexample");
-            chunk.endTag = " ##";
-            return;
-        }
-
-        var headerLevel = 0;     // The existing header level of the selected text.
-
-        // Remove any existing hash heading markdown and save the header level.
-        chunk.findTags(/#+[ ]*/, /[ ]*#+/);
-        if (/#+/.test(chunk.startTag)) {
-            headerLevel = re.lastMatch.length;
-        }
-        chunk.startTag = chunk.endTag = "";
-
-        // Try to get the current header level by looking for - and = in the line
-        // below the selection.
-        chunk.findTags(null, /\s?(-+|=+)/);
-        if (/=+/.test(chunk.endTag)) {
-            headerLevel = 1;
-        }
-        if (/-+/.test(chunk.endTag)) {
-            headerLevel = 2;
-        }
-
-        // Skip to the next line so we can create the header markdown.
-        chunk.startTag = chunk.endTag = "";
-        chunk.skipLines(1, 1);
-
-        // We make a level 2 header if there is no current header.
-        // If there is a header level, we substract one from the header level.
-        // If it's already a level 1 header, it's removed.
-        var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
-
-        if (headerLevelToCreate > 0) {
-
-            // The button only creates level 1 and 2 underline headers.
-            // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?
-            var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
-            var len = chunk.selection.length;
-            if (len > SETTINGS.lineLength) {
-                len = SETTINGS.lineLength;
-            }
-            chunk.endTag = "\n";
-            while (len--) {
-                chunk.endTag += headerChar;
-            }
-        }
-    };
-
-    commandProto.doHorizontalRule = function (chunk, postProcessing) {
-        chunk.startTag = "----------\n";
-        chunk.selection = "";
-        chunk.skipLines(2, 1, true);
-    }
-
-
-})();
--- a/wikked/assets/js/pagedown/Markdown.Sanitizer.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-(function () {
-    var output, Converter;
-    if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
-        output = exports;
-        Converter = require("./Markdown.Converter").Converter;
-    } else {
-        output = window.Markdown;
-        Converter = output.Converter;
-    }
-        
-    output.getSanitizingConverter = function () {
-        var converter = new Converter();
-        converter.hooks.chain("postConversion", sanitizeHtml);
-        converter.hooks.chain("postConversion", balanceTags);
-        return converter;
-    }
-
-    function sanitizeHtml(html) {
-        return html.replace(/<[^>]*>?/gi, sanitizeTag);
-    }
-
-    // (tags that can be opened/closed) | (tags that stand alone)
-    var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
-    // <a href="url..." optional title>|</a>
-    var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
-
-    // <img src="url..." optional width  optional height  optional alt  optional title
-    var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
-
-    function sanitizeTag(tag) {
-        if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
-            return tag;
-        else
-            return "";
-    }
-
-    /// <summary>
-    /// attempt to balance HTML tags in the html string
-    /// by removing any unmatched opening or closing tags
-    /// IMPORTANT: we *assume* HTML has *already* been 
-    /// sanitized and is safe/sane before balancing!
-    /// 
-    /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
-    /// </summary>
-    function balanceTags(html) {
-
-        if (html == "")
-            return "";
-
-        var re = /<\/?\w+[^>]*(\s|$|>)/g;
-        // convert everything to lower case; this makes
-        // our case insensitive comparisons easier
-        var tags = html.toLowerCase().match(re);
-
-        // no HTML tags present? nothing to do; exit now
-        var tagcount = (tags || []).length;
-        if (tagcount == 0)
-            return html;
-
-        var tagname, tag;
-        var ignoredtags = "<p><img><br><li><hr>";
-        var match;
-        var tagpaired = [];
-        var tagremove = [];
-        var needsRemoval = false;
-
-        // loop through matched tags in forward order
-        for (var ctag = 0; ctag < tagcount; ctag++) {
-            tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
-            // skip any already paired tags
-            // and skip tags in our ignore list; assume they're self-closed
-            if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
-                continue;
-
-            tag = tags[ctag];
-            match = -1;
-
-            if (!/^<\//.test(tag)) {
-                // this is an opening tag
-                // search forwards (next tags), look for closing tags
-                for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
-                    if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
-                        match = ntag;
-                        break;
-                    }
-                }
-            }
-
-            if (match == -1)
-                needsRemoval = tagremove[ctag] = true; // mark for removal
-            else
-                tagpaired[match] = true; // mark paired
-        }
-
-        if (!needsRemoval)
-            return html;
-
-        // delete all orphaned tags from the string
-
-        var ctag = 0;
-        html = html.replace(re, function (match) {
-            var res = tagremove[ctag] ? "" : match;
-            ctag++;
-            return res;
-        });
-        return html;
-    }
-})();
--- a/wikked/assets/js/text.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,323 +0,0 @@
-/**
- * @license RequireJS text 2.0.4 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
- * Available via the MIT or new BSD license.
- * see: http://github.com/requirejs/text for details
- */
-/*jslint regexp: true */
-/*global require: false, XMLHttpRequest: false, ActiveXObject: false,
-  define: false, window: false, process: false, Packages: false,
-  java: false, location: false */
-
-define(['module'], function (module) {
-    'use strict';
-
-    var text, fs,
-        progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
-        xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
-        bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
-        hasLocation = typeof location !== 'undefined' && location.href,
-        defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
-        defaultHostName = hasLocation && location.hostname,
-        defaultPort = hasLocation && (location.port || undefined),
-        buildMap = [],
-        masterConfig = (module.config && module.config()) || {};
-
-    text = {
-        version: '2.0.4',
-
-        strip: function (content) {
-            //Strips <?xml ...?> declarations so that external SVG and XML
-            //documents can be added to a document without worry. Also, if the string
-            //is an HTML document, only the part inside the body tag is returned.
-            if (content) {
-                content = content.replace(xmlRegExp, "");
-                var matches = content.match(bodyRegExp);
-                if (matches) {
-                    content = matches[1];
-                }
-            } else {
-                content = "";
-            }
-            return content;
-        },
-
-        jsEscape: function (content) {
-            return content.replace(/(['\\])/g, '\\$1')
-                .replace(/[\f]/g, "\\f")
-                .replace(/[\b]/g, "\\b")
-                .replace(/[\n]/g, "\\n")
-                .replace(/[\t]/g, "\\t")
-                .replace(/[\r]/g, "\\r")
-                .replace(/[\u2028]/g, "\\u2028")
-                .replace(/[\u2029]/g, "\\u2029");
-        },
-
-        createXhr: masterConfig.createXhr || function () {
-            //Would love to dump the ActiveX crap in here. Need IE 6 to die first.
-            var xhr, i, progId;
-            if (typeof XMLHttpRequest !== "undefined") {
-                return new XMLHttpRequest();
-            } else if (typeof ActiveXObject !== "undefined") {
-                for (i = 0; i < 3; i += 1) {
-                    progId = progIds[i];
-                    try {
-                        xhr = new ActiveXObject(progId);
-                    } catch (e) {}
-
-                    if (xhr) {
-                        progIds = [progId];  // so faster next time
-                        break;
-                    }
-                }
-            }
-
-            return xhr;
-        },
-
-        /**
-         * Parses a resource name into its component parts. Resource names
-         * look like: module/name.ext!strip, where the !strip part is
-         * optional.
-         * @param {String} name the resource name
-         * @returns {Object} with properties "moduleName", "ext" and "strip"
-         * where strip is a boolean.
-         */
-        parseName: function (name) {
-            var modName, ext, temp,
-                strip = false,
-                index = name.indexOf("."),
-                isRelative = name.indexOf('./') === 0 ||
-                             name.indexOf('../') === 0;
-
-            if (index !== -1 && (!isRelative || index > 1)) {
-                modName = name.substring(0, index);
-                ext = name.substring(index + 1, name.length);
-            } else {
-                modName = name;
-            }
-
-            temp = ext || modName;
-            index = temp.indexOf("!");
-            if (index !== -1) {
-                //Pull off the strip arg.
-                strip = temp.substring(index + 1) === "strip";
-                temp = temp.substring(0, index);
-                if (ext) {
-                    ext = temp;
-                } else {
-                    modName = temp;
-                }
-            }
-
-            return {
-                moduleName: modName,
-                ext: ext,
-                strip: strip
-            };
-        },
-
-        xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
-
-        /**
-         * Is an URL on another domain. Only works for browser use, returns
-         * false in non-browser environments. Only used to know if an
-         * optimized .js version of a text resource should be loaded
-         * instead.
-         * @param {String} url
-         * @returns Boolean
-         */
-        useXhr: function (url, protocol, hostname, port) {
-            var uProtocol, uHostName, uPort,
-                match = text.xdRegExp.exec(url);
-            if (!match) {
-                return true;
-            }
-            uProtocol = match[2];
-            uHostName = match[3];
-
-            uHostName = uHostName.split(':');
-            uPort = uHostName[1];
-            uHostName = uHostName[0];
-
-            return (!uProtocol || uProtocol === protocol) &&
-                   (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
-                   ((!uPort && !uHostName) || uPort === port);
-        },
-
-        finishLoad: function (name, strip, content, onLoad) {
-            content = strip ? text.strip(content) : content;
-            if (masterConfig.isBuild) {
-                buildMap[name] = content;
-            }
-            onLoad(content);
-        },
-
-        load: function (name, req, onLoad, config) {
-            //Name has format: some.module.filext!strip
-            //The strip part is optional.
-            //if strip is present, then that means only get the string contents
-            //inside a body tag in an HTML string. For XML/SVG content it means
-            //removing the <?xml ...?> declarations so the content can be inserted
-            //into the current doc without problems.
-
-            // Do not bother with the work if a build and text will
-            // not be inlined.
-            if (config.isBuild && !config.inlineText) {
-                onLoad();
-                return;
-            }
-
-            masterConfig.isBuild = config.isBuild;
-
-            var parsed = text.parseName(name),
-                nonStripName = parsed.moduleName +
-                    (parsed.ext ? '.' + parsed.ext : ''),
-                url = req.toUrl(nonStripName),
-                useXhr = (masterConfig.useXhr) ||
-                         text.useXhr;
-
-            //Load the text. Use XHR if possible and in a browser.
-            if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
-                text.get(url, function (content) {
-                    text.finishLoad(name, parsed.strip, content, onLoad);
-                }, function (err) {
-                    if (onLoad.error) {
-                        onLoad.error(err);
-                    }
-                });
-            } else {
-                //Need to fetch the resource across domains. Assume
-                //the resource has been optimized into a JS module. Fetch
-                //by the module name + extension, but do not include the
-                //!strip part to avoid file system issues.
-                req([nonStripName], function (content) {
-                    text.finishLoad(parsed.moduleName + '.' + parsed.ext,
-                                    parsed.strip, content, onLoad);
-                });
-            }
-        },
-
-        write: function (pluginName, moduleName, write, config) {
-            if (buildMap.hasOwnProperty(moduleName)) {
-                var content = text.jsEscape(buildMap[moduleName]);
-                write.asModule(pluginName + "!" + moduleName,
-                               "define(function () { return '" +
-                                   content +
-                               "';});\n");
-            }
-        },
-
-        writeFile: function (pluginName, moduleName, req, write, config) {
-            var parsed = text.parseName(moduleName),
-                extPart = parsed.ext ? '.' + parsed.ext : '',
-                nonStripName = parsed.moduleName + extPart,
-                //Use a '.js' file name so that it indicates it is a
-                //script that can be loaded across domains.
-                fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
-
-            //Leverage own load() method to load plugin value, but only
-            //write out values that do not have the strip argument,
-            //to avoid any potential issues with ! in file names.
-            text.load(nonStripName, req, function (value) {
-                //Use own write() method to construct full module value.
-                //But need to create shell that translates writeFile's
-                //write() to the right interface.
-                var textWrite = function (contents) {
-                    return write(fileName, contents);
-                };
-                textWrite.asModule = function (moduleName, contents) {
-                    return write.asModule(moduleName, fileName, contents);
-                };
-
-                text.write(pluginName, nonStripName, textWrite, config);
-            }, config);
-        }
-    };
-
-    if (masterConfig.env === 'node' || (!masterConfig.env &&
-            typeof process !== "undefined" &&
-            process.versions &&
-            !!process.versions.node)) {
-        //Using special require.nodeRequire, something added by r.js.
-        fs = require.nodeRequire('fs');
-
-        text.get = function (url, callback) {
-            var file = fs.readFileSync(url, 'utf8');
-            //Remove BOM (Byte Mark Order) from utf8 files if it is there.
-            if (file.indexOf('\uFEFF') === 0) {
-                file = file.substring(1);
-            }
-            callback(file);
-        };
-    } else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
-            text.createXhr())) {
-        text.get = function (url, callback, errback) {
-            var xhr = text.createXhr();
-            xhr.open('GET', url, true);
-
-            //Allow overrides specified in config
-            if (masterConfig.onXhr) {
-                masterConfig.onXhr(xhr, url);
-            }
-
-            xhr.onreadystatechange = function (evt) {
-                var status, err;
-                //Do not explicitly handle errors, those should be
-                //visible via console output in the browser.
-                if (xhr.readyState === 4) {
-                    status = xhr.status;
-                    if (status > 399 && status < 600) {
-                        //An http 4xx or 5xx error. Signal an error.
-                        err = new Error(url + ' HTTP status: ' + status);
-                        err.xhr = xhr;
-                        errback(err);
-                    } else {
-                        callback(xhr.responseText);
-                    }
-                }
-            };
-            xhr.send(null);
-        };
-    } else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
-            typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
-        //Why Java, why is this so awkward?
-        text.get = function (url, callback) {
-            var stringBuffer, line,
-                encoding = "utf-8",
-                file = new java.io.File(url),
-                lineSeparator = java.lang.System.getProperty("line.separator"),
-                input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
-                content = '';
-            try {
-                stringBuffer = new java.lang.StringBuffer();
-                line = input.readLine();
-
-                // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
-                // http://www.unicode.org/faq/utf_bom.html
-
-                // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
-                // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
-                if (line && line.length() && line.charAt(0) === 0xfeff) {
-                    // Eat the BOM, since we've already found the encoding on this file,
-                    // and we plan to concatenating this buffer with others; the BOM should
-                    // only appear at the top of a file.
-                    line = line.substring(1);
-                }
-
-                stringBuffer.append(line);
-
-                while ((line = input.readLine()) !== null) {
-                    stringBuffer.append(lineSeparator);
-                    stringBuffer.append(line);
-                }
-                //Make sure we return a JavaScript string and not a Java string.
-                content = String(stringBuffer.toString()); //String
-            } finally {
-                input.close();
-            }
-            callback(content);
-        };
-    }
-
-    return text;
-});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/assets/js/wikked.app.js	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,7 @@
+
+require(['wikked/app'],
+        function(app) {
+            console.log("Running app");
+            app.run();
+        });
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/assets/js/wikked.edit.js	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,7 @@
+
+require(['wikked/edit'],
+        function(edit) {
+            console.log("Running edit view");
+            edit.run();
+        });
+
--- a/wikked/assets/js/wikked.js	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/assets/js/wikked.js	Wed Sep 16 23:04:28 2015 -0700
@@ -4,18 +4,10 @@
  * We need to alias/shim some of the libraries.
  */
 require.config({
-    //urlArgs: "bust=" + (new Date()).getTime(),
     paths: {
-        jquery: 'js/jquery-1.8.3.min',
-        jquery_validate: 'js/jquery/jquery.validate.min',
-        underscore: 'js/underscore-min',
-        backbone: 'js/backbone-min',
-        handlebars: 'js/handlebars-1.0.rc.1',
-        moment: 'js/moment.min',
-        text: 'js/text',
-        pagedown_converter: 'js/pagedown/Markdown.Converter',
-        pagedown_editor: 'js/pagedown/Markdown.Editor',
-        pagedown_sanitizer: 'js/pagedown/Markdown.Sanitizer'
+        jquery: 'jquery-1.8.3.min',
+        jquery_validate: 'jquery/jquery.validate.min',
+        underscore: 'underscore-min',
     },
     shim: {
         'jquery': {
@@ -26,44 +18,7 @@
         },
         'underscore': {
             exports: '_'
-        },
-        'backbone': {
-            deps: ['underscore', 'jquery'],
-            exports: 'Backbone'
-        },
-        'handlebars': {
-            exports: 'Handlebars'
-        },
-        'pagedown_converter': {
-            exports: 'Markdown'
-        },
-        'pagedown_editor': {
-            deps: ['pagedown_converter']
-        },
-        'pagedown_sanitizer': {
-            deps: ['pagedown_editor']
         }
     }
 });
 
-//-------------------------------------------------------------//
-
-/**
- * Entry point: run Backbone!
- *
- * We also import scripts like `handlebars` that are not used directly
- * by anybody, but need to be evaluated.
- */
-require([
-        'js/wikked/app',
-        'js/wikked/handlebars',
-        'backbone',
-        'text'
-        ],
-    function(app, hb, Backbone, textExtension) {
-
-    var router = new app.Router();
-    Backbone.history.start();//{ pushState: true });
-
-});
-
--- a/wikked/assets/js/wikked/app.js	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/assets/js/wikked/app.js	Wed Sep 16 23:04:28 2015 -0700
@@ -1,240 +1,198 @@
-/**
- * The main Wikked app/router.
- */
+
 define([
         'jquery',
         'underscore',
-        'backbone',
-        'js/wikked/views',
-        'js/wikked/models'
         ],
-    function($, _, Backbone, Views, Models) {
+    function($, _) {
 
     var exports = {};
 
-    /**
-     * View manager.
-     */
-    var ViewManager = exports.ViewManager = function(el) {
+    // Main app method.
+    var run = exports.run = function() {
+        var nav = new NavigationView();
+        window.wikkedNav = nav;
+    };
+
+    // Navigation menu view.
+    var NavigationView = exports.NavigationView = function() {
         this.initialize.apply(this, arguments);
     };
-    _.extend(ViewManager.prototype, {
-        initialize: function(el) {
-            this.el = el;
-        },
-        _currentView: false,
-        switchView: function(view, autoFetch) {
-            if (this._currentView) {
-                this._currentView.dispose();
-                this._currentView = false;
+    _.extend(NavigationView.prototype, {
+        initialize: function() {
+            // Hide the drop-down for the search results.
+            this.searchPreviewList = $('#search-preview');
+            this.searchPreviewList.hide();
+            this.activeResultIndex = -1;
+
+            // Cache some stuff for handling the menu.
+            this.wikiMenu = $('#wiki-menu');
+            this.isMenuActive = (this.wikiMenu.css('left') == '0px');
+            this.isMenuActiveLocked = false;
+
+            // Apply local settings.
+            var ima = localStorage.getItem('wikked.nav.isMenuActive');
+            if (ima == 'true') {
+                this.wikiMenu.addClass('wiki-menu-active');
+                this.isMenuActive = true;
+                this._toggleWikiMenuPin(true);
             }
 
-            if (view) {
-                this._currentView = view;
-                if (autoFetch || autoFetch === undefined) {
-                    view.model.fetch();
-                } else {
-                    view.render();
+            // Hookup events.
+            this.listen("#wiki-menu-shortcut", 'click', '_onMenuShortcutClick');
+            this.listen("#wiki-menu-pin", 'click', '_onMenuShortcutClick');
+            this.listen("#wiki-menu-shortcut", 'mouseenter', '_onMenuShortcutHover');
+            this.listen("#wiki-menu-shortcut", 'mouseleave', '_onMenuShortcutLeave');
+            this.listen("#wiki-menu", 'mouseenter', '_onMenuHover');
+            this.listen("#wiki-menu", 'mouseleave', '_onMenuLeave');
+            this.listen("#search-query", 'focus', '_searchQueryFocused');
+            this.listen("#search-query", 'input', '_previewSearch');
+            this.listen("#search-query", 'keyup', '_searchQueryChanged');
+            this.listen("#search-query", 'blur', '_searchQueryBlurred');
+            this.listen("#search", 'submit', '_submitSearch');
+        },
+        listen: function(sel, evt, callback) {
+            var _t = this;
+            $(sel).on(evt, function(e) {
+                _t[callback](e);
+            });
+        },
+        _onMenuShortcutClick: function(e) {
+            this.isMenuActive = !this.isMenuActive;
+            localStorage.setItem('wikked.nav.isMenuActive', this.isMenuActive);
+            this._toggleWikiMenuPin(this.isMenuActive);
+        },
+        _onMenuShortcutHover: function(e) {
+            if (!this.isMenuActive && !this.isMenuActiveLocked)
+                this._toggleWikiMenu(true);
+        },
+        _onMenuShortcutLeave: function(e) {
+            if (!this.isMenuActive && !this.isMenuActiveLocked)
+                this._toggleWikiMenu(false);
+        },
+        _onMenuHover: function(e) {
+            if (!this.isMenuActive && !this.isMenuActiveLocked)
+                this._toggleWikiMenu(true);
+        },
+        _onMenuLeave: function(e) {
+            if (!this.isMenuActive && !this.isMenuActiveLocked)
+                this._toggleWikiMenu(false);
+        },
+        _toggleWikiMenu: function(onOff) {
+            if (onOff) {
+                this.wikiMenu.toggleClass('wiki-menu-inactive', false);
+                this.wikiMenu.toggleClass('wiki-menu-active', true);
+            } else {
+                this.wikiMenu.toggleClass('wiki-menu-active', false);
+                this.wikiMenu.toggleClass('wiki-menu-inactive', true);
+            }
+        },
+        _toggleWikiMenuPin: function(onOff) {
+            $('#wiki-menu-pin').toggleClass('wiki-menu-pin-active', onOff);
+        },
+        _searchQueryFocused: function(e) {
+            this.isMenuActiveLocked = true;
+            this.wikiMenu.toggleClass('wiki-menu-ext', true);
+        },
+        _searchQueryBlurred: function(e) {
+            $(e.currentTarget).val('').trigger('input');
+            this.wikiMenu.toggleClass('wiki-menu-ext', false);
+            this.isMenuActiveLocked = false;
+            if ($(document.activeElement).parents('#wiki-menu').length === 0)
+                this._onMenuLeave(e);
+        },
+        _submitSearch: function(e) {
+            if (this.activeResultIndex >= 0) {
+                var entries = this.searchPreviewList.children();
+                var choice = $('a', entries[this.activeResultIndex]);
+                var url = choice.attr('href') + "?no_redirect";
+                window.location.href = url;
+                e.preventDefault();
+                return false;
+            }
+            return true;
+        },
+        _previewSearch: function(e) {
+            var query = $(e.currentTarget).val();
+            if (query && query.length >= 1) {
+                var $view = this;
+                this._doPreviewSearch(query, function(data) {
+                    var resultStr = '';
+                    for (var i = 0; i < data.hits.length; ++i) {
+                        var hitUrl = data.hits[i].url.replace(/^\//, '');
+                        resultStr += '<li>' +
+                            '<a href="/read/' + hitUrl + '">' +
+                            data.hits[i].title +
+                            '</a>' +
+                            '</li>';
+                    }
+                    $view.searchPreviewList.html(resultStr);
+                    if (!$view.searchPreviewList.is(':visible'))
+                        $view.searchPreviewList.slideDown(200);
+                });
+            } else if(!query || query.length === 0) {
+                this.searchPreviewList.slideUp(200);
+            }
+        },
+        _isSearching: false,
+        _pendingQuery: null,
+        _pendingCallback: null,
+        _doPreviewSearch: function(query, callback) {
+            if (this._isSearching) {
+                this._pendingQuery = query;
+                this._pendingCallback = callback;
+                return;
+            }
+            this._isSearching = true;
+            var $view = this;
+            $.getJSON('/api/searchpreview', { q: query })
+                .done(function (data) {
+                    $view._isSearching = false;
+                    callback(data);
+                    $view._flushPendingQuery();
+                })
+                .fail(function() {
+                    $view._isSearching = false;
+                    $view._flushPendingQuery();
+                });
+        },
+        _flushPendingQuery: function() {
+            if (this._pendingQuery && this._pendingCallback) {
+                var q = this._pendingQuery;
+                var c = this._pendingCallback;
+                this._pendingQuery = null;
+                this._pendingCallback = null;
+                this._doPreviewSearch(q, c);
+            }
+        },
+        _searchQueryChanged: function(e) {
+            if (e.keyCode == 27) {
+                // Clear search on `Esc`.
+                $(e.currentTarget).val('').trigger('input');
+            } else if (e.keyCode == 38) {
+                // Up arrow.
+                e.preventDefault();
+                if (this.activeResultIndex >= 0) {
+                    this.activeResultIndex--;
+                    this._updateActiveResult();
                 }
-                this.el.empty().append(view.el);
+            } else if (e.keyCode == 40) {
+                // Down arrow.
+                e.preventDefault();
+                if (this.activeResultIndex < 
+                        this.searchPreviewList.children().length - 1) {
+                    this.activeResultIndex++;
+                    this._updateActiveResult();
+                }
             }
-
-            return this;
+        },
+        _updateActiveResult: function() {
+            var entries = this.searchPreviewList.children();
+            entries.toggleClass('search-result-hover', false);
+            if (this.activeResultIndex >= 0)
+                $(entries[this.activeResultIndex]).toggleClass('search-result-hover', true);
         }
     });
 
-    /**
-     * Main router.
-     */
-    var AppRouter = Backbone.Router.extend({
-        initialize: function(options) {
-            this.viewManager = options ? options.viewManager : undefined;
-            if (!this.viewManager) {
-                this.viewManager = new ViewManager($('#app'));
-            }
-
-            var $router = this;
-            Backbone.View.prototype.navigate = function(url, options) {
-                $router.navigate(url, options);
-            };
-            Backbone.Model.prototype.navigate = function(url, options) {
-                $router.navigate(url, options);
-            };
-        },
-        routes: {
-            'read/*path':           "readPage",
-            '':                     "readMainPage",
-            'create/*path':         "createPage",
-            '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",
-            'special':               "showSpecialPages",
-            'special/changes':       "showSiteChanges",
-            'special/changes/:rev':  "showSiteChangesAfterRev",
-            'special/list/:name':    "showPageList"
-        },
-        readPage: function(path) {
-            var path_clean = this.stripQuery(path);
-            var no_redirect = this.getQueryVariable('no_redirect', path);
-            var model_attrs = { path: path_clean };
-            if (no_redirect) model_attrs.no_redirect = true;
-
-            var view = new Views.PageReadView({
-                model: new Models.PageReadModel(model_attrs)
-            });
-
-            this.viewManager.switchView(view);
-            this.navigate('/read/' + path);
-        },
-        readMainPage: function() {
-            this.readPage('');
-        },
-        createPage: function(path) {
-            var view = new Views.PageEditView({
-                model: new Models.PageEditModel({ is_new: true, create_in: path })
-            });
-            this.viewManager.switchView(view, false);
-            this.navigate('/create/' + path);
-        },
-        editPage: function(path) {
-            var view = new Views.PageEditView({
-                model: new Models.PageEditModel({ path: path })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/edit/' + path);
-        },
-        showPageHistory: function(path) {
-            var view = new Views.PageHistoryView({
-                model: new Models.PageHistoryModel({ path: path })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/changes/' + path);
-        },
-        showIncomingLinks: function(path) {
-            var view = new Views.IncomingLinksView({
-                model: new Models.IncomingLinksModel({ path: path })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/inlinks/' + path);
-        },
-        readPageRevision: function(path, rev) {
-            var view = new Views.PageRevisionView({
-                rev: rev,
-                model: new Models.PageRevisionModel({ path: path, rev: rev })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/revision/' + path + '/' + rev);
-        },
-        showDiffWithPrevious: function(path, rev) {
-            var view = new Views.PageDiffView({
-                rev1: rev,
-                model: new Models.PageDiffModel({ path: path, rev1: rev })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/diff/c/' + path + '/' + rev);
-        },
-        showDiff: function(path, rev1, rev2) {
-            var view = new Views.PageDiffView({
-                rev1: rev1,
-                rev2: rev2,
-                model: new Models.PageDiffModel({ path: path, rev1: rev1, rev2: rev2 })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2);
-        },
-        showSearchResults: function(query) {
-            if (query === '') {
-                query = this.getQueryVariable('q');
-            }
-            var view = new Views.WikiSearchView({
-                model: new Models.WikiSearchModel({ query: query })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/search/' + query);
-        },
-        showLogin: function() {
-            var view = new Views.LoginView({
-                model: new Models.LoginModel()
-            });
-            this.viewManager.switchView(view, false);
-            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!");
-                });
-        },
-        showSpecialPages: function() {
-            var view = new Views.SpecialPagesView({
-                model: new Models.SpecialPagesModel()
-            });
-            this.viewManager.switchView(view, false);
-            this.navigate('/special');
-        },
-        showSiteChanges: function() {
-            var view = new Views.SpecialChangesView({
-                model: new Models.SpecialChangesModel()
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/special/changes');
-        },
-        showSiteChangesAfterRev: function(rev) {
-            var view = new Views.SpecialChangesView({
-                model: new Models.SpecialChangesModel({ after_rev: rev })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/special/changes/' + rev);
-        },
-        showPageList: function(name) {
-            var view = new Views.SpecialPageListView({
-                model: new Models.SpecialPageListModel({ name: name })
-            });
-            this.viewManager.switchView(view);
-            this.navigate('/special/list/' + name);
-        },
-        stripQuery: function(url) {
-            q = url.indexOf("?");
-            if (q < 0)
-                return url;
-            return url.substring(0, q);
-        },
-        getQueryVariable: function(variable, url) {
-            if (url === undefined) {
-                url = window.location.search.substring(1);
-            } else {
-                q = url.indexOf("?");
-                if (q < 0)
-                    return false;
-                url = url.substring(q + 1);
-            }
-            var vars = url.split("&");
-            for (var i = 0; i < vars.length; i++) {
-                var pair = vars[i].split("=");
-                if (pair[0] == variable) {
-                    if (pair.length > 1) {
-                        return unescape(pair[1]);
-                    } else {
-                        return true;
-                    }
-                }
-            }
-            return false;
-        }
-    });
-
-    return {
-        Router: AppRouter
-    };
+    return exports;
 });
 
--- a/wikked/assets/js/wikked/client.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/**
- * Client-side Wikked.
- */
-define([
-        'jquery',
-        'underscore'
-        ],
-    function($, _) {
-
-    /**
-     * Function to normalize an array of path parts (remove/simplify `./` and
-     * `../` elements).
-     */
-    function normalizeArray(parts, keepBlanks) {
-        var directories = [], prev;
-        for (var i = 0, l = parts.length - 1; i <= l; i++) {
-            var directory = parts[i];
-
-            // if it's blank, but it's not the first thing, and not the last thing, skip it.
-            if (directory === "" && i !== 0 && i !== l && !keepBlanks)
-                continue;
-
-            // if it's a dot, and there was some previous dir already, then skip it.
-            if (directory === "." && prev !== undefined)
-                continue;
-
-            // if it starts with "", and is a . or .., then skip it.
-            if (directories.length === 1 && directories[0] === "" && (
-                directory === "." || directory === ".."))
-                continue;
-
-                if (
-                    directory === ".." && directories.length && prev !== ".." && prev !== "." && prev !== undefined && (prev !== "" || keepBlanks)) {
-                    directories.pop();
-                prev = directories.slice(-1)[0];
-            } else {
-                if (prev === ".") directories.pop();
-                directories.push(directory);
-                prev = directory;
-            }
-        }
-        return directories;
-    }
-
-    /**
-     * Client-side page formatter, with support for previewing meta properties
-     * in the live preview window.
-     */
-    var PageFormatter = function(baseUrl) {
-        this.baseUrl = baseUrl;
-        if (baseUrl === undefined) {
-            this.baseUrl = '';
-        }
-    };
-    _.extend(PageFormatter.prototype, {
-        formatLink: function(link) {
-            var abs_link = link;
-            if (link[0] == '/') {
-                abs_link = link.substring(1);
-            } else {
-                raw_abs_link = this.baseUrl + link;
-                abs_link = normalizeArray(raw_abs_link.split('/')).join('/');
-            }
-            return encodeURI(abs_link);
-        },
-        formatText: function(text) {
-            var $f = this;
-            text = text.replace(
-                /^\{\{((__|\+)?[a-zA-Z][a-zA-Z0-9_\-]+)\:\s*(.*)\}\}\s*$/gm,
-                function(m, a, b, c) {
-                    if (!c) {
-                        c = 'true';
-                    }
-                    var p = "<p><span class=\"preview-wiki-meta\">\n";
-                    p += "<span class=\"meta-name\">" + a + "</span>";
-                    p += "<span class=\"meta-value\">" + c + "</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;
-        }
-    });
-
-    return {
-        PageFormatter: PageFormatter
-    };
-});
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/assets/js/wikked/edit.js	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,131 @@
+
+define([
+        'jquery',
+        'jquery_validate',
+        'underscore',
+        ],
+    function($, JQueryValidate, _) {
+
+    var exports = {};
+
+    // Edit initialization.
+    var run = exports.run = function() {
+        var editView = new EditPageView();
+        window.wikkedEditView = editView;
+    };
+
+    // Override JQuery-validation plugin's way of highlighting errors
+    // with something that works with Bootstrap.
+    $.validator.setDefaults({
+        highlight: function(element) {
+            $(element).closest('.form-group')
+                .addClass('has-error')
+                .removeClass('has-success');
+        },
+        unhighlight: function(element) {
+            $(element).closest('.form-group')
+                .removeClass('has-error')
+                .addClass('has-success');
+        },
+        errorElement: 'span',
+        errorClass: 'help-block',
+        errorPlacement: function(error, element) {
+            if(element.parent('.input-group').length) {
+                error.insertAfter(element.parent());
+            } else {
+                error.insertAfter(element);
+            }
+        }
+    });
+
+    // Edit page functionality.
+    var EditPageView = exports.EditPageView = function() {
+        this.initialize.apply(this, arguments);
+    };
+    _.extend(EditPageView.prototype, {
+        initialize: function() {
+            this.inputSection = $('.editing-input');
+            this.inputCtrl = $('#editing-input-area');
+
+            this.previewSection = $('.editing-preview');
+            this.previewButtonLabel = $('.editing-preview-button-label');
+            this.previewSection.hide();
+
+            this.errorSection = $('.editing-error');
+            this.errorSection.hide();
+
+            $('#page-edit').validate({
+                rules: {
+                    title: {
+                        required: true,
+                        remote: {
+                            url: '/api/validate/newpage',
+                            type: 'post'
+                        }
+                    }
+                }
+            });
+
+            this.listen('#editing-input-grip', 'mousedown', 
+                        '_inputGripMouseDown');
+            this.listen('#editing-preview-button', 'click', '_togglePreview');
+        },
+        listen: function(sel, evt, callback) {
+            var _t = this;
+            $(sel).on(evt, function(e) {
+                _t[callback](e);
+            });
+        },
+        _inputGripMouseDown: function(e) {
+            // Input area resizing with the grip.
+            var $view = this;
+            var last_pageY = e.pageY;
+            $('body')
+                .on('mousemove.wikked.editor_resize', function(e) {
+                    var ctrl = $view.inputCtrl;
+                    ctrl.height(ctrl.height() + e.pageY - last_pageY);
+                    last_pageY = e.pageY;
+                })
+                .on('mouseup.wikked.editor_resize mouseleave.wikked.editor_resize', function(e) {
+                    $('body').off('.wikked.editor_resize');
+                });
+        },
+        _togglePreview: function(e) {
+            e.preventDefault();
+
+            if (this.previewSection.is(':visible')) {
+                // Hide the preview, restore the textbox.
+                this.inputSection.show();
+                this.previewSection.hide();
+                this.previewButtonLabel.html("Preview");
+                return false;
+            }
+
+            // Get the server to compute the preview text, hide the textbox,
+            // show the rendered text.
+            var $view = this;
+            var previewBtn = $('#editing-preview-button');
+            var previewData = {
+                url: previewBtn.attr('data-wiki-url'),
+                text: this.inputCtrl.val()
+            };
+            $.post('/api/preview', previewData)
+                .success(function(data) {
+                    var el = $view.previewSection;
+                    el.html(data.text);
+                    el.show();
+                    $view.inputSection.hide();
+                    $view.previewButtonLabel.html("Edit");
+                    $view.errorSection.hide();
+                })
+                .error(function() {
+                    $('.editing-error-message').html("Error running preview.");
+                    $view.errorSection.show();
+                });
+            return false;
+        }
+    });
+
+    return exports;
+});
+
--- a/wikked/assets/js/wikked/handlebars.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-/**
- * Handlebars helpers and extensions.
- */
-define([
-        'handlebars',
-        'moment'
-        ],
-    function(Handlebars) {
-
-    /**
-     * 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);
-    });
-
-    /**
-     * Inverse if.
-     */
-    Handlebars.registerHelper('ifnot', function(context, options) {
-        if (!context) {
-            return options.fn(this);
-        }
-        return options.inverse(this);
-    });
-
-    /**
-     * Concatenate strings with a separator.
-     */
-    Handlebars.registerHelper('concat', function(context, options) {
-        if (context === undefined) {
-            return '';
-        }
-        data = undefined;
-        if (options.data) {
-            data = Handlebars.createFrame(options.data);
-        }
-
-        var sep = options.hash.sep;
-        var out = '';
-        for (var i = 0; i < context.length; i++) {
-            if (i > 0) {
-                out += sep;
-            }
-            out += options.fn(context[i], { data: data });
-        }
-        return out;
-    });
-
-    /**
-     * Format dates.
-     */
-    Handlebars.registerHelper('date', function(timestamp, options) {
-        var date = new Date(timestamp * 1000);
-        if ("format" in options.hash) {
-            return moment(date).format(options.hash.format);
-        }
-        return moment(date).format();
-    });
-    Handlebars.registerHelper('date_from_now', function(timestamp, options) {
-        var date = new Date(timestamp * 1000);
-        return moment(date).fromNow();
-    });
-
-    /**
-     * Format application URLs
-     */
-    Handlebars.registerHelper('get_read_url', function(url, options) {
-        url = url.toString();
-        return '/#/read/' + url.replace(/^\//, '');
-    });
-    Handlebars.registerHelper('get_edit_url', function(url, options) {
-        url = url.toString();
-        return '/#/edit/' + url.replace(/^\//, '');
-    });
-    Handlebars.registerHelper('get_cat_url', function(url, options) {
-        url = url.toString();
-        return '/#/read/' + url.replace(/^\//, '');
-    });
-});
-
--- a/wikked/assets/js/wikked/models.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,541 +0,0 @@
-/**
- * Wikked models.
- */
-define([
-        'require',
-        'jquery',
-        'underscore',
-        'backbone',
-        'handlebars',
-        'text!json/special-sections.json',
-        'text!json/special-pagelists.json',
-        ],
-    function(require, $, _, Backbone, Handlebars,
-        jsonSpecialSections, jsonSpecialPageLists) {
-
-    var exports = {};
-
-    var NavigationModel = exports.NavigationModel = Backbone.Model.extend({
-        defaults: function() {
-            return {
-                path: "",
-                username: false,
-                urls: [],
-                url_extras: [
-                    { name: 'Special Pages', url: '/#/special', icon: 'dashboard' }
-                ]
-            };
-        },
-        initialize: function() {
-            this.on('change:path', this._onChangePath, this);
-            this.on('change:user', this._onChangeUser, this);
-            this._onChangePath(this, this.get('path'));
-            this._onChangeUser(this, this.get('user'));
-            return this;
-        },
-        url: function() {
-            return '/api/user/info';
-        },
-        clearExtraUrls: function() {
-            this.get('url_extras').length = 0;
-        },
-        addExtraUrl: function(name, url, index, icon) {
-            extra = { name: name, url: url, icon: icon };
-            if (index === undefined || index < 0) {
-                this.get('url_extras').push(extra);
-            } else {
-                this.get('url_extras').splice(index, 0, extra);
-            }
-        },
-        doPreviewSearch: function(query, callback) {
-            if (this._isSearching) {
-                this._pendingQuery = query;
-                this._pendingCallback = callback;
-                return;
-            }
-            this._isSearching = true;
-            var $model = this;
-            $.getJSON('/api/searchpreview', { q: query })
-                .done(function (data) {
-                    $model._isSearching = false;
-                    callback(data);
-                    $model._flushPendingQuery();
-                })
-                .fail(function() {
-                    $model._isSearching = false;
-                    $model._flushPendingQuery();
-                });
-        },
-        doSearch: function(form) {
-            this.navigate('/search/' + $(form.q).val(), { trigger: true });
-        },
-        doGoToSearchResult: function(url) {
-            this.navigate(url + "?no_redirect", { trigger: true });
-        },
-        doNewPage: function(form) {
-            this.navigate('/create/', { trigger: true });
-        },
-        _onChangePath: function(model, path) {
-            var attrs ={
-                url_home: '/#/'};
-            var urls = this.get('urls');
-            if (_.contains(urls, 'read'))
-                attrs.url_read = '/#/read/' + path;
-            if (_.contains(urls, 'edit'))
-                attrs.url_edit = '/#/edit/' + path;
-            if (_.contains(urls, 'history'))
-                attrs.url_hist = '/#/changes/' + path;
-            this.set(attrs);
-        },
-        _isSearching: false,
-        _pendingQuery: null,
-        _pendingCallback: null,
-        _flushPendingQuery: function() {
-            if (this._pendingQuery && this._pendingCallback) {
-                var q = this._pendingQuery;
-                var c = this._pendingCallback;
-                this._pendingQuery = null;
-                this._pendingCallback = null;
-                this.doPreviewSearch(q, c);
-            }
-        },
-        _onChangeUser: function(model, user) {
-            if (user) {
-                this.set({
-                    url_login: false,
-                    url_logout: '/#/logout',
-                    url_profile: ('/#/read/' + user.page_url)
-                });
-            } else {
-                this.set({
-                    url_login: '/#/login',
-                    url_logout: false,
-                    url_profile: false
-                });
-            }
-        }
-    });
-
-    var FooterModel = exports.FooterModel = Backbone.Model.extend({
-        defaults: function() {
-            return {
-                url_extras: []
-            };
-        },
-        clearExtraUrls: function() {
-            this.get('url_extras').length = 0;
-        },
-        addExtraUrl: function(name, url, index, icon) {
-            extra = { name: name, url: url, icon: icon };
-            if (index === undefined || index < 0) {
-                this.get('url_extras').push(extra);
-            } else {
-                this.get('url_extras').splice(index, 0, extra);
-            }
-        }
-    });
-
-    var LoginModel = exports.LoginModel = Backbone.Model.extend({
-        title: 'Login',
-        setApp: function(app) {
-            this.app = app;
-        },
-        doLogin: function(form) {
-            var $model = this;
-            $.post('/api/user/login', $(form).serialize())
-                .done(function() {
-                    $model.navigate('/', { trigger: true });
-                })
-                .fail(function() {
-                    $model.set('has_error', true);
-                });
-        }
-    });
-
-    var PageModel = exports.PageModel = Backbone.Model.extend({
-        idAttribute: 'path',
-        defaults: function() {
-            return {
-                path: ""
-            };
-        },
-        initialize: function() {
-            this.on('change:path', this._onChangePath, this);
-            this.on('change:text', this._onChangeText, this);
-            this._onChangePath(this, this.get('path'));
-            this._onChangeText(this, '');
-            return this;
-        },
-        url: function() {
-            var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || Backbone.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(model, path) {
-        },
-        _onChangeText: function(model, text) {
-            this.set('content', new Handlebars.SafeString(text));
-        }
-    });
-
-    var PageStateModel = exports.PageStateModel = PageModel.extend({
-        urlRoot: '/api/state/',
-        _onChangePath: function(model, path) {
-            PageStateModel.__super__._onChangePath.apply(this, arguments);
-            this.set('url_edit', '/#/edit/' + path);
-        }
-    });
-
-    var MasterPageModel = exports.MasterPageModel = PageModel.extend({
-        navUrls: [],
-        initialize: function() {
-            var navAttrs = { path: this.id, urls: this.navUrls };
-            this.nav = new NavigationModel(navAttrs);
-            this.footer = new FooterModel();
-            MasterPageModel.__super__.initialize.apply(this, arguments);
-            this._addNavExtraUrls();
-            this._addFooterExtraUrls();
-            this.on('change:auth', this._onChangeAuth, this);
-            this.on('error', this._onError, this);
-            return this;
-        },
-        _addNavExtraUrls: function() {
-        },
-        _addFooterExtraUrls: function() {
-            var model = this;
-            this.footer.addExtraUrl(
-                'JSON',
-                function() { return _.result(model, 'url'); },
-                -1,
-                'cog');
-        },
-        _onAppSet: function(app) {
-            this.nav.app = app;
-            this.footer.app = app;
-        },
-        _onChangePath: function(model, path) {
-            MasterPageModel.__super__._onChangePath.apply(this, arguments);
-            this.nav.set('path', path);
-        },
-        _onChangeAuth: function(model, auth) {
-            this.nav.set('auth', auth);
-        },
-        _onError: function(model, resp) {
-            var setmap = {
-                has_error: true,
-                error_code: resp.status
-            };
-            switch (resp.status) {
-                case 401:
-                    setmap.error = 'Unauthorized';
-                    break;
-                case 404:
-                    setmap.error = 'Not Found';
-                    break;
-                default:
-                    setmap.error = 'Error';
-                    break;
-            }
-            this.set(setmap);
-        }
-    });
-
-    var PageReadModel = exports.PageReadModel = MasterPageModel.extend({
-        urlRoot: '/api/read/',
-        navUrls: ['edit', 'history'],
-        initialize: function() {
-            PageReadModel.__super__.initialize.apply(this, arguments);
-            this.on('change', this._onChange, this);
-            return this;
-        },
-        _addNavExtraUrls: function() {
-            PageReadModel.__super__._addNavExtraUrls.apply(this, arguments);
-            var model = this;
-            this.nav.addExtraUrl(
-                'Pages Linking Here',
-                function() { return '/#/inlinks/' + model.id; },
-                1,
-                'link');
-        },
-        _addFooterExtraUrls: function() {
-            PageReadModel.__super__._addFooterExtraUrls.apply(this, arguments);
-            var model = this;
-            this.footer.addExtraUrl(
-                'RAW',
-                function() { return '/api/raw/' + model.id; },
-                -1,
-                'wrench');
-        },
-        url: function() {
-            var url = PageReadModel.__super__.url.apply(this, arguments);
-            var qs = {};
-            if (!this.nav.get('user'))
-                qs.user = 1;
-            if (this.get('no_redirect'))
-                qs.no_redirect = 1;
-            if (_.size(qs) > 0)
-                url += '?' + $.param(qs);
-            return url;
-        },
-        checkStatePath: function() {
-            return this.get('path');
-        },
-        _onChange: function() {
-            if (this.get('user')) {
-                // Forward user info to the navigation model.
-                this.nav.set('user', this.get('user'));
-            }
-        }
-    });
-
-    var PageSourceModel = exports.PageSourceModel = MasterPageModel.extend({
-        urlRoot: '/api/raw/'
-    });
-
-    var PageEditModel = exports.PageEditModel = MasterPageModel.extend({
-        urlRoot: '/api/edit/',
-        navUrls: ['read', 'history'],
-        doEdit: function(form) {
-            if (this.get('is_new')) {
-                this.set('path', $('input[name="title"]', form).val());
-            }
-
-            var $model = this;
-            $.post(this.url(), $(form).serialize(), null, 'json')
-                .done(function(data) {
-                    $model._onEditSuccess();
-                })
-                .fail(function(jqxhr) {
-                    var err = $.parseJSON(jqxhr.responseText);
-                    $model.set('error', err.error);
-                });
-        },
-        doCancel: function() {
-            this._goToReadPage();
-        },
-        _onEditSuccess: function() {
-            this._goToReadPage();
-        },
-        _goToReadPage: function() {
-            this.navigate('/read/' + this.get('path') + '?no_redirect',
-                    { trigger: true });
-        }
-    });
-
-    var PageHistoryModel = exports.PageHistoryModel = MasterPageModel.extend({
-        urlRoot: '/api/history/',
-        navUrls: ['read', 'edit'],
-        doDiff: function(form) {
-            var rev1 = $('input[name=rev1]:checked', form).val();
-            var rev2 = $('input[name=rev2]:checked', form).val();
-            this.navigate('/diff/r/' + this.get('path') + '/' + rev1 + '/' + rev2, { trigger: true });
-        },
-        _onChangePath: function(model, path) {
-            PageHistoryModel.__super__._onChangePath.apply(this, arguments);
-            if (path === '') {
-                // This is the main page. Let's wait until we know its URL,
-                // and use that instead for all the view data links, because
-                // most of them require an actual page URL.
-                this.on('change:url', this._onChangeUrl, this);
-                return;
-            }
-            this._setViewData(path);
-        },
-        _onChangeUrl: function(model, url) {
-            this.off('change:meta', this._onChangeUrl, this);
-            this._setViewData(url);
-        },
-        _setViewData: function(path) {
-            this.set({
-                url_read: '/#/read/' + path,
-                url_edit: '/#/edit/' + path,
-                url_rev: '/#/revision/' + path,
-                url_diffc: '/#/diff/c/' + path,
-                url_diffr: '/#/diff/r/' + path
-            });
-        }
-    });
-
-    var PageRevisionModel = exports.PageRevisionModel = MasterPageModel.extend({
-        defaults: function() {
-            return {
-                path: "",
-                rev: "tip"
-            };
-        },
-        url: function() {
-            return '/api/revision/' + this.get('path') + '?rev=' + this.get('rev');
-        },
-        initialize: function() {
-            PageRevisionModel.__super__.initialize.apply(this, arguments);
-            this.on('change:rev', this._onChangeRev, this);
-            this._onChangeRev(this, this.get('rev'));
-            return this;
-        },
-        doRevert: function(form) {
-            var $model = this;
-            var path = this.get('path');
-            $.post('/api/revert/' + path, $(form).serialize())
-                .success(function(data) {
-                    $model.navigate('/read/' + path, { trigger: true });
-                })
-                .error(function() {
-                    alert('Error reverting page...');
-                });
-        },
-        _onChangeRev: function(model, rev) {
-            var setmap = { disp_rev: rev };
-            if (rev.match(/[a-f0-9]{40}/)) {
-                setmap.disp_rev = rev.substring(0, 8);
-            }
-            this.set(setmap);
-        }
-    });
-
-    var PageDiffModel = exports.PageDiffModel = MasterPageModel.extend({
-        defaults: function() {
-            return {
-                path: "",
-                rev1: "tip",
-                rev2: ""
-            };
-        },
-        url: function() {
-            var apiUrl = '/api/diff/' + this.get('path') + '?rev1=' + this.get('rev1');
-            if (this.get('rev2')) {
-                apiUrl += '&rev2=' + this.get('rev2');
-            }
-            return apiUrl;
-        },
-        initialize: function() {
-            PageDiffModel.__super__.initialize.apply(this, arguments);
-            this.on('change:rev1', this._onChangeRev1, this);
-            this.on('change:rev2', this._onChangeRev2, this);
-            this._onChangeRev1(this, this.get('rev1'));
-            this._onChangeRev2(this, this.get('rev2'));
-            return this;
-        },
-        _onChangeRev1: function(model, rev1) {
-            var setmap = { disp_rev1: rev1 };
-            if (rev1 !== undefined && rev1.match(/[a-f0-9]{40}/)) {
-                setmap.disp_rev1 = rev1.substring(0, 8);
-            }
-            this.set(setmap);
-        },
-        _onChangeRev2: function(model, rev2) {
-            var setmap = { disp_rev2:  rev2 };
-            if (rev2 !== undefined && rev2.match(/[a-f0-9]{40}/)) {
-                setmap.disp_rev2 = rev2.substring(0, 8);
-            }
-            this.set(setmap);
-        }
-    });
-
-    var IncomingLinksModel = exports.IncomingLinksModel = MasterPageModel.extend({
-        urlRoot: '/api/inlinks/'
-    });
-
-    var WikiSearchModel = exports.WikiSearchModel = MasterPageModel.extend({
-        urlRoot: '/api/search',
-        title: function() {
-            return 'Search';
-        },
-        url: function() {
-            return this.urlRoot + '?q=' + this.get('query');
-        }
-    });
-
-    var SpecialPagesModel = exports.SpecialPagesModel = MasterPageModel.extend({
-        title: function() {
-            return 'Special Pages';
-        },
-        initialize: function() {
-            SpecialPagesModel.__super__.initialize.apply(this, arguments);
-            this.set('sections', JSON.parse(jsonSpecialSections).sections);
-        },
-        _addFooterExtraUrls: function() {
-        }
-    });
-
-    var SpecialPageModel = exports.SpecialPageModel = MasterPageModel.extend({
-        initialize: function() {
-            SpecialPageModel.__super__.initialize.apply(this, arguments);
-        }
-    });
-
-    var SpecialChangesModel = exports.SpecialChangesModel = SpecialPageModel.extend({
-        title: "Wiki History",
-        initialize: function() {
-            SpecialChangesModel.__super__.initialize.apply(this, arguments);
-            this.on('change:history', this._onHistoryChanged, this);
-        },
-        url: function() {
-            var url = '/api/site-history';
-            if (this.get('after_rev'))
-                url += '?rev=' + this.get('after_rev');
-            return url;
-        },
-        _onHistoryChanged: function(model, history) {
-            for (var i = 0; i < history.length; ++i) {
-                var rev = history[i];
-                rev.collapsed = (rev.pages.length > 3);
-                for (var j = 0; j < rev.pages.length; ++j) {
-                    var page = rev.pages[j];
-                    switch (page.action) {
-                        case 'edit':
-                            page.action_label = 'edit';
-                            break;
-                        case 'add':
-                            page.action_label = 'added';
-                            break;
-                        case 'delete':
-                            page.action_label = 'deleted';
-                            break;
-                    }
-                }
-            }
-            model.set('first_page', '/#/special/changes');
-            if (history.length > 0) {
-                var last_rev = history[0].rev_name;
-                model.set('next_page', '/#/special/changes/' + last_rev);
-            }
-        }
-    });
-
-    var SpecialPageListModel = exports.SpecialPageListModel = SpecialPageModel.extend({
-        title: function() { return this.listData.titles[this.get('name')]; },
-        url: function() { return '/api/' + this.get('name'); },
-        initialize: function() {
-            SpecialPageListModel.__super__.initialize.apply(this, arguments);
-            var name = this.get('name');
-            var listData = JSON.parse(jsonSpecialPageLists);
-            this.set({
-                'title': listData.titles[name],
-                'message': listData.messages[name],
-                'aside': listData.asides[name],
-                'empty': listData.empties[name],
-                'url_suffix': listData.url_suffixes[name]
-            });
-            this.listData = listData;
-        }
-    });
-
-    return exports;
-});
-
--- a/wikked/assets/js/wikked/util.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-/**
- * 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
-    };
-});
-
--- a/wikked/assets/js/wikked/view-manager.js	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-/**
- * An object responsible for opening, switching, and closing
- * down view.
- */
-define([
-        ],
-    function() {
-
-    var ViewManager = {
-        _currentView: false,
-        switchView: function(view, autoFetch) {
-            if (this._currentView) {
-                this._currentView.remove();
-                this._currentView = false;
-            }
-
-            if (view) {
-                this._currentView = view;
-            }
-        }
-    };
-});
--- a/wikked/assets/js/wikked/views.js	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/assets/js/wikked/views.js	Wed Sep 16 23:04:28 2015 -0700
@@ -3,48 +3,14 @@
  */
 define([
         'jquery',
-        'jquery_validate',
-        'underscore',
-        'backbone',
-        'handlebars',
-        'pagedown_converter',
-        'pagedown_editor',
-        'pagedown_sanitizer',
-        'js/wikked/client',
-        'js/wikked/models',
-        'js/wikked/util',
-        'text!tpl/read-page.html',
-        'text!tpl/meta-page.html',
-        'text!tpl/edit-page.html',
-        'text!tpl/history-page.html',
-        'text!tpl/revision-page.html',
-        'text!tpl/diff-page.html',
-        'text!tpl/inlinks-page.html',
-        'text!tpl/nav.html',
-        'text!tpl/footer.html',
-        'text!tpl/search-results.html',
-        'text!tpl/login.html',
-        'text!tpl/error-unauthorized.html',
-        'text!tpl/error-not-found.html',
-        'text!tpl/error-unauthorized-edit.html',
-        'text!tpl/state-warning.html',
-        'text!tpl/special-nav.html',
-        'text!tpl/special-pages.html',
-        'text!tpl/special-changes.html',
-        'text!tpl/special-pagelist.html'
+        'underscore'
         ],
-    function($, JQueryValidate, _, Backbone, Handlebars,
-        PageDownConverter, PageDownEditor, PageDownSanitizer,
-        Client, Models, Util,
-        tplReadPage, tplMetaPage, tplEditPage, tplHistoryPage, tplRevisionPage, tplDiffPage, tplInLinksPage,
-        tplNav, tplFooter, tplSearchResults, tplLogin,
-        tplErrorNotAuthorized, tplErrorNotFound, tplErrorUnauthorizedEdit, tplStateWarning,
-        tplSpecialNav, tplSpecialPages, tplSpecialChanges, tplSpecialPageList) {
+    function($, _) {
 
     var exports = {};
 
     // JQuery feature for watching size changes in a DOM element.
-    jQuery.fn.watch = function(id, fn) {
+    /*jQuery.fn.watch = function(id, fn) {
         return this.each(function() {
             var self = this;
             var oldVal = self[id];
@@ -66,11 +32,11 @@
         return this.each(function() {
             clearInterval($(this).data('watch_timer'));
         });
-    };
+    };*/
 
     // Override JQuery-validation plugin's way of highlighting errors
     // with something that works with Bootstrap.
-    $.validator.setDefaults({
+    /*$.validator.setDefaults({
         highlight: function(element) {
             $(element).closest('.form-group')
                 .addClass('has-error')
@@ -90,376 +56,9 @@
                 error.insertAfter(element);
             }
         }
-    });
-
-    // Utility function to make wiki links into usable links for
-    // this UI frontend.
-    var processWikiLinks = function(el) {
-        $('a.wiki-link', el).each(function(i) {
-            var jel = $(this);
-            var wiki_url = jel.attr('data-wiki-url').replace(/^\//, '');
-            if (jel.hasClass('missing') || jel.attr('data-action') == 'edit')
-                jel.attr('href', '/#/edit/' + wiki_url);
-            else
-                jel.attr('href', '/#/read/' + wiki_url);
-        });
-    };
-
-    var PageView = exports.PageView = Backbone.View.extend({
-        tagName: 'div',
-        className: 'wrapper',
-        isMainPage: true,
-        initialize: function() {
-            PageView.__super__.initialize.apply(this, arguments);
-            if (this.model)
-                this.model.on("change", this._onModelChange, this);
-            return this;
-        },
-        dispose: function() {
-            this.remove();
-            this.unbind();
-            if (this.model) {
-                this.model.unbind();
-            }
-            if (this._onDispose) {
-                this._onDispose();
-            }
-        },
-        render: function(view) {
-            if (this.template === undefined && this.templateSource !== undefined) {
-                this.template = Handlebars.compile(_.result(this, 'templateSource'));
-            }
-            if (this.template !== undefined) {
-                var markup = this.renderTemplate(this.template);
-                var $markup = $(markup);
-                if (this.preRenderCallback !== undefined) {
-                    this.preRenderCallback($markup);
-                }
-                this.$el.empty().append($markup);
-                if (this.renderCallback !== undefined) {
-                    this.renderCallback();
-                }
-            }
-            if (this.isMainPage) {
-                this.renderTitle(this.titleFormat);
-            }
-            return this;
-        },
-        renderTemplate: function(tpl) {
-            var ctx = this.renderContext();
-            return tpl(ctx);
-        },
-        renderContext: function() {
-            return this.model.toJSON();
-        },
-        renderTitle: function(formatter) {
-            var title = _.result(this.model, 'title');
-            if (formatter !== undefined) {
-                title = formatter.call(this, title);
-            }
-            document.title = title;
-        },
-        _onModelChange: function() {
-            this.render();
-        }
-    });
-    _.extend(PageView, Backbone.Events);
-
-    var NavigationView = exports.NavigationView = PageView.extend({
-        className: 'nav-wrapper',
-        templateSource: tplNav,
-        isMainPage: false,
-        initialize: function() {
-            NavigationView.__super__.initialize.apply(this, arguments);
-            return this;
-        },
-        renderContext: function() {
-            var ctx = NavigationView.__super__.renderContext.apply(this, arguments);
-            var ima = localStorage.getItem('wikked.nav.isMenuActive');
-            if (ima == 'true') ctx.showMenu = true;
-            return ctx;
-        },
-        renderCallback: function() {
-            // Hide the drop-down for the search results.
-            this.searchPreviewList = this.$('#search-preview');
-            this.searchPreviewList.hide();
-            this.activeResultIndex = -1;
+    });*/
 
-            // Cache some stuff for handling the menu.
-            this.wikiMenu = this.$('#wiki-menu');
-            this.wrapperAndWikiMenu = $([this.parentEl, this.$('#wiki-menu')]);
-            this.isMenuActive = (this.wikiMenu.css('left') == '0px');
-            this.isMenuActiveLocked = false;
-        },
-        events: {
-            "click #wiki-menu-shortcut": "_onMenuShortcutClick",
-            "click #wiki-menu-pin": "_onMenuShortcutClick",
-            "mouseenter #wiki-menu-shortcut": "_onMenuShortcutHover",
-            "mouseleave #wiki-menu-shortcut": "_onMenuShortcutLeave",
-            "mouseenter #wiki-menu": "_onMenuHover",
-            "mouseleave #wiki-menu": "_onMenuLeave",
-            "submit #search": "_submitSearch",
-            "submit #newpage": "_submitNewPage",
-            "input #search-query": "_previewSearch",
-            "keyup #search-query": "_searchQueryChanged",
-            "focus #search-query": "_searchQueryFocused",
-            "blur #search-query": "_searchQueryBlurred"
-        },
-        _onMenuShortcutClick: function(e) {
-            this.isMenuActive = !this.isMenuActive;
-            localStorage.setItem('wikked.nav.isMenuActive', this.isMenuActive);
-        },
-        _onMenuShortcutHover: function(e) {
-            if (!this.isMenuActive && !this.isMenuActiveLocked)
-                this._toggleWikiMenu(true);
-        },
-        _onMenuShortcutLeave: function(e) {
-            if (!this.isMenuActive && !this.isMenuActiveLocked)
-                this._toggleWikiMenu(false);
-        },
-        _onMenuHover: function(e) {
-            if (!this.isMenuActive && !this.isMenuActiveLocked)
-                this._toggleWikiMenu(true);
-        },
-        _onMenuLeave: function(e) {
-            if (!this.isMenuActive && !this.isMenuActiveLocked)
-                this._toggleWikiMenu(false);
-        },
-        _toggleWikiMenu: function(onOff) {
-            if (onOff) {
-                this.wrapperAndWikiMenu.toggleClass('wiki-menu-inactive', false);
-                this.wrapperAndWikiMenu.toggleClass('wiki-menu-active', true);
-            } else {
-                this.wrapperAndWikiMenu.toggleClass('wiki-menu-active', false);
-                this.wrapperAndWikiMenu.toggleClass('wiki-menu-inactive', true);
-            }
-        },
-        _submitSearch: function(e) {
-            e.preventDefault();
-            if (this.activeResultIndex >= 0) {
-                var entries = this.searchPreviewList.children();
-                var choice = $('a', entries[this.activeResultIndex]);
-                this.model.doGoToSearchResult(choice.attr('href'));
-            } else {
-                this.model.doSearch(e.currentTarget);
-            }
-            return false;
-        },
-        _submitNewPage: function(e) {
-            e.preventDefault();
-            this.model.doNewPage(e.currentTarget);
-            return false;
-        },
-        _previewSearch: function(e) {
-            var query = $(e.currentTarget).val();
-            if (query && query.length >= 1) {
-                var $view = this;
-                this.model.doPreviewSearch(query, function(data) {
-                    var resultStr = '';
-                    for (var i = 0; i < data.hits.length; ++i) {
-                        var hitUrl = data.hits[i].url.replace(/^\//, '');
-                        resultStr += '<li>' +
-                            '<a href="/#read/' + hitUrl + '">' +
-                            data.hits[i].title +
-                            '</a>' +
-                            '</li>';
-                    }
-                    $view.searchPreviewList.html(resultStr);
-                    if (!$view.searchPreviewList.is(':visible'))
-                        $view.searchPreviewList.slideDown(200);
-                });
-            } else if(!query || query.length === 0) {
-                this.searchPreviewList.slideUp(200);
-            }
-        },
-        _searchQueryChanged: function(e) {
-            if (e.keyCode == 27) {
-                // Clear search on `Esc`.
-                $(e.currentTarget).val('').trigger('input');
-            } else if (e.keyCode == 38) {
-                // Up arrow.
-                e.preventDefault();
-                if (this.activeResultIndex >= 0) {
-                    this.activeResultIndex--;
-                    this._updateActiveResult();
-                }
-            } else if (e.keyCode == 40) {
-                // Down arrow.
-                e.preventDefault();
-                if (this.activeResultIndex < 
-                        this.searchPreviewList.children().length - 1) {
-                    this.activeResultIndex++;
-                    this._updateActiveResult();
-                }
-            }
-        },
-        _updateActiveResult: function() {
-            var entries = this.searchPreviewList.children();
-            entries.toggleClass('search-result-hover', false);
-            if (this.activeResultIndex >= 0)
-                $(entries[this.activeResultIndex]).toggleClass('search-result-hover', true);
-        },
-        _searchQueryFocused: function(e) {
-            this.isMenuActiveLocked = true;
-            this.wikiMenu.toggleClass('wiki-menu-ext', true);
-        },
-        _searchQueryBlurred: function(e) {
-            $(e.currentTarget).val('').trigger('input');
-            this.wikiMenu.toggleClass('wiki-menu-ext', false);
-            this.isMenuActiveLocked = false;
-            if ($(document.activeElement).parents('#wiki-menu').length === 0)
-                this._onMenuLeave(e);
-        }
-    });
-
-    var FooterView = exports.FooterView = PageView.extend({
-        className: 'footer-wrapper',
-        templateSource: tplFooter,
-        isMainPage: false,
-        initialize: function() {
-            FooterView.__super__.initialize.apply(this, arguments);
-            return this;
-        },
-        render: function() {
-            FooterView.__super__.render.apply(this, arguments);
-            return this;
-        }
-    });
-
-    var LoginView = exports.LoginView = PageView.extend({
-        templateSource: tplLogin,
-        events: {
-            "submit #login": "_submitLogin"
-        },
-        _submitLogin: function(e) {
-            e.preventDefault();
-            this.model.doLogin(e.currentTarget);
-            return false;
-        }
-    });
-
-    var MasterPageView = exports.MasterPageView = PageView.extend({
-        className: function() {
-            var cls = 'wrapper';
-
-            // HACK-ish: we need to know if the menu needs to be shown 
-            //           on init or not. Can't do it later otherwise
-            //           we'll get the CSS animation to play right away.
-            var ima = localStorage.getItem('wikked.nav.isMenuActive');
-            if (ima == 'true') cls += ' wiki-menu-active';
-
-            return cls;
-        },
-        initialize: function() {
-            MasterPageView.__super__.initialize.apply(this, arguments);
-            this.nav = this._createNavigation(this.model.nav);
-            this.nav.parentEl = this.el;
-            this.footer = this._createFooter(this.model.footer);
-            return this;
-        },
-        dispose: function() {
-            if (this.footer) this.footer.dispose();
-            if (this.nav) this.nav.dispose();
-            MasterPageView.__super__.dispose.apply(this, arguments);
-        },
-        renderCallback: function() {
-            if (this.nav) {
-                this.nav.render();
-                this.$el.prepend(this.nav.el);
-            }
-            if (this.footer) {
-                this.footer.render();
-                this.$el.append(this.footer.el);
-            }
-            this.isError = (this.model.get('error_code') !== undefined);
-        },
-        templateSource: function() {
-            switch (this.model.get('error_code')) {
-                case 401:
-                    return tplErrorNotAuthorized;
-                case 404:
-                    return tplErrorNotFound;
-                default:
-                    return _.result(this, 'defaultTemplateSource');
-            }
-        },
-        _createNavigation: function(model) {
-            return new NavigationView({ model: model });
-        },
-        _createFooter: function(model) {
-            return new FooterView({ model: model });
-        }
-    });
-
-    var PageReadView = exports.PageReadView = MasterPageView.extend({
-        defaultTemplateSource: tplReadPage,
-        initialize: function() {
-            PageReadView.__super__.initialize.apply(this, arguments);
-            this.warningTemplate = Handlebars.compile(tplStateWarning);
-            return this;
-        },
-        renderCallback: function() {
-            PageReadView.__super__.renderCallback.apply(this, arguments);
-            if (this.isError) {
-                return;
-            }
-
-            processWikiLinks(this.$el);
-
-            // If we've already rendered the content, see if we need to display a
-            // warning about the page's state.
-            if (this.model.get('content')) {
-                if (this._pageState === undefined) {
-                    if (!this._isCheckingPageState)
-                        this._checkPageState();
-                } else {
-                    this._showPageStateWarning();
-                }
-            }
-        },
-        _showPageStateWarning: function() {
-            if (this._pageState === undefined)
-                return;
-
-            var state = this._pageState.get('state');
-            if (state == 'new' || state == 'modified') {
-                var article = $('.wrapper>article');
-                var warning = $(this.warningTemplate(this._pageState.toJSON()));
-                $('[rel="tooltip"]', warning).tooltip({container:'body'});
-                //warning.css('display', 'none');
-                warning.prependTo(article);
-                //warning.slideDown();
-                $('.dismiss', warning).click(function() {
-                    //warning.slideUp();
-                    warning.remove();
-                    return false;
-                });
-            }
-        },
-        _enableStateCheck: false,
-        _isCheckingPageState: false,
-        _checkPageState: function() {
-            if (!this._enableStateCheck)
-                return;
-            this._isCheckingPageState = true;
-            var $view = this;
-            var statePath = this.model.checkStatePath();
-            if (!statePath)
-                return;
-            var stateModel = new Models.PageStateModel({ path: statePath });
-            stateModel.fetch({
-                success: function(model, response, options) {
-                    $view._pageState = model;
-                    $view._isCheckingPageState = false;
-                    if ($view.model && $view.model.get('content')) {
-                        $view._showPageStateWarning();
-                    }
-                }
-            });
-        }
-    });
-
-    var PageEditView = exports.PageEditView = MasterPageView.extend({
+    /*var PageEditView = exports.PageEditView = MasterPageView.extend({
         defaultTemplateSource: tplEditPage,
         dispose: function() {
             PageEditView.__super__.dispose.apply(this, arguments);
@@ -560,71 +159,9 @@
         titleFormat: function(title) {
             return 'Editing: ' + title;
         }
-    });
-
-    var PageHistoryView = exports.PageHistoryView = MasterPageView.extend({
-        defaultTemplateSource: tplHistoryPage,
-        events: {
-            "submit #diff-page": "_submitDiffPage"
-        },
-        _submitDiffPage: function(e) {
-            e.preventDefault();
-            this.model.doDiff(e.currentTarget);
-            return false;
-        },
-        titleFormat: function(title) {
-            return 'History: ' + title;
-        }
-    });
-
-    var PageRevisionView = exports.PageRevisionView = MasterPageView.extend({
-        defaultTemplateSource: tplRevisionPage,
-        titleFormat: function(title) {
-            return title + ' [' + this.model.get('rev') + ']';
-        },
-        events: {
-            "submit #revert-page": "_submitPageRevert"
-        },
-        _submitPageRevert: function(e) {
-            e.preventDefault();
-            this.model.doRevert(e.currentTarget);
-            return false;
-        }
-    });
+    });*/
 
-    var PageDiffView = exports.PageDiffView = MasterPageView.extend({
-        defaultTemplateSource: tplDiffPage,
-        titleFormat: function(title) {
-            return title + ' [' + this.model.get('rev1') + '-' + this.model.get('rev2') + ']';
-        }
-    });
-
-    var IncomingLinksView = exports.IncomingLinksView = MasterPageView.extend({
-        defaultTemplateSource: tplInLinksPage,
-        titleFormat: function(title) {
-            return 'Incoming Links: ' + title;
-        }
-    });
-
-    var WikiSearchView = exports.WikiSearchView = MasterPageView.extend({
-        defaultTemplateSource: tplSearchResults
-    });
-
-    var SpecialMasterPageView = exports.SpecialMasterPageView = MasterPageView.extend({
-        className: function() {
-            var cls = 'wrapper special';
-            // See comment for `MasterPageView`.
-            var ima = localStorage.getItem('wikked.nav.isMenuActive');
-            if (ima == 'true') cls += ' wiki-menu-active';
-            return cls;
-        }
-    });
-
-    var SpecialPagesView = exports.SpecialPagesView = SpecialMasterPageView.extend({
-        defaultTemplateSource: tplSpecialPages
-    });
-
-    var SpecialChangesView = exports.SpecialChangesView = SpecialMasterPageView.extend({
+    /*var SpecialChangesView = exports.SpecialChangesView = SpecialMasterPageView.extend({
         defaultTemplateSource: tplSpecialChanges,
         renderCallback: function() {
             SpecialChangesView.__super__.renderCallback.apply(this, arguments);
@@ -650,11 +187,7 @@
                 e.preventDefault();
             });
         }
-    });
-
-    var SpecialPageListView = exports.SpecialPageListView = SpecialMasterPageView.extend({
-        defaultTemplateSource: tplSpecialPageList
-    });
+    });*/
 
     return exports;
 });
--- a/wikked/assets/json/special-pagelists.json	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-{
-    "titles": {
-        "orphans": "Orphaned Pages",
-        "broken-redirects": "Broken Redirects",
-        "double-redirects": "Double Redirects",
-        "dead-ends": "Dead-Ends"
-    },
-    "messages": {
-        "orphans": "Here is a list of pages that don't have any pages linking to them. This means user will only be able to find them by searching for them, or by getting a direct link.",
-        "broken-redirects":
-            "Here is a list of pages that redirect to a non-existing page.",
-        "double-redirects":
-            "Here is a list of pages that redirect twice or more.",
-        "dead-ends":
-            "Here is a list of pages that don't have any outgoing links."
-    },
-    "asides": {
-        "orphans":
-            "The main page usually shows up here but that's OK since it's the page everyone sees first."
-    },
-    "empties": {
-        "orphans": "No orphaned pages!",
-        "broken-redirects": "No broken redirects!",
-        "double-redirects": "No double redirects!",
-        "dead-ends": "No dead-end pages!"
-    },
-    "url_suffixes": {
-        "broken-redirects": "?no_redirect"
-    }
-}
-
--- a/wikked/assets/json/special-sections.json	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-{
-    "sections": [
-        {
-            "title": "Wiki",
-            "pages": [
-                {
-                    "title": "Recent Changes",
-                    "url": "/#/special/changes",
-                    "description": "See all changes in the wiki."
-                }
-            ]
-        },
-        {
-            "title": "Page Lists",
-            "pages": [
-                {
-                    "title": "Orphaned Pages",
-                    "url": "/#/special/list/orphans",
-                    "description": "Lists pages in the wiki that have no links to them."
-                },
-                {
-                    "title": "Broken Redirects",
-                    "url": "/#/special/list/broken-redirects",
-                    "description": "Lists pages that redirect to a missing page."
-                },
-                {
-                    "title": "Double Redirects",
-                    "url": "/#/special/list/double-redirects",
-                    "description": "Lists pages that redirect twice or more."
-                },
-                {
-                    "title": "Dead-End Pages",
-                    "url": "/#/special/list/dead-ends",
-                    "description": "Lists pages that don't have any outgoing links."
-                }
-            ]
-        },
-        {
-            "title": "Users",
-            "pages": [
-                {
-                    "title": "All Users",
-                    "url": "/#/special/users",
-                    "description": "A list of all registered users."
-                }
-            ]
-        }
-    ]
-}
--- a/wikked/assets/tpl/404.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-<a id="wiki-menu-shortcut" class="wiki-logo">
-    <span>W</span>
-</a>
-<nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open">
-    <ul class="">
-        <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
-    </ul>
-</nav>
-<article class="row">
-    <header>
-        <h1>Page Not Found</h1>
-    </header>
-    <section>
-        <p>The page you requested was not found.</p>
-    </section>
-</article>
--- a/wikked/assets/tpl/category.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-<article>
-    <header>
-        <h1>{{query.category}}</h1>
-        <div class="decorator">Category</div>
-    </header>
-    <section>
-        {{content}}
-        <h2>Pages in category "{{query.category}}"</h2>
-        <ul class="list-category">
-        {{#each pages}}
-        <li><a href="{{get_read_url url}}">{{title}}</a></li>
-        {{/each}}
-        </ul>
-    </section>
-    <footer>
-    </footer>
-</article>
--- a/wikked/assets/tpl/diff-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-<article>
-    <header>
-        <h1>{{meta.title}}</h1>
-        <div class="decorator">Diff
-            {{#if disp_rev2}}
-            <span class="rev_id">{{disp_rev1}}</span> to <span class="rev_id">{{disp_rev2}}</span>
-            {{else}}
-            change <span class="rev_id">{{disp_rev1}}</span>
-            {{/if}}
-        </div>
-    </header>
-    <section>
-        <pre><code>{{{diff}}}</code></pre>
-    </section>
-</article>
--- a/wikked/assets/tpl/edit-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-<article>
-    <form id="page-edit" class="pure-form">
-        <fieldset>
-            <div class="header-wrapper">
-                <header>
-                    {{#if is_new}}
-                    <div class="pure-control-group pure-control-addon">
-                        <label for="title" class="">Title</label>
-                        <input type="text" name="title" placeholder="New Page" required="true" class=""></input>
-                    </div>
-                    <div class="pure-form-help">
-                        <span for="title">You can put this page in a folder by entering a title like <code>Folder/Page Title</code>.</span>
-                    </div>
-                    {{else}}
-                    <h1>{{meta.title}}</h1>
-                    <div class="decorator">Editing</div>
-                    {{/if}}
-                </header>
-            </div>
-            <div class="editing-wrapper">
-                <div class="editing">
-                    <section class="editing-error alert alert-danger">
-                        <p><strong>Error:</strong> <span class="editing-error-message"></span></p>
-                    </section>
-                    <section class="editing-input">
-                        <textarea id="editing-input-area" name="text" placeholder="Your page's contents go here...">{{content}}</textarea>
-                        <div id="editing-input-grip"></div>
-                    </section>
-                    <section class="editing-preview">
-                    </section>
-                    <section class="editing-meta">
-                        <div class="pure-control-group pure-control-addon">
-                            <label for="author">Author</label>
-                            <input type="text" name="author" class="form-control" placeholder="{{commit_meta.author}}"></input>
-                        </div>
-                        <div class="pure-control-group pure-control-addon">
-                            <label for="message">Description</label>
-                            <input type="text" name="message" class="form-control" placeholder="{{commit_meta.desc}}" minlength="3"></input>
-                        </div>
-                    </section>
-                    <section class="editing-submit">
-                        <div class="pure-control-group">
-                            <button type="submit" class="pure-button pure-button-primary"><span class="fa fa-check"></span> Save</button>
-                            <button id="editing-preview-button" type="submit" class="pure-button"><span class="fa fa-eye"></span> <span class="editing-preview-button-label">Preview</span></button>
-                            <button id="editing-cancel-button" type="submit" class="pure-button"><span class="fa fa-remove"></span> Cancel</button>
-                        </div>
-                    </section>
-                </div>
-            </div>
-        </fieldset>
-    </form>
-</article>
--- a/wikked/assets/tpl/error-not-found.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-<article>
-    <header>
-        <h1>Not Found</h1>
-    </header>
-    <section>
-        <p>The page you're trying to access does not exist. You can <a href="{{get_edit_url path}}">create it</a>.</p>
-    </section>
-</article>
--- a/wikked/assets/tpl/error-unauthorized-edit.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<article>
-    <header>
-        <h1>You're not authorized to edit this page</h1>
-    </header>
-    <section>
-        <p>The page you're trying to edit is protected.
-                Please <a href="/#/login">log into an account</a> that has write access to it.</p>
-    </section>
-</article>
--- a/wikked/assets/tpl/error-unauthorized.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<article>
-    <header>
-        <h1>You're not authorized for this</h1>
-    </header>
-    <section>
-        <p>The page you're trying to access is protected.
-                Please <a href="/#/login">log into an account</a> that has access to it.</p>
-    </section>
-</article>
--- a/wikked/assets/tpl/footer.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<footer role="navigation">
-    <ul>
-        <li><span class="wiki-icon"></span> Powered by <a href="http://bolt80.com/wikked/">Wikked</a></li>
-        {{#each url_extras}}
-        <li>{{#if icon}}<span class="fa fa-{{icon}}"></span> {{/if}}<a href="{{url}}">{{name}}</a></li>
-        {{/each}}
-        <!-- TODO: last modified, etc. -->
-    </ul>
-</footer>
--- a/wikked/assets/tpl/history-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-<article>
-    <header>
-        <h1>{{meta.title}}</h1>
-        <div class="decorator">History</div>
-    </header>
-    <section>
-        {{#if history}}
-        <p>Here's the revision log for <a href="{{url_read}}">{{meta.title}}</a>.</p>
-        <form id="diff-page">
-            <table class="pure-table pure-table-bordered">
-                <thead>
-                    <tr>
-                        <th>Revision</th>
-                        <th>Date</th>
-                        <th>Author</th>
-                        <th>Comment</th>
-                        <th><button id="diff-revs" class="pure-button">Show Diff.</button></th>
-                    </tr>
-                </thead>
-                <tbody>
-                    {{#eachr history}}
-                    <tr>
-                        <td><a href="{{../url_rev}}/{{rev_id}}">{{rev_name}}</a></td>
-                        <td>{{date_from_now timestamp}}</td>
-                        <td>{{author}}</td>
-                        <td>{{description}}</td>
-                        <td>
-                            <input type="radio" name="rev1" value="{{rev_id}}" {{#ifeq @index to=0 }}checked="true" {{/ifeq}}/>
-                            <input type="radio" name="rev2" value="{{rev_id}}" {{#ifeq @index to=1 }}checked="true" {{/ifeq}}/>
-                            <small><a href="{{../url_diffc}}/{{rev_id}}">with previous</a></small>
-                        </td>
-                    </tr>
-                    {{/eachr}}
-                </tbody>
-            </table>
-        </form>
-        {{else}}
-        <p><i class="icon-warning-sign large big"></i>
-            This page has not been committed to the repository yet.
-            <a href="{{url_edit}}">Edit it</a> to do that now.</p>
-        {{/if}}
-    </section>
-</article>
--- a/wikked/assets/tpl/inlinks-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-<article>
-    <header>
-        <h1>{{meta.title}}</h1>
-        <div class="decorator">Incoming Links</div>
-    </header>
-    <section>
-        <p>The following pages link to <a href="{{get_read_url meta.url}}">{{meta.title}}</a>:</p>
-        <ul>
-        {{#each in_links}}
-            <li>
-                {{#if missing}}
-                <a class="wiki-link missing" href="{{get_edit_url url}}">{{url}}</a>
-                {{else}}
-                <a class="wiki-link" href="{{get_read_url url}}">{{title}}</a>
-                {{/if}}
-            </li>
-        {{/each}}
-        </ul>
-    </section>
-</article>
--- a/wikked/assets/tpl/login.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-<div class="nav-wrapper">
-    <a id="wiki-menu-shortcut" class="wiki-logo">
-        <span>W</span>
-    </a>
-    <nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open">
-        <ul class="">
-            <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
-        </ul>
-    </nav>
-</div>
-<article>
-    <header>
-        <h1>Login</h1>
-    </header>
-    <section>
-        {{#if has_error}}
-        <div class="alert alert-danger alert-dismissable">
-            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
-            <strong>Begone!</strong> Those credentials don't seem to work here.
-        </div>
-        {{/if}}
-        <form id="login" class="pure-form pure-form-stacked" role="form">
-            <fieldset>
-                <input type="text" name="username" for="remember" class="form-control" placeholder="Username" required="true" autofocus="true"></input>
-                <input type="password" name="password" placeholder="Password" required="true"></input>
-                <label for="remember" class="pure-checkbox">
-                    <input type="checkbox" name="remember" value="remember-me"> Remember me</input>
-                </label>
-                <button class="pure-button pure-button-primary" type="submit">Log in</button>
-            </fieldset>
-        </form>
-    </section>
-</article>
--- a/wikked/assets/tpl/meta-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-<article>
-    <header>
-        <h1>{{meta_value}}</h1>
-        <div class="decorator">{{meta_query}}</div>
-    </header>
-    <section>
-        {{content}}
-    </section>
-    <footer>
-    </footer>
-</article>
--- a/wikked/assets/tpl/nav.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-<a id="wiki-menu-shortcut" class="wiki-logo">
-    <span>W</span>
-</a>
-<nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open{{#if showMenu}} wiki-menu-active{{/if}}">
-    <div id="wiki-menu-pin" title="Pin/unpin the wiki menu.">
-        <span class="fa fa-lock"></span>
-    </div>
-    <ul class="">
-        <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
-        <li><a href="/#/create/"><span class="fa fa-file"></span> New Page</a></li>
-        {{#if url_read}}<li><a href="{{url_read}}"><span class="fa fa-book"></span> Read</a></li>{{/if}}
-        {{#if url_edit}}<li><a href="{{url_edit}}"><span class="fa fa-edit"></span> Edit</a></li>{{/if}}
-        {{#if url_hist}}<li><a href="{{url_hist}}"><span class="fa fa-road"></span> History</a></li>{{/if}}
-    </ul>
-    <form role="search" id="search" class="pure-form pure-menu-form pure-menu-divider">
-        <fieldset>
-            <input type="text" name="q" id="search-query" class="pure-input-1" placeholder="Search..." autocomplete="off"></input>
-            <ul id="search-preview"></ul>
-        </fieldset>
-    </form>
-    {{#if url_extras}}
-    <ul class="pure-menu-divider">
-    {{#each url_extras}}
-        <li><a href="{{url}}">{{#if icon}}<span class="fa fa-{{icon}}"></span> {{/if}}{{name}}</a></li>
-    {{/each}}
-    </ul>
-    {{/if}}
-    <ul class="pure-menu-divider">
-        {{#if user}}
-        <li><a href="{{url_profile}}"><span class="fa fa-cog"></span> {{user.username}}</a></li>
-        <li><a href="{{url_logout}}"><span class="fa fa-log-out"></span> Logout</a></li>
-        {{else}}
-        <li><a href="{{url_login}}"><span class="fa fa-log-in"></span> Login</a></li>
-        {{/if}}
-    </ul>
-</nav>
--- a/wikked/assets/tpl/read-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-<article>
-    {{#ifnot meta.notitle}}
-    <header>
-        <h1>{{meta.title}}</h1>
-        {{#if endpoint}}
-        <div class="decorator">{{endpoint}}</div>
-        {{/if}}
-        {{#if redirected_from}}
-        <div class="decorator"><small>Redirected from 
-            {{#each redirected_from}}
-            {{#ifeq @index to=0}} {{else}} | {{/ifeq}}
-            <a href="{{get_read_url this}}?no_redirect">{{this}}</a>
-            {{/each}}
-        </small></div>
-        {{/if}}
-        {{#if meta.redirect}}
-        <div class="decorator"><small>Redirects to
-                <a href="{{get_read_url redirects_to}}">{{redirects_to}}</a>
-        </small></div>
-        {{/if}}
-    </header>
-    {{/ifnot}}
-    <section class="content">
-        {{content}}
-    </section>
-    {{#if meta.category}}
-    <section class="info">
-        {{#if meta.category}}
-        <span class="info-categories">Categories: </span>
-        {{#each meta.category}}
-        {{#ifeq @index to=0}} {{else}} | {{/ifeq}}
-        <span><a href="{{get_cat_url url}}">{{name}}</a></span>
-        {{/each}}
-        {{/if}}
-    </section>
-    {{/if}}
-</article>
--- a/wikked/assets/tpl/revision-page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-<article>
-    <header>
-        <h1>{{meta.title}}</h1>
-        <div class="decorator">Revision: <span class="rev_id">{{disp_rev}}</span></div>
-    </header>
-    <section>
-        <pre class="raw">{{text}}</pre>
-        <form id="page-revert" class="page-revert">
-            <input type="hidden" name="rev" value="{{rev}}"/>
-            <div class="form-group">
-                <button type="submit" id="revert-page" class="pure-button pure-button-warning">Revert</button>
-                <label class="small" for="revert-page">Revert the page to this revision</span>
-            </div>
-        </form>
-    </section>
-</div>
-</article>
--- a/wikked/assets/tpl/search-results.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<article>
-    <header>
-        <h1>Search Results</h1>
-    </header>
-    <section>
-        {{#if is_instant}}
-        <p><small>Press <code>Escape</code> to cancel.</small></p>
-        {{/if}}
-        {{#if hits}}
-        <ul class="search-results">
-            {{#each hits}}
-            <li>
-            <h3><a href="{{get_read_url url}}">{{title}}</a></h3>
-            <div class="highlighted">{{{text}}}</div>
-            </li>
-            {{/each}}
-        </ul>
-        {{else}}
-        <p>No matches found.</p>
-        {{/if}}
-    </section>
-</article>
--- a/wikked/assets/tpl/special-changes.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-<article>
-    <header>
-        <h1>Wiki History</h1>
-    </header>
-    <section>
-        <p>Here are the recent changes on this wiki.</p>
-        <form>
-            <table class="pure-table pure-table-bordered wiki-history">
-                <thead>
-                    <tr>
-                        <th>Revision</th>
-                        <th>Information</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    {{#eachr history}}
-                    <tr class='wiki-history-entry'>
-                        <td><code>{{rev_name}}</code></td>
-                        <td>
-                            <dl class="pure-dl-horizontal">
-                                <dt>Date</dt>
-                                <dd>{{date_from_now timestamp}}</dd>
-                                <dt>Author</dt>
-                                <dd>{{author}}</dd>
-                                <dt>Pages ({{num_pages}})</dt>
-                                <dd>
-                                {{#if collapsed}}
-                                    <button class="pure-button wiki-history-entry-collapser" data-index="{{index}}">
-                                        <span class="fa fa-chevron-down"></span>
-                                        <small>Show</small>
-                                    </button>
-                                    <div class="wiki-history-entry-details wiki-history-entry-details-{{index}}">
-                                {{/if}}
-                                    <ul class="pure-ul-unstyled">
-                                        {{#each pages}}
-                                        <li>
-                                            <a href="#/revision{{url}}/{{../rev_id}}">{{url}}</a>
-                                            {{action_label}}
-                                        </li>
-                                        {{/each}}
-                                    </ul>
-                                {{#if collapsed}}
-                                    </div>
-                                {{/if}}
-                                </dd>
-                                <dt>Comment</dt>
-                                <dd>{{description}}</dd>
-                            </dl>
-                        </td>
-                    </tr>
-                    {{/eachr}}
-                </tbody>
-            </table>
-            <div class="wiki-history-pagination pure-g">
-                <div class="pure-u-1-2">
-                    <a href="{{first_page}}">First page</a>
-                </div>
-                <div class="pure-u-1-2">
-                    <a href="{{next_page}}">Next page</a>
-                </div>
-            </div>
-        </form>
-    </section>
-</article>
--- a/wikked/assets/tpl/special-nav.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-<a id="wiki-menu-shortcut" class="wiki-logo">
-    <span>W</span>
-</a>
-<nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open">
-    <ul class="">
-        <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
-        {{#if show_root_link}}
-        <li><a href="/#/special"><span class="fa fa-dashboard"></span> Special Pages</a></li>
-        {{/if}}
-        <li><a href="/#/create/"><span class="fa fa-file"></span> New Page</a></li>
-    </ul>
-    <form role="search" id="search" class="pure-form pure-menu-form pure-menu-divider">
-        <fieldset>
-            <input type="text" name="q" id="search-query" class="pure-input-1" placeholder="Search..." autocomplete="off"></input>
-            <ul id="search-preview"></ul>
-        </fieldset>
-    </form>
-    <ul class="pure-menu-divider">
-        {{#if user}}
-        <li><a href="{{url_profile}}"><span class="fa fa-cog"></span> {{user.username}}</a></li>
-        <li><a href="{{url_logout}}"><span class="fa fa-log-out"></span> Logout</a></li>
-        {{else}}
-        <li><a href="{{url_login}}"><span class="fa fa-log-in"></span> Login</a></li>
-        {{/if}}
-    </ul>
-</nav>
--- a/wikked/assets/tpl/special-pagelist.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-<article>
-    <header>
-        <h1>{{title}}</h1>
-    </header>
-    <section>
-        <p>{{message}}</p>
-        {{#if aside}}
-        <aside><p>{{aside}}</p></aside>
-        {{/if}}
-        {{#if pages}}
-        <ul>
-        {{#each pages}}
-            <li><a href="{{get_read_url url}}{{../url_suffix}}">{{title}}</a></li>
-        {{/each}}
-        </ul>
-        {{else}}
-        <p>{{empty}}</p>
-        {{/if}}
-    </section>
-</article>
--- a/wikked/assets/tpl/special-pages.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-<article>
-    <header>
-        <h1>Special Pages</h1>
-    </header>
-    <section>
-        <div class="pure-g">
-            {{#each sections}}
-            <div class="pure-u-1-3">
-                <h2>{{title}}</h2>
-                {{#each pages}}
-                <h3><a href="{{url}}">{{title}}</a></h3>
-                {{#if description}}
-                <p>{{description}}</p>
-                {{/if}}
-                {{/each}}
-            </div>
-            {{/each}}
-        </div>
-    </section>
-</article>
--- a/wikked/assets/tpl/state-warning.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-<div class="ribbon state-warning">
-    <div class="ribbon-inner">
-        <span><a href="{{url_edit}}" rel="tooltip" title="This page is {{state}} in the repository. You can edit it now to commit." data-toggle="tooltip" data-placement="bottom">modified <i class="icon-info-sign icon-white"></i></a></span>
-    </div>
-</div>
--- a/wikked/commands/web.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/commands/web.py	Wed Sep 16 23:04:28 2015 -0700
@@ -37,11 +37,14 @@
                      "uncompressed scripts and stylesheets), along with using "
                      "code reloading and debugging.",
                 action='store_true')
-        parser.add_argument('--noupdate',
+        parser.add_argument('--no-update',
                 help="Don't auto-update the wiki if a page file has been "
                      "touched (which means you can refresh a locally modified "
                      "page with F5)",
                 action='store_true')
+        parser.add_argument('--no-startup-update',
+                help="Don't update the wiki before starting the server.",
+                action='store_true')
         parser.add_argument('-c', '--config',
                 help="Pass some configuration value to the Flask application. "
                      "This must be of the form: name=value",
@@ -75,7 +78,7 @@
         wikked.settings.WIKI_SERVE_FILES = True
         if ctx.args.dev:
             wikked.settings.WIKI_DEV_ASSETS = True
-        if not ctx.args.noupdate:
+        if not ctx.args.no_update:
             wikked.settings.WIKI_AUTO_RELOAD = True
             ctx.params.wiki_updater = autoreload_wiki_updater
 
@@ -85,7 +88,8 @@
         ctx.wiki.db.hookupWebApp(app)
 
         # Update if needed.
-        if bool(app.config.get('WIKI_UPDATE_ON_START')):
+        if (bool(app.config.get('WIKI_UPDATE_ON_START')) and
+                not ctx.args.no_startup_update):
             ctx.wiki.updateAll()
 
         # Run!
--- a/wikked/formatter.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/formatter.py	Wed Sep 16 23:04:28 2015 -0700
@@ -198,7 +198,7 @@
         else:
             abs_url = os.path.join('/files', ctx.urldir, value)
             abs_url = os.path.normpath(abs_url).replace('\\', '/')
-        return '<a class="wiki-asset" href="%s">%s</a>' % (abs_url, display)
+        return abs_url
 
     def _formatAssetLink(self, ctx, endpoint, value, display):
         img_exts = ['.jpg', '.jpeg', '.png', '.gif']
@@ -210,7 +210,8 @@
             abs_url = os.path.normpath(abs_url).replace('\\', '/')
 
         if ext in img_exts:
-            return '<img src="%s" alt="%s"></img>' % (abs_url, display)
+            return ('<img class="wiki-asset" src="%s" alt="%s"></img>' %
+                    (abs_url, display))
 
         return '<a class="wiki-asset" href="%s">%s</a>' % (abs_url, display)
 
--- a/wikked/resolver.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/resolver.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,6 +1,6 @@
 import re
-import urllib.request, urllib.parse, urllib.error
 import os.path
+import urllib.parse
 import logging
 import jinja2
 from wikked.formatter import PageFormatter, FormattingContext
@@ -120,7 +120,7 @@
         self.resolvers = {
                 'query': self._runQuery,
                 'include': self._runInclude
-            }
+                }
 
     @property
     def wiki(self):
@@ -153,7 +153,8 @@
         # Create default parameters.
         if not self.parameters:
             urldir = os.path.dirname(self.page.url)
-            full_title = os.path.join(urldir, self.page.title).replace('\\', '/')
+            full_title = os.path.join(
+                    urldir, self.page.title).replace('\\', '/')
             self.parameters = {
                 '__page': {
                     'url': self.page.url,
@@ -202,21 +203,30 @@
         if self.is_root:
             # Resolve any `{{foo}}` variable references.
             parameters = dict(self.parameters)
-            parameters.update(flatten_single_metas(dict(self.page.getLocalMeta())))
-            final_text = self._renderTemplate(final_text, parameters, error_url=self.page.url)
+            parameters.update(
+                    flatten_single_metas(dict(self.page.getLocalMeta())))
+            final_text = self._renderTemplate(
+                    final_text, parameters, error_url=self.page.url)
 
             # Resolve link states.
             def repl1(m):
                 raw_url = m.group('url')
+                is_edit = bool(m.group('isedit'))
                 url = self.ctx.getAbsoluteUrl(raw_url)
                 self.output.out_links.append(url)
+                action = 'edit' if is_edit else 'read'
                 quoted_url = urllib.parse.quote(url.encode('utf-8'))
                 if self.wiki.pageExists(url):
-                    return '<a class="wiki-link" data-wiki-url="%s">' % quoted_url
-                return '<a class="wiki-link missing" data-wiki-url="%s">' % quoted_url
+                    actual_url = '/%s/%s' % (action, quoted_url.lstrip('/'))
+                    return ('<a class="wiki-link" data-wiki-url="%s" '
+                            'href="%s"' % (quoted_url, actual_url))
+                actual_url = '/%s/%s' % (action, quoted_url.lstrip('/'))
+                return ('<a class="wiki-link missing" data-wiki-url="%s" '
+                        'href="%s"' % (quoted_url, actual_url))
 
             final_text = re.sub(
-                    r'<a class="wiki-link" data-wiki-url="(?P<url>[^"]+)">',
+                    r'<a class="wiki-link(?P<isedit>-edit)?" '
+                    r'data-wiki-url="(?P<url>[^"]+)"',
                     repl1,
                     final_text)
 
@@ -249,7 +259,8 @@
 
         # Check for circular includes.
         if include_url in self.ctx.url_trail:
-            raise CircularIncludeError(include_url, self.page.url, self.ctx.url_trail)
+            raise CircularIncludeError(include_url, self.page.url,
+                                       self.ctx.url_trail)
 
         # Parse the templating parameters.
         parameters = dict(self.parameters)
@@ -259,7 +270,9 @@
             # We do not, however, run them through the formatting -- this
             # will be done in one pass when everything is gathered on the
             # root page.
-            arg_pattern = r'<div class="wiki-param" data-name="(?P<name>\w[\w\d]*)?">(?P<value>.*?)</div>'
+            arg_pattern = (r'<div class="wiki-param" '
+                           r'data-name="(?P<name>\w[\w\d]*)?">'
+                           r'(?P<value>.*?)</div>')
             for i, m in enumerate(re.finditer(arg_pattern, args)):
                 value = m.group('value').strip()
                 value = html_unescape(value)
@@ -322,7 +335,8 @@
                     if self._isPageMatch(p, key, value):
                         matched_pages.append(p)
                 except Exception as e:
-                    logger.error("Can't query page '%s' for '%s':" % (p.url, self.page.url))
+                    logger.error("Can't query page '%s' for '%s':" % (
+                            p.url, self.page.url))
                     logger.exception(e.message)
 
         # We'll have to format things...
@@ -341,7 +355,8 @@
                 fmt_ctx, self._valueOrPageText(parameters['__header']))
         tpl_footer = fmt.formatText(
                 fmt_ctx, self._valueOrPageText(parameters['__footer']))
-        item_url, tpl_item = self._valueOrPageText(parameters['__item'], with_url=True)
+        item_url, tpl_item = self._valueOrPageText(parameters['__item'],
+                                                   with_url=True)
         tpl_item = fmt.formatText(fmt_ctx, tpl_item)
 
         text = tpl_header
@@ -368,7 +383,8 @@
             try:
                 page = self.page_getter(include_url)
             except PageNotFoundError:
-                raise IncludeError(include_url, self.page.url, "Page not found")
+                raise IncludeError(include_url, self.page.url,
+                                   "Page not found")
             if with_url:
                 return (page.url, page.text)
             return page.text
@@ -394,7 +410,7 @@
             actual = page.getLocalMeta().get(key)
             if (actual is not None and
                     ((type(actual) is list and value in actual) or
-                    (actual == value))):
+                        (actual == value))):
                 return True
 
         # Gather included pages' URLs.
@@ -445,7 +461,8 @@
                 return k
             known_exts += v
         raise FormatterNotFound(
-            "No formatter mapped to file extension '%s' (known extensions: %s)" %
+            "No formatter mapped to file extension '%s' "
+            "(known extensions: %s)" %
             (extension, known_exts))
 
     def _renderTemplate(self, text, parameters, error_url=None):
@@ -454,7 +471,8 @@
             template = env.from_string(text)
             return template.render(parameters)
         except jinja2.TemplateSyntaxError as tse:
-            raise Exception("Error in '%s': %s\n%s" % (error_url or 'Unknown URL', tse, text))
+            raise Exception("Error in '%s': %s\n%s" % (
+                    error_url or 'Unknown URL', tse, text))
 
     def _getJinjaEnvironment(self):
         if self.env is None:
@@ -467,10 +485,13 @@
 def generate_read_url(value, title=None):
     if title is None:
         title = value
-    return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % (value, title)
+    return ('<a class="wiki-link" data-wiki-url="%s">%s</a>' %
+            (value, title))
+
 
 def generate_edit_url(value, title=None):
     if title is None:
         title = value
-    return '<a class="wiki-link" data-wiki-url="%s" data-action="edit">%s</a>' % (value, title)
+    return ('<a class="wiki-link-edit" data-wiki-url="%s">%s</a>' %
+            (value, title))
 
--- a/wikked/scm/mercurial.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/scm/mercurial.py	Wed Sep 16 23:04:28 2015 -0700
@@ -312,9 +312,9 @@
 
     def diff(self, path, rev1, rev2):
         if rev2 is None:
-            return _s(self.client.diff(files=_b([path]), change=_b(rev1),
+            return _s(self.client.diff(files=_b([path]), change=rev1,
                                        git=True))
-        return _s(self.client.diff(files=_b([path]), revs=_b([rev1, rev2]),
+        return _s(self.client.diff(files=_b([path]), revs=[rev1, rev2],
                                    git=True))
 
     def commit(self, paths, op_meta):
@@ -334,7 +334,8 @@
                     **kwargs)
             self.client.rawcommand(args)
         except CommandError as e:
-            raise SourceControlError('commit', e.out, e.args, e.out)
+            raise SourceControlError('commit', _s(e.out), _s(e.args),
+                                     _s(e.out))
 
     def revert(self, paths=None):
         if paths is not None:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/404.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,19 @@
+{% extends 'index.html' %}
+{% block content %}
+<a id="wiki-menu-shortcut" class="wiki-logo">
+    <span>W</span>
+</a>
+<nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open">
+    <ul class="">
+        <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
+    </ul>
+</nav>
+<article class="row">
+    <header>
+        <h1>Page Not Found</h1>
+    </header>
+    <section>
+        <p>The page you requested was not found.</p>
+    </section>
+</article>
+{% endblock %}
--- a/wikked/templates/circular_include_error.html	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/templates/circular_include_error.html	Wed Sep 16 23:04:28 2015 -0700
@@ -1,3 +1,5 @@
+{% extends 'index.html' %}
+{% block content %}
 <div class="wiki-error">
     <p>{{message}}</p>
     <p>Here are the URLs we followed:</p>
@@ -7,3 +9,4 @@
         {% endfor %}
     </ul>
 </div>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/diff-page.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,18 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>{{meta.title}}</h1>
+        <div class="decorator">Diff
+            {%if disp_rev2%}
+            <span class="rev_id">{{disp_rev1}}</span> to <span class="rev_id">{{disp_rev2}}</span>
+            {%else%}
+            change <span class="rev_id">{{disp_rev1}}</span>
+            {%endif%}
+        </div>
+    </header>
+    <section>
+        <pre><code>{{diff|safe}}</code></pre>
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/edit-page.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,69 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <form id="page-edit" class="pure-form" action="{{post_back}}" method="POST">
+        <fieldset>
+            <div class="header-wrapper">
+                <header>
+                    {%if is_new%}
+                    <div class="pure-control-group pure-control-addon">
+                        <label for="title" class="">Title</label>
+                        <input type="text" name="title" placeholder="New Page" required="true" class="" value="{{create_in}}"></input>
+                    </div>
+                    <div class="pure-form-help">
+                        <span for="title">You can put this page in a folder by entering a title like <code>Folder/Page Title</code>.</span>
+                    </div>
+                    {%else%}
+                    <h1>{{meta.title}}</h1>
+                    <div class="decorator">Editing</div>
+                    {%endif%}
+                </header>
+            </div>
+            <div class="editing-wrapper">
+                <div class="editing">
+                    {%if error%}
+                    <section class="editing-error alert alert-danger">
+                        <p><strong>Error:</strong> <span class="editing-error-message">{{error}}</span></p>
+                    </section>
+                    {%endif%}
+                    <section class="editing-input">
+                        <textarea id="editing-input-area" name="text" placeholder="Your page's contents go here...">{{text}}</textarea>
+                        <div id="editing-input-grip"></div>
+                    </section>
+                    <section class="editing-preview">
+                    </section>
+                    <section class="editing-meta">
+                        <div class="pure-control-group pure-control-addon">
+                            <label for="author">Author</label>
+                            <input type="text" name="author" class="form-control" placeholder="{{commit_meta.author}}"></input>
+                        </div>
+                        <div class="pure-control-group pure-control-addon">
+                            <label for="message">Description</label>
+                            <input type="text" name="message" class="form-control" placeholder="{{commit_meta.desc}}" minlength="3"></input>
+                        </div>
+                    </section>
+                    <section class="editing-submit">
+                        <div class="pure-control-group">
+                            <button type="submit" class="pure-button pure-button-primary"><span class="fa fa-check"></span> Save</button>
+                            <button id="editing-preview-button" type="submit" class="pure-button" data-wiki-url="{{meta.url}}"><span class="fa fa-eye"></span> <span class="editing-preview-button-label">Preview</span></button>
+                            <button id="editing-cancel-button" type="submit" class="pure-button"><span class="fa fa-remove"></span> Cancel</button>
+                        </div>
+                    </section>
+                </div>
+            </div>
+        </fieldset>
+    </form>
+</article>
+{% endblock %}
+{% block scripts %}
+{% if is_dev %}
+<script data-main="/dev-assets/js/wikked.js" src="/dev-assets/js/require.js"></script>
+<script>
+require(['wikked.app', 'wikked.edit'], function(app, edit) {});
+</script>
+{% else %}
+<script src="/static/js/require.js"></script>
+<script src="/static/js/wikked.app.js"></script>
+<script src="/static/js/wikked.edit.js"></script>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/error-not-found.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,11 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>Not Found</h1>
+    </header>
+    <section>
+        <p>The page you're trying to access does not exist. You can <a href="{{get_edit_url(path)}}">create it</a>.</p>
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/error-unauthorized-edit.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,12 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>You're not authorized to edit this page</h1>
+    </header>
+    <section>
+        <p>The page you're trying to edit is protected.
+                Please <a href="/login">log into an account</a> that has write access to it.</p>
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/error-unauthorized.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,12 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>You're not authorized for this</h1>
+    </header>
+    <section>
+        <p>The page you're trying to access is protected.
+                Please <a href="/login">log into an account</a> that has access to it.</p>
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/footer.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,11 @@
+<footer role="navigation">
+    <ul>
+        <li><span class="wiki-icon"></span> Powered by <a href="http://bolt80.com/wikked/">Wikked</a></li>
+        {% if nav %}
+        {% for e in nav.footers %}
+        <li>{% if e.icon %}<span class="fa fa-{{e.icon}}"></span> {% endif %}<a href="{{e.url}}">{{e.title}}</a></li>
+        {% endfor %}
+        {% endif %}
+        <!-- TODO: last modified, etc. -->
+    </ul>
+</footer>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/history-page.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,46 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>{{meta.title}}</h1>
+        <div class="decorator">History</div>
+    </header>
+    <section>
+        {%if history%}
+        <p>Here's the revision log for <a href="{{nav.url_read}}">{{meta.title}}</a>.</p>
+        <form id="diff-page" action="{{get_diff_url(meta.url)}}" method="GET">
+            <table class="pure-table pure-table-bordered">
+                <thead>
+                    <tr>
+                        <th>Revision</th>
+                        <th>Date</th>
+                        <th>Author</th>
+                        <th>Comment</th>
+                        <th><button id="diff-revs" class="pure-button">Show Diff.</button></th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for e in history %}
+                    <tr>
+                        <td><a href="{{get_rev_url(meta.url, e.rev_id)}}">{{e.rev_name}}</a></td>
+                        <td>{{e.datetime}}</td>
+                        <td>{{e.author}}</td>
+                        <td>{{e.description}}</td>
+                        <td>
+                            <input type="radio" name="rev1" value="{{e.rev_id}}" {% if loop.index == 0 %}checked="true" {% endif %}/>
+                            <input type="radio" name="rev2" value="{{e.rev_id}}" {% if loop.index == 1 %}checked="true" {% endif %}/>
+                            <small><a href="{{get_diff_url(meta.url, e.rev_id)}}">with previous</a></small>
+                        </td>
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </form>
+        {%else%}
+        <p><i class="icon-warning-sign large big"></i>
+            This page has not been committed to the repository yet.
+            <a href="{{nav.url_edit}}">Edit it</a> to do that now.</p>
+        {%endif%}
+    </section>
+</article>
+{% endblock %}
--- a/wikked/templates/index-dev.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-<!doctype html>
-<html>
-    <head>
-        <title>Wikked</title>
-        <meta charset="utf-8"></meta>
-        <meta name="viewport" content="width=device-width, initial-scale=1.0">
-        <link rel="stylesheet" type="text/css" href="/static/css/wikked.min.css" />
-    </head>
-    <body>
-        <div id="app">
-        </div>
-        <script type="text/javascript">
-            var require = {
-                baseUrl: "/dev-assets/",
-                deps: ["/dev-assets/js/wikked.js"]
-            };
-        </script>
-        <script src="/dev-assets/js/require.js"></script>
-    </body>
-</html>
--- a/wikked/templates/index.html	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/templates/index.html	Wed Sep 16 23:04:28 2015 -0700
@@ -1,15 +1,33 @@
 <!doctype html>
 <html>
     <head>
-        <title>Wikked</title>
+    <title>{%if page_title%}{{page_title}} &ndash; {%endif%}Wikked</title>
         <meta charset="utf-8"></meta>
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <link rel="stylesheet" type="text/css" href="/static/css/wikked.min.css" />
     </head>
     <body>
         <div id="app">
+            <div class="nav-wrapper">
+                {% block nav %}{% include 'nav.html' %}{% endblock %}
+            </div>
+            <div class="wrapper{%if is_special_page%} special{%endif%}">
+                {% block content %}{% endblock %}
+            </div>
+            <div class="footer-wrapper">
+                {% block footer %}{% include 'footer.html' %}{% endblock %}
+            </div>
         </div>
+        {% block scripts %}
+        {% if is_dev %}
+        <script data-main="/dev-assets/js/wikked.js" src="/dev-assets/js/require.js"></script>
+        <script>
+        require(['wikked.app'], function(edit) {});
+        </script>
+        {% else %}
         <script src="/static/js/require.js"></script>
-        <script src="/static/js/wikked.min.js"></script>
+        <script src="/static/js/wikked.app.js"></script>
+        {% endif %}
+        {% endblock %}
     </body>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/inlinks-page.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,27 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>{{meta.title}}</h1>
+        <div class="decorator">Incoming Links</div>
+    </header>
+    <section>
+        {%if in_links%}
+        <p>The following pages link to <a href="{{get_read_url(meta.url)}}">{{meta.title}}</a>:</p>
+        <ul>
+        {%for l in in_links%}
+            <li>
+                {%if l.missing%}
+                <a class="wiki-link missing" href="{{get_edit_url(l.url)}}">{{l.url}}</a>
+                {%else%}
+                <a class="wiki-link" href="{{get_read_url(l.url)}}">{{l.title}}</a>
+                {%endif%}
+            </li>
+        {%endfor%}
+        </ul>
+        {%else%}
+        <p>No pages link to <a href="{{get_read_url(meta.url)}}">{{meta.title}}</a>.</p>
+        {%endif%}
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/login.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,26 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>Login</h1>
+    </header>
+    <section>
+        {%if has_error%}
+        <div class="alert alert-danger alert-dismissable">
+            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
+            <strong>Begone!</strong> Those credentials don't seem to work here.
+        </div>
+        {%endif%}
+        <form id="login" class="pure-form pure-form-stacked" role="form">
+            <fieldset>
+                <input type="text" name="username" for="remember" class="form-control" placeholder="Username" required="true" autofocus="true"></input>
+                <input type="password" name="password" placeholder="Password" required="true"></input>
+                <label for="remember" class="pure-checkbox">
+                    <input type="checkbox" name="remember" value="remember-me"> Remember me</input>
+                </label>
+                <button class="pure-button pure-button-primary" type="submit">Log in</button>
+            </fieldset>
+        </form>
+    </section>
+</article>
+{% endblock %}
--- a/wikked/templates/meta_page.html	Sun Aug 30 21:45:42 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-{% if info_text %}
-{{info_text|safe}}
-{% else %}
-<p>No additional information is available for this page. You can <a class="wiki-link missing" data-wiki-endpoint="{{name}}" data-wiki-url="{{url}}" data-action="edit">write some right now</a>.</p>
-{% endif %}
-
-<h3>Pages in {{name}} "{{value}}"</h3>
-
-<ul>
-    {% for p in pages %}
-    <li><a class="wiki-link" data-wiki-url="{{p.url}}">{{p.title}}</a></li>
-    {% endfor %}
-</ul>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/nav.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,36 @@
+<a id="wiki-menu-shortcut" class="wiki-logo">
+    <span>W</span>
+</a>
+<nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open">
+    <div id="wiki-menu-pin" title="Pin/unpin the wiki menu.">
+        <span class="fa fa-lock"></span>
+    </div>
+    <ul class="">
+        <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
+        <li><a href="/create/"><span class="fa fa-file"></span> New Page</a></li>
+        {%if nav.url_read%}<li><a href="{{nav.url_read}}"><span class="fa fa-book"></span> Read</a></li>{%endif%}
+        {%if nav.url_edit%}<li><a href="{{nav.url_edit}}"><span class="fa fa-edit"></span> Edit</a></li>{%endif%}
+        {%if nav.url_hist%}<li><a href="{{nav.url_hist}}"><span class="fa fa-road"></span> History</a></li>{%endif%}
+    </ul>
+    <form role="search" id="search" class="pure-form pure-menu-form pure-menu-divider" action="/search">
+        <fieldset>
+            <input type="text" name="q" id="search-query" class="pure-input-1" placeholder="Search..." autocomplete="off"></input>
+            <ul id="search-preview"></ul>
+        </fieldset>
+    </form>
+    {%if nav.extras%}
+    <ul class="pure-menu-divider">
+    {%for e in nav.extras%}
+        <li><a href="{{e.url}}">{%if e.icon%}<span class="fa fa-{{e.icon}}"></span> {%endif%}{{e.title}}</a></li>
+    {%endfor%}
+    </ul>
+    {%endif%}
+    <ul class="pure-menu-divider">
+        {%if auth.is_logged_in%}
+        <li><a href="{{auth.url_profile}}"><span class="fa fa-cog"></span> {{auth.username}}</a></li>
+        <li><a href="{{auth.url_logout}}"><span class="fa fa-log-out"></span> Logout</a></li>
+        {%else%}
+        <li><a href="{{auth.url_login}}"><span class="fa fa-log-in"></span> Login</a></li>
+        {%endif%}
+    </ul>
+</nav>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/read-page.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,53 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    {% if not meta.notitle %}
+    <header>
+        <h1>{{meta.title}}</h1>
+        {% if endpoint %}
+        <div class="decorator">{{endpoint}}</div>
+        {% endif %}
+        {% if redirected_from %}
+        <div class="decorator"><small>Redirected from 
+            {% for r in redirected_from %}
+            {% if not loop.first %} | {% endif %}
+            <a href="{{get_read_url(r)}}?no_redirect">{{r}}</a>
+            {% endfor %}
+        </small></div>
+        {% endif %}
+        {% if meta.redirect %}
+        <div class="decorator"><small>Redirects to
+                <a href="{{get_read_url(redirects_to)}}">{{redirects_to}}</a>
+        </small></div>
+        {% endif %}
+    </header>
+    {% endif %}
+    <section class="content">
+        {{text|safe}}
+        {% if is_query %}
+        {% if not text %}
+        <p>No additional information is available for this page.
+           You can <a href="{{get_edit_url(meta.url)}}">write some right now</a>.</p>
+        {% endif %}
+        <h3>Pages in {{meta_query}} "{{meta_value}}"</h3>
+        <ul>
+            {% for p in query_results %}
+            <li><a class="wiki-link" data-wiki-url="{{p.url}}"
+                   href="{{get_read_url(p.url)}}">{{p.title}}</a></li>
+            {% endfor %}
+        </ul>
+        {% endif %}
+    </section>
+    {% if meta.category %}
+    <section class="info">
+        {% if meta.category %}
+        <span class="info-categories">Categories: </span>
+        {% for c in meta.category %}
+        {% if not loop.first %} | {% endif %}
+        <span><a href="{{get_read_url(c.url)}}">{{c.name}}</a></span>
+        {% endfor %}
+        {% endif %}
+    </section>
+    {% endif %}
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/revision-page.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,20 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>{{meta.title}}</h1>
+        <div class="decorator">Revision: <span class="rev_id">{{disp_rev}}</span></div>
+    </header>
+    <section>
+        <pre class="raw">{{text}}</pre>
+        <form id="page-revert" class="page-revert">
+            <input type="hidden" name="rev" value="{{rev}}"/>
+            <div class="form-group">
+                <button type="submit" id="revert-page" class="pure-button pure-button-warning">Revert</button>
+                <label class="small" for="revert-page">Revert the page to this revision</span>
+            </div>
+        </form>
+    </section>
+</div>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/search-results.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,25 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>Search Results</h1>
+    </header>
+    <section>
+        {%if is_instant%}
+        <p><small>Press <code>Escape</code> to cancel.</small></p>
+        {%endif%}
+        {%if hits%}
+        <ul class="search-results">
+            {%for h in hits%}
+            <li>
+            <h3><a href="{{get_read_url(h.url)}}">{{h.title|safe}}</a></h3>
+            <div class="highlighted">{{h.text|safe}}</div>
+            </li>
+            {%endfor%}
+        </ul>
+        {%else%}
+        <p>No matches found.</p>
+        {%endif%}
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-broken-redirects.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,8 @@
+{% extends "special-pagelist.html" %}
+{% block message %}
+<p>Here is a list of pages that redirect to a non-existing page.</p>
+{% endblock %}
+{% block empty %}
+<p>No broken redirects!</p>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-changes.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,67 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>Wiki History</h1>
+    </header>
+    <section>
+        <p>Here are the recent changes on this wiki.</p>
+        <form>
+            <table class="pure-table pure-table-bordered wiki-history">
+                <thead>
+                    <tr>
+                        <th>Revision</th>
+                        <th>Information</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {%for e in history%}
+                    <tr class='wiki-history-entry'>
+                        <td><code>{{e.rev_name}}</code></td>
+                        <td>
+                            <dl class="pure-dl-horizontal">
+                                <dt>Date</dt>
+                                <dd>{{e.datetime}}</dd>
+                                <dt>Author</dt>
+                                <dd>{{e.author}}</dd>
+                                <dt>Pages ({{e.num_pages}})</dt>
+                                <dd>
+                                {%if collapsed%}}
+                                    <button class="pure-button wiki-history-entry-collapser" data-index="{{loop.index}}">
+                                        <span class="fa fa-chevron-down"></span>
+                                        <small>Show</small>
+                                    </button>
+                                    <div class="wiki-history-entry-details wiki-history-entry-details-{{loop.index}}">
+                                {%endif%}
+                                    <ul class="pure-ul-unstyled">
+                                        {%for p in e.pages%}
+                                        <li>
+                                            <a href="/revision{{p.url}}/{{e.rev_id}}">{{p.url}}</a>
+                                            {{p.action_label}}
+                                        </li>
+                                        {%endfor%}
+                                    </ul>
+                                {%if collapsed%}
+                                    </div>
+                                {%endif%}
+                                </dd>
+                                <dt>Comment</dt>
+                                <dd>{{e.description}}</dd>
+                            </dl>
+                        </td>
+                    </tr>
+                    {%endfor%}
+                </tbody>
+            </table>
+            <div class="wiki-history-pagination pure-g">
+                <div class="pure-u-1-2">
+                    <a href="{{first_page}}">First page</a>
+                </div>
+                <div class="pure-u-1-2">
+                    <a href="{{next_page}}">Next page</a>
+                </div>
+            </div>
+        </form>
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-dead-ends.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,8 @@
+{% extends "special-pagelist.html" %}
+{% block message %}
+<p>Here is a list of pages that don't have any outgoing links.</p>
+{% endblock %}
+{% block empty %}
+<p>No dead-end pages!</p>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-double-redirects.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,8 @@
+{% extends "special-pagelist.html" %}
+{% block message %}
+<p>Here is a list of pages that redirect twice or more.</p>
+{% endblock %}
+{% block empty %}
+<p>No double redirects!</p>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-nav.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,27 @@
+<a id="wiki-menu-shortcut" class="wiki-logo">
+    <span>W</span>
+</a>
+<nav id="wiki-menu" role="navigation" class="pure-menu pure-menu-open wiki-menu-active">
+    <div id="wiki-menu-pin" title="Pin/unpin the wiki menu.">
+        <span class="fa fa-lock"></span>
+    </div>
+    <ul class="">
+        <li><a href="/"><span class="fa fa-home"></span> Home</a></li>
+        <li><a href="/special"><span class="fa fa-dashboard"></span> Special Pages</a></li>
+        <li><a href="/create/"><span class="fa fa-file"></span> New Page</a></li>
+    </ul>
+    <form role="search" id="search" class="pure-form pure-menu-form pure-menu-divider">
+        <fieldset>
+            <input type="text" name="q" id="search-query" class="pure-input-1" placeholder="Search..." autocomplete="off"></input>
+            <ul id="search-preview"></ul>
+        </fieldset>
+    </form>
+    <ul class="pure-menu-divider">
+        {%if auth%}
+        <li><a href="{{auth.url_profile}}"><span class="fa fa-cog"></span> {{auth.username}}</a></li>
+        <li><a href="{{auth.url_logout}}"><span class="fa fa-log-out"></span> Logout</a></li>
+        {%else%}
+        <li><a href="{{auth.url_login}}"><span class="fa fa-log-in"></span> Login</a></li>
+        {%endif%}
+    </ul>
+</nav>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-orphans.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,10 @@
+{% extends "special-pagelist.html" %}
+{% block message %}
+<p>Here is a list of pages that don't have any pages linking to them. This means user will only be able to find them by searching for them, or by getting a direct link.</p>
+<aside>
+    <p>The main page usually shows up here but that's OK since it's the page everyone sees first.</p>
+</aside>
+{% endblock %}
+{% block empty %}
+<p>No orphaned pages!</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-pagelist.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,21 @@
+{% extends 'index.html' %}
+{% block nav %}{% include 'special-nav.html' %}{% endblock %}
+{% block content %}
+<article>
+    <header>
+        <h1>{{title}}</h1>
+    </header>
+    <section>
+        {%block message %}{% endblock%}
+        {%if pages%}
+        <ul>
+        {%for p in pages%}
+            <li><a href="{{get_read_url(p.url)}}{{url_suffix}}">{{p.title}}</a></li>
+        {%endfor%}
+        </ul>
+        {%else%}
+        {%block empty%}{%endblock%}
+        {%endif%}
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/special-pages.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,24 @@
+{% extends 'index.html' %}
+{% block nav %}{% include 'special-nav.html' %}{% endblock %}
+{% block content %}
+<article>
+    <header>
+        <h1>Special Pages</h1>
+    </header>
+    <section>
+        <div class="pure-g">
+            {%for s in sections%}
+            <div class="pure-u-1-3">
+                <h2>{{s.title}}</h2>
+                {%for p in s.pages%}
+                <h3><a href="{{p.url}}">{{p.title}}</a></h3>
+                {%if p.description%}
+                <p>{{p.description}}</p>
+                {%endif%}
+                {%endfor%}
+            </div>
+            {%endfor%}
+        </div>
+    </section>
+</article>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/state-warning.html	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,5 @@
+<div class="ribbon state-warning">
+    <div class="ribbon-inner">
+        <span><a href="{{nav.url_edit}}" rel="tooltip" title="This page is {{state}} in the repository. You can edit it now to commit." data-toggle="tooltip" data-placement="bottom">modified <i class="icon-info-sign icon-white"></i></a></span>
+    </div>
+</div>
--- a/wikked/utils.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/utils.py	Wed Sep 16 23:04:28 2015 -0700
@@ -7,7 +7,7 @@
 
 re_terminal_path = re.compile(r'[/\\]|(\w\:)')
 endpoint_regex = re.compile(r'(\w[\w\d]*)\:(.*)')
-strip_endpoint_regex = re.compile(r'^(\w[\w\d]+)\:')
+endpoint_prefix_regex = re.compile(r'^(\w[\w\d]+)\:')
 
 
 class PageNotFoundError(Exception):
@@ -74,6 +74,10 @@
     return abs_url
 
 
+def is_endpoint_url(url):
+    return endpoint_prefix_regex.match(url) is not None
+
+
 def split_page_url(url):
     m = endpoint_regex.match(url)
     if m is None:
--- a/wikked/views/__init__.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/__init__.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,215 +1,69 @@
-import os.path
-import urllib.request, urllib.parse, urllib.error
-import string
-import datetime
-from flask import g, abort, jsonify
 from flask.ext.login import current_user
-from wikked.fs import PageNotFoundError
-from wikked.utils import split_page_url, get_absolute_url
-from wikked.web import app, get_wiki
-
-
-DONT_CHECK = 0
-CHECK_FOR_READ = 1
-CHECK_FOR_WRITE = 2
-
-
-def url_from_viewarg(url):
-    endpoint, path = split_url_from_viewarg(url)
-    if endpoint:
-        return '%s:%s' % (endpoint, path)
-    return path
-
-
-def split_url_from_viewarg(url):
-    url = urllib.parse.unquote(url)
-    endpoint, path = split_page_url(url)
-    if endpoint:
-        return (endpoint, path)
-    return (None, '/' + path)
-
-
-def make_page_title(url):
-    return url[1:]
+from wikked.web import app
 
 
-def get_page_or_none(url, fields=None, convert_url=True,
-        check_perms=DONT_CHECK):
-    if convert_url:
-        url = url_from_viewarg(url)
-
-    auto_reload = app.config.get('WIKI_AUTO_RELOAD')
-    if auto_reload and fields is not None:
-        if 'path' not in fields:
-            fields.append('path')
-        if 'cache_time' not in fields:
-            fields.append('cache_time')
-        if 'is_resolved' not in fields:
-            fields.append('is_resolved')
-
-    try:
-        wiki = get_wiki()
-        page = wiki.getPage(url, fields=fields)
-    except PageNotFoundError:
-        return None
-
-    if auto_reload:
-        wiki = get_wiki()
-        path_time = datetime.datetime.fromtimestamp(
-            os.path.getmtime(page.path))
-        if path_time >= page.cache_time:
-            app.logger.info("Page '%s' has changed, reloading." % url)
-            wiki.updatePage(path=page.path)
-            page = wiki.getPage(url, fields=fields)
-        elif not page.is_resolved:
-            app.logger.info("Page '%s' was not resolved, resolving now." % url)
-            wiki.resolve(only_urls=[url])
-            wiki.index.updatePage(wiki.db.getPage(
-                url, fields=['url', 'path', 'title', 'text']))
-            page = wiki.getPage(url, fields=fields)
-
-    if check_perms == CHECK_FOR_READ and not is_page_readable(page):
-        abort(401)
-    elif check_perms == CHECK_FOR_WRITE and not is_page_writable(page):
-        abort(401)
-
-    return page
-
-
-def get_page_or_404(url, fields=None, convert_url=True,
-        check_perms=DONT_CHECK):
-    page = get_page_or_none(url, fields, convert_url, check_perms)
-    if page is not None:
-        return page
-    app.logger.error("No such page: " + url)
-    abort(404)
-
-
-def is_page_readable(page, user=current_user):
-    return page.wiki.auth.isPageReadable(page, user.get_id())
-
-
-def is_page_writable(page, user=current_user):
-    return page.wiki.auth.isPageWritable(page, user.get_id())
-
-
-def get_page_meta(page, local_only=False):
-    if local_only:
-        meta = dict(page.getLocalMeta() or {})
+def add_auth_data(data):
+    if current_user.is_authenticated():
+        user_page_url = 'user:/%s' % current_user.get_id()
+        data['auth'] = {
+                'is_logged_in': True,
+                'username': current_user.username,
+                'is_admin': current_user.is_admin(),
+                'url_logout': '/logout',
+                'url_profile': '/read/' % user_page_url
+                }
     else:
-        meta = dict(page.getMeta() or {})
-    meta['title'] = page.title
-    meta['url'] = urllib.parse.quote(page.url.encode('utf-8'))
-    for name in COERCE_META:
-        if name in meta:
-            meta[name] = COERCE_META[name](meta[name])
-    return meta
+        data['auth'] = {
+                'is_logged_in': False,
+                'url_login': '/login'
+                }
 
 
-def get_category_meta(category):
-    result = []
-    for item in category:
-        result.append({
-            'url': 'category:/' + urllib.parse.quote(item.encode('utf-8')),
-            'name': item
-            })
-    return result
-
-
-class CircularRedirectError(Exception):
-    def __init__(self, url, visited):
-        super(CircularRedirectError, self).__init__(
-                "Circular redirect detected at '%s' "
-                "after visiting: %s" % (url, visited))
-
-
-class RedirectNotFound(Exception):
-    def __init__(self, url, not_found):
-        super(RedirectNotFound, self).__init__(
-                "Target redirect page '%s' not found from '%s'." %
-                (url, not_found))
-
+def add_navigation_data(
+        url, data,
+        read=False, edit=False, history=False, inlinks=False,
+        raw_url=None, extras=None, footers=None):
+    if url is not None:
+        url = url.lstrip('/')
+    elif read or edit or history or inlinks:
+        raise Exception("Default navigation entries require a valid URL.")
 
-def get_redirect_target(path, fields=None, convert_url=False,
-                        check_perms=DONT_CHECK, first_only=False):
-    page = None
-    orig_path = path
-    visited_paths = []
-
-    while True:
-        page = get_page_or_none(
-                path,
-                fields=fields,
-                convert_url=convert_url,
-                check_perms=check_perms)
-        if page is None:
-            raise RedirectNotFound(orig_path, path)
+    nav = {'home': '/', 'extras': [], 'footers': []}
+    if read:
+        nav['url_read'] = '/read/%s' % url
+    if edit:
+        nav['url_edit'] = '/edit/%s' % url
+    if history:
+        nav['url_hist'] = '/hist/%s' % url
 
-        visited_paths.append(path)
-        redirect_meta = page.getMeta('redirect')
-        if redirect_meta is None:
-            break
-
-        path = get_absolute_url(path, redirect_meta)
-        if first_only:
-            visited_paths.append(path)
-            break
-
-        if path in visited_paths:
-            raise CircularRedirectError(path, visited_paths)
-
-    return page, visited_paths
+    if inlinks:
+        nav['extras'].append({
+            'title': "Pages Linking Here",
+            'url': '/inlinks/' + url,
+            'icon': 'link'
+            })
 
-
-COERCE_META = {
-    'category': get_category_meta
-    }
-
+    if raw_url:
+        nav['footers'].append({
+            'title': "RAW",
+            'url': raw_url,
+            'icon': 'wrench'
+            })
 
-def make_auth_response(data):
-    if current_user.is_authenticated():
-        data['auth'] = {
-                'username': current_user.username,
-                'is_admin': current_user.is_admin()
-                }
-    return jsonify(data)
-
+    nav['extras'].append({
+            'title': "Special Pages",
+            'url': '/special',
+            'icon': 'dashboard'})
 
-def get_or_build_pagelist(list_name, builder, fields=None):
-    # If the wiki is using background jobs, we can accept invalidated
-    # lists... it just means that a background job is hopefully still
-    # just catching up.
-    # Otherwise, everything is synchronous and we need to build the
-    # list if needed.
-    wiki = get_wiki()
-    build_inline = not app.config['WIKI_ASYNC_UPDATE']
-    page_list = wiki.db.getPageListOrNone(list_name, fields=fields,
-                                          valid_only=build_inline)
-    if page_list is None and build_inline:
-        app.logger.info("Regenerating list: %s" % list_name)
-        page_list = builder()
-        wiki.db.addPageList(list_name, page_list)
-
-    return page_list
-
+    if extras:
+        nav['extras'] = [{'title': e[0], 'url': e[1], 'icon': e[2]}
+                         for e in extras]
 
-def get_generic_pagelist_builder(filter_func, fields=None):
-    fields = fields or ['url', 'title', 'meta']
-    def builder():
-        # Make sure all pages have been resolved.
-        wiki = get_wiki()
-        wiki.resolve()
+    if footers:
+        nav['footers'] = [{'title': f[0], 'url': f[1], 'icon': f[2]}
+                          for f in footers]
+    data['nav'] = nav
 
-        pages = []
-        for page in wiki.getPages(
-                no_endpoint_only=True,
-                fields=fields):
-            try:
-                if filter_func(page):
-                    pages.append(page)
-            except Exception as e:
-                app.logger.error("Error while inspecting page: %s" % page.url)
-                app.logger.error("   %s" % e)
-        return pages
-    return builder
+    if app.config['WIKI_DEV_ASSETS']:
+        data['is_dev'] = True
 
--- a/wikked/views/admin.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/admin.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,74 +1,28 @@
-from flask import g, jsonify, abort, request
-from flask.ext.login import login_user, logout_user, current_user
-from wikked.web import app, get_wiki, login_manager
+from flask import request, redirect, render_template
+from flask.ext.login import login_user, logout_user
+from wikked.web import app, get_wiki
 
 
-@app.route('/api/admin/reindex', methods=['POST'])
-def api_admin_reindex():
-    if not current_user.is_authenticated() or not current_user.is_admin():
-        return login_manager.unauthorized()
-    wiki = get_wiki()
-    wiki.index.reset(wiki.getPages())
-    result = {'ok': 1}
-    return jsonify(result)
-
-
-@app.route('/api/user/login', methods=['POST'])
-def api_user_login():
+@app.route('/login')
+def login():
     username = request.form.get('username')
     password = request.form.get('password')
     remember = request.form.get('remember')
+    back_url = request.form.get('back_url')
 
     wiki = get_wiki()
     user = wiki.auth.getUser(username)
     if user is not None and app.bcrypt:
         if app.bcrypt.check_password_hash(user.password, password):
             login_user(user, remember=bool(remember))
-            result = {'username': username, 'logged_in': 1}
-            return jsonify(result)
-    abort(401)
-
+            return redirect(back_url or '/')
 
-@app.route('/api/user/is_logged_in')
-def api_user_is_logged_in():
-    if current_user.is_authenticated():
-        result = {'logged_in': True}
-        return jsonify(result)
-    abort(401)
-
-
-@app.route('/api/user/logout', methods=['POST'])
-def api_user_logout():
-    logout_user()
-    result = {'ok': 1}
-    return jsonify(result)
+    data = {'has_error': True}
+    return render_template('login.html', **data)
 
 
-@app.route('/api/user/info')
-def api_current_user_info():
-    user = current_user
-    if user.is_authenticated():
-        result = {
-                'user': {
-                    'username': user.username,
-                    'groups': user.groups
-                    }
-                }
-        return jsonify(result)
-    return jsonify({'user': False})
-
+@app.route('/logout')
+def logout():
+    logout_user()
+    redirect('/')
 
-@app.route('/api/user/info/<name>')
-def api_user_info(name):
-    wiki = get_wiki()
-    user = wiki.auth.getUser(name)
-    if user is not None:
-        result = {
-                'user': {
-                    'username': user.username,
-                    'groups': user.groups
-                    }
-                }
-        return jsonify(result)
-    abort(404)
-
--- a/wikked/views/edit.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/edit.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,172 +1,63 @@
-import urllib.request, urllib.parse, urllib.error
-from flask import g, abort, request, jsonify
+from flask import redirect, url_for, request, render_template
 from flask.ext.login import current_user
-from wikked.page import Page, PageData
-from wikked.formatter import PageFormatter, FormattingContext
-from wikked.resolver import PageResolver
-from wikked.views import (make_page_title, get_page_or_none,
-        is_page_writable, get_page_meta, url_from_viewarg,
-        split_url_from_viewarg)
+from wikked.views import (
+        add_auth_data, add_navigation_data)
 from wikked.web import app, get_wiki
+from wikked.webimpl import url_from_viewarg
+from wikked.webimpl.edit import get_edit_page, do_edit_page
+
+
+@app.route('/create/')
+def create_page_at_root():
+    return create_page('/')
 
 
-class DummyPage(Page):
-    """ A dummy page for previewing in-progress editing.
-    """
-    def __init__(self, wiki, url, text):
-        data = self._loadData(wiki, url, text)
-        super(DummyPage, self).__init__(wiki, data)
-
-    def _loadData(self, wiki, url, text):
-        data = PageData()
-        extension = wiki.fs.default_extension
-        data.url = url
-        data.path = '__preview__.' + extension
-        data.raw_text = text
-
-        ctx = FormattingContext(url)
-        f = PageFormatter()
-        data.formatted_text = f.formatText(ctx, data.raw_text)
-        data.local_meta = ctx.meta
-        data.local_links = ctx.out_links
-
-        data.title = (data.local_meta.get('title') or
-                make_page_title(url))
-        if isinstance(data.title, list):
-            data.title = data.title[0]
-
-        return data
-
-
-def get_edit_page(url, default_title=None, custom_data=None):
-    page = get_page_or_none(url, convert_url=False)
-    if page is None:
-        result = {
-                'meta': {
-                    'url': urllib.parse.quote(url.encode('utf-8')),
-                    'title': default_title or make_page_title(url)
-                    },
-                'text': ''
-                }
-    else:
-        if not is_page_writable(page):
-            abort(401)
-        result = {
-                'meta': get_page_meta(page, True),
-                'text': page.raw_text
-                }
-    result['commit_meta'] = {
-            'author': request.remote_addr,
-            'desc': 'Editing ' + result['meta']['title']
+@app.route('/create/<path:url>')
+def create_page(url):
+    data = {
+            'is_new': True,
+            'create_in': url.lstrip('/'),
+            'text': '',
+            'commit_meta': {
+                'author': current_user.get_id() or request.remote_addr,
+                'desc': 'Editing ' + url
+                },
+            'post_back': '/edit'
             }
-    if custom_data:
-        result.update(custom_data)
-    return jsonify(result)
+    add_auth_data(data)
+    add_navigation_data(url, data)
+    return render_template('edit-page.html', **data)
 
 
-def do_edit_page(url, default_message):
-    page = get_page_or_none(url, convert_url=False)
-    if page and not is_page_writable(page):
-        app.logger.error("Page '%s' is not writable for user '%s'." % (
-            url, current_user.get_id()))
-        abort(401)
-
-    if not 'text' in request.form:
-        abort(400)
-    text = request.form['text']
-    author = request.remote_addr
-    if 'author' in request.form and len(request.form['author']) > 0:
-        author = request.form['author']
-    message = 'Edited ' + url
-    if 'message' in request.form and len(request.form['message']) > 0:
-        message = request.form['message']
-
-    page_fields = {
-            'text': text,
-            'author': author,
-            'message': message
-            }
-    wiki = get_wiki()
-    wiki.setPage(url, page_fields)
-
-    result = {'saved': 1}
-    return jsonify(result)
+@app.route('/edit', methods=['POST'])
+def edit_new_page():
+    url = request.form['title']
+    return edit_page(url)
 
 
-@app.route('/api/edit/', methods=['GET', 'POST'])
-def api_edit_main_page():
+@app.route('/edit/<path:url>', methods=['GET', 'POST'])
+def edit_page(url):
     wiki = get_wiki()
-    return api_edit_page(wiki.main_page_url.lstrip('/'))
-
-
-@app.route('/api/edit/<path:url>', methods=['GET', 'POST'])
-def api_edit_page(url):
-    endpoint, path = split_url_from_viewarg(url)
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
 
     if request.method == 'GET':
-        url = path
-        default_title = None
-        custom_data = None
-        if endpoint is not None:
-            url = '%s:%s' % (endpoint, path)
-            default_title = '%s: %s' % (endpoint, path)
-            custom_data = {
-                    'meta_query': endpoint,
-                    'meta_value': path.lstrip('/')
-                    }
-
-        return get_edit_page(
-                url,
-                default_title=default_title,
-                custom_data=custom_data)
-
-    url = path
-    default_message = 'Edited ' + url
-    if endpoint is not None:
-        url = '%s:%s' % (endpoint, path)
-        default_message = 'Edited %s %s' % (endpoint, path.lstrip('/'))
-    return do_edit_page(url, default_message)
-
-
-@app.route('/api/preview', methods=['POST'])
-def api_preview():
-    url = request.form.get('url')
-    url = url_from_viewarg(url)
-    text = request.form.get('text')
-    wiki = get_wiki()
-    dummy = DummyPage(wiki, url, text)
+        author = user or request.remote_addr
+        custom_data = {'post_back': '/edit/' + url.lstrip('/')}
+        data = get_edit_page(wiki, user, url,
+                             author=author, custom_data=custom_data)
+        add_auth_data(data)
+        add_navigation_data(
+                url, data,
+                read=True, history=True, inlinks=True,
+                raw_url='/api/edit/' + url.lstrip('/'))
+        return render_template('edit-page.html', **data)
 
-    resolver = PageResolver(dummy)
-    dummy._setExtendedData(resolver.run())
-
-    result = {'text': dummy.text}
-    return jsonify(result)
-
-
-@app.route('/api/rename/<path:url>', methods=['POST'])
-def api_rename_page(url):
-    pass
-
-
-@app.route('/api/delete/<path:url>', methods=['POST'])
-def api_delete_page(url):
-    pass
-
+    if request.method == 'POST':
+        text = request.form['text']
+        author = user or request.form['author'] or request.remote_addr
+        message = request.form['message'] or 'Editing ' + url
+        do_edit_page(wiki, user, url, text,
+                     author=author, message=message)
+        return redirect(url_for('read', url=url.lstrip('/')))
 
-@app.route('/api/validate/newpage', methods=['GET', 'POST'])
-def api_validate_newpage():
-    path = request.form.get('title')
-    if path is None:
-        abort(400)
-    path = url_from_viewarg(path)
-    try:
-        # Check that there's no page with that name already, and that
-        # the name can be correctly mapped to a filename.
-        wiki = get_wiki()
-        if wiki.pageExists(path):
-            raise Exception("Page '%s' already exists" % path)
-        wiki.fs.getPhysicalPagePath(path, make_new=True)
-    except Exception:
-        return '"This page name is invalid or unavailable"'
-    return '"true"'
-
--- a/wikked/views/error.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/error.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,6 +1,7 @@
 from flask import jsonify
 from wikked.scm.base import SourceControlError
 from wikked.web import app
+from wikked.webimpl import PermissionError
 
 
 @app.errorhandler(SourceControlError)
@@ -15,3 +16,15 @@
             }
     return jsonify(resp), 500
 
+
+@app.errorhandler(PermissionError)
+def handle_permission_error(error):
+    app.log_exception(error)
+    resp = {
+            'error': {
+                'type': 'permission',
+                'message': error.message
+                }
+            }
+    return jsonify(resp), 403
+
--- a/wikked/views/history.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/history.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,135 +1,86 @@
-import os.path
-from flask import g, jsonify, request, abort
-from pygments import highlight
-from pygments.lexers import get_lexer_by_name
-from pygments.formatters import get_formatter_by_name
-from wikked.page import PageLoadingError
-from wikked.scm.base import ACTION_NAMES
-from wikked.utils import PageNotFoundError
-from wikked.views import (is_page_readable, get_page_meta, get_page_or_404,
-        url_from_viewarg,
-        CHECK_FOR_READ)
+import urllib.parse
+from flask import request, abort, render_template
+from flask.ext.login import current_user
+from wikked.views import add_auth_data, add_navigation_data
 from wikked.web import app, get_wiki
+from wikked.webimpl import url_from_viewarg
+from wikked.webimpl.history import (
+        get_site_history, get_page_history,
+        read_page_rev, diff_page_revs)
 
 
-def get_history_data(history, needs_files=False):
-    hist_data = []
+@app.route('/special/history')
+def site_history():
     wiki = get_wiki()
-    for i, rev in enumerate(reversed(history)):
-        rev_data = {
-            'index': i + 1,
-            'rev_id': rev.rev_id,
-            'rev_name': rev.rev_name,
-            'author': rev.author.name,
-            'timestamp': rev.timestamp,
-            'description': rev.description
-            }
-        if needs_files:
-            rev_data['pages'] = []
-            for f in rev.files:
-                url = None
-                path = os.path.join(wiki.root, f['path'])
-                try:
-                    page = wiki.db.getPage(path=path)
-                    # Hide pages that the user can't see.
-                    if not is_page_readable(page):
-                        continue
-                    url = page.url
-                except PageNotFoundError:
-                    pass
-                except PageLoadingError:
-                    pass
-                if not url:
-                    url = os.path.splitext(f['path'])[0]
-                rev_data['pages'].append({
-                    'url': url,
-                    'action': ACTION_NAMES[f['action']]
-                    })
-            rev_data['num_pages'] = len(rev_data['pages'])
-            if len(rev_data['pages']) > 0:
-                hist_data.append(rev_data)
-        else:
-            hist_data.append(rev_data)
-    return hist_data
+    user = current_user.get_id()
+    after_rev = request.args.get('rev')
+    data = get_site_history(wiki, user, after_rev=after_rev)
+    add_auth_data(data)
+    add_navigation_data(
+            '', data,
+            raw_url='/api/site-history')
+    return render_template('special-changes.html', **data)
 
 
-@app.route('/api/site-history')
-def api_site_history():
+@app.route('/hist/<path:url>')
+def page_history(url):
     wiki = get_wiki()
-    after_rev = request.args.get('rev')
-    history = wiki.getHistory(limit=10, after_rev=after_rev)
-    hist_data = get_history_data(history, needs_files=True)
-    result = {'history': hist_data}
-    return jsonify(result)
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    data = get_page_history(wiki, user, url)
+    add_auth_data(data)
+    add_navigation_data(
+            url, data,
+            read=True, edit=True, inlinks=True,
+            raw_url='/api/history/' + url.lstrip('/'))
+    return render_template('history-page.html', **data)
 
 
-@app.route('/api/history/')
-def api_main_page_history():
-    wiki = get_wiki()
-    return api_page_history(wiki.main_page_url.lstrip('/'))
-
-
-@app.route('/api/history/<path:url>')
-def api_page_history(url):
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ)
-    history = page.getHistory()
-    hist_data = get_history_data(history)
-    result = {'url': url, 'meta': get_page_meta(page), 'history': hist_data}
-    return jsonify(result)
-
-
-@app.route('/api/revision/<path:url>')
-def api_read_page_rev(url):
+@app.route('/rev/<path:url>')
+def page_rev(url):
     rev = request.args.get('rev')
     if rev is None:
         abort(400)
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ)
-    page_rev = page.getRevision(rev)
-    meta = dict(get_page_meta(page, True), rev=rev)
-    result = {'meta': meta, 'text': page_rev}
-    return jsonify(result)
+
+    raw_url_args = {'rev': rev}
+
+    wiki = get_wiki()
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    data = read_page_rev(wiki, user, url, rev=rev)
+    add_auth_data(data)
+    add_navigation_data(
+            url, data,
+            read=True,
+            raw_url='/api/revision/%s?%s' % (
+                url.lstrip('/'),
+                urllib.parse.urlencode(raw_url_args)))
+    return render_template('revision-page.html', **data)
 
 
-@app.route('/api/diff/<path:url>')
-def api_diff_page(url):
+@app.route('/diff/<path:url>')
+def diff_page(url):
     rev1 = request.args.get('rev1')
     rev2 = request.args.get('rev2')
+    raw = request.args.get('raw')
     if rev1 is None:
         abort(400)
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ)
-    diff = page.getDiff(rev1, rev2)
-    if 'raw' not in request.args:
-        lexer = get_lexer_by_name('diff')
-        formatter = get_formatter_by_name('html')
-        diff = highlight(diff, lexer, formatter)
-    if rev2 is None:
-        meta = dict(get_page_meta(page, True), change=rev1)
-    else:
-        meta = dict(get_page_meta(page, True), rev1=rev1, rev2=rev2)
-    result = {'meta': meta, 'diff': diff}
-    return jsonify(result)
 
+    raw_url_args = {'rev1': rev1}
+    if rev2:
+        raw_url_args['rev2'] = rev2
 
-@app.route('/api/revert/<path:url>', methods=['POST'])
-def api_revert_page(url):
-    if not 'rev' in request.form:
-        abort(400)
-    rev = request.form['rev']
-    author = request.remote_addr
-    if 'author' in request.form and len(request.form['author']) > 0:
-        author = request.form['author']
-    message = 'Reverted %s to revision %s' % (url, rev)
-    if 'message' in request.form and len(request.form['message']) > 0:
-        message = request.form['message']
+    wiki = get_wiki()
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    data = diff_page_revs(wiki, user, url,
+                          rev1=rev1, rev2=rev2, raw=raw)
+    add_auth_data(data)
+    add_navigation_data(
+            url, data,
+            read=True,
+            raw_url='/api/diff/%s?%s' % (
+                url.lstrip('/'),
+                urllib.parse.urlencode(raw_url_args)))
+    return render_template('diff-page.html', **data)
 
-    url = url_from_viewarg(url)
-    page_fields = {
-            'rev': rev,
-            'author': author,
-            'message': message
-            }
-    wiki = get_wiki()
-    wiki.revertPage(url, page_fields)
-    result = {'reverted': 1}
-    return jsonify(result)
-
--- a/wikked/views/read.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/read.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,259 +1,69 @@
-import time
-import urllib.request, urllib.parse, urllib.error
-from flask import (render_template, request, g, jsonify, make_response,
-                   abort)
+import urllib.parse
+from flask import (
+        render_template, request, abort)
 from flask.ext.login import current_user
-from wikked.views import (get_page_meta, get_page_or_404, get_page_or_none,
-        is_page_readable, get_redirect_target,
-        url_from_viewarg, split_url_from_viewarg,
-        RedirectNotFound, CircularRedirectError,
-        CHECK_FOR_READ)
+from wikked.views import add_auth_data, add_navigation_data
 from wikked.web import app, get_wiki
-from wikked.scm.base import STATE_NAMES
+from wikked.webimpl import url_from_viewarg
+from wikked.webimpl.read import (
+        read_page, get_incoming_links)
+from wikked.webimpl.special import get_search_results
 
 
 @app.route('/')
 def home():
-    tpl_name = 'index.html'
-    if app.config['WIKI_DEV_ASSETS']:
-        tpl_name = 'index-dev.html'
-    return render_template(tpl_name, cache_bust=('?%d' % time.time()))
+    wiki = get_wiki()
+    url = wiki.main_page_url.lstrip('/')
+    return read(url)
 
 
 @app.route('/read/<path:url>')
 def read(url):
-    tpl_name = 'index.html'
-    if app.config['WIKI_DEV_ASSETS']:
-        tpl_name = 'index-dev.html'
-    return render_template(tpl_name, cache_bust=('?%d' % time.time()))
+    wiki = get_wiki()
+    url = url_from_viewarg(url)
+    user = current_user.get_id()
+    no_redirect = 'no_redirect' in request.args
+    data = read_page(wiki, user, url, no_redirect=no_redirect)
+    add_auth_data(data)
+    add_navigation_data(
+            url, data,
+            edit=True, history=True, inlinks=True,
+            raw_url='/api/raw/' + url.lstrip('/'))
+    return render_template('read-page.html', **data)
 
 
 @app.route('/search')
 def search():
-    tpl_name = 'index.html'
-    if app.config['WIKI_DEV_ASSETS']:
-        tpl_name = 'index-dev.html'
-    return render_template(tpl_name, cache_bust=('?%d' % time.time()))
-
-
-@app.route('/api/list')
-def api_list_all_pages():
-    return api_list_pages(None)
-
-
-@app.route('/api/list/<path:url>')
-def api_list_pages(url):
-    wiki = get_wiki()
-    pages = list(filter(is_page_readable, wiki.getPages(url_from_viewarg(url))))
-    page_metas = [get_page_meta(page) for page in pages]
-    result = {'path': url, 'pages': list(page_metas)}
-    return jsonify(result)
-
-
-@app.route('/api/read/')
-def api_read_main_page():
-    wiki = get_wiki()
-    return api_read_page(wiki.main_page_url.lstrip('/'))
-
-
-@app.route('/api/read/<path:url>')
-def api_read_page(url):
-    additional_info = {}
-    if 'user' in request.args:
-        user = current_user
-        if user.is_authenticated():
-            user_page_url = 'user:' + user.username.title()
-            additional_info['user'] = {
-                    'username': user.username,
-                    'groups': user.groups,
-                    'page_url': user_page_url
-                    }
-        else:
-            additional_info['user'] = False
-
-    no_redirect = ('no_redirect' in request.args)
-
-    endpoint, path = split_url_from_viewarg(url)
-    if endpoint is None:
-        # Normal page.
-        try:
-            page, visited_paths = get_redirect_target(
-                    path,
-                    fields=['url', 'title', 'text', 'meta'],
-                    convert_url=False,
-                    check_perms=CHECK_FOR_READ,
-                    first_only=no_redirect)
-        except RedirectNotFound as e:
-            app.logger.exception(e)
-            abort(404)
-        except CircularRedirectError as e:
-            app.logger.exception(e)
-            abort(409)
-        if page is None:
-            abort(404)
-
-        if no_redirect:
-            additional_info['redirects_to'] = visited_paths[-1]
-        elif len(visited_paths) > 1:
-            additional_info['redirected_from'] = visited_paths[:-1]
-
-        result = {'meta': get_page_meta(page), 'text': page.text}
-        result.update(additional_info)
-        return jsonify(result)
-
-    # Meta listing page or special endpoint.
-    meta_page_url = '%s:%s' % (endpoint, path)
-    info_page = get_page_or_none(
-            meta_page_url,
-            fields=['url', 'title', 'text', 'meta'],
-            convert_url=False,
-            check_perms=CHECK_FOR_READ)
+    query = request.args.get('q')
+    if query is None or query == '':
+        abort(400)
 
     wiki = get_wiki()
-    endpoint_info = wiki.endpoints.get(endpoint)
-    if endpoint_info is not None:
-        # We have some information about this endpoint...
-        if endpoint_info.default and info_page is None:
-            # Default page text.
-            info_page = get_page_or_404(
-                    endpoint_info.default,
-                    fields=['url', 'title', 'text', 'meta'],
-                    convert_url=False,
-                    check_perms=CHECK_FOR_READ)
-
-        if not endpoint_info.query:
-            # Not a query-based endpoint (like categories). Let's just
-            # return the text.
-            result = {
-                    'endpoint': endpoint,
-                    'meta': get_page_meta(info_page),
-                    'text': info_page.text}
-            result.update(additional_info)
-            return jsonify(result)
-
-    # Get the list of pages to show here.
-    value = path.lstrip('/')
-    query = {endpoint: [value]}
-    pages = wiki.getPages(meta_query=query,
-            fields=['url', 'title', 'text', 'meta'])
-    tpl_data = {
-            'name': endpoint,
-            'url': meta_page_url,
-            'value': value,
-            'safe_value': urllib.parse.quote(value.encode('utf-8')),
-            'pages': [get_page_meta(p) for p in pages]
-            # TODO: skip pages that are forbidden for the current user
-        }
-    if info_page:
-        tpl_data['info_text'] = info_page.text
-
-    # Render the final page as the list of pages matching the query,
-    # under either a default text, or the text from the meta page.
-    text = render_template('meta_page.html', **tpl_data)
-    result = {
-            'endpoint': endpoint,
-            'meta_query': endpoint,
-            'meta_value': value,
-            'query': query,
-            'meta': {
-                    'url': urllib.parse.quote(meta_page_url.encode('utf-8')),
-                    'title': value
-                },
-            'text': text
-        }
-    if info_page:
-        result['meta'] = get_page_meta(info_page)
-
-    result.update(additional_info)
-    return jsonify(result)
-
-
-@app.route('/api/raw/')
-def api_read_main_page_raw():
-    wiki = get_wiki()
-    return api_read_page_raw(wiki.main_page_url.lstrip('/'))
-
-
-@app.route('/api/raw/<path:url>')
-def api_read_page_raw(url):
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ,
-            fields=['raw_text', 'meta'])
-    resp = make_response(page.raw_text)
-    resp.mimetype = 'text/plain'
-    return resp
+    user = current_user.get_id()
+    data = get_search_results(wiki, user, query)
+    add_auth_data(data)
+    add_navigation_data(
+            None, data,
+            raw_url='/api/search?%s' % urllib.parse.urlencode({'q': query}))
+    return render_template('search-results.html', **data)
 
 
-@app.route('/api/query')
-def api_query():
-    wiki = get_wiki()
-    query = dict(request.args)
-    pages = wiki.getPages(meta_query=query)
-    result = {
-            'query': query,
-            'pages': [get_page_meta(p) for p in pages]
-        }
-    return jsonify(result)
-
-
-@app.route('/api/state/')
-def api_get_main_page_state():
+@app.route('/inlinks')
+def incoming_links_to_main_page():
     wiki = get_wiki()
-    return api_get_state(wiki.main_page_url.lstrip('/'))
-
-
-@app.route('/api/state/<path:url>')
-def api_get_state(url):
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ,
-            fields=['url', 'title', 'path', 'meta'])
-    state = page.getState()
-    return jsonify({
-        'meta': get_page_meta(page, True),
-        'state': STATE_NAMES[state]
-        })
-
-
-@app.route('/api/outlinks/')
-def api_get_main_page_outgoing_links():
-    wiki = get_wiki()
-    return api_get_outgoing_links(wiki.main_page_url.lstrip('/'))
+    return incoming_links(wiki.main_page_url.lstrip('/'))
 
 
-@app.route('/api/outlinks/<path:url>')
-def api_get_outgoing_links(url):
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ,
-            fields=['url', 'title', 'links'])
-    links = []
-    for link in page.links:
-        other = get_page_or_none(link, convert_url=False,
-                fields=['url', 'title', 'meta'])
-        if other is not None and is_page_readable(other):
-            links.append(get_page_meta(other))
-        else:
-            links.append({'url': link, 'missing': True})
-
-    result = {'meta': get_page_meta(page), 'out_links': links}
-    return jsonify(result)
-
-
-@app.route('/api/inlinks/')
-def api_get_main_page_incoming_links():
+@app.route('/inlinks/<path:url>')
+def incoming_links(url):
     wiki = get_wiki()
-    return api_get_incoming_links(wiki.main_page_url.lstrip('/'))
-
+    user = current_user.get_id()
+    url = url_from_viewarg(url)
+    data = get_incoming_links(wiki, user, url)
+    add_auth_data(data)
+    add_navigation_data(
+            url, data,
+            read=True, edit=True, history=True,
+            raw_url='/api/inlinks/' + url.lstrip('/'))
+    return render_template('inlinks-page.html', **data)
 
-@app.route('/api/inlinks/<path:url>')
-def api_get_incoming_links(url):
-    page = get_page_or_404(url, check_perms=CHECK_FOR_READ,
-            fields=['url', 'title', 'meta'])
-    links = []
-    for link in page.getIncomingLinks():
-        other = get_page_or_none(link, convert_url=False,
-                fields=['url', 'title', 'meta'])
-        if other is not None and is_page_readable(other):
-            links.append(get_page_meta(other))
-        else:
-            links.append({'url': link, 'missing': True})
-
-    result = {'meta': get_page_meta(page), 'in_links': links}
-    return jsonify(result)
-
--- a/wikked/views/special.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/views/special.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,159 +1,116 @@
-from flask import g, jsonify, request, abort
-from wikked.views import (
-    is_page_readable, get_page_meta, get_page_or_none,
-    get_or_build_pagelist, get_generic_pagelist_builder,
-    get_redirect_target, CircularRedirectError, RedirectNotFound)
-from wikked.utils import get_absolute_url
+from flask import render_template
+from flask.ext.login import current_user
+from wikked.views import add_auth_data
 from wikked.web import app, get_wiki
-
-
-def build_pagelist_view_data(pages):
-    pages = sorted(pages, key=lambda p: p.url)
-    data = [get_page_meta(p) for p in pages if is_page_readable(p)]
-    result = {'pages': data}
-    return jsonify(result)
-
-
-def generic_pagelist_view(list_name, filter_func, fields=None):
-    fields = fields or ['url', 'title', 'meta']
-    pages = get_or_build_pagelist(
-            list_name,
-            get_generic_pagelist_builder(filter_func, fields),
-            fields=fields)
-    return build_pagelist_view_data(pages)
-
-
-@app.route('/api/orphans')
-def api_special_orphans():
-    def builder_func():
-        wiki = get_wiki()
-        wiki.resolve()
-
-        pages = {}
-        rev_links = {}
-        for p in wiki.getPages(
-                no_endpoint_only=True,
-                fields=['url', 'title', 'meta', 'links']):
-            pages[p.url] = p
-            rev_links[p.url] = 0
-
-            for l in p.links:
-                abs_l = get_absolute_url(p.url, l)
-                cnt = rev_links.get(abs_l, 0)
-                rev_links[abs_l] = cnt + 1
-
-        or_pages = []
-        for tgt, cnt in rev_links.items():
-            if cnt == 0:
-                or_pages.append(pages[tgt])
-        return or_pages
-
-    fields = ['url', 'title', 'meta', 'links']
-    pages = get_or_build_pagelist('orphans', builder_func, fields)
-    return build_pagelist_view_data(pages)
-
-
-@app.route('/api/broken-redirects')
-def api_special_broken_redirects():
-    def filter_func(page):
-        redirect_meta = page.getMeta('redirect')
-        if redirect_meta is None:
-            return False
-
-        path = get_absolute_url(page.url, redirect_meta)
-        try:
-            target, visited = get_redirect_target(
-                    path,
-                    fields=['url', 'meta'])
-        except CircularRedirectError:
-            return True
-        except RedirectNotFound:
-            return True
-        return False
-
-    return generic_pagelist_view('broken_redirects', filter_func)
+from wikked.webimpl.special import (
+        get_orphans, get_broken_redirects, get_double_redirects,
+        get_dead_ends)
 
 
-@app.route('/api/double-redirects')
-def api_special_double_redirects():
-    def builder_func():
-        wiki = get_wiki()
-        wiki.resolve()
-
-        pages = {}
-        redirs = {}
-        for p in wiki.getPages(
-                no_endpoint_only=True,
-                fields=['url', 'title', 'meta']):
-            pages[p.url] = p
-
-            target = p.getMeta('redirect')
-            if target:
-                target = get_absolute_url(p.url, target)
-                redirs[p.url] = target
+special_sections = [
+        {
+            'name': 'wiki',
+            'title': 'Wiki'
+            },
+        {
+            'name': 'lists',
+            'title': 'Page Lists'
+            },
+        {
+            'name': 'users',
+            'title': 'Users'
+            }
+        ]
 
-        dr_pages = []
-        for src, tgt in redirs.items():
-            if tgt in redirs:
-                dr_pages.append(pages[src])
-        return dr_pages
-
-    fields = ['url', 'title', 'meta']
-    pages = get_or_build_pagelist('double_redirects', builder_func, fields)
-    return build_pagelist_view_data(pages)
-
-
-@app.route('/api/dead-ends')
-def api_special_dead_ends():
-    def filter_func(page):
-        return len(page.links) == 0
-
-    return generic_pagelist_view(
-            'dead_ends', filter_func,
-            fields=['url', 'title', 'meta', 'links'])
+special_pages = {
+        'changes': {
+            "title": "Recent Changes",
+            "url": "/special/history",
+            "description": "See all changes in the wiki.",
+            "section": "wiki",
+            },
+        'orphans': {
+            "title": "Orphaned Pages",
+            "url": "/special/list/orphans",
+            "description": ("Lists pages in the wiki that have no "
+                            "links to them."),
+            "section": "lists",
+            "template": "special-orphans.html"
+            },
+        'broken-redirects': {
+            "title": "Broken Redirects",
+            "url": "/special/list/broken-redirects",
+            "description": ("Lists pages that redirect to a missing "
+                            "page."),
+            "section": "lists",
+            "template": "special-broken-redirects.html"
+            },
+        'double-redirects': {
+            "title": "Double Redirects",
+            "url": "/special/list/double-redirects",
+            "description": "Lists pages that redirect twice or more.",
+            "section": "lists",
+            "template": "special-double-redirects.html"
+            },
+        'dead-ends': {
+            "title": "Dead-End Pages",
+            "url": "/special/list/dead-ends",
+            "description": ("Lists pages that don't have any "
+                            "outgoing links."),
+            "section": "lists",
+            "template": "special-dead-ends.html"
+            },
+        'users': {
+            "title": "All Users",
+            "url": "/special/users",
+            "description": "A list of all registered users.",
+            "section": "users",
+            }
+        }
 
 
-@app.route('/api/search')
-def api_search():
-    query = request.args.get('q')
-    if query is None or query == '':
-        abort(400)
+@app.route('/special')
+def special_pages_dashboard():
+    data = {'sections': []}
+    for info in special_sections:
+        sec = {'title': info['title'], 'pages': []}
+        for k, p in special_pages.items():
+            if p['section'] == info['name']:
+                sec['pages'].append(p)
+        sec['pages'] = sorted(sec['pages'], key=lambda i: i['title'])
+        data['sections'].append(sec)
 
-    readable_hits = []
-    wiki = get_wiki()
-    hits = list(wiki.index.search(query))
-    for h in hits:
-        page = get_page_or_none(h.url, convert_url=False)
-        if page is not None and is_page_readable(page):
-            readable_hits.append({
-                    'url': h.url,
-                    'title': h.title,
-                    'text': h.hl_text})
-
-    result = {
-            'query': query,
-            'hit_count': len(readable_hits),
-            'hits': readable_hits}
-    return jsonify(result)
+    add_auth_data(data)
+    return render_template('special-pages.html', **data)
 
 
-@app.route('/api/searchpreview')
-def api_searchpreview():
-    query = request.args.get('q')
-    if query is None or query == '':
-        abort(400)
-
-    readable_hits = []
+def call_api(page_name, api_func, *args, **kwargs):
     wiki = get_wiki()
-    hits = list(wiki.index.previewSearch(query))
-    for h in hits:
-        page = get_page_or_none(h.url, convert_url=False)
-        if page is not None and is_page_readable(page):
-            readable_hits.append({'url': h.url, 'title': h.title})
+    user = current_user.get_id()
+    info = special_pages[page_name]
+    data = api_func(wiki, user, *args, **kwargs)
+    add_auth_data(data)
+    data['title'] = info['title']
+    data['is_special_page'] = True
+    return render_template(info['template'], **data)
+
+
+@app.route('/special/list/orphans')
+def special_list_orphans():
+    return call_api('orphans', get_orphans)
+
 
-    result = {
-            'query': query,
-            'hit_count': len(readable_hits),
-            'hits': readable_hits}
-    return jsonify(result)
+@app.route('/special/list/broken-redirects')
+def special_list_broken_redirects():
+    return call_api('broken-redirects', get_broken_redirects)
+
 
+@app.route('/special/list/double-redirects')
+def special_list_double_redirects():
+    return call_api('double-redirects', get_double_redirects)
+
+
+@app.route('/special/list/dead-ends')
+def special_list_dead_ends():
+    return call_api('dead-ends', get_dead_ends)
+
--- a/wikked/web.py	Sun Aug 30 21:45:42 2015 -0700
+++ b/wikked/web.py	Wed Sep 16 23:04:28 2015 -0700
@@ -1,6 +1,7 @@
 import os
 import os.path
 import logging
+import urllib.parse
 from werkzeug import SharedDataMiddleware
 from flask import Flask, abort, g
 from wikked.wiki import Wiki, WikiParameters
@@ -25,7 +26,8 @@
 app.config.setdefault('WIKI_AUTO_RELOAD', False)
 app.config.setdefault('WIKI_ASYNC_UPDATE', False)
 app.config.setdefault('WIKI_SERVE_FILES', False)
-app.config.setdefault('WIKI_BROKER_URL', 'sqla+sqlite:///%(root)s/.wiki/broker.db')
+app.config.setdefault('WIKI_BROKER_URL',
+                      'sqla+sqlite:///%(root)s/.wiki/broker.db')
 app.config.setdefault('WIKI_NO_FLASK_LOGGER', False)
 app.config.setdefault('PROFILE', False)
 app.config.setdefault('PROFILE_DIR', None)
@@ -55,9 +57,11 @@
 
 # Make the app serve static content and wiki assets in DEBUG mode.
 if app.config['WIKI_DEV_ASSETS'] or app.config['WIKI_SERVE_FILES']:
-    app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
-        '/files': os.path.join(wiki_root, '_files')
-    })
+    app.wsgi_app = SharedDataMiddleware(
+            app.wsgi_app,
+            {
+                '/files': os.path.join(wiki_root, '_files')},
+            cache=False)
 
 
 # In DEBUG mode, also serve raw assets instead of static ones.
@@ -113,6 +117,40 @@
     return wiki.auth.getUser(username)
 
 
+# Setup the Jinja environment.
+def get_read_url(url):
+    return '/read/' + url.lstrip('/')
+
+
+def get_edit_url(url):
+    return '/edit/' + url.lstrip('/')
+
+
+def get_rev_url(url, rev):
+    return '/rev/%s?%s' % (url.lstrip('/'),
+                           urllib.parse.urlencode({'rev': rev}))
+
+
+def get_diff_url(url, rev1=None, rev2=None):
+    args = {}
+    if rev1 is not None:
+        args['rev1'] = rev1
+    if rev2 is not None:
+        args['rev2'] = rev2
+    if len(args) > 0:
+        return '/diff/%s?%s' % (url.lstrip('/'),
+                                urllib.parse.urlencode(args))
+    return '/diff/%s' % url.lstrip('/')
+
+
+app.jinja_env.globals.update({
+    'get_read_url': get_read_url,
+    'get_edit_url': get_edit_url,
+    'get_rev_url': get_rev_url,
+    'get_diff_url': get_diff_url
+    })
+
+
 from flask.ext.login import LoginManager
 login_manager = LoginManager()
 login_manager.init_app(app)
@@ -128,12 +166,17 @@
 # Import the views.
 # (this creates a PyLint warning but it's OK)
 # pylint: disable=unused-import
-import wikked.views.error
-import wikked.views.read
+import wikked.api.admin
+import wikked.api.edit
+import wikked.api.history
+import wikked.api.read
+import wikked.api.special
+import wikked.views.admin
 import wikked.views.edit
+import wikked.views.error
 import wikked.views.history
+import wikked.views.read
 import wikked.views.special
-import wikked.views.admin
 
 
 # Async wiki update.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/webimpl/__init__.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,194 @@
+import os.path
+import logging
+import datetime
+import urllib.parse
+from wikked.utils import (
+        get_absolute_url, PageNotFoundError, split_page_url, is_endpoint_url)
+
+
+logger = logging.getLogger(__name__)
+
+
+CHECK_FOR_READ = 1
+CHECK_FOR_WRITE = 2
+
+
+class CircularRedirectError(Exception):
+    def __init__(self, url, visited):
+        super(CircularRedirectError, self).__init__(
+                "Circular redirect detected at '%s' "
+                "after visiting: %s" % (url, visited))
+
+
+class RedirectNotFound(Exception):
+    def __init__(self, url, not_found):
+        super(RedirectNotFound, self).__init__(
+                "Target redirect page '%s' not found from '%s'." %
+                (url, not_found))
+
+
+class PermissionError(Exception):
+    pass
+
+
+def url_from_viewarg(url):
+    if is_endpoint_url(url):
+        return url
+    return '/' + url
+
+
+def split_url_from_viewarg(url):
+    url = urllib.parse.unquote(url)
+    endpoint, path = split_page_url(url)
+    if endpoint:
+        return (endpoint, path)
+    return (None, '/' + path)
+
+
+def get_page_or_raise(wiki, url, fields=None,
+                      check_perms=None, auto_reload=False):
+    if auto_reload and fields is not None:
+        if 'path' not in fields:
+            fields.append('path')
+        if 'cache_time' not in fields:
+            fields.append('cache_time')
+        if 'is_resolved' not in fields:
+            fields.append('is_resolved')
+
+    page = wiki.getPage(url, fields=fields)
+
+    if auto_reload:
+        path_time = datetime.datetime.fromtimestamp(
+                os.path.getmtime(page.path))
+        if path_time >= page.cache_time:
+            logger.info("Page '%s' has changed, reloading." % url)
+            wiki.updatePage(path=page.path)
+            page = wiki.getPage(url, fields=fields)
+        elif not page.is_resolved:
+            logger.info("Page '%s' was not resolved, resolving now." % url)
+            wiki.resolve(only_urls=[url])
+            wiki.index.updatePage(wiki.db.getPage(
+                url, fields=['url', 'path', 'title', 'text']))
+            page = wiki.getPage(url, fields=fields)
+
+    if check_perms is not None:
+        user, mode = check_perms
+        if mode == CHECK_FOR_READ and not is_page_readable(page, user):
+            raise PermissionError()
+        elif mode == CHECK_FOR_WRITE and not is_page_writable(page, user):
+            raise PermissionError()
+
+    return page
+
+
+def get_page_or_none(wiki, url, **kwargs):
+    try:
+        return get_page_or_raise(wiki, url, **kwargs)
+    except PageNotFoundError:
+        return None
+
+
+def is_page_readable(page, user):
+    return page.wiki.auth.isPageReadable(page, user)
+
+
+def is_page_writable(page, user):
+    return page.wiki.auth.isPageWritable(page, user)
+
+
+def get_page_meta(page, local_only=False):
+    if local_only:
+        meta = dict(page.getLocalMeta() or {})
+    else:
+        meta = dict(page.getMeta() or {})
+    meta['title'] = page.title
+    meta['url'] = urllib.parse.quote(page.url.encode('utf-8'))
+    for name in COERCE_META:
+        if name in meta:
+            meta[name] = COERCE_META[name](meta[name])
+    return meta
+
+
+def get_category_meta(category):
+    result = []
+    for item in category:
+        result.append({
+            'url': 'category:/' + urllib.parse.quote(item.encode('utf-8')),
+            'name': item
+            })
+    return result
+
+
+def get_redirect_target(wiki, path, fields=None,
+                        check_perms=None, first_only=False):
+    page = None
+    orig_path = path
+    visited_paths = []
+
+    while True:
+        page = get_page_or_none(
+                wiki, path,
+                fields=fields,
+                check_perms=check_perms)
+        if page is None:
+            raise RedirectNotFound(orig_path, path)
+
+        visited_paths.append(path)
+        redirect_meta = page.getMeta('redirect')
+        if redirect_meta is None:
+            break
+
+        path = get_absolute_url(path, redirect_meta)
+        if first_only:
+            visited_paths.append(path)
+            break
+
+        if path in visited_paths:
+            raise CircularRedirectError(path, visited_paths)
+
+    return page, visited_paths
+
+
+COERCE_META = {
+    'category': get_category_meta
+    }
+
+
+def get_or_build_pagelist(wiki, list_name, builder, fields=None,
+                          build_inline=True):
+    # If the wiki is using background jobs, we can accept invalidated
+    # lists... it just means that a background job is hopefully still
+    # just catching up.
+    # Otherwise, everything is synchronous and we need to build the
+    # list if needed.
+    page_list = wiki.db.getPageListOrNone(list_name, fields=fields,
+                                          valid_only=build_inline)
+    if page_list is None and build_inline:
+        logger.info("Regenerating list: %s" % list_name)
+        page_list = builder()
+        wiki.db.addPageList(list_name, page_list)
+
+    return page_list
+
+
+def get_generic_pagelist_builder(wiki, filter_func, fields=None):
+    fields = fields or ['url', 'title', 'meta']
+
+    def builder():
+        # Make sure all pages have been resolved.
+        wiki.resolve()
+
+        pages = []
+        for page in wiki.getPages(
+                no_endpoint_only=True,
+                fields=fields):
+            try:
+                if filter_func(page):
+                    pages.append(page)
+            except Exception as e:
+                logger.error("Error while inspecting page: %s" % page.url)
+                logger.error("   %s" % e)
+        return pages
+
+    return builder
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/webimpl/admin.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,18 @@
+from flask import request
+from flask.ext.login import login_user
+from wikked.web import app, get_wiki
+
+
+def do_login_user():
+    username = request.form.get('username')
+    password = request.form.get('password')
+    remember = request.form.get('remember')
+
+    wiki = get_wiki()
+    user = wiki.auth.getUser(username)
+    if user is not None and app.bcrypt:
+        if app.bcrypt.check_password_hash(user.password, password):
+            login_user(user, remember=bool(remember))
+            return True
+    return False
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/webimpl/edit.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,117 @@
+import logging
+import urllib.parse
+from wikked.page import Page, PageData
+from wikked.formatter import PageFormatter, FormattingContext
+from wikked.resolver import PageResolver
+from wikked.utils import PageNotFoundError, split_page_url
+from wikked.webimpl import (
+        CHECK_FOR_WRITE,
+        get_page_or_raise, get_page_meta)
+
+
+logger = logging.getLogger(__name__)
+
+
+class DummyPage(Page):
+    """ A dummy page for previewing in-progress editing.
+    """
+    def __init__(self, wiki, url, text):
+        data = self._loadData(wiki, url, text)
+        super(DummyPage, self).__init__(wiki, data)
+
+    def _loadData(self, wiki, url, text):
+        data = PageData()
+        extension = wiki.fs.default_extension
+        data.url = url
+        data.path = '__preview__.' + extension
+        data.raw_text = text
+
+        ctx = FormattingContext(url)
+        f = PageFormatter()
+        data.formatted_text = f.formatText(ctx, data.raw_text)
+        data.local_meta = ctx.meta
+        data.local_links = ctx.out_links
+
+        data.title = (data.local_meta.get('title') or
+                      make_page_title(url))
+        if isinstance(data.title, list):
+            data.title = data.title[0]
+
+        return data
+
+
+def make_page_title(url):
+    endpoint, path = split_page_url(url)
+    last_slash = path.rstrip('/').rfind('/')
+    if last_slash < 0:
+        title = url[1:]
+    else:
+        title = url[last_slash + 1:]
+    if endpoint:
+        return '%s: %s' % (endpoint, title)
+    return title
+
+
+def get_edit_page(wiki, user, url, author=None, custom_data=None):
+    page = None
+    try:
+        page = get_page_or_raise(wiki, url,
+                                 check_perms=(user, CHECK_FOR_WRITE))
+    except PageNotFoundError:
+        # Only catch errors about the page not existing. Permission
+        # errors still go through.
+        page = None
+
+    if page is None:
+        result = {
+                'meta': {
+                    'url': urllib.parse.quote(url.encode('utf-8')),
+                    'title': make_page_title(url)
+                    },
+                'text': ''
+                }
+    else:
+        result = {
+                'meta': get_page_meta(page, True),
+                'text': page.raw_text
+                }
+
+    result['commit_meta'] = {
+            'author': author,
+            'desc': 'Editing ' + result['meta']['title']
+            }
+
+    if custom_data:
+        result.update(custom_data)
+
+    return result
+
+
+def do_edit_page(wiki, user, url, text, author=None, message=None):
+    try:
+        get_page_or_raise(wiki, url,
+                          check_perms=(user, CHECK_FOR_WRITE))
+    except PageNotFoundError:
+        # Only catch errors about the page not existing. Permission
+        # errors still go through.
+        pass
+
+    author = author or user
+    if author is None:
+        raise Exception("No author or user was specified.")
+
+    message = message or 'Edited ' + url
+    page_fields = {
+            'text': text,
+            'author': user,
+            'message': message
+            }
+    wiki.setPage(url, page_fields)
+
+
+def preview_edited_page(wiki, url, raw_text):
+    dummy = DummyPage(wiki, url, raw_text)
+    resolver = PageResolver(dummy)
+    dummy._setExtendedData(resolver.run())
+    return dummy.text
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/webimpl/history.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,102 @@
+import os.path
+import datetime
+from pygments import highlight
+from pygments.formatters import get_formatter_by_name
+from pygments.lexers import get_lexer_by_name
+from wikked.page import PageLoadingError
+from wikked.scm.base import ACTION_NAMES
+from wikked.utils import PageNotFoundError
+from wikked.webimpl import (
+        CHECK_FOR_READ,
+        is_page_readable, get_page_meta, get_page_or_raise)
+
+
+def get_history_data(wiki, user, history, needs_files=False):
+    hist_data = []
+    for i, rev in enumerate(history):
+        rev_data = {
+            'index': i + 1,
+            'rev_id': rev.rev_id,
+            'rev_name': rev.rev_name,
+            'author': rev.author.name,
+            'timestamp': rev.timestamp,
+            'description': rev.description
+            }
+        dt = datetime.datetime.fromtimestamp(rev.timestamp)
+        rev_data['datetime'] = dt.strftime('%x %X')
+        if needs_files:
+            rev_data['pages'] = []
+            for f in rev.files:
+                url = None
+                path = os.path.join(wiki.root, f['path'])
+                try:
+                    page = wiki.db.getPage(path=path)
+                    # Hide pages that the user can't see.
+                    if not is_page_readable(page, user):
+                        continue
+                    url = page.url
+                except PageNotFoundError:
+                    pass
+                except PageLoadingError:
+                    pass
+                if not url:
+                    url = os.path.splitext(f['path'])[0]
+                rev_data['pages'].append({
+                    'url': url,
+                    'action': ACTION_NAMES[f['action']]
+                    })
+            rev_data['num_pages'] = len(rev_data['pages'])
+            if len(rev_data['pages']) > 0:
+                hist_data.append(rev_data)
+        else:
+            hist_data.append(rev_data)
+    return hist_data
+
+
+def get_site_history(wiki, user, after_rev=None):
+    history = wiki.getHistory(limit=10, after_rev=after_rev)
+    hist_data = get_history_data(wiki, user, history, needs_files=True)
+    result = {'history': hist_data}
+    return result
+
+
+def get_page_history(wiki, user, url):
+    page = get_page_or_raise(wiki, url, check_perms=(user, CHECK_FOR_READ))
+    history = page.getHistory()
+    hist_data = get_history_data(wiki, user, history)
+    result = {'url': url, 'meta': get_page_meta(page), 'history': hist_data}
+    return result
+
+
+def read_page_rev(wiki, user, url, rev):
+    page = get_page_or_raise(wiki, url, check_perms=(user, CHECK_FOR_READ))
+    page_rev = page.getRevision(rev)
+    meta = dict(get_page_meta(page, True), rev=rev)
+    result = {'meta': meta, 'text': page_rev}
+    return result
+
+
+def diff_page_revs(wiki, user, url, rev1, rev2=None, raw=False):
+    page = get_page_or_raise(wiki, url, check_perms=(user, CHECK_FOR_READ))
+    diff = page.getDiff(rev1, rev2)
+    if not raw:
+        lexer = get_lexer_by_name('diff')
+        formatter = get_formatter_by_name('html')
+        diff = highlight(diff, lexer, formatter)
+    if rev2 is None:
+        meta = dict(get_page_meta(page, True), change=rev1)
+    else:
+        meta = dict(get_page_meta(page, True), rev1=rev1, rev2=rev2)
+    result = {'meta': meta, 'diff': diff}
+    return result
+
+
+def revert_page(wiki, user, url, rev, message=None):
+    message = message or 'Reverted %s to revision %s' % (url, rev)
+    page_fields = {
+            'rev': rev,
+            'author': user,
+            'message': message
+            }
+    wiki.revertPage(url, page_fields)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/webimpl/read.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,142 @@
+import urllib.parse
+from wikked.webimpl import (
+        CHECK_FOR_READ,
+        get_redirect_target, get_page_meta, get_page_or_raise)
+from wikked.utils import split_page_url, PageNotFoundError
+
+
+def read_page(wiki, user, url, *, no_redirect=False):
+    additional_info = {}
+    endpoint, path = split_page_url(url)
+    if endpoint is None:
+        # Normal page.
+        page, visited_paths = get_redirect_target(
+                wiki, path,
+                fields=['url', 'title', 'text', 'meta'],
+                check_perms=(user, CHECK_FOR_READ),
+                first_only=no_redirect)
+        if page is None:
+            raise PageNotFoundError(url)
+
+        if no_redirect:
+            additional_info['redirects_to'] = visited_paths[-1]
+        elif len(visited_paths) > 1:
+            additional_info['redirected_from'] = visited_paths[:-1]
+
+        result = {'meta': get_page_meta(page), 'text': page.text,
+                  'page_title': page.title}
+        result.update(additional_info)
+        return result
+
+    # Meta listing page or special endpoint.
+    meta_page_url = '%s:%s' % (endpoint, path)
+    try:
+        info_page = get_page_or_raise(
+                wiki, meta_page_url,
+                fields=['url', 'title', 'text', 'meta'],
+                check_perms=(user, CHECK_FOR_READ))
+    except PageNotFoundError:
+        # Let permissions errors go through, but if the info page is not
+        # found that's OK.
+        info_page = None
+
+    endpoint_info = wiki.endpoints.get(endpoint)
+    if endpoint_info is not None:
+        # We have some information about this endpoint...
+        if endpoint_info.default and info_page is None:
+            # Default page text.
+            info_page = get_page_or_raise(
+                    wiki, endpoint_info.default,
+                    fields=['url', 'title', 'text', 'meta'],
+                    check_perms=(user, CHECK_FOR_READ))
+
+        if not endpoint_info.query:
+            # Not a query-based endpoint (like categories). Let's just
+            # return the text.
+            result = {
+                    'endpoint': endpoint,
+                    'meta': get_page_meta(info_page),
+                    'text': info_page.text,
+                    'page_title': info_page.title}
+            result.update(additional_info)
+            return result
+
+    # Get the list of pages to show here.
+    value = path.lstrip('/')
+    value_safe = urllib.parse.quote(value.encode('utf-8'))
+    query = {endpoint: [value]}
+    pages = wiki.getPages(meta_query=query,
+                          fields=['url', 'title', 'text', 'meta'])
+    meta = {}
+    page_title = value
+    if info_page:
+        meta = get_page_meta(info_page)
+        page_title = info_page.title
+    # Need to override the info page's URL and title.
+    meta.update({
+            'url': urllib.parse.quote(meta_page_url.encode('utf-8')),
+            'title': value
+            })
+    # TODO: skip pages that are forbidden for the current user
+    pages_meta = [get_page_meta(p) for p in pages]
+
+    result = {
+            'endpoint': endpoint,
+            'is_query': True,
+            'meta_query': endpoint,
+            'meta_value': value,
+            'meta_value_safe': value_safe,
+            'query': query,
+            'query_results': pages_meta,
+            'meta': {
+                    'url': urllib.parse.quote(meta_page_url.encode('utf-8')),
+                    'title': value
+                    },
+            'page_title': page_title
+            }
+    if info_page:
+        result['text'] = info_page.text
+
+    result.update(additional_info)
+    return result
+
+
+def get_incoming_links(wiki, user, url):
+    page = get_page_or_raise(
+            wiki, url,
+            check_perms=(user, CHECK_FOR_READ),
+            fields=['url', 'title', 'meta'])
+    links = []
+    for link in page.getIncomingLinks():
+        try:
+            other = get_page_or_raise(
+                    wiki, link,
+                    check_perms=(user, CHECK_FOR_READ),
+                    fields=['url', 'title', 'meta'])
+            links.append(get_page_meta(other))
+        except PageNotFoundError:
+            links.append({'url': link, 'missing': True})
+
+    result = {'meta': get_page_meta(page), 'in_links': links}
+    return result
+
+
+def get_outgoing_links(wiki, user, url):
+    page = get_page_or_raise(
+            wiki, url,
+            check_perms=(user, CHECK_FOR_READ),
+            fields=['url', 'title', 'links'])
+    links = []
+    for link in page.links:
+        try:
+            other = get_page_or_raise(
+                    wiki, link,
+                    check_perms=(user, CHECK_FOR_READ),
+                    fields=['url', 'title', 'meta'])
+            links.append(get_page_meta(other))
+        except PageNotFoundError:
+            links.append({'url': link, 'missing': True})
+
+    result = {'meta': get_page_meta(page), 'out_links': links}
+    return result
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/webimpl/special.py	Wed Sep 16 23:04:28 2015 -0700
@@ -0,0 +1,159 @@
+from wikked.utils import get_absolute_url
+from wikked.webimpl import (
+        CHECK_FOR_READ,
+        get_page_meta, get_page_or_raise,
+        is_page_readable, get_redirect_target,
+        get_or_build_pagelist, get_generic_pagelist_builder,
+        CircularRedirectError, RedirectNotFound)
+
+
+def build_pagelist_view_data(pages, user):
+    pages = sorted(pages, key=lambda p: p.url)
+    data = [get_page_meta(p) for p in pages if is_page_readable(p, user)]
+    result = {'pages': data}
+    return result
+
+
+def generic_pagelist_view(wiki, user, list_name, filter_func, fields=None):
+    fields = fields or ['url', 'title', 'meta']
+    pages = get_or_build_pagelist(
+            wiki,
+            list_name,
+            get_generic_pagelist_builder(wiki, filter_func, fields),
+            fields=fields)
+    return build_pagelist_view_data(pages, user)
+
+
+def get_orphans(wiki, user):
+    def builder_func():
+        wiki.resolve()
+
+        pages = {}
+        rev_links = {}
+        for p in wiki.getPages(
+                no_endpoint_only=True,
+                fields=['url', 'title', 'meta', 'links']):
+            pages[p.url] = p
+            rev_links[p.url] = 0
+
+            for l in p.links:
+                abs_l = get_absolute_url(p.url, l)
+                cnt = rev_links.get(abs_l, 0)
+                rev_links[abs_l] = cnt + 1
+
+        or_pages = []
+        for tgt, cnt in rev_links.items():
+            if cnt == 0:
+                or_pages.append(pages[tgt])
+        return or_pages
+
+    fields = ['url', 'title', 'meta', 'links']
+    pages = get_or_build_pagelist(wiki, 'orphans', builder_func, fields)
+    return build_pagelist_view_data(pages, user)
+
+
+def get_broken_redirects(wiki, user):
+    def filter_func(page):
+        redirect_meta = page.getMeta('redirect')
+        if redirect_meta is None:
+            return False
+
+        path = get_absolute_url(page.url, redirect_meta)
+        try:
+            target, visited = get_redirect_target(
+                    path,
+                    fields=['url', 'meta'])
+        except CircularRedirectError:
+            return True
+        except RedirectNotFound:
+            return True
+        return False
+
+    return generic_pagelist_view(wiki, user, 'broken_redirects', filter_func)
+
+
+def get_double_redirects(wiki, user):
+    def builder_func():
+        wiki.resolve()
+
+        pages = {}
+        redirs = {}
+        for p in wiki.getPages(
+                no_endpoint_only=True,
+                fields=['url', 'title', 'meta']):
+            pages[p.url] = p
+
+            target = p.getMeta('redirect')
+            if target:
+                target = get_absolute_url(p.url, target)
+                redirs[p.url] = target
+
+        dr_pages = []
+        for src, tgt in redirs.items():
+            if tgt in redirs:
+                dr_pages.append(pages[src])
+        return dr_pages
+
+    fields = ['url', 'title', 'meta']
+    pages = get_or_build_pagelist(wiki, 'double_redirects', builder_func,
+                                  fields)
+    return build_pagelist_view_data(pages, user)
+
+
+def get_dead_ends(wiki, user):
+    def filter_func(page):
+        return len(page.links) == 0
+
+    return generic_pagelist_view(
+            wiki, user, 'dead_ends', filter_func,
+            fields=['url', 'title', 'meta', 'links'])
+
+
+def list_pages(wiki, user, url=None):
+    pages = list(filter(is_page_readable, wiki.getPages(url)))
+    page_metas = [get_page_meta(page) for page in pages]
+    result = {'path': url, 'pages': list(page_metas)}
+    return result
+
+
+def get_search_results(wiki, user, query):
+    readable_hits = []
+    hits = list(wiki.index.search(query))
+    for h in hits:
+        try:
+            get_page_or_raise(wiki, h.url,
+                              check_perms=(user, CHECK_FOR_READ))
+        except PermissionError:
+            continue
+
+        readable_hits.append({
+                'url': h.url,
+                'title': h.title,
+                'text': h.hl_text})
+
+    result = {
+            'query': query,
+            'hit_count': len(readable_hits),
+            'hits': readable_hits}
+    return result
+
+
+def get_search_preview_results(wiki, user, query):
+    readable_hits = []
+    hits = list(wiki.index.previewSearch(query))
+    for h in hits:
+        try:
+            get_page_or_raise(wiki, h.url,
+                              check_perms=(user, CHECK_FOR_READ))
+        except PermissionError:
+            continue
+
+        readable_hits.append({'url': h.url, 'title': h.title})
+
+    result = {
+            'query': query,
+            'hit_count': len(readable_hits),
+            'hits': readable_hits}
+    return result
+
+