Merge pull request #22 from scragg0x/firepad

Firepad
This commit is contained in:
Matthew Scragg 2014-10-03 13:50:59 -05:00
commit 50a25a2c22
24 changed files with 1013 additions and 762 deletions

View file

@ -88,7 +88,7 @@ This will ask you questions and create a config.json file in the app root direct
Of course you can manually edit this file as well. Of course you can manually edit this file as well.
Any config value set in config.json will override values set in ```realms/config/__init__.py``` Any config value set in config.json will override values set in ```realms/config/__init__.py```
## Nginx Setup ### Nginx Setup
sudo apt-get install -y nginx sudo apt-get install -y nginx
@ -130,6 +130,23 @@ Reload Nginx
sudo service nginx reload sudo service nginx reload
### Mysql Setup
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
realms-wiki pip install python-memcached
### MariaDB Setup
sudo apt-get install -y mariadb-server mariadb-client libmariadbclient-dev
realms-wiki pip install MySQL-Python
### Postgres
sudo apt-get install -y libpq-dev postgresql postgresql-contrib postgresql-client
realms-wiki pip install psycopg2
_Don't forget to create your database._
## Running ## Running
Current there are different ways. Current there are different ways.
@ -150,6 +167,21 @@ Access from your browser
http://localhost:5000 http://localhost:5000
## Templating
Realms uses handlebars partials to create templates.
Each page that you create can be imported as a partial.
This page imports and uses a partial:
http://realms.io/_edit/hbs
This page contains the content of the partial:
http://realms.io/_edit/example-tmpl
I locked these pages to preserve them.
You may copy and paste into a new page to test.
## Author ## Author

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.3.0

View file

@ -12,7 +12,8 @@
"parsleyjs": "~2.0.3", "parsleyjs": "~2.0.3",
"marked": "~0.3.2", "marked": "~0.3.2",
"js-yaml": "~3.2.1", "js-yaml": "~3.2.1",
"localforage": "~0.9.2", "store-js": "~1.3.16",
"bootswatch-dist": "3.2.0-flatly" "bootswatch-dist": "3.2.0-flatly",
"bootbox": "4.3.0"
} }
} }

View file

@ -23,7 +23,7 @@ sudo add-apt-repository -y ppa:chris-lea/node.js
sudo apt-get update sudo apt-get update
sudo apt-get install -y python build-essential git libpcre3-dev \ sudo apt-get install -y python build-essential git libpcre3-dev \
python-pip python-virtualenv python-dev pkg-config curl libxml2-dev libxslt1-dev zlib1g-dev \ python-pip python-virtualenv python-dev pkg-config curl libxml2-dev libxslt1-dev zlib1g-dev \
libffi-dev nodejs libyaml-dev libffi-dev nodejs libyaml-dev libssl-dev
# Default cache is memoization # Default cache is memoization

View file

@ -1,8 +1,10 @@
from gevent import wsgi from gevent import wsgi
from realms import config, app, cli, db from realms import config, app, cli, db
from realms.lib.util import random_string from realms.lib.util import random_string
from subprocess import call
import click import click
import json import json
import sys
@cli.command() @cli.command()
@ -77,6 +79,37 @@ def setup_redis(**kw):
conf[k.upper()] = v conf[k.upper()] = v
config.update(conf) config.update(conf)
install_redis()
def get_pip():
""" Get virtualenv path for pip
"""
return sys.prefix + '/bin/pip'
@cli.command()
@click.argument('cmd', nargs=-1)
def pip(cmd):
""" Execute pip commands for this virtualenv
"""
call(get_pip() + ' ' + ' '.join(cmd), shell=True)
def install_redis():
call([get_pip(), 'install', 'redis'])
def install_mysql():
call([get_pip(), 'install', 'MySQL-Python'])
def install_postgres():
call([get_pip(), 'install', 'psycopg2'])
def install_memcached():
call([get_pip(), 'install', 'python-memcached'])
@click.command() @click.command()
@ -139,5 +172,13 @@ def drop_db():
click.echo("Dropping all tables") click.echo("Dropping all tables")
db.drop_all() db.drop_all()
@cli.command()
def version():
""" Output version
"""
with open('VERSION') as f:
return f.read().strip()
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()

View file

@ -192,7 +192,8 @@ assets.register('main.js',
'js/html-sanitizer-minified.js', # don't minify? 'js/html-sanitizer-minified.js', # don't minify?
'vendor/highlightjs/highlight.pack.js', 'vendor/highlightjs/highlight.pack.js',
'vendor/parsleyjs/dist/parsley.js', 'vendor/parsleyjs/dist/parsley.js',
'js/main.js') 'js/hbs-helpers.js',
'js/mdr.js')
assets.register('main.css', assets.register('main.css',
'vendor/bootswatch-dist/css/bootstrap.css', 'vendor/bootswatch-dist/css/bootstrap.css',

View file

@ -38,7 +38,11 @@ PORT = 5000
BASE_URL = 'http://localhost' BASE_URL = 'http://localhost'
SITE_TITLE = "Realms" SITE_TITLE = "Realms"
# https://pythonhosted.org/Flask-SQLAlchemy/config.html#connection-uri-format
DB_URI = 'sqlite:///%s/wiki.db' % USER_HOME DB_URI = 'sqlite:///%s/wiki.db' % USER_HOME
# DB_URI = 'mysql://scott:tiger@localhost/mydatabase'
# DB_URI = 'postgresql://scott:tiger@localhost/mydatabase'
# DB_URI = 'oracle://scott:tiger@127.0.0.1:1521/sidname'
CACHE_TYPE = 'simple' CACHE_TYPE = 'simple'
@ -52,7 +56,6 @@ CACHE_REDIS_DB = '0'
#CACHE_TYPE = 'memcached' #CACHE_TYPE = 'memcached'
CACHE_MEMCACHED_SERVERS = ['127.0.0.1:11211'] CACHE_MEMCACHED_SERVERS = ['127.0.0.1:11211']
# Get ReCaptcha Keys for your domain here: # Get ReCaptcha Keys for your domain here:
# https://www.google.com/recaptcha/admin#whyrecaptcha # https://www.google.com/recaptcha/admin#whyrecaptcha
RECAPTCHA_ENABLE = False RECAPTCHA_ENABLE = False
@ -75,6 +78,12 @@ REGISTRATION_ENABLED = True
# Used by Flask-Login # Used by Flask-Login
LOGIN_DISABLED = ALLOW_ANON LOGIN_DISABLED = ALLOW_ANON
# None, firepad, or togetherjs
COLLABORATION = 'togetherjs'
# Required for firepad
FIREBASE_HOSTNAME = None
# Page names that can't be modified # Page names that can't be modified
WIKI_LOCKED_PAGES = [] WIKI_LOCKED_PAGES = []
# Depreciated variable name # Depreciated variable name

View file

@ -91,6 +91,7 @@ def to_canonical(s):
s = re.sub(r"\-\-+", "-", s) s = re.sub(r"\-\-+", "-", s)
s = re.sub(r"[^a-zA-Z0-9\-]", "", s) s = re.sub(r"[^a-zA-Z0-9\-]", "", s)
s = s[:64] s = s[:64]
s = s.lower()
return s return s

View file

@ -44,9 +44,9 @@ class AnonUser(AnonymousUserMixin):
class User(Model, UserMixin): class User(Model, UserMixin):
__tablename__ = 'users' __tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, unique=True) username = db.Column(db.String(128), unique=True)
email = db.Column(db.String, unique=True) email = db.Column(db.String(128), unique=True)
password = db.Column(db.String) password = db.Column(db.String(60))
admin = False admin = False
hidden_fields = ['password'] hidden_fields = ['password']

View file

@ -1,9 +1,10 @@
from realms import assets from realms import assets
assets.register('editor.js', assets.register('editor.js',
'vendor/localforage/dist/localforage.js', 'vendor/store-js/store.js',
'vendor/bootbox/bootbox.js',
'vendor/ace-builds/src/ace.js', 'vendor/ace-builds/src/ace.js',
'vendor/ace-builds/src/mode-markdown.js', 'vendor/ace-builds/src/mode-markdown.js',
'vendor/ace-builds/src/ext-keybinding_menu.js', 'vendor/ace-builds/src/ext-keybinding_menu.js',
'vendor/keymaster/keymaster.js', 'vendor/keymaster/keymaster.js',
'js/editor.js') 'js/aced.js')

View file

@ -139,11 +139,11 @@ class Wiki():
return cached return cached
# commit = gittle.utils.git.commit_info(self.repo[sha]) # commit = gittle.utils.git.commit_info(self.repo[sha])
name = self.cname_to_filename(name).encode('latin-1') filename = self.cname_to_filename(name).encode('latin-1')
sha = sha.encode('latin-1') sha = sha.encode('latin-1')
try: try:
data = self.gittle.get_commit_files(sha, paths=[name]).get(name) data = self.gittle.get_commit_files(sha, paths=[filename]).get(filename)
if not data: if not data:
return None return None
partials = {} partials = {}
@ -153,6 +153,7 @@ class Wiki():
for partial_name in meta['import']: for partial_name in meta['import']:
partials[partial_name] = self.get_page(partial_name) partials[partial_name] = self.get_page(partial_name)
data['partials'] = partials data['partials'] = partials
data['info'] = self.get_history(name, limit=1)[0]
return data return data
except KeyError: except KeyError:
@ -175,10 +176,10 @@ class Wiki():
new = self.get_page(name, sha=new_sha) new = self.get_page(name, sha=new_sha)
return ghdiff.diff(old['data'], new['data']) return ghdiff.diff(old['data'], new['data'])
def get_history(self, name): def get_history(self, name, limit=100):
file_path = self.cname_to_filename(name) file_path = self.cname_to_filename(name)
versions = [] versions = []
walker = self.repo.get_walker(paths=[file_path], max_entries=100) walker = self.repo.get_walker(paths=[file_path], max_entries=limit)
for entry in walker: for entry in walker:
change_type = None change_type = None
for change in entry.changes(): for change in entry.changes():

View file

@ -36,7 +36,7 @@ def revert():
commit = request.form.get('commit') commit = request.form.get('commit')
cname = to_canonical(name) cname = to_canonical(name)
if cname.lower() in app.config.WIKI_LOCKED_PAGES: if cname in app.config.WIKI_LOCKED_PAGES:
flash("Page is locked") flash("Page is locked")
return redirect(url_for(app.config['ROOT_ENDPOINT'])) return redirect(url_for(app.config['ROOT_ENDPOINT']))
@ -59,10 +59,10 @@ def edit(name):
if request.method == 'POST': if request.method == 'POST':
edit_cname = to_canonical(request.form['name']) edit_cname = to_canonical(request.form['name'])
if edit_cname.lower() in app.config['WIKI_LOCKED_PAGES']: if edit_cname in app.config['WIKI_LOCKED_PAGES']:
return redirect(url_for(app.config['ROOT_ENDPOINT'])) return redirect(url_for(app.config['ROOT_ENDPOINT']))
if edit_cname.lower() != cname.lower(): if edit_cname != cname.lower():
g.current_wiki.rename_page(cname, edit_cname) g.current_wiki.rename_page(cname, edit_cname)
g.current_wiki.write_page(edit_cname, g.current_wiki.write_page(edit_cname,
@ -74,7 +74,12 @@ def edit(name):
name = remove_ext(data['name']) name = remove_ext(data['name'])
content = data.get('data') content = data.get('data')
g.assets['js'].append('editor.js') g.assets['js'].append('editor.js')
return render_template('wiki/edit.html', name=name, content=content, sha=data.get('sha'), partials=data.get('partials')) return render_template('wiki/edit.html',
name=name,
content=content,
info=data.get('info'),
sha=data.get('sha'),
partials=data.get('partials'))
else: else:
return redirect(url_for('wiki.create', name=cname)) return redirect(url_for('wiki.create', name=cname))
@ -110,7 +115,10 @@ def create(name):
return redirect(url_for('wiki.edit', name=cname)) return redirect(url_for('wiki.edit', name=cname))
g.assets['js'].append('editor.js') g.assets['js'].append('editor.js')
return render_template('wiki/edit.html', name=cname, content="") return render_template('wiki/edit.html',
name=cname,
content="",
info={})
@blueprint.route("/", defaults={'name': 'home'}) @blueprint.route("/", defaults={'name': 'home'})

View file

@ -58,9 +58,9 @@
#app-wrap { #app-wrap {
top: 60px; top: 60px;
left: -5px; left: 0;
bottom: 0; bottom: 0;
right: -5px; right: 0;
position: fixed; position: fixed;
} }
@ -188,7 +188,6 @@ a.label {
left: 0; left: 0;
padding: 40px 10px 10px 10px; padding: 40px 10px 10px 10px;
overflow: auto; overflow: auto;
//word-break: break-word;
cursor: default; cursor: default;
} }
@ -217,7 +216,7 @@ a.label {
background-color: #eee; background-color: #eee;
} }
#editor { .editor {
margin-top: 40px; margin-top: 40px;
} }
@ -226,7 +225,7 @@ a.label {
padding: 3px; padding: 3px;
} }
#editor { .editor {
margin-top: 0; margin-top: 0;
} }

430
realms/static/js/aced.js Normal file
View file

@ -0,0 +1,430 @@
function Aced(settings) {
var id,
options,
editor,
element,
preview,
previewWrapper,
profile,
autoInterval,
themes,
themeSelect,
loadedThemes = {};
settings = settings || {};
options = {
sanitize: true,
preview: null,
editor: null,
theme: 'idle_fingers',
themePath: '/static/vendor/ace-builds/src',
mode: 'markdown',
autoSave: true,
autoSaveInterval: 5000,
syncPreview: false,
keyMaster: false,
submit: function(data){ alert(data); },
showButtonBar: false,
themeSelect: null,
submitBtn: null,
renderer: null,
info: null
};
themes = {
chrome: "Chrome",
clouds: "Clouds",
clouds_midnight: "Clouds Midnight",
cobalt: "Cobalt",
crimson_editor: "Crimson Editor",
dawn: "Dawn",
dreamweaver: "Dreamweaver",
eclipse: "Eclipse",
idle_fingers: "idleFingers",
kr_theme: "krTheme",
merbivore: "Merbivore",
merbivore_soft: "Merbivore Soft",
mono_industrial: "Mono Industrial",
monokai: "Monokai",
pastel_on_dark: "Pastel on Dark",
solarized_dark: "Solarized Dark",
solarized_light: "Solarized Light",
textmate: "TextMate",
tomorrow: "Tomorrow",
tomorrow_night: "Tomorrow Night",
tomorrow_night_blue: "Tomorrow Night Blue",
tomorrow_night_bright: "Tomorrow Night Bright",
tomorrow_night_eighties: "Tomorrow Night 80s",
twilight: "Twilight",
vibrant_ink: "Vibrant Ink"
};
function editorId() {
return "aced." + id;
}
function infoKey() {
return editorId() + ".info";
}
function gc() {
// Clean up localstorage
store.forEach(function(key, val) {
var re = new RegExp("aced\.(.*?)\.info");
var info = re.exec(key);
if (!info || !val.time) {
return;
}
var id = info[1];
// Remove week+ old stuff
var now = new Date().getTime() / 1000;
if (now > (val.time + 604800)) {
store.remove(key);
store.remove('aced.' + id);
}
});
}
function buildThemeSelect() {
var $sel = $("<select class='aced-theme-sel' data-placeholder='Theme'></select>");
$sel.append('<option></option>');
$.each(themes, function(k, v) {
$sel.append("<option value='" + k + "'>" + v + "</option>");
});
return $("<div/>").html($sel);
}
function toJquery(o) {
return (typeof o == 'string') ? $("#" + o) : $(o);
}
function initProfile() {
profile = {theme: ''};
try {
// Need to merge in any undefined/new properties from last release
// Meaning, if we add new features they may not have them in profile
profile = $.extend(true, profile, store.get('aced.profile'));
} catch (e) { }
}
function updateProfile(obj) {
profile = $.extend(null, profile, obj);
store.set('profile', profile);
}
function render(content) {
return (options.renderer) ? options.renderer(content) : content;
}
function bindKeyboard() {
// CMD+s TO SAVE DOC
key('command+s, ctrl+s', function (e) {
submit();
e.preventDefault();
});
var saveCommand = {
name: "save",
bindKey: {
mac: "Command-S",
win: "Ctrl-S"
},
exec: function () {
submit();
}
};
editor.commands.addCommand(saveCommand);
}
function info(info) {
if (info) {
store.set(infoKey(), info);
}
return store.get(infoKey());
}
function val(val) {
// Alias func
if (val) {
editor.getSession().setValue(val);
}
return editor.getSession().getValue();
}
function discardDraft() {
stopAutoSave();
store.remove(editorId());
store.remove(infoKey());
location.reload();
}
function save() {
store.set(editorId(), val());
}
function submit() {
store.remove(editorId());
store.remove(editorId() + ".info");
options.submit(val());
}
function autoSave() {
if (options.autoSave) {
autoInterval = setInterval(function () {
save();
}, options.autoSaveInterval);
} else {
stopAutoSave();
}
}
function stopAutoSave() {
if (autoInterval){
clearInterval(autoInterval)
}
}
function renderPreview() {
if (!preview) {
return;
}
preview.html(render(val()));
$('pre code', preview).each(function(i, e) {
hljs.highlightBlock(e)
});
}
function getScrollHeight($prevFrame) {
// Different browsers attach the scrollHeight of a document to different
// elements, so handle that here.
if ($prevFrame[0].scrollHeight !== undefined) {
return $prevFrame[0].scrollHeight;
} else if ($prevFrame.find('html')[0].scrollHeight !== undefined &&
$prevFrame.find('html')[0].scrollHeight !== 0) {
return $prevFrame.find('html')[0].scrollHeight;
} else {
return $prevFrame.find('body')[0].scrollHeight;
}
}
function getPreviewWrapper(obj) {
// Attempts to get the wrapper for preview based on overflow prop
if (!obj) {
return;
}
if (obj.css('overflow') == 'auto' || obj.css('overflow') == 'scroll') {
return obj;
} else {
return getPreviewWrapper(obj.parent());
}
}
function syncPreview() {
var editorScrollRange = (editor.getSession().getLength());
var previewScrollRange = (getScrollHeight(preview));
// Find how far along the editor is (0 means it is scrolled to the top, 1
// means it is at the bottom).
var scrollFactor = editor.getFirstVisibleRow() / editorScrollRange;
// Set the scroll position of the preview pane to match. jQuery will
// gracefully handle out-of-bounds values.
previewWrapper.scrollTop(scrollFactor * previewScrollRange);
}
function asyncLoad(filename, cb) {
(function (d, t) {
var leScript = d.createElement(t)
, scripts = d.getElementsByTagName(t)[0];
leScript.async = 1;
leScript.src = filename;
scripts.parentNode.insertBefore(leScript, scripts);
leScript.onload = function () {
cb && cb();
}
}(document, 'script'));
}
function setTheme(theme) {
var cb = function(theme) {
editor.setTheme('ace/theme/'+theme);
updateProfile({theme: theme});
};
if (loadedThemes[theme]) {
cb(theme);
} else {
asyncLoad(options.themePath + "/theme-" + theme + ".js", function () {
cb(theme);
loadedThemes[theme] = true;
});
}
}
function initSyncPreview() {
if (!preview || !options.syncPreview) return;
previewWrapper = getPreviewWrapper(preview);
window.onload = function () {
/**
* Bind synchronization of preview div to editor scroll and change
* of editor cursor position.
*/
editor.session.on('changeScrollTop', syncPreview);
editor.session.selection.on('changeCursor', syncPreview);
};
}
function initProps() {
// Id of editor
if (typeof settings == 'string') {
settings = { editor: settings };
}
if ('theme' in profile && profile['theme']) {
settings['theme'] = profile['theme'];
}
if (settings['preview'] && !settings.hasOwnProperty('syncPreview')) {
settings['syncPreview'] = true;
}
$.extend(options, settings);
if (options.editor) {
element = toJquery(options.editor);
}
$.each(options, function(k, v){
if (element.data(k.toLowerCase())) {
options[k] = element.data(k.toLowerCase());
}
});
if (options.themeSelect) {
themeSelect = toJquery(options.themeSelect);
}
if (options.submitBtn) {
var submitBtn = toJquery(options.submitBtn);
submitBtn.click(function(){
submit();
});
}
if (options.preview) {
preview = toJquery(options.preview);
// Enable sync unless set otherwise
if (!settings.hasOwnProperty('syncPreview')) {
options['syncPreview'] = true;
}
}
if (!element.attr('id')) {
// No id, make one!
id = Math.random().toString(36).substring(7);
element.attr('id', id);
} else {
id = element.attr('id')
}
}
function initEditor() {
editor = ace.edit(id);
setTheme(profile.theme || options.theme);
editor.getSession().setMode('ace/mode/' + options.mode);
if (store.get(editorId()) && store.get(editorId()) != val()) {
editor.getSession().setValue(store.get(editorId()));
}
editor.getSession().setUseWrapMode(true);
editor.getSession().setTabSize(2);
editor.getSession().setUseSoftTabs(true);
editor.setShowPrintMargin(false);
editor.renderer.setShowInvisibles(true);
editor.renderer.setShowGutter(false);
if (options.showButtonBar) {
var $btnBar = $('<div class="aced-button-bar aced-button-bar-top">' + buildThemeSelect().html() + ' <button type="button" class="btn btn-primary btn-xs aced-save">Save</button></div>')
element.find('.ace_content').before($btnBar);
$(".aced-save", $btnBar).click(function(){
submit();
});
if ($.fn.chosen) {
$('select', $btnBar).chosen().change(function(){
setTheme($(this).val());
});
}
}
if (options.keyMaster) {
bindKeyboard();
}
if (preview) {
editor.getSession().on('change', function (e) {
renderPreview();
});
renderPreview();
}
if (themeSelect) {
themeSelect
.find('li > a')
.bind('click', function (e) {
setTheme($(e.target).data('value'));
$(e.target).blur();
return false;
});
}
if (options.info) {
// If no info exists, save it to storage
if (!store.get(infoKey())) {
store.set(infoKey(), options.info);
} else {
// Check info in storage against one passed in
// for possible changes in data that may have occurred
var info = store.get(infoKey());
if (info['sha'] != options.info['sha'] && !info['ignore']) {
// Data has changed since start of draft
$(document).trigger('shaMismatch');
}
}
}
$(this).trigger('ready');
}
function init() {
gc();
initProfile();
initProps();
initEditor();
initSyncPreview();
autoSave();
}
init();
return {
editor: editor,
submit: submit,
val: val,
discard: discardDraft,
info: info
};
}

View file

@ -0,0 +1,52 @@
// Helper to get hash from end of URL or generate a random one.
function getExampleRef() {
var ref = new Firebase('https://' + Config['FIREBASE_HOSTNAME']);
var hash = window.location.hash.replace(/^#fp-/, '');
if (hash) {
ref = ref.child(hash);
} else {
ref = ref.push(); // generate unique location.
window.location = window.location + '#fp-' + ref.name(); // add it as a hash to the URL.
}
return ref;
}
function initFirepad() {
var new_ = true;
if (window.location.hash.lastIndexOf('#fp-', 0) === 0) {
new_ = false;
}
var firepadRef = getExampleRef();
var session = aced.editor.session;
var content;
if (new_) {
content = session.getValue();
}
// Firepad wants an empty editor
session.setValue('');
//// Create Firepad.
var firepad = Firepad.fromACE(firepadRef, aced.editor, {
defaultText: content
});
firepad.on('ready', function() {
startCollaboration();
});
$(document).on('end-collaboration', function() {
firepad.dispose();
});
}
$(document).on('loading-collaboration', function() {
initFirepad(true);
});
$(function(){
if (window.location.hash.lastIndexOf('#fp-', 0) === 0) {
loadingCollaboration();
}
});

View file

@ -0,0 +1,36 @@
var $startCollaborationBtn = $('#start-collaboration');
var $endCollaborationBtn = $('#end-collaboration');
var $loadingCollaborationBtn = $('#loading-collaboration');
function loadingCollaboration() {
$endCollaborationBtn.hide();
$startCollaborationBtn.hide();
$loadingCollaborationBtn.show();
$(document).trigger('loading-collaboration');
}
function startCollaboration() {
$loadingCollaborationBtn.hide();
$startCollaborationBtn.hide();
$endCollaborationBtn.show();
$(document).trigger('start-collaboration');
}
function endCollaboration() {
$loadingCollaborationBtn.hide();
$endCollaborationBtn.hide();
$startCollaborationBtn.show();
$(document).trigger('end-collaboration');
}
$(function() {
$startCollaborationBtn.click(function(e) {
loadingCollaboration();
e.preventDefault();
});
$endCollaborationBtn.click(function(e) {
endCollaboration();
e.preventDefault();
});
});

View file

@ -0,0 +1,28 @@
$(document).on('loading-collaboration', function() {
TogetherJS();
});
$(document).on('end-collaboration', function() {
TogetherJS();
});
TogetherJSConfig_toolName = "Collaboration";
TogetherJSConfig_suppressJoinConfirmation = true;
if (User.is_authenticated) {
TogetherJSConfig_getUserName = function () {
return User.username;
};
TogetherJSConfig_getUserAvatar = function () {
return User.avatar;
};
}
TogetherJSConfig_on_ready = function () {
startCollaboration();
};
TogetherJSConfig_on_close = function () {
//endCollaboration();
};

View file

@ -1,529 +1,77 @@
/* var $entry_markdown_header = $("#entry-markdown-header");
Source is modified version of http://dillinger.io/ var $entry_preview_header = $("#entry-preview-header");
*/ var $entry_markdown = $(".entry-markdown");
$(function () { var $entry_preview = $(".entry-preview");
var url_prefix = ""; // Tabs
var sha = $("#sha").text(); $entry_markdown_header.click(function(){
var $theme = $('#theme-list'); $entry_markdown.addClass('active');
var $preview = $('#preview'); $entry_preview.removeClass('active');
var $autosave = $('#autosave');
var $wordcount = $('#wordcount');
var $wordcounter = $('#wordcounter');
var $pagename = $("#page-name");
var $entry_markdown_header = $("#entry-markdown-header");
var $entry_preview_header = $("#entry-preview-header");
// Tabs
$entry_markdown_header.click(function(){
$("section.entry-markdown").addClass('active');
$("section.entry-preview").removeClass('active');
});
$entry_preview_header.click(function(){
$("section.entry-preview").addClass('active');
$("section.entry-markdown").removeClass('active');
});
var editor;
var autoInterval;
var profile = {
theme: 'ace/theme/idle_fingers',
currentMd: '',
autosave: {
enabled: true,
interval: 3000 // might be too aggressive; don't want to block UI for large saves.
},
current_filename: $pagename.val()
};
// Feature detect ish
var dillinger = 'dillinger';
var dillingerElem = document.createElement(dillinger);
var dillingerStyle = dillingerElem.style;
var domPrefixes = 'Webkit Moz O ms Khtml'.split(' ');
/// UTILS =================
/**
* Utility method to async load a JavaScript file.
*
* @param {String} The name of the file to load
* @param {Function} Optional callback to be executed after the script loads.
* @return {void}
*/
function asyncLoad(filename, cb) {
(function (d, t) {
var leScript = d.createElement(t)
, scripts = d.getElementsByTagName(t)[0];
leScript.async = 1;
leScript.src = filename;
scripts.parentNode.insertBefore(leScript, scripts);
leScript.onload = function () {
cb && cb();
}
}(document, 'script'));
}
/**
* Grab the user's profile from localStorage and stash in "profile" variable.
*
* @return {Void}
*/
function getUserProfile() {
localforage.getItem('profile', function(p) {
profile = $.extend(true, profile, p);
if (profile.filename != $pagename.val()) {
setEditorValue("");
updateUserProfile({ filename: $pagename.val(), currentMd: "" });
} else {
if (profile.currentMd) {
setEditorValue(profile.currentMd);
}
}
});
}
/**
* Update user's profile in localStorage by merging in current profile with passed in param.
*
* @param {Object} An object containg proper keys and values to be JSON.stringify'd
* @return {Void}
*/
function updateUserProfile(obj) {
localforage.clear();
localforage.setItem('profile', $.extend(true, profile, obj));
}
/**
* Utility method to test if particular property is supported by the browser or not.
* Completely ripped from Modernizr with some mods.
* Thx, Modernizr team!
*
* @param {String} The property to test
* @return {Boolean}
*/
function prefixed(prop) {
return testPropsAll(prop, 'pfx')
}
/**
* A generic CSS / DOM property test; if a browser supports
* a certain property, it won't return undefined for it.
* A supported CSS property returns empty string when its not yet set.
*
* @param {Object} A hash of properties to test
* @param {String} A prefix
* @return {Boolean}
*/
function testProps(props, prefixed) {
for (var i in props) {
if (dillingerStyle[ props[i] ] !== undefined) {
return prefixed === 'pfx' ? props[i] : true;
}
}
return false
}
/**
* Tests a list of DOM properties we want to check against.
* We specify literally ALL possible (known and/or likely) properties on
* the element including the non-vendor prefixed one, for forward-
* compatibility.
*
* @param {String} The name of the property
* @param {String} The prefix string
* @return {Boolean}
*/
function testPropsAll(prop, prefixed) {
var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1)
, props = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' ');
return testProps(props, prefixed);
}
/**
* Normalize the transitionEnd event across browsers.
*
* @return {String}
*/
function normalizeTransitionEnd() {
var transEndEventNames =
{
'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'msTransitionEnd' // maybe?
, 'transition': 'transitionend'
};
return transEndEventNames[ prefixed('transition') ];
}
/**
* Returns the full text of an element and all its children.
* The script recursively traverses all text nodes, and returns a
* concatenated string of all texts.
*
* Taken from
* http://stackoverflow.com/questions/2653670/innertext-textcontent-vs-retrieving-each-text-node
*
* @param node
* @return {int}
*/
function getTextInElement(node) {
if (node.nodeType === 3) {
return node.data;
}
var txt = '';
if (node = node.firstChild) do {
txt += getTextInElement(node);
} while (node = node.nextSibling);
return txt;
}
/**
* Counts the words in a string
*
* @param string
* @return int
*/
function countWords(string) {
var words = string.replace(/W+/g, ' ').match(/\S+/g);
return words && words.length || 0;
}
/**
* Initialize application.
*
* @return {Void}
*/
function init() {
// Attach to jQuery support object for later use.
$.support.transitionEnd = normalizeTransitionEnd();
initAce();
getUserProfile();
initUi();
bindPreview();
bindNav();
bindKeyboard();
autoSave();
}
function initAce() {
editor = ace.edit("editor");
editor.focus();
editor.setOptions({
enableBasicAutocompletion: true
});
}
function initUi() {
// Set proper theme value in theme dropdown
fetchTheme(profile.theme, function () {
$theme.find('li > a[data-value="' + profile.theme + '"]').addClass('selected');
editor.setBehavioursEnabled(true);
editor.getSession().setUseWrapMode(true);
editor.setShowPrintMargin(false);
editor.getSession().setTabSize(2);
editor.getSession().setUseSoftTabs(true);
editor.renderer.setShowInvisibles(true);
editor.renderer.setShowGutter(false);
editor.getSession().setMode('ace/mode/markdown');
setEditorValue(profile.currentMd || editor.getSession().getValue());
previewMd();
});
// Set text for dis/enable autosave / word counter
$autosave.html(profile.autosave.enabled ? '<i class="icon-remove"></i>&nbsp;Disable Autosave' : '<i class="icon-ok"></i>&nbsp;Enable Autosave');
$wordcount.html(!profile.wordcount ? '<i class="icon-remove"></i>&nbsp;Disabled Word Count' : '<i class="icon-ok"></i>&nbsp;Enabled Word Count');
$('.dropdown-toggle').dropdown();
}
function clearSelection() {
setEditorValue("");
previewMd();
}
function saveFile(isManual) {
updateUserProfile({currentMd: editor.getSession().getValue()});
if (isManual) {
updateUserProfile({ currentMd: "" });
var data = {
name: $pagename.val(),
message: $("#page-message").val(),
content: editor.getSession().getValue()
};
$.post(window.location, data, function() {
location.href = url_prefix + '/' + data['name'];
});
}
}
function autoSave() {
if (profile.autosave.enabled) {
autoInterval = setInterval(function() {
saveFile();
}, profile.autosave.interval);
} else {
clearInterval(autoInterval)
}
}
$("#save-native").on('click', function() {
saveFile(true);
});
function resetProfile() {
// For some reason, clear() is not working in Chrome.
localforage.clear();
// Let's turn off autosave
profile.autosave.enabled = false;
localforage.removeItem('profile', function() {
window.location.reload();
});
}
function changeTheme(e) {
// check for same theme
var $target = $(e.target);
if ($target.attr('data-value') === profile.theme) {
return;
}
else {
// add/remove class
$theme.find('li > a.selected').removeClass('selected');
$target.addClass('selected');
// grabnew theme
var newTheme = $target.attr('data-value');
$(e.target).blur();
fetchTheme(newTheme, function () {
});
}
}
function fetchTheme(th, cb) {
var name = th.split('/').pop();
asyncLoad("/static/vendor/ace-builds/src/theme-" + name + ".js", function () {
editor.setTheme(th);
cb && cb();
updateBg(name);
updateUserProfile({theme: th});
});
}
function updateBg(name) {
// document.body.style.backgroundColor = bgColors[name]
}
function setEditorValue(str) {
editor.getSession().setValue(str);
}
function previewMd() {
$preview.html(MDR.convert(editor.getSession().getValue(), true));
}
function updateFilename(str) {
// Check for string because it may be keyup event object
var f;
if (typeof str === 'string') {
f = str;
} else {
f = getCurrentFilenameFromField();
}
updateUserProfile({ current_filename: f });
}
function showHtml() {
// TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME
var unmd = editor.getSession().getValue();
function _doneHandler(jqXHR, data, response) {
// console.dir(resp)
var resp = JSON.parse(response.responseText);
$('#myModalBody').text(resp.data);
$('#myModal').modal();
}
function _failHandler() {
alert("Roh-roh. Something went wrong. :(");
}
var config = {
type: 'POST',
data: "unmd=" + encodeURIComponent(unmd),
dataType: 'json',
url: '/factory/fetch_html_direct',
error: _failHandler,
success: _doneHandler
};
$.ajax(config)
}
function toggleAutoSave() {
$autosave.html(profile.autosave.enabled ? '<i class="icon-remove"></i>&nbsp;Disable Autosave' : '<i class="icon-ok"></i>&nbsp;Enable Autosave');
updateUserProfile({autosave: {enabled: !profile.autosave.enabled }});
autoSave();
}
function bindPreview() {
editor.getSession().on('change', function (e) {
previewMd();
});
}
function bindNav() {
$theme
.find('li > a')
.bind('click', function (e) {
changeTheme(e);
return false;
});
$('#clear')
.on('click', function () {
clearSelection();
return false;
});
$("#autosave")
.on('click', function () {
toggleAutoSave();
return false;
});
$('#reset')
.on('click', function () {
resetProfile();
return false;
});
$('#cheat').
on('click', function () {
window.open("https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet", "_blank");
return false;
});
} // end bindNav()
function bindKeyboard() {
// CMD+s TO SAVE DOC
key('command+s, ctrl+s', function (e) {
saveFile(true);
e.preventDefault(); // so we don't save the web page - native browser functionality
});
var saveCommand = {
name: "save",
bindKey: {
mac: "Command-S",
win: "Ctrl-S"
},
exec: function () {
saveFile(true);
}
};
editor.commands.addCommand(saveCommand);
}
init();
}); });
$entry_preview_header.click(function(){
$entry_preview.addClass('active');
$entry_markdown.removeClass('active');
});
function getScrollHeight($prevFrame) { $(document).on('shaMismatch', function() {
// Different browsers attach the scrollHeight of a document to different bootbox.dialog({
// elements, so handle that here. title: "Page has changed",
if ($prevFrame[0].scrollHeight !== undefined) { message: "This page has changed and differs from your draft. What do you want to do?",
return $prevFrame[0].scrollHeight; buttons: {
} else if ($prevFrame.find('html')[0].scrollHeight !== undefined && ignore: {
$prevFrame.find('html')[0].scrollHeight !== 0) { label: "Ignore",
return $prevFrame.find('html')[0].scrollHeight; className: "btn-default",
} else { callback: function() {
return $prevFrame.find('body')[0].scrollHeight; var info = aced.info();
info['ignore'] = true;
aced.info(info);
}
},
discard: {
label: "Discard Draft",
className: "btn-danger",
callback: function() {
aced.discard();
}
},
changes: {
label: "Show Diff",
className: "btn-primary",
callback: function() {
bootbox.alert("Draft diff not done! Sorry");
}
}
} }
}
function syncPreview() {
var $ed = window.ace.edit('editor');
var $prev = $('#preview');
var editorScrollRange = ($ed.getSession().getLength());
var previewScrollRange = (getScrollHeight($prev));
// Find how far along the editor is (0 means it is scrolled to the top, 1
// means it is at the bottom).
var scrollFactor = $ed.getFirstVisibleRow() / editorScrollRange;
// Set the scroll position of the preview pane to match. jQuery will
// gracefully handle out-of-bounds values.
$prev.parent().scrollTop(scrollFactor * previewScrollRange);
}
window.onload = function () {
var $loading = $('#loading');
if ($.support.transition) {
$loading
.bind($.support.transitionEnd, function () {
$('#main').removeClass('bye');
$loading.remove();
}) })
.addClass('fade_slow'); });
} else {
$('#main').removeClass('bye');
$loading.remove();
}
/** $(function(){
* Bind synchronization of preview div to editor scroll and change $("#discard-draft-btn").click(function() {
* of editor cursor position. aced.discard();
*/ });
window.ace.edit('editor').session.on('changeScrollTop', syncPreview);
window.ace.edit('editor').session.selection.on('changeCursor', syncPreview); $(".entry-markdown .floatingheader").click(function(){
}; aced.editor.focus();
});
$("#delete-draft-btn").click(function() {
bootbox.alert("Not Done Yet! Sorry");
});
});
var aced = new Aced({
editor: $('#entry-markdown-content').find('.editor').attr('id'),
renderer: function(md) { return MDR.convert(md) },
info: Commit.info,
submit: function(content) {
var data = {
name: $("#page-name").val(),
message: $("#page-message").val(),
content: content
};
$.post(window.location, data, function() {
location.href = Config['RELATIVE_PATH'] + '/' + data['name'];
});
}
});

View file

@ -0,0 +1,12 @@
// Handlebar helpers
Handlebars.registerHelper('well', function(options) {
return '<div class="well">' + options.fn(this) + '</div>';
});
Handlebars.registerHelper('well-sm', function(options) {
return '<div class="well well-sm">' + options.fn(this) + '</div>';
});
Handlebars.registerHelper('well-lg', function(options) {
return '<div class="well well-lg">' + options.fn(this) + '</div>';
});

View file

@ -1,111 +0,0 @@
// Handlebar helpers
Handlebars.registerHelper('well', function(options) {
return '<div class="well">' + options.fn(this) + '</div>';
});
/* © 2013 j201
* https://github.com/j201/meta-marked */
// Splits the given string into a meta section and a markdown section if a meta section is present, else returns null
function splitInput(str) {
if (str.slice(0, 3) !== '---') return;
var matcher = /\n(\.{3}|\-{3})/g;
var metaEnd = matcher.exec(str);
return metaEnd && [str.slice(0, metaEnd.index), str.slice(matcher.lastIndex)];
}
var metaMarked = function(src, opt, callback) {
if (Object.prototype.toString.call(src) !== '[object String]')
throw new TypeError('First parameter must be a string.');
var mySplitInput = splitInput(src);
if (mySplitInput) {
var meta;
try {
meta = jsyaml.safeLoad(mySplitInput[0]);
} catch(e) {
meta = null;
}
return {
meta: meta,
md: mySplitInput[1]
};
} else {
return {
meta: null,
md: src
}
}
};
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
});
// Init highlight JS
hljs.initHighlightingOnLoad();
// Markdown Renderer
var MDR = {
meta: null,
md: null,
sanitize: false, // Override
parse: function(md){ return marked(md); },
convert: function(md, sanitize){
if (this.sanitize !== null) {
sanitize = this.sanitize;
}
this.md = md;
this.processMeta();
try {
var html = this.parse(this.md);
} catch(e) {
return this.md;
}
if (sanitize) {
// Causes some problems with inline styles
html = html_sanitize(html, function(url) {
try {
var prot = decodeURIComponent(unescape(url))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return '';
}
if (prot.indexOf('javascript:') === 0) {
return '';
}
return prot;
}, function(id){
return id;
});
}
this.hook();
return html;
},
processMeta: function() {
var doc = metaMarked(this.md);
this.md = doc.md;
this.meta = doc.meta;
if (this.meta) {
try {
var template = Handlebars.compile(this.md);
this.md = template(this.meta);
} catch(e) {}
}
},
hook: function() {
}
};

106
realms/static/js/mdr.js Normal file
View file

@ -0,0 +1,106 @@
// Init highlight JS
hljs.initHighlightingOnLoad();
function splitInput(str) {
if (str.slice(0, 3) !== '---') return;
var matcher = /\n(\.{3}|\-{3})/g;
var metaEnd = matcher.exec(str);
return metaEnd && [str.slice(0, metaEnd.index), str.slice(matcher.lastIndex)];
}
/* © 2013 j201
* https://github.com/j201/meta-marked */
// Splits the given string into a meta section and a markdown section if a meta section is present, else returns null
var metaMarked = function(src, opt, callback) {
if (Object.prototype.toString.call(src) !== '[object String]')
throw new TypeError('First parameter must be a string.');
var mySplitInput = splitInput(src);
if (mySplitInput) {
var meta;
try {
meta = jsyaml.safeLoad(mySplitInput[0]);
} catch(e) {
meta = null;
}
return {
meta: meta,
md: mySplitInput[1]
};
} else {
return {
meta: null,
md: src
}
}
};
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
});
// Markdown Renderer
var MDR = {
meta: null,
md: null,
sanitize: true, // Override
parse: function(md){ return marked(md); },
convert: function(md, sanitize) {
if (this.sanitize !== null) {
sanitize = this.sanitize;
}
this.md = md;
this.processMeta();
try {
var html = this.parse(this.md);
} catch(e) {
return this.md;
}
if (sanitize) {
// Causes some problems with inline styles
html = html_sanitize(html, function(url) {
try {
var prot = decodeURIComponent(url.toString());
} catch (e) {
return '';
}
if (prot.indexOf('javascript:') === 0) {
return '';
}
return prot;
}, function(id){
return id;
});
}
this.hook();
return html;
},
processMeta: function() {
var doc = metaMarked(this.md);
this.md = doc.md;
this.meta = doc.meta;
if (this.meta) {
try {
var template = Handlebars.compile(this.md);
this.md = template(this.meta);
} catch(e) {
console.log(e);
}
}
},
hook: function() {
}
};

View file

@ -7,6 +7,7 @@
<meta name="author" content=""> <meta name="author" content="">
<title>{{ config.SITE_TITLE }}</title> <title>{{ config.SITE_TITLE }}</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
{% for bundle in g.assets['css'] %} {% for bundle in g.assets['css'] %}
@ -14,6 +15,7 @@
<link href="{{ ASSET_URL }}" rel="stylesheet"> <link href="{{ ASSET_URL }}" rel="stylesheet">
{% endassets %} {% endassets %}
{% endfor %} {% endfor %}
{% block css %}{% endblock %} {% block css %}{% endblock %}
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
@ -88,6 +90,20 @@
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
</div> </div>
<script>
var Config = {};
{% for attr in ['RELATIVE_PATH'] %}
Config.{{ attr }} = {{ config[attr]|tojson }};
{% endfor %}
var User = {};
User.is_authenticated = {{ current_user.is_authenticated()|tojson }};
{% for attr in ['username', 'email'] %}
User.{{ attr }} = {{ current_user[attr]|tojson }};
{% endfor %}
</script>
{% for bundle in g.assets['js'] %} {% for bundle in g.assets['js'] %}
{% assets bundle %} {% assets bundle %}
{% if bundle == 'editor.js' %} {% if bundle == 'editor.js' %}
@ -97,6 +113,8 @@
{% endif %} {% endif %}
{% endassets %} {% endassets %}
{% endfor %} {% endfor %}
{% block js %}{% endblock %} {% block js %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -1,110 +1,147 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block js %} {% block js %}
<script> <script>
$(function(){ var Commit = {};
Commit.info = {{ info|tojson }};
</script>
<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
{% if partials %} {% if partials %}
<script>
$(function() {
{% for name, value in partials.items() %} {% for name, value in partials.items() %}
{% if name and value %} {% if name and value %}
try { try {
Handlebars.registerPartial({{ name|tojson|safe }}, {{ value.data|tojson|safe }}); Handlebars.registerPartial({{ name|tojson|safe }}, {{ value.data|tojson|safe }});
} catch(e) { } catch (e) {
// no data? // no data?
} }
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %}
$("#start-togetherjs").click(function(){
$(this).prop('disabled', true).html("Loading");
}); });
}); </script>
TogetherJSConfig_toolName = "Collaboration"; {% endif %}
TogetherJSConfig_suppressJoinConfirmation = true;
{% if current_user.is_authenticated() %} {% if config.get('COLLABORATION') %}
TogetherJSConfig_getUserName = function () { <script src="{{ url_for('static', filename='js/collaboration/main.js') }}"></script>
return {{ current_user.username|tojson }}; {% endif %}
};
{% if config.get('COLLABORATION') == 'firepad' %}
TogetherJSConfig_getUserAvatar = function () { <script>
return {{ current_user.avatar|tojson }}; Config['FIREBASE_HOSTNAME'] = {{ config.get('FIREBASE_HOSTNAME')|tojson }};
}; </script>
<script src="https://cdn.firebase.com/js/client/1.0.17/firebase.js"></script>
<script src="https://cdn.firebase.com/libs/firepad/1.0.0/firepad.min.js"></script>
<script src="{{ url_for('static', filename='js/collaboration/firepad.js') }}"></script>
{% endif %}
{% if config.get('COLLABORATION') == 'togetherjs' %}
<script src="{{ url_for('static', filename='js/collaboration/togetherjs.js') }}"></script>
<script src="https://togetherjs.com/togetherjs-min.js"></script>
{% endif %} {% endif %}
TogetherJSConfig_on_ready = function () {
MDR.sanitize = true;
$("#preview").html('');
$("#start-togetherjs").addClass('btn-danger').html('End Collaboration').prop('disabled', false);
};
TogetherJSConfig_on_close = function () {
MDR.sanitize = false;
$("#start-togetherjs").removeClass('btn-danger').html('Collaborate').prop('disabled', false);
};
</script>
<script src="https://togetherjs.com/togetherjs-min.js"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div id="app-wrap"> <div id="app-wrap">
<div id="app-controls" class="row"> <div id="app-controls" class="row">
<div class="col-xs-4 col-md-3"> <div class="col-xs-4 col-md-3">
<input id="page-name" type="text" class="form-control input-sm" name="name" placeholder="Name" value="{{- name -}}" /> <input id="page-name" type="text" class="form-control input-sm" name="name"
placeholder="Name" value="{{- name -}}" />
</div> </div>
<div class="col-xs-4 col-md-3"> <div class="col-xs-4 col-md-3">
<input id="page-message" type="text" class="form-control input-sm" name="page-message" placeholder="Comment" value="" /> <input id="page-message" type="text" class="form-control input-sm" name="page-message"
placeholder="Comment" value="" />
</div> </div>
<div class="col-md-6 col-xs-4"> <div class="col-md-6 col-xs-4 text-right">
<div class="pull-right">
<button class="btn btn-default btn-sm" id="start-togetherjs" type="button" {% if config.get('COLLABORATION') %}
onclick="TogetherJS(this); return false"> <div class="btn-group">
<button style='display:none' class="btn btn-danger btn-sm" id="end-collaboration">
<i class="fa fa-comments-o"></i>
<span class="hidden-xs">End Collaboration</span>
</button>
</div>
<div class="btn-group">
<button style='display:none' class="btn btn-default btn-sm" id="loading-collaboration" type="button">
<i class="fa fa-cog fa-spin"></i>
<span class="hidden-xs">Loading</span>
</button>
</div>
{% endif %}
<i class="fa fa-comments-o visible-xs"></i> <div class="dropdown btn-group">
<span class="hidden-xs">Collaborate</span> <button class="btn btn-default btn-sm dropdown-toggle" type="button" id="editor-actions"
data-toggle="dropdown" title="Actions">
<i class="fa fa-cog"></i>
<span class="hidden-xs">Actions <i class="fa fa-caret-down"></i></span>
</button> </button>
<a href="#" id="drop6" role="button" class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown"> <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="editor-actions">
<i class="fa fa-paint-brush visible-xs"></i> <li role="presentation">
<span class="hidden-xs">Theme <i class="fa fa-caret-down"></i></span> <a role="menuitem" tabindex="-1" href="#" id="start-collaboration">Collaborate</a>
</a> </li>
<ul id="theme-list" class="dropdown-menu" role="menu" aria-labelledby="drop6"> <li role="presentation" class="divider"></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/chrome" >Chrome</a></li> <li role="presentation">
<li><a tabindex="-1" href="#" data-value="ace/theme/clouds" >Clouds</a></li> <a role="menuitem" tabindex="-1" href="#" id="discard-draft-btn">Delete Draft</a>
<li><a tabindex="-1" href="#" data-value="ace/theme/clouds_midnight" >Clouds Midnight</a></li> </li>
<li><a tabindex="-1" href="#" data-value="ace/theme/cobalt" >Cobalt</a></li> <li role="presentation">
<li><a tabindex="-1" href="#" data-value="ace/theme/crimson_editor" >Crimson Editor</a></li> <a role="menuitem" tabindex="-1" href="#" id="delete-page-btn">Delete Page</a>
<li><a tabindex="-1" href="#" data-value="ace/theme/dawn" class="selected">Dawn</a></li> </li>
<li><a tabindex="-1" href="#" data-value="ace/theme/dreamweaver" >Dreamweaver</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/eclipse" >Eclipse</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/idle_fingers" >idleFingers</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/kr_theme" >krTheme</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/merbivore" >Merbivore</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/merbivore_soft" >Merbivore Soft</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/mono_industrial" >Mono Industrial</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/monokai" >Monokai</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/pastel_on_dark">Pastel on Dark</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/solarized_dark" >Solarized Dark</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/solarized_light" >Solarized Light</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/textmate" >TextMate</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow" >Tomorrow</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night">Tomorrow Night</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_blue" >Tomorrow Night Blue</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_bright" >Tomorrow Night Bright</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_eighties" >Tomorrow Night 80s</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/twilight" >Twilight</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/vibrant_ink" >Vibrant Ink</a></li>
</ul> </ul>
{% if name in config.LOCKED %} </div>
<div class="dropdown btn-group">
<button id="theme-list-btn" type="button" class="dropdown-toggle btn btn-default btn-sm"
data-toggle="dropdown" title="Change Theme">
<i class="fa fa-paint-brush"></i>
<span class="hidden-xs">Theme <i class="fa fa-caret-down"></i></span>
</button>
<ul id="theme-list" class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="theme-list">
<li><a tabindex="-1" href="#" data-value="chrome" >Chrome</a></li>
<li><a tabindex="-1" href="#" data-value="clouds" >Clouds</a></li>
<li><a tabindex="-1" href="#" data-value="clouds_midnight" >Clouds Midnight</a></li>
<li><a tabindex="-1" href="#" data-value="cobalt" >Cobalt</a></li>
<li><a tabindex="-1" href="#" data-value="crimson_editor" >Crimson Editor</a></li>
<li><a tabindex="-1" href="#" data-value="dawn" class="selected">Dawn</a></li>
<li><a tabindex="-1" href="#" data-value="dreamweaver" >Dreamweaver</a></li>
<li><a tabindex="-1" href="#" data-value="eclipse" >Eclipse</a></li>
<li><a tabindex="-1" href="#" data-value="idle_fingers" >idleFingers</a></li>
<li><a tabindex="-1" href="#" data-value="kr_theme" >krTheme</a></li>
<li><a tabindex="-1" href="#" data-value="merbivore" >Merbivore</a></li>
<li><a tabindex="-1" href="#" data-value="merbivore_soft" >Merbivore Soft</a></li>
<li><a tabindex="-1" href="#" data-value="mono_industrial" >Mono Industrial</a></li>
<li><a tabindex="-1" href="#" data-value="monokai" >Monokai</a></li>
<li><a tabindex="-1" href="#" data-value="pastel_on_dark">Pastel on Dark</a></li>
<li><a tabindex="-1" href="#" data-value="solarized_dark" >Solarized Dark</a></li>
<li><a tabindex="-1" href="#" data-value="solarized_light" >Solarized Light</a></li>
<li><a tabindex="-1" href="#" data-value="textmate" >TextMate</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow" >Tomorrow</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night">Tomorrow Night</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night_blue" >Tomorrow Night Blue</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night_bright" >Tomorrow Night Bright</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night_eighties" >Tomorrow Night 80s</a></li>
<li><a tabindex="-1" href="#" data-value="twilight" >Twilight</a></li>
<li><a tabindex="-1" href="#" data-value="vibrant_ink" >Vibrant Ink</a></li>
</ul>
</div>
<div class="btn-group">
{% if name in config['LOCKED'] %}
<a class="btn btn-danger btn-sm"> <a class="btn btn-danger btn-sm">
<i class="fa fa-lock"></i> <i class="fa fa-lock"></i>
<span class="hidden-xs">Locked</span> <span class="hidden-xs">Locked</span>
</a> </a>
{% else %} {% else %}
<a id="save-native" class="btn btn-primary btn-sm"> <a id="submit-btn" class="btn btn-primary btn-sm">
<i class="fa fa-save"></i> <i class="fa fa-save"></i>
<span class="hidden-xs">Save</span> <span class="hidden-xs">Publish</span>
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -114,7 +151,8 @@
<a class="markdown-help" href=""><span class="hidden">What is Markdown?</span></a> <a class="markdown-help" href=""><span class="hidden">What is Markdown?</span></a>
</header> </header>
<section id="entry-markdown-content" class="entry-markdown-content"> <section id="entry-markdown-content" class="entry-markdown-content">
<div id="editor" class="ace-editor">{{ content }}</div> <div id="editor-{{ name }}" data-submitbtn='submit-btn' data-themeselect="theme-list" data-mode="markdown"
data-preview="preview" class="editor">{{ content }}</div>
</section> </section>
</section> </section>
@ -127,8 +165,6 @@
</section> </section>
</section> </section>
<input id="sha" type="hidden" name="sha" value="{{ sha }}" />
</div> </div>
{% endblock %} {% endblock %}

View file

@ -8,7 +8,8 @@ with open('README.md') as f:
with open('requirements.txt') as f: with open('requirements.txt') as f:
required = f.read().splitlines() required = f.read().splitlines()
VERSION = '0.2.2' with open('VERSION') as f:
VERSION = f.read().strip()
CLASSIFIERS = [ CLASSIFIERS = [
'Intended Audience :: Developers', 'Intended Audience :: Developers',