Mercurial > wikked
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 = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - var escapeChar = function(chr) { - return escape[chr] || "&"; - }; - - 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 ' '), - 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 ' '), - 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, """)); - } - - // 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:  - // 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, ">").replace(/</g, "<").replace(/"/g, """); - } - - 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, "&"); - - // Do the angle bracket song and dance: - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - - // 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, "&"); - - // Encode naked <'s - text = text.replace(/<(?![a-z\/?\$!])/gi, "<"); - - 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, "(").replace(/\)/g, ")").replace(/</g, "<").replace(/>/g, ">"); - } - 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">×</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}} – {%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">×</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 + +